001package gudusoft.gsqlparser.dlineage.dataflow.model; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.IdentityHashMap; 006import java.util.List; 007import java.util.Map; 008import java.util.Stack; 009import java.util.concurrent.atomic.AtomicInteger; 010 011import gudusoft.gsqlparser.TCustomSqlStatement; 012import gudusoft.gsqlparser.nodes.TResultColumn; 013import gudusoft.gsqlparser.dlineage.util.DlineageUtil; 014import gudusoft.gsqlparser.nodes.TObjectName; 015 016/** 017 * What one dlineage analysis observed while it bound names, recorded for the SQL-LR producer 018 * ({@code gudusoft.gsqlparser.lineage.v2.producer}, plan {@code docs/designs/data-lineage/06-sqllr-m2-producer-plan-zh.md}). 019 * 020 * <p>Internal API: the producer creates a recorder, gives it to the analyzer it runs, and reads it on the analysis 021 * thread before {@code dispose()}. The hooks only record: they never change the model, allocate a model id or call 022 * a resolver, so legacy output is byte-identical whether a recorder is present or not. With no recorder bound, 023 * every hook is one volatile read.</p> 024 * 025 * <p>A recorder made with {@code producerMode = true} also switches the analysis into SQL-LR production: parsers 026 * never guess between candidates, every input is its own unit with its text unchanged, and a failed top-level 027 * statement is isolated instead of ending the analysis. Only the producer makes such a recorder; an observe-only 028 * recorder ({@link #SqllrObservations()}) changes nothing.</p> 029 * 030 * <p>Not thread-safe: one recorder belongs to one analysis on one thread.</p> 031 * 032 * @since 4.2.17 033 */ 034public final class SqllrObservations { 035 036 /** Live recorders in this JVM; the hooks return immediately while it is zero. */ 037 private static final AtomicInteger ACTIVE = new AtomicInteger(); 038 039 /** One relationship element added as a source or set as a target, with the reference it was added under. */ 040 public static final class ElementRecord { 041 private final AbstractRelationship relation; 042 private final RelationshipElement<?> element; 043 private final boolean target; 044 private final TObjectName reference; 045 private final TCustomSqlStatement statement; 046 private final TCustomSqlStatement topStatement; 047 private final long sequence; 048 049 ElementRecord(AbstractRelationship relation, RelationshipElement<?> element, boolean target, 050 TObjectName reference, TCustomSqlStatement statement, TCustomSqlStatement topStatement, long sequence) { 051 this.relation = relation; 052 this.element = element; 053 this.target = target; 054 this.reference = reference; 055 this.statement = statement; 056 this.topStatement = topStatement; 057 this.sequence = sequence; 058 } 059 060 public AbstractRelationship getRelation() { 061 return relation; 062 } 063 064 public RelationshipElement<?> getElement() { 065 return element; 066 } 067 068 public boolean isTarget() { 069 return target; 070 } 071 072 /** The name reference the analysis was processing when it added the element, or {@code null}. */ 073 public TObjectName getReference() { 074 return reference; 075 } 076 077 /** The innermost statement being analysed ({@code stmtStack.peek()}). */ 078 public TCustomSqlStatement getStatement() { 079 return statement; 080 } 081 082 /** The top-level statement of {@link #getStatement()}. */ 083 public TCustomSqlStatement getTopStatement() { 084 return topStatement; 085 } 086 087 /** Recording order. */ 088 public long getSequence() { 089 return sequence; 090 } 091 } 092 093 /** 094 * One name reference the analysis processed for a target, whether or not it added a source: a reference that 095 * bound nothing (an ambiguous or unknown name) is still an attempted read. 096 */ 097 public static final class ReferenceRecord { 098 private final TObjectName reference; 099 private final Object target; 100 private final TCustomSqlStatement statement; 101 private final TCustomSqlStatement topStatement; 102 private final long sequence; 103 104 ReferenceRecord(TObjectName reference, Object target, TCustomSqlStatement statement, 105 TCustomSqlStatement topStatement, long sequence) { 106 this.reference = reference; 107 this.target = target; 108 this.statement = statement; 109 this.topStatement = topStatement; 110 this.sequence = sequence; 111 } 112 113 public TObjectName getReference() { 114 return reference; 115 } 116 117 /** The model object the reference was read for (the frame's target), or {@code null}. */ 118 public Object getTarget() { 119 return target; 120 } 121 122 public TCustomSqlStatement getStatement() { 123 return statement; 124 } 125 126 public TCustomSqlStatement getTopStatement() { 127 return topStatement; 128 } 129 130 public long getSequence() { 131 return sequence; 132 } 133 } 134 135 /** One analysed top-level statement and how its analysis ended. */ 136 public static final class StatementRecord { 137 private final TCustomSqlStatement statement; 138 private final int unitIndex; 139 private final boolean primary; 140 private Throwable failure; 141 private boolean finished; 142 143 StatementRecord(TCustomSqlStatement statement, int unitIndex, boolean primary) { 144 this.statement = statement; 145 this.unitIndex = unitIndex; 146 this.primary = primary; 147 } 148 149 /** 150 * Whether the statement is one of its unit's parsed top-level statements; false for a statement the analysis 151 * derived (dynamic SQL text) and analysed as a top-level statement of its own. 152 */ 153 public boolean isPrimary() { 154 return primary; 155 } 156 157 public TCustomSqlStatement getStatement() { 158 return statement; 159 } 160 161 /** The index of the input (the analyzer's {@code SqlInfo[]}) the statement was parsed from, or -1. */ 162 public int getUnitIndex() { 163 return unitIndex; 164 } 165 166 /** The exception that ended the statement's analysis, or {@code null}. */ 167 public Throwable getFailure() { 168 return failure; 169 } 170 171 public boolean isFinished() { 172 return finished; 173 } 174 } 175 176 /** 177 * A relationship as it stood when the analysis finished, before output materialization changed the model. Only 178 * the relationship's target and sources are copied; the model objects they hold stay live, so a reader takes a 179 * result set's columns from {@link #getFrozenColumns} and never from the live model. 180 */ 181 public static final class FrozenRelation { 182 private final AbstractRelationship relation; 183 private final RelationshipElement<?> target; 184 private final List<RelationshipElement<?>> sources; 185 186 FrozenRelation(AbstractRelationship relation) { 187 this.relation = relation; 188 this.target = relation.getTarget(); 189 this.sources = Collections.unmodifiableList(new ArrayList<RelationshipElement<?>>(relation.getSources())); 190 } 191 192 public AbstractRelationship getRelation() { 193 return relation; 194 } 195 196 public RelationshipElement<?> getTarget() { 197 return target; 198 } 199 200 public List<RelationshipElement<?>> getSources() { 201 return sources; 202 } 203 } 204 205 /** One reference frame: the target its loop reads for and the reference of the current iteration. */ 206 private static final class Frame { 207 final Object target; 208 TObjectName reference; 209 210 Frame(Object target) { 211 this.target = target; 212 } 213 } 214 215 private final boolean producerMode; 216 private final List<Frame> frames = new ArrayList<Frame>(); 217 private final List<ElementRecord> elements = new ArrayList<ElementRecord>(); 218 private final List<ReferenceRecord> references = new ArrayList<ReferenceRecord>(); 219 private Map<ResultSet, List<ResultColumn>> frozenColumns; 220 private List<ResultSet> frozenResultSets; 221 private final Map<TCustomSqlStatement, StatementRecord> statements = 222 new IdentityHashMap<TCustomSqlStatement, StatementRecord>(); 223 private final List<StatementRecord> statementOrder = new ArrayList<StatementRecord>(); 224 private final Map<String, Integer> unitIndexes = new java.util.HashMap<String, Integer>(); 225 private int currentUnit = -1; 226 private List<FrozenRelation> frozen; 227 private boolean closed; 228 private long sequence; 229 230 /** 231 * Builds the value snapshot (plan 09, Section 3.1) alongside the live records. A failure while building it never 232 * reaches the analysis: the snapshot is dropped and the failure kept ({@link #getSnapshotFailure()}). 233 */ 234 private SnapshotRecorder snapshotRecorder = new SnapshotRecorder(); 235 private SqllrSnapshot snapshot; 236 private String snapshotFailure; 237 private long snapshotNanos = -1; 238 239 private void snapshotFailed(String where, Throwable e) { 240 if (snapshotFailure == null) { 241 snapshotFailure = where + ": " + e; 242 } 243 snapshotRecorder = null; 244 } 245 246 /** An observe-only recorder: the analysis behaves exactly as without it. */ 247 public SqllrObservations() { 248 this(false); 249 } 250 251 /** 252 * @param producerMode true only for the SQL-LR producer's own analysis, which this switches into production 253 * (see the class comment) 254 */ 255 public SqllrObservations(boolean producerMode) { 256 this.producerMode = producerMode; 257 ACTIVE.incrementAndGet(); 258 } 259 260 /** Whether the bound analysis runs as SQL-LR production rather than only being observed. */ 261 public boolean isProducerMode() { 262 return producerMode; 263 } 264 265 /** Stops recording; the hooks of other analyses no longer pay for this recorder. Idempotent. */ 266 public void close() { 267 if (!closed) { 268 closed = true; 269 ACTIVE.decrementAndGet(); 270 } 271 } 272 273 /** The recorder of the analysis bound to the current thread, or {@code null}. */ 274 public static SqllrObservations current() { 275 if (ACTIVE.get() == 0) { 276 return null; 277 } 278 ModelBindingManager manager = ModelBindingManager.get(); 279 SqllrObservations observations = manager == null ? null : manager.getSqllrObservations(); 280 return observations == null || observations.closed ? null : observations; 281 } 282 283 // --------------------------------------------------------------------------------------------- 284 // Reference frames: the analysis opens a frame around a loop over name references and names the reference of 285 // each iteration. A nested analysis opens a barrier (a frame with no reference) over its whole body, so neither 286 // its own elements nor those it adds outside its loops inherit the reference of the loop that called it. 287 // --------------------------------------------------------------------------------------------- 288 289 /** Opens a barrier: elements added until it closes carry no reference unless an inner frame names one. */ 290 public int openFrame() { 291 return openFrame(null); 292 } 293 294 /** 295 * Opens a frame for a loop that reads references for {@code target}; pass the result to {@link #reference} and 296 * {@link #closeFrame}. 297 */ 298 public int openFrame(Object target) { 299 frames.add(new Frame(target)); 300 return frames.size() - 1; 301 } 302 303 /** The reference the frame is processing from now on; recorded as an attempted read of the frame's target. */ 304 public void reference(int frame, TObjectName name) { 305 if (frame >= 0 && frame < frames.size()) { 306 Frame f = frames.get(frame); 307 f.reference = name; 308 if (name != null && frozen == null) { 309 TCustomSqlStatement statement = currentStatement(); 310 TCustomSqlStatement top = statement == null ? null : DlineageUtil.getTopStmt(statement); 311 long at = sequence++; 312 references.add(new ReferenceRecord(name, f.target, statement, top, at)); 313 if (snapshotRecorder != null) { 314 try { 315 snapshotRecorder.referenceEvent(at, name, f.target, statement, top); 316 } catch (RuntimeException e) { 317 snapshotFailed("reference", e); 318 } 319 } 320 } 321 } 322 } 323 324 /** Closes the frame and any frame a failed nested analysis left open above it. */ 325 public void closeFrame(int frame) { 326 while (frame >= 0 && frames.size() > frame) { 327 frames.remove(frames.size() - 1); 328 } 329 } 330 331 private TObjectName currentReference() { 332 return frames.isEmpty() ? null : frames.get(frames.size() - 1).reference; 333 } 334 335 private static TCustomSqlStatement currentStatement() { 336 Stack<TCustomSqlStatement> stack = ModelBindingManager.getGlobalStmtStack(); 337 return stack == null || stack.isEmpty() ? null : stack.peek(); 338 } 339 340 // --------------------------------------------------------------------------------------------- 341 // Hooks 342 // --------------------------------------------------------------------------------------------- 343 344 /** Called by {@link AbstractRelationship#addSource} before its de-duplication, so {@code a + a} is seen twice. */ 345 void onSource(AbstractRelationship relation, RelationshipElement<?> element) { 346 record(relation, element, false); 347 } 348 349 /** Called by {@link AbstractRelationship#setTarget}. */ 350 void onTarget(AbstractRelationship relation, RelationshipElement<?> element) { 351 record(relation, element, true); 352 } 353 354 private void record(AbstractRelationship relation, RelationshipElement<?> element, boolean target) { 355 if (element == null || frozen != null) { 356 return; 357 } 358 TCustomSqlStatement statement = currentStatement(); 359 TCustomSqlStatement top = statement == null ? null : DlineageUtil.getTopStmt(statement); 360 if (injectDuringPrefix != null && top != null && top.toString().trim().startsWith(injectDuringPrefix)) { 361 injectDuringPrefix = null; 362 if (injectError) { 363 throw new AssertionError("injected error"); 364 } 365 throw new IllegalStateException("injected failure during the analysis"); 366 } 367 long at = sequence++; 368 elements.add(new ElementRecord(relation, element, target, currentReference(), statement, top, at)); 369 if (snapshotRecorder != null) { 370 try { 371 snapshotRecorder.elementEvent(at, relation, element, target, currentReference(), statement, top); 372 } catch (RuntimeException e) { 373 snapshotFailed("element", e); 374 } 375 } 376 } 377 378 // --------------------------------------------------------------------------------------------- 379 // Statement entries and fallback decisions (observe-only; snapshot only) 380 // --------------------------------------------------------------------------------------------- 381 382 /** The analysis enters a statement: every statement it analyses, nested and dynamic ones included. */ 383 public void enterStatement(TCustomSqlStatement statement) { 384 if (snapshotRecorder == null || frozen != null) { 385 return; 386 } 387 try { 388 snapshotRecorder.enter(statement); 389 } catch (RuntimeException e) { 390 snapshotFailed("enter", e); 391 } 392 } 393 394 /** An orphan column linked to the first FROM item ({@code Option.linkOrphanColumnToFirstTable}). */ 395 public static final String DECISION_ORPHAN_FIRST_TABLE = "ORPHAN_FIRST_TABLE"; 396 /** An orphan column with a qualified prefix linked to a pseudo table named after the prefix. */ 397 public static final String DECISION_ORPHAN_PSEUDO_TABLE = "ORPHAN_PSEUDO_TABLE"; 398 /** An ARRAY_AGG ORDER BY key linked to the first FROM item, written back to the parse tree. */ 399 public static final String DECISION_ARRAY_AGG_ORDER_BY_FIRST_TABLE = "ARRAY_AGG_ORDER_BY_FIRST_TABLE"; 400 /** The array an UNNEST reads taken from the first FROM item. */ 401 public static final String DECISION_UNNEST_FIRST_TABLE = "UNNEST_FIRST_TABLE"; 402 /** A column bound to the first FROM item that lists a column of its name ({@code ModelBindingManager.getTable}). */ 403 public static final String DECISION_FIRST_TABLE_WITH_COLUMN = "FIRST_TABLE_WITH_COLUMN"; 404 /** A column bound to any table the run has seen with a column of its name ({@code guessTable}). */ 405 public static final String DECISION_GUESS_TABLE = "GUESS_TABLE"; 406 407 /** 408 * The analysis decided a binding by a fallback (plan 06, Section 3.3): called before it acts on the decision, so 409 * the reference is recorded as it stood. {@code selected} is the parse tree node or model object chosen. 410 */ 411 public void decision(String id, TObjectName reference, Object selected, String reason) { 412 if (snapshotRecorder == null || frozen != null) { 413 return; 414 } 415 try { 416 snapshotRecorder.decision(sequence++, id, reference, selected, reason, currentStatement()); 417 } catch (RuntimeException e) { 418 snapshotFailed("decision", e); 419 } 420 } 421 422 /** The analyzer keyed input {@code index} as {@code unitKey}. */ 423 public void registerUnit(String unitKey, int index) { 424 unitIndexes.put(unitKey, Integer.valueOf(index)); 425 } 426 427 /** The analyzer starts the statements of the unit with this key. */ 428 public void beginUnit(String unitKey) { 429 Integer index = unitIndexes.get(unitKey); 430 currentUnit = index == null ? -1 : index.intValue(); 431 } 432 433 /** One parsed unit: its top-level statements and syntax errors, copied when it was parsed. */ 434 public static final class UnitRecord { 435 private final int unitIndex; 436 private final int parseResult; 437 private final List<TCustomSqlStatement> statements; 438 private final List<gudusoft.gsqlparser.TSyntaxError> syntaxErrors; 439 private final List<gudusoft.gsqlparser.TSyntaxError> recoveredSyntaxErrors; 440 private final String parsedText; 441 442 UnitRecord(int unitIndex, gudusoft.gsqlparser.TGSqlParser parser, int parseResult) { 443 this.unitIndex = unitIndex; 444 this.parseResult = parseResult; 445 this.parsedText = parser.sqltext; 446 List<TCustomSqlStatement> list = new ArrayList<TCustomSqlStatement>(); 447 if (parser.sqlstatements != null) { 448 for (int i = 0; i < parser.sqlstatements.size(); i++) { 449 list.add(parser.sqlstatements.get(i)); 450 } 451 } 452 this.statements = Collections.unmodifiableList(list); 453 List<gudusoft.gsqlparser.TSyntaxError> errors = new ArrayList<gudusoft.gsqlparser.TSyntaxError>(); 454 if (parser.getSyntaxErrors() != null) { 455 errors.addAll(parser.getSyntaxErrors()); 456 } 457 this.syntaxErrors = Collections.unmodifiableList(errors); 458 List<gudusoft.gsqlparser.TSyntaxError> recovered = new ArrayList<gudusoft.gsqlparser.TSyntaxError>(); 459 if (parser.getRecoveredSyntaxErrors() != null) { 460 recovered.addAll(parser.getRecoveredSyntaxErrors()); 461 } 462 this.recoveredSyntaxErrors = Collections.unmodifiableList(recovered); 463 } 464 465 public int getUnitIndex() { 466 return unitIndex; 467 } 468 469 /** The result of {@code TGSqlParser.parse()} for the unit: 0 when it parsed without error. */ 470 public int getParseResult() { 471 return parseResult; 472 } 473 474 /** The top-level statements of the unit, in text order; valid until the analyzer is disposed. */ 475 public List<TCustomSqlStatement> getStatements() { 476 return statements; 477 } 478 479 public List<gudusoft.gsqlparser.TSyntaxError> getSyntaxErrors() { 480 return syntaxErrors; 481 } 482 483 /** The text the parser read for the unit. */ 484 public String getParsedText() { 485 return parsedText; 486 } 487 488 /** Errors of statements routine-body recovery skipped: the parse "succeeded" without them. */ 489 public List<gudusoft.gsqlparser.TSyntaxError> getRecoveredSyntaxErrors() { 490 return recoveredSyntaxErrors; 491 } 492 } 493 494 private final List<UnitRecord> units = new ArrayList<UnitRecord>(); 495 496 /** The analyzer parsed the unit it announced with {@link #beginUnit}. */ 497 public void parsedUnit(gudusoft.gsqlparser.TGSqlParser parser, int parseResult) { 498 // The analysis parses every input with one parser, whose token list the next parse clears and refills, so the 499 // tokens of an earlier input would find the next input's tokens at their positions. Right after its parse, 500 // before it is analysed or recorded, an input's tokens get a list of their own, in their order, so that 501 // whatever reads its trees later (SQL-LR spans and keyword scans, the snapshot recorder) finds its own 502 // neighbours. Producer mode only: an observe-only recorder leaves the analysis exactly as without it (Codex 503 // 13a #6). 504 gudusoft.gsqlparser.TSourceTokenList shared = parser == null || !producerMode ? null : parser.sourcetokenlist; 505 if (shared != null) { 506 gudusoft.gsqlparser.TSourceTokenList own = new gudusoft.gsqlparser.TSourceTokenList(); 507 own.setGsqlparser(parser); 508 for (int i = 0; i < shared.size(); i++) { 509 own.add(shared.get(i)); 510 } 511 for (int i = 0; i < own.size(); i++) { 512 if (own.get(i).container == shared) { 513 own.get(i).container = own; 514 } 515 } 516 } 517 units.add(new UnitRecord(currentUnit, parser, parseResult)); 518 } 519 520 /** The parsed units, in analysis order. */ 521 public List<UnitRecord> getUnits() { 522 return Collections.unmodifiableList(units); 523 } 524 525 /** A statement of dynamic SQL text the analysis parsed, with the statement whose analysis parsed it (its site). */ 526 public static final class DerivedRecord { 527 private final TCustomSqlStatement statement; 528 private final TCustomSqlStatement site; 529 530 DerivedRecord(TCustomSqlStatement statement, TCustomSqlStatement site) { 531 this.statement = statement; 532 this.site = site; 533 } 534 535 public TCustomSqlStatement getStatement() { 536 return statement; 537 } 538 539 public TCustomSqlStatement getSite() { 540 return site; 541 } 542 } 543 544 private final Map<Object, TResultColumn> structItems = new java.util.IdentityHashMap<Object, TResultColumn>(); 545 private final List<Object> structOrder = new ArrayList<Object>(); 546 547 /** A column the analysis built for a field of a STRUCT that a select item constructs. Observe-only. */ 548 public void structField(Object column, TResultColumn item) { 549 if (structItems.put(column, item) == null) { 550 structOrder.add(column); 551 } 552 if (snapshotRecorder != null) { 553 try { 554 snapshotRecorder.model(column); 555 } catch (RuntimeException e) { 556 snapshotFailed("struct field", e); 557 } 558 } 559 } 560 561 /** The select item whose STRUCT a field column belongs to, or null. */ 562 public TResultColumn getStructItem(Object column) { 563 return structItems.get(column); 564 } 565 566 /** The field columns {@link #structField} recorded, in the order it first recorded them. */ 567 public List<Object> getStructFieldColumns() { 568 return Collections.unmodifiableList(structOrder); 569 } 570 571 private final List<DerivedRecord> derived = new ArrayList<DerivedRecord>(); 572 573 /** A statement of dynamic SQL text is analysed, inside the analysis of {@code site}. Observe-only. */ 574 public void derivedStatement(TCustomSqlStatement statement, TCustomSqlStatement site) { 575 if (statement != null) { 576 derived.add(new DerivedRecord(statement, site)); 577 if (snapshotRecorder != null) { 578 snapshotRecorder.handle(statement); 579 snapshotRecorder.handle(site); 580 } 581 } 582 } 583 584 /** The statements of dynamic SQL texts, in analysis order. */ 585 public List<DerivedRecord> getDerived() { 586 return Collections.unmodifiableList(derived); 587 } 588 589 /** A top-level statement's analysis starts. */ 590 public void beginStatement(TCustomSqlStatement statement) { 591 if (statement == null || statements.containsKey(statement)) { 592 return; 593 } 594 boolean primary = false; 595 if (!units.isEmpty()) { 596 for (TCustomSqlStatement s : units.get(units.size() - 1).getStatements()) { 597 if (s == statement) { 598 primary = true; 599 break; 600 } 601 } 602 } 603 StatementRecord record = new StatementRecord(statement, currentUnit, primary); 604 statements.put(statement, record); 605 statementOrder.add(record); 606 if (snapshotRecorder != null) { 607 try { 608 snapshotRecorder.statement(statement, null); 609 } catch (RuntimeException e) { 610 snapshotFailed("statement", e); 611 } 612 } 613 checkInjectedFailure(statement, true); 614 } 615 616 private String injectPrefix; 617 private boolean injectBefore; 618 private String injectDuringPrefix; 619 private boolean injectError; 620 621 /** 622 * Tests only: make the analysis of the first top-level statement whose text starts with {@code prefix} fail, 623 * before its analysis ({@code before}) or after it changed the model. Exercises the failure isolation of SQL-LR 624 * production. 625 */ 626 public void injectFailure(String prefix, boolean before) { 627 this.injectPrefix = prefix; 628 this.injectBefore = before; 629 } 630 631 /** 632 * Tests only: make the analysis of the first top-level statement whose text starts with {@code prefix} fail inside 633 * the analysis, when it records its first relationship element; with {@code error} the failure is a 634 * {@code java.lang.Error}. 635 */ 636 public void injectFailureDuring(String prefix, boolean error) { 637 this.injectDuringPrefix = prefix; 638 this.injectError = error; 639 } 640 641 /** Called by the analyzer: throws the injected failure for this statement and point, if any. */ 642 public void checkInjectedFailure(TCustomSqlStatement statement, boolean before) { 643 if (injectPrefix != null && before == injectBefore && statement.toString().trim().startsWith(injectPrefix)) { 644 injectPrefix = null; 645 throw new IllegalStateException("injected failure"); 646 } 647 } 648 649 /** A top-level statement's analysis ended, normally ({@code failure == null}) or with an exception. */ 650 public void endStatement(TCustomSqlStatement statement, Throwable failure) { 651 StatementRecord record = statements.get(statement); 652 if (record != null) { 653 record.finished = true; 654 if (failure != null && record.failure == null) { 655 record.failure = failure; 656 } 657 } 658 } 659 660 /** 661 * Freezes the runtime graph when the analysis is complete and before output materialization changes it 662 * ({@code SELECT *} links, inferred sources). Later hook calls are ignored. 663 */ 664 public void freeze(ModelBindingManager manager) { 665 if (frozen != null) { 666 return; 667 } 668 List<FrozenRelation> list = new ArrayList<FrozenRelation>(); 669 Map<ResultSet, List<ResultColumn>> columns = new IdentityHashMap<ResultSet, List<ResultColumn>>(); 670 List<ResultSet> resultSets = new ArrayList<ResultSet>(); 671 for (Object rs : manager.getResultSets()) { 672 if (rs instanceof ResultSet && !columns.containsKey(rs)) { 673 columns.put((ResultSet) rs, Collections.unmodifiableList( 674 new ArrayList<ResultColumn>(((ResultSet) rs).getColumns()))); 675 resultSets.add((ResultSet) rs); 676 } 677 } 678 for (Relationship relation : manager.getRelations()) { 679 if (relation instanceof AbstractRelationship) { 680 FrozenRelation f = new FrozenRelation((AbstractRelationship) relation); 681 list.add(f); 682 freezeColumns(f.getTarget(), columns, resultSets); 683 for (RelationshipElement<?> source : f.getSources()) { 684 freezeColumns(source, columns, resultSets); 685 } 686 } 687 } 688 frozen = Collections.unmodifiableList(list); 689 frozenColumns = columns; 690 frozenResultSets = Collections.unmodifiableList(resultSets); 691 if (snapshotRecorder != null) { 692 long started = System.nanoTime(); 693 try { 694 snapshot = snapshotRecorder.freeze(manager, frozen, frozenColumns, statementOrder, units, derived, 695 structItems); 696 snapshotNanos = System.nanoTime() - started; 697 } catch (RuntimeException e) { 698 snapshotFailed("freeze", e); 699 } 700 snapshotRecorder = null; // the snapshot holds values only; the live objects it read are released 701 } 702 if (freezeHook != null) { 703 try { 704 freezeHook.frozen(this); 705 } catch (RuntimeException | StackOverflowError e) { 706 freezeHookFailure = e; 707 } 708 } 709 } 710 711 /** 712 * The value snapshot of the analysis (plan 09, Section 3.1), taken at {@link #freeze}; {@code null} before the 713 * freeze or when building it failed ({@link #getSnapshotFailure()}). It holds no live object and stays readable 714 * after the analyzer is disposed, from any thread. 715 */ 716 public SqllrSnapshot getSnapshot() { 717 return snapshot; 718 } 719 720 /** Why the snapshot could not be built, or {@code null}. */ 721 public String getSnapshotFailure() { 722 return snapshotFailure; 723 } 724 725 /** Called at the end of {@link #freeze}: the SQL-LR producer projects what it reads there (plan 09, slice 1b). */ 726 public interface FreezeHook { 727 void frozen(SqllrObservations observations); 728 } 729 730 private FreezeHook freezeHook; 731 private Throwable freezeHookFailure; 732 733 /** The producer's hook, run once when the analysis freezes; a failure is kept, never thrown into the analysis. */ 734 public void setFreezeHook(FreezeHook hook) { 735 this.freezeHook = hook; 736 } 737 738 /** What the freeze hook threw, or {@code null}. */ 739 public Throwable getFreezeHookFailure() { 740 return freezeHookFailure; 741 } 742 743 /** How long building the snapshot's topology at the freeze took, in nanoseconds; -1 when it was not built. */ 744 public long getSnapshotNanos() { 745 return snapshotNanos; 746 } 747 748 /** Output materialization adds the columns a {@code SELECT *} expands to: the producer reads the list as it was. */ 749 private static void freezeColumns(RelationshipElement<?> element, Map<ResultSet, List<ResultColumn>> columns, 750 List<ResultSet> resultSets) { 751 Object model = element == null ? null : element.getElement(); 752 if (model instanceof ResultColumn) { 753 ResultSet rs = ((ResultColumn) model).getResultSet(); 754 if (rs != null && !columns.containsKey(rs)) { 755 columns.put(rs, Collections.unmodifiableList(new ArrayList<ResultColumn>(rs.getColumns()))); 756 resultSets.add(rs); 757 } 758 } 759 } 760 761 /** Every result set at the freeze (the model's, then any only a relationship reaches), in creation order. */ 762 public List<ResultSet> getFrozenResultSets() { 763 return frozenResultSets; 764 } 765 766 /** 767 * The columns of a result set at the freeze, or {@code null} when no frozen relationship reaches the result set. 768 */ 769 public List<ResultColumn> getFrozenColumns(ResultSet resultSet) { 770 return frozenColumns == null ? null : frozenColumns.get(resultSet); 771 } 772 773 /** Every reference the analysis read for a target, in recording order. */ 774 public List<ReferenceRecord> getReferences() { 775 return Collections.unmodifiableList(references); 776 } 777 778 public boolean isFrozen() { 779 return frozen != null; 780 } 781 782 /** The relationships at the freeze, in creation order; {@code null} before {@link #freeze}. */ 783 public List<FrozenRelation> getFrozenRelations() { 784 return frozen; 785 } 786 787 /** Every recorded element, in recording order. */ 788 public List<ElementRecord> getElements() { 789 return Collections.unmodifiableList(elements); 790 } 791 792 /** The analysed top-level statements, in the order their analysis started. */ 793 public List<StatementRecord> getStatements() { 794 return Collections.unmodifiableList(statementOrder); 795 } 796}