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
@@ -3,7 +3,7 @@
3
3
  * @import { PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
4
4
  * @import { ResolvedOutputMode } from './output-view.types.ts';
5
5
  * @import {
6
- * ParsedClause,
6
+ * ParsedExpression,
7
7
  * ParsedRelationTargetTerm,
8
8
  * ParsedRelationTerm,
9
9
  * ParsedTerm,
@@ -19,6 +19,10 @@ import { parseWhereClause } from './parse-where-clause.js';
19
19
  * @typedef {{ kind: 'ad_hoc' } | { kind: 'stored_query', name: string }} QuerySource
20
20
  */
21
21
 
22
+ /**
23
+ * @typedef {import('./parse-where-clause.types.ts').ParsedFieldTerm | import('./parse-where-clause.types.ts').ParsedFieldSetTerm} FieldDiagnosticTerm
24
+ */
25
+
22
26
  /**
23
27
  * @typedef {{ query_source: QuerySource, where_clause: string }} ResolvedWhereClause
24
28
  */
@@ -33,10 +37,10 @@ import { parseWhereClause } from './parse-where-clause.js';
33
37
  * limit: number | null,
34
38
  * offset: number,
35
39
  * },
40
+ * expression?: ParsedExpression,
36
41
  * inspection_mode: 'explain' | 'lint',
37
42
  * query_source: QuerySource,
38
43
  * where_clause: string,
39
- * clauses?: ParsedClause[],
40
44
  * }} QueryInspectionSuccess
41
45
  */
42
46
 
@@ -67,10 +71,10 @@ export function inspectQuery(
67
71
  };
68
72
  }
69
73
 
70
- const diagnostics = collectRelationDiagnostics(
74
+ const diagnostics = getQuerySemanticDiagnostics(
71
75
  repo_config,
72
76
  resolved_where_clause.query_source,
73
- parse_result.clauses,
77
+ parse_result.expression,
74
78
  );
75
79
 
76
80
  if (diagnostics.length > 0) {
@@ -94,11 +98,11 @@ export function inspectQuery(
94
98
  return {
95
99
  success: true,
96
100
  value: {
97
- clauses: parse_result.clauses,
98
101
  execution: {
99
102
  limit: inspection_options.limit,
100
103
  offset: inspection_options.offset,
101
104
  },
105
+ expression: parse_result.expression,
102
106
  inspection_mode: 'explain',
103
107
  query_source: resolved_where_clause.query_source,
104
108
  where_clause: resolved_where_clause.where_clause,
@@ -138,24 +142,29 @@ export function renderQueryInspection(query_inspection, output_mode) {
138
142
  }
139
143
 
140
144
  /**
145
+ * Collect schema-aware diagnostics for one parsed where clause.
146
+ *
141
147
  * @param {PatramRepoConfig} repo_config
142
148
  * @param {QuerySource} query_source
143
- * @param {ParsedClause[]} clauses
149
+ * @param {ParsedExpression} expression
144
150
  * @returns {PatramDiagnostic[]}
145
151
  */
146
- function collectRelationDiagnostics(repo_config, query_source, clauses) {
147
- if (!repo_config.relations) {
148
- return [];
149
- }
150
-
151
- const known_relation_names = new Set(Object.keys(repo_config.relations));
152
+ export function getQuerySemanticDiagnostics(
153
+ repo_config,
154
+ query_source,
155
+ expression,
156
+ ) {
157
+ const known_relation_names = new Set(
158
+ Object.keys(repo_config.relations ?? {}),
159
+ );
152
160
  /** @type {PatramDiagnostic[]} */
153
161
  const diagnostics = [];
154
162
 
155
- collectClauseDiagnostics(
156
- clauses,
163
+ collectExpressionDiagnostics(
164
+ expression,
157
165
  diagnostics,
158
166
  known_relation_names,
167
+ repo_config.fields ?? {},
159
168
  formatQueryDiagnosticPath(query_source),
160
169
  );
161
170
 
@@ -163,37 +172,72 @@ function collectRelationDiagnostics(repo_config, query_source, clauses) {
163
172
  }
164
173
 
165
174
  /**
166
- * @param {ParsedClause[]} clauses
175
+ * @param {ParsedExpression} expression
167
176
  * @param {PatramDiagnostic[]} diagnostics
168
177
  * @param {Set<string>} known_relation_names
178
+ * @param {Record<string, import('./load-patram-config.types.ts').MetadataFieldConfig>} known_field_definitions
169
179
  * @param {string} diagnostic_path
170
180
  */
171
- function collectClauseDiagnostics(
172
- clauses,
181
+ function collectExpressionDiagnostics(
182
+ expression,
173
183
  diagnostics,
174
184
  known_relation_names,
185
+ known_field_definitions,
175
186
  diagnostic_path,
176
187
  ) {
177
- for (const clause of clauses) {
188
+ if (expression.kind === 'and' || expression.kind === 'or') {
189
+ for (const subexpression of expression.expressions) {
190
+ collectExpressionDiagnostics(
191
+ subexpression,
192
+ diagnostics,
193
+ known_relation_names,
194
+ known_field_definitions,
195
+ diagnostic_path,
196
+ );
197
+ }
198
+
199
+ return;
200
+ }
201
+
202
+ if (expression.kind === 'not') {
203
+ collectExpressionDiagnostics(
204
+ expression.expression,
205
+ diagnostics,
206
+ known_relation_names,
207
+ known_field_definitions,
208
+ diagnostic_path,
209
+ );
210
+
211
+ return;
212
+ }
213
+
214
+ if (expression.kind === 'term') {
178
215
  collectTermDiagnostics(
179
- clause.term,
216
+ expression.term,
180
217
  diagnostics,
181
218
  known_relation_names,
219
+ known_field_definitions,
182
220
  diagnostic_path,
183
221
  );
222
+
223
+ return;
184
224
  }
225
+
226
+ throw new Error('Unsupported query inspection expression.');
185
227
  }
186
228
 
187
229
  /**
188
230
  * @param {ParsedTerm} term
189
231
  * @param {PatramDiagnostic[]} diagnostics
190
232
  * @param {Set<string>} known_relation_names
233
+ * @param {Record<string, import('./load-patram-config.types.ts').MetadataFieldConfig>} known_field_definitions
191
234
  * @param {string} diagnostic_path
192
235
  */
193
236
  function collectTermDiagnostics(
194
237
  term,
195
238
  diagnostics,
196
239
  known_relation_names,
240
+ known_field_definitions,
197
241
  diagnostic_path,
198
242
  ) {
199
243
  if (term.kind === 'aggregate') {
@@ -203,10 +247,11 @@ function collectTermDiagnostics(
203
247
  known_relation_names,
204
248
  diagnostic_path,
205
249
  );
206
- collectClauseDiagnostics(
207
- term.clauses,
250
+ collectExpressionDiagnostics(
251
+ term.expression,
208
252
  diagnostics,
209
253
  known_relation_names,
254
+ known_field_definitions,
210
255
  diagnostic_path,
211
256
  );
212
257
 
@@ -233,7 +278,256 @@ function collectTermDiagnostics(
233
278
  diagnostic_path,
234
279
  'relation clause',
235
280
  );
281
+
282
+ return;
236
283
  }
284
+
285
+ collectFieldDiagnostics(
286
+ /** @type {FieldDiagnosticTerm} */ (term),
287
+ diagnostics,
288
+ known_field_definitions,
289
+ diagnostic_path,
290
+ );
291
+ }
292
+
293
+ /**
294
+ * @param {FieldDiagnosticTerm} term
295
+ * @param {PatramDiagnostic[]} diagnostics
296
+ * @param {Record<string, import('./load-patram-config.types.ts').MetadataFieldConfig>} known_field_definitions
297
+ * @param {string} diagnostic_path
298
+ */
299
+ function collectFieldDiagnostics(
300
+ term,
301
+ diagnostics,
302
+ known_field_definitions,
303
+ diagnostic_path,
304
+ ) {
305
+ const field_diagnostics_collector = getFieldDiagnosticsCollector(
306
+ term.field_name,
307
+ );
308
+
309
+ if (field_diagnostics_collector) {
310
+ field_diagnostics_collector(term, diagnostics, diagnostic_path);
311
+ return;
312
+ }
313
+
314
+ if (term.field_name.startsWith('$')) {
315
+ diagnostics.push(
316
+ createQueryDiagnostic(
317
+ diagnostic_path,
318
+ term.column,
319
+ `Reserved field "${term.field_name}" is not available.`,
320
+ 'query.reserved_field',
321
+ ),
322
+ );
323
+
324
+ return;
325
+ }
326
+
327
+ const field_definition = known_field_definitions[term.field_name];
328
+
329
+ if (!field_definition) {
330
+ diagnostics.push(
331
+ createQueryDiagnostic(
332
+ diagnostic_path,
333
+ term.column,
334
+ `Unknown field "${term.field_name}".`,
335
+ 'query.unknown_field',
336
+ ),
337
+ );
338
+
339
+ return;
340
+ }
341
+
342
+ if (term.kind === 'field_set') {
343
+ if (term.operator === 'in' || term.operator === 'not in') {
344
+ return;
345
+ }
346
+ }
347
+
348
+ if (term.kind === 'field') {
349
+ const operator_diagnostic = getMetadataFieldOperatorDiagnostic(
350
+ term,
351
+ field_definition,
352
+ diagnostic_path,
353
+ );
354
+
355
+ if (operator_diagnostic) {
356
+ diagnostics.push(operator_diagnostic);
357
+ }
358
+ }
359
+ }
360
+
361
+ /**
362
+ * @param {string} field_name
363
+ * @returns {((term: FieldDiagnosticTerm, diagnostics: PatramDiagnostic[], diagnostic_path: string) => void) | null}
364
+ */
365
+ function getFieldDiagnosticsCollector(field_name) {
366
+ if (field_name === 'title') {
367
+ return collectTitleFieldDiagnostics;
368
+ }
369
+
370
+ if (
371
+ field_name === '$id' ||
372
+ field_name === '$class' ||
373
+ field_name === '$path'
374
+ ) {
375
+ return collectStructuralFieldDiagnostics;
376
+ }
377
+
378
+ return null;
379
+ }
380
+
381
+ /**
382
+ * @param {FieldDiagnosticTerm} term
383
+ * @param {PatramDiagnostic[]} diagnostics
384
+ * @param {string} diagnostic_path
385
+ */
386
+ function collectTitleFieldDiagnostics(term, diagnostics, diagnostic_path) {
387
+ const operator = term.kind === 'field' ? term.operator : term.operator;
388
+ const allowed = new Set(['=', '!=', 'in', 'not in', '~']);
389
+
390
+ if (allowed.has(operator)) {
391
+ return;
392
+ }
393
+
394
+ diagnostics.push(
395
+ createQueryDiagnostic(
396
+ diagnostic_path,
397
+ term.column,
398
+ `Field "title" does not support the "${operator}" operator.`,
399
+ 'query.invalid_operator',
400
+ ),
401
+ );
402
+ }
403
+
404
+ /**
405
+ * @param {FieldDiagnosticTerm} term
406
+ * @param {PatramDiagnostic[]} diagnostics
407
+ * @param {string} diagnostic_path
408
+ */
409
+ function collectStructuralFieldDiagnostics(term, diagnostics, diagnostic_path) {
410
+ const operator = term.kind === 'field' ? term.operator : term.operator;
411
+ const allowed_by_field = new Map([
412
+ ['$class', new Set(['=', '!=', 'in', 'not in'])],
413
+ ['$id', new Set(['=', '!=', 'in', 'not in', '^='])],
414
+ ['$path', new Set(['=', '!=', 'in', 'not in', '^='])],
415
+ ]);
416
+ const allowed = allowed_by_field.get(term.field_name);
417
+
418
+ if (!allowed || allowed.has(operator)) {
419
+ return;
420
+ }
421
+
422
+ diagnostics.push(
423
+ createQueryDiagnostic(
424
+ diagnostic_path,
425
+ term.column,
426
+ `Field "${term.field_name}" does not support the "${operator}" operator.`,
427
+ 'query.invalid_operator',
428
+ ),
429
+ );
430
+ }
431
+
432
+ /**
433
+ * @param {import('./parse-where-clause.types.ts').ParsedFieldTerm} term
434
+ * @param {import('./load-patram-config.types.ts').MetadataFieldConfig} field_definition
435
+ * @param {string} diagnostic_path
436
+ * @returns {PatramDiagnostic | null}
437
+ */
438
+ function getMetadataFieldOperatorDiagnostic(
439
+ term,
440
+ field_definition,
441
+ diagnostic_path,
442
+ ) {
443
+ if (supportsMetadataFieldOperator(term.operator, field_definition)) {
444
+ return null;
445
+ }
446
+
447
+ return createQueryDiagnostic(
448
+ diagnostic_path,
449
+ term.column,
450
+ getUnsupportedOperatorMessage(term.field_name, term.operator),
451
+ 'query.invalid_operator',
452
+ );
453
+ }
454
+
455
+ /**
456
+ * @param {string} operator
457
+ * @param {import('./load-patram-config.types.ts').MetadataFieldConfig} field_definition
458
+ * @returns {boolean}
459
+ */
460
+ function supportsMetadataFieldOperator(operator, field_definition) {
461
+ if (operator === '=' || operator === '!=') {
462
+ return true;
463
+ }
464
+
465
+ if (operator === '^=') {
466
+ return supportsPrefixOperator(field_definition);
467
+ }
468
+
469
+ if (operator === '~') {
470
+ return supportsContainsOperator(field_definition);
471
+ }
472
+
473
+ return supportsOrderedComparison(operator, field_definition.type);
474
+ }
475
+
476
+ /**
477
+ * @param {import('./load-patram-config.types.ts').MetadataFieldConfig} field_definition
478
+ * @returns {boolean}
479
+ */
480
+ function supportsPrefixOperator(field_definition) {
481
+ if (field_definition.type === 'path') {
482
+ return true;
483
+ }
484
+
485
+ return (
486
+ field_definition.type === 'string' &&
487
+ field_definition.query?.prefix === true
488
+ );
489
+ }
490
+
491
+ /**
492
+ * @param {import('./load-patram-config.types.ts').MetadataFieldConfig} field_definition
493
+ * @returns {boolean}
494
+ */
495
+ function supportsContainsOperator(field_definition) {
496
+ return (
497
+ field_definition.type === 'string' &&
498
+ field_definition.query?.contains === true
499
+ );
500
+ }
501
+
502
+ /**
503
+ * @param {string} operator
504
+ * @param {string} field_type
505
+ * @returns {boolean}
506
+ */
507
+ function supportsOrderedComparison(operator, field_type) {
508
+ if (
509
+ operator !== '<' &&
510
+ operator !== '<=' &&
511
+ operator !== '>' &&
512
+ operator !== '>='
513
+ ) {
514
+ return false;
515
+ }
516
+
517
+ return (
518
+ field_type === 'integer' ||
519
+ field_type === 'date' ||
520
+ field_type === 'date_time'
521
+ );
522
+ }
523
+
524
+ /**
525
+ * @param {string} field_name
526
+ * @param {string} operator
527
+ * @returns {string}
528
+ */
529
+ function getUnsupportedOperatorMessage(field_name, operator) {
530
+ return `Field "${field_name}" does not support the "${operator}" operator.`;
237
531
  }
238
532
 
239
533
  /**
@@ -290,7 +584,6 @@ function collectTraversalDiagnostic(
290
584
 
291
585
  /**
292
586
  * @param {QueryInspectionSuccess} query_inspection
293
- * @returns {object}
294
587
  */
295
588
  function formatJsonQueryInspection(query_inspection) {
296
589
  if (query_inspection.inspection_mode === 'lint') {
@@ -303,9 +596,9 @@ function formatJsonQueryInspection(query_inspection) {
303
596
  }
304
597
 
305
598
  return {
306
- clauses: query_inspection.clauses,
307
599
  diagnostics: [],
308
600
  execution: query_inspection.execution,
601
+ expression: query_inspection.expression,
309
602
  mode: 'explain',
310
603
  source: query_inspection.query_source,
311
604
  where: query_inspection.where_clause,
@@ -351,9 +644,12 @@ function renderTextQueryInspection(query_inspection, render_options) {
351
644
  : String(query_inspection.execution?.limit ?? ''),
352
645
  ),
353
646
  '',
354
- `${render_options.label('clauses:')}`,
355
- ...formatExplainClauseBlock(
356
- query_inspection.clauses ?? [],
647
+ `${render_options.label('expression:')}`,
648
+ ...formatExplainExpressionBlock(
649
+ query_inspection.expression ?? {
650
+ expressions: [],
651
+ kind: 'and',
652
+ },
357
653
  render_options,
358
654
  '',
359
655
  ),
@@ -363,42 +659,49 @@ function renderTextQueryInspection(query_inspection, render_options) {
363
659
  }
364
660
 
365
661
  /**
366
- * @param {ParsedClause[]} clauses
662
+ * @param {ParsedExpression} expression
367
663
  * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
368
664
  * @param {string} indentation
369
665
  * @returns {string[]}
370
666
  */
371
- function formatExplainClauseBlock(clauses, render_options, indentation) {
372
- /** @type {string[]} */
373
- const output_lines = [];
667
+ function formatExplainExpressionBlock(expression, render_options, indentation) {
668
+ if (expression.kind === 'and') {
669
+ return formatExplainExpressionItems(
670
+ expression.expressions,
671
+ render_options,
672
+ indentation,
673
+ );
674
+ }
374
675
 
375
- clauses.forEach((clause, clause_index) => {
376
- const clause_number = clause_index + 1;
377
- const clause_text = formatClauseSummary(clause);
676
+ return formatExplainExpressionItems(
677
+ [expression],
678
+ render_options,
679
+ indentation,
680
+ );
681
+ }
378
682
 
379
- output_lines.push(`${indentation}${clause_number}. ${clause_text}`);
683
+ /**
684
+ * @param {ParsedExpression[]} expressions
685
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
686
+ * @param {string} indentation
687
+ * @returns {string[]}
688
+ */
689
+ function formatExplainExpressionItems(
690
+ expressions,
691
+ render_options,
692
+ indentation,
693
+ ) {
694
+ /** @type {string[]} */
695
+ const output_lines = [];
380
696
 
381
- if (clause.term.kind !== 'aggregate') {
382
- return;
383
- }
697
+ expressions.forEach((expression, expression_index) => {
698
+ const expression_number = expression_index + 1;
384
699
 
385
700
  output_lines.push(
386
- `${indentation} ${render_options.label('traversal:')} ${formatTraversal(clause.term.traversal)}`,
701
+ `${indentation}${expression_number}. ${formatExpressionSummary(expression)}`,
387
702
  );
388
-
389
- if (clause.term.aggregate_name === 'count') {
390
- output_lines.push(
391
- `${indentation} ${render_options.label('comparison:')} ${clause.term.comparison} ${clause.term.value}`,
392
- );
393
- }
394
-
395
703
  output_lines.push(
396
- `${indentation} ${render_options.label('nested clauses:')}`,
397
- ...formatExplainClauseBlock(
398
- clause.term.clauses,
399
- render_options,
400
- `${indentation} `,
401
- ),
704
+ ...formatExpressionDetailLines(expression, render_options, indentation),
402
705
  );
403
706
  });
404
707
 
@@ -406,17 +709,89 @@ function formatExplainClauseBlock(clauses, render_options, indentation) {
406
709
  }
407
710
 
408
711
  /**
409
- * @param {ParsedClause} clause
712
+ * @param {ParsedExpression} expression
410
713
  * @returns {string}
411
714
  */
412
- function formatClauseSummary(clause) {
413
- const clause_prefix = clause.is_negated ? 'not ' : '';
715
+ function formatExpressionSummary(expression) {
716
+ if (expression.kind === 'and') {
717
+ return 'all of';
718
+ }
719
+
720
+ if (expression.kind === 'or') {
721
+ return 'any of';
722
+ }
723
+
724
+ if (expression.kind === 'not') {
725
+ if (expression.expression.kind === 'term') {
726
+ return `not ${formatTermSummary(expression.expression.term)}`;
727
+ }
728
+
729
+ return 'not';
730
+ }
731
+
732
+ if (expression.kind === 'term') {
733
+ return formatTermSummary(expression.term);
734
+ }
735
+
736
+ throw new Error('Unsupported explain expression.');
737
+ }
738
+
739
+ /**
740
+ * @param {ParsedExpression} expression
741
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
742
+ * @param {string} indentation
743
+ * @returns {string[]}
744
+ */
745
+ function formatExpressionDetailLines(expression, render_options, indentation) {
746
+ if (expression.kind === 'and' || expression.kind === 'or') {
747
+ return formatExplainExpressionItems(
748
+ expression.expressions,
749
+ render_options,
750
+ `${indentation} `,
751
+ );
752
+ }
753
+
754
+ if (expression.kind === 'not') {
755
+ if (expression.expression.kind === 'term') {
756
+ return [];
757
+ }
758
+
759
+ return formatExplainExpressionBlock(
760
+ expression.expression,
761
+ render_options,
762
+ `${indentation} `,
763
+ );
764
+ }
765
+
766
+ if (expression.kind !== 'term') {
767
+ throw new Error('Unsupported explain expression details.');
768
+ }
769
+
770
+ if (expression.term.kind !== 'aggregate') {
771
+ return [];
772
+ }
773
+
774
+ /** @type {string[]} */
775
+ const output_lines = [
776
+ `${indentation} ${render_options.label('traversal:')} ${formatTraversal(expression.term.traversal)}`,
777
+ ];
414
778
 
415
- if (clause.term.kind === 'aggregate') {
416
- return `${clause_prefix}aggregate ${clause.term.aggregate_name}`;
779
+ if (expression.term.aggregate_name === 'count') {
780
+ output_lines.push(
781
+ `${indentation} ${render_options.label('comparison:')} ${expression.term.comparison} ${expression.term.value}`,
782
+ );
417
783
  }
418
784
 
419
- return `${clause_prefix}${formatTermSummary(clause.term)}`;
785
+ output_lines.push(
786
+ `${indentation} ${render_options.label('nested expression:')}`,
787
+ ...formatExplainExpressionBlock(
788
+ expression.term.expression,
789
+ render_options,
790
+ `${indentation} `,
791
+ ),
792
+ );
793
+
794
+ return output_lines;
420
795
  }
421
796
 
422
797
  /**
@@ -424,6 +799,10 @@ function formatClauseSummary(clause) {
424
799
  * @returns {string}
425
800
  */
426
801
  function formatTermSummary(term) {
802
+ if (term.kind === 'aggregate') {
803
+ return `aggregate ${term.aggregate_name}`;
804
+ }
805
+
427
806
  if (term.kind === 'field') {
428
807
  return `${term.field_name} ${term.operator} ${term.value}`;
429
808
  }
@@ -440,7 +819,7 @@ function formatTermSummary(term) {
440
819
  return `${term.relation_name} = ${term.target_id}`;
441
820
  }
442
821
 
443
- throw new Error('Expected a non-aggregate query term.');
822
+ throw new Error('Expected a parsed query term.');
444
823
  }
445
824
 
446
825
  /**
@@ -489,11 +868,12 @@ function formatQueryDiagnosticPath(query_source) {
489
868
  * @param {string} diagnostic_path
490
869
  * @param {number} column
491
870
  * @param {string} message
871
+ * @param {PatramDiagnostic['code']} [code]
492
872
  * @returns {PatramDiagnostic}
493
873
  */
494
- function createQueryDiagnostic(diagnostic_path, column, message) {
874
+ function createQueryDiagnostic(diagnostic_path, column, message, code) {
495
875
  return {
496
- code: 'query.unknown_relation',
876
+ code: code ?? 'query.unknown_relation',
497
877
  column,
498
878
  level: 'error',
499
879
  line: 1,