X Tutup
using System; using System.Collections.Generic; using System.Text; // ReSharper disable once CheckNamespace namespace NpgsqlTypes; /// /// Represents a PostgreSQL tsquery. This is the base class for the /// lexeme, not, or, and, and "followed by" nodes. /// public abstract class NpgsqlTsQuery : IEquatable { /// /// Node kind /// public NodeKind Kind { get; } /// /// NodeKind /// public enum NodeKind { /// /// Represents the empty tsquery. Should only be used at top level. /// Empty = -1, /// /// Lexeme /// Lexeme = 0, /// /// Not operator /// Not = 1, /// /// And operator /// And = 2, /// /// Or operator /// Or = 3, /// /// "Followed by" operator /// Phrase = 4 } /// /// Constructs an . /// /// protected NpgsqlTsQuery(NodeKind kind) => Kind = kind; /// /// Writes the tsquery in PostgreSQL's text format. /// public void Write(StringBuilder stringBuilder) => WriteCore(stringBuilder, true); internal abstract void WriteCore(StringBuilder sb, bool first = false); /// /// Writes the tsquery in PostgreSQL's text format. /// public override string ToString() { var sb = new StringBuilder(); Write(sb); return sb.ToString(); } /// /// Parses a tsquery in PostgreSQL's text format. /// /// /// [Obsolete("Client-side parsing of NpgsqlTsQuery is unreliable and cannot fully duplicate the PostgreSQL logic. Use PG functions instead (e.g. to_tsquery)")] public static NpgsqlTsQuery Parse(string value) { ArgumentNullException.ThrowIfNull(value); var valStack = new Stack(); var opStack = new Stack(); var sb = new StringBuilder(); var pos = 0; var expectingBinOp = false; short lastFollowedByOpDistance = -1; 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 == '<') { var endOfOperatorConsumed = false; var sbCurrentLength = sb.Length; while (pos < value.Length) { var c = value[pos++]; if (c == '>') { endOfOperatorConsumed = true; break; } sb.Append(c); } if (sb.Length == sbCurrentLength || !endOfOperatorConsumed) throw new FormatException("Syntax error in tsquery. Malformed 'followed by' operator."); var followedByOpDistanceString = sb.ToString(sbCurrentLength, sb.Length - sbCurrentLength); if (followedByOpDistanceString == "-") { lastFollowedByOpDistance = 1; } else if (!short.TryParse(followedByOpDistanceString, out lastFollowedByOpDistance) || lastFollowedByOpDistance < 0) { throw new FormatException("Syntax error in tsquery. Malformed distance in 'followed by' operator."); } sb.Length -= followedByOpDistanceString.Length; } if (ch == '(' || ch == '!' || ch == '&' || ch == '<') { opStack.Push(new NpgsqlTsQueryOperator(ch, lastFollowedByOpDistance)); expectingBinOp = false; lastFollowedByOpDistance = 0; 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(); var tsOp = opStack.Pop(); valStack.Push((char)tsOp switch { '&' => new NpgsqlTsQueryAnd(left, right), '|' => new NpgsqlTsQueryOr(left, right), '<' => new NpgsqlTsQueryFollowedBy(left, tsOp.FollowedByDistance, right), _ => throw new FormatException("Syntax error in tsquery") }); } 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]; switch (ch) { case '*': ((NpgsqlTsQueryLexeme)valStack.Peek()).IsPrefixSearch = true; break; case 'a' or 'A': ((NpgsqlTsQueryLexeme)valStack.Peek()).Weights |= NpgsqlTsQueryLexeme.Weight.A; break; case 'b' or 'B': ((NpgsqlTsQueryLexeme)valStack.Peek()).Weights |= NpgsqlTsQueryLexeme.Weight.B; break; case 'c' or 'C': ((NpgsqlTsQueryLexeme)valStack.Peek()).Weights |= NpgsqlTsQueryLexeme.Weight.C; break; case 'd' or 'D': ((NpgsqlTsQueryLexeme)valStack.Peek()).Weights |= NpgsqlTsQueryLexeme.Weight.D; break; default: goto PushedVal; } pos++; goto InWeightInfo; PushedVal: sb.Clear(); var processTightBindingOperator = true; while (opStack.Count > 0 && processTightBindingOperator) { var tsOp = opStack.Peek(); switch (tsOp) { case '&': if (valStack.Count < 2) throw new FormatException("Syntax error in tsquery"); var andRight = valStack.Pop(); var andLeft = valStack.Pop(); valStack.Push(new NpgsqlTsQueryAnd(andLeft, andRight)); opStack.Pop(); break; case '!': if (valStack.Count == 0) throw new FormatException("Syntax error in tsquery"); valStack.Push(new NpgsqlTsQueryNot(valStack.Pop())); opStack.Pop(); break; case '<': if (valStack.Count < 2) throw new FormatException("Syntax error in tsquery"); var followedByRight = valStack.Pop(); var followedByLeft = valStack.Pop(); valStack.Push( new NpgsqlTsQueryFollowedBy( followedByLeft, tsOp.FollowedByDistance, followedByRight)); opStack.Pop(); break; default: processTightBindingOperator = false; break; } } expectingBinOp = true; goto NextToken; Finish: while (opStack.Count > 0) { if (valStack.Count < 2) throw new FormatException("Syntax error in tsquery"); var right = valStack.Pop(); var left = valStack.Pop(); var tsOp = opStack.Pop(); var query = (char)tsOp switch { '&' => (NpgsqlTsQuery)new NpgsqlTsQueryAnd(left, right), '|' => new NpgsqlTsQueryOr(left, right), '<' => new NpgsqlTsQueryFollowedBy(left, tsOp.FollowedByDistance, right), _ => throw new FormatException("Syntax error in tsquery") }; valStack.Push(query); } if (valStack.Count != 1) throw new FormatException("Syntax error in tsquery"); return valStack.Pop(); } /// public override int GetHashCode() => throw new NotSupportedException("Must be overridden"); /// public override bool Equals(object? obj) => obj is NpgsqlTsQuery query && query.Equals(this); /// /// Returns a value indicating whether this instance and a specified object represent the same value. /// /// An object to compare to this instance. /// if g is equal to this instance; otherwise, . public abstract bool Equals(NpgsqlTsQuery? other); /// /// Indicates whether the values of two specified objects are equal. /// /// The first object to compare. /// The second object to compare. /// if and are equal; otherwise, . public static bool operator ==(NpgsqlTsQuery? left, NpgsqlTsQuery? right) => left is null ? right is null : left.Equals(right); /// /// Indicates whether the values of two specified objects are not equal. /// /// The first object to compare. /// The second object to compare. /// if and are not equal; otherwise, . public static bool operator !=(NpgsqlTsQuery? left, NpgsqlTsQuery? right) => left is null ? right is not null : !left.Equals(right); } readonly struct NpgsqlTsQueryOperator(char character, short followedByDistance) { public readonly char Char = character; public readonly short FollowedByDistance = followedByDistance; public static implicit operator NpgsqlTsQueryOperator(char c) => new(c, 0); public static implicit operator char(NpgsqlTsQueryOperator o) => o.Char; } /// /// TsQuery Lexeme node. /// public sealed class NpgsqlTsQueryLexeme : NpgsqlTsQuery { string _text; /// /// Lexeme text. /// public string Text { get => _text; set { ArgumentException.ThrowIfNullOrEmpty(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) : base(NodeKind.Lexeme) { _text = text; Weights = weights; IsPrefixSearch = isPrefixSearch; } /// /// Weight enum, can be OR'ed together. /// [Flags] public enum Weight { /// /// None /// None = 0, /// /// D /// D = 1, /// /// C /// C = 2, /// /// B /// B = 4, /// /// A /// A = 8 } internal override void WriteCore(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'); } /// public override bool Equals(NpgsqlTsQuery? other) => other is NpgsqlTsQueryLexeme lexeme && lexeme.Text == Text && lexeme.Weights == Weights && lexeme.IsPrefixSearch == IsPrefixSearch; /// public override int GetHashCode() => HashCode.Combine(Text, Weights, IsPrefixSearch); } /// /// 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(NpgsqlTsQuery child) : base(NodeKind.Not) => Child = child; internal override void WriteCore(StringBuilder sb, bool first = false) { sb.Append('!'); if (Child == null) { sb.Append("''"); } else { if (Child.Kind != NodeKind.Lexeme) sb.Append("( "); Child.WriteCore(sb, true); if (Child.Kind != NodeKind.Lexeme) sb.Append(" )"); } } /// public override bool Equals(NpgsqlTsQuery? other) => other is NpgsqlTsQueryNot not && not.Child == Child; /// public override int GetHashCode() => Child?.GetHashCode() ?? 0; } /// /// 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; } /// /// Constructs a . /// protected NpgsqlTsQueryBinOp(NodeKind kind, NpgsqlTsQuery left, NpgsqlTsQuery right) : base(kind) { Left = left; Right = right; } } /// /// TsQuery And node. /// public sealed class NpgsqlTsQueryAnd : NpgsqlTsQueryBinOp { /// /// Creates an and operator, with two given child nodes. /// /// /// public NpgsqlTsQueryAnd(NpgsqlTsQuery left, NpgsqlTsQuery right) : base(NodeKind.And, left, right) {} internal override void WriteCore(StringBuilder sb, bool first = false) { Left.WriteCore(sb); sb.Append(" & "); Right.WriteCore(sb); } /// public override bool Equals(NpgsqlTsQuery? other) => other is NpgsqlTsQueryAnd and && and.Left == Left && and.Right == Right; /// public override int GetHashCode() => HashCode.Combine(Left, Right); } /// /// TsQuery Or Node. /// public sealed class NpgsqlTsQueryOr : NpgsqlTsQueryBinOp { /// /// Creates an or operator, with two given child nodes. /// /// /// public NpgsqlTsQueryOr(NpgsqlTsQuery left, NpgsqlTsQuery right) : base(NodeKind.Or, left, right) {} internal override void WriteCore(StringBuilder sb, bool first = false) { // TODO: Figure out the nullability strategy here if (!first) sb.Append("( "); Left.WriteCore(sb); sb.Append(" | "); Right.WriteCore(sb); if (!first) sb.Append(" )"); } /// public override bool Equals(NpgsqlTsQuery? other) => other is NpgsqlTsQueryOr or && or.Left == Left && or.Right == Right; /// public override int GetHashCode() => HashCode.Combine(Left, Right); } /// /// TsQuery "Followed by" Node. /// public sealed class NpgsqlTsQueryFollowedBy : NpgsqlTsQueryBinOp { /// /// The distance between the 2 nodes, in lexemes. /// public short Distance { get; set; } /// /// Creates a "followed by" operator, specifying 2 child nodes and the /// distance between them in lexemes. /// /// /// /// public NpgsqlTsQueryFollowedBy( NpgsqlTsQuery left, short distance, NpgsqlTsQuery right) : base(NodeKind.Phrase, left, right) { ArgumentOutOfRangeException.ThrowIfNegative(distance); Distance = distance; } internal override void WriteCore(StringBuilder sb, bool first = false) { // TODO: Figure out the nullability strategy here if (!first) sb.Append("( "); Left.WriteCore(sb); sb.Append(" <"); if (Distance == 1) sb.Append("-"); else sb.Append(Distance); sb.Append("> "); Right.WriteCore(sb); if (!first) sb.Append(" )"); } /// public override bool Equals(NpgsqlTsQuery? other) => other is NpgsqlTsQueryFollowedBy followedBy && followedBy.Left == Left && followedBy.Right == Right && followedBy.Distance == Distance; /// public override int GetHashCode() => HashCode.Combine(Left, Right, Distance); } /// /// Represents an empty tsquery. Should 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() : base(NodeKind.Empty) {} internal override void WriteCore(StringBuilder sb, bool first = false) { } /// public override bool Equals(NpgsqlTsQuery? other) => other is NpgsqlTsQueryEmpty; /// public override int GetHashCode() => Kind.GetHashCode(); }
X Tutup