001package gudusoft.gsqlparser.stmt.mysql;
002
003import gudusoft.gsqlparser.EDbObjectType;
004import gudusoft.gsqlparser.EDbVendor;
005import gudusoft.gsqlparser.ESqlStatementType;
006import gudusoft.gsqlparser.ETokenType;
007import gudusoft.gsqlparser.TBaseType;
008import gudusoft.gsqlparser.TCustomSqlStatement;
009import gudusoft.gsqlparser.TSourceToken;
010import gudusoft.gsqlparser.nodes.TObjectName;
011import gudusoft.gsqlparser.nodes.TObjectNameList;
012import gudusoft.gsqlparser.nodes.TParseTreeVisitor;
013import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
014import gudusoft.gsqlparser.util.SQLUtil;
015
016import java.util.ArrayList;
017import java.util.HashSet;
018import java.util.List;
019import java.util.Locale;
020import java.util.Set;
021
022/**
023 * MySQL {@code LOAD DATA [LOCAL] INFILE 'file' [REPLACE | IGNORE] INTO TABLE t [PARTITION (...)] [options]
024 * [(col_or_@var, ...)] [SET col = expr, ...]}.
025 * <p>
026 * The grammar keeps the statement as text, so the parts that lineage needs are read from its tokens (Mantis 4963):
027 * the file ({@link #getFileName()}), the table ({@link #getTableName()}), the columns the file's fields are loaded into
028 * ({@link #getColumnList()}, user variables left out), the columns a {@code SET} assigns from that list
029 * ({@link #getSetColumnList()}), and the columns that end up holding file data ({@link #getLoadedColumnList()}).
030 */
031public class TLoadDataStmt extends TCustomMySQLStmt {
032
033    private TSourceToken fileName;
034    private TObjectName tableName;
035    private TObjectNameList columnList;
036    private TObjectNameList setColumnList;
037    private TObjectNameList loadedColumnList;
038    /** The SET columns whose value reads nothing of the column list, which replaces the field loaded into them. */
039    private final TObjectNameList overwrittenColumns = new TObjectNameList();
040
041    public TLoadDataStmt(EDbVendor dbvendor) {
042        super(dbvendor);
043        sqlstatementtype = ESqlStatementType.sstLoadData;
044    }
045
046    /**
047     * @return the string literal after INFILE, as written, or null when it was not found
048     */
049    public TSourceToken getFileName() {
050        return fileName;
051    }
052
053    /**
054     * @return the table after INTO TABLE, or null when it was not found
055     */
056    public TObjectName getTableName() {
057        return tableName;
058    }
059
060    /**
061     * @return the columns of the column list, without its user variables; null when the statement has no column list
062     */
063    public TObjectNameList getColumnList() {
064        return columnList;
065    }
066
067    /**
068     * @return the columns of the SET clause whose value reads a user variable or a column of the column list; empty
069     *         when none
070     */
071    public TObjectNameList getSetColumnList() {
072        return setColumnList;
073    }
074
075    /**
076     * @return the columns whose new value comes from the file: those of the column list that no SET assigns a value
077     *         reading nothing of the list, and {@link #getSetColumnList()}; null when the statement has no column list
078     */
079    public TObjectNameList getLoadedColumnList() {
080        return loadedColumnList;
081    }
082
083    public int doParseStatement(TCustomSqlStatement psql) {
084        if (rootNode == null) return -1;
085        super.doParseStatement(psql);
086        readLoadParts();
087        return 0;
088    }
089
090    private void readLoadParts() {
091        setColumnList = new TObjectNameList();
092        List<TSourceToken> tokens = new ArrayList<TSourceToken>();
093        for (int i = 0; i < sourcetokenlist.size(); i++) {
094            TSourceToken token = sourcetokenlist.get(i);
095            if (!token.isnonsolidtoken()) {
096                tokens.add(token);
097            }
098        }
099        int i = indexOfWord(tokens, 0, "INFILE");
100        // INFILE takes a string, "/data/x.csv" as well: a double-quoted text is a string there whatever ANSI_QUOTES says
101        if ((i < 0) || (i + 1 >= tokens.size()) || ((tokens.get(i + 1).tokencode != TBaseType.sconst)
102                && (tokens.get(i + 1).tokentype != ETokenType.ttdqstring))) return;
103        fileName = tokens.get(i + 1);
104        i = indexOfWord(tokens, i + 2, "INTO");
105        if ((i < 0) || !isWord(tokens, i + 1, "TABLE")) return;
106        i += 2;
107        List<TSourceToken> parts = new ArrayList<TSourceToken>();
108        i = readName(tokens, i, parts);
109        if (parts.isEmpty() || (parts.size() > 2)) return;
110        tableName = parts.size() == 1 ? TObjectName.createObjectName(dbvendor, EDbObjectType.table, parts.get(0))
111                : TObjectName.createObjectName(dbvendor, EDbObjectType.table, parts.get(0), parts.get(1));
112
113        Set<String> variables = new HashSet<String>();
114        while (i < tokens.size()) {
115            TSourceToken token = tokens.get(i);
116            if (isWord(tokens, i, "PARTITION") && isTokenType(tokens, i + 1, ETokenType.ttleftparenthesis)) {
117                i = closingParenthesis(tokens, i + 1) + 1;
118            } else if ((token.tokentype == ETokenType.ttleftparenthesis) && (columnList == null)) {
119                columnList = new TObjectNameList();
120                i = readColumnList(tokens, i + 1, variables);
121            } else if (isWord(tokens, i, "SET") && !isWord(tokens, i - 1, "CHARACTER")) {
122                readSetClause(tokens, i + 1, variables);
123                break;
124            } else {
125                i++;
126            }
127        }
128        if (columnList != null) {
129            loadedColumnList = new TObjectNameList();
130            for (TObjectName column : columnList) {
131                if (!contains(overwrittenColumns, column)) {
132                    loadedColumnList.addObjectName(column);
133                }
134            }
135            for (TObjectName column : setColumnList) {
136                if (!contains(loadedColumnList, column)) {
137                    loadedColumnList.addObjectName(column);
138                }
139            }
140        }
141    }
142
143    private boolean contains(TObjectNameList columns, TObjectName column) {
144        for (TObjectName listed : columns) {
145            if (SQLUtil.sameName(dbvendor, ESQLDataObjectType.dotColumn, listed.toString(), column.toString())) {
146                return true;
147            }
148        }
149        return false;
150    }
151
152    /**
153     * A user variable at i, {@code @v} or quoted {@code @`v`}, {@code @'v'}, {@code @"v"}: its key and the index after
154     * it, or null. MySQL user variable names are not case-sensitive and the quotes only allow any character, so the
155     * key is the unquoted name in upper case; user variables are no database objects.
156     */
157    private static Object[] readVariable(List<TSourceToken> tokens, int i) {
158        TSourceToken token = tokens.get(i);
159        if (token.tokentype == ETokenType.ttsqlvar && token.toString().startsWith("@")) {
160            return new Object[] {token.toString().substring(1).toUpperCase(Locale.ROOT), i + 1};
161        }
162        if ((token.tokentype == ETokenType.ttatsign) && (i + 1 < tokens.size())) {
163            String quoted = tokens.get(i + 1).toString();
164            if (quoted.length() >= 2 && (quoted.charAt(0) == '`' || quoted.charAt(0) == '\'' || quoted.charAt(0) == '"')
165                    && quoted.charAt(quoted.length() - 1) == quoted.charAt(0)) {
166                return new Object[] {quoted.substring(1, quoted.length() - 1).toUpperCase(Locale.ROOT), i + 2};
167            }
168        }
169        return null;
170    }
171
172    /** The names of the list up to its closing parenthesis; a user variable is remembered, not listed. */
173    private int readColumnList(List<TSourceToken> tokens, int i, Set<String> variables) {
174        while (i < tokens.size()) {
175            TSourceToken token = tokens.get(i);
176            if (token.tokentype == ETokenType.ttrightparenthesis) {
177                return i + 1;
178            }
179            Object[] variable = readVariable(tokens, i);
180            if (variable != null) {
181                variables.add((String) variable[0]);
182                i = (Integer) variable[1];
183            } else if (isNamePart(token)) {
184                List<TSourceToken> parts = new ArrayList<TSourceToken>();
185                i = readName(tokens, i, parts);
186                columnList.addObjectName(TObjectName.createObjectName(dbvendor, EDbObjectType.column, parts.get(parts.size() - 1)));
187            } else {
188                i++;
189            }
190        }
191        return i;
192    }
193
194    /**
195     * SET col = expr, ...: a column whose value reads a user variable or a column of the list is loaded from the file;
196     * a listed column assigned a value that reads neither no longer holds its field.
197     */
198    private void readSetClause(List<TSourceToken> tokens, int i, Set<String> variables) {
199        while (i < tokens.size()) {
200            List<TSourceToken> parts = new ArrayList<TSourceToken>();
201            i = readName(tokens, i, parts);
202            if (parts.isEmpty() || !isTokenType(tokens, i, ETokenType.ttequals)) return;
203            i++;
204            boolean readsFile = false;
205            int depth = 0;
206            while (i < tokens.size()) {
207                TSourceToken token = tokens.get(i);
208                if (token.tokentype == ETokenType.ttleftparenthesis) {
209                    depth++;
210                } else if (token.tokentype == ETokenType.ttrightparenthesis) {
211                    depth--;
212                } else if ((depth == 0) && ((token.tokentype == ETokenType.ttcomma) || (token.tokentype == ETokenType.ttsemicolon))) {
213                    break;
214                } else {
215                    Object[] variable = readVariable(tokens, i);
216                    if (variable != null) {
217                        readsFile |= variables.contains((String) variable[0]);
218                        i = (Integer) variable[1];
219                        continue;
220                    }
221                    // a name before ( calls a function, NOW(), it reads no column
222                    if (isNamePart(token) && (columnList != null) && !isTokenType(tokens, i + 1, ETokenType.ttleftparenthesis)
223                            && contains(columnList, TObjectName.createObjectName(dbvendor, EDbObjectType.column, token))) {
224                        readsFile = true;
225                    }
226                }
227                i++;
228            }
229            TObjectName target = TObjectName.createObjectName(dbvendor, EDbObjectType.column, parts.get(parts.size() - 1));
230            if (readsFile) {
231                setColumnList.addObjectName(target);
232            } else {
233                overwrittenColumns.addObjectName(target);
234            }
235            if ((i >= tokens.size()) || (tokens.get(i).tokentype != ETokenType.ttcomma)) return;
236            i++;
237        }
238    }
239
240    /** A dotted name starting at i: its parts go to parts, the index after it is returned. */
241    private static int readName(List<TSourceToken> tokens, int i, List<TSourceToken> parts) {
242        while ((i < tokens.size()) && isNamePart(tokens.get(i))) {
243            parts.add(tokens.get(i));
244            i++;
245            if (!isTokenType(tokens, i, ETokenType.ttperiod)) break;
246            i++;
247        }
248        return i;
249    }
250
251    /** Where only a name can stand, a keyword is a name too: (id, date) lists a column named date. */
252    private static boolean isNamePart(TSourceToken token) {
253        return (token.tokencode == TBaseType.ident) || (token.tokentype == ETokenType.ttidentifier)
254                || (token.tokentype == ETokenType.ttdqstring) || (token.tokentype == ETokenType.ttkeyword);
255    }
256
257    private static int closingParenthesis(List<TSourceToken> tokens, int open) {
258        int depth = 0;
259        for (int i = open; i < tokens.size(); i++) {
260            if (tokens.get(i).tokentype == ETokenType.ttleftparenthesis) depth++;
261            if ((tokens.get(i).tokentype == ETokenType.ttrightparenthesis) && (--depth == 0)) return i;
262        }
263        return tokens.size();
264    }
265
266    private static int indexOfWord(List<TSourceToken> tokens, int from, String word) {
267        for (int i = from; i < tokens.size(); i++) {
268            if (isWord(tokens, i, word)) return i;
269        }
270        return -1;
271    }
272
273    private static boolean isWord(List<TSourceToken> tokens, int i, String word) {
274        return (i >= 0) && (i < tokens.size()) && (tokens.get(i).tokentype == ETokenType.ttkeyword)
275                && tokens.get(i).toString().equalsIgnoreCase(word); // non-identifier-compare: LOAD DATA keyword
276    }
277
278    private static boolean isTokenType(List<TSourceToken> tokens, int i, ETokenType type) {
279        return (i >= 0) && (i < tokens.size()) && (tokens.get(i).tokentype == type);
280    }
281
282    public void accept(TParseTreeVisitor v){
283        v.preVisit(this);
284        v.postVisit(this);
285    }
286
287    public void acceptChildren(TParseTreeVisitor v){
288        v.preVisit(this);
289        v.postVisit(this);
290    }
291
292}