patram 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/lib/build-graph-identity.js +86 -99
  2. package/lib/build-graph.js +536 -31
  3. package/lib/build-graph.types.ts +6 -2
  4. package/lib/check-directive-metadata.js +534 -0
  5. package/lib/check-directive-value.js +291 -0
  6. package/lib/check-graph.js +23 -5
  7. package/lib/cli-help-metadata.js +56 -16
  8. package/lib/command-output.js +16 -1
  9. package/lib/derived-summary.js +10 -8
  10. package/lib/directive-diagnostics.js +38 -0
  11. package/lib/directive-type-rules.js +133 -0
  12. package/lib/discover-fields.js +435 -0
  13. package/lib/discover-fields.types.ts +52 -0
  14. package/lib/document-node-identity.js +317 -0
  15. package/lib/format-node-header.js +9 -7
  16. package/lib/format-output-metadata.js +15 -23
  17. package/lib/layout-stored-queries.js +124 -85
  18. package/lib/load-patram-config.js +433 -96
  19. package/lib/load-patram-config.types.ts +98 -3
  20. package/lib/load-project-graph.js +4 -1
  21. package/lib/output-view.types.ts +14 -6
  22. package/lib/parse-cli-arguments.types.ts +1 -1
  23. package/lib/parse-where-clause.js +344 -107
  24. package/lib/parse-where-clause.types.ts +25 -8
  25. package/lib/patram-cli.js +68 -4
  26. package/lib/patram-config.js +31 -31
  27. package/lib/patram-config.types.ts +10 -4
  28. package/lib/query-graph.js +269 -40
  29. package/lib/query-inspection.js +440 -60
  30. package/lib/render-field-discovery.js +184 -0
  31. package/lib/render-json-output.js +21 -22
  32. package/lib/render-output-view.js +301 -34
  33. package/lib/render-plain-output.js +1 -1
  34. package/lib/render-rich-output.js +1 -1
  35. package/lib/render-rich-source.js +245 -14
  36. package/lib/resolve-patram-graph-config.js +15 -9
  37. package/lib/show-document.js +66 -9
  38. package/package.json +5 -5
@@ -6,7 +6,7 @@
6
6
  * ParsedAggregateComparison,
7
7
  * ParsedAggregateName,
8
8
  * ParsedAggregateTerm,
9
- * ParsedClause,
9
+ * ParsedExpression,
10
10
  * ParsedFieldName,
11
11
  * ParsedTerm,
12
12
  * ParsedTraversalTerm,
@@ -17,6 +17,16 @@
17
17
  * @typedef {{ index: number, where_clause: string }} ParserState
18
18
  */
19
19
 
20
+ /**
21
+ * @typedef {{
22
+ * expression: ParsedExpression,
23
+ * success: true,
24
+ * } | {
25
+ * diagnostic: PatramDiagnostic,
26
+ * success: false,
27
+ * }} ParseExpressionResult
28
+ */
29
+
20
30
  /**
21
31
  * @typedef {{
22
32
  * success: true,
@@ -28,7 +38,7 @@
28
38
  */
29
39
 
30
40
  /**
31
- * Parse one where clause into structured clauses.
41
+ * Parse one where clause into a structured boolean expression.
32
42
  *
33
43
  * @param {string} where_clause
34
44
  * @returns {ParseWhereClauseResult}
@@ -43,10 +53,10 @@ export function parseWhereClause(where_clause) {
43
53
  return fail(1, 'Query must not be empty.');
44
54
  }
45
55
 
46
- const clauses_result = parseClauses(parser_state, null);
56
+ const expression_result = parseExpression(parser_state, null);
47
57
 
48
- if (!clauses_result.success) {
49
- return clauses_result;
58
+ if (!expression_result.success) {
59
+ return expression_result;
50
60
  }
51
61
 
52
62
  skipWhitespace(parser_state);
@@ -56,7 +66,7 @@ export function parseWhereClause(where_clause) {
56
66
  }
57
67
 
58
68
  return {
59
- clauses: clauses_result.clauses,
69
+ expression: expression_result.expression,
60
70
  success: true,
61
71
  };
62
72
  }
@@ -64,12 +74,83 @@ export function parseWhereClause(where_clause) {
64
74
  /**
65
75
  * @param {ParserState} parser_state
66
76
  * @param {')' | null} stop_character
67
- * @returns {ParseWhereClauseResult}
77
+ * @returns {ParseExpressionResult}
78
+ */
79
+ function parseExpression(parser_state, stop_character) {
80
+ return parseOrExpression(parser_state, stop_character);
81
+ }
82
+
83
+ /**
84
+ * @param {ParserState} parser_state
85
+ * @param {')' | null} stop_character
86
+ * @returns {ParseExpressionResult}
87
+ */
88
+ function parseOrExpression(parser_state, stop_character) {
89
+ const first_expression_result = parseAndExpression(
90
+ parser_state,
91
+ stop_character,
92
+ );
93
+
94
+ if (!first_expression_result.success) {
95
+ return first_expression_result;
96
+ }
97
+
98
+ /** @type {ParsedExpression[]} */
99
+ const expressions = [first_expression_result.expression];
100
+
101
+ while (true) {
102
+ skipWhitespace(parser_state);
103
+
104
+ if (
105
+ currentCharacter(parser_state) === stop_character ||
106
+ isAtEnd(parser_state)
107
+ ) {
108
+ return collapseBooleanExpression('or', expressions);
109
+ }
110
+
111
+ if (!consumeKeyword(parser_state, 'or')) {
112
+ return collapseBooleanExpression('or', expressions);
113
+ }
114
+
115
+ skipWhitespace(parser_state);
116
+
117
+ if (
118
+ currentCharacter(parser_state) === stop_character ||
119
+ isAtEnd(parser_state)
120
+ ) {
121
+ return failExpectedTerm(parser_state);
122
+ }
123
+
124
+ const next_expression_result = parseAndExpression(
125
+ parser_state,
126
+ stop_character,
127
+ );
128
+
129
+ if (!next_expression_result.success) {
130
+ return next_expression_result;
131
+ }
132
+
133
+ expressions.push(next_expression_result.expression);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * @param {ParserState} parser_state
139
+ * @param {')' | null} stop_character
140
+ * @returns {ParseExpressionResult}
68
141
  */
69
- function parseClauses(parser_state, stop_character) {
70
- /** @type {ParsedClause[]} */
71
- const clauses = [];
72
- let is_first_clause = true;
142
+ function parseAndExpression(parser_state, stop_character) {
143
+ const first_expression_result = parseUnaryExpression(
144
+ parser_state,
145
+ stop_character,
146
+ );
147
+
148
+ if (!first_expression_result.success) {
149
+ return first_expression_result;
150
+ }
151
+
152
+ /** @type {ParsedExpression[]} */
153
+ const expressions = [first_expression_result.expression];
73
154
 
74
155
  while (true) {
75
156
  skipWhitespace(parser_state);
@@ -78,68 +159,131 @@ function parseClauses(parser_state, stop_character) {
78
159
  currentCharacter(parser_state) === stop_character ||
79
160
  isAtEnd(parser_state)
80
161
  ) {
81
- return is_first_clause
82
- ? fail(parser_state.where_clause.length + 1, 'Expected a query term.')
83
- : { clauses, success: true };
162
+ return collapseBooleanExpression('and', expressions);
84
163
  }
85
164
 
86
- if (!is_first_clause) {
87
- if (!consumeKeyword(parser_state, 'and')) {
88
- return failToken(parser_state);
89
- }
90
-
91
- skipWhitespace(parser_state);
92
-
93
- if (
94
- currentCharacter(parser_state) === stop_character ||
95
- isAtEnd(parser_state)
96
- ) {
97
- return fail(
98
- parser_state.where_clause.length + 1,
99
- 'Expected a query term.',
100
- );
101
- }
165
+ if (!consumeKeyword(parser_state, 'and')) {
166
+ return collapseBooleanExpression('and', expressions);
102
167
  }
103
168
 
104
- const clause_result = parseClause(parser_state);
169
+ skipWhitespace(parser_state);
105
170
 
106
- if (!clause_result.success) {
107
- return clause_result;
171
+ if (
172
+ currentCharacter(parser_state) === stop_character ||
173
+ isAtEnd(parser_state)
174
+ ) {
175
+ return failExpectedTerm(parser_state);
108
176
  }
109
177
 
110
- clauses.push(clause_result.clause);
111
- is_first_clause = false;
178
+ const next_expression_result = parseUnaryExpression(
179
+ parser_state,
180
+ stop_character,
181
+ );
182
+
183
+ if (!next_expression_result.success) {
184
+ return next_expression_result;
185
+ }
186
+
187
+ expressions.push(next_expression_result.expression);
112
188
  }
113
189
  }
114
190
 
115
191
  /**
116
192
  * @param {ParserState} parser_state
117
- * @returns {{ clause: ParsedClause, success: true } | { diagnostic: PatramDiagnostic, success: false }}
193
+ * @param {')' | null} stop_character
194
+ * @returns {ParseExpressionResult}
118
195
  */
119
- function parseClause(parser_state) {
120
- const clause_start = parser_state.index;
121
- const is_negated = consumeKeyword(parser_state, 'not');
196
+ function parseUnaryExpression(parser_state, stop_character) {
197
+ skipWhitespace(parser_state);
198
+ const start_index = parser_state.index;
199
+
200
+ if (!consumeKeyword(parser_state, 'not')) {
201
+ return parsePrimaryExpression(parser_state, stop_character);
202
+ }
203
+
204
+ const whitespace_length = skipWhitespace(parser_state);
122
205
 
123
- if (is_negated && !consumeRequiredWhitespace(parser_state)) {
206
+ if (whitespace_length === 0 && currentCharacter(parser_state) !== '(') {
124
207
  return fail(
125
- clause_start + 1,
126
- `Unsupported query token "${readToken(parser_state, clause_start)}".`,
208
+ start_index + 1,
209
+ `Unsupported query token "${readToken(parser_state, start_index)}".`,
127
210
  );
128
211
  }
129
212
 
213
+ if (
214
+ currentCharacter(parser_state) === stop_character ||
215
+ isAtEnd(parser_state)
216
+ ) {
217
+ return failExpectedTerm(parser_state);
218
+ }
219
+
220
+ const expression_result = parseUnaryExpression(parser_state, stop_character);
221
+
222
+ if (!expression_result.success) {
223
+ return expression_result;
224
+ }
225
+
226
+ return successExpression({
227
+ expression: expression_result.expression,
228
+ kind: 'not',
229
+ });
230
+ }
231
+
232
+ /**
233
+ * @param {ParserState} parser_state
234
+ * @param {')' | null} stop_character
235
+ * @returns {ParseExpressionResult}
236
+ */
237
+ function parsePrimaryExpression(parser_state, stop_character) {
238
+ skipWhitespace(parser_state);
239
+
240
+ if (
241
+ currentCharacter(parser_state) === stop_character ||
242
+ isAtEnd(parser_state)
243
+ ) {
244
+ return failExpectedTerm(parser_state);
245
+ }
246
+
247
+ if (consumeOperator(parser_state, '(')) {
248
+ const expression_result = parseExpression(parser_state, ')');
249
+
250
+ if (!expression_result.success) {
251
+ return expression_result;
252
+ }
253
+
254
+ if (!consumeOperator(parser_state, ')')) {
255
+ return failToken(parser_state);
256
+ }
257
+
258
+ return expression_result;
259
+ }
260
+
130
261
  const term_result = parseTerm(parser_state);
131
262
 
132
263
  if (!term_result.success) {
133
264
  return term_result;
134
265
  }
135
266
 
136
- return {
137
- clause: {
138
- is_negated,
139
- term: term_result.term,
140
- },
141
- success: true,
142
- };
267
+ return successExpression({
268
+ kind: 'term',
269
+ term: term_result.term,
270
+ });
271
+ }
272
+
273
+ /**
274
+ * @param {'and' | 'or'} kind
275
+ * @param {ParsedExpression[]} expressions
276
+ * @returns {ParseExpressionResult}
277
+ */
278
+ function collapseBooleanExpression(kind, expressions) {
279
+ if (expressions.length === 1) {
280
+ return successExpression(expressions[0]);
281
+ }
282
+
283
+ return successExpression({
284
+ expressions,
285
+ kind,
286
+ });
143
287
  }
144
288
 
145
289
  /**
@@ -188,10 +332,10 @@ function parseAggregate(parser_state) {
188
332
  }
189
333
 
190
334
  skipWhitespace(parser_state);
191
- const clauses_result = parseClauses(parser_state, ')');
335
+ const expression_result = parseExpression(parser_state, ')');
192
336
 
193
- if (!clauses_result.success) {
194
- return clauses_result;
337
+ if (!expression_result.success) {
338
+ return expression_result;
195
339
  }
196
340
 
197
341
  if (!consumeOperator(parser_state, ')')) {
@@ -202,7 +346,7 @@ function parseAggregate(parser_state) {
202
346
  parser_state,
203
347
  aggregate_name,
204
348
  traversal_result.traversal,
205
- clauses_result.clauses,
349
+ expression_result.expression,
206
350
  );
207
351
  }
208
352
 
@@ -218,30 +362,32 @@ function parseAtomicTerm(parser_state) {
218
362
  return failToken(parser_state);
219
363
  }
220
364
 
221
- return (
222
- parseFieldSet(parser_state, field_or_relation_name) ??
223
- parseOperatorTerm(parser_state, start_index, field_or_relation_name)
224
- );
365
+ return parseOperatorTerm(parser_state, start_index, field_or_relation_name);
225
366
  }
226
367
 
227
368
  /**
228
369
  * @param {ParserState} parser_state
229
370
  * @param {ParsedAggregateName} aggregate_name
230
371
  * @param {ParsedTraversalTerm} traversal
231
- * @param {ParsedClause[]} clauses
372
+ * @param {ParsedExpression} expression
232
373
  * @returns {ParseTermResult}
233
374
  */
234
- function createAggregateTerm(parser_state, aggregate_name, traversal, clauses) {
375
+ function createAggregateTerm(
376
+ parser_state,
377
+ aggregate_name,
378
+ traversal,
379
+ expression,
380
+ ) {
235
381
  /** @type {ParsedAggregateTerm} */
236
382
  const aggregate_term = {
237
383
  aggregate_name,
238
- clauses,
384
+ expression,
239
385
  kind: 'aggregate',
240
386
  traversal,
241
387
  };
242
388
 
243
389
  if (aggregate_name !== 'count') {
244
- return success(aggregate_term);
390
+ return successTerm(aggregate_term);
245
391
  }
246
392
 
247
393
  const count_tail = parseCountTail(parser_state);
@@ -250,7 +396,7 @@ function createAggregateTerm(parser_state, aggregate_name, traversal, clauses) {
250
396
  return failToken(parser_state);
251
397
  }
252
398
 
253
- return success({
399
+ return successTerm({
254
400
  ...aggregate_term,
255
401
  comparison: count_tail.comparison,
256
402
  value: count_tail.value,
@@ -259,15 +405,12 @@ function createAggregateTerm(parser_state, aggregate_name, traversal, clauses) {
259
405
 
260
406
  /**
261
407
  * @param {ParserState} parser_state
408
+ * @param {number} start_index
262
409
  * @param {ParsedFieldName | string} field_name
263
410
  * @returns {ParseTermResult | null}
264
411
  */
265
- function parseFieldSet(parser_state, field_name) {
266
- if (!isSupportedFieldName(field_name)) {
267
- return null;
268
- }
269
-
270
- const start_index = parser_state.index;
412
+ function parseFieldSet(parser_state, start_index, field_name) {
413
+ const operator_start_index = parser_state.index;
271
414
 
272
415
  if (!consumeRequiredWhitespace(parser_state)) {
273
416
  return null;
@@ -276,7 +419,7 @@ function parseFieldSet(parser_state, field_name) {
276
419
  const operator = parseSetOperator(parser_state);
277
420
 
278
421
  if (!operator) {
279
- parser_state.index = start_index;
422
+ parser_state.index = operator_start_index;
280
423
  return null;
281
424
  }
282
425
 
@@ -287,7 +430,13 @@ function parseFieldSet(parser_state, field_name) {
287
430
  const values = parseList(parser_state);
288
431
 
289
432
  return values
290
- ? success({ field_name, kind: 'field_set', operator, values })
433
+ ? successTerm({
434
+ column: start_index + 1,
435
+ field_name,
436
+ kind: 'field_set',
437
+ operator,
438
+ values,
439
+ })
291
440
  : failToken(parser_state);
292
441
  }
293
442
 
@@ -298,18 +447,46 @@ function parseFieldSet(parser_state, field_name) {
298
447
  * @returns {ParseTermResult}
299
448
  */
300
449
  function parseOperatorTerm(parser_state, start_index, field_or_relation_name) {
301
- const prefix_term = parsePrefixTerm(parser_state, field_or_relation_name);
450
+ const field_set = parseFieldSet(
451
+ parser_state,
452
+ start_index,
453
+ field_or_relation_name,
454
+ );
455
+
456
+ if (field_set) {
457
+ return field_set;
458
+ }
459
+
460
+ const prefix_term = parsePrefixTerm(
461
+ parser_state,
462
+ start_index,
463
+ field_or_relation_name,
464
+ );
302
465
 
303
466
  if (prefix_term) {
304
467
  return prefix_term;
305
468
  }
306
469
 
307
- const contains_term = parseContainsTerm(parser_state, field_or_relation_name);
470
+ const contains_term = parseContainsTerm(
471
+ parser_state,
472
+ start_index,
473
+ field_or_relation_name,
474
+ );
308
475
 
309
476
  if (contains_term) {
310
477
  return contains_term;
311
478
  }
312
479
 
480
+ const comparison_term = parseFieldComparisonTerm(
481
+ parser_state,
482
+ start_index,
483
+ field_or_relation_name,
484
+ );
485
+
486
+ if (comparison_term) {
487
+ return comparison_term;
488
+ }
489
+
313
490
  const equality_term = parseEqualityTerm(
314
491
  parser_state,
315
492
  start_index,
@@ -321,7 +498,7 @@ function parseOperatorTerm(parser_state, start_index, field_or_relation_name) {
321
498
  }
322
499
 
323
500
  if (consumeOperator(parser_state, ':*')) {
324
- return success({
501
+ return successTerm({
325
502
  column: start_index + 1,
326
503
  kind: 'relation',
327
504
  relation_name: field_or_relation_name,
@@ -395,39 +572,51 @@ function parseSetOperator(parser_state) {
395
572
 
396
573
  /**
397
574
  * @param {ParserState} parser_state
575
+ * @param {number} start_index
398
576
  * @param {string} field_name
399
577
  * @returns {ParseTermResult | null}
400
578
  */
401
- function parsePrefixTerm(parser_state, field_name) {
402
- if (field_name !== 'id' && field_name !== 'path') {
403
- return null;
404
- }
405
-
579
+ function parsePrefixTerm(parser_state, start_index, field_name) {
406
580
  if (!consumeOperator(parser_state, '^=')) {
407
581
  return null;
408
582
  }
409
583
 
584
+ skipWhitespace(parser_state);
410
585
  const value = parseBareValue(parser_state);
411
586
 
412
587
  return value
413
- ? success({ field_name, kind: 'field', operator: '^=', value })
588
+ ? successTerm({
589
+ column: start_index + 1,
590
+ field_name,
591
+ kind: 'field',
592
+ operator: '^=',
593
+ value,
594
+ })
414
595
  : failToken(parser_state);
415
596
  }
416
597
 
417
598
  /**
418
599
  * @param {ParserState} parser_state
600
+ * @param {number} start_index
419
601
  * @param {string} field_name
420
602
  * @returns {ParseTermResult | null}
421
603
  */
422
- function parseContainsTerm(parser_state, field_name) {
423
- if (field_name !== 'title' || !consumeOperator(parser_state, '~')) {
604
+ function parseContainsTerm(parser_state, start_index, field_name) {
605
+ if (!consumeOperator(parser_state, '~')) {
424
606
  return null;
425
607
  }
426
608
 
609
+ skipWhitespace(parser_state);
427
610
  const value = parseBareValue(parser_state);
428
611
 
429
612
  return value
430
- ? success({ field_name: 'title', kind: 'field', operator: '~', value })
613
+ ? successTerm({
614
+ column: start_index + 1,
615
+ field_name,
616
+ kind: 'field',
617
+ operator: '~',
618
+ value,
619
+ })
431
620
  : failToken(parser_state);
432
621
  }
433
622
 
@@ -442,14 +631,20 @@ function parseEqualityTerm(parser_state, start_index, field_or_relation_name) {
442
631
  return null;
443
632
  }
444
633
 
634
+ skipWhitespace(parser_state);
445
635
  const value = parseBareValue(parser_state);
446
636
 
447
637
  if (!value) {
448
638
  return failToken(parser_state);
449
639
  }
450
640
 
451
- if (isExactMatchField(field_or_relation_name)) {
452
- return success({
641
+ if (
642
+ field_or_relation_name.startsWith('$') ||
643
+ field_or_relation_name === 'title' ||
644
+ !value.includes(':')
645
+ ) {
646
+ return successTerm({
647
+ column: start_index + 1,
453
648
  field_name: field_or_relation_name,
454
649
  kind: 'field',
455
650
  operator: '=',
@@ -457,17 +652,54 @@ function parseEqualityTerm(parser_state, start_index, field_or_relation_name) {
457
652
  });
458
653
  }
459
654
 
460
- if (value.includes(':')) {
461
- return success({
462
- column: start_index + 1,
463
- kind: 'relation_target',
464
- relation_name: field_or_relation_name,
465
- target_id: value,
466
- });
655
+ return successTerm({
656
+ column: start_index + 1,
657
+ kind: 'relation_target',
658
+ relation_name: field_or_relation_name,
659
+ target_id: value,
660
+ });
661
+ }
662
+
663
+ /**
664
+ * @param {ParserState} parser_state
665
+ * @param {number} start_index
666
+ * @param {string} field_name
667
+ * @returns {ParseTermResult | null}
668
+ */
669
+ function parseFieldComparisonTerm(parser_state, start_index, field_name) {
670
+ const operator = parseFieldComparisonOperator(parser_state);
671
+
672
+ if (!operator) {
673
+ return null;
467
674
  }
468
675
 
469
- parser_state.index = start_index;
470
- return failToken(parser_state);
676
+ skipWhitespace(parser_state);
677
+ const value = parseBareValue(parser_state);
678
+
679
+ if (!value) {
680
+ return failToken(parser_state);
681
+ }
682
+
683
+ return successTerm({
684
+ column: start_index + 1,
685
+ field_name,
686
+ kind: 'field',
687
+ operator,
688
+ value,
689
+ });
690
+ }
691
+
692
+ /**
693
+ * @param {ParserState} parser_state
694
+ * @returns {'!=' | '<=' | '>=' | '<' | '>' | null}
695
+ */
696
+ function parseFieldComparisonOperator(parser_state) {
697
+ /** @type {Array<'!=' | '<=' | '>=' | '<' | '>'>} */
698
+ const comparisons = ['!=', '<=', '>=', '<', '>'];
699
+
700
+ return (
701
+ comparisons.find((value) => consumeOperator(parser_state, value)) ?? null
702
+ );
471
703
  }
472
704
 
473
705
  /**
@@ -526,7 +758,7 @@ function parseComparison(parser_state) {
526
758
  * @returns {string | null}
527
759
  */
528
760
  function parseIdentifier(parser_state) {
529
- return readMatch(parser_state, /^[a-z_]+/u);
761
+ return readMatch(parser_state, /^\$?[a-z_][a-z0-9_]*/u);
530
762
  }
531
763
 
532
764
  /**
@@ -644,26 +876,18 @@ function isAtEnd(parser_state) {
644
876
  }
645
877
 
646
878
  /**
647
- * @param {string} field_name
648
- * @returns {field_name is ParsedFieldName}
649
- */
650
- function isSupportedFieldName(field_name) {
651
- return ['id', 'kind', 'path', 'status', 'title'].includes(field_name);
652
- }
653
-
654
- /**
655
- * @param {string} field_name
656
- * @returns {field_name is 'id' | 'kind' | 'path' | 'status'}
879
+ * @param {ParsedExpression} expression
880
+ * @returns {ParseExpressionResult}
657
881
  */
658
- function isExactMatchField(field_name) {
659
- return ['id', 'kind', 'path', 'status'].includes(field_name);
882
+ function successExpression(expression) {
883
+ return { expression, success: true };
660
884
  }
661
885
 
662
886
  /**
663
887
  * @param {ParsedTerm} term
664
888
  * @returns {ParseTermResult}
665
889
  */
666
- function success(term) {
890
+ function successTerm(term) {
667
891
  return { success: true, term };
668
892
  }
669
893
 
@@ -678,6 +902,19 @@ function failToken(parser_state) {
678
902
  );
679
903
  }
680
904
 
905
+ /**
906
+ * @param {ParserState} parser_state
907
+ * @returns {{ diagnostic: PatramDiagnostic, success: false }}
908
+ */
909
+ function failExpectedTerm(parser_state) {
910
+ return fail(
911
+ isAtEnd(parser_state)
912
+ ? parser_state.where_clause.length + 1
913
+ : parser_state.index + 1,
914
+ 'Expected a query term.',
915
+ );
916
+ }
917
+
681
918
  /**
682
919
  * @param {number} column
683
920
  * @param {string} message