001package gudusoft.gsqlparser.lineage.v2.producer;
002
003import java.util.ArrayDeque;
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.Collections;
007import java.util.Comparator;
008import java.util.Deque;
009import java.util.HashMap;
010import java.util.HashSet;
011import java.util.IdentityHashMap;
012import java.util.List;
013import java.util.Locale;
014import java.util.Map;
015import java.util.Set;
016
017import gudusoft.gsqlparser.EDbVendor;
018import gudusoft.gsqlparser.EExpressionType;
019import gudusoft.gsqlparser.EJoinType;
020import gudusoft.gsqlparser.ESetOperatorType;
021import gudusoft.gsqlparser.ETableSource;
022import gudusoft.gsqlparser.ETokenType;
023import gudusoft.gsqlparser.lineage.v2.shape.TDb2ReturnStmt;
024import gudusoft.gsqlparser.lineage.v2.shape.TMssqlReturn;
025import gudusoft.gsqlparser.lineage.v2.shape.TReturnStmt;
026import gudusoft.gsqlparser.lineage.v2.shape.TCreateFunctionStmt;
027import gudusoft.gsqlparser.lineage.v2.shape.TDoExecuteBlockStmt;
028import gudusoft.gsqlparser.lineage.v2.shape.TCommonBlock;
029import gudusoft.gsqlparser.lineage.v2.shape.TMssqlBlock;
030import gudusoft.gsqlparser.lineage.v2.shape.TStoredProcedureSqlStatement;
031import gudusoft.gsqlparser.lineage.v2.shape.ShapeBuilder;
032import gudusoft.gsqlparser.lineage.v2.shape.TCTE;
033import gudusoft.gsqlparser.lineage.v2.shape.TCaseExpression;
034import gudusoft.gsqlparser.lineage.v2.shape.TCreateMaterializedSqlStatement;
035import gudusoft.gsqlparser.lineage.v2.shape.TCreateTableOption;
036import gudusoft.gsqlparser.lineage.v2.shape.TCreateTableSqlStatement;
037import gudusoft.gsqlparser.lineage.v2.shape.TCreateViewSqlStatement;
038import gudusoft.gsqlparser.lineage.v2.shape.TCustomSqlStatement;
039import gudusoft.gsqlparser.lineage.v2.shape.TDeleteSqlStatement;
040import gudusoft.gsqlparser.lineage.v2.shape.TExpression;
041import gudusoft.gsqlparser.lineage.v2.shape.TExpressionList;
042import gudusoft.gsqlparser.lineage.v2.shape.TFunctionCall;
043import gudusoft.gsqlparser.lineage.v2.shape.TGroupBy;
044import gudusoft.gsqlparser.lineage.v2.shape.THierarchical;
045import gudusoft.gsqlparser.lineage.v2.shape.TInsertCondition;
046import gudusoft.gsqlparser.lineage.v2.shape.TInsertIntoValue;
047import gudusoft.gsqlparser.lineage.v2.shape.TInsertSqlStatement;
048import gudusoft.gsqlparser.lineage.v2.shape.TJoin;
049import gudusoft.gsqlparser.lineage.v2.shape.TJoinItem;
050import gudusoft.gsqlparser.lineage.v2.shape.TJoinList;
051import gudusoft.gsqlparser.lineage.v2.shape.TMergeSqlStatement;
052import gudusoft.gsqlparser.lineage.v2.shape.TMergeWhenClause;
053import gudusoft.gsqlparser.lineage.v2.shape.TMultiTarget;
054import gudusoft.gsqlparser.lineage.v2.shape.TObjectName;
055import gudusoft.gsqlparser.lineage.v2.shape.TOutputClause;
056import gudusoft.gsqlparser.lineage.v2.shape.TPTNodeList;
057import gudusoft.gsqlparser.lineage.v2.shape.TParseTreeNode;
058import gudusoft.gsqlparser.lineage.v2.shape.TParseTreeVisitor;
059import gudusoft.gsqlparser.lineage.v2.shape.TPivotClause;
060import gudusoft.gsqlparser.lineage.v2.shape.TPivotInClause;
061import gudusoft.gsqlparser.lineage.v2.shape.TReturningClause;
062import gudusoft.gsqlparser.lineage.v2.shape.TSelectSqlStatement;
063import gudusoft.gsqlparser.lineage.v2.shape.TSourceToken;
064import gudusoft.gsqlparser.lineage.v2.shape.TLateralView;
065import gudusoft.gsqlparser.lineage.v2.shape.TTable;
066import gudusoft.gsqlparser.lineage.v2.shape.TUnnestClause;
067import gudusoft.gsqlparser.lineage.v2.shape.TUpdateSqlStatement;
068import gudusoft.gsqlparser.lineage.v2.shape.TWhenClauseItem;
069import gudusoft.gsqlparser.lineage.v2.shape.TWindowDef;
070import gudusoft.gsqlparser.lineage.v2.shape.TWindowDefinition;
071import gudusoft.gsqlparser.lineage.v2.shape.TWindowSpecification;
072
073/**
074 * The {@code sqllr-path-1} paths of one statement ({@code lineage-spec/algorithms/path-v1.md}).
075 *
076 * <p>PATH-V1 anchors a reference at "the innermost step whose construct contains the first character of the
077 * reference" (Section 6.1), so this index records, for every step the statement emits, the text range of its
078 * construct and its path, and answers {@link #anchor} by the innermost range containing an offset. Steps are
079 * assigned top down from the parse tree with explicit work queues: the clauses of a query block and the steps of a
080 * data-changing statement have their parent fixed by Sections 3 and 4; expression steps ({@code FUNCTION_ARG},
081 * {@code CASE_*}, {@code SUBQUERY}, {@code WINDOW}) take the innermost enclosing step of their clause as parent.
082 * GSP's {@code TParseTreeNode.parent} is not used: it is set in only a few places.</p>
083 *
084 * <p>Forms PATH-V1 does not cover (Section 2, item 3) are recorded as uncovered ranges; a reference inside one has
085 * no {@code sqllr-path-1} anchor ({@link Anchor#isCovered()} is false), and the producer publishes nothing anchored
086 * there (plan 06, decision 1). A column reference that lands directly on a statement, query block or FROM-item step
087 * sits in a clause or table modifier this index does not know (MODEL, MATCH_RECOGNIZE, SAMPLE, table hints, OUTPUT,
088 * time travel): {@link #anchorColumn} reports it uncovered rather than inventing a path.</p>
089 *
090 * @since 4.2.17
091 */
092public final class PathIndex {
093
094    /** The anchor of an offset: a path, or the fact that it lies in an uncovered form or outside the statement. */
095    public static final class Anchor {
096        private final String path;
097        private final String uncoveredForm;
098
099        Anchor(String path, String uncoveredForm) {
100            this.path = path;
101            this.uncoveredForm = uncoveredForm;
102        }
103
104        /** The {@code sqllr-path-1} path; {@code null} when not covered. */
105        public String getPath() {
106            return path;
107        }
108
109        public boolean isCovered() {
110            return path != null;
111        }
112
113        /** The uncovered form that contains the offset, or {@code null}. */
114        public String getUncoveredForm() {
115            return uncoveredForm;
116        }
117
118        @Override
119        public String toString() {
120            return path != null ? path : "UNCOVERED " + uncoveredForm;
121        }
122    }
123
124    /** One step's construct. */
125    static final class Construct {
126        final String code;
127        int index;
128        final int start;
129        final int end;
130        Construct parent;
131        /** For expression steps: the clause step whose expressions are searched for the innermost parent. */
132        final Construct scope;
133        final int order;
134        String path;
135        int depth = -1;
136        final List<Construct> textChildren = new ArrayList<Construct>();
137
138        Construct(String code, int index, int start, int end, Construct parent, Construct scope, int order) {
139            this.code = code;
140            this.index = index;
141            this.start = start;
142            this.end = end;
143            this.parent = parent;
144            this.scope = scope;
145            this.order = order;
146        }
147
148        boolean contains(int offset) {
149            return start <= offset && offset < end;
150        }
151
152        boolean containsRange(Construct other) {
153            return start <= other.start && other.end <= end;
154        }
155
156        int length() {
157            return end - start;
158        }
159    }
160
161    private static final class Uncovered {
162        final String form;
163        final int start;
164        final int end;
165
166        Uncovered(String form, int start, int end) {
167            this.form = form;
168            this.start = start;
169            this.end = end;
170        }
171    }
172
173    /** Steps on which a column reference never lands in a covered form. */
174    private static final Set<String> NOT_A_COLUMN_ANCHOR = new HashSet<String>(Arrays.asList("SELECT", "INSERT",
175            "UPDATE", "DELETE", "MERGE", "CREATE_TABLE", "CREATE_VIEW", "SUBQUERY", "CTE", "BRANCH", "SET_OP", "WITH",
176            "FROM", "JOIN", "TARGET", "USING", "LATERAL"));
177
178    /** Functions whose FIRST operand can be a bare date-part keyword (SQL Server, Snowflake, MySQL), not an operand. */
179    private static final Set<String> FIRST_IS_DATE_PART = new HashSet<String>(Arrays.asList("DATEADD", "DATEDIFF",
180            "DATEDIFF_BIG", "DATEPART", "DATENAME", "DATETRUNC", "DATE_BUCKET", "DATE_PART", "DATE_TRUNC",
181            "TIMESTAMPADD", "TIMESTAMPDIFF", "TIMEADD", "TIMEDIFF", "TIME_SLICE"));
182
183    /** Functions whose LAST operand can be a bare date-part keyword (BigQuery), not an operand. */
184    private static final Set<String> LAST_IS_DATE_PART = new HashSet<String>(Arrays.asList("DATE_DIFF",
185            "DATETIME_DIFF", "TIMESTAMP_DIFF", "TIME_DIFF", "DATE_TRUNC", "DATETIME_TRUNC", "TIMESTAMP_TRUNC",
186            "TIME_TRUNC", "LAST_DAY"));
187
188    private static final Set<String> DATE_PARTS = new HashSet<String>(Arrays.asList("YEAR", "YY", "YYYY", "YEARS",
189            "QUARTER", "QQ", "Q", "QUARTERS", "MONTH", "MM", "M", "MONTHS", "DAYOFYEAR", "DY", "Y", "DAY", "DD", "D",
190            "DAYS", "WEEK", "WK", "WW", "WEEKS", "WEEKDAY", "DW", "HOUR", "HH", "HOURS", "MINUTE", "MI", "N", "MINUTES",
191            "SECOND", "SS", "S", "SECONDS", "MILLISECOND", "MS", "MILLISECONDS", "MICROSECOND", "MCS", "MICROSECONDS",
192            "NANOSECOND", "NS", "NANOSECONDS", "ISO_WEEK", "ISOWK", "ISOWW", "ISOWEEK", "ISOYEAR", "TZOFFSET", "TZ",
193            "DAYOFWEEK", "DAYOFMONTH", "EPOCH", "DATE", "TIME", "DATETIME", "TIMESTAMP", "CENTURY", "DECADE",
194            "MILLENNIUM", "YEAROFWEEK", "YEAROFWEEKISO", "WEEKISO", "WEEKOFYEAR"));
195
196    private final String statementStep;
197    private final EDbVendor vendor;
198    private final List<Construct> constructs = new ArrayList<Construct>();
199    private final List<Uncovered> uncovered = new ArrayList<Uncovered>();
200    private final Map<TSelectSqlStatement, Construct> blocks = new IdentityHashMap<TSelectSqlStatement, Construct>();
201    private final SourceText text;
202    private final List<Construct> roots = new ArrayList<Construct>();
203    private final Deque<Object[]> work = new ArrayDeque<Object[]>();
204    private Construct root;
205    private boolean statementUncovered;
206    private String statementUncoveredForm;
207
208    private PathIndex(SourceText text, String statementStep, EDbVendor vendor) {
209        this.text = text;
210        this.statementStep = statementStep;
211        this.vendor = vendor;
212    }
213
214    /** The statement step code of a statement, or {@code null} when PATH-V1 has none for it. */
215    public static String statementStep(TCustomSqlStatement stmt) {
216        if (stmt instanceof TSelectSqlStatement) {
217            return "SELECT";
218        }
219        if (stmt instanceof TInsertSqlStatement) {
220            return "INSERT";
221        }
222        if (stmt instanceof TUpdateSqlStatement) {
223            return "UPDATE";
224        }
225        if (stmt instanceof TDeleteSqlStatement) {
226            return "DELETE";
227        }
228        if (stmt instanceof TMergeSqlStatement) {
229            return "MERGE";
230        }
231        if (stmt instanceof TCreateTableSqlStatement) {
232            return "CREATE_TABLE";
233        }
234        if (stmt instanceof TCreateViewSqlStatement || stmt instanceof TCreateMaterializedSqlStatement) {
235            return "CREATE_VIEW";
236        }
237        if (isRoutineDefinition(stmt)) {
238            return "CREATE_ROUTINE"; // for the values it returns and the procedural parts of its body (Sections 3.5, 3.6)
239        }
240        if (isBlock(stmt)) {
241            return "BLOCK"; // for the procedural parts of its body (Section 3.6)
242        }
243        if (Procedural.isStandaloneAssignment(stmt)) {
244            return "ASSIGNMENT"; // SET @v = e, DECLARE @v INT = e outside every body (Section 3.6)
245        }
246        if (DocumentBuilder.isDynamicSite(stmt)) {
247            return "EXECUTE"; // dynamic execution, with its arguments (the text and the values it binds, slice 12)
248        }
249        if (stmt instanceof gudusoft.gsqlparser.lineage.v2.shape.TCallStatement
250                || stmt instanceof gudusoft.gsqlparser.lineage.v2.shape.TDb2CallStmt || isPlsqlCall(stmt)) {
251            return "CALL"; // a call of a routine, with its arguments (plan 09 slice 12)
252        }
253        if (isProcedureExecute(stmt)) {
254            return "EXECUTE"; // SQL Server EXEC of a named procedure, with its arguments (plan 09 slice 12)
255        }
256        return null;
257    }
258
259    /**
260     * A PL/SQL procedure call written as a statement ({@code p(a, b);}, {@code pkg.p(x => 1);}), as opposed to the
261     * DBMS_SQL.PARSE call that carries dynamic SQL text (a dynamic site).
262     */
263    static boolean isPlsqlCall(TCustomSqlStatement stmt) {
264        return stmt instanceof gudusoft.gsqlparser.lineage.v2.shape.TBasicStmt
265                && ((gudusoft.gsqlparser.lineage.v2.shape.TBasicStmt) stmt).getExpr() != null
266                && ((gudusoft.gsqlparser.lineage.v2.shape.TBasicStmt) stmt).getExpr().getFunctionCall() != null
267                && !DocumentBuilder.isDynamicSite(stmt);
268    }
269
270    /** A SQL Server EXEC of a named procedure, as opposed to an EXEC of dynamic SQL text (a dynamic site). */
271    static boolean isProcedureExecute(TCustomSqlStatement stmt) {
272        return stmt instanceof gudusoft.gsqlparser.lineage.v2.shape.TMssqlExecute
273                && ((gudusoft.gsqlparser.lineage.v2.shape.TMssqlExecute) stmt).getModuleName() != null
274                && !DocumentBuilder.isDynamicSite(stmt);
275    }
276
277    /**
278     * The arguments of a call statement, in text order (plan 09 slice 12): of a SQL Server EXEC, each parameter's value
279     * (a named one's value); of a CALL, each argument (a named one {@code p => v} as written, one operand).
280     */
281    static List<TExpression> callArguments(TCustomSqlStatement stmt) {
282        List<TExpression> out = new ArrayList<TExpression>();
283        if (stmt instanceof gudusoft.gsqlparser.lineage.v2.shape.TMssqlExecute) {
284            gudusoft.gsqlparser.lineage.v2.shape.TExecParameterList list =
285                    ((gudusoft.gsqlparser.lineage.v2.shape.TMssqlExecute) stmt).getParameters();
286            for (int i = 0; list != null && i < list.size(); i++) {
287                gudusoft.gsqlparser.lineage.v2.shape.TExecParameter x = list.getExecParameter(i);
288                out.add(x == null ? null : x.getParameterValue());
289            }
290        } else if (stmt instanceof gudusoft.gsqlparser.lineage.v2.shape.TCallStatement) {
291            TExpressionList args = ((gudusoft.gsqlparser.lineage.v2.shape.TCallStatement) stmt).getArgs();
292            for (int i = 0; args != null && i < args.size(); i++) {
293                out.add(args.getExpression(i));
294            }
295        } else if (stmt instanceof gudusoft.gsqlparser.lineage.v2.shape.TDb2CallStmt) {
296            TExpressionList args = ((gudusoft.gsqlparser.lineage.v2.shape.TDb2CallStmt) stmt).getParameters();
297            for (int i = 0; args != null && i < args.size(); i++) {
298                out.add(args.getExpression(i));
299            }
300        } else if (stmt instanceof gudusoft.gsqlparser.lineage.v2.shape.TExecImmeStmt) {
301            // EXECUTE IMMEDIATE text USING a, b: the text, then the bound values
302            gudusoft.gsqlparser.lineage.v2.shape.TExecImmeStmt e = (gudusoft.gsqlparser.lineage.v2.shape.TExecImmeStmt) stmt;
303            out.add(e.getDynamicStringExpr());
304            for (int i = 0; e.getBindArguments() != null && i < e.getBindArguments().size(); i++) {
305                gudusoft.gsqlparser.lineage.v2.shape.TBindArgument arg = e.getBindArguments().getBindArgument(i);
306                out.add(arg == null ? null : arg.getBindArgumentExpr());
307            }
308        } else if (stmt instanceof gudusoft.gsqlparser.lineage.v2.shape.TBasicStmt
309                && ((gudusoft.gsqlparser.lineage.v2.shape.TBasicStmt) stmt).getExpr() != null
310                && ((gudusoft.gsqlparser.lineage.v2.shape.TBasicStmt) stmt).getExpr().getFunctionCall() != null) {
311            TExpressionList args = ((gudusoft.gsqlparser.lineage.v2.shape.TBasicStmt) stmt).getExpr().getFunctionCall()
312                    .getArgs();
313            for (int i = 0; args != null && i < args.size(); i++) {
314                out.add(args.getExpression(i));
315            }
316        }
317        return out;
318    }
319
320    /**
321     * An anonymous block that runs where it stands (DECLARE ... BEGIN ... END, BEGIN ... END, PostgreSQL DO), or a
322     * control-flow statement outside every body (SQL Server IF and WHILE of a batch).
323     */
324    static boolean isBlock(TCustomSqlStatement stmt) {
325        // a control-flow statement is a statement of the document only outside every body (plan 09 slice 8): there it
326        // runs where it stands, as a block does
327        return stmt instanceof TMssqlBlock || stmt instanceof TCommonBlock || stmt instanceof TDoExecuteBlockStmt
328                || Procedural.isControlBlock(stmt);
329    }
330
331    /** A routine definition (CREATE FUNCTION, CREATE PROCEDURE), not a block that runs where it stands. */
332    static boolean isRoutineDefinition(TCustomSqlStatement stmt) {
333        return stmt instanceof TStoredProcedureSqlStatement && !(stmt instanceof TMssqlBlock
334                || stmt instanceof TCommonBlock || stmt instanceof TDoExecuteBlockStmt);
335    }
336
337    /**
338     * The values a routine definition returns, in the order of their RETURN[k] steps (path-v1.md, Section 3.5): the
339     * expression of a function whose body is one expression; otherwise each RETURN with a value of its own body in
340     * text order, through blocks and control flow but not into a nested routine definition, and not a RETURN of a
341     * query (RETURN QUERY, a RETURN whose value is a statement) or of nothing. Shared by the paths and the statement
342     * graph, so both read one rule.
343     */
344    static List<TExpression> returnValues(TCustomSqlStatement routine) {
345        return returnsValue(routine) ? valuesReturned(routine) : new ArrayList<TExpression>();
346    }
347
348    /**
349     * Whether a routine definition can return a value (Codex S7 #12): a function, and the procedures whose RETURN
350     * gives the caller a value: SQL Server and Sybase (the status), DB2 SQL PL (the status), Snowflake (RETURNS). The
351     * database rejects a RETURN with a value in an Oracle, MySQL or PostgreSQL procedure and in a trigger.
352     */
353    static boolean returnsValue(TCustomSqlStatement routine) {
354        if (!isRoutineDefinition(routine)) {
355            return false;
356        }
357        // the parse's statement type names the definition (sstcreatefunction, sstmssqlcreateprocedure, ...)
358        String type = String.valueOf(routine.sqlstatementtype).toLowerCase(Locale.ROOT);
359        if (type.contains("trigger")) {
360            return false;
361        }
362        if (type.contains("function")) {
363            return true;
364        }
365        EDbVendor vendor = routine.dbvendor;
366        return type.contains("procedure") && (vendor == EDbVendor.dbvmssql || vendor == EDbVendor.dbvazuresql
367                || vendor == EDbVendor.dbvsybase || vendor == EDbVendor.dbvsybasease || vendor == EDbVendor.dbvdb2
368                || vendor == EDbVendor.dbvsnowflake);
369    }
370
371    /** The value a RETURN statement gives, or null: none, or a query (RETURN QUERY, RETURN (SELECT ...), RETURN SELECT). */
372    static TExpression returnedValue(TCustomSqlStatement ret) {
373        TExpression value = rawReturnValue(ret);
374        return value == null || returnsQuery(ret) || returnsNext(ret) || queryExecute(ret) ? null : value;
375    }
376
377    /** PL/pgSQL {@code RETURN NEXT}, with or without an operand: one row of the set a function returns. */
378    static boolean returnsNext(TCustomSqlStatement ret) {
379        if (!(ret instanceof TReturnStmt) || ret.getStartToken() == null) {
380            return false;
381        }
382        TSourceToken next = nextSolidToken(ret.getStartToken());
383        return next != null && "NEXT".equalsIgnoreCase(next.getAstext()); // non-identifier-compare: keyword
384    }
385
386    /**
387     * PL/pgSQL {@code RETURN NEXT}, {@code RETURN QUERY} and {@code RETURN QUERY EXECUTE}: they add rows to the set the
388     * function returns and execution goes on after them, so they are no jump (Codex 13f #2).
389     */
390    static boolean appendsRows(TCustomSqlStatement ret) {
391        return returnsNext(ret) || returnsRows(ret) || queryExecute(ret);
392    }
393
394    /** PL/pgSQL {@code RETURN QUERY EXECUTE}: the rows of dynamic SQL text. */
395    static boolean queryExecute(TCustomSqlStatement ret) {
396        if (!(ret instanceof TReturnStmt) || ret.getStartToken() == null) {
397            return false;
398        }
399        TSourceToken next = nextSolidToken(ret.getStartToken());
400        TSourceToken after = next == null ? null : nextSolidToken(next);
401        return next != null && after != null && "QUERY".equalsIgnoreCase(next.getAstext()) // non-identifier-compare: keyword
402                && "EXECUTE".equalsIgnoreCase(after.getAstext()); // non-identifier-compare: keyword
403    }
404
405    /**
406     * Whether a routine's {@code RETURN NEXT} is modelled (plan 09 slice 13f; path-v1 Section 3.5, fold-v1 Section 4.7,
407     * item 8): a PostgreSQL-family function that returns a set of rows ({@code RETURNS TABLE}, {@code RETURNS SETOF}).
408     */
409    static boolean nextModelled(TCustomSqlStatement routine) {
410        if (!(routine instanceof TCreateFunctionStmt) || !DocumentBuilder.postgresFamily(routine.dbvendor)) {
411            return false;
412        }
413        TCreateFunctionStmt f = (TCreateFunctionStmt) routine;
414        if (f.getReturnsType() == gudusoft.gsqlparser.EFunctionReturnsType.frtInlineTableValue) {
415            return true;
416        }
417        return f.getReturnDataType() != null && DocumentBuilder.hasWord(f.getReturnDataType(), "SETOF");
418    }
419
420    /**
421     * A RETURN of a table that path-v1 does not cover (Section 2, item 3), never a value: RETURN QUERY EXECUTE (Codex
422     * 13d #3: its command text was taken for the value returned), and RETURN NEXT of a routine that does not return a
423     * set of rows of the PostgreSQL family, which the database rejects; the form's words, or null.
424     */
425    static String unmodelledRowReturn(TCustomSqlStatement routine, TCustomSqlStatement ret) {
426        if (queryExecute(ret)) {
427            return "RETURN QUERY EXECUTE";
428        }
429        return returnsNext(ret) && !nextModelled(routine) ? "RETURN NEXT" : null;
430    }
431
432    /**
433     * The steps RETURN[k] of a routine in order (path-v1 Section 3.5): {the RETURN statement, or null for an expression
434     * body; its value, or null}: each RETURN with a value {@link #returnValues} lists, and each RETURN NEXT of a routine
435     * that models it ({@link #nextModelled}), with its operand or without one (plan 09 slice 13f), in text order.
436     */
437    static List<Object[]> returnSteps(TCustomSqlStatement routine) {
438        List<Object[]> out = new ArrayList<Object[]>();
439        if (!returnsValue(routine)) {
440            return out;
441        }
442        if (routine instanceof TCreateFunctionStmt && ((TCreateFunctionStmt) routine).getSqlExpression() != null) {
443            out.add(new Object[] {null, ((TCreateFunctionStmt) routine).getSqlExpression()});
444            return out;
445        }
446        boolean next = nextModelled(routine);
447        for (TCustomSqlStatement ret : returnsOf(routine)) {
448            if (returnsNext(ret) || queryExecute(ret)) {
449                // RETURN NEXT (slice 13f); RETURN QUERY EXECUTE, whose command expression follows RETURN[k] and
450                // whose literal text is a derived unit with RETURN[k] its site (slice 13g)
451                if (next) {
452                    out.add(new Object[] {ret, rawReturnValue(ret)});
453                }
454                continue;
455            }
456            TExpression value = valueOf(routine, ret);
457            if (value != null) {
458                out.add(new Object[] {ret, value});
459            }
460        }
461        return out;
462    }
463
464    /** The expression a RETURN gives, a query included (not DB2's RETURN of a subquery), or null. */
465    private static TExpression rawReturnValue(TCustomSqlStatement ret) {
466        return ret instanceof TReturnStmt ? ((TReturnStmt) ret).getExpression()
467                : ret instanceof TMssqlReturn ? ((TMssqlReturn) ret).getReturnExpr()
468                : ret instanceof TDb2ReturnStmt && ((TDb2ReturnStmt) ret).getSubquery() == null
469                ? ((TDb2ReturnStmt) ret).getReturnExpr() : null;
470    }
471
472    /**
473     * PL/pgSQL {@code RETURN QUERY q} (not {@code RETURN QUERY EXECUTE}): the query's rows are rows of the set the
474     * function returns (plan 09 slice 8).
475     */
476    static boolean returnsRows(TCustomSqlStatement ret) {
477        if (!(ret instanceof TReturnStmt) || ret.getStartToken() == null) {
478            return false;
479        }
480        TSourceToken next = nextSolidToken(ret.getStartToken());
481        TSourceToken after = next == null ? null : nextSolidToken(next);
482        return next != null && "QUERY".equalsIgnoreCase(next.getAstext()) // non-identifier-compare: keyword
483                && (after == null || !"EXECUTE".equalsIgnoreCase(after.getAstext())); // non-identifier-compare: keyword
484    }
485
486    /**
487     * Whether a RETURN of a query is one the paths and the graph model (plan 09 slice 8; path-v1.md, Section 3.5):
488     * PL/pgSQL RETURN QUERY q, whose rows the function returns, and RETURN (SELECT ...) of a function that returns
489     * one value, a scalar subquery; not an inline table function's RETURN SELECT, RETURN QUERY EXECUTE or DB2's RETURN
490     * of a subquery.
491     */
492    static boolean modelledQueryReturn(TCustomSqlStatement routine, TCustomSqlStatement ret) {
493        if (inlineTableQuery(routine, ret) != null) {
494            return true; // an inline table function's RETURN of its query (Section 3.5)
495        }
496        if (!returnsQuery(ret) || rawReturnValue(ret) == null) {
497            return false;
498        }
499        if (returnsRows(ret)) {
500            return true;
501        }
502        if (ret instanceof TReturnStmt && "QUERY".equalsIgnoreCase( // non-identifier-compare: keyword
503                String.valueOf(nextSolidToken(ret.getStartToken()) == null ? null
504                        : nextSolidToken(ret.getStartToken()).getAstext()))) {
505            return false; // RETURN QUERY EXECUTE: dynamic SQL
506        }
507        return routine instanceof TCreateFunctionStmt && ((TCreateFunctionStmt) routine).getReturnsType()
508                != gudusoft.gsqlparser.EFunctionReturnsType.frtInlineTableValue;
509    }
510
511    /**
512     * A SQL Server inline table function: {@code RETURNS TABLE AS RETURN SELECT ...} or {@code RETURN (SELECT ...)},
513     * no declared return table (plan 09 phase C; path-v1 Section 3.5). Its RETURN's query gives the rows the
514     * function returns, as PL/pgSQL's RETURN QUERY does.
515     */
516    static boolean inlineTable(TCustomSqlStatement routine) {
517        if (!(routine instanceof TCreateFunctionStmt) || (routine.dbvendor != EDbVendor.dbvmssql
518                && routine.dbvendor != EDbVendor.dbvazuresql)) {
519            return false;
520        }
521        TCreateFunctionStmt f = (TCreateFunctionStmt) routine;
522        return f.getReturnsType() == gudusoft.gsqlparser.EFunctionReturnsType.frtInlineTableValue
523                && (f.getReturnTableDefinitions() == null || f.getReturnTableDefinitions().size() == 0);
524    }
525
526    /** The query an inline table function's RETURN gives (in parentheses or not), or null. */
527    static TSelectSqlStatement inlineTableQuery(TCustomSqlStatement routine, TCustomSqlStatement ret) {
528        if (!(ret instanceof TMssqlReturn) || !inlineTable(routine)) {
529            return null;
530        }
531        TExpression value = ((TMssqlReturn) ret).getReturnExpr();
532        if (value != null) {
533            return queryOf(value);
534        }
535        return ret.getStatements() != null && ret.getStatements().size() == 1
536                && ret.getStatements().get(0) instanceof TSelectSqlStatement
537                ? (TSelectSqlStatement) ret.getStatements().get(0) : null;
538    }
539
540    /**
541     * The RETURN of an inline table function written without parentheses ({@code RETURN SELECT ...}), whose query is
542     * no expression: {RETURN, query}, or null. Its step is RETURN[0] with the query as SUBQUERY[0] below it.
543     */
544    static Object[] inlineBareReturn(TCustomSqlStatement routine) {
545        if (!inlineTable(routine)) {
546            return null;
547        }
548        for (TCustomSqlStatement ret : returnsOf(routine)) {
549            if (((TMssqlReturn) ret).getReturnExpr() == null && inlineTableQuery(routine, ret) != null) {
550                return new Object[] {ret, inlineTableQuery(routine, ret)};
551            }
552        }
553        return null;
554    }
555
556    /** The RETURN statements of a routine's own body, in text order (whatever they return). */
557    static List<TCustomSqlStatement> returnStatementsOf(TCustomSqlStatement routine) {
558        return returnsOf(routine);
559    }
560
561    /** Whether a RETURN of the routine returns rows: PL/pgSQL RETURN QUERY, or an inline table function's query. */
562    static boolean returnsRows(TCustomSqlStatement routine, TCustomSqlStatement ret) {
563        return returnsRows(ret) || inlineTableQuery(routine, ret) != null;
564    }
565
566    /** The query an expression is as a whole (through parentheses), or null. */
567    static TSelectSqlStatement queryOf(TExpression value) {
568        TExpression e = value;
569        while (e != null && e.getExpressionType() == EExpressionType.parenthesis_t && e.getLeftOperand() != null) {
570            e = e.getLeftOperand();
571        }
572        return e != null && e.getExpressionType() == EExpressionType.subquery_t ? e.getSubQuery() : null;
573    }
574
575    /** The RETURN statement of each value {@link #returnValues} lists, in the same order (null for an expression body). */
576    static List<TCustomSqlStatement> returnStatements(TCustomSqlStatement routine) {
577        List<TCustomSqlStatement> out = new ArrayList<TCustomSqlStatement>();
578        if (!returnsValue(routine)) {
579            return out;
580        }
581        if (routine instanceof TCreateFunctionStmt && ((TCreateFunctionStmt) routine).getSqlExpression() != null) {
582            out.add(null);
583            return out;
584        }
585        for (TCustomSqlStatement ret : returnsOf(routine)) {
586            if (valueOf(routine, ret) != null) {
587                out.add(ret);
588            }
589        }
590        return out;
591    }
592
593    /** The value one RETURN of the routine's own body gives in RETURN[k] (Section 3.5), or null. */
594    private static TExpression valueOf(TCustomSqlStatement routine, TCustomSqlStatement ret) {
595        return returnsQuery(ret) ? (modelledQueryReturn(routine, ret) ? rawReturnValue(ret) : null)
596                : returnedValue(ret);
597    }
598
599    /**
600     * Whether a RETURN returns a query (Codex S7 #4): RETURN QUERY, DB2 RETURN of a subquery, or a value that is a
601     * query as a whole (an inline table function's RETURN SELECT, RETURN (SELECT ...)). A scalar expression with a
602     * subquery inside it (COALESCE((SELECT ...), @p)) returns a value: its other inputs are read.
603     */
604    static boolean returnsQuery(TCustomSqlStatement ret) {
605        if (ret instanceof TDb2ReturnStmt && ((TDb2ReturnStmt) ret).getSubquery() != null) {
606            return true;
607        }
608        TExpression value = ret instanceof TReturnStmt ? ((TReturnStmt) ret).getExpression()
609                : ret instanceof TMssqlReturn ? ((TMssqlReturn) ret).getReturnExpr() : null;
610        if (value == null) {
611            return ret.getStatements() != null && ret.getStatements().size() > 0;
612        }
613        while (value.getExpressionType() == EExpressionType.parenthesis_t && value.getLeftOperand() != null) {
614            value = value.getLeftOperand();
615        }
616        return value.getExpressionType() == EExpressionType.subquery_t;
617    }
618
619    /** The values of the RETURNs of a routine's own body (or its expression body), whatever the routine may return. */
620    static List<TExpression> valuesReturned(TCustomSqlStatement routine) {
621        List<TExpression> out = new ArrayList<TExpression>();
622        if (!isRoutineDefinition(routine)) {
623            return out;
624        }
625        if (routine instanceof TCreateFunctionStmt && ((TCreateFunctionStmt) routine).getSqlExpression() != null) {
626            out.add(((TCreateFunctionStmt) routine).getSqlExpression());
627            return out;
628        }
629        for (TCustomSqlStatement ret : returnsOf(routine)) {
630            TExpression value = valueOf(routine, ret);
631            if (value != null) {
632                out.add(value);
633            }
634        }
635        return out;
636    }
637
638    /** The RETURN statements of a routine's own body in text order, through blocks and control flow. */
639    private static List<TCustomSqlStatement> returnsOf(TCustomSqlStatement routine) {
640        List<TCustomSqlStatement> found = new ArrayList<TCustomSqlStatement>();
641        Deque<TCustomSqlStatement> pending = new ArrayDeque<TCustomSqlStatement>();
642        pending.push(routine);
643        while (!pending.isEmpty()) {
644            TCustomSqlStatement container = pending.pop();
645            for (int i = 0; container.getStatements() != null && i < container.getStatements().size(); i++) {
646                TCustomSqlStatement child = container.getStatements().get(i);
647                if (child == null || isRoutineDefinition(child)) {
648                    continue; // a nested routine's RETURNs are its own
649                }
650                if (child instanceof TReturnStmt || child instanceof TMssqlReturn || child instanceof TDb2ReturnStmt) {
651                    found.add(child);
652                    continue;
653                }
654                pending.push(child);
655            }
656        }
657        Collections.sort(found, new Comparator<TCustomSqlStatement>() {
658            @Override
659            public int compare(TCustomSqlStatement a, TCustomSqlStatement b) {
660                return Integer.compare(a.getStartToken() == null ? 0 : a.getStartToken().posinlist,
661                        b.getStartToken() == null ? 0 : b.getStartToken().posinlist);
662            }
663        });
664        return found;
665    }
666
667    /**
668     * The paths of one statement.
669     *
670     * @param stmt the statement
671     * @param text the text of the source unit the statement's tokens are positioned in
672     * @param step the statement step to use when {@link #statementStep} has none ({@code COPY}, {@code CALL},
673     *             {@code EXECUTE}), or {@code null}: the statement then has no covered anchor at all
674     */
675    public static PathIndex build(TCustomSqlStatement stmt, SourceText text, String step) {
676        String code = statementStep(stmt);
677        PathIndex index = new PathIndex(text, code != null ? code : step, stmt.dbvendor);
678        index.build(stmt, code != null);
679        return index;
680    }
681
682    /**
683     * The same for a statement of dynamic SQL text: the analysis moves its tokens' lines and columns to the site, so
684     * positions come from the tokens' offsets into the text.
685     */
686    public static PathIndex buildByOffset(TCustomSqlStatement stmt, SourceText text) {
687        String code = statementStep(stmt);
688        PathIndex index = new PathIndex(text, code, stmt.dbvendor);
689        index.byOffset = true;
690        index.build(stmt, code != null);
691        return index;
692    }
693
694    /** Positions from token offsets (a statement of dynamic SQL text) instead of lines and columns. */
695    private boolean byOffset;
696
697    private int tokenOffset(TSourceToken token) {
698        if (byOffset) {
699            return token.offset < 0 ? -1 : (int) token.offset;
700        }
701        return text.lexerOffset(token.lineNo, token.columnNo);
702    }
703
704    /** {@link #build(TCustomSqlStatement, SourceText, String)} with no fallback statement step. */
705    public static PathIndex build(TCustomSqlStatement stmt, SourceText text) {
706        return build(stmt, text, null);
707    }
708
709    /**
710     * The paths of a statement a caller holds as a live parse tree (a parse with no analysis): the tree is projected
711     * first (plan 09, slice 1b), and the live overloads of {@link #anchorColumn} and {@link #blockPath} take its nodes.
712     */
713    public static PathIndex build(gudusoft.gsqlparser.TCustomSqlStatement stmt, SourceText text) {
714        ShapeBuilder projection =
715                ShapeBuilder.projectKeepingLive(stmt);
716        PathIndex index = build((TCustomSqlStatement) projection.shape().root(), text, null);
717        index.projection = projection;
718        return index;
719    }
720
721    /** The projection of a live tree {@link #build(gudusoft.gsqlparser.TCustomSqlStatement, SourceText)} took. */
722    private ShapeBuilder projection;
723
724    private <T> T mirrorOf(Object live, Class<T> type) {
725        if (projection == null) {
726            throw new IllegalStateException("the index was built from a projection, not from a live tree");
727        }
728        return type.cast(projection.mirrorOf(live));
729    }
730
731    /** The statement step path, such as {@code INSERT[0]}, or {@code null} when the statement has none. */
732    public String getStatementPath() {
733        return statementStep == null ? null : statementStep + "[0]";
734    }
735
736    /** The path of a query block (the {@code QUERY_BLOCK} scope path), or {@code null} when not indexed. */
737    public String blockPath(TSelectSqlStatement block) {
738        Construct c = blocks.get(block);
739        return c == null ? null : c.path;
740    }
741
742    /** {@link #blockPath(TSelectSqlStatement)} of a live query block of the tree the index was built from. */
743    public String blockPath(gudusoft.gsqlparser.stmt.TSelectSqlStatement block) {
744        return blockPath(mirrorOf(block, TSelectSqlStatement.class));
745    }
746
747    /** Every indexed query block and its path, in no particular order. */
748    public Map<TSelectSqlStatement, String> blockPaths() {
749        Map<TSelectSqlStatement, String> out = new IdentityHashMap<TSelectSqlStatement, String>();
750        for (Map.Entry<TSelectSqlStatement, Construct> e : blocks.entrySet()) {
751            out.put(e.getKey(), e.getValue().path);
752        }
753        return out;
754    }
755
756    /** The innermost query block path that is a prefix of (or equal to) a path, or {@code null}. */
757    public String enclosingBlockPath(String path) {
758        if (path == null) {
759            return null;
760        }
761        String best = null;
762        for (Construct c : blocks.values()) {
763            String p = c.path;
764            if ((path.equals(p) || path.startsWith(p + "/")) && (best == null || p.length() > best.length())) {
765                best = p;
766            }
767        }
768        return best;
769    }
770
771    /** Whether every part of the statement is in a covered form. */
772    public boolean isFullyCovered() {
773        return !statementUncovered && uncovered.isEmpty();
774    }
775
776    /** The anchor of the reference whose first character is at the UTF-16 offset. */
777    public Anchor anchor(int offset) {
778        if (statementUncovered) {
779            return new Anchor(null, statementUncoveredForm);
780        }
781        for (Uncovered u : uncovered) {
782            if (u.start <= offset && offset < u.end) {
783                return new Anchor(null, u.form);
784            }
785        }
786        Construct best = null;
787        List<Construct> level = roots;
788        while (true) {
789            Construct hit = find(level, offset);
790            if (hit == null) {
791                break;
792            }
793            best = hit;
794            level = hit.textChildren;
795        }
796        if (best == null) {
797            return new Anchor(null, "OUTSIDE_STATEMENT");
798        }
799        return new Anchor(best.path, null);
800    }
801
802    /** The child of a sorted, disjoint level that contains the offset, or null. */
803    private static Construct find(List<Construct> level, int offset) {
804        int low = 0;
805        int high = level.size() - 1;
806        int candidate = -1;
807        while (low <= high) {
808            int mid = (low + high) >>> 1;
809            if (level.get(mid).start <= offset) {
810                candidate = mid;
811                low = mid + 1;
812            } else {
813                high = mid - 1;
814            }
815        }
816        for (int i = candidate; i >= 0 && i >= candidate - 2; i--) {
817            if (level.get(i).contains(offset)) {
818                return level.get(i);
819            }
820        }
821        return null;
822    }
823
824    /** The anchor of the first character of a token. */
825    public Anchor anchor(TSourceToken token) {
826        int offset = token == null ? -1 : tokenOffset(token);
827        return offset < 0 ? new Anchor(null, "UNPOSITIONED") : anchor(offset);
828    }
829
830    /** The anchor of the first token of a node. */
831    public Anchor anchor(TParseTreeNode node) {
832        return anchor(node == null ? null : node.getStartToken());
833    }
834
835    /** {@link #anchorColumn(TObjectName)} of a live column reference of the tree the index was built from. */
836    public Anchor anchorColumn(gudusoft.gsqlparser.nodes.TObjectName column) {
837        return anchorColumn(mirrorOf(column, TObjectName.class));
838    }
839
840    /**
841     * The anchor of a column reference: as {@link #anchor(TParseTreeNode)}, but a column that lands directly on a
842     * statement, query block, WITH, FROM, JOIN, TARGET or USING step is in a construct this index does not model
843     * and is reported uncovered.
844     */
845    public Anchor anchorColumn(TObjectName column) {
846        Anchor a = anchor(column);
847        if (!a.isCovered()) {
848            return a;
849        }
850        String last = a.getPath().substring(a.getPath().lastIndexOf('/') + 1);
851        String code = last.substring(0, last.indexOf('['));
852        if (NOT_A_COLUMN_ANCHOR.contains(code)) {
853            return new Anchor(null, "UNMODELLED CLAUSE AT " + a.getPath());
854        }
855        return a;
856    }
857
858    // ---------------------------------------------------------------------------------------------
859    // Building
860    // ---------------------------------------------------------------------------------------------
861
862    private int start(TParseTreeNode node) {
863        if (node == null || node.getStartToken() == null) {
864            return -1;
865        }
866        TSourceToken t = node.getStartToken();
867        return tokenOffset(t);
868    }
869
870    private int end(TParseTreeNode node) {
871        if (node == null || node.getEndToken() == null) {
872            return -1;
873        }
874        TSourceToken t = node.getEndToken();
875        int offset = tokenOffset(t);
876        return offset < 0 ? -1 : offset + (t.getAstext() == null ? 0 : t.getAstext().length());
877    }
878
879    private int offsetOf(TSourceToken token) {
880        return token == null ? -1 : tokenOffset(token);
881    }
882
883    private static TSourceToken previousSolid(TSourceToken token) {
884        TSourceToken t = token == null ? null : token.getPrevTokenInChain();
885        while (t != null && (t.tokentype == ETokenType.ttwhitespace || t.tokentype == ETokenType.ttreturn
886                || t.tokentype == ETokenType.ttsimplecomment || t.tokentype == ETokenType.ttbracketedcomment)) {
887            t = t.getPrevTokenInChain();
888        }
889        return t;
890    }
891
892    /** The offset of the keyword just before a node when it is one of the given words, or the node's start. */
893    private int keywordStart(TParseTreeNode node, String... keywords) {
894        int s = start(node);
895        TSourceToken t = node == null ? null : previousSolid(node.getStartToken());
896        for (int hops = 0; hops < 3 && t != null; hops++) {
897            for (String keyword : keywords) {
898                if (keyword.equalsIgnoreCase(t.getAstext())) { // non-identifier-compare: SQL keyword text
899                    int o = offsetOf(t);
900                    if (o >= 0 && o < s) {
901                        s = o;
902                    }
903                    return s;
904                }
905            }
906            t = previousSolid(t);
907        }
908        return s;
909    }
910
911    private Construct add(String code, int index, int start, int end, Construct parent) {
912        if (start < 0 || end < start) {
913            return null;
914        }
915        Construct c = new Construct(code, index, start, end, parent, null, constructs.size());
916        constructs.add(c);
917        return c;
918    }
919
920    private Construct add(String code, int index, TParseTreeNode node, Construct parent) {
921        return add(code, index, start(node), end(node), parent);
922    }
923
924    /** An expression step whose parent is the innermost step of {@code scope} that contains it. */
925    private Construct addExpression(String code, int index, int start, int end, Construct scope) {
926        if (start < 0 || end < start) {
927            return null;
928        }
929        Construct c = new Construct(code, index, start, end, null, scope, constructs.size());
930        constructs.add(c);
931        return c;
932    }
933
934    private void markUncovered(String form, TParseTreeNode node) {
935        markUncovered(form, start(node), end(node));
936    }
937
938    private void markUncovered(String form, int s, int e) {
939        if (s >= 0 && e >= s) {
940            uncovered.add(new Uncovered(form, s, e));
941        }
942    }
943
944    private void build(TCustomSqlStatement stmt, boolean covered) {
945        if (statementStep == null) {
946            statementUncovered = true;
947            statementUncoveredForm = "STATEMENT " + stmt.sqlstatementtype;
948            return;
949        }
950        root = add(statementStep, 0, stmt, null);
951        if (root == null) {
952            statementUncovered = true;
953            statementUncoveredForm = "UNPOSITIONED";
954            return;
955        }
956        if (!covered) {
957            // COPY, CALL, EXECUTE: the statement step only; their arguments are not covered
958            uncovered.add(new Uncovered("ARGUMENTS OF " + statementStep, root.start, root.end));
959            finish();
960            return;
961        }
962        dmlCtes = coveredDmlCtes(stmt);
963        try {
964            if (stmt instanceof TSelectSqlStatement) {
965                queryExpression((TSelectSqlStatement) stmt, root);
966            } else if (stmt instanceof TInsertSqlStatement) {
967                insert((TInsertSqlStatement) stmt);
968            } else if (stmt instanceof TUpdateSqlStatement) {
969                update((TUpdateSqlStatement) stmt);
970            } else if (stmt instanceof TDeleteSqlStatement) {
971                delete((TDeleteSqlStatement) stmt);
972            } else if (stmt instanceof TMergeSqlStatement) {
973                merge((TMergeSqlStatement) stmt);
974            } else if (stmt instanceof TCreateTableSqlStatement) {
975                createTable((TCreateTableSqlStatement) stmt);
976            } else if (stmt instanceof TCreateViewSqlStatement) {
977                TCreateViewSqlStatement view = (TCreateViewSqlStatement) stmt;
978                createView(view, view.getViewName(), view.getViewAliasClause(), view.getSubquery());
979            } else if (stmt instanceof TCreateMaterializedSqlStatement) {
980                TCreateMaterializedSqlStatement view = (TCreateMaterializedSqlStatement) stmt;
981                createView(view, view.getViewName(), view.getViewAliasClause(), view.getSubquery());
982            } else if (isRoutineDefinition(stmt) || isBlock(stmt)) {
983                List<Object[]> steps = returnSteps(stmt);
984                for (int k = 0; k < steps.size(); k++) {
985                    TExpression value = (TExpression) steps.get(k)[1];
986                    if (value != null) {
987                        expressions(add("RETURN", k, value, root), value);
988                    } else {
989                        add("RETURN", k, (TCustomSqlStatement) steps.get(k)[0], root); // RETURN NEXT without an operand
990                    }
991                }
992                Object[] bare = inlineBareReturn(stmt);
993                if (bare != null) {
994                    // RETURN SELECT ... of an inline table function: RETURN[0], its query SUBQUERY[0] below it
995                    Construct ret = add("RETURN", 0, (TCustomSqlStatement) bare[0], root);
996                    Construct sub = ret == null ? null : add("SUBQUERY", 0, (TSelectSqlStatement) bare[1], ret);
997                    if (sub != null) {
998                        work.push(new Object[] {bare[1], sub});
999                    }
1000                }
1001                procedural(Procedural.of(stmt));
1002            } else if ("ASSIGNMENT".equals(statementStep)) {
1003                procedural(Procedural.standalone(stmt));
1004            } else if ("CALL".equals(statementStep) || "EXECUTE".equals(statementStep)) {
1005                callArgumentSteps(stmt, root);
1006            }
1007            drain();
1008            returning(stmt);
1009        } catch (RuntimeException e) {
1010            // a parse tree this index cannot walk (GSP leaves some children null): nothing is covered
1011            statementUncovered = true;
1012            statementUncoveredForm = "UNTRAVERSABLE PARSE TREE (" + e.getClass().getSimpleName() + ")";
1013            return;
1014        }
1015        finish();
1016        markUnmodelledIdentifiers(stmt);
1017    }
1018
1019    /**
1020     * The procedural parts of a routine's or a block's own body, or of a standalone assignment (Section 3.6): ASSIGN[k]
1021     * with its value's expression steps, or with the query of a SQL Server SELECT @v = ... as SUBQUERY[0]; CONDITION[k]
1022     * with its expression steps; CURSOR[c] introducing the cursor's query; FETCH[f] with nothing below it.
1023     */
1024    private void procedural(Procedural parts) {
1025        for (int k = 0; k < parts.assigns.size(); k++) {
1026            Procedural.Assign a = parts.assigns.get(k);
1027            int end = a.last == null ? -1 : offsetOf(a.last);
1028            Construct c = add("ASSIGN", k, offsetOf(a.first),
1029                    end < 0 ? -1 : end + (a.last.getAstext() == null ? 0 : a.last.getAstext().length()), root);
1030            if (c == null) {
1031                continue;
1032            }
1033            if (a.query != null) {
1034                Construct query = add("SUBQUERY", 0, a.query, c);
1035                if (query != null) {
1036                    work.push(new Object[] {a.query, query});
1037                }
1038            } else {
1039                expressions(c, a.value);
1040            }
1041        }
1042        for (int k = 0; k < parts.conditions.size(); k++) {
1043            Procedural.Condition c = parts.conditions.get(k);
1044            expressions(add("CONDITION", k, c.expr, root), c.expr);
1045        }
1046        for (int k = 0; k < parts.cursors.size(); k++) {
1047            Procedural.Cursor c = parts.cursors.get(k);
1048            Construct cursor = add("CURSOR", k, c.query, root);
1049            if (cursor != null) {
1050                work.push(new Object[] {c.query, cursor});
1051            }
1052        }
1053        for (int k = 0; k < parts.fetches.size(); k++) {
1054            add("FETCH", k, parts.fetches.get(k).node, root);
1055        }
1056    }
1057
1058    private void drain() {
1059        while (!work.isEmpty()) {
1060            Object[] item = work.pop();
1061            queryExpression((TSelectSqlStatement) item[0], (Construct) item[1]);
1062        }
1063    }
1064
1065    private void with(TCustomSqlStatement stmt, Construct parent) {
1066        if (stmt.getCteList() == null || stmt.getCteList().size() == 0) {
1067            return;
1068        }
1069        Construct with = add("WITH", 0, stmt.getCteList(), parent);
1070        if (with == null) {
1071            return;
1072        }
1073        for (int c = 0; c < stmt.getCteList().size(); c++) {
1074            TCTE cte = stmt.getCteList().getCTE(c);
1075            Construct cc = add("CTE", c, cte, with);
1076            if (cc == null) {
1077                continue;
1078            }
1079            if (cte.getColumnList() != null) {
1080                add("COLUMN_LIST", 0, cte.getColumnList(), cc);
1081            }
1082            if (cte.getSubquery() != null) {
1083                work.push(new Object[] {cte.getSubquery(), cc});
1084            } else if (coveredHere(cte)) {
1085                dataChangingBody(cte, cc);
1086            } else {
1087                markUncovered("CTE BODY", cte);
1088            }
1089        }
1090    }
1091
1092    /**
1093     * The data-changing statement a CTE's body is (plan 09 slice 13e; path-v1 Section 3.2, item 6): its step, INSERT[0],
1094     * UPDATE[0] or DELETE[0] directly below CTE[c], then its own clauses below that step, as the statement step's of a
1095     * statement of its own would be; its RETURNING is RETURNING[0] below it.
1096     */
1097    private void dataChangingBody(TCTE cte, Construct cc) {
1098        TCustomSqlStatement body = dataChanging(cte);
1099        String step = body instanceof TInsertSqlStatement ? "INSERT" : body instanceof TUpdateSqlStatement ? "UPDATE"
1100                : body instanceof TMergeSqlStatement ? "MERGE" : "DELETE";
1101        int bodyStart = start(body);
1102        int bodyEnd = end(body);
1103        if (bodyStart >= 0 && bodyEnd < 0 && cte.getEndToken() != null) {
1104            // the parse gives no end to a body such as DELETE FROM t (Codex 13e #1): it ends at the CTE's ")"
1105            bodyEnd = offsetOf(cte.getEndToken());
1106        }
1107        Construct dml = bodyStart < 0 || bodyEnd < bodyStart ? null : add(step, 0, bodyStart, bodyEnd, cc);
1108        if (dml == null) {
1109            markUncovered("CTE BODY", cte);
1110            return;
1111        }
1112        Construct savedRoot = root;
1113        TParseTreeNode savedOwn = ownReturning;
1114        root = dml;
1115        ownReturning = null;
1116        try {
1117            if (body instanceof TInsertSqlStatement) {
1118                insert((TInsertSqlStatement) body);
1119            } else if (body instanceof TUpdateSqlStatement) {
1120                update((TUpdateSqlStatement) body);
1121            } else if (body instanceof TMergeSqlStatement) {
1122                merge((TMergeSqlStatement) body);
1123            } else {
1124                delete((TDeleteSqlStatement) body);
1125            }
1126            if (ownReturning != null) {
1127                ownedReturnings.add(Integer.valueOf(start(ownReturning)));
1128            }
1129            dmlCteSteps.put(cte, dml);
1130        } finally {
1131            root = savedRoot;
1132            ownReturning = savedOwn;
1133        }
1134    }
1135
1136    /** The covered data-changing CTEs of the statement ({@link #coveredDmlCtes}), in WITH order. */
1137    private List<TCTE> dmlCtes = java.util.Collections.<TCTE>emptyList();
1138
1139    /** Their statement steps, CTE[c]/UPDATE[0] and the like, once indexed (paths are set by finish()). */
1140    private final Map<TCTE, Construct> dmlCteSteps = new java.util.IdentityHashMap<TCTE, Construct>();
1141
1142    private boolean coveredHere(TCTE cte) {
1143        for (TCTE c : dmlCtes) {
1144            if (c == cte) {
1145                return true;
1146            }
1147        }
1148        return false;
1149    }
1150
1151    /** The starts of the RETURNING clauses covered besides the statement's own: the covered CTE's. */
1152    private final java.util.Set<Integer> ownedReturnings = new java.util.HashSet<Integer>();
1153
1154    /** The path of the step of a covered data-changing CTE's statement (CTE[c]/UPDATE[0]), or null. */
1155    public String getDmlCtePath(TCTE cte) {
1156        Construct step = dmlCteSteps.get(cte);
1157        return step == null ? null : step.path;
1158    }
1159
1160    /** The INSERT, UPDATE, DELETE or MERGE (PostgreSQL 17, slice 13i) a CTE's body is, or null (a query). */
1161    static TCustomSqlStatement dataChanging(TCTE cte) {
1162        TCustomSqlStatement body = cte == null || cte.getSubquery() != null ? null : cte.getPreparableStmt();
1163        return body instanceof TInsertSqlStatement || body instanceof TUpdateSqlStatement
1164                || body instanceof TDeleteSqlStatement || body instanceof TMergeSqlStatement ? body : null;
1165    }
1166
1167    /**
1168     * The data-changing CTEs the paths and the graph cover (plan 09 slices 13e, 13h and 13i, owner rulings 2026-10-17
1169     * and 2026-10-18; fold-v1 Section 4.8, item 7): the PostgreSQL family's
1170     * {@code WITH d AS (INSERT/UPDATE/DELETE ... [RETURNING ...])}, every one of the WITH of a query, INSERT, UPDATE,
1171     * DELETE or MERGE, in WITH order, each with its own model (an INSERT inside an INSERT included). A target is
1172     * always a table and a body never reads its own CTE: the parse binds them so (TCustomSqlStatement.unbindCteTarget,
1173     * TCTE.unbindSelfReferences, measured on PostgreSQL 17); a parse that still binds one to a CTE is not covered,
1174     * said, never read as the CTE. One rule for the paths and the graph.
1175     */
1176    static List<TCTE> coveredDmlCtes(TCustomSqlStatement stmt) {
1177        if (stmt == null || !dmlCteDialect(stmt.dbvendor)) {
1178            return Collections.<TCTE>emptyList();
1179        }
1180        if (!(stmt instanceof TSelectSqlStatement || stmt instanceof TInsertSqlStatement
1181                || stmt instanceof TUpdateSqlStatement || stmt instanceof TDeleteSqlStatement
1182                || stmt instanceof TMergeSqlStatement) || stmt.getCteList() == null
1183                || (stmt instanceof TInsertSqlStatement && ((TInsertSqlStatement) stmt).isMultiTableInsert())) {
1184            return Collections.<TCTE>emptyList();
1185        }
1186        List<TCTE> out = new ArrayList<TCTE>();
1187        for (int c = 0; c < stmt.getCteList().size(); c++) {
1188            TCTE cte = stmt.getCteList().getCTE(c);
1189            TCustomSqlStatement body = dataChanging(cte);
1190            if (body == null || boundToCte(body.getTargetTable()) || boundToCte(stmt.getTargetTable())) {
1191                continue;
1192            }
1193            boolean self = false;
1194            for (int i = 0; body.tables != null && i < body.tables.size(); i++) {
1195                TTable t = body.tables.getTable(i);
1196                self |= t != null && t.isCTEName() && t.getCTE() == cte;
1197            }
1198            if (!self) {
1199                out.add(cte);
1200            }
1201        }
1202        return out;
1203    }
1204
1205    /**
1206     * The dialects whose data-changing CTEs are covered: PostgreSQL, Greenplum and EDB (GaussDB parses them too but its
1207     * rules are not verified: not covered, said).
1208     */
1209    static boolean dmlCteDialect(EDbVendor v) {
1210        return v == EDbVendor.dbvpostgresql || v == EDbVendor.dbvgreenplum || v == EDbVendor.dbvedb;
1211    }
1212
1213    /** Whether the parse bound a target to a CTE (it does not for the PostgreSQL family: a guard, never a guess). */
1214    private static boolean boundToCte(TTable target) {
1215        return target != null && target.isCTEName();
1216    }
1217
1218    private void queryExpression(TSelectSqlStatement s, Construct in) {
1219        blocks.put(s, in);
1220        with(s, in);
1221        if (s.getValueClause() != null) {
1222            // a VALUES list used as a query (plan 09 slice 11): its rows are VALUES[r] below the step that introduces
1223            // the query expression
1224            if (s.getValueClause().getRows() == null || s.getValueClause().getRows().isEmpty()) {
1225                markUncovered("VALUES AS A QUERY", s);
1226            } else {
1227                valuesRows(s.getValueClause().getRows(), in);
1228            }
1229            return;
1230        }
1231        if (isSetOperation(s)) {
1232            if (mixesPrecedence(s)) {
1233                // GSP groups A UNION B INTERSECT C left to right; the SQL standard (and these dialects) bind
1234                // INTERSECT tighter, so the parse tree's branches are not the query's: no path is published
1235                markUncovered("SET OPERATION PRECEDENCE", s);
1236                return;
1237            }
1238            int[] range = setRange(s);
1239            Construct setOp = add("SET_OP", 0, range[0], range[1], in);
1240            if (setOp == null) {
1241                return;
1242            }
1243            List<TSelectSqlStatement> operands = operands(s);
1244            for (int b = 0; b < operands.size(); b++) {
1245                TSelectSqlStatement operand = operands.get(b);
1246                Construct branch = add("BRANCH", b, operand, setOp);
1247                if (branch != null) {
1248                    work.push(new Object[] {operand, branch});
1249                }
1250            }
1251            if (s.getOrderbyClause() != null) {
1252                expressions(add("ORDER_BY", 0, s.getOrderbyClause(), setOp), s.getOrderbyClause());
1253            }
1254            limits(s, setOp);
1255            return;
1256        }
1257        block(s, in);
1258    }
1259
1260    private static boolean isSetOperation(TSelectSqlStatement s) {
1261        return s.getSetOperatorType() != null && s.getSetOperatorType() != ESetOperatorType.none
1262                && s.getLeftStmt() != null && s.getRightStmt() != null;
1263    }
1264
1265    /**
1266     * Whether an unparenthesized chain mixes INTERSECT with UNION/EXCEPT/MINUS in a dialect that binds INTERSECT
1267     * tighter (all six portable dialects but Oracle, where the set operators have equal precedence).
1268     */
1269    private boolean mixesPrecedence(TSelectSqlStatement s) {
1270        if (vendor == EDbVendor.dbvoracle) {
1271            return false;
1272        }
1273        boolean intersect = false;
1274        boolean other = false;
1275        Deque<TSelectSqlStatement> stack = new ArrayDeque<TSelectSqlStatement>();
1276        stack.push(s);
1277        while (!stack.isEmpty()) {
1278            TSelectSqlStatement x = stack.pop();
1279            if (!isSetOperation(x) || (x != s && x.getParenthesisCount() > 0)) {
1280                continue;
1281            }
1282            if (x.getSetOperatorType() == ESetOperatorType.intersect) {
1283                intersect = true;
1284            } else {
1285                other = true;
1286            }
1287            stack.push(x.getLeftStmt());
1288            stack.push(x.getRightStmt());
1289        }
1290        return intersect && other;
1291    }
1292
1293    /** From the first operand to the end of the set operation, excluding a WITH clause written before it. */
1294    private int[] setRange(TSelectSqlStatement s) {
1295        TSelectSqlStatement first = s;
1296        while (isSetOperation(first)) {
1297            first = first.getLeftStmt();
1298        }
1299        return new int[] {start(first), end(s)};
1300    }
1301
1302    /**
1303     * The operands of one {@code SET_OP}: a chain of one operator with the same {@code ALL}/{@code DISTINCT} form and
1304     * no parentheses is flattened, in text order (Section 3.2, item 3).
1305     */
1306    private List<TSelectSqlStatement> operands(TSelectSqlStatement s) {
1307        List<TSelectSqlStatement> out = new ArrayList<TSelectSqlStatement>();
1308        Deque<TSelectSqlStatement> stack = new ArrayDeque<TSelectSqlStatement>();
1309        stack.push(s.getRightStmt());
1310        TSelectSqlStatement left = s.getLeftStmt();
1311        while (isSetOperation(left) && left.getSetOperatorType() == s.getSetOperatorType() && left.isAll() == s.isAll()
1312                && left.getParenthesisCount() == 0 && (left.getCteList() == null || left.getCteList().size() == 0)
1313                && left.getOrderbyClause() == null) {
1314            stack.push(left.getRightStmt());
1315            left = left.getLeftStmt();
1316        }
1317        out.add(left);
1318        while (!stack.isEmpty()) {
1319            out.add(stack.pop());
1320        }
1321        return out;
1322    }
1323
1324    private void block(TSelectSqlStatement s, Construct in) {
1325        if (s.getResultColumnList() != null) {
1326            expressions(add("SELECT_LIST", 0, s.getResultColumnList(), in), s.getResultColumnList());
1327        }
1328        if (s.getJoins() != null && s.getJoins().size() > 0) {
1329            fromClause("FROM", s.getJoins(), keywordOffset(s.getJoins(), "FROM"), in);
1330        }
1331        if (s.getWhereClause() != null) {
1332            expressions(add("WHERE", 0, s.getWhereClause(), in), s.getWhereClause());
1333        }
1334        TGroupBy groupBy = s.getGroupByClause();
1335        if (groupBy != null) {
1336            if (groupBy.getItems() != null && groupBy.getItems().size() > 0) {
1337                int st = groupBy.getGROUP() != null ? offsetOf(groupBy.getGROUP()) : start(groupBy.getItems());
1338                expressions(add("GROUP_BY", 0, st, end(groupBy.getItems()), in), groupBy.getItems());
1339            }
1340            if (groupBy.getHavingClause() != null) {
1341                int st = groupBy.getHAVING() != null ? offsetOf(groupBy.getHAVING()) : start(groupBy.getHavingClause());
1342                expressions(add("HAVING", 0, st, end(groupBy.getHavingClause()), in), groupBy.getHavingClause());
1343            }
1344        }
1345        if (s.getQualifyClause() != null) {
1346            expressions(add("QUALIFY", 0, s.getQualifyClause(), in), s.getQualifyClause());
1347        }
1348        if (s.getOrderbyClause() != null) {
1349            expressions(add("ORDER_BY", 0, s.getOrderbyClause(), in), s.getOrderbyClause());
1350        }
1351        limits(s, in);
1352        if (s.getWindowClause() != null) {
1353            if (s.getWindowClause().getWindowDefs() != null) {
1354                for (int w = 0; w < s.getWindowClause().getWindowDefs().size(); w++) {
1355                    TWindowDef definition = (TWindowDef) s.getWindowClause().getWindowDefs().getElement(w);
1356                    windowParts(add("WINDOW", w, definition, in), definition.getPartitionClause(),
1357                            definition.getOrderBy());
1358                }
1359            } else if (s.getWindowClause().getWindowDefinitions() != null) {
1360                for (int w = 0; w < s.getWindowClause().getWindowDefinitions().size(); w++) {
1361                    TWindowDefinition definition =
1362                            (TWindowDefinition) s.getWindowClause()
1363                                    .getWindowDefinitions().getElement(w);
1364                    Construct window = add("WINDOW", w, definition, in);
1365                    TWindowSpecification spec = definition.getWindowSpecification();
1366                    if (spec != null) {
1367                        windowParts(window, spec.getPartitionClause(), spec.getOrderBy());
1368                    }
1369                }
1370            }
1371        }
1372        THierarchical hierarchical = s.getHierarchicalClause();
1373        if (hierarchical != null) {
1374            markUncovered("CONNECT BY", hierarchical);
1375        }
1376    }
1377
1378    /** The offset of a keyword written just before the node (such as FROM before the first table). */
1379    private int keywordOffset(TParseTreeNode node, String keyword) {
1380        TParseTreeNode first = node;
1381        if (node instanceof TJoinList && ((TJoinList) node).size() > 0) {
1382            first = ((TJoinList) node).getJoin(0);
1383            if (first.getStartToken() == null) {
1384                first = ((TJoinList) node).getJoin(0).getTable();
1385            }
1386        }
1387        return keywordStart(first, keyword) < start(first) ? keywordStart(first, keyword) : -1;
1388    }
1389
1390    /** TOP, LIMIT, OFFSET and FETCH are one LIMIT step; each fragment is a construct with that path. */
1391    private void limits(TSelectSqlStatement s, Construct in) {
1392        TParseTreeNode[] parts = {s.getTopClause(), s.getLimitClause(), s.getOffsetClause(), s.getFetchFirstClause()};
1393        for (TParseTreeNode part : parts) {
1394            if (part != null) {
1395                expressions(add("LIMIT", 0, part, in), part);
1396            }
1397        }
1398    }
1399
1400    /** {@code FROM}, {@code USING} below DELETE/MERGE, or the target list of UPDATE: joins and derived tables. */
1401    private Construct fromClause(String code, TJoinList joins, int keywordAt, Construct in) {
1402        int[] range = joinListRange(joins);
1403        if (range == null) {
1404            return null;
1405        }
1406        Construct from = add(code, 0, keywordAt >= 0 && keywordAt < range[0] ? keywordAt : range[0], range[1], in);
1407        if (from == null) {
1408            return null;
1409        }
1410        joinsAndTables(joins, from);
1411        return from;
1412    }
1413
1414    private int[] joinListRange(TJoinList joins) {
1415        int st = -1;
1416        int en = -1;
1417        for (int i = 0; i < joins.size(); i++) {
1418            TJoin join = joins.getJoin(i);
1419            TParseTreeNode[] parts = {join, join.getTable()};
1420            for (TParseTreeNode part : parts) {
1421                int ps = start(part);
1422                int pe = end(part);
1423                if (ps >= 0 && pe >= ps) {
1424                    st = st < 0 ? ps : Math.min(st, ps);
1425                    en = Math.max(en, pe);
1426                }
1427            }
1428            if (join.getJoinItems() != null) {
1429                for (int k = 0; k < join.getJoinItems().size(); k++) {
1430                    TJoinItem item = join.getJoinItems().getJoinItem(k);
1431                    int ps = start(item);
1432                    int pe = Math.max(end(item), end(item.getOnCondition()));
1433                    if (ps >= 0 && pe >= ps) {
1434                        st = st < 0 ? ps : Math.min(st, ps);
1435                        en = Math.max(en, pe);
1436                    }
1437                }
1438            }
1439        }
1440        return st < 0 ? null : new int[] {st, en};
1441    }
1442
1443    private void joinsAndTables(TParseTreeNode node, Construct from) {
1444        final List<TJoinItem> joinItems = new ArrayList<TJoinItem>();
1445        final List<TTable> tables = new ArrayList<TTable>();
1446        final List<TSelectSqlStatement> selects = new ArrayList<TSelectSqlStatement>();
1447        final List<TPivotClause> pivots = new ArrayList<TPivotClause>();
1448        node.acceptChildren(new TParseTreeVisitor() {
1449            @Override
1450            public void preVisit(TJoinItem item) {
1451                joinItems.add(item);
1452            }
1453
1454            @Override
1455            public void preVisit(TTable table) {
1456                tables.add(table);
1457            }
1458
1459            @Override
1460            public void preVisit(TSelectSqlStatement stmt) {
1461                selects.add(stmt);
1462            }
1463
1464            @Override
1465            public void preVisit(TPivotClause pivot) {
1466                pivots.add(pivot);
1467            }
1468        });
1469        List<TSelectSqlStatement> outer = outermost(selects);
1470        // JOIN[j]: the join items outside every derived table, in text order of their JOIN keyword; an APPLY is no
1471        // JOIN keyword (its item is a lateral item of the clause, LATERAL[l] below FROM[0], plan 09 slice 11)
1472        List<TJoinItem> items = new ArrayList<TJoinItem>();
1473        final List<TTable> applied = new ArrayList<TTable>();
1474        for (TJoinItem item : joinItems) {
1475            if (containedInAny(item, outer)) {
1476                continue;
1477            }
1478            if (item.getJoinType() == EJoinType.crossapply || item.getJoinType() == EJoinType.outerapply) {
1479                if (item.getTable() != null) {
1480                    applied.add(item.getTable());
1481                }
1482                continue;
1483            }
1484            items.add(item);
1485        }
1486        Collections.sort(items, new Comparator<TJoinItem>() {
1487            @Override
1488            public int compare(TJoinItem a, TJoinItem b) {
1489                return Integer.compare(start(a), start(b));
1490            }
1491        });
1492        List<Construct> joinConstructs = new ArrayList<Construct>();
1493        for (int j = 0; j < items.size(); j++) {
1494            TJoinItem item = items.get(j);
1495            int en = Math.max(end(item), end(item.getOnCondition()));
1496            if (item.getUsingColumns() != null) {
1497                en = Math.max(en, end(item.getUsingColumns()));
1498            }
1499            Construct join = add("JOIN", j, keywordStart(item, "JOIN"), en, from);
1500            if (join == null) {
1501                continue;
1502            }
1503            joinConstructs.add(join);
1504            if (item.getOnCondition() != null) {
1505                expressions(add("ON", 0, keywordStart(item.getOnCondition(), "ON"), end(item.getOnCondition()), join),
1506                        item.getOnCondition());
1507            }
1508            if (item.getUsingColumns() != null) {
1509                add("USING", 0, keywordStart(item.getUsingColumns(), "USING"), end(item.getUsingColumns()), join);
1510            }
1511        }
1512        final List<TPivotClause> covered = new ArrayList<TPivotClause>();
1513        for (TTable table : tables) {
1514            if (table.getSubquery() == null ? containedInAny(table, outer) : !outer.contains(table.getSubquery())) {
1515                continue;
1516            }
1517            List<TPivotClause> operators = pivotClauses(table);
1518            if (!operators.isEmpty()) {
1519                // PATH-V1 PIVOT[p] (plan 09 slice 10): the operators below the step of the item they apply to, in text
1520                // order; the item itself (a table name, or a derived table, SUBQUERY[k]) is reached as an item of its own
1521                Construct parent = from;
1522                for (Construct join : joinConstructs) {
1523                    if (join.contains(start(table)) && (parent == from || join.length() < parent.length())) {
1524                        parent = join;
1525                    }
1526                }
1527                for (TPivotClause pivot : operators) {
1528                    Construct op = add("PIVOT", -1, keywordStart(pivot, "PIVOT", "UNPIVOT"), end(pivot), parent);
1529                    if (op == null) {
1530                        continue;
1531                    }
1532                    covered.add(pivot);
1533                    if (pivot.getAggregation_function_list() != null) {
1534                        expressions(op, pivot.getAggregation_function_list());
1535                    } else if (pivot.getAggregation_function() != null) {
1536                        expressions(op, pivot.getAggregation_function());
1537                    }
1538                    TPivotInClause in = pivot.getPivotInClause();
1539                    if (in != null && in.getSubQuery() != null) {
1540                        Construct sub = add("SUBQUERY", -1, in.getSubQuery(), op);
1541                        if (sub != null) {
1542                            work.push(new Object[] {in.getSubQuery(), sub});
1543                        }
1544                    }
1545                }
1546                continue;
1547            }
1548            if (lateralItem(table, applied, from, joinConstructs)) {
1549                continue;
1550            }
1551            String form = uncoveredTableForm(table);
1552            if (form != null) {
1553                markUncovered(form, table);
1554                continue;
1555            }
1556            TSourceToken pattern = matchRecognizeToken(table);
1557            if (pattern != null) {
1558                // path-v1 Section 2, item 3: the clause (its partition, measures, pattern and definitions) is not
1559                // covered; the item's rows are its table's (doc 10, Section 2.1), its columns are not (StatementGraph)
1560                markUncovered("MATCH_RECOGNIZE", offsetOf(pattern), end(table));
1561            }
1562            if (table.getSubquery() != null) {
1563                Construct parent = from;
1564                for (Construct join : joinConstructs) {
1565                    if (join.contains(start(table)) && (parent == from || join.length() < parent.length())) {
1566                        parent = join;
1567                    }
1568                }
1569                Construct sub = add("SUBQUERY", -1, table.getSubquery(), parent);
1570                if (sub != null) {
1571                    work.push(new Object[] {table.getSubquery(), sub});
1572                }
1573            }
1574        }
1575        for (TPivotClause pivot : pivots) {
1576            if (!containedInAny(pivot, outer) && !covered.contains(pivot)) {
1577                markUncovered("PIVOT", pivot);
1578            }
1579        }
1580    }
1581
1582    /**
1583     * PATH-V1 {@code LATERAL[l]} (plan 09 slice 11): a lateral item of a FROM clause, below the step of the item
1584     * (FROM[0], or the JOIN[j] that introduces it), numbered in text order among the lateral items below that step: a
1585     * derived table written after LATERAL or the right side of an APPLY (its query is SUBQUERY[0] below it), and a call
1586     * in FROM (a table function, TABLE(...), UNNEST, a Hive-family LATERAL VIEW), whose operands are FUNCTION_ARG[k]
1587     * below it (Section 5, item 1: TABLE(f(x)) is f's call as FUNCTION_ARG[0]). A VALUES list used as a FROM item is
1588     * a derived table, SUBQUERY[k], whose rows are VALUES[r]. Its Hive LATERAL VIEWs follow a table name. Returns
1589     * whether the item was one of these.
1590     */
1591    private boolean lateralItem(TTable table, List<TTable> applied, Construct from, List<Construct> joinConstructs) {
1592        Construct parent = from;
1593        // an APPLY's item is directly below FROM[0] even inside a parenthesized join tree (APPLY is no JOIN keyword;
1594        // Codex slice 11 #10)
1595        for (Construct join : applied.contains(table) ? Collections.<Construct>emptyList() : joinConstructs) {
1596            if (join.contains(start(table)) && (parent == from || join.length() < parent.length())) {
1597                parent = join;
1598            }
1599        }
1600        if (table.getTableType() == ETableSource.objectname && table.getLateralViewList() != null) {
1601            for (TLateralView view : table.getLateralViewList()) {
1602                Construct lateral = add("LATERAL", -1, view, from);
1603                if (lateral != null && view.getUdtf() != null) {
1604                    tableCall(lateral, view.getUdtf());
1605                }
1606            }
1607            return false; // the table itself is an item of its own
1608        }
1609        ETableSource type = table.getTableType();
1610        boolean query = table.getSubquery() != null && (applied.contains(table) || "LATERAL".equals(lateralKeyword(table)));
1611        if (query) {
1612            int st = keywordStart(table, "LATERAL");
1613            Construct lateral = add("LATERAL", -1, Math.min(st >= 0 ? st : start(table), start(table.getSubquery())),
1614                    Math.max(end(table), end(table.getSubquery())), parent);
1615            Construct sub = lateral == null ? null : add("SUBQUERY", 0, table.getSubquery(), lateral);
1616            if (sub != null) {
1617                work.push(new Object[] {table.getSubquery(), sub});
1618            }
1619            return true;
1620        }
1621        if (type == ETableSource.function || type == ETableSource.unnest || type == ETableSource.tableExpr) {
1622            int st = keywordStart(table, "LATERAL");
1623            Construct lateral = add("LATERAL", -1, st >= 0 ? st : start(table), end(table), parent);
1624            if (lateral == null) {
1625                return true;
1626            }
1627            if (type == ETableSource.function && table.getFuncCall() != null) {
1628                tableCall(lateral, table.getFuncCall());
1629            } else if (type == ETableSource.unnest && table.getUnnestClause() != null) {
1630                // UNNEST(a, b) is a call: its arrays are its operands (an expression, or the column of the legacy
1631                // UNNEST(t.arr), in the order TableFunctionLayout reads them)
1632                List<TParseTreeNode> arrays = unnestArrays(table.getUnnestClause());
1633                for (int k = 0; k < arrays.size(); k++) {
1634                    addExpression("FUNCTION_ARG", k, start(arrays.get(k)), end(arrays.get(k)), lateral);
1635                    expressions(lateral, arrays.get(k));
1636                }
1637            } else if (type == ETableSource.tableExpr && table.getTableExpr() != null) {
1638                // Oracle TABLE(collection): a call whose one operand is the collection
1639                addExpression("FUNCTION_ARG", 0, start(table.getTableExpr()), end(table.getTableExpr()), lateral);
1640                expressions(lateral, table.getTableExpr());
1641            }
1642            return true;
1643        }
1644        if (type == ETableSource.rowList || (type == ETableSource.subquery && table.getSubquery() == null
1645                && table.getValueClause() != null)) {
1646            Construct sub = add("SUBQUERY", -1, table, parent);
1647            if (sub != null) {
1648                valuesRows(valueRows(table), sub);
1649            }
1650            return true;
1651        }
1652        return false;
1653    }
1654
1655    /**
1656     * The operands of a call in FROM, FUNCTION_ARG[k] below its LATERAL[l] (Section 5, item 1): each argument as
1657     * written, a named argument ({@code input => x}) whole. Read from the call's arguments: a vendor's own call class
1658     * (Snowflake's FLATTEN) is not a call the expression visitor lists.
1659     */
1660    private void tableCall(Construct lateral, TFunctionCall call) {
1661        TExpressionList args = call.getArgs();
1662        for (int k = 0; args != null && k < args.size(); k++) {
1663            TExpression arg = args.getExpression(k);
1664            if (arg != null && start(arg) >= 0) {
1665                addExpression("FUNCTION_ARG", k, start(arg), end(arg), lateral);
1666                expressions(lateral, arg);
1667            }
1668        }
1669    }
1670
1671    /** The arrays of an UNNEST, as {@code TableFunctionLayout.unnestArrays} reads them. */
1672    private static List<TParseTreeNode> unnestArrays(TUnnestClause clause) {
1673        List<TParseTreeNode> arrays = new ArrayList<TParseTreeNode>();
1674        if (clause.getExprList() != null && clause.getExprList().size() > 0) {
1675            for (int i = 0; i < clause.getExprList().size(); i++) {
1676                arrays.add(clause.getExprList().getExpression(i));
1677            }
1678        } else if (clause.getArrayExpr() != null) {
1679            arrays.add(clause.getArrayExpr());
1680        } else if (clause.getColumns() != null) {
1681            for (int i = 0; i < clause.getColumns().size(); i++) {
1682                arrays.add(clause.getColumns().getObjectName(i));
1683            }
1684        }
1685        return arrays;
1686    }
1687
1688    /** "LATERAL" when that keyword stands before a derived table (or between its start and its query), else null. */
1689    private String lateralKeyword(TTable table) {
1690        TSourceToken before = previousSolid(table.getStartToken());
1691        if (before != null && "LATERAL".equalsIgnoreCase(before.getAstext())) { // non-identifier-compare: keyword
1692            return "LATERAL";
1693        }
1694        if (table.getSubquery() != null && table.getStartToken() != null) {
1695            int subStart = start(table.getSubquery());
1696            for (TSourceToken t = table.getStartToken(); t != null && offsetOf(t) >= 0 && offsetOf(t) < subStart;
1697                    t = t.getNextTokenInChain()) {
1698                if ("LATERAL".equalsIgnoreCase(t.getAstext())) { // non-identifier-compare: keyword
1699                    return "LATERAL";
1700                }
1701            }
1702        }
1703        return null;
1704    }
1705
1706    /** The rows of a VALUES list used as a FROM item: its value clause's, or its row list's. */
1707    private List<TParseTreeNode> valueRows(TTable table) {
1708        List<TParseTreeNode> rows = new ArrayList<TParseTreeNode>();
1709        if (table.getValueClause() != null && table.getValueClause().getRows() != null
1710                && !table.getValueClause().getRows().isEmpty()) {
1711            rows.addAll(table.getValueClause().getRows());
1712        } else if (table.getRowList() != null) {
1713            for (int r = 0; r < table.getRowList().size(); r++) {
1714                rows.add(table.getRowList().getMultiTarget(r));
1715            }
1716        }
1717        return rows;
1718    }
1719
1720    /** VALUES[r] (plan 09 slice 11): the r-th row of a VALUES list used as a query, directly below its step. */
1721    private void valuesRows(List<? extends TParseTreeNode> rows, Construct in) {
1722        for (int r = 0; r < rows.size(); r++) {
1723            TParseTreeNode row = rows.get(r);
1724            Construct c = add("VALUES", r, row, in);
1725            if (c != null) {
1726                expressions(c, row);
1727            }
1728        }
1729    }
1730
1731    /**
1732     * The one PIVOT or UNPIVOT operator a FROM item applies to its table or derived table (plan 09 slice 10), or null:
1733     * an item with no operator, with several chained operators, or a Teradata TD_UNPIVOT table operator.
1734     */
1735    static TPivotClause singlePivot(TTable table) {
1736        List<TPivotClause> clauses = pivotClauses(table);
1737        return clauses.size() == 1 ? clauses.get(0) : null;
1738    }
1739
1740    /**
1741     * The PIVOT and UNPIVOT operators a FROM item applies to its table or derived table, in text order (a chain has
1742     * several), or none: an item with no operator, or a Teradata TD_UNPIVOT table operator.
1743     */
1744    static List<TPivotClause> pivotClauses(TTable table) {
1745        List<TPivotClause> out = new ArrayList<TPivotClause>();
1746        if (table == null || table.getTableType() != ETableSource.pivoted_table || table.getPivotedTable() == null
1747                || table.getPivotedTable().getTableSource() == null) {
1748            return out;
1749        }
1750        TPTNodeList clauses = table.getPivotedTable().getPivotClauseList();
1751        for (int i = 0; clauses != null && i < clauses.size(); i++) {
1752            if (!(clauses.getElement(i) instanceof TPivotClause)) {
1753                return new ArrayList<TPivotClause>();
1754            }
1755            out.add((TPivotClause) clauses.getElement(i));
1756        }
1757        return out;
1758    }
1759
1760    /**
1761     * The PATH-V1 form a table source is not covered by, or null for a table name or a derived table (lateral items
1762     * and VALUES lists are steps of their own: {@link #lateralItem}).
1763     */
1764    private String uncoveredTableForm(TTable table) {
1765        ETableSource type = table.getTableType();
1766        if (table.getPivotClause() != null || type == ETableSource.pivoted_table || type == ETableSource.td_unpivot) {
1767            return "PIVOT";
1768        }
1769        if (type == ETableSource.subquery || table.getSubquery() != null || type == ETableSource.objectname
1770                || type == ETableSource.join || type == ETableSource.lateralView) {
1771            // a LATERAL VIEW's table: its step is the view's own, made with the table it follows
1772            return null;
1773        }
1774        return "TABLE " + type;
1775    }
1776
1777    /**
1778     * Whether a FROM item carries a MATCH_RECOGNIZE clause. GSP keeps no node for it: the item's tokens run over the
1779     * clause, whose result has the partition columns and the measures, never the table's other columns.
1780     */
1781    static boolean matchRecognize(TTable item) {
1782        return item != null && item.getTableType() != ETableSource.function && matchRecognizeToken(item) != null;
1783    }
1784
1785    /**
1786     * Why a column read through a MATCH_RECOGNIZE item is not the table's column of that name, or null when it is
1787     * (plan 09 slice 13j). The result has the PARTITION BY columns and the measures; ALL ROWS PER MATCH keeps every
1788     * other column of the table too. A measure takes its name from its alias, so a name a measure carries is never
1789     * the table's column, and with ONE ROW PER MATCH a name no PARTITION BY item carries is none of the result's.
1790     * The clause has no node: its tokens are read at depth one of its parentheses.
1791     */
1792    static String matchRecognizeColumn(TTable item, EDbVendor vendor, String column) {
1793        TSourceToken keyword = matchRecognizeToken(item);
1794        String over = item.getTableName() == null ? "the table" : item.getTableName().toString();
1795        String unknown = "may name a measure of MATCH_RECOGNIZE over " + over + ", whose columns are not published yet";
1796        if (keyword == null) {
1797            return unknown;
1798        }
1799        List<TSourceToken> solid = new ArrayList<TSourceToken>();
1800        for (int i = keyword.posinlist + 1; i < keyword.container.size(); i++) {
1801            TSourceToken t = keyword.container.get(i);
1802            if (!blank(t)) {
1803                solid.add(t);
1804            }
1805            if (t == item.getEndToken()) {
1806                break;
1807            }
1808        }
1809        List<TSourceToken> partition = new ArrayList<TSourceToken>();
1810        List<TSourceToken> measures = new ArrayList<TSourceToken>();
1811        boolean allRows = false;
1812        boolean closed = false;
1813        String clause = "";
1814        TSourceToken last = null; // the last solid token of the measure being read: its alias
1815        int depth = 0;
1816        for (int i = 0; i < solid.size() && !closed; i++) {
1817            TSourceToken t = solid.get(i);
1818            String text = t.getAstext();
1819            String k = text.toUpperCase(Locale.ROOT);
1820            boolean word = t.tokentype != ETokenType.ttdqstring && t.tokentype != ETokenType.ttsqstring;
1821            if ("(".equals(text)) {
1822                depth++;
1823                if (depth > 1 && "MEASURES".equals(clause)) {
1824                    last = t;
1825                }
1826            } else if (")".equals(text)) {
1827                depth--;
1828                closed = depth == 0;
1829                if (!closed && "MEASURES".equals(clause)) {
1830                    last = t;
1831                }
1832            } else if (depth == 1 && word && !"DEFINE".equals(clause) && ("PARTITION".equals(k) || "ORDER".equals(k)
1833                    || "MEASURES".equals(k) || "ONE".equals(k) || "ALL".equals(k) || "AFTER".equals(k)
1834                    || "PATTERN".equals(k) || "SUBSET".equals(k) || "DEFINE".equals(k))) {
1835                if ("MEASURES".equals(clause)) {
1836                    if (last == null) {
1837                        return unknown;
1838                    }
1839                    measures.add(last);
1840                }
1841                last = null;
1842                clause = k;
1843                allRows |= "ALL".equals(k) && i + 1 < solid.size()
1844                        && "ROWS".equals(solid.get(i + 1).getAstext().toUpperCase(Locale.ROOT));
1845            } else if ("MEASURES".equals(clause)) {
1846                if (depth == 1 && ",".equals(text)) {
1847                    if (last == null) {
1848                        return unknown;
1849                    }
1850                    measures.add(last);
1851                    last = null;
1852                } else {
1853                    last = t;
1854                }
1855            } else if (depth == 1 && "PARTITION".equals(clause) && !"BY".equals(k) && !",".equals(text)) {
1856                if (".".equals(text)) {
1857                    if (!partition.isEmpty()) {
1858                        partition.remove(partition.size() - 1); // a qualifier: the column is the part after it
1859                    }
1860                } else {
1861                    partition.add(t);
1862                }
1863            }
1864        }
1865        if (!closed) {
1866            return unknown;
1867        }
1868        if ("MEASURES".equals(clause)) {
1869            if (last == null) {
1870                return unknown;
1871            }
1872            measures.add(last);
1873        }
1874        for (TSourceToken alias : measures) {
1875            if (alias.tokentype != ETokenType.ttidentifier && alias.tokentype != ETokenType.ttdqstring
1876                    && alias.tokentype != ETokenType.ttkeyword) {
1877                return unknown; // a measure that does not end in a name: its alias is not read
1878            }
1879            if (gudusoft.gsqlparser.util.SQLUtil.sameName(vendor,
1880                    gudusoft.gsqlparser.sqlenv.ESQLDataObjectType.dotColumn, alias.getAstext(), column)) {
1881                return "names a measure of MATCH_RECOGNIZE over " + over + ", whose value is not published yet";
1882            }
1883        }
1884        if (allRows) {
1885            return null;
1886        }
1887        for (TSourceToken name : partition) {
1888            if (gudusoft.gsqlparser.util.SQLUtil.sameName(vendor,
1889                    gudusoft.gsqlparser.sqlenv.ESQLDataObjectType.dotColumn, name.getAstext(), column)) {
1890                return null;
1891            }
1892        }
1893        return "is no PARTITION BY column or measure of MATCH_RECOGNIZE over " + over + " (ONE ROW PER MATCH): the "
1894                + "result has no such column";
1895    }
1896
1897    private static boolean blank(TSourceToken t) {
1898        return t.tokentype == ETokenType.ttwhitespace || t.tokentype == ETokenType.ttreturn
1899                || t.tokentype == ETokenType.ttsimplecomment || t.tokentype == ETokenType.ttbracketedcomment;
1900    }
1901
1902    /** Whether the tokens of a node name MATCH_RECOGNIZE anywhere (a cheap test before {@link #matchRecognize}). */
1903    static boolean mentionsMatchRecognize(TParseTreeNode node) {
1904        TSourceToken start = node == null ? null : node.getStartToken();
1905        TSourceToken end = node == null ? null : node.getEndToken();
1906        if (start == null || end == null || start.container == null || end.container != start.container) {
1907            return true; // no range to scan: ask item by item
1908        }
1909        for (int i = start.posinlist; i <= end.posinlist && i < start.container.size(); i++) {
1910            if ("MATCH_RECOGNIZE".equalsIgnoreCase(start.container.get(i).getAstext())) { // non-identifier-compare: keyword
1911                return true;
1912            }
1913        }
1914        return false;
1915    }
1916
1917    /** The MATCH_RECOGNIZE keyword of a FROM item followed by its parenthesis, or null. */
1918    private static TSourceToken matchRecognizeToken(TTable item) {
1919        TSourceToken start = item == null ? null : item.getStartToken();
1920        TSourceToken end = item == null ? null : item.getEndToken();
1921        if (start == null || end == null || start.container == null || end.container != start.container) {
1922            return null;
1923        }
1924        int depth = 0;
1925        for (int i = start.posinlist; i <= end.posinlist && i < start.container.size(); i++) {
1926            TSourceToken t = start.container.get(i);
1927            if ("(".equals(t.getAstext())) {
1928                depth++;
1929            } else if (")".equals(t.getAstext())) {
1930                depth--;
1931            }
1932            // the item's own clause, never one of a query or a call nested in it (Codex 13j #2)
1933            if (depth != 0 || !"MATCH_RECOGNIZE".equalsIgnoreCase(t.getAstext())) { // non-identifier-compare: keyword
1934                continue;
1935            }
1936            for (int j = i + 1; j <= end.posinlist && j < start.container.size(); j++) {
1937                TSourceToken n = start.container.get(j);
1938                if (!blank(n)) {
1939                    if ("(".equals(n.getAstext())) {
1940                        return t;
1941                    }
1942                    break;
1943                }
1944            }
1945        }
1946        return null;
1947    }
1948
1949    private List<TSelectSqlStatement> outermost(List<TSelectSqlStatement> selects) {
1950        List<int[]> ranges = new ArrayList<int[]>();
1951        for (TSelectSqlStatement s : selects) {
1952            ranges.add(new int[] {start(s), end(s)});
1953        }
1954        List<TSelectSqlStatement> out = new ArrayList<TSelectSqlStatement>();
1955        for (int i = 0; i < selects.size(); i++) {
1956            boolean inner = false;
1957            for (int j = 0; j < selects.size() && !inner; j++) {
1958                if (i != j && ranges.get(j)[0] <= ranges.get(i)[0] && ranges.get(i)[1] <= ranges.get(j)[1]
1959                        && (ranges.get(j)[0] < ranges.get(i)[0] || ranges.get(i)[1] < ranges.get(j)[1] || j < i)) {
1960                    inner = true;
1961                }
1962            }
1963            if (!inner) {
1964                out.add(selects.get(i));
1965            }
1966        }
1967        return out;
1968    }
1969
1970    /** Whether the whole node lies within one of the statements (a join item can start at a derived table's paren). */
1971    private boolean containedInAny(TParseTreeNode node, List<TSelectSqlStatement> selects) {
1972        int ns = start(node);
1973        int ne = Math.max(end(node), node instanceof TJoinItem ? end(((TJoinItem) node).getOnCondition()) : -1);
1974        for (TSelectSqlStatement s : selects) {
1975            if (start(s) <= ns && ne <= end(s)) {
1976                return true;
1977            }
1978        }
1979        return false;
1980    }
1981
1982    private boolean insideAny(int offset, List<TSelectSqlStatement> selects) {
1983        for (TSelectSqlStatement s : selects) {
1984            if (start(s) <= offset && offset < end(s)) {
1985                return true;
1986            }
1987        }
1988        return false;
1989    }
1990
1991    /**
1992     * The expression steps of one clause (Section 5): calls and their operands, CASE parts, subqueries and OVER
1993     * clauses. Nodes inside a subquery are left to that subquery's own query expression.
1994     */
1995    private void expressions(final Construct clause, TParseTreeNode node) {
1996        if (clause == null || node == null) {
1997            return;
1998        }
1999        final List<TFunctionCall> calls = new ArrayList<TFunctionCall>();
2000        final List<TCaseExpression> cases = new ArrayList<TCaseExpression>();
2001        final List<TExpression> exprs = new ArrayList<TExpression>();
2002        final List<TSelectSqlStatement> selects = new ArrayList<TSelectSqlStatement>();
2003        final List<TParseTreeNode> notCovered = new ArrayList<TParseTreeNode>();
2004        node.acceptChildren(new TParseTreeVisitor() {
2005            @Override
2006            public void preVisit(TFunctionCall call) {
2007                calls.add(call);
2008            }
2009
2010            @Override
2011            public void preVisit(TCaseExpression expr) {
2012                cases.add(expr);
2013            }
2014
2015            @Override
2016            public void preVisit(TExpression expr) {
2017                exprs.add(expr);
2018            }
2019
2020            @Override
2021            public void preVisit(TSelectSqlStatement stmt) {
2022                selects.add(stmt);
2023            }
2024
2025            @Override
2026            public void preVisit(TPivotClause pivot) {
2027                notCovered.add(pivot);
2028            }
2029
2030            @Override
2031            public void preVisit(TReturningClause returning) {
2032                notCovered.add(returning);
2033            }
2034
2035            @Override
2036            public void preVisit(TOutputClause output) {
2037                notCovered.add(output);
2038            }
2039        });
2040        if (node instanceof TExpression) {
2041            TExpression e = (TExpression) node;
2042            if (exprs.isEmpty() || exprs.get(0) != e) {
2043                exprs.add(0, e); // TExpression.acceptChildren visits the node itself first: never list it twice
2044            }
2045            if (e.getFunctionCall() != null && !calls.contains(e.getFunctionCall())) {
2046                calls.add(0, e.getFunctionCall());
2047            }
2048            if (e.getCaseExpression() != null && !cases.contains(e.getCaseExpression())) {
2049                cases.add(0, e.getCaseExpression());
2050            }
2051            if (e.getSubQuery() != null && !selects.contains(e.getSubQuery())) {
2052                selects.add(0, e.getSubQuery());
2053            }
2054        }
2055        List<TSelectSqlStatement> outer = outermost(selects);
2056        for (TSelectSqlStatement sub : outer) {
2057            Construct c = addExpression("SUBQUERY", -1, start(sub), end(sub), clause);
2058            if (c != null) {
2059                work.push(new Object[] {sub, c});
2060            }
2061        }
2062        for (TParseTreeNode n : notCovered) {
2063            if (!insideAny(start(n), outer)) {
2064                markUncovered(n instanceof TPivotClause ? "PIVOT" : n instanceof TOutputClause ? "OUTPUT" : "RETURNING",
2065                        n);
2066            }
2067        }
2068        for (TExpression e : exprs) {
2069            if (e.getExpressionType() == EExpressionType.lambda_t && !insideAny(start(e), outer)) {
2070                markUncovered("LAMBDA", e);
2071            }
2072        }
2073        for (TCaseExpression k : cases) {
2074            if (insideAny(start(k), outer) || k.getWhenClauseItemList() == null) {
2075                continue;
2076            }
2077            for (int i = 0; i < k.getWhenClauseItemList().size(); i++) {
2078                TWhenClauseItem when = k.getWhenClauseItemList().getWhenClauseItem(i);
2079                if (when.getComparison_expr() != null) {
2080                    addExpression("CASE_WHEN", i, start(when.getComparison_expr()), end(when.getComparison_expr()),
2081                            clause);
2082                }
2083                if (when.getReturn_expr() != null) {
2084                    addExpression("CASE_THEN", i, start(when.getReturn_expr()), end(when.getReturn_expr()), clause);
2085                }
2086            }
2087            if (k.getElse_expr() != null) {
2088                addExpression("CASE_ELSE", 0, start(k.getElse_expr()), end(k.getElse_expr()), clause);
2089            }
2090        }
2091        ExpressionTree tree = new ExpressionTree(exprs, outer);
2092        for (TFunctionCall call : calls) {
2093            if (insideAny(start(call), outer)) {
2094                continue;
2095            }
2096            List<int[]> excluded = new ArrayList<int[]>();
2097            TWindowDef over = call.getWindowDef();
2098            if (over != null && start(over) >= 0) {
2099                int[] window = {start(over), end(over)};
2100                excluded.add(window);
2101                if (over.getWithinGroup() != null && !over.isIncludingOverClause()) {
2102                    // SQL Server keeps WITHIN GROUP in the window definition
2103                    markUncovered("WITHIN GROUP", window[0], window[1]);
2104                } else if (over.getName() == null || over.getPartitionClause() != null || over.getOrderBy() != null) {
2105                    Construct w = addExpression("WINDOW", 0, window[0], window[1], clause);
2106                    windowParts(w, over.getPartitionClause(), over.getOrderBy());
2107                }
2108            }
2109            if (call.getFilterClause() != null) {
2110                markUncovered("AGGREGATE FILTER", call.getFilterClause());
2111                excluded.add(new int[] {start(call.getFilterClause()), end(call.getFilterClause())});
2112            }
2113            if (call.getWithinGroup() != null) {
2114                markUncovered("WITHIN GROUP", call.getWithinGroup());
2115                excluded.add(new int[] {start(call.getWithinGroup()), end(call.getWithinGroup())});
2116            }
2117            if (call.getTrimArgument() != null) {
2118                int k = 0;
2119                TExpression[] trimOperands = {call.getTrimArgument().getTrimCharacter(),
2120                    call.getTrimArgument().getStringExpression()};
2121                for (TExpression operand : trimOperands) {
2122                    if (operand != null && start(operand) >= 0) {
2123                        addExpression("FUNCTION_ARG", k++, start(operand), end(operand), clause);
2124                    }
2125                }
2126                // the visitor does not walk the trim character: its calls, CASEs and subqueries get their steps here
2127                if (call.getTrimArgument().getTrimCharacter() != null) {
2128                    expressions(clause, call.getTrimArgument().getTrimCharacter());
2129                }
2130                continue;
2131            }
2132            List<TExpression> operands = tree.operandsOf(call, excluded);
2133            for (int k = 0; k < operands.size(); k++) {
2134                addExpression("FUNCTION_ARG", k, start(operands.get(k)), end(operands.get(k)), clause);
2135            }
2136        }
2137    }
2138
2139    /**
2140     * The TExpression nodes of one clause as a containment forest (their text ranges nest), so the operands of a
2141     * call are the children of its own expression node: no scan of every expression per call.
2142     */
2143    private final class ExpressionTree {
2144        private final Map<TExpression, List<TExpression>> children = new IdentityHashMap<TExpression, List<TExpression>>();
2145        private final Map<Long, TExpression> byRange = new HashMap<Long, TExpression>();
2146
2147        private final List<TExpression> all;
2148
2149        ExpressionTree(List<TExpression> exprs, List<TSelectSqlStatement> outer) {
2150            List<TExpression> list = new ArrayList<TExpression>();
2151            final Map<TExpression, Integer> order = new IdentityHashMap<TExpression, Integer>();
2152            for (TExpression e : exprs) {
2153                // one node per object: a node listed twice would be its own child, and operandsOf would never end
2154                if (start(e) >= 0 && end(e) >= start(e) && !insideAny(start(e), outer) && !order.containsKey(e)) {
2155                    order.put(e, Integer.valueOf(list.size()));
2156                    list.add(e);
2157                }
2158            }
2159            Collections.sort(list, new Comparator<TExpression>() {
2160                @Override
2161                public int compare(TExpression a, TExpression b) {
2162                    int c = Integer.compare(start(a), start(b));
2163                    if (c != 0) {
2164                        return c;
2165                    }
2166                    c = Integer.compare(end(b), end(a));
2167                    return c != 0 ? c : order.get(a).compareTo(order.get(b));
2168                }
2169            });
2170            this.all = list;
2171            Deque<TExpression> stack = new ArrayDeque<TExpression>();
2172            for (TExpression e : list) {
2173                while (!stack.isEmpty() && !(start(stack.peek()) <= start(e) && end(e) <= end(stack.peek()))) {
2174                    stack.pop();
2175                }
2176                if (!stack.isEmpty()) {
2177                    List<TExpression> kids = children.get(stack.peek());
2178                    if (kids == null) {
2179                        kids = new ArrayList<TExpression>();
2180                        children.put(stack.peek(), kids);
2181                    }
2182                    kids.add(e);
2183                }
2184                stack.push(e);
2185                Long key = Long.valueOf(((long) start(e) << 32) | (end(e) & 0xffffffffL));
2186                if (!byRange.containsKey(key) && e.getExpressionType() == EExpressionType.function_t) {
2187                    byRange.put(key, e);
2188                }
2189            }
2190        }
2191
2192        /**
2193         * The outermost expressions strictly inside a range, for a call whose own expression node is not in the
2194         * tree (its range differs from the call's): a scan of the clause, used only then.
2195         */
2196        private List<TExpression> maximalWithin(int from, int to) {
2197            List<TExpression> out = new ArrayList<TExpression>();
2198            int lastEnd = -1;
2199            for (TExpression e : all) { // sorted by start, then end descending
2200                int es = start(e);
2201                int ee = end(e);
2202                if (es < from || ee > to || (es == from && ee == to) || es < lastEnd) {
2203                    continue;
2204                }
2205                out.add(e);
2206                lastEnd = ee;
2207            }
2208            return out;
2209        }
2210
2211        /** The expression operands of a call, in text order (Section 5, item 1). */
2212        List<TExpression> operandsOf(TFunctionCall call, List<int[]> excluded) {
2213            List<TExpression> out = new ArrayList<TExpression>();
2214            int callStart = start(call);
2215            int callEnd = end(call);
2216            int nameEnd = end(call.getFunctionName());
2217            TExpression self = byRange.get(Long.valueOf(((long) callStart << 32) | (callEnd & 0xffffffffL)));
2218            List<TExpression> candidates = self != null ? children.get(self) : null;
2219            if (candidates == null) {
2220                candidates = maximalWithin(callStart, callEnd);
2221            }
2222            // a child with the same range as the call is the call itself again (a wrapper): use its children
2223            while (candidates.size() == 1 && start(candidates.get(0)) == callStart && end(candidates.get(0)) == callEnd) {
2224                List<TExpression> next = children.get(candidates.get(0));
2225                if (next == null) {
2226                    return out;
2227                }
2228                candidates = next;
2229            }
2230            String name = functionName(call);
2231            for (TExpression e : candidates) {
2232                int es = start(e);
2233                if (es < nameEnd || end(e) > callEnd || insideAnyRange(es, excluded) || isWildcard(e)
2234                        || isKeywordOnly(e)) {
2235                    continue;
2236                }
2237                out.add(e);
2238            }
2239            if (!out.isEmpty() && FIRST_IS_DATE_PART.contains(name) && isDatePart(out.get(0))) {
2240                out.remove(0);
2241            } else if (!out.isEmpty() && LAST_IS_DATE_PART.contains(name) && isDatePart(out.get(out.size() - 1))) {
2242                out.remove(out.size() - 1);
2243            }
2244            return out;
2245        }
2246    }
2247
2248    private static String functionName(TFunctionCall call) {
2249        if (call.getFunctionName() == null) {
2250            return "";
2251        }
2252        String name = call.getFunctionName().getObjectString();
2253        if (name == null || name.isEmpty()) {
2254            name = call.getFunctionName().toString();
2255        }
2256        return name.toUpperCase(Locale.ROOT); // non-identifier-compare: built-in function names
2257    }
2258
2259    private static boolean insideAnyRange(int offset, List<int[]> ranges) {
2260        for (int[] r : ranges) {
2261            if (r[0] <= offset && offset < r[1]) {
2262                return true;
2263            }
2264        }
2265        return false;
2266    }
2267
2268    /** The {@code *} of {@code COUNT(*)}: a wildcard, decided by the parse (its part token is an asterisk). */
2269    private static boolean isWildcard(TExpression e) {
2270        if (e.getStartToken() != null && e.getStartToken() == e.getEndToken()
2271                && e.getStartToken().tokentype == ETokenType.ttasterisk) {
2272            return true;
2273        }
2274        TObjectName name = e.getObjectOperand();
2275        return e.getExpressionType() == EExpressionType.simple_object_name_t && name != null
2276                && name.getPartToken() != null && name.getPartToken().tokentype == ETokenType.ttasterisk;
2277    }
2278
2279    /** A date-part name such as {@code day} in {@code DATEADD(day, n, d)}: an unqualified name, not an operand. */
2280    private static boolean isDatePart(TExpression e) {
2281        if (e.getExpressionType() != EExpressionType.simple_object_name_t || e.getObjectOperand() == null) {
2282            return false;
2283        }
2284        TObjectName name = e.getObjectOperand();
2285        if (name.getObjectToken() != null || e.getStartToken() != e.getEndToken()) {
2286            return false;
2287        }
2288        String text = e.getStartToken().getAstext();
2289        return text != null && DATE_PARTS.contains(text.toUpperCase(Locale.ROOT)); // non-identifier-compare: keyword
2290    }
2291
2292    /** A date-part or similar keyword that the grammar wraps in an expression is not an operand. */
2293    private static boolean isKeywordOnly(TExpression e) {
2294        TSourceToken st = e.getStartToken();
2295        TSourceToken et = e.getEndToken();
2296        if (st == null || st != et) {
2297            return false;
2298        }
2299        return st.tokentype == ETokenType.ttkeyword
2300                && e.getExpressionType() != EExpressionType.simple_object_name_t
2301                && e.getExpressionType() != EExpressionType.simple_constant_t;
2302    }
2303
2304    private void windowParts(Construct window, TParseTreeNode partition, TParseTreeNode orderBy) {
2305        if (window == null) {
2306            return;
2307        }
2308        if (partition != null) {
2309            Construct p = add("PARTITION_BY", 0, partition, window);
2310            if (p != null && window.scope == null) {
2311                expressions(p, partition); // a named window: its own clause scope
2312            }
2313        }
2314        if (orderBy != null) {
2315            Construct o = add("ORDER_BY", 0, orderBy, window);
2316            if (o != null && window.scope == null) {
2317                expressions(o, orderBy);
2318            }
2319        }
2320    }
2321
2322    private void insert(TInsertSqlStatement stmt) {
2323        with(stmt, root);
2324        if (stmt.isMultiTableInsert()) {
2325            // INSERT ALL / INSERT FIRST: sqllr-path-1 has no step for an INTO clause or its WHEN condition (Section 2,
2326            // item 3); the parse's target table is one of the INTO targets, never TARGET[0] (plan 09 slice 5)
2327            uncoveredInto(stmt.getInsertIntoValues());
2328            TPTNodeList conditions = stmt.getInsertConditions();
2329            for (int i = 0; conditions != null && i < conditions.size(); i++) {
2330                Object condition = conditions.getElement(i);
2331                if (condition instanceof TInsertCondition) {
2332                    markUncovered("MULTI-TABLE INSERT WHEN", (TInsertCondition) condition);
2333                }
2334            }
2335            uncoveredInto(stmt.getElseIntoValues());
2336            if (stmt.getSubQuery() != null) {
2337                Construct select = add("SELECT", 0, stmt.getSubQuery(), root);
2338                if (select != null) {
2339                    work.push(new Object[] {stmt.getSubQuery(), select});
2340                }
2341            }
2342            return;
2343        }
2344        if (stmt.getTargetTable() != null) {
2345            add("TARGET", 0, stmt.getTargetTable(), root);
2346        }
2347        if (stmt.getColumnList() != null && stmt.getColumnList().size() > 0) {
2348            add("COLUMN_LIST", 0, stmt.getColumnList(), root);
2349        }
2350        if (stmt.getSubQuery() != null) {
2351            Construct select = add("SELECT", 0, stmt.getSubQuery(), root);
2352            if (select != null) {
2353                work.push(new Object[] {stmt.getSubQuery(), select});
2354            }
2355        }
2356        if (stmt.getValues() != null) {
2357            for (int r = 0; r < stmt.getValues().size(); r++) {
2358                TMultiTarget row = stmt.getValues().getMultiTarget(r);
2359                expressions(add("VALUES", r, row, root), row);
2360            }
2361        }
2362        if (stmt.getExecuteStmt() != null && isProcedureExecute(stmt.getExecuteStmt())) {
2363            // INSERT ... EXEC p args: the call is EXECUTE[0] below INSERT[0], its arguments below it (slice 12)
2364            Construct call = add("EXECUTE", 0, stmt.getExecuteStmt(), root);
2365            if (call != null) {
2366                callArgumentSteps(stmt.getExecuteStmt(), call);
2367            }
2368        }
2369        returnedRows(stmt);
2370    }
2371
2372    /** The arguments of a call as FUNCTION_ARG[k] below its step, in text order, with their expression steps. */
2373    private void callArgumentSteps(TCustomSqlStatement call, Construct parent) {
2374        List<TExpression> args = callArguments(call);
2375        for (int k = 0; k < args.size(); k++) {
2376            if (args.get(k) != null) {
2377                expressions(add("FUNCTION_ARG", k, args.get(k), parent), args.get(k));
2378            }
2379        }
2380    }
2381
2382    /** The RETURNING or OUTPUT clause RETURNING[0] covers (returnedRows), or null. */
2383    private TParseTreeNode ownReturning;
2384
2385    /**
2386     * The rows an INSERT, UPDATE or DELETE returns (PATH-V1 Section 3.4, plan 09 Section 10 Q6): RETURNING[0] below
2387     * the statement step over its RETURNING (PostgreSQL, SQLite, Oracle's RETURNING ... INTO) or OUTPUT (SQL Server)
2388     * clause, with the items' expression steps below it; for OUTPUT ... INTO h, the table as TARGET[0] and its column
2389     * list as COLUMN_LIST[0] below RETURNING[0]. Oracle's INTO variables are names at RETURNING[0] itself. A statement
2390     * the parse gives both clauses is left uncovered.
2391     */
2392    private void returnedRows(TCustomSqlStatement stmt) {
2393        TOutputClause output = stmt.getOutputClause();
2394        TReturningClause returning = stmt.getReturningClause();
2395        if (output != null && returning != null) {
2396            return;
2397        }
2398        if (output != null) {
2399            Construct c = add("RETURNING", 0, output, root);
2400            if (c == null) {
2401                return;
2402            }
2403            ownReturning = output;
2404            if (output.getSelectItemList() != null) {
2405                expressions(c, output.getSelectItemList());
2406            }
2407            if (output.getIntoTable() != null) {
2408                add("TARGET", 0, output.getIntoTable(), c);
2409            }
2410            if (output.getIntoColumnList() != null && output.getIntoColumnList().size() > 0) {
2411                add("COLUMN_LIST", 0, output.getIntoColumnList(), c);
2412            }
2413        } else if (returning != null) {
2414            Construct c = add("RETURNING", 0, returning, root);
2415            if (c == null) {
2416                return;
2417            }
2418            ownReturning = returning;
2419            if (returning.getResultExprList() != null) {
2420                expressions(c, returning.getResultExprList());
2421            }
2422            if (returning.getColumnValueList() != null) {
2423                expressions(c, returning.getColumnValueList());
2424            }
2425            if (returning.getVariableList() != null) {
2426                expressions(c, returning.getVariableList());
2427            }
2428        }
2429    }
2430
2431    /**
2432     * Every RETURNING and OUTPUT clause of the statement but the one RETURNING[0] covers is not covered: a MERGE's, a
2433     * nested data-changing statement's (PostgreSQL WITH d AS (DELETE ... RETURNING ...)).
2434     */
2435    private void returning(TCustomSqlStatement stmt) {
2436        final int own = ownReturning == null ? -1 : start(ownReturning);
2437        stmt.acceptChildren(new TParseTreeVisitor() {
2438            @Override
2439            public void preVisit(TReturningClause returning) {
2440                if (start(returning) != own && !ownedReturnings.contains(Integer.valueOf(start(returning)))) {
2441                    markUncovered("RETURNING", returning);
2442                }
2443            }
2444
2445            @Override
2446            public void preVisit(TOutputClause output) {
2447                if (start(output) != own) {
2448                    markUncovered("OUTPUT", output);
2449                }
2450            }
2451        });
2452        if (stmt.getOutputClause() != null && start(stmt.getOutputClause()) != own) {
2453            markUncovered("OUTPUT", stmt.getOutputClause());
2454        }
2455    }
2456
2457    private void update(TUpdateSqlStatement stmt) {
2458        with(stmt, root);
2459        int setAt = offsetOf(statementKeywordAfter(stmt, "SET", -1));
2460        int fromAt = offsetOf(statementKeywordAfter(stmt, "FROM", setAt));
2461        TJoinList joins = stmt.getJoins();
2462        int[] joinRange = joins == null || joins.size() == 0 ? null : joinListRange(joins);
2463        if (joinRange != null && fromAt >= 0 && joinRange[0] > fromAt) {
2464            // UPDATE t SET ... FROM u: GSP keeps the FROM list in getJoins()
2465            fromClause("FROM", joins, fromAt, root);
2466            if (stmt.getTargetTable() != null) {
2467                dmlTarget(stmt.getTargetTable());
2468            }
2469        } else if (joinRange != null && (setAt < 0 || joinRange[0] < setAt)) {
2470            // UPDATE t1, t2 SET / UPDATE t1 JOIN t2 ON ... SET: the target list
2471            fromClause("TARGET", joins, -1, root);
2472        } else if (stmt.getTargetTable() != null) {
2473            dmlTarget(stmt.getTargetTable());
2474        }
2475        if (stmt.getReferenceJoins() != null && stmt.getReferenceJoins().size() > 0) {
2476            fromClause("FROM", stmt.getReferenceJoins(), fromAt, root);
2477        }
2478        if (stmt.getResultColumnList() != null) {
2479            int st = setAt >= 0 ? setAt : start(stmt.getResultColumnList());
2480            expressions(add("SET", 0, st, end(stmt.getResultColumnList()), root), stmt.getResultColumnList());
2481        }
2482        if (stmt.getWhereClause() != null) {
2483            expressions(add("WHERE", 0, stmt.getWhereClause(), root), stmt.getWhereClause());
2484        }
2485        dmlRowLimit(stmt, stmt.getOrderByClause(), stmt.getLimitClause());
2486        returnedRows(stmt);
2487    }
2488
2489    /**
2490     * The ORDER BY and the row limit (LIMIT, TOP) that choose the rows an UPDATE or DELETE changes (MySQL {@code ...
2491     * ORDER BY b LIMIT 10}, SQL Server {@code TOP (@n)}): ORDER_BY[0] and LIMIT[0] below the statement step, one LIMIT
2492     * step for every form of the limit (PATH-V1 Section 3.4, plan 09 slice 9-rest).
2493     */
2494    private void dmlRowLimit(TCustomSqlStatement stmt, gudusoft.gsqlparser.lineage.v2.shape.TOrderBy orderBy,
2495            TParseTreeNode limit) {
2496        if (orderBy != null) {
2497            expressions(add("ORDER_BY", 0, orderBy, root), orderBy);
2498        }
2499        for (TParseTreeNode part : new TParseTreeNode[] {stmt.getTopClause(), limit}) {
2500            if (part != null) {
2501                expressions(add("LIMIT", 0, part, root), part);
2502            }
2503        }
2504    }
2505
2506    /**
2507     * The token a DELETE writes its target with: the first word after DELETE that is not FROM, ONLY, a TOP clause or
2508     * a MySQL modifier ({@code DELETE a FROM t a}: {@code a}; {@code DELETE FROM d.t}: {@code d}). Shared by the paths
2509     * (TARGET[0]) and the statement graph (the anchor of the DELETE's effect), so both read one rule.
2510     */
2511    static TSourceToken deleteTarget(TDeleteSqlStatement stmt) {
2512        TSourceToken t = stmt.getStartToken();
2513        TSourceToken last = stmt.getEndToken();
2514        boolean afterDelete = false;
2515        boolean afterOnly = false;
2516        int depth = 0;
2517        int targetDepth = 0;
2518        while (t != null) {
2519            String s = t.getAstext();
2520            if ("(".equals(s)) {
2521                if (afterDelete && afterOnly && depth == 0) {
2522                    targetDepth = 1; // PostgreSQL DELETE FROM ONLY (t): the target is inside the parentheses
2523                }
2524                depth++;
2525            } else if (")".equals(s)) {
2526                depth--;
2527            } else if (depth == targetDepth && t.tokentype != gudusoft.gsqlparser.ETokenType.ttwhitespace
2528                    && t.tokentype != gudusoft.gsqlparser.ETokenType.ttreturn
2529                    && t.tokentype != gudusoft.gsqlparser.ETokenType.ttsimplecomment
2530                    && t.tokentype != gudusoft.gsqlparser.ETokenType.ttbracketedcomment) {
2531                if (!afterDelete) {
2532                    // Teradata abbreviates DELETE as DEL (Codex slice 9 #9)
2533                    afterDelete = "DELETE".equalsIgnoreCase(s) // non-identifier-compare: SQL keyword text
2534                            || "DEL".equalsIgnoreCase(s); // non-identifier-compare: SQL keyword text
2535                } else if (t.tokentype == gudusoft.gsqlparser.ETokenType.ttidentifier
2536                        || !isDeleteModifier(s)) {
2537                    return t;
2538                } else {
2539                    afterOnly = "ONLY".equalsIgnoreCase(s); // non-identifier-compare: SQL keyword text
2540                }
2541            }
2542            if (t == last) {
2543                break;
2544            }
2545            t = t.getNextTokenInChain();
2546        }
2547        return null;
2548    }
2549
2550    private static boolean isDeleteModifier(String s) {
2551        for (String m : new String[] {"FROM", "ONLY", "TOP", "PERCENT", "LOW_PRIORITY", "QUICK", "IGNORE"}) {
2552            if (m.equalsIgnoreCase(s)) { // non-identifier-compare: SQL keyword text
2553                return true;
2554            }
2555        }
2556        return false;
2557    }
2558
2559    /** The first statement-level keyword token after a UTF-16 offset (or anywhere when the offset is negative). */
2560    private TSourceToken statementKeywordAfter(TCustomSqlStatement stmt, String keyword, int after) {
2561        TSourceToken t = stmt.getStartToken();
2562        TSourceToken last = stmt.getEndToken();
2563        int depth = 0;
2564        while (t != null) {
2565            String s = t.getAstext();
2566            if ("(".equals(s)) {
2567                depth++;
2568            } else if (")".equals(s)) {
2569                depth--;
2570            } else if (depth == 0 && keyword.equalsIgnoreCase(s) && offsetOf(t) > after) { // non-identifier-compare: SQL keyword text
2571                return t;
2572            }
2573            if (t == last) {
2574                break;
2575            }
2576            t = t.getNextTokenInChain();
2577        }
2578        return null;
2579    }
2580
2581    /**
2582     * TARGET[0] of an UPDATE, DELETE or MERGE, and below it SUBQUERY[0] when the target is a query (an inline view:
2583     * Oracle UPDATE (SELECT ...) v, DELETE FROM (SELECT ...), MERGE INTO (SELECT ...) d; PATH-V1 Section 3.4, slice 6).
2584     */
2585    private Construct dmlTarget(TTable target) {
2586        Construct c = add("TARGET", 0, target, root);
2587        if (c != null && target.getSubquery() != null) {
2588            Construct sub = add("SUBQUERY", 0, target.getSubquery(), c);
2589            if (sub != null) {
2590                work.push(new Object[] {target.getSubquery(), sub});
2591            }
2592        }
2593        return c;
2594    }
2595
2596    private void uncoveredInto(TPTNodeList list) {
2597        for (int i = 0; list != null && i < list.size(); i++) {
2598            Object into = list.getElement(i);
2599            if (into instanceof TInsertIntoValue) {
2600                markUncovered("MULTI-TABLE INSERT INTO", (TInsertIntoValue) into);
2601            }
2602        }
2603    }
2604
2605    private void delete(TDeleteSqlStatement stmt) {
2606        with(stmt, root);
2607        TJoinList list = stmt.getReferenceJoins() != null && stmt.getReferenceJoins().size() > 0
2608                ? stmt.getReferenceJoins() : stmt.getJoins();
2609        int[] range = list == null || list.size() == 0 ? null : joinListRange(list);
2610        int targetStart = start(stmt.getTargetTable());
2611        if (range != null && targetStart >= range[0] && targetStart < range[1]) {
2612            // SQL Server, Teradata DELETE a FROM t a JOIN ...: the parse's target is the FROM item the written target
2613            // names, so TARGET[0] is the name written after DELETE and the FROM list is the FROM clause (slice 9)
2614            TSourceToken written = deleteTarget(stmt);
2615            int writtenAt = offsetOf(written);
2616            if (written != null && writtenAt < range[0]) {
2617                add("TARGET", 0, writtenAt, writtenAt + written.getAstext().length(), root);
2618            }
2619            TSourceToken from = null;
2620            for (TSourceToken t = statementKeywordAfter(stmt, "FROM", -1); t != null && offsetOf(t) < range[0];
2621                    t = statementKeywordAfter(stmt, "FROM", offsetOf(t))) {
2622                from = t; // the last FROM before the list
2623            }
2624            fromClause("FROM", list, offsetOf(from), root);
2625            if (stmt.getWhereClause() != null) {
2626                expressions(add("WHERE", 0, stmt.getWhereClause(), root), stmt.getWhereClause());
2627            }
2628            dmlRowLimit(stmt, stmt.getOrderByClause(), stmt.getLimitClause());
2629            returnedRows(stmt);
2630            return;
2631        }
2632        int targetEnd = end(stmt.getTargetTable());
2633        int[] targetList = list == stmt.getReferenceJoins() && stmt.getJoins() != null && stmt.getJoins().size() > 1
2634                ? joinListRange(stmt.getJoins()) : null;
2635        if (targetList != null && range != null && targetList[1] <= range[0]) {
2636            // MySQL DELETE t1, t2 FROM ... / DELETE FROM t1, t2 USING ...: the list of tables whose rows are deleted
2637            // is TARGET[0], as an UPDATE's table list is (PATH-V1 Section 3.4, plan 09 slice 9-rest)
2638            add("TARGET", 0, targetList[0], targetList[1], root);
2639            targetEnd = targetList[1];
2640        } else if (stmt.getTargetTable() != null) {
2641            dmlTarget(stmt.getTargetTable());
2642        }
2643        if (range != null && range[0] >= targetEnd) {
2644            int usingAt = offsetOf(statementKeywordAfter(stmt, "USING", targetEnd - 1));
2645            int fromAt = offsetOf(statementKeywordAfter(stmt, "FROM", targetEnd - 1));
2646            boolean using = usingAt >= 0 && usingAt < range[0] && (fromAt < 0 || usingAt > fromAt || fromAt > range[0]);
2647            fromClause(using ? "USING" : "FROM", list, using ? usingAt : fromAt, root);
2648        }
2649        if (stmt.getWhereClause() != null) {
2650            expressions(add("WHERE", 0, stmt.getWhereClause(), root), stmt.getWhereClause());
2651        }
2652        dmlRowLimit(stmt, stmt.getOrderByClause(), stmt.getLimitClause());
2653        returnedRows(stmt);
2654    }
2655
2656    private void merge(TMergeSqlStatement stmt) {
2657        with(stmt, root);
2658        Construct target = stmt.getTargetTable() == null ? null : dmlTarget(stmt.getTargetTable());
2659        TTable using = stmt.getUsingTable();
2660        if (using != null) {
2661            String form = uncoveredTableForm(using);
2662            if (target != null && start(using) == target.start && end(using) == target.end) {
2663                markUncovered("MERGE SOURCE (range equals the target)", using);
2664            } else if (form != null) {
2665                markUncovered(form, using);
2666            } else {
2667                Construct u = add("USING", 0, keywordStart(using, "USING"), end(using), root);
2668                if (u != null && using.getSubquery() != null) {
2669                    Construct sub = add("SUBQUERY", 0, using.getSubquery(), u);
2670                    if (sub != null) {
2671                        work.push(new Object[] {using.getSubquery(), sub});
2672                    }
2673                }
2674            }
2675        }
2676        if (stmt.getCondition() != null) {
2677            expressions(add("ON", 0, stmt.getCondition(), root), stmt.getCondition());
2678        }
2679        if (stmt.getWhenClauses() != null) {
2680            for (int w = 0; w < stmt.getWhenClauses().size(); w++) {
2681                TMergeWhenClause when = (TMergeWhenClause) stmt.getWhenClauses().getElement(w);
2682                Construct wc = add("WHEN", w, when, root);
2683                if (wc == null) {
2684                    continue;
2685                }
2686                if (when.getCondition() != null) {
2687                    expressions(wc, when.getCondition());
2688                }
2689                // Oracle writes a WHEN clause's condition after its action (UPDATE SET ... WHERE c, DELETE WHERE c,
2690                // INSERT ... VALUES (...) WHERE c): conditions of the WHEN clause, directly below it (slice 6)
2691                if (when.getUpdateClause() != null && when.getUpdateClause().getUpdateWhereClause() != null) {
2692                    expressions(wc, when.getUpdateClause().getUpdateWhereClause());
2693                }
2694                if (when.getUpdateClause() != null && when.getUpdateClause().getDeleteWhereClause() != null) {
2695                    expressions(wc, when.getUpdateClause().getDeleteWhereClause());
2696                }
2697                if (when.getInsertClause() != null && when.getInsertClause().getInsertWhereClause() != null) {
2698                    expressions(wc, when.getInsertClause().getInsertWhereClause());
2699                }
2700                if (when.getUpdateClause() != null && when.getUpdateClause().getUpdateColumnList() != null) {
2701                    expressions(add("SET", 0, when.getUpdateClause().getUpdateColumnList(), wc),
2702                            when.getUpdateClause().getUpdateColumnList());
2703                }
2704                if (when.getInsertClause() != null) {
2705                    if (when.getInsertClause().getColumnList() != null) {
2706                        add("COLUMN_LIST", 0, when.getInsertClause().getColumnList(), wc);
2707                    }
2708                    if (when.getInsertClause().getValuelist() != null) {
2709                        expressions(add("VALUES", 0, when.getInsertClause().getValuelist(), wc),
2710                                when.getInsertClause().getValuelist());
2711                    }
2712                }
2713            }
2714        }
2715        if (stmt.getReturningClause() != null) {
2716            markUncovered("RETURNING", stmt.getReturningClause());
2717        }
2718    }
2719
2720    private void createTable(TCreateTableSqlStatement stmt) {
2721        with(stmt, root);
2722        if (stmt.getTargetTable() != null) {
2723            add("TARGET", 0, stmt.getTargetTable(), root);
2724        }
2725        if (stmt.getSubQuery() == null) {
2726            List<Source> sources = createSources(stmt);
2727            for (int s = 0; s < sources.size(); s++) {
2728                Source source = sources.get(s);
2729                TSourceToken last = source.last;
2730                int end = offsetOf(last);
2731                add("SOURCE", s, offsetOf(source.first), end < 0 ? -1
2732                        : end + (last.getAstext() == null ? 0 : last.getAstext().length()), root);
2733            }
2734            return;
2735        }
2736        if (stmt.getColumnList() != null && stmt.getColumnList().size() > 0
2737                && start(stmt.getColumnList()) < start(stmt.getSubQuery())) {
2738            add("COLUMN_LIST", 0, stmt.getColumnList(), root);
2739        }
2740        Construct select = add("SELECT", 0, stmt.getSubQuery(), root);
2741        if (select != null) {
2742            work.push(new Object[] {stmt.getSubQuery(), select});
2743        }
2744    }
2745
2746    /**
2747     * One source a CREATE TABLE without a query copies or reads (PATH-V1 {@code SOURCE[s]}, plan 09 slice 2): the
2748     * relation of a CLONE ({@link #name}), or a storage location literal ({@link #literal} as written).
2749     */
2750    static final class Source {
2751        final TSourceToken first;
2752        final TSourceToken last;
2753        final TObjectName name;
2754        final String literal;
2755
2756        Source(TSourceToken first, TSourceToken last, TObjectName name, String literal) {
2757            this.first = first;
2758            this.last = last;
2759            this.name = name;
2760            this.literal = literal;
2761        }
2762    }
2763
2764    /**
2765     * The sources of a CREATE TABLE without a query, in text order: the CLONE source; the LOCATION or
2766     * external_location literal; each URI literal of BigQuery {@code uris} (found among the option's string tokens,
2767     * as the parse keeps the list as text). The producer reads its sources in this order (StatementGraph), so the
2768     * index of {@code SOURCE[s]} and the source a statement reads agree.
2769     */
2770    static List<Source> createSources(TCreateTableSqlStatement stmt) {
2771        List<Source> out = new ArrayList<Source>();
2772        if (stmt.getSubQuery() != null) {
2773            return out;
2774        }
2775        TObjectName clone = stmt.getCloneSourceTable();
2776        if (clone != null && clone.getStartToken() != null) {
2777            out.add(new Source(clone.getStartToken(), clone.getEndToken(), clone, null));
2778        }
2779        TObjectName location = stmt.getTableLocation();
2780        if (location != null && location.getStartToken() != null) {
2781            TSourceToken t = location.getStartToken();
2782            out.add(new Source(t, t, null, t.getAstext()));
2783        }
2784        for (int i = 0; stmt.getTableOptions() != null && i < stmt.getTableOptions().size(); i++) {
2785            TCreateTableOption option = stmt.getTableOptions().get(i);
2786            if (option.getCreateTableOptionType() != gudusoft.gsqlparser.ECreateTableOption.etoBigQueryExternal
2787                    || option.getUris() == null || option.getStartToken() == null || option.getEndToken() == null) {
2788                continue;
2789            }
2790            List<String> uris = option.getUris();
2791            TSourceToken t = option.getStartToken();
2792            int k = 0;
2793            while (t != null && k < uris.size()) {
2794                if ((t.tokentype == ETokenType.ttsqstring || t.tokentype == ETokenType.ttdqstring)
2795                        && uris.get(k).equals(t.getAstext())) { // token text: the parse keeps each URI as written
2796                    out.add(new Source(t, t, null, t.getAstext()));
2797                    k++;
2798                }
2799                if (t == option.getEndToken()) {
2800                    break;
2801                }
2802                t = t.getNextTokenInChain();
2803            }
2804        }
2805        TSourceToken path = optionsPath(stmt);
2806        if (path != null) {
2807            out.add(new Source(path, path, null, path.getAstext()));
2808        }
2809        Collections.sort(out, new java.util.Comparator<Source>() {
2810            @Override
2811            public int compare(Source a, Source b) {
2812                return Integer.compare(a.first.posinlist, b.first.posinlist);
2813            }
2814        });
2815        return out;
2816    }
2817
2818    /**
2819     * The literal of the {@code path} option of {@code OPTIONS (...)} (Spark SQL, Databricks
2820     * {@code CREATE TABLE ... USING <format> OPTIONS (path '...')}): where the table's files are. The parse keeps the
2821     * options as text, so they are read from the tokens before the statement's query, if it has one.
2822     */
2823    static TSourceToken optionsPath(TCreateTableSqlStatement stmt) {
2824        TSourceToken end = stmt.getSubQuery() != null && stmt.getSubQuery().getStartToken() != null
2825                ? stmt.getSubQuery().getStartToken() : stmt.getEndToken();
2826        for (TSourceToken t = stmt.getStartToken(); t != null; t = t.getNextTokenInChain()) {
2827            TSourceToken open = "OPTIONS".equalsIgnoreCase(t.getAstext()) ? nextSolidToken(t) : null; // non-identifier-compare: keyword
2828            if (open != null && "(".equals(open.getAstext())) {
2829                int depth = 0;
2830                for (TSourceToken u = open; u != null && u != end; u = u.getNextTokenInChain()) {
2831                    if ("(".equals(u.getAstext())) {
2832                        depth++;
2833                    } else if (")".equals(u.getAstext())) {
2834                        if (--depth == 0) {
2835                            break;
2836                        }
2837                    } else if (depth == 1 && isPathKey(u)) {
2838                        TSourceToken v = nextSolidToken(u);
2839                        if (v != null && "=".equals(v.getAstext())) {
2840                            v = nextSolidToken(v);
2841                        }
2842                        if (v != null && (v.tokentype == ETokenType.ttsqstring || v.tokentype == ETokenType.ttdqstring)) {
2843                            return v;
2844                        }
2845                    }
2846                }
2847            }
2848            if (t == end) {
2849                break;
2850            }
2851        }
2852        return null;
2853    }
2854
2855    /** An option key {@code path}, written as a word or as a string ({@code 'path'}). */
2856    private static boolean isPathKey(TSourceToken t) {
2857        String text = t.getAstext();
2858        if (text == null) {
2859            return false;
2860        }
2861        if (text.length() == 6 && (text.charAt(0) == '\'' || text.charAt(0) == '"') && text.charAt(5) == text.charAt(0)) {
2862            text = text.substring(1, 5);
2863        }
2864        return "PATH".equalsIgnoreCase(text); // non-identifier-compare: an option key, not an object name
2865    }
2866
2867    private static TSourceToken nextSolidToken(TSourceToken token) {
2868        TSourceToken t = token == null ? null : token.getNextTokenInChain();
2869        while (t != null && (t.tokentype == ETokenType.ttwhitespace || t.tokentype == ETokenType.ttreturn
2870                || t.tokentype == ETokenType.ttsimplecomment || t.tokentype == ETokenType.ttbracketedcomment)) {
2871            t = t.getNextTokenInChain();
2872        }
2873        return t;
2874    }
2875
2876    private void createView(TCustomSqlStatement stmt, TObjectName name, TParseTreeNode columns,
2877            TSelectSqlStatement query) {
2878        with(stmt, root);
2879        if (name != null) {
2880            add("TARGET", 0, name, root);
2881        }
2882        if (columns != null) {
2883            add("COLUMN_LIST", 0, columns, root);
2884        }
2885        if (query != null) {
2886            Construct select = add("SELECT", 0, query, root);
2887            if (select != null) {
2888                work.push(new Object[] {query, select});
2889            }
2890        }
2891    }
2892
2893    // ---------------------------------------------------------------------------------------------
2894    // Finishing: parents, indexes, paths, lookup tree
2895    // ---------------------------------------------------------------------------------------------
2896
2897    /** Resolve expression parents, number the counted steps, compute every path and the lookup tree. */
2898    private void finish() {
2899        resolveExpressionParents();
2900        assignPaths();
2901        buildLookup();
2902    }
2903
2904    /** Steps that hold clauses, never a name: an identifier landing on one is in a clause this index does not know. */
2905    private static final Set<String> BLOCK_STEPS = new HashSet<String>(Arrays.asList("SELECT", "INSERT", "UPDATE",
2906            "DELETE", "MERGE", "CREATE_TABLE", "CREATE_VIEW", "SUBQUERY", "CTE", "BRANCH", "SET_OP", "WITH"));
2907
2908    /**
2909     * Marks every identifier token of the statement that lands directly on a statement or query block step as
2910     * uncovered: it belongs to a clause GSP parses but this index does not model (Oracle MODEL, FOR UPDATE OF,
2911     * BigQuery FOR SYSTEM_TIME, table options), so no path of PATH-V1 describes it.
2912     */
2913    private void markUnmodelledIdentifiers(TCustomSqlStatement stmt) {
2914        TSourceToken last = stmt.getEndToken();
2915        for (TSourceToken t = stmt.getStartToken(); t != null; t = t.getNextTokenInChain()) {
2916            ETokenType type = t.tokentype;
2917            if (type == ETokenType.ttidentifier || type == ETokenType.ttdqstring || type == ETokenType.ttdbstring
2918                    || type == ETokenType.ttbrstring) {
2919                int o = offsetOf(t);
2920                if (o >= 0) {
2921                    Construct innermost = innermost(o);
2922                    if (innermost != null && BLOCK_STEPS.contains(innermost.code)) {
2923                        markUncovered("UNMODELLED CLAUSE", o, o + t.getAstext().length());
2924                    }
2925                }
2926            }
2927            if (t == last) {
2928                break;
2929            }
2930        }
2931    }
2932
2933    private Construct innermost(int offset) {
2934        Construct best = null;
2935        List<Construct> level = roots;
2936        while (true) {
2937            Construct hit = find(level, offset);
2938            if (hit == null) {
2939                return best;
2940            }
2941            best = hit;
2942            level = hit.textChildren;
2943        }
2944    }
2945
2946    /**
2947     * Each expression step's parent is the innermost step of its clause that contains it; a stack sweep over the
2948     * clause's steps sorted by (start, end descending, rank) finds it in n log n.
2949     */
2950    private void resolveExpressionParents() {
2951        Map<Construct, List<Construct>> byClause = new IdentityHashMap<Construct, List<Construct>>();
2952        for (Construct c : constructs) {
2953            Construct clause = c.scope != null ? c.scope
2954                    : c.parent != null && "WINDOW".equals(c.parent.code) && c.parent.scope != null ? c.parent.scope : null;
2955            if (clause == null) {
2956                continue;
2957            }
2958            List<Construct> list = byClause.get(clause);
2959            if (list == null) {
2960                list = new ArrayList<Construct>();
2961                byClause.put(clause, list);
2962            }
2963            list.add(c);
2964        }
2965        for (Map.Entry<Construct, List<Construct>> entry : byClause.entrySet()) {
2966            Construct clause = entry.getKey();
2967            List<Construct> list = entry.getValue();
2968            Collections.sort(list, new Comparator<Construct>() {
2969                @Override
2970                public int compare(Construct a, Construct b) {
2971                    int c = Integer.compare(a.start, b.start);
2972                    if (c != 0) {
2973                        return c;
2974                    }
2975                    c = Integer.compare(b.end, a.end);
2976                    if (c != 0) {
2977                        return c;
2978                    }
2979                    c = Integer.compare(rank(a), rank(b));
2980                    return c != 0 ? c : Integer.compare(a.order, b.order);
2981                }
2982            });
2983            Deque<Construct> stack = new ArrayDeque<Construct>();
2984            for (Construct c : list) {
2985                while (!stack.isEmpty() && !stack.peek().containsRange(c)) {
2986                    stack.pop();
2987                }
2988                if (c.parent == null) {
2989                    c.parent = stack.isEmpty() ? clause : stack.peek();
2990                }
2991                stack.push(c);
2992            }
2993        }
2994    }
2995
2996    /** Nesting rank for equal ranges: an operand step encloses a subquery written as the whole operand. */
2997    private static int rank(Construct c) {
2998        return "SUBQUERY".equals(c.code) ? 2 : 1;
2999    }
3000
3001    private int depth(Construct c) {
3002        if (c.depth >= 0) {
3003            return c.depth;
3004        }
3005        int d = 0;
3006        for (Construct p = c.parent; p != null; p = p.parent) {
3007            d++;
3008            if (d > constructs.size()) {
3009                throw new IllegalStateException("cyclic construct parents");
3010            }
3011        }
3012        c.depth = d;
3013        return d;
3014    }
3015
3016    /**
3017     * Paths level by level; {@code SUBQUERY[k]} and {@code PIVOT[p]} count the subqueries and the operators directly
3018     * below the same step, in text order,
3019     * where "the same step" is its path (TOP, LIMIT, OFFSET and FETCH fragments are one {@code LIMIT[0]} step).
3020     */
3021    private void assignPaths() {
3022        List<Construct> ordered = new ArrayList<Construct>(constructs);
3023        final Map<Construct, Integer> depths = new IdentityHashMap<Construct, Integer>();
3024        for (Construct c : ordered) {
3025            depths.put(c, Integer.valueOf(depth(c)));
3026        }
3027        Collections.sort(ordered, new Comparator<Construct>() {
3028            @Override
3029            public int compare(Construct a, Construct b) {
3030                int c = depths.get(a).compareTo(depths.get(b));
3031                return c != 0 ? c : Integer.compare(a.start, b.start);
3032            }
3033        });
3034        int i = 0;
3035        while (i < ordered.size()) {
3036            int d = depths.get(ordered.get(i)).intValue();
3037            int j = i;
3038            while (j < ordered.size() && depths.get(ordered.get(j)).intValue() == d) {
3039                j++;
3040            }
3041            Map<String, Integer> counters = new HashMap<String, Integer>();
3042            for (int k = i; k < j; k++) {
3043                Construct c = ordered.get(k); // ordered by start within the level
3044                if (("SUBQUERY".equals(c.code) || "PIVOT".equals(c.code) || "LATERAL".equals(c.code)) && c.index < 0) {
3045                    // PIVOT[p] counts the operators below the same step, in text order (a chain: PIVOT[0], PIVOT[1])
3046                    String key = c.code + "|" + (c.parent == null ? "" : c.parent.path);
3047                    Integer n = counters.get(key);
3048                    c.index = n == null ? 0 : n.intValue();
3049                    counters.put(key, Integer.valueOf(c.index + 1));
3050                }
3051                c.path = (c.parent == null ? "" : c.parent.path + "/") + c.code + "[" + Math.max(c.index, 0) + "]";
3052            }
3053            i = j;
3054        }
3055    }
3056
3057    /** The containment forest of construct ranges, for {@link #anchor} by binary search. */
3058    private void buildLookup() {
3059        List<Construct> sorted = new ArrayList<Construct>(constructs);
3060        Collections.sort(sorted, new Comparator<Construct>() {
3061            @Override
3062            public int compare(Construct a, Construct b) {
3063                int c = Integer.compare(a.start, b.start);
3064                if (c != 0) {
3065                    return c;
3066                }
3067                c = Integer.compare(b.end, a.end);
3068                return c != 0 ? c : Integer.compare(depth(a), depth(b));
3069            }
3070        });
3071        Deque<Construct> stack = new ArrayDeque<Construct>();
3072        for (Construct c : sorted) {
3073            if (c.end <= c.start) {
3074                continue; // an empty range contains no offset
3075            }
3076            while (!stack.isEmpty() && !stack.peek().containsRange(c)) {
3077                stack.pop();
3078            }
3079            if (stack.isEmpty()) {
3080                roots.add(c);
3081            } else {
3082                stack.peek().textChildren.add(c);
3083            }
3084            stack.push(c);
3085        }
3086    }
3087
3088    /** The constructs, for tests: path, start, end. */
3089    List<String> describe() {
3090        List<String> out = new ArrayList<String>();
3091        for (Construct c : constructs) {
3092            out.add(c.path + " " + c.start + ".." + c.end);
3093        }
3094        for (Uncovered u : uncovered) {
3095            out.add("UNCOVERED " + u.form + " " + u.start + ".." + u.end);
3096        }
3097        return out;
3098    }
3099}