3232namespace 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