001package gudusoft.gsqlparser.resolver2.metadata;
002
003import gudusoft.gsqlparser.EResolverType;
004import gudusoft.gsqlparser.TCustomSqlStatement;
005import gudusoft.gsqlparser.TGSqlParser;
006import gudusoft.gsqlparser.TStatementList;
007import gudusoft.gsqlparser.nodes.TObjectName;
008import gudusoft.gsqlparser.resolver.TMetadataCollector;
009import gudusoft.gsqlparser.resolver2.context.DatabaseContextTracker;
010import gudusoft.gsqlparser.sqlenv.TSQLCatalog;
011import gudusoft.gsqlparser.sqlenv.TSQLEnv;
012import gudusoft.gsqlparser.sqlenv.TSQLSchema;
013import gudusoft.gsqlparser.sqlenv.TSQLTable;
014import gudusoft.gsqlparser.sqlenv.TemporaryTableSemantics;
015import gudusoft.gsqlparser.stmt.TCreateTableSqlStatement;
016import gudusoft.gsqlparser.stmt.TSelectSqlStatement;
017import gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement;
018
019import java.util.ArrayDeque;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Deque;
023import java.util.IdentityHashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027
028/**
029 * The tables a script declares, as the parse of each later statement sees them (Mantis 4797).
030 *
031 * <p>Phase 1 links a statement's columns as soon as the statement is parsed, but RESOLVER and resolver2 put the
032 * script's {@code CREATE TABLE} statements into the parser's catalog only after every statement was parsed, and
033 * {@link EResolverType#NONE} never does. This catalog takes each declaration when it is parsed, so Phase 1 of the
034 * statements after it reads it in every resolver type. It places a declaration where resolver2 does
035 * ({@link StatementMetadata}): in the context of the {@code USE} statements before it, and a routine's transient
036 * table in that routine's own catalog (Mantis 4715). A {@code CREATE TABLE ... AS SELECT} has the columns of its
037 * query, and a name temporary by its spelling ({@code #t}) is the same table in every database and schema.</p>
038 *
039 * <p>Only Phase 1 reads it, before the parser's catalog, and the parser drops it when the statements are parsed: the
040 * later passes register the declarations at their positions themselves, so a {@code CREATE OR REPLACE} never takes the
041 * place of a catalog table ahead of them. With no later pass the parser publishes into its catalog what resolver2
042 * would ({@link #publish}). A declaration of a table the parser's catalog already holds where it would put it (caller
043 * metadata, a DDL preload) is not copied unless it replaces it: Phase 1 reads that table, as resolver2 keeps it.</p>
044 */
045public final class ParseTimeCatalog {
046    private final TSQLEnv shared;
047    private final TStatementList statements;
048    /** The context after the top-level statements before {@link #parsed}. */
049    private final DatabaseContextTracker tracker;
050    private final Set<TCustomSqlStatement> seen =
051            Collections.newSetFromMap(new IdentityHashMap<TCustomSqlStatement, Boolean>());
052    private int parsed;
053    /** The walk of the top-level statement being parsed; null until a statement in it asks for its context. */
054    private Cursor cursor;
055    /** The script's declarations outside a routine's transient tables; created on the first one. */
056    private TSQLEnv declared;
057    private final Map<TStoredProcedureSqlStatement, TSQLEnv> routines = new IdentityHashMap<>();
058    /** What {@link #publish} replays into the parser's catalog, in order. */
059    private final List<TCreateTableSqlStatement> publishable = new ArrayList<>();
060    private final Map<TCreateTableSqlStatement, StatementMetadata.Context> contexts = new IdentityHashMap<>();
061
062    /**
063     * @param shared     the parser's catalog
064     * @param statements the parser's statements, parsed in this order
065     * @param catalog    the database the script starts in, before any of its {@code USE} statements
066     * @param schema     the schema the script starts in
067     */
068    public ParseTimeCatalog(TSQLEnv shared, TStatementList statements, String catalog, String schema) {
069        this.shared = shared;
070        this.statements = statements;
071        this.tracker = new DatabaseContextTracker(catalog, schema);
072    }
073
074    /** The catalog of the parse {@code statement} belongs to while it is running, or null. */
075    private static ParseTimeCatalog of(TCustomSqlStatement statement) {
076        TGSqlParser parser = statement == null ? null : statement.getGsqlparser();
077        return parser == null ? null : parser.getParseTimeCatalog();
078    }
079
080    /** Called before the top-level statement at {@code index} is parsed: the statements before it set the context. */
081    public void advanceTo(int index) {
082        for (; parsed < index && parsed < statements.size(); parsed++) {
083            cursor = null;
084            replay(statements.get(parsed));
085        }
086    }
087
088    /** Feed a parsed top-level statement and the statements under it to the tracker, depth first in source order. */
089    private void replay(TCustomSqlStatement root) {
090        Deque<TCustomSqlStatement> pending = new ArrayDeque<>();
091        pending.push(root);
092        while (!pending.isEmpty()) {
093            TCustomSqlStatement current = pending.pop();
094            if (!seen.add(current)) continue;
095            tracker.processStatement(current);
096            TStatementList children = current.getStatements();
097            for (int i = children.size() - 1; i >= 0; i--) {
098                if (children.get(i) != null) pending.push(children.get(i));
099            }
100        }
101    }
102
103    /**
104     * Phase 1 of a {@code CREATE TABLE}: register the declared table where the statements after it look it up.
105     */
106    public static void declare(TCreateTableSqlStatement create) {
107        ParseTimeCatalog catalog = of(create);
108        if (catalog != null && create.getTableName() != null) catalog.add(create);
109    }
110
111    /**
112     * Whether {@code SELECT ... INTO target} creates a transient table of the routine it is in ({@code INTO #t}), which
113     * resolver2 keeps in the routine's own catalog: a named routine's, not an anonymous block's, which runs as part of
114     * the script around it.
115     */
116    public static boolean isRoutineTransient(TSelectSqlStatement select, TObjectName target) {
117        return (target != null) && TemporaryTableSemantics.hasTransientTables(select.dbvendor)
118                && TemporaryTableSemantics.isTransient(TemporaryTableSemantics.ofSelectInto(select.dbvendor, target))
119                && (StatementMetadata.procedureOwner(select) != null);
120    }
121
122    /**
123     * Phase 1 of {@code SELECT ... INTO} a routine's transient table ({@link #isRoutineTransient}): register it in the
124     * routine's own catalog, as resolver2 does. Any other target is the parser's catalog's, where the statement
125     * registers it.
126     */
127    public static void declareSelectInto(TSelectSqlStatement select, TObjectName target) {
128        ParseTimeCatalog catalog = of(select);
129        if (catalog == null || !isRoutineTransient(select, target)) return;
130        StatementMetadata.Context context = catalog.contextAt(select);
131        TSQLEnv local = catalog.routineCatalog(StatementMetadata.procedureOwner(select), context);
132        StatementMetadata.declareQueryTable(local, target, select, context.catalog, context.schema, false);
133    }
134
135    /**
136     * The table Phase 1 of {@code statement} reads under {@code database.schema.table} (the qualifiers may be empty)
137     * before the parser's catalog: a transient table its routine declared before it, or a table the script declared
138     * before it. Null when there is none or no parse is running.
139     */
140    public static TSQLTable searchTable(TCustomSqlStatement statement, String database, String schema, String table) {
141        ParseTimeCatalog catalog = of(statement);
142        return (catalog == null || table == null) ? null : catalog.search(statement, database, schema, table);
143    }
144
145    /**
146     * Put the declarations into the parser's catalog at their positions, as resolver2 would, for a parse with no
147     * later pass ({@link EResolverType#NONE}): a table declared with its columns, and a top-level {@code #t}.
148     * resolver2 reads a plain {@code CREATE TABLE ... AS SELECT} from the statement, never from its catalog.
149     */
150    public void publish() {
151        if (!shared.isEnableGetMetadataFromDDL()) return;
152        for (TCreateTableSqlStatement create : publishable) {
153            StatementMetadata.Context context = contexts.get(create);
154            if (TMetadataCollector.registerDeclaredTable(create, shared, context.catalog, context.schema) != null) continue;
155            if (isTransient(create) && (TemporaryTableSemantics.ofName(create.dbvendor, create.getTableName()) != null)) {
156                StatementMetadata.declareQueryTable(shared, create.getTableName(), create.getSubQuery(),
157                        context.catalog, context.schema, false);
158            }
159        }
160    }
161
162    private void add(TCreateTableSqlStatement create) {
163        StatementMetadata.Context context = contextAt(create);
164        boolean transientTable = isTransient(create);
165        TStoredProcedureSqlStatement routine = transientTable ? StatementMetadata.procedureOwner(create) : null;
166        if (routine != null) {
167            declareInto(routineCatalog(routine, context), create, context, true);
168            return;
169        }
170        // The recovery that parses a CREATE TABLE again declares it again.
171        if (contexts.put(create, context) == null) publishable.add(create);
172        if (!TMetadataCollector.replacesTable(create)
173                && TMetadataCollector.findDeclaredTable(create, shared, context.catalog, context.schema) != null) {
174            return;
175        }
176        declareInto(declaredCatalog(), create, context, transientTable);
177    }
178
179    /**
180     * Put a declaration into {@code target}, a catalog of this one: the columns it declares, else the columns of its
181     * query; a transient table with neither ({@code LIKE}, a copy) is a table of its own whose shape is not known.
182     */
183    private static void declareInto(TSQLEnv target, TCreateTableSqlStatement create, StatementMetadata.Context context,
184            boolean transientTable) {
185        if (TMetadataCollector.addDeclaredTable(create, target, context.catalog, context.schema) != null) return;
186        boolean replace = TMetadataCollector.replacesTable(create);
187        if (create.getSubQuery() == null && !transientTable) return;
188        if (!replace && TMetadataCollector.findDeclaredTable(create, target, context.catalog, context.schema) != null) {
189            return;
190        }
191        StatementMetadata.declareQueryTable(target, create.getTableName(), create.getSubQuery(), context.catalog,
192                context.schema, replace);
193    }
194
195    private TSQLTable search(TCustomSqlStatement statement, String database, String schema, String table) {
196        if (declared == null && routines.isEmpty()) return null;
197        // A name temporary by its spelling is the same table whatever qualifies it, as resolver2 keys it.
198        boolean anywhere = TemporaryTableSemantics.ofNameSegment(statement.dbvendor, table) != null;
199        StatementMetadata.Context context = anywhere ? null : contextAt(statement);
200        String name = nullToEmpty(database) + "." + nullToEmpty(schema) + "." + table;
201        TSQLTable found = null;
202        if (!routines.isEmpty()) {
203            TStoredProcedureSqlStatement routine = StatementMetadata.procedureOwner(statement);
204            TSQLEnv local = routine == null ? null : routines.get(routine);
205            if (local != null) found = anywhere ? findAnywhere(local, table) : lookUp(local, context, name);
206        }
207        if (found == null && declared != null) {
208            found = anywhere ? findAnywhere(declared, table) : lookUp(declared, context, name);
209        }
210        return found;
211    }
212
213    /** A lookup in {@code env}, a catalog of this one, in the context of the statement that reads it. */
214    private static TSQLTable lookUp(TSQLEnv env, StatementMetadata.Context context, String qualifiedName) {
215        env.setDefaultCatalogName(context.catalog);
216        env.setDefaultSchemaName(context.schema);
217        return env.searchTable(qualifiedName);
218    }
219
220    private static TSQLTable findAnywhere(TSQLEnv env, String table) {
221        for (TSQLCatalog catalog : env.getCatalogList()) {
222            for (TSQLSchema schema : catalog.getSchemaList()) {
223                TSQLTable found = schema.findTable(table);
224                if (found != null) return found;
225            }
226        }
227        return null;
228    }
229
230    private static String nullToEmpty(String s) {
231        return s == null ? "" : s;
232    }
233
234    private static boolean isTransient(TCreateTableSqlStatement create) {
235        return TemporaryTableSemantics.hasTransientTables(create.dbvendor)
236                && TemporaryTableSemantics.isTransient(TemporaryTableSemantics.ofDeclaration(create));
237    }
238
239    private TSQLEnv declaredCatalog() {
240        if (declared == null) {
241            // Not a routine's catalog: a plain environment under the parser catalog's rules and DDL gate.
242            declared = new TSQLEnv(shared.getDBVendor(), shared.getIdentifierProfile()) {
243                @Override public void initSQLEnv() { }
244            };
245            declared.setEnableGetMetadataFromDDL(shared.isEnableGetMetadataFromDDL());
246        }
247        return declared;
248    }
249
250    private TSQLEnv routineCatalog(TStoredProcedureSqlStatement routine, StatementMetadata.Context context) {
251        TSQLEnv local = routines.get(routine);
252        if (local == null) {
253            local = StatementMetadata.newProcedureEnvironment(shared, context);
254            routines.put(routine, local);
255        }
256        local.setDefaultCatalogName(context.catalog);
257        local.setDefaultSchemaName(context.schema);
258        return local;
259    }
260
261    /**
262     * The context {@code statement} runs in: the top-level statements before it, and the statements of its own
263     * top-level statement before it, in the order {@link StatementMetadata#collect} walks them.
264     */
265    private StatementMetadata.Context contextAt(TCustomSqlStatement statement) {
266        TCustomSqlStatement top = statement.getAncestorStmt();
267        if (cursor == null || cursor.root != top) cursor = new Cursor(top);
268        return cursor.contextOf(statement);
269    }
270
271    /** A statement whose child statements the walk is in, with the index of the next one. */
272    private static final class Frame {
273        final TCustomSqlStatement statement;
274        int next;
275
276        Frame(TCustomSqlStatement statement) {
277            this.statement = statement;
278        }
279    }
280
281    /**
282     * The walk of a top-level statement {@link StatementMetadata#collect} makes, depth first in source order, taken as
283     * far as its statements are parsed and resumed as more are. A statement keeps the context the walk reached it in,
284     * so the walk is made once however often its statements ask.
285     */
286    private final class Cursor {
287        final TCustomSqlStatement root;
288        /** The context after the statements visited so far. */
289        private final DatabaseContextTracker at =
290                new DatabaseContextTracker(tracker.getCurrentDatabase(), tracker.getCurrentSchemaComponent());
291        /** The statements from the root down to the last one visited. */
292        private final List<Frame> path = new ArrayList<>();
293        private final Map<TCustomSqlStatement, StatementMetadata.Context> reached = new IdentityHashMap<>();
294        private StatementMetadata.Context current;
295
296        Cursor(TCustomSqlStatement root) {
297            this.root = root;
298            visit(root);
299        }
300
301        StatementMetadata.Context contextOf(TCustomSqlStatement statement) {
302            StatementMetadata.Context context = reached.get(statement);
303            while (context == null && advance()) context = reached.get(statement);
304            // A statement the walk cannot reach yet runs after every statement parsed so far.
305            return context != null ? context : current;
306        }
307
308        /** Visit the next statement parsed so far; false when the walk has reached every one. */
309        boolean advance() {
310            for (int depth = path.size() - 1; depth >= 0; depth--) {
311                Frame frame = path.get(depth);
312                TStatementList children = frame.statement.getStatements();
313                while (frame.next < children.size()) {
314                    TCustomSqlStatement child = children.get(frame.next++);
315                    if (child == null || reached.containsKey(child)) continue;
316                    // A later sibling is parsed after the statements under the one before it.
317                    while (path.size() > depth + 1) path.remove(path.size() - 1);
318                    visit(child);
319                    return true;
320                }
321            }
322            return false;
323        }
324
325        private void visit(TCustomSqlStatement statement) {
326            // A statement an earlier top-level statement holds too was walked there.
327            if (!seen.contains(statement)) at.processStatement(statement);
328            current = StatementMetadata.contextOf(at, statement.dbvendor, shared);
329            reached.put(statement, current);
330            path.add(new Frame(statement));
331        }
332    }
333}