org.voltdb.parser.SQLParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of voltdbclient Show documentation
Show all versions of voltdbclient Show documentation
VoltDB client interface libraries
/* 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 .
*/
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
*
* 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 [ ... ] FROM
*
* 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 [ ... ] AS
*
* 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 PROCEDURE [ ... ] AS ### ### LANGUAGE
*
* CREATE PROCEDURE with inline implementation script, e.g. Groovy, statement regex
* NB supports only unquoted table and column names
* The only supported language is GROOVY for now, but to avoid confusing with the
* other CREATE PROCEDURE ... AS variant match anything that has the block delimiters.
*
* 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)
"\\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)
"(?:\\s+WITH\\s+" + // (start optional WITH clause block)
"(\\w+(?:\\s*,\\s*\\w+)*)" + // (2)
")?" + // (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)
"(\\s+IF EXISTS)?" + // (2)
";\\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("exisit")))
).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)|" + // 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)
"(?:\\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 FRIENDLY_TYPE_NAMES =
ImmutableMap.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 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 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 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 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 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 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 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:
*
* - YYYY-MM-DD
* - YYYY-MM-DD HH:MM:SS
* - YYYY-MM-DD HH:MM:SS.SSSSSS
*
*
* 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(, );
// 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(, 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 params = null;
public List 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>> 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>> 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> 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
* @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
* @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
* @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;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy