Skip to content

Commit 90b3943

Browse files
rwasef1830YohDeadfall
authored andcommitted
Support for the tsquery 'FOLLOWED BY' operator introduced in 9.6
1 parent c7e7862 commit 90b3943

File tree

4 files changed

+247
-26
lines changed

4 files changed

+247
-26
lines changed

src/Npgsql/NpgsqlTypes/NpgsqlTsQuery.cs

Lines changed: 198 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
namespace NpgsqlTypes
3333
{
3434
/// <summary>
35-
/// Represents a PostgreSQL tsquery. This is the base class for lexeme, not, and and or nodes.
35+
/// Represents a PostgreSQL tsquery. This is the base class for the
36+
/// lexeme, not, or, and, and "followed by" nodes.
3637
/// </summary>
3738
public abstract class NpgsqlTsQuery
3839
{
@@ -46,26 +47,30 @@ public abstract class NpgsqlTsQuery
4647
/// </summary>
4748
public enum NodeKind
4849
{
50+
/// <summary>
51+
/// Represents the empty tsquery. Should only be used at top level.
52+
/// </summary>
53+
Empty = -1,
4954
/// <summary>
5055
/// Lexeme
5156
/// </summary>
52-
Lexeme,
57+
Lexeme = 0,
5358
/// <summary>
5459
/// Not operator
5560
/// </summary>
56-
Not,
61+
Not = 1,
5762
/// <summary>
5863
/// And operator
5964
/// </summary>
60-
And,
65+
And = 2,
6166
/// <summary>
6267
/// Or operator
6368
/// </summary>
64-
Or,
69+
Or = 3,
6570
/// <summary>
66-
/// Represents the empty tsquery. Should only be used at top level.
71+
/// "Followed by" operator
6772
/// </summary>
68-
Empty
73+
Phrase = 4
6974
}
7075

7176
internal abstract void Write(StringBuilder sb, bool first = false);
@@ -92,12 +97,14 @@ public static NpgsqlTsQuery Parse(string value)
9297
throw new ArgumentNullException(nameof(value));
9398

9499
var valStack = new Stack<NpgsqlTsQuery>();
95-
var opStack = new Stack<char>();
100+
var opStack = new Stack<NpgsqlTsQueryOperator>();
96101

97102
var sb = new StringBuilder();
98103
var pos = 0;
99104
var expectingBinOp = false;
100105

106+
var lastFollowedByOpDistance = -1;
107+
101108
NextToken:
102109
if (pos >= value.Length)
103110
goto Finish;
@@ -106,12 +113,49 @@ public static NpgsqlTsQuery Parse(string value)
106113
goto WaitEndComplex;
107114
if ((ch == ')' || ch == '|' || ch == '&') && !expectingBinOp || (ch == '(' || ch == '!') && expectingBinOp)
108115
throw new FormatException("Syntax error in tsquery. Unexpected token.");
109-
if (ch == '(' || ch == '!' || ch == '&')
116+
117+
if (ch == '<')
110118
{
111-
opStack.Push(ch);
119+
var endOfOperatorConsumed = false;
120+
var sbCurrentLength = sb.Length;
121+
122+
while (pos < value.Length)
123+
{
124+
var c = value[pos++];
125+
if (c == '>')
126+
{
127+
endOfOperatorConsumed = true;
128+
break;
129+
}
130+
131+
sb.Append(c);
132+
}
133+
134+
if (sb.Length == sbCurrentLength || !endOfOperatorConsumed)
135+
throw new FormatException("Syntax error in tsquery. Malformed 'followed by' operator.");
136+
137+
var followedByOpDistanceString = sb.ToString(sbCurrentLength, sb.Length - sbCurrentLength);
138+
if (followedByOpDistanceString == "-")
139+
{
140+
lastFollowedByOpDistance = 1;
141+
}
142+
else if (!int.TryParse(followedByOpDistanceString, out lastFollowedByOpDistance)
143+
|| lastFollowedByOpDistance < 0)
144+
{
145+
throw new FormatException("Syntax error in tsquery. Malformed distance in 'followed by' operator.");
146+
}
147+
148+
sb.Length -= followedByOpDistanceString.Length;
149+
}
150+
151+
if (ch == '(' || ch == '!' || ch == '&' || ch == '<')
152+
{
153+
opStack.Push(new NpgsqlTsQueryOperator(ch, lastFollowedByOpDistance));
112154
expectingBinOp = false;
155+
lastFollowedByOpDistance = 0;
113156
goto NextToken;
114157
}
158+
115159
if (ch == '|')
116160
{
117161
if (opStack.Count > 0 && opStack.Peek() == '|')
@@ -128,25 +172,48 @@ public static NpgsqlTsQuery Parse(string value)
128172
expectingBinOp = false;
129173
goto NextToken;
130174
}
175+
131176
if (ch == ')')
132177
{
133178
while (opStack.Count > 0 && opStack.Peek() != '(')
134179
{
135180
if (valStack.Count < 2 || opStack.Peek() == '!')
136181
throw new FormatException("Syntax error in tsquery");
182+
137183
var right = valStack.Pop();
138184
var left = valStack.Pop();
139-
valStack.Push(opStack.Pop() == '&' ? (NpgsqlTsQuery)new NpgsqlTsQueryAnd(left, right) : new NpgsqlTsQueryOr(left, right));
185+
186+
var tsOp = opStack.Pop();
187+
switch (tsOp)
188+
{
189+
case '&':
190+
valStack.Push(new NpgsqlTsQueryAnd(left, right));
191+
break;
192+
193+
case '|':
194+
valStack.Push(new NpgsqlTsQueryOr(left, right));
195+
break;
196+
197+
case '<':
198+
valStack.Push(new NpgsqlTsQueryFollowedBy(left, tsOp.FollowedByDistance, right));
199+
break;
200+
201+
default:
202+
throw new FormatException("Syntax error in tsquery");
203+
}
140204
}
141205
if (opStack.Count == 0)
142206
throw new FormatException("Syntax error in tsquery: closing parenthesis without an opening parenthesis");
143207
opStack.Pop();
144208
goto PushedVal;
145209
}
210+
146211
if (ch == ':')
147212
throw new FormatException("Unexpected : while parsing tsquery");
213+
148214
if (char.IsWhiteSpace(ch))
149215
goto NextToken;
216+
150217
pos--;
151218
if (expectingBinOp)
152219
throw new FormatException("Unexpected lexeme while parsing tsquery");
@@ -227,42 +294,101 @@ public static NpgsqlTsQuery Parse(string value)
227294

228295
PushedVal:
229296
sb.Clear();
230-
while (opStack.Count > 0 && (opStack.Peek() == '&' || opStack.Peek() == '!'))
297+
var processTightBindingOperator = true;
298+
while (opStack.Count > 0 && processTightBindingOperator)
231299
{
232-
if (opStack.Peek() == '&')
300+
var tsOp = opStack.Peek();
301+
switch (tsOp)
233302
{
303+
case '&':
234304
if (valStack.Count < 2)
235305
throw new FormatException("Syntax error in tsquery");
236-
var right = valStack.Pop();
237-
var left = valStack.Pop();
238-
valStack.Push(new NpgsqlTsQueryAnd(left, right));
239-
}
240-
else if (opStack.Peek() == '!')
241-
{
306+
var andRight = valStack.Pop();
307+
var andLeft = valStack.Pop();
308+
valStack.Push(new NpgsqlTsQueryAnd(andLeft, andRight));
309+
opStack.Pop();
310+
break;
311+
312+
case '!':
242313
if (valStack.Count == 0)
243314
throw new FormatException("Syntax error in tsquery");
244315
valStack.Push(new NpgsqlTsQueryNot(valStack.Pop()));
316+
opStack.Pop();
317+
break;
318+
319+
case '<':
320+
if (valStack.Count < 2)
321+
throw new FormatException("Syntax error in tsquery");
322+
var followedByRight = valStack.Pop();
323+
var followedByLeft = valStack.Pop();
324+
valStack.Push(
325+
new NpgsqlTsQueryFollowedBy(
326+
followedByLeft,
327+
tsOp.FollowedByDistance,
328+
followedByRight));
329+
opStack.Pop();
330+
break;
331+
332+
default:
333+
processTightBindingOperator = false;
334+
break;
245335
}
246-
opStack.Pop();
247336
}
248337
expectingBinOp = true;
249338
goto NextToken;
250339

251340
Finish:
252341
while (opStack.Count > 0)
253342
{
254-
if (valStack.Count < 2 || (opStack.Peek() != '|' && opStack.Peek() != '&'))
343+
if (valStack.Count < 2)
255344
throw new FormatException("Syntax error in tsquery");
345+
256346
var right = valStack.Pop();
257347
var left = valStack.Pop();
258-
valStack.Push(opStack.Pop() == '&' ? (NpgsqlTsQuery)new NpgsqlTsQueryAnd(left, right) : new NpgsqlTsQueryOr(left, right));
348+
349+
NpgsqlTsQuery query;
350+
var tsOp = opStack.Pop();
351+
switch (tsOp)
352+
{
353+
case '&':
354+
query = new NpgsqlTsQueryAnd(left, right);
355+
break;
356+
357+
case '|':
358+
query = new NpgsqlTsQueryOr(left, right);
359+
break;
360+
361+
case '<':
362+
query = new NpgsqlTsQueryFollowedBy(left, tsOp.FollowedByDistance, right);
363+
break;
364+
365+
default:
366+
throw new FormatException("Syntax error in tsquery");
367+
}
368+
369+
valStack.Push(query);
259370
}
260371
if (valStack.Count != 1)
261372
throw new FormatException("Syntax error in tsquery");
262373
return valStack.Pop();
263374
}
264375
}
265376

377+
readonly struct NpgsqlTsQueryOperator
378+
{
379+
public readonly char Char;
380+
public readonly int FollowedByDistance;
381+
382+
public NpgsqlTsQueryOperator(char character, int followedByDistance)
383+
{
384+
Char = character;
385+
FollowedByDistance = followedByDistance;
386+
}
387+
388+
public static implicit operator NpgsqlTsQueryOperator(char c) => new NpgsqlTsQueryOperator(c, 0);
389+
public static implicit operator char(NpgsqlTsQueryOperator o) => o.Char;
390+
}
391+
266392
/// <summary>
267393
/// TsQuery Lexeme node.
268394
/// </summary>
@@ -492,6 +618,56 @@ internal override void Write(StringBuilder sb, bool first = false)
492618
}
493619
}
494620

621+
/// <summary>
622+
/// TsQuery "Followed by" Node.
623+
/// </summary>
624+
public sealed class NpgsqlTsQueryFollowedBy : NpgsqlTsQueryBinOp
625+
{
626+
/// <summary>
627+
/// The distance between the 2 nodes, in lexemes.
628+
/// </summary>
629+
public int Distance { get; set; }
630+
631+
/// <summary>
632+
/// Creates a "followed by" operator, specifying 2 child nodes and the
633+
/// distance between them in lexemes.
634+
/// </summary>
635+
/// <param name="left"></param>
636+
/// <param name="distance"></param>
637+
/// <param name="right"></param>
638+
public NpgsqlTsQueryFollowedBy(
639+
[CanBeNull] NpgsqlTsQuery left,
640+
int distance,
641+
[CanBeNull] NpgsqlTsQuery right)
642+
{
643+
if (distance < 0)
644+
throw new ArgumentOutOfRangeException(nameof(distance));
645+
646+
Left = left;
647+
Distance = distance;
648+
Right = right;
649+
Kind = NodeKind.Phrase;
650+
}
651+
652+
internal override void Write(StringBuilder sb, bool first = false)
653+
{
654+
if (!first)
655+
sb.Append("( ");
656+
657+
Left.Write(sb);
658+
659+
sb.Append(" <");
660+
if (Distance == 1) sb.Append("-");
661+
else sb.Append(Distance);
662+
sb.Append("> ");
663+
664+
Right.Write(sb);
665+
666+
if (!first)
667+
sb.Append(" )");
668+
}
669+
}
670+
495671
/// <summary>
496672
/// Represents an empty tsquery. Shold only be used as top node.
497673
/// </summary>

0 commit comments

Comments
 (0)