@gblikas/querykit 0.1.0 → 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/.cursor/BUGBOT.md +65 -2
- package/README.md +163 -1
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.js +1 -0
- package/dist/parser/input-parser.d.ts +215 -0
- package/dist/parser/input-parser.js +493 -0
- package/dist/parser/parser.d.ts +148 -1
- package/dist/parser/parser.js +880 -6
- package/dist/parser/types.d.ts +432 -0
- package/examples/qk-next/app/page.tsx +6 -1
- package/package.json +1 -1
- package/src/parser/divergence.test.ts +357 -0
- package/src/parser/index.ts +2 -1
- package/src/parser/input-parser.test.ts +770 -0
- package/src/parser/input-parser.ts +697 -0
- package/src/parser/parse-with-context-suggestions.test.ts +360 -0
- package/src/parser/parse-with-context-validation.test.ts +447 -0
- package/src/parser/parse-with-context.test.ts +325 -0
- package/src/parser/parser.test.ts +209 -1
- package/src/parser/parser.ts +1106 -25
- package/src/parser/token-consistency.test.ts +341 -0
- package/src/parser/types.ts +545 -23
- package/examples/qk-next/pnpm-lock.yaml +0 -5623
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests documenting the differences between the input parser and main parser.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: The input parser is designed for UI/UX purposes (autocomplete, highlighting)
|
|
5
|
+
* and uses simplified regex-based tokenization. The main parser uses Liqe's full grammar.
|
|
6
|
+
*
|
|
7
|
+
* These tests document known divergences so developers understand the differences.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { QueryParser } from './parser';
|
|
11
|
+
import {
|
|
12
|
+
parseQueryInput,
|
|
13
|
+
extractKeyValue,
|
|
14
|
+
isInputComplete
|
|
15
|
+
} from './input-parser';
|
|
16
|
+
|
|
17
|
+
describe('Parser Divergence Documentation', () => {
|
|
18
|
+
const mainParser = new QueryParser();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Helper to check if main parser accepts input
|
|
22
|
+
*/
|
|
23
|
+
function mainParserAccepts(input: string): boolean {
|
|
24
|
+
try {
|
|
25
|
+
mainParser.parse(input);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('Known Divergences - Logical operators as values', () => {
|
|
33
|
+
/**
|
|
34
|
+
* DIVERGENCE: Liqe treats AND/OR/NOT as reserved keywords.
|
|
35
|
+
* The main parser REJECTS these as values.
|
|
36
|
+
* The input parser correctly handles them as literal values.
|
|
37
|
+
*/
|
|
38
|
+
it('DIVERGENCE: "status:AND" - main parser rejects, input parser accepts', () => {
|
|
39
|
+
const input = 'status:AND';
|
|
40
|
+
|
|
41
|
+
// Main parser (Liqe) treats AND as a keyword and fails
|
|
42
|
+
expect(mainParserAccepts(input)).toBe(false);
|
|
43
|
+
|
|
44
|
+
// Input parser correctly handles it as a value
|
|
45
|
+
const inputResult = parseQueryInput(input);
|
|
46
|
+
expect(inputResult.terms[0]).toMatchObject({
|
|
47
|
+
key: 'status',
|
|
48
|
+
operator: ':',
|
|
49
|
+
value: 'AND'
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// extractKeyValue also works
|
|
53
|
+
expect(extractKeyValue(input)).toEqual({ key: 'status', value: 'AND' });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('DIVERGENCE: "status:OR" - main parser rejects, input parser accepts', () => {
|
|
57
|
+
const input = 'status:OR';
|
|
58
|
+
|
|
59
|
+
// Main parser rejects
|
|
60
|
+
expect(mainParserAccepts(input)).toBe(false);
|
|
61
|
+
|
|
62
|
+
// Input parser handles it
|
|
63
|
+
const inputResult = parseQueryInput(input);
|
|
64
|
+
expect(inputResult.terms[0]).toMatchObject({
|
|
65
|
+
key: 'status',
|
|
66
|
+
value: 'OR'
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('DIVERGENCE: "status:NOT" - main parser rejects, input parser accepts', () => {
|
|
71
|
+
const input = 'status:NOT';
|
|
72
|
+
|
|
73
|
+
expect(mainParserAccepts(input)).toBe(false);
|
|
74
|
+
|
|
75
|
+
const inputResult = parseQueryInput(input);
|
|
76
|
+
expect(inputResult.terms[0]).toMatchObject({
|
|
77
|
+
key: 'status',
|
|
78
|
+
value: 'NOT'
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('WORKAROUND: Quote the value to make main parser accept it', () => {
|
|
83
|
+
const input = 'status:"AND"';
|
|
84
|
+
|
|
85
|
+
// Both parsers handle quoted values
|
|
86
|
+
expect(mainParserAccepts(input)).toBe(true);
|
|
87
|
+
|
|
88
|
+
const inputResult = parseQueryInput(input);
|
|
89
|
+
expect(inputResult.terms[0]).toMatchObject({
|
|
90
|
+
key: 'status',
|
|
91
|
+
value: '"AND"'
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('Known Divergences - Incomplete input', () => {
|
|
97
|
+
/**
|
|
98
|
+
* The main advantage of the input parser: handling incomplete input
|
|
99
|
+
* that users are still typing.
|
|
100
|
+
*/
|
|
101
|
+
it('DIVERGENCE: "status:" - main parser rejects, input parser returns partial', () => {
|
|
102
|
+
const input = 'status:';
|
|
103
|
+
|
|
104
|
+
// Main parser requires a value
|
|
105
|
+
expect(mainParserAccepts(input)).toBe(false);
|
|
106
|
+
|
|
107
|
+
// Input parser returns what it can
|
|
108
|
+
const inputResult = parseQueryInput(input);
|
|
109
|
+
expect(inputResult.terms[0]).toMatchObject({
|
|
110
|
+
key: 'status',
|
|
111
|
+
operator: ':',
|
|
112
|
+
value: null
|
|
113
|
+
});
|
|
114
|
+
expect(inputResult.cursorContext).toBe('value');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('DIVERGENCE: "status:done AND" - main parser rejects trailing operator', () => {
|
|
118
|
+
const input = 'status:done AND';
|
|
119
|
+
|
|
120
|
+
// Main parser rejects incomplete expression
|
|
121
|
+
expect(mainParserAccepts(input)).toBe(false);
|
|
122
|
+
|
|
123
|
+
// Input parser parses what it can
|
|
124
|
+
const inputResult = parseQueryInput(input);
|
|
125
|
+
expect(inputResult.terms[0]).toMatchObject({
|
|
126
|
+
key: 'status',
|
|
127
|
+
value: 'done'
|
|
128
|
+
});
|
|
129
|
+
expect(inputResult.logicalOperators).toContainEqual(
|
|
130
|
+
expect.objectContaining({ operator: 'AND' })
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// isInputComplete correctly identifies this as incomplete
|
|
134
|
+
expect(isInputComplete(input)).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('DIVERGENCE: unclosed quote - main parser rejects, input parser handles', () => {
|
|
138
|
+
const input = 'name:"John';
|
|
139
|
+
|
|
140
|
+
expect(mainParserAccepts(input)).toBe(false);
|
|
141
|
+
|
|
142
|
+
const inputResult = parseQueryInput(input);
|
|
143
|
+
expect(inputResult.terms[0]).toMatchObject({
|
|
144
|
+
key: 'name',
|
|
145
|
+
value: '"John' // Partial quoted value
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(isInputComplete(input)).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('Known Divergences - Range and Array syntax', () => {
|
|
153
|
+
/**
|
|
154
|
+
* Complex syntax like ranges and arrays are preprocessed by the main parser
|
|
155
|
+
* but not fully understood by the input parser.
|
|
156
|
+
*/
|
|
157
|
+
it('DIVERGENCE: "field:[1 TO 10]" - different handling', () => {
|
|
158
|
+
const input = 'field:[1 TO 10]';
|
|
159
|
+
|
|
160
|
+
// Main parser handles range syntax
|
|
161
|
+
expect(mainParserAccepts(input)).toBe(true);
|
|
162
|
+
const mainAst = mainParser.parse(input);
|
|
163
|
+
// Main parser converts to logical AND of comparisons
|
|
164
|
+
expect(mainAst.type).toBe('logical');
|
|
165
|
+
|
|
166
|
+
// Input parser sees partial bracket content
|
|
167
|
+
const inputResult = parseQueryInput(input);
|
|
168
|
+
// It doesn't fully understand range syntax
|
|
169
|
+
expect(inputResult.terms.length).toBeGreaterThanOrEqual(1);
|
|
170
|
+
expect(inputResult.terms[0].key).toBe('field');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('DIVERGENCE: "field:[a, b, c]" - main parser expands, input parser partial', () => {
|
|
174
|
+
const input = 'field:[a, b, c]';
|
|
175
|
+
|
|
176
|
+
// Main parser expands to OR expression
|
|
177
|
+
expect(mainParserAccepts(input)).toBe(true);
|
|
178
|
+
const mainAst = mainParser.parse(input);
|
|
179
|
+
expect(mainAst.type).toBe('logical');
|
|
180
|
+
|
|
181
|
+
// Input parser handles it partially
|
|
182
|
+
const inputResult = parseQueryInput(input);
|
|
183
|
+
expect(inputResult.terms[0].key).toBe('field');
|
|
184
|
+
// The value parsing for array syntax is limited
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('Consistent Behavior - Simple cases', () => {
|
|
189
|
+
/**
|
|
190
|
+
* For typical key:value patterns, both parsers agree.
|
|
191
|
+
*/
|
|
192
|
+
it('CONSISTENT: "status:done" - both parsers agree', () => {
|
|
193
|
+
const input = 'status:done';
|
|
194
|
+
|
|
195
|
+
expect(mainParserAccepts(input)).toBe(true);
|
|
196
|
+
const mainAst = mainParser.parse(input);
|
|
197
|
+
expect(mainAst).toMatchObject({
|
|
198
|
+
type: 'comparison',
|
|
199
|
+
field: 'status',
|
|
200
|
+
value: 'done'
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const inputResult = parseQueryInput(input);
|
|
204
|
+
expect(inputResult.terms[0]).toMatchObject({
|
|
205
|
+
key: 'status',
|
|
206
|
+
value: 'done'
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(extractKeyValue(input)).toEqual({ key: 'status', value: 'done' });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('CONSISTENT: "status:d" - partial value works in both', () => {
|
|
213
|
+
const input = 'status:d';
|
|
214
|
+
|
|
215
|
+
// Main parser accepts partial values
|
|
216
|
+
expect(mainParserAccepts(input)).toBe(true);
|
|
217
|
+
const mainAst = mainParser.parse(input);
|
|
218
|
+
expect(mainAst).toMatchObject({
|
|
219
|
+
type: 'comparison',
|
|
220
|
+
field: 'status',
|
|
221
|
+
value: 'd'
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Input parser matches
|
|
225
|
+
const inputResult = parseQueryInput(input);
|
|
226
|
+
expect(inputResult.terms[0]).toMatchObject({
|
|
227
|
+
key: 'status',
|
|
228
|
+
value: 'd'
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('CONSISTENT: "priority:>5" - comparison operators work in both', () => {
|
|
233
|
+
const input = 'priority:>5';
|
|
234
|
+
|
|
235
|
+
expect(mainParserAccepts(input)).toBe(true);
|
|
236
|
+
const mainAst = mainParser.parse(input);
|
|
237
|
+
expect(mainAst).toMatchObject({
|
|
238
|
+
type: 'comparison',
|
|
239
|
+
field: 'priority',
|
|
240
|
+
operator: '>',
|
|
241
|
+
value: 5
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const inputResult = parseQueryInput(input);
|
|
245
|
+
expect(inputResult.terms[0]).toMatchObject({
|
|
246
|
+
key: 'priority',
|
|
247
|
+
operator: ':>',
|
|
248
|
+
value: '5' // Note: input parser keeps as string (for display)
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('CONSISTENT: quoted values work in both (with quote handling difference)', () => {
|
|
253
|
+
const input = 'name:"John Doe"';
|
|
254
|
+
|
|
255
|
+
expect(mainParserAccepts(input)).toBe(true);
|
|
256
|
+
const mainAst = mainParser.parse(input);
|
|
257
|
+
expect(mainAst).toMatchObject({
|
|
258
|
+
type: 'comparison',
|
|
259
|
+
field: 'name',
|
|
260
|
+
value: 'John Doe' // Main parser STRIPS quotes
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const inputResult = parseQueryInput(input);
|
|
264
|
+
expect(inputResult.terms[0]).toMatchObject({
|
|
265
|
+
key: 'name',
|
|
266
|
+
value: '"John Doe"' // Input parser PRESERVES quotes (for highlighting)
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('CONSISTENT: multiple terms with AND', () => {
|
|
271
|
+
const input = 'status:done AND priority:high';
|
|
272
|
+
|
|
273
|
+
expect(mainParserAccepts(input)).toBe(true);
|
|
274
|
+
const mainAst = mainParser.parse(input);
|
|
275
|
+
expect(mainAst.type).toBe('logical');
|
|
276
|
+
|
|
277
|
+
const inputResult = parseQueryInput(input);
|
|
278
|
+
expect(inputResult.terms).toHaveLength(2);
|
|
279
|
+
expect(inputResult.logicalOperators).toHaveLength(1);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('Type Differences', () => {
|
|
284
|
+
/**
|
|
285
|
+
* The main parser converts values to their proper types.
|
|
286
|
+
* The input parser keeps everything as strings (for display purposes).
|
|
287
|
+
*/
|
|
288
|
+
it('TYPE DIFFERENCE: numeric values', () => {
|
|
289
|
+
const input = 'count:42';
|
|
290
|
+
|
|
291
|
+
const mainAst = mainParser.parse(input);
|
|
292
|
+
expect(mainAst).toMatchObject({
|
|
293
|
+
value: 42 // Number type
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const inputResult = parseQueryInput(input);
|
|
297
|
+
expect(inputResult.terms[0].value).toBe('42'); // String type
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('TYPE DIFFERENCE: boolean values', () => {
|
|
301
|
+
const input = 'active:true';
|
|
302
|
+
|
|
303
|
+
const mainAst = mainParser.parse(input);
|
|
304
|
+
expect(mainAst).toMatchObject({
|
|
305
|
+
value: true // Boolean type
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const inputResult = parseQueryInput(input);
|
|
309
|
+
expect(inputResult.terms[0].value).toBe('true'); // String type
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('Developer Guidance', () => {
|
|
314
|
+
/**
|
|
315
|
+
* Summary of when to use each parser and known limitations.
|
|
316
|
+
*/
|
|
317
|
+
it('documents usage guidance', () => {
|
|
318
|
+
/**
|
|
319
|
+
* USE THE MAIN PARSER (QueryParser) when:
|
|
320
|
+
* - Executing queries against a database
|
|
321
|
+
* - Validating complete query syntax
|
|
322
|
+
* - Need proper type conversion (string -> number/boolean)
|
|
323
|
+
* - Need the full AST for SQL/Drizzle translation
|
|
324
|
+
*
|
|
325
|
+
* USE THE INPUT PARSER (parseQueryInput) when:
|
|
326
|
+
* - Building autocomplete/suggestions UI
|
|
327
|
+
* - Highlighting key/value in search input as user types
|
|
328
|
+
* - Need position information for cursor-aware features
|
|
329
|
+
* - Handling incomplete input gracefully
|
|
330
|
+
* - Display purposes (values stay as strings)
|
|
331
|
+
*
|
|
332
|
+
* KNOWN DIVERGENCES:
|
|
333
|
+
* 1. Logical keywords as values:
|
|
334
|
+
* - Main parser REJECTS: status:AND, status:OR, status:NOT
|
|
335
|
+
* - Input parser ACCEPTS these as valid key:value pairs
|
|
336
|
+
* - Workaround: Quote the value: status:"AND"
|
|
337
|
+
*
|
|
338
|
+
* 2. Incomplete input:
|
|
339
|
+
* - Main parser REJECTS: status:, status:done AND
|
|
340
|
+
* - Input parser returns partial results
|
|
341
|
+
*
|
|
342
|
+
* 3. Quote handling:
|
|
343
|
+
* - Main parser STRIPS quotes from values
|
|
344
|
+
* - Input parser PRESERVES quotes (for display)
|
|
345
|
+
*
|
|
346
|
+
* 4. Type conversion:
|
|
347
|
+
* - Main parser converts to proper types (42 -> number)
|
|
348
|
+
* - Input parser keeps as strings ("42")
|
|
349
|
+
*
|
|
350
|
+
* 5. Complex syntax (ranges, arrays):
|
|
351
|
+
* - Main parser fully understands [1 TO 10], [a, b, c]
|
|
352
|
+
* - Input parser has limited support
|
|
353
|
+
*/
|
|
354
|
+
expect(true).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
package/src/parser/index.ts
CHANGED