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