#region License
// The PostgreSQL License
//
// Copyright (C) 2015 The Npgsql Development Team
//
// Permission to use, copy, modify, and distribute this software and its
// documentation for any purpose, without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph and the following two paragraphs appear in all copies.
//
// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
//
// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS
// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#endregion
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
namespace Npgsql
{
static class SqlQueryParser
{
static readonly Array ParamNameCharTable;
static SqlQueryParser()
{
ParamNameCharTable = BuildParameterNameCharacterTable();
}
///
/// Receives a raw SQL query as passed in by the user, and performs some processing necessary
/// before sending to the backend.
/// This includes doing parameter placebolder processing (@p => $1), and splitting the query
/// up by semicolons if needed (SELECT 1; SELECT 2)
///
/// Raw user-provided query.
/// Whether the PostgreSQL session is configured to use standard conformant strings.
/// The parameters configured on the of this query.
/// An empty list to be populated with the queries parsed by this method
static internal void ParseRawQuery(string sql, bool standardConformantStrings, NpgsqlParameterCollection parameters, List queries)
{
Contract.Requires(sql != null);
Contract.Requires(queries != null && !queries.Any());
var currCharOfs = 0;
var end = sql.Length;
var ch = '\0';
int dollarTagStart;
int dollarTagEnd;
var currTokenBeg = 0;
var blockCommentLevel = 0;
queries.Clear();
// TODO: Recycle
var paramIndexMap = new Dictionary();
var currentSql = new StringWriter();
var currentParameters = new List();
None:
if (currCharOfs >= end) {
goto Finish;
}
var lastChar = ch;
ch = sql[currCharOfs++];
NoneContinue:
for (; ; lastChar = ch, ch = sql[currCharOfs++]) {
switch (ch) {
case '/':
goto BlockCommentBegin;
case '-':
goto LineCommentBegin;
case '\'':
if (standardConformantStrings)
goto Quoted;
else
goto Escaped;
case '$':
if (!IsIdentifier(lastChar))
goto DollarQuotedStart;
else
break;
case '"':
goto DoubleQuoted;
case ':':
if (lastChar != ':')
goto ParamStart;
else
break;
case '@':
if (lastChar != '@')
goto ParamStart;
else
break;
case ';':
goto SemiColon;
case 'e':
case 'E':
if (!IsLetter(lastChar))
goto EscapedStart;
else
break;
}
if (currCharOfs >= end) {
goto Finish;
}
}
ParamStart:
if (currCharOfs < end) {
lastChar = ch;
ch = sql[currCharOfs];
if (IsParamNameChar(ch)) {
if (currCharOfs - 1 > currTokenBeg) {
currentSql.Write(sql.Substring(currTokenBeg, currCharOfs - 1 - currTokenBeg));
}
currTokenBeg = currCharOfs++ - 1;
goto Param;
} else {
currCharOfs++;
goto NoneContinue;
}
}
goto Finish;
Param:
// We have already at least one character of the param name
for (; ; ) {
lastChar = ch;
if (currCharOfs >= end || !IsParamNameChar(ch = sql[currCharOfs])) {
var paramName = sql.Substring(currTokenBeg, currCharOfs - currTokenBeg);
int index;
if (!paramIndexMap.TryGetValue(paramName, out index)) {
// Parameter hasn't been seen before in this query
NpgsqlParameter parameter;
if (!parameters.TryGetValue(paramName, out parameter)) {
throw new Exception(
$"Parameter '{paramName}' referenced in SQL but not found in parameter list");
}
if (!parameter.IsInputDirection) {
throw new Exception(
$"Parameter '{paramName}' referenced in SQL but is an out-only parameter");
}
currentParameters.Add(parameter);
index = paramIndexMap[paramName] = currentParameters.Count;
}
currentSql.Write('$');
currentSql.Write(index);
currTokenBeg = currCharOfs;
if (currCharOfs >= end) {
goto Finish;
}
currCharOfs++;
goto NoneContinue;
} else {
currCharOfs++;
}
}
Quoted:
while (currCharOfs < end) {
if (sql[currCharOfs++] == '\'') {
ch = '\0';
goto None;
}
}
goto Finish;
DoubleQuoted:
while (currCharOfs < end) {
if (sql[currCharOfs++] == '"') {
ch = '\0';
goto None;
}
}
goto Finish;
EscapedStart:
if (currCharOfs < end) {
lastChar = ch;
ch = sql[currCharOfs++];
if (ch == '\'') {
goto Escaped;
}
goto NoneContinue;
}
goto Finish;
Escaped:
while (currCharOfs < end) {
ch = sql[currCharOfs++];
if (ch == '\'') {
goto MaybeConcatenatedEscaped;
}
if (ch == '\\') {
if (currCharOfs >= end) {
goto Finish;
}
currCharOfs++;
}
}
goto Finish;
MaybeConcatenatedEscaped:
while (currCharOfs < end) {
ch = sql[currCharOfs++];
if (ch == '\r' || ch == '\n') {
goto MaybeConcatenatedEscaped2;
}
if (ch != ' ' && ch != '\t' && ch != '\f') {
lastChar = '\0';
goto NoneContinue;
}
}
goto Finish;
MaybeConcatenatedEscaped2:
while (currCharOfs < end) {
ch = sql[currCharOfs++];
if (ch == '\'') {
goto Escaped;
}
if (ch == '-') {
if (currCharOfs >= end) {
goto Finish;
}
ch = sql[currCharOfs++];
if (ch == '-') {
goto MaybeConcatenatedEscapeAfterComment;
}
lastChar = '\0';
goto NoneContinue;
}
if (ch != ' ' && ch != '\t' && ch != '\n' & ch != '\r' && ch != '\f') {
lastChar = '\0';
goto NoneContinue;
}
}
goto Finish;
MaybeConcatenatedEscapeAfterComment:
while (currCharOfs < end) {
ch = sql[currCharOfs++];
if (ch == '\r' || ch == '\n') {
goto MaybeConcatenatedEscaped2;
}
}
goto Finish;
DollarQuotedStart:
if (currCharOfs < end) {
ch = sql[currCharOfs];
if (ch == '$') {
// Empty tag
dollarTagStart = dollarTagEnd = currCharOfs;
currCharOfs++;
goto DollarQuoted;
}
if (IsIdentifierStart(ch)) {
dollarTagStart = currCharOfs;
currCharOfs++;
goto DollarQuotedInFirstDelim;
}
lastChar = '$';
currCharOfs++;
goto NoneContinue;
}
goto Finish;
DollarQuotedInFirstDelim:
while (currCharOfs < end) {
lastChar = ch;
ch = sql[currCharOfs++];
if (ch == '$') {
dollarTagEnd = currCharOfs - 1;
goto DollarQuoted;
}
if (!IsDollarTagIdentifier(ch)) {
goto NoneContinue;
}
}
goto Finish;
DollarQuoted: {
var tag = sql.Substring(dollarTagStart - 1, dollarTagEnd - dollarTagStart + 2);
var pos = sql.IndexOf(tag, dollarTagEnd + 1); // Not linear time complexity, but that's probably not a problem, since PostgreSQL backend's isn't either
if (pos == -1) {
currCharOfs = end;
goto Finish;
}
currCharOfs = pos + dollarTagEnd - dollarTagStart + 2;
ch = '\0';
goto None;
}
LineCommentBegin:
if (currCharOfs < end) {
ch = sql[currCharOfs++];
if (ch == '-') {
goto LineComment;
}
lastChar = '\0';
goto NoneContinue;
}
goto Finish;
LineComment:
while (currCharOfs < end) {
ch = sql[currCharOfs++];
if (ch == '\r' || ch == '\n') {
goto None;
}
}
goto Finish;
BlockCommentBegin:
while (currCharOfs < end) {
ch = sql[currCharOfs++];
if (ch == '*') {
blockCommentLevel++;
goto BlockComment;
}
if (ch != '/') {
if (blockCommentLevel > 0) {
goto BlockComment;
}
lastChar = '\0';
goto NoneContinue;
}
}
goto Finish;
BlockComment:
while (currCharOfs < end) {
ch = sql[currCharOfs++];
if (ch == '*') {
goto BlockCommentEnd;
}
if (ch == '/') {
goto BlockCommentBegin;
}
}
goto Finish;
BlockCommentEnd:
while (currCharOfs < end) {
ch = sql[currCharOfs++];
if (ch == '/') {
if (--blockCommentLevel > 0) {
goto BlockComment;
}
goto None;
}
if (ch != '*') {
goto BlockComment;
}
}
goto Finish;
SemiColon:
currentSql.Write(sql.Substring(currTokenBeg, currCharOfs - currTokenBeg - 1));
queries.Add(new NpgsqlStatement(currentSql.ToString(), currentParameters));
while (currCharOfs < end) {
ch = sql[currCharOfs];
if (char.IsWhiteSpace(ch)) {
currCharOfs++;
continue;
}
// TODO: Handle end of line comment? Although psql doesn't seem to handle them...
currTokenBeg = currCharOfs;
paramIndexMap.Clear();
if (queries.Count > NpgsqlCommand.MaxStatements) {
throw new NotSupportedException(
$"A single command cannot contain more than {NpgsqlCommand.MaxStatements} queries");
}
currentSql = new StringWriter();
currentParameters = new List();
goto None;
}
return;
Finish:
currentSql.Write(sql.Substring(currTokenBeg, end - currTokenBeg));
queries.Add(new NpgsqlStatement(currentSql.ToString(), currentParameters));
}
static bool IsLetter(char ch)
{
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z';
}
static bool IsIdentifierStart(char ch)
{
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || 128 <= ch && ch <= 255;
}
static bool IsDollarTagIdentifier(char ch)
{
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || '0' <= ch && ch <= '9' || ch == '_' || 128 <= ch && ch <= 255;
}
static bool IsIdentifier(char ch)
{
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || '0' <= ch && ch <= '9' || ch == '_' || ch == '$' || 128 <= ch && ch <= 255;
}
static bool IsParamNameChar(char ch)
{
if (ch < '.' || ch > 'z') {
return false;
}
return ((byte)ParamNameCharTable.GetValue(ch) != 0);
}
static Array BuildParameterNameCharacterTable()
{
// Table has lower bound of (int)'.';
var paramNameCharTable = Array.CreateInstance(typeof(byte), new[] { 'z' - '.' + 1 }, new int[] { '.' });
paramNameCharTable.SetValue((byte)'.', '.');
for (int i = '0'; i <= '9'; i++) {
paramNameCharTable.SetValue((byte)i, i);
}
for (int i = 'A'; i <= 'Z'; i++) {
paramNameCharTable.SetValue((byte)i, i);
}
paramNameCharTable.SetValue((byte)'_', '_');
for (int i = 'a'; i <= 'z'; i++) {
paramNameCharTable.SetValue((byte)i, i);
}
return paramNameCharTable;
}
}
}