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.
- package/lib/build-graph-identity.js +86 -99
- package/lib/build-graph.js +536 -31
- package/lib/build-graph.types.ts +6 -2
- package/lib/check-directive-metadata.js +534 -0
- package/lib/check-directive-value.js +291 -0
- package/lib/check-graph.js +23 -5
- package/lib/cli-help-metadata.js +56 -16
- package/lib/command-output.js +16 -1
- package/lib/derived-summary.js +10 -8
- package/lib/directive-diagnostics.js +38 -0
- package/lib/directive-type-rules.js +133 -0
- package/lib/discover-fields.js +435 -0
- package/lib/discover-fields.types.ts +52 -0
- package/lib/document-node-identity.js +317 -0
- package/lib/format-node-header.js +9 -7
- package/lib/format-output-metadata.js +15 -23
- package/lib/layout-stored-queries.js +124 -85
- package/lib/load-patram-config.js +433 -96
- package/lib/load-patram-config.types.ts +98 -3
- package/lib/load-project-graph.js +4 -1
- package/lib/output-view.types.ts +14 -6
- package/lib/parse-cli-arguments.types.ts +1 -1
- package/lib/parse-where-clause.js +344 -107
- package/lib/parse-where-clause.types.ts +25 -8
- package/lib/patram-cli.js +68 -4
- package/lib/patram-config.js +31 -31
- package/lib/patram-config.types.ts +10 -4
- package/lib/query-graph.js +269 -40
- package/lib/query-inspection.js +440 -60
- package/lib/render-field-discovery.js +184 -0
- package/lib/render-json-output.js +21 -22
- package/lib/render-output-view.js +301 -34
- package/lib/render-plain-output.js +1 -1
- package/lib/render-rich-output.js +1 -1
- package/lib/render-rich-source.js +245 -14
- package/lib/resolve-patram-graph-config.js +15 -9
- package/lib/show-document.js +66 -9
- package/package.json +5 -5
package/lib/query-inspection.js
CHANGED
|
@@ -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
|
-
*
|
|
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 =
|
|
74
|
+
const diagnostics = getQuerySemanticDiagnostics(
|
|
71
75
|
repo_config,
|
|
72
76
|
resolved_where_clause.query_source,
|
|
73
|
-
parse_result.
|
|
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 {
|
|
149
|
+
* @param {ParsedExpression} expression
|
|
144
150
|
* @returns {PatramDiagnostic[]}
|
|
145
151
|
*/
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const known_relation_names = new Set(
|
|
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
|
-
|
|
156
|
-
|
|
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 {
|
|
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
|
|
172
|
-
|
|
181
|
+
function collectExpressionDiagnostics(
|
|
182
|
+
expression,
|
|
173
183
|
diagnostics,
|
|
174
184
|
known_relation_names,
|
|
185
|
+
known_field_definitions,
|
|
175
186
|
diagnostic_path,
|
|
176
187
|
) {
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
term.
|
|
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('
|
|
355
|
-
...
|
|
356
|
-
query_inspection.
|
|
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 {
|
|
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
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
676
|
+
return formatExplainExpressionItems(
|
|
677
|
+
[expression],
|
|
678
|
+
render_options,
|
|
679
|
+
indentation,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
378
682
|
|
|
379
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
697
|
+
expressions.forEach((expression, expression_index) => {
|
|
698
|
+
const expression_number = expression_index + 1;
|
|
384
699
|
|
|
385
700
|
output_lines.push(
|
|
386
|
-
`${indentation}
|
|
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
|
-
|
|
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 {
|
|
712
|
+
* @param {ParsedExpression} expression
|
|
410
713
|
* @returns {string}
|
|
411
714
|
*/
|
|
412
|
-
function
|
|
413
|
-
|
|
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 (
|
|
416
|
-
|
|
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
|
-
|
|
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
|
|
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,
|