001package gudusoft.gsqlparser.parser;
002
003import gudusoft.gsqlparser.EDbObjectType;
004import gudusoft.gsqlparser.EDbVendor;
005import gudusoft.gsqlparser.ETokenType;
006import gudusoft.gsqlparser.TBaseType;
007import gudusoft.gsqlparser.TCustomSqlStatement;
008import gudusoft.gsqlparser.TSourceToken;
009import gudusoft.gsqlparser.TSourceTokenList;
010import gudusoft.gsqlparser.nodes.TObjectName;
011import gudusoft.gsqlparser.stmt.TCreateTableSqlStatement;
012
013/**
014 * The location of an Athena CREATE TABLE whose Hive clauses the grammar cannot read.
015 *
016 * <p>Athena creates tables with Hive DDL, as in {@code CREATE EXTERNAL TABLE events (...) PARTITIONED BY (...)
017 * STORED AS PARQUET LOCATION 's3://bucket/events/'}. The Athena grammar reads the column list but none of the
018 * clauses after it, so the general create-table error recovery ignores those clauses and parses the statement
019 * again. {@code LOCATION} names the files of the table, which lineage shows as the path the table comes from, as it
020 * does for Hive. This recovery reads it before the clauses are ignored and gives it to the parsed statement, so
021 * that {@link TCreateTableSqlStatement#getTableLocation()} returns it as it does for Hive.</p>
022 *
023 * <p>Used by both parse paths, {@code TGSqlParser} and the vendor {@code SqlParser}, so that they recover in the
024 * same way. Not intended for use outside the parser.</p>
025 */
026public final class CreateTableLocationRecovery {
027
028    private CreateTableLocationRecovery() {
029    }
030
031    /**
032     * The string of the LOCATION clause after the column list of a CREATE TABLE: {@code 's3://bucket/events/'} in
033     * {@code CREATE EXTERNAL TABLE events (...) STORED AS PARQUET LOCATION 's3://bucket/events/'}. Only a clause at
034     * parenthesis depth zero counts, so a string in a property list, such as
035     * {@code TBLPROPERTIES ('location' = '...')}, is not one. Read the tokens before the recovery ignores them.
036     *
037     * @param tokens the tokens of one CREATE TABLE statement
038     * @param locationKeyword the lexer's token code of the keyword LOCATION, 0 when the lexer has none
039     * @return the string token after LOCATION, or null when there is no such clause
040     */
041    public static TSourceToken locationAfterColumnList(TSourceTokenList tokens, int locationKeyword) {
042        if (tokens == null || locationKeyword == 0) {
043            return null;
044        }
045        int nested = 0;
046        boolean afterColumnList = false;
047        boolean afterKeyword = false;
048        for (int k = 0; k < tokens.size(); k++) {
049            TSourceToken st = tokens.get(k);
050            if (!st.issolidtoken()) {
051                continue;
052            }
053            if (afterKeyword) {
054                return st.tokentype == ETokenType.ttsqstring ? st : null;
055            }
056            if (afterColumnList && nested == 0 && isLocationKeyword(st, locationKeyword)) {
057                afterKeyword = true;
058            } else if (st.tokencode == (int) '(' || st.tokencode == TBaseType.left_parenthesis_2) {
059                nested++;
060            } else if (st.tokencode == (int) ')') {
061                nested--;
062                if (nested == 0) {
063                    afterColumnList = true;
064                }
065            }
066        }
067        return null;
068    }
069
070    /**
071     * The keyword LOCATION. When it is the token where the syntax error is, as in
072     * {@code CREATE EXTERNAL TABLE events (...) LOCATION 's3://bucket/events/'}, the parser has tried it again as an
073     * identifier: its token code is then {@link TBaseType#ident}, while its token type is still a keyword.
074     */
075    private static boolean isLocationKeyword(TSourceToken st, int locationKeyword) {
076        if (st.tokencode == locationKeyword) {
077            return true;
078        }
079        return st.tokencode == TBaseType.ident && st.tokentype == ETokenType.ttkeyword
080                && "LOCATION".equalsIgnoreCase(st.toString()); // non-identifier-compare: the keyword LOCATION
081    }
082
083    /**
084     * Gives the location found by {@link #locationAfterColumnList} to the statement that the recovery parsed again.
085     * Nothing happens when {@code location} is null or the statement is not a CREATE TABLE.
086     */
087    public static void setLocation(TCustomSqlStatement stmt, TSourceToken location, EDbVendor vendor) {
088        if (location != null && stmt instanceof TCreateTableSqlStatement) {
089            ((TCreateTableSqlStatement) stmt).setTableLocation(
090                    TObjectName.createObjectName(vendor, EDbObjectType.unknown, location));
091        }
092    }
093}