#region License
// The PostgreSQL License
//
// Copyright (C) 2017 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.Text;
using JetBrains.Annotations;
#pragma warning disable CA1034
// ReSharper disable once CheckNamespace
namespace NpgsqlTypes
{
///
/// Represents a PostgreSQL tsquery. This is the base class for lexeme, not, and and or nodes.
///
public abstract class NpgsqlTsQuery
{
///
/// Node kind
///
public NodeKind Kind { get; protected set; }
///
/// NodeKind
///
public enum NodeKind
{
///
/// Lexeme
///
Lexeme,
///
/// Not operator
///
Not,
///
/// And operator
///
And,
///
/// Or operator
///
Or,
///
/// Represents the empty tsquery. Should only be used at top level.
///
Empty
}
internal abstract void Write(StringBuilder sb, bool first = false);
///
/// Writes the tsquery in PostgreSQL's text format.
///
///
public override string ToString()
{
var sb = new StringBuilder();
Write(sb, true);
return sb.ToString();
}
///
/// Parses a tsquery in PostgreSQL's text format.
///
///
///
public static NpgsqlTsQuery Parse(string value)
{
if (value == null)
throw new ArgumentNullException(nameof(value));
var valStack = new Stack();
var opStack = new Stack();
var sb = new StringBuilder();
var pos = 0;
var expectingBinOp = false;
NextToken:
if (pos >= value.Length)
goto Finish;
var ch = value[pos++];
if (ch == '\'')
goto WaitEndComplex;
if ((ch == ')' || ch == '|' || ch == '&') && !expectingBinOp || (ch == '(' || ch == '!') && expectingBinOp)
throw new FormatException("Syntax error in tsquery. Unexpected token.");
if (ch == '(' || ch == '!' || ch == '&')
{
opStack.Push(ch);
expectingBinOp = false;
goto NextToken;
}
if (ch == '|')
{
if (opStack.Count > 0 && opStack.Peek() == '|')
{
if (valStack.Count < 2)
throw new FormatException("Syntax error in tsquery");
var right = valStack.Pop();
var left = valStack.Pop();
valStack.Push(new NpgsqlTsQueryOr(left, right));
// Implicit pop and repush |
}
else
opStack.Push('|');
expectingBinOp = false;
goto NextToken;
}
if (ch == ')')
{
while (opStack.Count > 0 && opStack.Peek() != '(')
{
if (valStack.Count < 2 || opStack.Peek() == '!')
throw new FormatException("Syntax error in tsquery");
var right = valStack.Pop();
var left = valStack.Pop();
valStack.Push(opStack.Pop() == '&' ? (NpgsqlTsQuery)new NpgsqlTsQueryAnd(left, right) : new NpgsqlTsQueryOr(left, right));
}
if (opStack.Count == 0)
throw new FormatException("Syntax error in tsquery: closing parenthesis without an opening parenthesis");
opStack.Pop();
goto PushedVal;
}
if (ch == ':')
throw new FormatException("Unexpected : while parsing tsquery");
if (char.IsWhiteSpace(ch))
goto NextToken;
pos--;
if (expectingBinOp)
throw new FormatException("Unexpected lexeme while parsing tsquery");
// Proceed to WaitEnd
WaitEnd:
if (pos >= value.Length || char.IsWhiteSpace(ch = value[pos]) || ch == '!' || ch == '&' || ch == '|' || ch == '(' || ch == ')')
{
valStack.Push(new NpgsqlTsQueryLexeme(sb.ToString()));
goto PushedVal;
}
pos++;
if (ch == ':')
{
valStack.Push(new NpgsqlTsQueryLexeme(sb.ToString()));
sb.Clear();
goto InWeightInfo;
}
if (ch == '\\')
{
if (pos >= value.Length)
throw new FormatException(@"Unexpected \ in end of value");
ch = value[pos++];
}
sb.Append(ch);
goto WaitEnd;
WaitEndComplex:
if (pos >= value.Length)
throw new FormatException("Missing terminating ' in string literal");
ch = value[pos++];
if (ch == '\'')
{
if (pos < value.Length && value[pos] == '\'')
{
ch = '\'';
pos++;
}
else
{
valStack.Push(new NpgsqlTsQueryLexeme(sb.ToString()));
if (pos < value.Length && value[pos] == ':')
{
pos++;
goto InWeightInfo;
}
goto PushedVal;
}
}
if (ch == '\\')
{
if (pos >= value.Length)
throw new FormatException(@"Unexpected \ in end of value");
ch = value[pos++];
}
sb.Append(ch);
goto WaitEndComplex;
InWeightInfo:
if (pos >= value.Length)
goto Finish;
ch = value[pos];
if (ch == '*')
((NpgsqlTsQueryLexeme)valStack.Peek()).IsPrefixSearch = true;
else if (ch == 'a' || ch == 'A')
((NpgsqlTsQueryLexeme)valStack.Peek()).Weights |= NpgsqlTsQueryLexeme.Weight.A;
else if (ch == 'b' || ch == 'B')
((NpgsqlTsQueryLexeme)valStack.Peek()).Weights |= NpgsqlTsQueryLexeme.Weight.B;
else if (ch == 'c' || ch == 'C')
((NpgsqlTsQueryLexeme)valStack.Peek()).Weights |= NpgsqlTsQueryLexeme.Weight.C;
else if (ch == 'd' || ch == 'D')
((NpgsqlTsQueryLexeme)valStack.Peek()).Weights |= NpgsqlTsQueryLexeme.Weight.D;
else
goto PushedVal;
pos++;
goto InWeightInfo;
PushedVal:
sb.Clear();
while (opStack.Count > 0 && (opStack.Peek() == '&' || opStack.Peek() == '!'))
{
if (opStack.Peek() == '&')
{
if (valStack.Count < 2)
throw new FormatException("Syntax error in tsquery");
var right = valStack.Pop();
var left = valStack.Pop();
valStack.Push(new NpgsqlTsQueryAnd(left, right));
}
else if (opStack.Peek() == '!')
{
if (valStack.Count == 0)
throw new FormatException("Syntax error in tsquery");
valStack.Push(new NpgsqlTsQueryNot(valStack.Pop()));
}
opStack.Pop();
}
expectingBinOp = true;
goto NextToken;
Finish:
while (opStack.Count > 0)
{
if (valStack.Count < 2 || (opStack.Peek() != '|' && opStack.Peek() != '&'))
throw new FormatException("Syntax error in tsquery");
var right = valStack.Pop();
var left = valStack.Pop();
valStack.Push(opStack.Pop() == '&' ? (NpgsqlTsQuery)new NpgsqlTsQueryAnd(left, right) : new NpgsqlTsQueryOr(left, right));
}
if (valStack.Count != 1)
throw new FormatException("Syntax error in tsquery");
return valStack.Pop();
}
}
///
/// TsQuery Lexeme node.
///
public sealed class NpgsqlTsQueryLexeme : NpgsqlTsQuery
{
string _text;
///
/// Lexeme text.
///
public string Text
{
get => _text;
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException("Text is null or empty string", nameof(value));
_text = value;
}
}
Weight _weights;
///
/// Weights is a bitmask of the Weight enum.
///
public Weight Weights
{
get => _weights;
set
{
if (((byte)value >> 4) != 0)
throw new ArgumentOutOfRangeException(nameof(value), "Illegal weights");
_weights = value;
}
}
///
/// Prefix search.
///
public bool IsPrefixSearch { get; set; }
///
/// Creates a tsquery lexeme with only lexeme text.
///
/// Lexeme text.
public NpgsqlTsQueryLexeme(string text) : this(text, Weight.None, false) { }
///
/// Creates a tsquery lexeme with lexeme text and weights.
///
/// Lexeme text.
/// Bitmask of enum Weight.
public NpgsqlTsQueryLexeme(string text, Weight weights) : this(text, weights, false) { }
///
/// Creates a tsquery lexeme with lexeme text, weights and prefix search flag.
///
/// Lexeme text.
/// Bitmask of enum Weight.
/// Is prefix search?
public NpgsqlTsQueryLexeme(string text, Weight weights, bool isPrefixSearch)
{
Kind = NodeKind.Lexeme;
Text = text;
Weights = weights;
IsPrefixSearch = isPrefixSearch;
}
///
/// Weight enum, can be OR'ed together.
///
#pragma warning disable CA1714
[Flags]
public enum Weight
#pragma warning restore CA1714
{
///
/// None
///
None = 0,
///
/// D
///
D = 1,
///
/// C
///
C = 2,
///
/// B
///
B = 4,
///
/// A
///
A = 8
}
internal override void Write(StringBuilder sb, bool first = false)
{
sb.Append('\'').Append(Text.Replace(@"\", @"\\").Replace("'", "''")).Append('\'');
if (IsPrefixSearch || Weights != Weight.None)
sb.Append(':');
if (IsPrefixSearch)
sb.Append('*');
if ((Weights & Weight.A) != Weight.None)
sb.Append('A');
if ((Weights & Weight.B) != Weight.None)
sb.Append('B');
if ((Weights & Weight.C) != Weight.None)
sb.Append('C');
if ((Weights & Weight.D) != Weight.None)
sb.Append('D');
}
}
///
/// TsQuery Not node.
///
public sealed class NpgsqlTsQueryNot : NpgsqlTsQuery
{
///
/// Child node
///
public NpgsqlTsQuery Child { get; set; }
///
/// Creates a not operator, with a given child node.
///
///
public NpgsqlTsQueryNot([CanBeNull] NpgsqlTsQuery child)
{
Kind = NodeKind.Not;
Child = child;
}
internal override void Write(StringBuilder sb, bool first = false)
{
sb.Append('!');
if (Child == null)
{
sb.Append("''");
}
else
{
if (Child.Kind != NodeKind.Lexeme)
sb.Append("( ");
Child.Write(sb, true);
if (Child.Kind != NodeKind.Lexeme)
sb.Append(" )");
}
}
}
///
/// Base class for TsQuery binary operators (& and |).
///
public abstract class NpgsqlTsQueryBinOp : NpgsqlTsQuery
{
///
/// Left child
///
public NpgsqlTsQuery Left { get; set; }
///
/// Right child
///
public NpgsqlTsQuery Right { get; set; }
}
///
/// TsQuery And node.
///
public sealed class NpgsqlTsQueryAnd : NpgsqlTsQueryBinOp
{
///
/// Creates an and operator, with two given child nodes.
///
///
///
public NpgsqlTsQueryAnd([CanBeNull] NpgsqlTsQuery left, [CanBeNull] NpgsqlTsQuery right)
{
Kind = NodeKind.And;
Left = left;
Right = right;
}
internal override void Write(StringBuilder sb, bool first = false)
{
Left.Write(sb);
sb.Append(" & ");
Right.Write(sb);
}
}
///
/// TsQuery Or Node.
///
public sealed class NpgsqlTsQueryOr : NpgsqlTsQueryBinOp
{
///
/// Creates an or operator, with two given child nodes.
///
///
///
public NpgsqlTsQueryOr([CanBeNull] NpgsqlTsQuery left, [CanBeNull] NpgsqlTsQuery right)
{
Kind = NodeKind.Or;
Left = left;
Right = right;
}
internal override void Write(StringBuilder sb, bool first = false)
{
if (!first)
sb.Append("( ");
Left.Write(sb);
sb.Append(" | ");
Right.Write(sb);
if (!first)
sb.Append(" )");
}
}
///
/// Represents an empty tsquery. Shold only be used as top node.
///
public sealed class NpgsqlTsQueryEmpty : NpgsqlTsQuery
{
///
/// Creates a tsquery that represents an empty query. Should not be used as child node.
///
public NpgsqlTsQueryEmpty()
{
Kind = NodeKind.Empty;
}
internal override void Write(StringBuilder sb, bool first = false)
{
}
}
}