patram 0.0.2 → 0.2.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 (67) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +270 -0
  3. package/lib/build-graph.js +156 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/cli-help-metadata.js +552 -0
  7. package/lib/command-output.js +83 -0
  8. package/lib/derived-summary.js +278 -0
  9. package/lib/format-derived-summary-row.js +9 -0
  10. package/lib/format-node-header.js +19 -0
  11. package/lib/format-output-item-block.js +22 -0
  12. package/lib/format-output-metadata.js +62 -0
  13. package/lib/layout-stored-queries.js +361 -0
  14. package/lib/list-queries.js +18 -0
  15. package/lib/list-source-files.js +50 -15
  16. package/lib/load-patram-config.js +505 -18
  17. package/lib/load-patram-config.types.ts +40 -0
  18. package/lib/load-project-graph.js +124 -0
  19. package/lib/output-view.types.ts +88 -0
  20. package/lib/parse-claims.js +38 -158
  21. package/lib/parse-claims.types.ts +7 -0
  22. package/lib/parse-cli-arguments-helpers.js +446 -0
  23. package/lib/parse-cli-arguments.js +266 -0
  24. package/lib/parse-cli-arguments.types.ts +69 -0
  25. package/lib/parse-cli-color-options.js +44 -0
  26. package/lib/parse-cli-query-pagination.js +49 -0
  27. package/lib/parse-jsdoc-blocks.js +184 -0
  28. package/lib/parse-jsdoc-claims.js +280 -0
  29. package/lib/parse-jsdoc-prose.js +111 -0
  30. package/lib/parse-markdown-claims.js +242 -0
  31. package/lib/parse-markdown-directives.js +136 -0
  32. package/lib/parse-where-clause.js +707 -0
  33. package/lib/parse-where-clause.types.ts +70 -0
  34. package/lib/patram-cli.js +464 -0
  35. package/lib/patram-config.js +3 -1
  36. package/lib/patram-config.types.ts +2 -1
  37. package/lib/patram.js +6 -0
  38. package/lib/query-graph.js +368 -0
  39. package/lib/query-inspection.js +523 -0
  40. package/lib/render-check-output.js +315 -0
  41. package/lib/render-cli-help.js +419 -0
  42. package/lib/render-json-output.js +161 -0
  43. package/lib/render-output-view.js +222 -0
  44. package/lib/render-plain-output.js +182 -0
  45. package/lib/render-rich-output.js +240 -0
  46. package/lib/render-rich-source.js +1333 -0
  47. package/lib/resolve-check-target.js +190 -0
  48. package/lib/resolve-output-mode.js +60 -0
  49. package/lib/resolve-patram-graph-config.js +88 -0
  50. package/lib/resolve-where-clause.js +66 -0
  51. package/lib/show-document.js +311 -0
  52. package/lib/source-file-defaults.js +28 -0
  53. package/lib/tagged-fenced-block-error.js +17 -0
  54. package/lib/tagged-fenced-block-markdown.js +111 -0
  55. package/lib/tagged-fenced-block-metadata.js +97 -0
  56. package/lib/tagged-fenced-block-parser.js +292 -0
  57. package/lib/tagged-fenced-blocks.js +100 -0
  58. package/lib/tagged-fenced-blocks.types.ts +38 -0
  59. package/lib/write-paged-output.js +87 -0
  60. package/package.json +28 -12
  61. package/bin/patram.test.js +0 -184
  62. package/lib/build-graph.test.js +0 -141
  63. package/lib/check-graph.test.js +0 -103
  64. package/lib/list-source-files.test.js +0 -101
  65. package/lib/load-patram-config.test.js +0 -211
  66. package/lib/parse-claims.test.js +0 -113
  67. package/lib/patram-config.test.js +0 -147
@@ -0,0 +1,707 @@
1
+ /* eslint-disable max-lines */
2
+ /**
3
+ * @import { PatramDiagnostic } from './load-patram-config.types.ts';
4
+ * @import {
5
+ * ParseWhereClauseResult,
6
+ * ParsedAggregateComparison,
7
+ * ParsedAggregateName,
8
+ * ParsedAggregateTerm,
9
+ * ParsedClause,
10
+ * ParsedFieldName,
11
+ * ParsedTerm,
12
+ * ParsedTraversalTerm,
13
+ * } from './parse-where-clause.types.ts';
14
+ */
15
+
16
+ /**
17
+ * @typedef {{ index: number, where_clause: string }} ParserState
18
+ */
19
+
20
+ /**
21
+ * @typedef {{
22
+ * success: true,
23
+ * term: ParsedTerm,
24
+ * } | {
25
+ * diagnostic: PatramDiagnostic,
26
+ * success: false,
27
+ * }} ParseTermResult
28
+ */
29
+
30
+ /**
31
+ * Parse one where clause into structured clauses.
32
+ *
33
+ * @param {string} where_clause
34
+ * @returns {ParseWhereClauseResult}
35
+ */
36
+ export function parseWhereClause(where_clause) {
37
+ /** @type {ParserState} */
38
+ const parser_state = { index: 0, where_clause };
39
+
40
+ skipWhitespace(parser_state);
41
+
42
+ if (isAtEnd(parser_state)) {
43
+ return fail(1, 'Query must not be empty.');
44
+ }
45
+
46
+ const clauses_result = parseClauses(parser_state, null);
47
+
48
+ if (!clauses_result.success) {
49
+ return clauses_result;
50
+ }
51
+
52
+ skipWhitespace(parser_state);
53
+
54
+ if (!isAtEnd(parser_state)) {
55
+ return failToken(parser_state);
56
+ }
57
+
58
+ return {
59
+ clauses: clauses_result.clauses,
60
+ success: true,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * @param {ParserState} parser_state
66
+ * @param {')' | null} stop_character
67
+ * @returns {ParseWhereClauseResult}
68
+ */
69
+ function parseClauses(parser_state, stop_character) {
70
+ /** @type {ParsedClause[]} */
71
+ const clauses = [];
72
+ let is_first_clause = true;
73
+
74
+ while (true) {
75
+ skipWhitespace(parser_state);
76
+
77
+ if (
78
+ currentCharacter(parser_state) === stop_character ||
79
+ isAtEnd(parser_state)
80
+ ) {
81
+ return is_first_clause
82
+ ? fail(parser_state.where_clause.length + 1, 'Expected a query term.')
83
+ : { clauses, success: true };
84
+ }
85
+
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
+ }
102
+ }
103
+
104
+ const clause_result = parseClause(parser_state);
105
+
106
+ if (!clause_result.success) {
107
+ return clause_result;
108
+ }
109
+
110
+ clauses.push(clause_result.clause);
111
+ is_first_clause = false;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * @param {ParserState} parser_state
117
+ * @returns {{ clause: ParsedClause, success: true } | { diagnostic: PatramDiagnostic, success: false }}
118
+ */
119
+ function parseClause(parser_state) {
120
+ const clause_start = parser_state.index;
121
+ const is_negated = consumeKeyword(parser_state, 'not');
122
+
123
+ if (is_negated && !consumeRequiredWhitespace(parser_state)) {
124
+ return fail(
125
+ clause_start + 1,
126
+ `Unsupported query token "${readToken(parser_state, clause_start)}".`,
127
+ );
128
+ }
129
+
130
+ const term_result = parseTerm(parser_state);
131
+
132
+ if (!term_result.success) {
133
+ return term_result;
134
+ }
135
+
136
+ return {
137
+ clause: {
138
+ is_negated,
139
+ term: term_result.term,
140
+ },
141
+ success: true,
142
+ };
143
+ }
144
+
145
+ /**
146
+ * @param {ParserState} parser_state
147
+ * @returns {ParseTermResult}
148
+ */
149
+ function parseTerm(parser_state) {
150
+ return parseAggregate(parser_state) ?? parseAtomicTerm(parser_state);
151
+ }
152
+
153
+ /**
154
+ * @param {ParserState} parser_state
155
+ * @returns {ParseTermResult | null}
156
+ */
157
+ function parseAggregate(parser_state) {
158
+ const start_index = parser_state.index;
159
+ const aggregate_name = parseIdentifier(parser_state);
160
+
161
+ if (
162
+ aggregate_name !== 'any' &&
163
+ aggregate_name !== 'count' &&
164
+ aggregate_name !== 'none'
165
+ ) {
166
+ parser_state.index = start_index;
167
+ return null;
168
+ }
169
+
170
+ skipWhitespace(parser_state);
171
+
172
+ if (currentCharacter(parser_state) !== '(') {
173
+ parser_state.index = start_index;
174
+ return null;
175
+ }
176
+
177
+ parser_state.index += 1;
178
+ skipWhitespace(parser_state);
179
+
180
+ const traversal_result = parseTraversal(parser_state);
181
+
182
+ if (!traversal_result.success) {
183
+ return traversal_result;
184
+ }
185
+
186
+ if (!consumeOperator(parser_state, ',')) {
187
+ return failToken(parser_state);
188
+ }
189
+
190
+ skipWhitespace(parser_state);
191
+ const clauses_result = parseClauses(parser_state, ')');
192
+
193
+ if (!clauses_result.success) {
194
+ return clauses_result;
195
+ }
196
+
197
+ if (!consumeOperator(parser_state, ')')) {
198
+ return failToken(parser_state);
199
+ }
200
+
201
+ return createAggregateTerm(
202
+ parser_state,
203
+ aggregate_name,
204
+ traversal_result.traversal,
205
+ clauses_result.clauses,
206
+ );
207
+ }
208
+
209
+ /**
210
+ * @param {ParserState} parser_state
211
+ * @returns {ParseTermResult}
212
+ */
213
+ function parseAtomicTerm(parser_state) {
214
+ const start_index = parser_state.index;
215
+ const field_or_relation_name = parseIdentifier(parser_state);
216
+
217
+ if (!field_or_relation_name) {
218
+ return failToken(parser_state);
219
+ }
220
+
221
+ return (
222
+ parseFieldSet(parser_state, field_or_relation_name) ??
223
+ parseOperatorTerm(parser_state, start_index, field_or_relation_name)
224
+ );
225
+ }
226
+
227
+ /**
228
+ * @param {ParserState} parser_state
229
+ * @param {ParsedAggregateName} aggregate_name
230
+ * @param {ParsedTraversalTerm} traversal
231
+ * @param {ParsedClause[]} clauses
232
+ * @returns {ParseTermResult}
233
+ */
234
+ function createAggregateTerm(parser_state, aggregate_name, traversal, clauses) {
235
+ /** @type {ParsedAggregateTerm} */
236
+ const aggregate_term = {
237
+ aggregate_name,
238
+ clauses,
239
+ kind: 'aggregate',
240
+ traversal,
241
+ };
242
+
243
+ if (aggregate_name !== 'count') {
244
+ return success(aggregate_term);
245
+ }
246
+
247
+ const count_tail = parseCountTail(parser_state);
248
+
249
+ if (!count_tail) {
250
+ return failToken(parser_state);
251
+ }
252
+
253
+ return success({
254
+ ...aggregate_term,
255
+ comparison: count_tail.comparison,
256
+ value: count_tail.value,
257
+ });
258
+ }
259
+
260
+ /**
261
+ * @param {ParserState} parser_state
262
+ * @param {ParsedFieldName | string} field_name
263
+ * @returns {ParseTermResult | null}
264
+ */
265
+ function parseFieldSet(parser_state, field_name) {
266
+ if (!isSupportedFieldName(field_name)) {
267
+ return null;
268
+ }
269
+
270
+ const start_index = parser_state.index;
271
+
272
+ if (!consumeRequiredWhitespace(parser_state)) {
273
+ return null;
274
+ }
275
+
276
+ const operator = parseSetOperator(parser_state);
277
+
278
+ if (!operator) {
279
+ parser_state.index = start_index;
280
+ return null;
281
+ }
282
+
283
+ if (!consumeRequiredWhitespace(parser_state)) {
284
+ return failToken(parser_state);
285
+ }
286
+
287
+ const values = parseList(parser_state);
288
+
289
+ return values
290
+ ? success({ field_name, kind: 'field_set', operator, values })
291
+ : failToken(parser_state);
292
+ }
293
+
294
+ /**
295
+ * @param {ParserState} parser_state
296
+ * @param {number} start_index
297
+ * @param {string} field_or_relation_name
298
+ * @returns {ParseTermResult}
299
+ */
300
+ function parseOperatorTerm(parser_state, start_index, field_or_relation_name) {
301
+ const prefix_term = parsePrefixTerm(parser_state, field_or_relation_name);
302
+
303
+ if (prefix_term) {
304
+ return prefix_term;
305
+ }
306
+
307
+ const contains_term = parseContainsTerm(parser_state, field_or_relation_name);
308
+
309
+ if (contains_term) {
310
+ return contains_term;
311
+ }
312
+
313
+ const equality_term = parseEqualityTerm(
314
+ parser_state,
315
+ start_index,
316
+ field_or_relation_name,
317
+ );
318
+
319
+ if (equality_term) {
320
+ return equality_term;
321
+ }
322
+
323
+ if (consumeOperator(parser_state, ':*')) {
324
+ return success({
325
+ column: start_index + 1,
326
+ kind: 'relation',
327
+ relation_name: field_or_relation_name,
328
+ });
329
+ }
330
+
331
+ parser_state.index = start_index;
332
+ return failToken(parser_state);
333
+ }
334
+
335
+ /**
336
+ * @param {ParserState} parser_state
337
+ * @returns {{ comparison: ParsedAggregateComparison, value: number } | null}
338
+ */
339
+ function parseCountTail(parser_state) {
340
+ if (!consumeRequiredWhitespace(parser_state)) {
341
+ return null;
342
+ }
343
+
344
+ const comparison = parseComparison(parser_state);
345
+
346
+ if (!comparison || !consumeRequiredWhitespace(parser_state)) {
347
+ return null;
348
+ }
349
+
350
+ const value = parseInteger(parser_state);
351
+
352
+ return value === null ? null : { comparison, value };
353
+ }
354
+
355
+ /**
356
+ * @param {ParserState} parser_state
357
+ * @returns {{ success: true, traversal: ParsedTraversalTerm } | { diagnostic: PatramDiagnostic, success: false }}
358
+ */
359
+ function parseTraversal(parser_state) {
360
+ const column = parser_state.index + 1;
361
+ const direction = parseIdentifier(parser_state);
362
+
363
+ if (
364
+ (direction !== 'in' && direction !== 'out') ||
365
+ !consumeOperator(parser_state, ':')
366
+ ) {
367
+ return failToken(parser_state);
368
+ }
369
+
370
+ const relation_name = parseIdentifier(parser_state);
371
+
372
+ return relation_name
373
+ ? { success: true, traversal: { column, direction, relation_name } }
374
+ : failToken(parser_state);
375
+ }
376
+
377
+ /**
378
+ * @param {ParserState} parser_state
379
+ * @returns {'in' | 'not in' | null}
380
+ */
381
+ function parseSetOperator(parser_state) {
382
+ if (consumeKeyword(parser_state, 'in')) {
383
+ return 'in';
384
+ }
385
+
386
+ if (
387
+ !consumeKeyword(parser_state, 'not') ||
388
+ !consumeRequiredWhitespace(parser_state)
389
+ ) {
390
+ return null;
391
+ }
392
+
393
+ return consumeKeyword(parser_state, 'in') ? 'not in' : null;
394
+ }
395
+
396
+ /**
397
+ * @param {ParserState} parser_state
398
+ * @param {string} field_name
399
+ * @returns {ParseTermResult | null}
400
+ */
401
+ function parsePrefixTerm(parser_state, field_name) {
402
+ if (field_name !== 'id' && field_name !== 'path') {
403
+ return null;
404
+ }
405
+
406
+ if (!consumeOperator(parser_state, '^=')) {
407
+ return null;
408
+ }
409
+
410
+ const value = parseBareValue(parser_state);
411
+
412
+ return value
413
+ ? success({ field_name, kind: 'field', operator: '^=', value })
414
+ : failToken(parser_state);
415
+ }
416
+
417
+ /**
418
+ * @param {ParserState} parser_state
419
+ * @param {string} field_name
420
+ * @returns {ParseTermResult | null}
421
+ */
422
+ function parseContainsTerm(parser_state, field_name) {
423
+ if (field_name !== 'title' || !consumeOperator(parser_state, '~')) {
424
+ return null;
425
+ }
426
+
427
+ const value = parseBareValue(parser_state);
428
+
429
+ return value
430
+ ? success({ field_name: 'title', kind: 'field', operator: '~', value })
431
+ : failToken(parser_state);
432
+ }
433
+
434
+ /**
435
+ * @param {ParserState} parser_state
436
+ * @param {number} start_index
437
+ * @param {string} field_or_relation_name
438
+ * @returns {ParseTermResult | null}
439
+ */
440
+ function parseEqualityTerm(parser_state, start_index, field_or_relation_name) {
441
+ if (!consumeOperator(parser_state, '=')) {
442
+ return null;
443
+ }
444
+
445
+ const value = parseBareValue(parser_state);
446
+
447
+ if (!value) {
448
+ return failToken(parser_state);
449
+ }
450
+
451
+ if (isExactMatchField(field_or_relation_name)) {
452
+ return success({
453
+ field_name: field_or_relation_name,
454
+ kind: 'field',
455
+ operator: '=',
456
+ value,
457
+ });
458
+ }
459
+
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
+ });
467
+ }
468
+
469
+ parser_state.index = start_index;
470
+ return failToken(parser_state);
471
+ }
472
+
473
+ /**
474
+ * @param {ParserState} parser_state
475
+ * @returns {string[] | null}
476
+ */
477
+ function parseList(parser_state) {
478
+ if (!consumeOperator(parser_state, '[')) {
479
+ return null;
480
+ }
481
+
482
+ /** @type {string[]} */
483
+ const values = [];
484
+
485
+ while (true) {
486
+ skipWhitespace(parser_state);
487
+
488
+ if (consumeOperator(parser_state, ']')) {
489
+ return values.length > 0 ? values : null;
490
+ }
491
+
492
+ const list_value = parseListValue(parser_state);
493
+
494
+ if (!list_value) {
495
+ return null;
496
+ }
497
+
498
+ values.push(list_value);
499
+ skipWhitespace(parser_state);
500
+
501
+ if (consumeOperator(parser_state, ']')) {
502
+ return values;
503
+ }
504
+
505
+ if (!consumeOperator(parser_state, ',')) {
506
+ return null;
507
+ }
508
+ }
509
+ }
510
+
511
+ /**
512
+ * @param {ParserState} parser_state
513
+ * @returns {ParsedAggregateComparison | null}
514
+ */
515
+ function parseComparison(parser_state) {
516
+ /** @type {ParsedAggregateComparison[]} */
517
+ const comparisons = ['>=', '<=', '!=', '=', '>', '<'];
518
+
519
+ return (
520
+ comparisons.find((value) => consumeOperator(parser_state, value)) ?? null
521
+ );
522
+ }
523
+
524
+ /**
525
+ * @param {ParserState} parser_state
526
+ * @returns {string | null}
527
+ */
528
+ function parseIdentifier(parser_state) {
529
+ return readMatch(parser_state, /^[a-z_]+/u);
530
+ }
531
+
532
+ /**
533
+ * @param {ParserState} parser_state
534
+ * @returns {number | null}
535
+ */
536
+ function parseInteger(parser_state) {
537
+ const numeric_text = readMatch(parser_state, /^\d+/u);
538
+ return numeric_text ? Number.parseInt(numeric_text, 10) : null;
539
+ }
540
+
541
+ /**
542
+ * @param {ParserState} parser_state
543
+ * @returns {string | null}
544
+ */
545
+ function parseBareValue(parser_state) {
546
+ return readMatch(parser_state, /^[^\s\],)]+/u);
547
+ }
548
+
549
+ /**
550
+ * @param {ParserState} parser_state
551
+ * @returns {string | null}
552
+ */
553
+ function parseListValue(parser_state) {
554
+ const list_value = readMatch(parser_state, /^[^\s\],][^\],)]*/u);
555
+ return list_value?.trim() || null;
556
+ }
557
+
558
+ /**
559
+ * @param {ParserState} parser_state
560
+ * @param {RegExp} pattern
561
+ * @returns {string | null}
562
+ */
563
+ function readMatch(parser_state, pattern) {
564
+ const match = parser_state.where_clause
565
+ .slice(parser_state.index)
566
+ .match(pattern);
567
+
568
+ if (!match) {
569
+ return null;
570
+ }
571
+
572
+ parser_state.index += match[0].length;
573
+ return match[0];
574
+ }
575
+
576
+ /**
577
+ * @param {ParserState} parser_state
578
+ * @param {string} operator
579
+ * @returns {boolean}
580
+ */
581
+ function consumeOperator(parser_state, operator) {
582
+ skipWhitespace(parser_state);
583
+
584
+ if (!parser_state.where_clause.startsWith(operator, parser_state.index)) {
585
+ return false;
586
+ }
587
+
588
+ parser_state.index += operator.length;
589
+ return true;
590
+ }
591
+
592
+ /**
593
+ * @param {ParserState} parser_state
594
+ * @param {string} keyword
595
+ * @returns {boolean}
596
+ */
597
+ function consumeKeyword(parser_state, keyword) {
598
+ if (!parser_state.where_clause.startsWith(keyword, parser_state.index)) {
599
+ return false;
600
+ }
601
+
602
+ const next_character =
603
+ parser_state.where_clause[parser_state.index + keyword.length];
604
+
605
+ if (next_character && /[a-z_]/u.test(next_character)) {
606
+ return false;
607
+ }
608
+
609
+ parser_state.index += keyword.length;
610
+ return true;
611
+ }
612
+
613
+ /**
614
+ * @param {ParserState} parser_state
615
+ * @returns {boolean}
616
+ */
617
+ function consumeRequiredWhitespace(parser_state) {
618
+ return skipWhitespace(parser_state) > 0;
619
+ }
620
+
621
+ /**
622
+ * @param {ParserState} parser_state
623
+ * @returns {number}
624
+ */
625
+ function skipWhitespace(parser_state) {
626
+ const whitespace = readMatch(parser_state, /^\s+/u);
627
+ return whitespace?.length ?? 0;
628
+ }
629
+
630
+ /**
631
+ * @param {ParserState} parser_state
632
+ * @returns {string | undefined}
633
+ */
634
+ function currentCharacter(parser_state) {
635
+ return parser_state.where_clause[parser_state.index];
636
+ }
637
+
638
+ /**
639
+ * @param {ParserState} parser_state
640
+ * @returns {boolean}
641
+ */
642
+ function isAtEnd(parser_state) {
643
+ return parser_state.index >= parser_state.where_clause.length;
644
+ }
645
+
646
+ /**
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'}
657
+ */
658
+ function isExactMatchField(field_name) {
659
+ return ['id', 'kind', 'path', 'status'].includes(field_name);
660
+ }
661
+
662
+ /**
663
+ * @param {ParsedTerm} term
664
+ * @returns {ParseTermResult}
665
+ */
666
+ function success(term) {
667
+ return { success: true, term };
668
+ }
669
+
670
+ /**
671
+ * @param {ParserState} parser_state
672
+ * @returns {{ diagnostic: PatramDiagnostic, success: false }}
673
+ */
674
+ function failToken(parser_state) {
675
+ return fail(
676
+ parser_state.index + 1,
677
+ `Unsupported query token "${readToken(parser_state, parser_state.index)}".`,
678
+ );
679
+ }
680
+
681
+ /**
682
+ * @param {number} column
683
+ * @param {string} message
684
+ * @returns {{ diagnostic: PatramDiagnostic, success: false }}
685
+ */
686
+ function fail(column, message) {
687
+ return {
688
+ diagnostic: {
689
+ code: 'query.invalid',
690
+ column,
691
+ level: 'error',
692
+ line: 1,
693
+ message,
694
+ path: '<query>',
695
+ },
696
+ success: false,
697
+ };
698
+ }
699
+
700
+ /**
701
+ * @param {ParserState} parser_state
702
+ * @param {number} start_index
703
+ * @returns {string}
704
+ */
705
+ function readToken(parser_state, start_index) {
706
+ return parser_state.where_clause.slice(start_index).match(/^\S+/u)?.[0] ?? '';
707
+ }