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}