001package gudusoft.gsqlparser.parser;
002
003import gudusoft.gsqlparser.TBaseType;
004import gudusoft.gsqlparser.TSourceToken;
005import gudusoft.gsqlparser.TSourceTokenList;
006
007/**
008 * Error recovery for a Doris or StarRocks {@code CREATE TABLE ... AS SELECT} whose table clauses
009 * the grammar cannot read.
010 *
011 * <p>When a CREATE TABLE fails to parse, the general recovery ignores everything after the first
012 * closing parenthesis at depth zero and parses the statement again, so a table whose column list
013 * is followed by an unsupported option still yields its columns. For a CREATE TABLE AS SELECT that
014 * ignores the query too, and with it every lineage edge of the statement, while the statement
015 * reports success. When the first closing parenthesis belongs to an option, as in
016 * {@code DUPLICATE KEY(k1)}, or to the query itself, the result is a query cut short.</p>
017 *
018 * <p>This recovery keeps the header and the query and ignores only the table clauses between
019 * them: {@code CREATE TABLE t DUPLICATE KEY(k1) DISTRIBUTED BY HASH(k1) AS SELECT ...} is parsed as
020 * {@code CREATE TABLE t AS SELECT ...}. When nothing stands between the header and the query, the
021 * error lies in one of them and is kept.</p>
022 *
023 * <p>Used by both parse paths, {@code TGSqlParser} and the vendor {@code SqlParser}, so that they
024 * recover in the same way. Not intended for use outside the parser.</p>
025 */
026public final class CreateTableAsSelectRecovery {
027
028    private CreateTableAsSelectRecovery() {
029    }
030
031    /**
032     * Position of the AS that starts the query of a CREATE TABLE AS SELECT: the first AS at
033     * parenthesis depth zero that is followed by SELECT, WITH or an opening parenthesis, or -1 when
034     * the statement has no query. A generated column ({@code k2 INT AS k1 + 1}) sits inside the
035     * column list, one level deep, so it is never taken for the query.
036     *
037     * @param tokens the tokens of one CREATE TABLE statement
038     * @return the position of the AS in {@code tokens}, or -1
039     */
040    public static int queryStart(TSourceTokenList tokens) {
041        int depth = 0;
042        for (int k = 0; k < tokens.size(); k++) {
043            TSourceToken st = tokens.get(k);
044            if (st.tokencode == '(') {
045                depth++;
046            } else if (st.tokencode == ')') {
047                depth--;
048            } else if ((depth == 0) && (st.tokencode == TBaseType.rrw_as)) {
049                int next = nextSolidTokenPos(tokens, k);
050                if (next >= 0) {
051                    int code = tokens.get(next).tokencode;
052                    if ((code == TBaseType.rrw_select) || (code == TBaseType.rrw_with) || (code == '(')) {
053                        return k;
054                    }
055                }
056            }
057        }
058        return -1;
059    }
060
061    /**
062     * Ignores the table clauses between the header and the query, so that the grammar reads both
063     * even when it cannot read a clause in between (an OLAP key, a partition or distribution
064     * clause, properties, ...).
065     *
066     * @param tokens the tokens of one CREATE TABLE statement
067     * @param queryStart the position returned by {@link #queryStart}
068     * @return true when a clause was ignored and the statement must be parsed again; false when
069     * nothing stands between the header and the query, or the header was not found, so the
070     * tokens are unchanged and the error is kept
071     */
072    public static boolean ignoreClausesBeforeQuery(TSourceTokenList tokens, int queryStart) {
073        int headerEnd = headerEnd(tokens, queryStart);
074        if (headerEnd < 0) {
075            return false;
076        }
077        boolean hasClause = false;
078        for (int k = headerEnd + 1; k < queryStart; k++) {
079            if (tokens.get(k).issolidtoken()) {
080                hasClause = true;
081                break;
082            }
083        }
084        if (!hasClause) {
085            return false;
086        }
087        for (int k = headerEnd + 1; k < queryStart; k++) {
088            tokens.get(k).tokencode = TBaseType.sqlpluscmd;
089        }
090        return true;
091    }
092
093    /**
094     * Position of the last token of the header {@code CREATE ... TABLE [IF NOT EXISTS] name}, or of
095     * the closing parenthesis of the column list that follows the name, or -1 when the header is not
096     * found before the query.
097     */
098    private static int headerEnd(TSourceTokenList tokens, int queryStart) {
099        int table = -1;
100        for (int k = 0; k < queryStart; k++) {
101            if (tokens.get(k).tokencode == TBaseType.rrw_table) {
102                table = k;
103                break;
104            }
105        }
106        if (table < 0) {
107            return -1;
108        }
109        int beforeName = table;
110        int next = nextSolidTokenPos(tokens, table);
111        if ((next >= 0) && (tokens.get(next).tokencode == TBaseType.rrw_if)) {
112            // IF NOT EXISTS: the name follows EXISTS, the second solid token after IF
113            beforeName = nextSolidTokenPos(tokens, nextSolidTokenPos(tokens, next));
114            if (beforeName < 0) {
115                return -1;
116            }
117        }
118        int nameEnd = tokens.nextObjectNameToken(beforeName, 1, false);
119        if ((nameEnd < 0) || (nameEnd >= queryStart)) {
120            return -1;
121        }
122        int after = nextSolidTokenPos(tokens, nameEnd);
123        if ((after < 0) || (after >= queryStart) || (tokens.get(after).tokencode != '(')) {
124            return nameEnd;
125        }
126        int depth = 0;
127        for (int k = after; k < queryStart; k++) {
128            int code = tokens.get(k).tokencode;
129            if (code == '(') {
130                depth++;
131            } else if ((code == ')') && (--depth == 0)) {
132                return k;
133            }
134        }
135        return -1;
136    }
137
138    private static int nextSolidTokenPos(TSourceTokenList tokens, int pos) {
139        if (pos < 0) {
140            return -1;
141        }
142        for (int k = pos + 1; k < tokens.size(); k++) {
143            if (tokens.get(k).issolidtoken()) {
144                return k;
145            }
146        }
147        return -1;
148    }
149}