/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with VoltDB. If not, see <http://www.gnu.org/licenses/>. */ package org.voltdb.parser; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.math.BigDecimal; import java.math.BigInteger; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.voltdb.types.GeographyPointValue; import org.voltdb.types.GeographyValue; import org.voltdb.types.TimestampType; import org.voltdb.utils.Encoder; import com.google_voltpatches.common.collect.ImmutableMap; /** * Provides an API for performing various parse operations on SQL/DML/DDL text. * * Keep the regular expressions private and just expose methods needed for parsing. */ public class SQLParser extends SQLPatternFactory { public static class Exception extends RuntimeException { private Exception(String message, Object... args) { super(String.format(message, args)); } private Exception(Throwable cause) { super(cause.getMessage(), cause); } private Exception(Throwable cause, String message, Object... args) { super(String.format(message, args), cause); } private static final long serialVersionUID = -4043500523038225173L; } //========== Private Parsing Data ========== /** * Pattern: SET <PARAMETER NAME> <PARAMETER VALUE> * * Capture groups: * (1) parameter name * (2) parameter value */ private static final Pattern SET_GLOBAL_PARAM = Pattern.compile( "(?i)" + // (ignore case) "\\A" + // (start statement) "SET" + // SET "\\s+([\\w_]+)" + // (1) PARAMETER NAME "\\s*=\\s*([\\w_]+)" + // (2) PARAMETER VALUE "\\s*;\\z" // (end statement) ); static final Pattern SET_GLOBAL_PARAM_FOR_WHITELIST = Pattern.compile( "(?i)" + // (ignore case) "\\A" + // (start statement) "SET" + // SET "\\s+.*\\z" // (end statement) ); /** * Pattern: PARTITION PROCEDURE|TABLE ... * * Capture groups: * (1) target type: "procedure" or "table" */ private static final Pattern PAT_PARTITION_ANY_PREAMBLE = SPF.statement( SPF.token("partition"), SPF.capture(SPF.tokenAlternatives("procedure", "table")), SPF.anyClause() ).compile("PAT_PARTITION_ANY_PREAMBLE"); /** * Pattern: PARTITION TABLE tablename ON COLUMN columnname * * NB supports only unquoted table and column names * * Capture groups: * (1) table name * (2) column name */ private static final Pattern PAT_PARTITION_TABLE = SPF.statement( SPF.token("partition"), SPF.token("table"), SPF.capture(SPF.databaseObjectName()), SPF.token("on"), SPF.token("column"), SPF.capture(SPF.databaseObjectName()) ).compile("PAT_PARTITION_TABLE"); /** * PARTITION PROCEDURE procname ON TABLE tablename COLUMN columnname [PARAMETER paramnum] * * NB supports only unquoted table and column names * * Capture groups: * (1) Procedure name * (2) Table name * (3) Column name * (4) Parameter number */ private static final Pattern PAT_PARTITION_PROCEDURE = SPF.statement( SPF.token("partition"), SPF.token("procedure"), SPF.capture(SPF.procedureName()), SPF.token("on"), SPF.token("table"), SPF.capture(SPF.databaseObjectName()), SPF.token("column"), SPF.capture(SPF.databaseObjectName()), SPF.optional(SPF.clause(SPF.token("parameter"), SPF.capture(SPF.integer()))) ).compile("PAT_PARTITION_PROCEDURE"); //TODO: Convert to pattern factory usage below this point. /* * CREATE PROCEDURE [ <MODIFIER_CLAUSE> ... ] FROM <JAVA_CLASS> * * CREATE PROCEDURE from Java class statement pattern. * NB supports only unquoted table and column names * * Capture groups: * (1) ALLOW/PARTITION clauses full text - needs further parsing * (2) Class name */ private static final Pattern PAT_CREATE_PROCEDURE_FROM_CLASS = SPF.statement( SPF.token("create"), SPF.token("procedure"), unparsedProcedureModifierClauses(), SPF.token("from"), SPF.token("class"), SPF.capture(SPF.className()) ).compile("PAT_CREATE_PROCEDURE_FROM_CLASS"); /* * CREATE PROCEDURE <NAME> [ <MODIFIER_CLAUSE> ... ] AS <SQL_STATEMENT> * * CREATE PROCEDURE with single SELECT or DML statement pattern * NB supports only unquoted table and column names * * Capture groups: * (1) Procedure name * (2) ALLOW/PARTITION clauses full text - needs further parsing * (3) SELECT or DML statement */ private static final Pattern PAT_CREATE_PROCEDURE_FROM_SQL = SPF.statement( SPF.token("create"), SPF.token("procedure"), SPF.capture(SPF.procedureName()), unparsedProcedureModifierClauses(), SPF.token("as"), SPF.capture(SPF.anyClause()) ).compile("PAT_CREATE_PROCEDURE_FROM_SQL"); /* * CREATE FUNCTION <NAME> FROM METHOD <CLASS NAME>.<METHOD NAME> * * CREATE FUNCTION with the designated method from the given class. * * Capture groups: * (1) Function name * (2) The class name * (3) The method name */ private static final Pattern PAT_CREATE_FUNCTION_FROM_METHOD = SPF.statement( SPF.token("create"), SPF.token("function"), SPF.capture(SPF.functionName()), SPF.token("from"), SPF.token("method"), SPF.capture(SPF.classPath()), SPF.dot().withFlags(ADD_LEADING_SPACE_TO_CHILD), SPF.capture(SPF.functionName().withFlags(ADD_LEADING_SPACE_TO_CHILD)) ).compile("PAT_CREATE_FUNCTION_FROM_METHOD"); /* * DROP FUNCTION <NAME> [IF EXISTS] * * Drop a user-defined function. * * Capture groups: * (1) Function name * (2) If exists */ private static final Pattern PAT_DROP_FUNCTION = SPF.statement( SPF.token("drop"), SPF.token("function"), SPF.capture(SPF.functionName()), SPF.optional(SPF.capture(SPF.clause(SPF.token("if"), SPF.token("exists")))) ).compile("PAT_DROP_FUNCTION"); /* * CREATE PROCEDURE <NAME> [ <MODIFIER_CLAUSE> ... ] AS ### <PROCEDURE_CODE> ### LANGUAGE <LANGUAGE_NAME> * * CREATE PROCEDURE with inline implementation script, e.g. Groovy, statement regex * NB supports only unquoted table and column names * This used to support GROOVY, but now will just offer a compile error. * * Capture groups: * (1) Procedure name * (2) ALLOW/PARTITION clauses - needs further parsing * (3) Code block content * (4) Language name */ private static final Pattern PAT_CREATE_PROCEDURE_AS_SCRIPT = SPF.statement( SPF.token("create"), SPF.token("procedure"), SPF.capture(SPF.procedureName()), unparsedProcedureModifierClauses(), SPF.token("as"), SPF.delimitedCaptureBlock(SQLLexer.BLOCK_DELIMITER, null), // Match anything after the last delimiter to get a good error for a bad language clause. SPF.oneOf( SPF.clause(SPF.token("language"), SPF.capture(SPF.languageName())), SPF.anyClause() ) ).compile("PAT_CREATE_PROCEDURE_AS_SCRIPT"); /** * Pattern for parsing a single ALLOW or PARTITION clauses within a CREATE PROCEDURE statement. * * Capture groups: * (1) ALLOW clause: entire role list with commas and internal whitespace * (2) PARTITION clause: procedure name * (3) PARTITION clause: table name * (4) PARTITION clause: column name * * An ALLOW clause will have (1) be non-null and (2,3,4) be null. * A PARTITION clause will have (1) be null and (2,3,4) be non-null. */ private static final Pattern PAT_ANY_CREATE_PROCEDURE_STATEMENT_CLAUSE = parsedProcedureModifierClause().compile("PAT_ANY_CREATE_PROCEDURE_STATEMENT_CLAUSE"); /** * Pattern for parsing a single EXPORT or PARTITION clauses within a CREATE STREAM statement. * * Capture groups: * (1) ALLOW clause: target name * (2) PARTITION clause: column name * */ private static final Pattern PAT_ANY_CREATE_STREAM_STATEMENT_CLAUSE = parsedStreamModifierClause().compile("PAT_ANY_CREATE_STREAM_STATEMENT_CLAUSE"); /** * DROP PROCEDURE statement regex */ private static final Pattern PAT_DROP_PROCEDURE = Pattern.compile( "(?i)" + // ignore case "\\A" + // beginning of statement "DROP" + // DROP token "\\s+" + // one or more spaces "PROCEDURE" + // PROCEDURE token "\\s+" + // one or more spaces "([\\w$.]+)" + // (1) class name or procedure name "(\\s+IF EXISTS)?" + // (2) <optional IF EXISTS> "\\s*" + // zero or more spaces ";" + // semicolon terminator "\\z" // end of statement ); /** * IMPORT CLASS with pattern for matching classfiles in * the current classpath. */ private static final Pattern PAT_IMPORT_CLASS = Pattern.compile( "(?i)" + // (ignore case) "\\A" + // (start statement) "IMPORT\\s+CLASS\\s+" + // IMPORT CLASS "([^;]+)" + // (1) class matching pattern ";\\z" // (end statement) ); /** * Regex to parse the CREATE ROLE statement with optional WITH clause. * Leave the WITH clause argument as a single group because regexes * aren't capable of producing a variable number of groups. * Capture groups are tagged as (1) and (2) in comments below. */ private static final Pattern PAT_CREATE_ROLE = Pattern.compile( "(?i)" + // (ignore case) "\\A" + // (start statement) "CREATE\\s+ROLE\\s+" + // CREATE ROLE "([\\w.$]+)" + // (1) <role name> "(?:\\s+WITH\\s+" + // (start optional WITH clause block) "(\\w+(?:\\s*,\\s*\\w+)*)" + // (2) <comma-separated argument string> ")?" + // (end optional WITH clause block) ";\\z" // (end statement) ); /** * Regex to parse the DROP ROLE statement. * Capture group is tagged as (1) in comments below. */ private static final Pattern PAT_DROP_ROLE = Pattern.compile( "(?i)" + // (ignore case) "\\A" + // (start statement) "DROP\\s+ROLE\\s+" + // DROP ROLE "([\\w.$]+)" + // (1) <role name> "(\\s+IF EXISTS)?" + // (2) <optional IF EXISTS> ";\\z" // (end statement) ); /** * Regex to parse the DROP STREAM statement. * Capture group is tagged as (1) in comments below. */ private static final Pattern PAT_DROP_STREAM = SPF.statementLeader( SPF.token("drop"), SPF.token("stream"), SPF.capture("name", SPF.databaseObjectName()), SPF.optional(SPF.clause(SPF.token("if"), SPF.token("exists"))) ).compile("PAT_DROP_STREAM"); /** * NB supports only unquoted table names * Captures 1 group, the table name. */ private static final Pattern PAT_REPLICATE_TABLE = Pattern.compile( "(?i)" + // ignore case instruction "\\A" + // beginning of statement "REPLICATE\\s+TABLE\\s+" + // REPLICATE TABLE tokens with whitespace terminators "([\\w$]+)" + // (group 1) table name "\\s*" + // optional whitespace ";\\z" // semicolon at end of statement ); /* * CREATE STREAM statement regex * * Capture groups: * (1) stream name * (2) optional target name */ private static final Pattern PAT_CREATE_STREAM = SPF.statement( SPF.token("create"), SPF.token("stream"), SPF.capture("name", SPF.databaseObjectName()), unparsedStreamModifierClauses(), SPF.anyColumnFields() ).compile("PAT_CREATE_STREAM"); /** * If the statement starts with a VoltDB-specific DDL command, * one of create procedure, create role, drop procedure, drop role, * partition, replicate, export, import, or dr, the one match group * is set to the matching command EXCEPT as special (needlessly obscure) * cases, simply returns only "procedure" for "create procedure", * only "role" for "create role", and only "drop" for either * "drop procedure" OR "drop role". * ALSO (less than helpfully) returns "drop" for non-VoltDB-specific * "drop" commands like "drop table". * TODO: post-processing would be much simpler if this pattern reliably * accepted VoltDB commands, rejected non-VoltDB commands, and grouped * the actual command keyword(s) with their arbitrary whitespace * separators. A wrapper function should clean up from there. */ private static final Pattern PAT_ALL_VOLTDB_STATEMENT_PREAMBLES = Pattern.compile( "(?i)" + // ignore case instruction //TODO: why not factor \\A out of the group -- it's common to all options "(" + // start (group 1) // <= means zero-width positive lookbehind. // This means that the "CREATE\\s{}" is required to match but is not part of the capture. "(?<=\\ACREATE\\s{0,1024})" + //TODO: 0 min whitespace should be 1? "(?:PROCEDURE|ROLE|FUNCTION)|" + // token options after CREATE // the rest are stand-alone token options "\\ADROP|" + "\\APARTITION|" + "\\AREPLICATE|" + "\\AIMPORT|" + "\\ADR|" + "\\ASET" + ")" + // end (group 1) "\\s" + // one required whitespace to terminate keyword ""); private static final Pattern PAT_DR_TABLE = Pattern.compile( "(?i)" + // (ignore case) "\\A" + // start statement "DR\\s+TABLE\\s+" + // DR TABLE "([\\w.$|\\\\*]+)" + // (1) <table name> "(?:\\s+(DISABLE))?" + // (2) optional DISABLE argument "\\s*;\\z" // (end statement) ); //========== Patterns from SQLCommand ========== private static final String EndOfLineCommentPatternString = "(?:\\/\\/|--)" + // '--' or even C++-style '//' comment starter ".*$"; // commented out text continues to end of line private static final Pattern OneWholeLineComment = Pattern.compile( "^\\s*" + // optional whitespace indent prior to comment EndOfLineCommentPatternString); private static final Pattern AnyWholeLineComments = Pattern.compile( "^\\s*" + // optional whitespace indent prior to comment EndOfLineCommentPatternString, Pattern.MULTILINE); private static final Pattern EndOfLineComment = Pattern.compile( EndOfLineCommentPatternString, Pattern.MULTILINE); private static final Pattern OneWhitespace = Pattern.compile("\\s"); private static final Pattern EscapedSingleQuote = Pattern.compile("''", Pattern.MULTILINE); private static final Pattern SingleQuotedString = Pattern.compile("'[^']*'", Pattern.MULTILINE); private static final Pattern SingleQuotedStringContainingParameterSeparators = Pattern.compile( "'" + "[^',\\s]*" + // arbitrary string content NOT matching param separators "[,\\s]" + // the first match for a param separator "[^']*" + // arbitrary string content "'", // end of string OR start of escaped quote Pattern.MULTILINE); private static final Pattern SingleQuotedHexLiteral = Pattern.compile("[Xx]'([0-9A-Fa-f]*)'", Pattern.MULTILINE); // Define a common pattern to sweep up a mix of semicolons and space and // meaningless garbage at the end of the simpler sqlcmd directives. // The garbage parts (well, enough of them, anyway) are captured so that // they can optionally be detected in a post-processing step that MAY // generate a complaint about an improperly terminated command. private static String InitiallyForgivingDirectiveTermination = "\\s*" + // spaces "([^;\\s]*)" + // (first) non-space non-semicolon garbage word (last group +1) "[;\\s]*" + // trailing spaces and semicolons "(.*)" + // trailing garbage (last group +2) "$"; // HELP can support sub-commands someday. Capture group 2 is the sub-command. private static final Pattern HelpToken = Pattern.compile( "^\\s*" + // optional indent at start of line "help" + // required HELP command token "(\\W|$)" + // require an end to the keyword OR EOL (group 1) // Make everything that follows optional so that help // command diagnostics can "own" any line starting with the word // help. "\\s*" + // optional whitespace before subcommand "([^;\\s]*)" + // optional subcommand (group 2) InitiallyForgivingDirectiveTermination, Pattern.CASE_INSENSITIVE); private static final Pattern EchoToken = Pattern.compile( "^\\s*" + // optional indent at start of line "echo" + // required ECHO command token "(\\W|$)" + // require an end to the keyword OR EOL (group 1) "(.*)" + // Make everything that follows optional (group 2). "$", Pattern.CASE_INSENSITIVE); private static final Pattern EchoErrorToken = Pattern.compile( "^\\s*" + // optional indent at start of line "echoerror" + // required ECHOERROR command token "(\\W|$)" + // require an end to the keyword OR EOL (group 1) "(.*)" + // Make everything that follows optional (group 2). "$", Pattern.CASE_INSENSITIVE); private static final Pattern ExitToken = Pattern.compile( "^\\s*" + // optional indent at start of line "(?:exit|quit)" + // keyword alternatives, synonymous so don't bother capturing "(\\W|$)" + // require an end to the keyword OR EOL (group 1) // Make everything that follows optional so that exit/quit // command diagnostics can "own" any line starting with the word // exit or quit. InitiallyForgivingDirectiveTermination, Pattern.CASE_INSENSITIVE); private static final Pattern ShowToken = Pattern.compile( "^\\s*" + // optional indent at start of line "(?:list|show)" + // keyword alternatives, synonymous so don't bother capturing "(\\W|$)" + // require an end to the keyword OR EOL (group 1) // Make everything that follows optional so that list/show // command diagnostics can "own" any line starting with the word // list or show. "\\s*" + // extra spaces "([^;\\s]*)" + // non-space non-semicolon subcommand (group 2) InitiallyForgivingDirectiveTermination, Pattern.CASE_INSENSITIVE); private static final Pattern RecallToken = Pattern.compile( "^\\s*" + // optional indent at start of line "recall" + // required RECALL command token "(\\W|$)" + // require an end to the keyword OR EOL (group 1) // Make everything that follows optional so that recall command // diagnostics can "own" any line starting with the word recall. "\\s*" + // extra spaces "([^;\\s]*)" + // (first) non-space non-semicolon garbage word (group 2) InitiallyForgivingDirectiveTermination, Pattern.CASE_INSENSITIVE); private static final Pattern SemicolonToken = Pattern.compile( "^.*" + // match anything ";+" + // one required semicolon at end except for "\\s*" + // optional whitespace "(--)?$", // and an optional end-of-line comment Pattern.CASE_INSENSITIVE); // SQLCommand's FILE command. If this pattern matches, we // assume that the user meant to enter a file command, and // produce appropriate error messages. private static final Pattern FileToken = Pattern.compile( "^\\s*" + // optional indent at start of line "file" + // FILE keyword "(?:(?=\\s|;)|$)", // Must be either followed by whitespace or semicolon // (zero-width consumed) // or the end of the line Pattern.CASE_INSENSITIVE); private static final Pattern DashBatchToken = Pattern.compile( "\\s+" + // required preceding whitespace "-batch", // -batch option, whitespace terminated Pattern.CASE_INSENSITIVE); private static final Pattern DashInlineBatchToken = Pattern.compile( "\\s+" + // required preceding whitespace "-inlinebatch", // -inlinebatch option, whitespace terminated Pattern.CASE_INSENSITIVE); private static final Pattern FilenameToken = Pattern.compile( "\\s+" + // required preceding whitespace "['\"]*" + // optional opening quotes of either kind (ignored) (?) "([^;'\"]+)" + // file path assumed to end at the next quote or semicolon "['\"]*" + // optional closing quotes -- assumed to match opening quotes (?) "\\s*" + // optional whitespace //FIXME: strangely allowing more than one strictly adjacent semicolon. ";*" + // optional semicolons "\\s*", // more optional whitespace Pattern.CASE_INSENSITIVE); private static final Pattern DelimiterToken = Pattern.compile( "\\s+" + // required preceding whitespace "([^\\s;]+)" + // a string of characters not containing semis or spaces "\\s*;?\\s*", // an optional semicolon surrounded by whitespace Pattern.CASE_INSENSITIVE); // Query Execution private static final Pattern ExecuteCallPreamble = Pattern.compile( "^\\s*" + // optional indent at start of line "(?:exec|execute)" + // required command or alias non-grouping "(\\W|$)" + // require an end to the keyword OR EOL (group 1) // Make everything that follows optional so that exec command // diagnostics can "own" any line starting with the word // exec or execute. "\\s*", // extra spaces Pattern.MULTILINE + Pattern.CASE_INSENSITIVE); // Match queries that start with "explain" (case insensitive). We'll convert them to @Explain invocations. private static final Pattern ExplainCallPreamble = Pattern.compile( "^\\s*" + // optional indent at start of line "explain" + // required command, whitespace terminated "(\\W|$)" + // require an end to the keyword OR EOL (group 1) // Make everything that follows optional so that explain command // diagnostics can "own" any line starting with the word // explain. "\\s*", // extra spaces Pattern.MULTILINE + Pattern.CASE_INSENSITIVE); // Match queries that start with "explainproc" (case insensitive). We'll convert them to @ExplainProc invocations. private static final Pattern ExplainProcCallPreamble = Pattern.compile( "^\\s*" + // optional indent at start of line "explainProc" + // required command, whitespace terminated "(\\W|$)" + // require an end to the keyword OR EOL (group 1) // Make everything that follows optional so that explainproc command // diagnostics can "own" any line starting with the word // explainproc. "\\s*", // extra spaces Pattern.MULTILINE + Pattern.CASE_INSENSITIVE); // Match queries that start with "explainview" (case insensitive). We'll convert them to @ExplainView invocations. private static final Pattern ExplainViewCallPreamble = Pattern.compile( "^\\s*" + // optional indent at start of line "explainView" + // required command, whitespace terminated "(\\W|$)" + // require an end to the keyword OR EOL (group 1) // Make everything that follows optional so that explainproc command // diagnostics can "own" any line starting with the word // explainview. "\\s*", // extra spaces Pattern.MULTILINE + Pattern.CASE_INSENSITIVE); private static final SimpleDateFormat FullDateParser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); private static final SimpleDateFormat WholeSecondDateParser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final SimpleDateFormat DayDateParser = new SimpleDateFormat("yyyy-MM-dd"); private static final Pattern Unquote = Pattern.compile("^'|'$", Pattern.MULTILINE); private static final Map<String, String> FRIENDLY_TYPE_NAMES = ImmutableMap.<String, String>builder().put("tinyint", "byte numeric") .put("smallint", "short numeric") .put("int", "numeric") .put("integer", "numeric") .put("bigint", "long numeric") .build(); // The argument capture group for LOAD/REMOVE CLASSES loosely captures everything // through the trailing semicolon. It relies on post-parsing code to make sure // the argument is reasonable. // Capture group 1 for LOAD CLASSES is the jar file. private static final SingleArgumentCommandParser loadClassesParser = new SingleArgumentCommandParser("load classes", "jar file"); private static final SingleArgumentCommandParser removeClassesParser = new SingleArgumentCommandParser("remove classes", "class selector"); private static final Pattern ClassSelectorToken = Pattern.compile( "^[\\w*.$]+$", Pattern.CASE_INSENSITIVE); //========== Public Interface ========== /** * Match statement against set global parameter pattern * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchSetGlobalParam(String statement) { return SET_GLOBAL_PARAM.matcher(statement); } /** * Match statement against pattern for all VoltDB-specific statement preambles * TODO: Much more useful would be a String parseVoltDBSpecificDdlStatementPreamble * function that used a corrected pattern and some minimal post-processing to return * an upper cased single-space-separated preamble token string for ONLY VoltDB-specific * commands (or null if not a match). * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchAllVoltDBStatementPreambles(String statement) { return PAT_ALL_VOLTDB_STATEMENT_PREAMBLES.matcher(statement); } /** * Match statement against create role pattern * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchCreateRole(String statement) { return PAT_CREATE_ROLE.matcher(statement); } /** * Match statement against drop role pattern * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchDropRole(String statement) { return PAT_DROP_ROLE.matcher(statement); } /** * Match statement against drop stream pattern * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchDropStream(String statement) { return PAT_DROP_STREAM.matcher(statement); } /** * Match statement against create stream pattern * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchCreateStream(String statement) { return PAT_CREATE_STREAM.matcher(statement); } /** * Match statement against DR table pattern * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchDRTable(String statement) { return PAT_DR_TABLE.matcher(statement); } /** * Match statement against import class pattern * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchImportClass(String statement) { return PAT_IMPORT_CLASS.matcher(statement); } /** * Match statement against pattern for start of any partition statement * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchPartitionStatementPreamble(String statement) { return PAT_PARTITION_ANY_PREAMBLE.matcher(statement); } /** * Match statement against pattern for partition table statement * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchPartitionTable(String statement) { return PAT_PARTITION_TABLE.matcher(statement); } /** * Match statement against pattern for partition procedure statement * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchPartitionProcedure(String statement) { return PAT_PARTITION_PROCEDURE.matcher(statement); } /** * Match statement against pattern for create procedure as SQL * with allow/partition clauses * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchCreateProcedureAsSQL(String statement) { return PAT_CREATE_PROCEDURE_FROM_SQL.matcher(statement); } /** * Match statement against pattern for create procedure as script * with allow/partition clauses * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchCreateProcedureAsScript(String statement) { return PAT_CREATE_PROCEDURE_AS_SCRIPT.matcher(statement); } /** * Match statement against pattern for create procedure from class * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchCreateProcedureFromClass(String statement) { return PAT_CREATE_PROCEDURE_FROM_CLASS.matcher(statement); } /** * Match statement against the pattern for create function from method * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchCreateFunctionFromMethod(String statement) { return PAT_CREATE_FUNCTION_FROM_METHOD.matcher(statement); } /** * Match statement against the pattern for drop function * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchDropFunction(String statement) { return PAT_DROP_FUNCTION.matcher(statement); } /** * Match statement against pattern for drop procedure * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchDropProcedure(String statement) { return PAT_DROP_PROCEDURE.matcher(statement); } /** * Match statement against pattern for allow/partition clauses of create procedure statement * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchAnyCreateProcedureStatementClause(String statement) { return PAT_ANY_CREATE_PROCEDURE_STATEMENT_CLAUSE.matcher(statement); } /** * Match statement against pattern for export/partition clauses of create stream statement * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchAnyCreateStreamStatementClause(String statement) { return PAT_ANY_CREATE_STREAM_STATEMENT_CLAUSE.matcher(statement); } /** * Match statement against pattern for replicate table * @param statement statement to match against * @return pattern matcher object */ public static Matcher matchReplicateTable(String statement) { return PAT_REPLICATE_TABLE.matcher(statement); } /** * Build a pattern segment to accept a single optional ALLOW or PARTITION clause * to modify CREATE PROCEDURE statements. * * @param captureTokens Capture individual tokens if true * @return Inner pattern to be wrapped by the caller as appropriate * * Capture groups (when captureTokens is true): * (1) ALLOW clause: entire role list with commas and internal whitespace * (2) PARTITION clause: procedure name * (3) PARTITION clause: table name * (4) PARTITION clause: column name */ private static SQLPatternPart makeInnerProcedureModifierClausePattern(boolean captureTokens) { return SPF.oneOf( SPF.clause( SPF.token("allow"), SPF.group(captureTokens, SPF.commaList(SPF.userName())) ), SPF.clause( SPF.token("partition"), SPF.token("on"), SPF.token("table"), SPF.group(captureTokens, SPF.databaseObjectName()), SPF.token("column"), SPF.group(captureTokens, SPF.databaseObjectName()), SPF.optional( SPF.clause( SPF.token("parameter"), SPF.group(captureTokens, SPF.integer()) ) ) ) ); } /** * Build a pattern segment to accept and parse a single optional ALLOW or PARTITION * clause used to modify a CREATE PROCEDURE statement. * * @return ALLOW/PARTITION modifier clause parsing pattern. * * Capture groups: * (1) ALLOW clause: entire role list with commas and internal whitespace * (2) PARTITION clause: procedure name * (3) PARTITION clause: table name * (4) PARTITION clause: column name */ static SQLPatternPart parsedProcedureModifierClause() { return SPF.clause(makeInnerProcedureModifierClausePattern(true)); } /** * Build a pattern segment to recognize all the ALLOW or PARTITION modifier clauses * of a CREATE PROCEDURE statement. * * @return Pattern to be used by the caller inside a CREATE PROCEDURE pattern. * * Capture groups: * (1) All ALLOW/PARTITION modifier clauses as one string */ static SQLPatternPart unparsedProcedureModifierClauses() { // Force the leading space to go inside the repeat block. return SPF.capture(SPF.repeat(makeInnerProcedureModifierClausePattern(false))).withFlags(SQLPatternFactory.ADD_LEADING_SPACE_TO_CHILD); } /** * Build a pattern segment to accept a single optional EXPORT or PARTITION clause * to modify CREATE STREAM statements. * * @param captureTokens Capture individual tokens if true * @return Inner pattern to be wrapped by the caller as appropriate * * Capture groups (when captureTokens is true): * (1) EXPORT clause: target name * (2) PARTITION clause: column name */ private static SQLPatternPart makeInnerStreamModifierClausePattern(boolean captureTokens) { return SPF.oneOf( SPF.clause( SPF.token("export"),SPF.token("to"),SPF.token("target"), SPF.group(captureTokens, SPF.databaseObjectName()) ), SPF.clause( SPF.token("partition"), SPF.token("on"), SPF.token("column"), SPF.group(captureTokens, SPF.databaseObjectName()) ) ); } /** * Build a pattern segment to accept and parse a single optional EXPORT or PARTITION * clause used to modify a CREATE STREAM statement. * * @return EXPORT/PARTITION modifier clause parsing pattern. * * Capture groups: * (1) EXPORT clause: target name * * (2) PARTITION clause: column name */ static SQLPatternPart parsedStreamModifierClause() { return SPF.clause(makeInnerStreamModifierClausePattern(true)); } /** * Build a pattern segment to recognize all the EXPORT or PARTITION modifier clauses * of a CREATE STREAM statement. * * @return Pattern to be used by the caller inside a CREATE STREAM pattern. * * Capture groups: * (1) All EXPORT/PARTITION modifier clauses as one string */ private static SQLPatternPart unparsedStreamModifierClauses() { // Force the leading space to go inside the repeat block. return SPF.capture(SPF.repeat(makeInnerStreamModifierClausePattern(false))).withFlags(SQLPatternFactory.ADD_LEADING_SPACE_TO_CHILD); } //========== Other utilities from or for SQLCommand ========== /** * Parses locally-interpreted commands with a prefix and a single quoted * or unquoted string argument. * This can be more general if the need arises, e.g. more than one argument * or other argument data types. */ public static class SingleArgumentCommandParser { final String prefix; final Pattern patPrefix; final Pattern patFull; final String argName; /** * Constructor * @param prefix command prefix (blank separator is replaced with \s+) */ SingleArgumentCommandParser(String prefix, String argName) { // Replace single space with flexible whitespace pattern. this.prefix = prefix.toUpperCase(); String prefixPat = prefix.replace(" ", "\\s+"); this.patPrefix = Pattern.compile( String.format( "^\\s*" + // optional indent at start of line "%s" + // modified prefix "\\s" + // a required whitespace ".*$", // arbitrary end matter? prefixPat), Pattern.CASE_INSENSITIVE); this.patFull = Pattern.compile( String.format( "^\\s*" + // optional indent at start of line "%s" + // modified prefix "\\s+" + // a required whitespace "([^;]+)" + // at least one other non-semicolon character (?) "[;\\s]*$", // optional trailing semicolons or whitespace prefixPat), Pattern.CASE_INSENSITIVE); this.argName = argName; } /** * Parse line and return argument or null if parsing fails. * @param line input line * @return output argument or null if parsing fails */ String parse(String line) throws SQLParser.Exception { // If it doesn't start with the expected command prefix return null. // Allows better errors for missing or inappropriate arguments, // rather than passing it along to the engine for a strange error. if (line == null || !this.patPrefix.matcher(line).matches()) { return null; } Matcher matcher = this.patFull.matcher(line); String arg = null; if (matcher.matches()) { arg = parseOptionallyQuotedString(matcher.group(1)); if (arg == null) { throw new SQLParser.Exception("Bad %s argument to %s: %s", this.argName, this.prefix, arg); } } else { throw new SQLParser.Exception("Missing %s argument to %s.", this.argName, this.prefix); } return arg; } private static String parseOptionallyQuotedString(String sIn) throws SQLParser.Exception { String sOut = null; if (sIn != null) { // If it starts with a quote make sure it ends with the same one. if (sIn.startsWith("'") || sIn.startsWith("\"")) { if (sIn.length() > 1 && sIn.endsWith(sIn.substring(0, 1))) { sOut = sIn.substring(1, sIn.length() - 1); } else { throw new SQLParser.Exception("Quoted string is not properly closed: %s", sIn); } } else { // Unquoted string returned as is. sOut = sIn; } } return sOut; } } public static List<String> parseQuery(String query) { if (query == null) { return null; } //* enable to debug */ System.err.println("Parsing command queue:\n" + query); /* * Here begins the struggle between honoring comment starters and * honoring single quotes and honoring semicolons as statement separators. * * For example, whole-line comments are eliminated early -- assumed * never to be part of text literals, even though a text literal could * have been started on a prior line and could optionally be ended * with a quote on the current line and optionally followed by a * statement-ending semicolon all within the supposed comment line. */ query = AnyWholeLineComments.matcher(query).replaceAll(""); /* * replace all escaped single quotes with the #(SQL_PARSER_ESCAPE_SINGLE_QUOTE) tag */ query = EscapedSingleQuote.matcher(query).replaceAll("#(SQL_PARSER_ESCAPE_SINGLE_QUOTE)"); /* * Move all single quoted strings into the string fragments list, and do in place * replacements with numbered instances of the #(SQL_PARSER_STRING_FRAGMENT#[n]) tag * * WARNING: ENG-7594 This will find a quote (perhaps an informal * apostrophe) in an end-of-line comment and take it as the start * of a quoted string, hiding everything between it and the next * quote as literal text, including any semicolons or comment * starters in between. * Properly preserving semicolons and recognizing all comment * boundaries is tricky, especially in a way that preserves * quoted literals that contain "--", even literals that may be * started and/or terminated on a different line from the "--". * I (--paul) would find it comforting to rely on some interface * to HSQL parser technology for this, * The other possibility is to use SQLLexer.splitStatements * if it has already solved this problem. * And yet we don't yet know how compatible either of those is with * our intended free-form syntax for "exec" commands -- that may be * a bit TOO free form and may require tightening up before we can * find any reasonable solution. */ Matcher stringFragmentMatcher = SingleQuotedString.matcher(query); ArrayList<String> stringFragments = new ArrayList<>(); int i = 0; while (stringFragmentMatcher.find()) { stringFragments.add(stringFragmentMatcher.group()); query = stringFragmentMatcher.replaceFirst("#(SQL_PARSER_STRING_FRAGMENT#" + i + ")"); stringFragmentMatcher = SingleQuotedString.matcher(query); i++; } // Strip out inline comments // At this point, all the quoted strings have been pulled out of the // code mostly because they may contain semicolons. // They will not be restored until after the split. // So any user's quoted string containing ';' will be safe here. // OTOH, this next line MAY eliminate blocks of code after any // end-on-line comment that contains an unbalanced quote until // the following quote. ENG-7594 // The reason for eliminating the comments here and now is to make sure that // comment text containing a semicolon does not cause an erroneous statement // split mid-comment. query = EndOfLineComment.matcher(query).replaceAll(""); String[] sqlFragments = query.split("\\s*;+\\s*"); ArrayList<String> queries = new ArrayList<>(); for (String fragment : sqlFragments) { if (fragment.isEmpty()) { continue; } if (fragment.indexOf("#(SQL_PARSER_STRING_FRAGMENT#") > -1) { int k = 0; for (String strFrag : stringFragments) { fragment = fragment.replace("#(SQL_PARSER_STRING_FRAGMENT#" + k + ")", strFrag); k++; } } fragment = fragment.replace("#(SQL_PARSER_ESCAPE_SINGLE_QUOTE)", "''"); queries.add(fragment); } return queries; } // Process the quirky syntax for "exec" arguments -- a procedure name and // parameter values (optionally SINGLE-quoted) separated by arbitrary // whitespace and commas. // Assumes that this is the exact text between the "exec/execute" and // its terminating semicolon (exclusive) and that // to the extent that comments are supported they have already been stripped out. private static List<String> parseExecParameters(String paramText) { final String SafeParamStringValuePattern = "#(SQL_PARSER_SAFE_PARAMSTRING)"; // Find all quoted strings. // Mask out strings that contain whitespace or commas // that must not be confused with parameter separators. // "Safe" strings that don't contain these characters don't need to be masked // but they DO need to be found and explicitly skipped so that their closing // quotes don't trigger a false positive for the START of an unsafe string. // Skipping is accomplished by resetting paramText to an offset substring // after copying the skipped (or substituted) text to a string builder. ArrayList<String> originalString = new ArrayList<>(); Matcher stringMatcher = SingleQuotedString.matcher(paramText); StringBuilder safeText = new StringBuilder(); while (stringMatcher.find()) { // Save anything before the found string. safeText.append(paramText.substring(0, stringMatcher.start())); String asMatched = stringMatcher.group(); if (SingleQuotedStringContainingParameterSeparators.matcher(asMatched).matches()) { // The matched string is unsafe, provide cover for it in safeText. originalString.add(asMatched); safeText.append(SafeParamStringValuePattern); } else { // The matched string is safe. Add it to safeText. safeText.append(asMatched); } paramText = paramText.substring(stringMatcher.end()); stringMatcher = SingleQuotedString.matcher(paramText); } // Save anything after the last found string. safeText.append(paramText); ArrayList<String> params = new ArrayList<>(); int subCount = 0; int neededSubs = originalString.size(); // Split the params at the separators String[] split = safeText.toString().split("[\\s,]+"); for (String fragment : split) { if (fragment.isEmpty()) { continue; // ignore effects of leading or trailing separators } // Replace each substitution in order exactly once. if (subCount < neededSubs) { // Substituted strings will normally take up an entire parameter, // but some cases like parameters containing escaped single quotes // may require multiple serial substitutions. while (fragment.indexOf(SafeParamStringValuePattern) > -1) { fragment = fragment.replace(SafeParamStringValuePattern, originalString.get(subCount)); ++subCount; } } params.add(fragment); } assert(subCount == neededSubs); return params; } /** * Check whether statement is terminated by a semicolon. * @param statement statement to check * @return true if it is terminated by a semicolon */ public static boolean isSemiColonTerminated(String statement) { return SemicolonToken.matcher(statement).matches(); } /** * Check for EXIT command. * @param statement statement to check * @return true if it is EXIT command */ public static boolean isExitCommand(String statement) { //TODO: consider processing match groups to detect and // complain about garbage parameters. return ExitToken.matcher(statement).matches(); } /** * Results from parseRecallStatement */ public static class ParseRecallResults { private final int line; private final String error; ParseRecallResults(int line) { this.line = line; this.error = null; } ParseRecallResults(String error) { this.line = -1; this.error = error; } // Attempts to use these methods gets a mysterious NoSuchMethodError, // so keep them disabled and keep the attributes public for now. public int getLine() { return line; } public String getError() { return error; } } /** * Parse RECALL statement for sqlcmd. * @param statement statement to parse * @param lineMax maximum line # + 1 * @return results object or NULL if statement wasn't recognized */ public static ParseRecallResults parseRecallStatement(String statement, int lineMax) { Matcher matcher = RecallToken.matcher(statement); if (matcher.matches()) { String commandWordTerminator = matcher.group(1); String lineNumberText = matcher.group(2); String error; if (OneWhitespace.matcher(commandWordTerminator).matches()) { String trailings = matcher.group(3) + ";" + matcher.group(4); // In a valid command, both "trailings" groups should be empty. if (trailings.equals(";")) { try { int line = Integer.parseInt(lineNumberText) - 1; if (line < 0 || line > lineMax) { throw new NumberFormatException(); } // Return the recall line number. return new ParseRecallResults(line); } catch (NumberFormatException e) { error = "Invalid RECALL line number argument: '" + lineNumberText + "'"; } } // For an invalid form of the command, // return an approximation of the garbage input. else { error = "Invalid RECALL line number argument: '" + lineNumberText + " " + trailings + "'"; } } else if (commandWordTerminator.equals("") || commandWordTerminator.equals(";")) { error = "Incomplete RECALL command. RECALL expects a line number argument."; } else { error = "Invalid RECALL command: a space and line number are required after 'recall'"; } return new ParseRecallResults(error); } return null; } /** * An enum that describes the options that can be applied * to sqlcmd's "file" command */ static public enum FileOption { PLAIN { @Override String optionString() { return ""; } }, BATCH { @Override String optionString() { return "-batch "; } }, INLINEBATCH { @Override String optionString() { return "-inlinebatch "; } }; abstract String optionString(); } /** * This class encapsulates information produced by * parsing sqlcmd's "file" command. */ public static class FileInfo { private final FileInfo m_context; private final FileOption m_option; private final File m_file; private final String m_delimiter; private static FileInfo m_oneForSystemIn = null; // Create on demand. FileInfo(FileInfo context, FileOption option, String filenameOrDelimiter) { m_context = context; m_option = option; switch (option) { case PLAIN: case BATCH: m_file = new File(filenameOrDelimiter); m_delimiter = null; break; case INLINEBATCH: default: assert(option == FileOption.INLINEBATCH); assert(m_context != null); m_file = null; m_delimiter = filenameOrDelimiter; break; } } // special case constructor for System.in. private FileInfo() { m_context = null; m_option = FileOption.PLAIN; m_file = null; m_delimiter = null; } /** @return a dummy FileInfo instance to describe System.in **/ public static FileInfo forSystemIn() { if (m_oneForSystemIn == null) { m_oneForSystemIn = new FileInfo() { @Override public String getFilePath() { return "(standard input)"; } }; } return m_oneForSystemIn; } public File getFile() { return m_file; } public String getFilePath() { switch (m_option) { case PLAIN: case BATCH: return m_file.getPath(); case INLINEBATCH: default: assert(m_option == FileOption.INLINEBATCH); return "(inline batch delimited by '" + m_delimiter + "' in " + m_context.getFilePath() + ")"; } } public String getDelimiter() { assert (m_option == FileOption.INLINEBATCH); return m_delimiter; } public boolean isBatch() { return m_option == FileOption.BATCH || m_option == FileOption.INLINEBATCH; } public FileOption getOption() { return m_option; } /** * This is actually echoed back to the user so make it look * more or less like their input line. **/ @Override public String toString() { return "FILE " + m_option.optionString() + ((m_file != null) ? m_file.toString() : m_delimiter); } } /** * Parse FILE statement for sqlcmd. * @param fileInfo optional parent file context for better diagnostics. * @param statement statement to parse * @return File object or NULL if statement wasn't recognized */ public static FileInfo parseFileStatement(FileInfo parentContext, String statement) { Matcher fileMatcher = FileToken.matcher(statement); if (! fileMatcher.lookingAt()) { // This input does not start with FILE, // so it's not a file command, it's something else. // Return to caller a null and no errors. return null; } String remainder = statement.substring(fileMatcher.end(), statement.length()); Matcher inlineBatchMatcher = DashInlineBatchToken.matcher(remainder); if (inlineBatchMatcher.lookingAt()) { remainder = remainder.substring(inlineBatchMatcher.end(), remainder.length()); Matcher delimiterMatcher = DelimiterToken.matcher(remainder); // use matches here (not lookingAt) because we want to match // all of the remainder, not just beginning if (delimiterMatcher.matches()) { String delimiter = delimiterMatcher.group(1); return new FileInfo(parentContext, FileOption.INLINEBATCH, delimiter); } throw new SQLParser.Exception( "Did not find valid delimiter for \"file -inlinebatch\" command."); } // It is either a plain or a -batch file command. FileOption option = FileOption.PLAIN; Matcher batchMatcher = DashBatchToken.matcher(remainder); if (batchMatcher.lookingAt()) { option = FileOption.BATCH; remainder = remainder.substring(batchMatcher.end(), remainder.length()); } Matcher filenameMatcher = FilenameToken.matcher(remainder); String filename = null; // Use matches to match all input, not just beginning if (filenameMatcher.matches()) { filename = filenameMatcher.group(1); // Trim whitespace from beginning and end of the file name. // User may have wanted quoted whitespace at the beginning or end // of the file name, but that seems very unlikely. filename = filename.trim(); } // If no filename, or a filename of only spaces, then throw an error. if (filename == null || filename.length() == 0) { String msg = String.format("Did not find valid file name in \"file%s\" command.", option == FileOption.BATCH ? " -batch" : ""); throw new SQLParser.Exception(msg); } if (filename.startsWith("~")) { filename = filename.replaceFirst("~", System.getProperty("user.home")); } return new FileInfo(parentContext, option, filename); } /** * Parse FILE statement for interactive sqlcmd (or simple tests). * @param statement statement to parse * @return File object or NULL if statement wasn't recognized */ public static FileInfo parseFileStatement(String statement) { // There is no parent file context to reference. return parseFileStatement(null, statement); } /** * Parse a SHOW or LIST statement for sqlcmd. * @param statement statement to parse * @return String containing captured argument(s) possibly invalid, * or null if a show/list statement wasn't recognized */ public static String parseShowStatementSubcommand(String statement) { Matcher matcher = ShowToken.matcher(statement); if (matcher.matches()) { String commandWordTerminator = matcher.group(1); if (OneWhitespace.matcher(commandWordTerminator).matches()) { String trailings = matcher.group(3) + ";" + matcher.group(4); // In a valid command, both "trailings" groups should be empty. if (trailings.equals(";")) { // Return the subcommand keyword -- possibly a valid one. return matcher.group(2); } // For an invalid form of the command, // return an approximation of the garbage input. return matcher.group(2) + " " + trailings; } if (commandWordTerminator.equals("") || commandWordTerminator.equals(";")) { return commandWordTerminator; // EOL or ; reached before subcommand } } return null; } /** * Parse HELP statement for sqlcmd. * The sub-command will be "" if the user just typed HELP. * @param statement statement to parse * @return Sub-command or NULL if statement wasn't recognized */ public static String parseHelpStatement(String statement) { Matcher matcher = HelpToken.matcher(statement); if (matcher.matches()) { String commandWordTerminator = matcher.group(1); if (OneWhitespace.matcher(commandWordTerminator).matches()) { String trailings = matcher.group(3) + ";" + matcher.group(4); // In a valid command, both "trailings" groups should be empty. if (trailings.equals(";")) { // Return the subcommand keyword -- possibly a valid one. return matcher.group(2); } // For an invalid form of the command, // return an approximation of the garbage input. return matcher.group(2) + " " + trailings; } if (commandWordTerminator.equals("") || commandWordTerminator.equals(";")) { return ""; // EOL or ; reached before subcommand } return matcher.group(1).trim(); } return null; } /** * Parse a date string. We parse the documented forms, which are: * <ul> * <li>YYYY-MM-DD</li> * <li>YYYY-MM-DD HH:MM:SS</li> * <li>YYYY-MM-DD HH:MM:SS.SSSSSS</li> * </ul> * * As it turns out, TimestampType takes string parameters in just this * format. So, we defer to TimestampType, and return what it * constructs. This has microsecond granularity. * * @param dateIn input date string * @return TimestampType object * @throws SQLParser.Exception */ public static TimestampType parseDate(String dateIn) { // Remove any quotes around the timestamp value. ENG-2623 String dateRepled = dateIn.replaceAll("^\"|\"$", "").replaceAll("^'|'$", ""); return new TimestampType(dateRepled); } public static GeographyPointValue parseGeographyPoint(String param) { int spos = param.indexOf("'"); int epos = param.lastIndexOf("'"); if (spos < 0) { spos = -1; } if (epos < 0) { epos = param.length(); } return GeographyPointValue.fromWKT(param.substring(spos+1, epos)); } public static GeographyValue parseGeography(String param) { int spos = param.indexOf("'"); int epos = param.lastIndexOf("'"); if (spos < 0) { spos = -1; } if (epos < 0) { epos = param.length(); } return GeographyValue.fromWKT(param.substring(spos+1, epos)); } /** * Given a parameter string, if it's of the form x'0123456789ABCDEF', * return a string containing just the digits. Otherwise, return null. */ public static String getDigitsFromHexLiteral(String paramString) { Matcher matcher = SingleQuotedHexLiteral.matcher(paramString); if (matcher.matches()) { return matcher.group(1); } return null; } /** * Given a string of hex digits, produce a long value, assuming * a 2's complement representation. */ public static long hexDigitsToLong(String hexDigits) throws SQLParser.Exception { // BigInteger.longValue() will truncate to the lowest 64 bits, // so we need to explicitly check if there's too many digits. if (hexDigits.length() > 16) { throw new SQLParser.Exception("Too many hexadecimal digits for BIGINT value"); } if (hexDigits.length() == 0) { throw new SQLParser.Exception("Zero hexadecimal digits is invalid for BIGINT value"); } // The method // Long.parseLong(<digits>, <radix>); // Doesn't quite do what we want---it expects a '-' to // indicate negative values, and doesn't want the sign bit set // in the hex digits. // // Once we support Java 1.8, we can use Long.parseUnsignedLong(<digits>, 16) // instead. long val = new BigInteger(hexDigits, 16).longValue(); return val; } /** * Results returned by parseExecuteCall() */ public static class ExecuteCallResults { public String procedure = null; public List<String> params = null; public List<String> paramTypes = null; // Uppercase param. // Remove any quotes. // Trim private static String preprocessParam(String param) { if ((param.charAt(0) == '\'' && param.charAt(param.length()-1) == '\'') || (param.charAt(0) == '"' && param.charAt(param.length()-1) == '"')) { // The position of the closing quote, param.length()-1 is where to end the substring // to get a result with two fewer characters. param = param.substring(1, param.length()-1); } param = param.trim(); param = param.toUpperCase(); return param; } private static String friendlyTypeDescription(String paramType) { String friendly = FRIENDLY_TYPE_NAMES.get(paramType); if (friendly != null) { return friendly; } return paramType; } public Object[] getParameterObjects() throws SQLParser.Exception { Object[] objectParams = new Object[this.params.size()]; int i = 0; try { for (; i < this.params.size(); i++) { String paramType = this.paramTypes.get(i); String param = this.params.get(i); Object objParam = null; // For simplicity, handle first the types that don't allow null as a special value. if (paramType.equals("bit")) { //TODO: upper/mixed case Yes and True should be treated as "1"? //TODO: non-0 integers besides 1 should be treated as "1"? //TODO: garbage values and null should be rejected, not accepted as "0": // (case-insensitive) "no"/"false"/"0" should be required for "0"? if (param.equals("yes") || param.equals("true") || param.equals("1")) { objParam = (byte)1; } else { objParam = (byte)0; } } else if (paramType.equals("statisticscomponent") || paramType.equals("sysinfoselector") || paramType.equals("metadataselector")) { objParam = preprocessParam(param); } else if ( ! "null".equalsIgnoreCase(param)) { if (paramType.equals("tinyint")) { objParam = Byte.parseByte(param); } else if (paramType.equals("smallint")) { objParam = Short.parseShort(param); } else if (paramType.equals("int") || paramType.equals("integer")) { objParam = Integer.parseInt(param); } else if (paramType.equals("bigint")) { // Could be literal of the form x'0007' // or just a simple decimal literal String hexDigits = getDigitsFromHexLiteral(param); if (hexDigits != null) { objParam = hexDigitsToLong(hexDigits); } else { // It's a decimal literal objParam = Long.parseLong(param); } } else if (paramType.equals("float")) { objParam = Double.parseDouble(param); } else if (paramType.equals("varchar")) { objParam = Unquote.matcher(param).replaceAll("").replace("''","'"); } else if (paramType.equals("decimal")) { objParam = new BigDecimal(param); } else if (paramType.equals("timestamp")) { objParam = parseDate(param); } else if (paramType.equals("geography_point")) { objParam = parseGeographyPoint(param); } else if (paramType.equals("geography")) { objParam = parseGeography(param); } else if (paramType.equals("varbinary") || paramType.equals("tinyint_array")) { // A VARBINARY literal may or may not be // prefixed with an X. String hexDigits = getDigitsFromHexLiteral(param); if (hexDigits == null) { hexDigits = Unquote.matcher(param).replaceAll(""); } // The following call with throw an exception if we // have an odd number of hex digits. objParam = Encoder.hexDecode(hexDigits); } else { throw new SQLParser.Exception("Unsupported Data Type: %s", paramType); } } // else param is keyword "null", so leave objParam as null. objectParams[i] = objParam; } } catch (NumberFormatException nfe) { throw new SQLParser.Exception(nfe, "Invalid parameter: Expected a %s value, got '%s' (param %d).", friendlyTypeDescription(this.paramTypes.get(i)), this.params.get(i), i+1); } return objectParams; } // No public constructor. ExecuteCallResults() {} @Override public String toString() { return "ExecuteCallResults { " + "procedure: " + procedure + ", " + "params: " + params + ", " + "paramTypes: " + paramTypes + " }"; } } /** * Parse EXECUTE procedure call. * @param statement statement to parse * @param procedures maps procedures to parameter signature maps * @return results object or NULL if statement wasn't recognized * @throws SQLParser.Exception */ public static ExecuteCallResults parseExecuteCall( String statement, Map<String,Map<Integer, List<String>>> procedures) throws SQLParser.Exception { assert(procedures != null); return parseExecuteCallInternal(statement, procedures); } /** * Parse EXECUTE procedure call for testing without looking up parameter types. * Used for testing. * @param statement statement to parse * @param procedures maps procedures to parameter signature maps * @return results object or NULL if statement wasn't recognized * @throws SQLParser.Exception */ public static ExecuteCallResults parseExecuteCallWithoutParameterTypes( String statement) throws SQLParser.Exception { return parseExecuteCallInternal(statement, null); } /** * Private implementation of parse EXECUTE procedure call. * Also supports short-circuiting procedure lookup for testing. * @param statement statement to parse * @param procedures maps procedures to parameter signature maps * @return results object or NULL if statement wasn't recognized * @throws SQLParser.Exception */ private static ExecuteCallResults parseExecuteCallInternal( String statement, Map<String,Map<Integer, List<String>>> procedures ) throws SQLParser.Exception { Matcher matcher = ExecuteCallPreamble.matcher(statement); if ( ! matcher.lookingAt()) { return null; } String commandWordTerminator = matcher.group(1); if (OneWhitespace.matcher(commandWordTerminator).matches() || // Might as well accept a comma delimiter anywhere in the exec command, // even near the start commandWordTerminator.equals(",")) { ExecuteCallResults results = new ExecuteCallResults(); String rawParams = statement.substring(matcher.end()); results.params = parseExecParameters(rawParams); results.procedure = results.params.remove(0); // TestSqlCmdInterface passes procedures==null because it // doesn't need/want the param types. if (procedures == null) { results.paramTypes = null; return results; } Map<Integer, List<String>> signature = procedures.get(results.procedure); if (signature == null) { throw new SQLParser.Exception("Undefined procedure: %s", results.procedure); } results.paramTypes = signature.get(results.params.size()); if (results.paramTypes == null || results.params.size() != results.paramTypes.size()) { String expectedSizes = ""; for (Integer expectedSize : signature.keySet()) { expectedSizes += expectedSize + ", "; } throw new SQLParser.Exception( "Invalid parameter count for procedure: %s (expected: %s received: %d)", results.procedure, expectedSizes, results.params.size()); } return results; } if (commandWordTerminator.equals(";")) { // EOL or ; reached before subcommand throw new SQLParser.Exception( "Incomplete EXECUTE command. EXECUTE requires a procedure name argument."); } throw new SQLParser.Exception( "Invalid EXECUTE command. unexpected input: '" + commandWordTerminator + "'."); } /** * Parse EXPLAIN <query> * @param statement statement to parse * @return query parameter string or NULL if statement wasn't recognized */ public static String parseExplainCall(String statement) { Matcher matcher = ExplainCallPreamble.matcher(statement); if ( ! matcher.lookingAt()) { return null; } return statement.substring(matcher.end()); } /** * Parse EXPLAINPROC <procedure> * @param statement statement to parse * @return procedure name parameter string or NULL if statement wasn't recognized */ public static String parseExplainProcCall(String statement) { Matcher matcher = ExplainProcCallPreamble.matcher(statement); if ( ! matcher.lookingAt()) { return null; } // This all could probably be done more elegantly via a group extracted // from a more comprehensive regexp. // Clean up any extra spaces around the remainder of the line, // which should be a proc name. return statement.substring(matcher.end()).trim(); } /** * Parse EXPLAINVIEW <view> * @param statement statement to parse * @return view name parameter string or NULL if statement wasn't recognized */ public static String parseExplainViewCall(String statement) { Matcher matcher = ExplainViewCallPreamble.matcher(statement); if ( ! matcher.lookingAt()) { return null; } // This all could probably be done more elegantly via a group extracted // from a more comprehensive regexp. // Clean up any extra spaces around the remainder of the line, // which should be a view name. return statement.substring(matcher.end()).trim(); } /** * Check if query is DDL * @param query query to check * @return true if query is DDL */ public static boolean queryIsDDL(String query) { return SQLLexer.extractDDLToken(query) != null; } /** * @param statement input statement * @return jar file path argument, or null if statement is not "LOAD CLASSES" * @throws SQLParser.Exception if the LOAD CLASSES argument is not a valid file path. */ public static String parseLoadClasses(String statement) throws SQLParser.Exception { String arg = loadClassesParser.parse(statement); if (arg == null) { return null; } if (! new File(arg).isFile()) { throw new SQLParser.Exception("Jar file not found: '" + arg + "'"); } return arg; } /** * @param statement input statement * @return class selector argument, or null if statement is not "REMOVE CLASSES" * @throws SQLParser.Exception if the REMOVE CLASSES argument is not a valid class selector. */ public static String parseRemoveClasses(String statement) throws SQLParser.Exception { String arg = removeClassesParser.parse(statement); if (arg == null) { return null; } // reject obviously bad class selectors if (!ClassSelectorToken.matcher(arg).matches()) { throw new SQLParser.Exception("Invalid class selector: '" + arg + "'"); } return arg; } /** * @param line * @return true if the input contains only a SQL line comment with optional indent. */ public static boolean isWholeLineComment(String line) { return OneWholeLineComment.matcher(line).matches(); } /** * Make sure that the batch starts with an appropriate DDL verb. We do not * look further than the first token of the first non-comment and non-whitespace line. * * Empty batches are considered to be trivially valid. * * @param batch A SQL string containing multiple statements separated by semicolons * @return true if the first keyword of the first statement is a DDL verb * like CREATE, ALTER, DROP, PARTITION, DR, SET or EXPORT, * or if the batch is empty. * See the official list of DDL verbs in the "// Supported verbs" section of * the static initializer for SQLLexer.VERB_TOKENS) */ public static boolean appearsToBeValidDDLBatch(String batch) { BufferedReader reader = new BufferedReader(new StringReader(batch)); String line; try { while ((line = reader.readLine()) != null) { if (isWholeLineComment(line)) { continue; } line = line.trim(); if (line.equals("")) continue; // we have a non-blank line that contains more than just a comment. return queryIsDDL(line); } } catch (IOException e) { // This should never happen for a StringReader assert(false); } // trivial empty batch: no lines are non-blank or non-comments return true; } /** * Parse ECHO statement for sqlcmd. * The result will be "" if the user just typed ECHO. * @param statement statement to parse * @return Argument text or NULL if statement wasn't recognized */ public static String parseEchoStatement(String statement) { Matcher matcher = EchoToken.matcher(statement); if (matcher.matches()) { String commandWordTerminator = matcher.group(1); if (OneWhitespace.matcher(commandWordTerminator).matches()) { return matcher.group(2); } return ""; } return null; } /** * Parse ECHOERROR statement for sqlcmd. * The result will be "" if the user just typed ECHOERROR. * @param statement statement to parse * @return Argument text or NULL if statement wasn't recognized */ public static String parseEchoErrorStatement(String statement) { Matcher matcher = EchoErrorToken.matcher(statement); if (matcher.matches()) { String commandWordTerminator = matcher.group(1); if (OneWhitespace.matcher(commandWordTerminator).matches()) { return matcher.group(2); } return ""; } return null; } }