/* 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;
}
}