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