patram 0.11.0 → 0.12.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/bin/patram.js +4 -4
- package/lib/cli/commands/fields.js +0 -4
- package/lib/cli/commands/queries.js +10 -20
- package/lib/cli/commands/query.js +1 -8
- package/lib/cli/commands/refs.js +3 -10
- package/lib/cli/commands/show.js +1 -8
- package/lib/cli/help-metadata.js +71 -106
- package/lib/cli/main.js +10 -10
- package/lib/cli/parse-arguments-helpers.js +165 -59
- package/lib/cli/parse-arguments.js +4 -4
- package/lib/cli/render-help.js +2 -2
- package/lib/config/defaults.js +33 -25
- package/lib/config/load-patram-config.d.ts +8 -33
- package/lib/config/load-patram-config.js +9 -33
- package/lib/config/load-patram-config.types.d.ts +3 -40
- package/lib/config/manage-stored-queries-helpers.d.ts +4 -4
- package/lib/config/manage-stored-queries-helpers.js +91 -33
- package/lib/config/manage-stored-queries.d.ts +4 -4
- package/lib/config/manage-stored-queries.js +11 -5
- package/lib/config/patram-config.d.ts +34 -34
- package/lib/config/patram-config.js +3 -3
- package/lib/config/patram-config.types.d.ts +5 -11
- package/lib/config/resolve-patram-graph-config.d.ts +5 -1
- package/lib/config/resolve-patram-graph-config.js +3 -119
- package/lib/config/schema.d.ts +158 -269
- package/lib/config/schema.js +72 -210
- package/lib/config/validate-patram-config-value.js +6 -31
- package/lib/config/validation.d.ts +2 -12
- package/lib/config/validation.js +125 -483
- package/lib/find-close-match.d.ts +4 -1
- package/lib/graph/build-graph-identity.d.ts +1 -32
- package/lib/graph/build-graph-identity.js +5 -269
- package/lib/graph/build-graph.d.ts +13 -4
- package/lib/graph/build-graph.js +347 -488
- package/lib/graph/build-graph.types.d.ts +8 -9
- package/lib/graph/check-directive-metadata-helpers.d.ts +30 -0
- package/lib/graph/check-directive-metadata-helpers.js +126 -0
- package/lib/graph/check-directive-metadata.d.ts +8 -9
- package/lib/graph/check-directive-metadata.js +70 -561
- package/lib/graph/check-directive-path-target.d.ts +6 -13
- package/lib/graph/check-directive-path-target.js +26 -57
- package/lib/graph/check-directive-value.d.ts +1 -5
- package/lib/graph/check-directive-value.js +40 -180
- package/lib/graph/check-graph.d.ts +5 -5
- package/lib/graph/check-graph.js +8 -6
- package/lib/graph/document-node-identity.d.ts +23 -7
- package/lib/graph/document-node-identity.js +417 -160
- package/lib/graph/graph-node.d.ts +42 -0
- package/lib/graph/graph-node.js +83 -0
- package/lib/graph/inspect-reverse-references.js +16 -11
- package/lib/graph/load-project-graph.d.ts +7 -7
- package/lib/graph/load-project-graph.js +7 -7
- package/lib/graph/parse-where-clause.types.d.ts +3 -2
- package/lib/graph/query/cypher-reader.d.ts +59 -0
- package/lib/graph/query/cypher-reader.js +151 -0
- package/lib/graph/query/cypher-support.d.ts +79 -0
- package/lib/graph/query/cypher-support.js +213 -0
- package/lib/graph/query/cypher-tokenize.d.ts +13 -0
- package/lib/graph/query/cypher-tokenize.js +225 -0
- package/lib/graph/query/cypher.types.d.ts +43 -0
- package/lib/graph/query/execute.d.ts +7 -7
- package/lib/graph/query/execute.js +71 -33
- package/lib/graph/query/inspect.js +58 -24
- package/lib/graph/query/parse-cypher-patterns.d.ts +27 -0
- package/lib/graph/query/parse-cypher-patterns.js +382 -0
- package/lib/graph/query/parse-cypher.d.ts +7 -0
- package/lib/graph/query/parse-cypher.js +580 -0
- package/lib/graph/query/parse-query.d.ts +13 -0
- package/lib/graph/query/parse-query.js +97 -0
- package/lib/graph/query/resolve.js +77 -23
- package/lib/output/command-output.js +12 -5
- package/lib/output/compact-layout.js +221 -0
- package/lib/output/format-output-item-block.js +31 -1
- package/lib/output/format-output-metadata.js +16 -29
- package/lib/output/format-stored-query-block.js +95 -0
- package/lib/output/layout-incoming-references.js +101 -19
- package/lib/output/layout-stored-queries.js +23 -330
- package/lib/output/list-queries.js +1 -1
- package/lib/output/render-field-discovery.js +11 -2
- package/lib/output/render-output-view.js +9 -5
- package/lib/output/renderers/json.js +5 -26
- package/lib/output/renderers/plain.js +155 -35
- package/lib/output/renderers/rich.js +250 -36
- package/lib/output/resolved-link-layout.js +43 -0
- package/lib/output/rich-source/render.js +193 -35
- package/lib/output/show-document.js +25 -18
- package/lib/output/view-model/index.js +124 -103
- package/lib/parse/jsdoc/parse-jsdoc-blocks.js +1 -1
- package/lib/parse/jsdoc/parse-jsdoc-claims.js +12 -6
- package/lib/parse/markdown/parse-markdown-claims.js +99 -62
- package/lib/parse/markdown/parse-markdown-directives.d.ts +10 -6
- package/lib/parse/markdown/parse-markdown-directives.js +104 -18
- package/lib/parse/markdown/parse-markdown-prose.d.ts +27 -0
- package/lib/parse/markdown/parse-markdown-prose.js +243 -0
- package/lib/parse/parse-claims.d.ts +2 -6
- package/lib/parse/parse-claims.js +11 -53
- package/lib/parse/tagged-fenced/tagged-fenced-blocks.d.ts +4 -4
- package/lib/parse/tagged-fenced/tagged-fenced-blocks.js +4 -4
- package/lib/parse/yaml/parse-yaml-claims.js +4 -4
- package/lib/patram.d.ts +3 -5
- package/lib/patram.js +1 -1
- package/lib/scan/discover-fields.js +194 -55
- package/lib/scan/list-source-files.d.ts +4 -4
- package/lib/scan/list-source-files.js +4 -4
- package/package.json +1 -1
- package/lib/directive-validation-test-helpers.js +0 -87
- package/lib/graph/query/parse.d.ts +0 -75
- package/lib/graph/query/parse.js +0 -1064
- package/lib/output/derived-summary.js +0 -280
- package/lib/output/format-derived-summary-row.js +0 -9
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
/**
|
|
3
|
+
* @import {
|
|
4
|
+
* ParsedAggregateComparison,
|
|
5
|
+
* ParsedAggregateTerm,
|
|
6
|
+
* ParsedExpression,
|
|
7
|
+
* ParsedFieldTerm,
|
|
8
|
+
* ParseWhereClauseResult,
|
|
9
|
+
* } from '../parse-where-clause.types.ts';
|
|
10
|
+
* @import { CypherExpressionResult, CypherParserState, CypherToken } from './cypher.types.ts';
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
collapseAndExpressions,
|
|
15
|
+
collapseBooleanExpression,
|
|
16
|
+
createFieldExpression,
|
|
17
|
+
createFieldSetExpression,
|
|
18
|
+
createNodeLabelExpression,
|
|
19
|
+
isAggregateComparison,
|
|
20
|
+
} from './cypher-support.js';
|
|
21
|
+
import {
|
|
22
|
+
consumeKeyword,
|
|
23
|
+
consumeSymbol,
|
|
24
|
+
consumeToken,
|
|
25
|
+
expectKeyword,
|
|
26
|
+
expectSymbol,
|
|
27
|
+
failAtCurrent,
|
|
28
|
+
peekKeyword,
|
|
29
|
+
peekToken,
|
|
30
|
+
} from './cypher-reader.js';
|
|
31
|
+
import {
|
|
32
|
+
parseCypherListValue,
|
|
33
|
+
parseCypherScalarValue,
|
|
34
|
+
parseNodePattern,
|
|
35
|
+
parseSubqueryAggregate,
|
|
36
|
+
} from './parse-cypher-patterns.js';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {CypherParserState} parser_state
|
|
40
|
+
* @returns {ParseWhereClauseResult}
|
|
41
|
+
*/
|
|
42
|
+
export function parseCypherExpression(parser_state) {
|
|
43
|
+
expectKeyword(parser_state, 'MATCH');
|
|
44
|
+
const root_node = parseNodePattern(parser_state);
|
|
45
|
+
|
|
46
|
+
if (!root_node.variable_name) {
|
|
47
|
+
return failAtCurrent(
|
|
48
|
+
parser_state,
|
|
49
|
+
'Cypher root MATCH requires a variable.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
parser_state.root_variable_name = root_node.variable_name;
|
|
54
|
+
|
|
55
|
+
/** @type {ParsedExpression[]} */
|
|
56
|
+
const expressions = [];
|
|
57
|
+
const label_expression = createNodeLabelExpression(
|
|
58
|
+
root_node,
|
|
59
|
+
parser_state.repo_config,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (label_expression) {
|
|
63
|
+
expressions.push(label_expression);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (consumeKeyword(parser_state, 'WHERE')) {
|
|
67
|
+
const where_result = parseCypherBooleanExpression(parser_state);
|
|
68
|
+
|
|
69
|
+
if (!where_result.success) {
|
|
70
|
+
return where_result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
expressions.push(where_result.expression);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
expectKeyword(parser_state, 'RETURN');
|
|
77
|
+
return resolveReturnExpression(
|
|
78
|
+
parser_state,
|
|
79
|
+
root_node.variable_name,
|
|
80
|
+
expressions,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {CypherParserState} parser_state
|
|
86
|
+
* @returns {CypherExpressionResult}
|
|
87
|
+
*/
|
|
88
|
+
function parseCypherBooleanExpression(parser_state) {
|
|
89
|
+
return parseCypherOrExpression(parser_state);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {CypherParserState} parser_state
|
|
94
|
+
* @returns {CypherExpressionResult}
|
|
95
|
+
*/
|
|
96
|
+
function parseCypherOrExpression(parser_state) {
|
|
97
|
+
return parseBooleanExpression(
|
|
98
|
+
parser_state,
|
|
99
|
+
'OR',
|
|
100
|
+
'or',
|
|
101
|
+
parseCypherAndExpression,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {CypherParserState} parser_state
|
|
107
|
+
* @returns {CypherExpressionResult}
|
|
108
|
+
*/
|
|
109
|
+
function parseCypherAndExpression(parser_state) {
|
|
110
|
+
return parseBooleanExpression(
|
|
111
|
+
parser_state,
|
|
112
|
+
'AND',
|
|
113
|
+
'and',
|
|
114
|
+
parseCypherUnaryExpression,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {CypherParserState} parser_state
|
|
120
|
+
* @returns {CypherExpressionResult}
|
|
121
|
+
*/
|
|
122
|
+
function parseCypherUnaryExpression(parser_state) {
|
|
123
|
+
if (!consumeKeyword(parser_state, 'NOT')) {
|
|
124
|
+
return parseCypherPrimaryExpression(parser_state);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const expression_result = parseCypherUnaryExpression(parser_state);
|
|
128
|
+
|
|
129
|
+
if (!expression_result.success) {
|
|
130
|
+
return expression_result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
expression: {
|
|
135
|
+
expression: expression_result.expression,
|
|
136
|
+
kind: 'not',
|
|
137
|
+
},
|
|
138
|
+
success: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {CypherParserState} parser_state
|
|
144
|
+
* @returns {CypherExpressionResult}
|
|
145
|
+
*/
|
|
146
|
+
function parseCypherPrimaryExpression(parser_state) {
|
|
147
|
+
if (consumeSymbol(parser_state, '(')) {
|
|
148
|
+
return parseGroupedExpression(parser_state);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (peekKeyword(parser_state, 'EXISTS')) {
|
|
152
|
+
return parseExistsSubquery(parser_state);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (peekKeyword(parser_state, 'COUNT')) {
|
|
156
|
+
return parseCountSubquery(parser_state);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return parsePropertyPredicate(parser_state);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {CypherParserState} parser_state
|
|
164
|
+
* @returns {CypherExpressionResult}
|
|
165
|
+
*/
|
|
166
|
+
function parseExistsSubquery(parser_state) {
|
|
167
|
+
const exists_token = consumeToken(parser_state);
|
|
168
|
+
|
|
169
|
+
if (!exists_token) {
|
|
170
|
+
return failAtCurrent(parser_state, 'Expected EXISTS.');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const aggregate_result = parseSubqueryAggregate(
|
|
174
|
+
parser_state,
|
|
175
|
+
exists_token.column,
|
|
176
|
+
'any',
|
|
177
|
+
parseCypherBooleanExpression,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (!aggregate_result.success) {
|
|
181
|
+
return aggregate_result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return createAggregateExpression(aggregate_result.term);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {CypherParserState} parser_state
|
|
189
|
+
* @returns {CypherExpressionResult}
|
|
190
|
+
*/
|
|
191
|
+
function parseCountSubquery(parser_state) {
|
|
192
|
+
const count_token = consumeToken(parser_state);
|
|
193
|
+
|
|
194
|
+
if (!count_token) {
|
|
195
|
+
return failAtCurrent(parser_state, 'Expected COUNT.');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const aggregate_result = parseSubqueryAggregate(
|
|
199
|
+
parser_state,
|
|
200
|
+
count_token.column,
|
|
201
|
+
'count',
|
|
202
|
+
parseCypherBooleanExpression,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (!aggregate_result.success) {
|
|
206
|
+
return aggregate_result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const comparison_result = parseCountComparison(parser_state);
|
|
210
|
+
|
|
211
|
+
if (!comparison_result.success) {
|
|
212
|
+
return comparison_result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
assignCountComparison(aggregate_result.term, comparison_result);
|
|
216
|
+
return createAggregateExpression(aggregate_result.term);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @param {CypherParserState} parser_state
|
|
221
|
+
* @returns {CypherExpressionResult}
|
|
222
|
+
*/
|
|
223
|
+
function parsePropertyPredicate(parser_state) {
|
|
224
|
+
const field_reference = parsePredicateLeftHandSide(parser_state);
|
|
225
|
+
|
|
226
|
+
if (!field_reference.success) {
|
|
227
|
+
return field_reference;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const operator_result = parsePredicateOperator(parser_state);
|
|
231
|
+
|
|
232
|
+
if (!operator_result.success) {
|
|
233
|
+
return operator_result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (operator_result.kind === 'scalar') {
|
|
237
|
+
return createFieldExpression(
|
|
238
|
+
field_reference.field_name,
|
|
239
|
+
operator_result.operator,
|
|
240
|
+
parseCypherScalarValue(parser_state),
|
|
241
|
+
field_reference.column,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return createFieldSetExpression(
|
|
246
|
+
field_reference.field_name,
|
|
247
|
+
operator_result.operator,
|
|
248
|
+
parseCypherListValue(parser_state),
|
|
249
|
+
field_reference.column,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {CypherParserState} parser_state
|
|
255
|
+
* @returns {{ success: true, column: number, field_name: string } | ReturnType<typeof failAtCurrent>}
|
|
256
|
+
*/
|
|
257
|
+
function parsePredicateLeftHandSide(parser_state) {
|
|
258
|
+
const identifier_token = consumeToken(parser_state);
|
|
259
|
+
|
|
260
|
+
if (!identifier_token || identifier_token.kind !== 'identifier') {
|
|
261
|
+
return failAtCurrent(parser_state, 'Expected a Cypher predicate.');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (peekToken(parser_state)?.value === '(') {
|
|
265
|
+
return parseStructuralFunctionPredicate(parser_state, identifier_token);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (peekToken(parser_state)?.value !== '.') {
|
|
269
|
+
return failAtCurrent(parser_state, 'Expected a Cypher predicate.');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
expectSymbol(parser_state, '.');
|
|
273
|
+
const field_token = consumeToken(parser_state);
|
|
274
|
+
|
|
275
|
+
if (!field_token || field_token.kind !== 'identifier') {
|
|
276
|
+
return failAtCurrent(parser_state, 'Expected a property name.');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
column: field_token.column,
|
|
281
|
+
field_name: field_token.value,
|
|
282
|
+
success: true,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @param {CypherParserState} parser_state
|
|
288
|
+
* @param {CypherToken} identifier_token
|
|
289
|
+
* @returns {{ success: true, column: number, field_name: string } | ReturnType<typeof failAtCurrent>}
|
|
290
|
+
*/
|
|
291
|
+
function parseStructuralFunctionPredicate(parser_state, identifier_token) {
|
|
292
|
+
const field_name = resolveStructuralFunctionFieldName(identifier_token.value);
|
|
293
|
+
|
|
294
|
+
if (!field_name) {
|
|
295
|
+
return failAtCurrent(parser_state, 'Expected a Cypher predicate.');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
expectSymbol(parser_state, '(');
|
|
299
|
+
|
|
300
|
+
const variable_token = consumeToken(parser_state);
|
|
301
|
+
|
|
302
|
+
if (!variable_token || variable_token.kind !== 'identifier') {
|
|
303
|
+
return failAtCurrent(parser_state, 'Expected a variable name.');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
expectSymbol(parser_state, ')');
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
column: identifier_token.column,
|
|
310
|
+
field_name,
|
|
311
|
+
success: true,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @param {CypherParserState} parser_state
|
|
317
|
+
* @param {string} keyword
|
|
318
|
+
* @param {'and' | 'or'} kind
|
|
319
|
+
* @param {(parser_state: CypherParserState) => CypherExpressionResult} parse_next_expression
|
|
320
|
+
* @returns {CypherExpressionResult}
|
|
321
|
+
*/
|
|
322
|
+
function parseBooleanExpression(
|
|
323
|
+
parser_state,
|
|
324
|
+
keyword,
|
|
325
|
+
kind,
|
|
326
|
+
parse_next_expression,
|
|
327
|
+
) {
|
|
328
|
+
const first_result = parse_next_expression(parser_state);
|
|
329
|
+
|
|
330
|
+
if (!first_result.success) {
|
|
331
|
+
return first_result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** @type {ParsedExpression[]} */
|
|
335
|
+
const expressions = [first_result.expression];
|
|
336
|
+
|
|
337
|
+
while (consumeKeyword(parser_state, keyword)) {
|
|
338
|
+
const next_result = parse_next_expression(parser_state);
|
|
339
|
+
|
|
340
|
+
if (!next_result.success) {
|
|
341
|
+
return next_result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
expressions.push(next_result.expression);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
expression: collapseBooleanExpression(kind, expressions),
|
|
349
|
+
success: true,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* @param {CypherParserState} parser_state
|
|
355
|
+
* @returns {CypherExpressionResult}
|
|
356
|
+
*/
|
|
357
|
+
function parseGroupedExpression(parser_state) {
|
|
358
|
+
const expression_result = parseCypherBooleanExpression(parser_state);
|
|
359
|
+
|
|
360
|
+
if (!expression_result.success) {
|
|
361
|
+
return expression_result;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
expectSymbol(parser_state, ')');
|
|
365
|
+
return expression_result;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* @param {CypherParserState} parser_state
|
|
370
|
+
* @returns {{ success: true, comparison: ParsedAggregateComparison, value: number } | ReturnType<typeof failAtCurrent>}
|
|
371
|
+
*/
|
|
372
|
+
function parseCountComparison(parser_state) {
|
|
373
|
+
const comparison = resolveAggregateComparison(consumeToken(parser_state));
|
|
374
|
+
|
|
375
|
+
if (!comparison) {
|
|
376
|
+
return failAtCurrent(parser_state, 'Expected a count comparison operator.');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const value_token = consumeToken(parser_state);
|
|
380
|
+
|
|
381
|
+
if (!value_token || value_token.kind !== 'number') {
|
|
382
|
+
return failAtCurrent(parser_state, 'Expected a count comparison value.');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
comparison,
|
|
387
|
+
success: true,
|
|
388
|
+
value: Number.parseInt(value_token.value, 10),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* @param {CypherParserState} parser_state
|
|
394
|
+
* @returns {{
|
|
395
|
+
* success: true,
|
|
396
|
+
* kind: 'scalar',
|
|
397
|
+
* operator: ParsedFieldTerm['operator'],
|
|
398
|
+
* } | {
|
|
399
|
+
* success: true,
|
|
400
|
+
* kind: 'set',
|
|
401
|
+
* operator: 'in' | 'not in',
|
|
402
|
+
* } | ReturnType<typeof failAtCurrent>}
|
|
403
|
+
*/
|
|
404
|
+
function parsePredicateOperator(parser_state) {
|
|
405
|
+
if (consumeKeyword(parser_state, 'STARTS')) {
|
|
406
|
+
expectKeyword(parser_state, 'WITH');
|
|
407
|
+
return {
|
|
408
|
+
kind: 'scalar',
|
|
409
|
+
operator: '^=',
|
|
410
|
+
success: true,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (consumeKeyword(parser_state, 'ENDS')) {
|
|
415
|
+
expectKeyword(parser_state, 'WITH');
|
|
416
|
+
return {
|
|
417
|
+
kind: 'scalar',
|
|
418
|
+
operator: '$=',
|
|
419
|
+
success: true,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (consumeKeyword(parser_state, 'CONTAINS')) {
|
|
424
|
+
return {
|
|
425
|
+
kind: 'scalar',
|
|
426
|
+
operator: '~',
|
|
427
|
+
success: true,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (consumeKeyword(parser_state, 'NOT')) {
|
|
432
|
+
expectKeyword(parser_state, 'IN');
|
|
433
|
+
return {
|
|
434
|
+
kind: 'set',
|
|
435
|
+
operator: 'not in',
|
|
436
|
+
success: true,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (consumeKeyword(parser_state, 'IN')) {
|
|
441
|
+
return {
|
|
442
|
+
kind: 'set',
|
|
443
|
+
operator: 'in',
|
|
444
|
+
success: true,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const operator = resolveFieldComparisonOperator(peekToken(parser_state));
|
|
449
|
+
|
|
450
|
+
if (!operator) {
|
|
451
|
+
return failAtCurrent(parser_state, 'Expected a Cypher predicate operator.');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
consumeToken(parser_state);
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
kind: 'scalar',
|
|
458
|
+
operator,
|
|
459
|
+
success: true,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* @param {CypherParserState} parser_state
|
|
465
|
+
* @param {string} root_variable_name
|
|
466
|
+
* @param {ParsedExpression[]} expressions
|
|
467
|
+
* @returns {ParseWhereClauseResult}
|
|
468
|
+
*/
|
|
469
|
+
function resolveReturnExpression(
|
|
470
|
+
parser_state,
|
|
471
|
+
root_variable_name,
|
|
472
|
+
expressions,
|
|
473
|
+
) {
|
|
474
|
+
const return_token = consumeToken(parser_state);
|
|
475
|
+
|
|
476
|
+
if (!return_token || return_token.kind !== 'identifier') {
|
|
477
|
+
return failAtCurrent(parser_state, 'Expected a return variable.');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (return_token.value !== root_variable_name) {
|
|
481
|
+
return failAtCurrent(
|
|
482
|
+
parser_state,
|
|
483
|
+
'Cypher RETURN must return the root MATCH variable.',
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
expression: collapseAndExpressions(expressions),
|
|
489
|
+
success: true,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* @param {ParsedAggregateTerm} aggregate_term
|
|
495
|
+
* @param {{ success: true, comparison: ParsedAggregateComparison, value: number }} comparison_result
|
|
496
|
+
*/
|
|
497
|
+
function assignCountComparison(aggregate_term, comparison_result) {
|
|
498
|
+
aggregate_term.comparison = comparison_result.comparison;
|
|
499
|
+
aggregate_term.value = comparison_result.value;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* @param {ParsedAggregateTerm} aggregate_term
|
|
504
|
+
* @returns {CypherExpressionResult}
|
|
505
|
+
*/
|
|
506
|
+
function createAggregateExpression(aggregate_term) {
|
|
507
|
+
return {
|
|
508
|
+
expression: {
|
|
509
|
+
kind: 'term',
|
|
510
|
+
term: aggregate_term,
|
|
511
|
+
},
|
|
512
|
+
success: true,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* @param {CypherToken | undefined} token
|
|
518
|
+
* @returns {ParsedAggregateComparison | undefined}
|
|
519
|
+
*/
|
|
520
|
+
function resolveAggregateComparison(token) {
|
|
521
|
+
const operator = resolveComparisonOperator(token);
|
|
522
|
+
|
|
523
|
+
if (!operator || !isAggregateComparison(operator)) {
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return operator;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* @param {CypherToken | undefined} token
|
|
532
|
+
* @returns {ParsedFieldTerm['operator'] | undefined}
|
|
533
|
+
*/
|
|
534
|
+
function resolveFieldComparisonOperator(token) {
|
|
535
|
+
const operator = resolveComparisonOperator(token);
|
|
536
|
+
|
|
537
|
+
if (!operator) {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return /** @type {ParsedFieldTerm['operator']} */ (operator);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* @param {string} function_name
|
|
546
|
+
* @returns {'$id' | '$path' | undefined}
|
|
547
|
+
*/
|
|
548
|
+
function resolveStructuralFunctionFieldName(function_name) {
|
|
549
|
+
if (function_name === 'id') {
|
|
550
|
+
return '$id';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (function_name === 'path') {
|
|
554
|
+
return '$path';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* @param {CypherToken | undefined} token
|
|
562
|
+
* @returns {string | undefined}
|
|
563
|
+
*/
|
|
564
|
+
function resolveComparisonOperator(token) {
|
|
565
|
+
if (!token || token.kind !== 'symbol') {
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
switch (token.value) {
|
|
570
|
+
case '<':
|
|
571
|
+
case '<=':
|
|
572
|
+
case '<>':
|
|
573
|
+
case '=':
|
|
574
|
+
case '>':
|
|
575
|
+
case '>=':
|
|
576
|
+
return token.value;
|
|
577
|
+
default:
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse one query string as the constrained Cypher subset.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} query_text
|
|
5
|
+
* @param {PatramRepoConfig | null} repo_config
|
|
6
|
+
* @param {{ bindings?: Record<string, string> }=} options
|
|
7
|
+
* @returns {ParseWhereClauseResult}
|
|
8
|
+
*/
|
|
9
|
+
export function parseQueryExpression(query_text: string, repo_config: PatramRepoConfig | null, options?: {
|
|
10
|
+
bindings?: Record<string, string>;
|
|
11
|
+
} | undefined): ParseWhereClauseResult;
|
|
12
|
+
import type { PatramRepoConfig } from '../../config/load-patram-config.types.d.ts';
|
|
13
|
+
import type { ParseWhereClauseResult } from '../parse-where-clause.types.d.ts';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { PatramRepoConfig } from '../../config/load-patram-config.types.ts';
|
|
3
|
+
* @import { ParseWhereClauseResult } from '../parse-where-clause.types.ts';
|
|
4
|
+
* @import { CypherParserState } from './cypher.types.ts';
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { tokenizeCypher } from './cypher-tokenize.js';
|
|
8
|
+
import { parseCypherExpression } from './parse-cypher.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse one query string as the constrained Cypher subset.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} query_text
|
|
14
|
+
* @param {PatramRepoConfig | null} repo_config
|
|
15
|
+
* @param {{ bindings?: Record<string, string> }=} options
|
|
16
|
+
* @returns {ParseWhereClauseResult}
|
|
17
|
+
*/
|
|
18
|
+
export function parseQueryExpression(query_text, repo_config, options = {}) {
|
|
19
|
+
const tokenize_result = tokenizeCypher(query_text);
|
|
20
|
+
|
|
21
|
+
if (!tokenize_result.success) {
|
|
22
|
+
return tokenize_result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @type {CypherParserState} */
|
|
26
|
+
const parser_state = {
|
|
27
|
+
bindings: options.bindings ?? {},
|
|
28
|
+
index: 0,
|
|
29
|
+
query_text,
|
|
30
|
+
repo_config,
|
|
31
|
+
root_variable_name: null,
|
|
32
|
+
tokens: tokenize_result.tokens,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const expression_result = parseCypherExpression(parser_state);
|
|
37
|
+
|
|
38
|
+
if (!expression_result.success) {
|
|
39
|
+
return expression_result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return resolveTrailingTokens(parser_state, expression_result);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return resolveCypherSyntaxError(query_text, parser_state, error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {CypherParserState} parser_state
|
|
50
|
+
* @param {ParseWhereClauseResult & { success: true }} expression_result
|
|
51
|
+
* @returns {ParseWhereClauseResult}
|
|
52
|
+
*/
|
|
53
|
+
function resolveTrailingTokens(parser_state, expression_result) {
|
|
54
|
+
if (parser_state.index >= parser_state.tokens.length) {
|
|
55
|
+
return expression_result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const token = parser_state.tokens[parser_state.index];
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
diagnostic: {
|
|
62
|
+
code: 'query.invalid',
|
|
63
|
+
column: token.column,
|
|
64
|
+
level: 'error',
|
|
65
|
+
line: 1,
|
|
66
|
+
message: `Unsupported query token "${token.value}".`,
|
|
67
|
+
path: '<query>',
|
|
68
|
+
},
|
|
69
|
+
success: false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} query_text
|
|
75
|
+
* @param {CypherParserState} parser_state
|
|
76
|
+
* @param {unknown} error
|
|
77
|
+
* @returns {ParseWhereClauseResult}
|
|
78
|
+
*/
|
|
79
|
+
function resolveCypherSyntaxError(query_text, parser_state, error) {
|
|
80
|
+
if (!(error instanceof Error)) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
diagnostic: {
|
|
86
|
+
code: 'query.invalid',
|
|
87
|
+
column:
|
|
88
|
+
parser_state.tokens[parser_state.index]?.column ??
|
|
89
|
+
query_text.length + 1,
|
|
90
|
+
level: 'error',
|
|
91
|
+
line: 1,
|
|
92
|
+
message: error.message,
|
|
93
|
+
path: '<query>',
|
|
94
|
+
},
|
|
95
|
+
success: false,
|
|
96
|
+
};
|
|
97
|
+
}
|