@delma/fylo 1.1.1 → 2.0.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/.github/copilot-instructions.md +1 -1
- package/.github/prompts/release.prompt.md +4 -43
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/README.md +141 -62
- package/eslint.config.js +8 -4
- package/package.json +9 -7
- package/src/CLI +16 -14
- package/src/adapters/cipher.ts +12 -6
- package/src/adapters/redis.ts +193 -123
- package/src/adapters/s3.ts +6 -12
- package/src/core/collection.ts +5 -0
- package/src/core/directory.ts +120 -151
- package/src/core/extensions.ts +4 -2
- package/src/core/format.ts +390 -419
- package/src/core/parser.ts +167 -142
- package/src/core/query.ts +31 -26
- package/src/core/walker.ts +68 -61
- package/src/core/write-queue.ts +7 -4
- package/src/engines/s3-files.ts +888 -0
- package/src/engines/types.ts +21 -0
- package/src/index.ts +754 -378
- package/src/migrate-cli.ts +22 -0
- package/src/migrate.ts +74 -0
- package/src/types/bun-runtime.d.ts +73 -0
- package/src/types/fylo.d.ts +115 -27
- package/src/types/node-runtime.d.ts +61 -0
- package/src/types/query.d.ts +6 -2
- package/src/types/vendor-modules.d.ts +8 -7
- package/src/worker.ts +7 -1
- package/src/workers/write-worker.ts +25 -24
- package/tests/collection/truncate.test.js +35 -0
- package/tests/{data.ts → data.js} +8 -21
- package/tests/{index.ts → index.js} +4 -9
- package/tests/integration/aws-s3-files.canary.test.js +22 -0
- package/tests/integration/{create.test.ts → create.test.js} +13 -31
- package/tests/integration/delete.test.js +95 -0
- package/tests/integration/{edge-cases.test.ts → edge-cases.test.js} +50 -124
- package/tests/integration/{encryption.test.ts → encryption.test.js} +20 -65
- package/tests/integration/{export.test.ts → export.test.js} +8 -23
- package/tests/integration/{join-modes.test.ts → join-modes.test.js} +37 -104
- package/tests/integration/migration.test.js +38 -0
- package/tests/integration/nested.test.js +142 -0
- package/tests/integration/operators.test.js +122 -0
- package/tests/integration/{queue.test.ts → queue.test.js} +24 -40
- package/tests/integration/read.test.js +119 -0
- package/tests/integration/rollback.test.js +60 -0
- package/tests/integration/s3-files.test.js +108 -0
- package/tests/integration/update.test.js +99 -0
- package/tests/mocks/{cipher.ts → cipher.js} +11 -26
- package/tests/mocks/redis.js +123 -0
- package/tests/mocks/{s3.ts → s3.js} +24 -58
- package/tests/schemas/album.json +1 -1
- package/tests/schemas/comment.json +1 -1
- package/tests/schemas/photo.json +1 -1
- package/tests/schemas/post.json +1 -1
- package/tests/schemas/tip.json +1 -1
- package/tests/schemas/todo.json +1 -1
- package/tests/schemas/user.d.ts +12 -12
- package/tests/schemas/user.json +1 -1
- package/tsconfig.json +4 -2
- package/tsconfig.typecheck.json +31 -0
- package/.github/prompts/issue.prompt.md +0 -19
- package/.github/prompts/pr.prompt.md +0 -18
- package/.github/prompts/review-pr.prompt.md +0 -19
- package/.github/prompts/sync-main.prompt.md +0 -14
- package/tests/collection/truncate.test.ts +0 -56
- package/tests/integration/delete.test.ts +0 -147
- package/tests/integration/nested.test.ts +0 -212
- package/tests/integration/operators.test.ts +0 -167
- package/tests/integration/read.test.ts +0 -203
- package/tests/integration/rollback.test.ts +0 -105
- package/tests/integration/update.test.ts +0 -130
- package/tests/mocks/redis.ts +0 -169
package/src/core/parser.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
1
|
// Token types for SQL lexing
|
|
4
2
|
enum TokenType {
|
|
5
3
|
CREATE = 'CREATE',
|
|
@@ -78,16 +76,16 @@ class SQLLexer {
|
|
|
78
76
|
let result = ''
|
|
79
77
|
const quote = this.current
|
|
80
78
|
this.advance() // Skip opening quote
|
|
81
|
-
|
|
79
|
+
|
|
82
80
|
while (this.current && this.current !== quote) {
|
|
83
81
|
result += this.current
|
|
84
82
|
this.advance()
|
|
85
83
|
}
|
|
86
|
-
|
|
84
|
+
|
|
87
85
|
if (this.current === quote) {
|
|
88
86
|
this.advance() // Skip closing quote
|
|
89
87
|
}
|
|
90
|
-
|
|
88
|
+
|
|
91
89
|
return result
|
|
92
90
|
}
|
|
93
91
|
|
|
@@ -111,32 +109,32 @@ class SQLLexer {
|
|
|
111
109
|
|
|
112
110
|
private getKeywordType(word: string): TokenType {
|
|
113
111
|
const keywords: Record<string, TokenType> = {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
112
|
+
SELECT: TokenType.SELECT,
|
|
113
|
+
FROM: TokenType.FROM,
|
|
114
|
+
WHERE: TokenType.WHERE,
|
|
115
|
+
INSERT: TokenType.INSERT,
|
|
116
|
+
INTO: TokenType.INTO,
|
|
117
|
+
VALUES: TokenType.VALUES,
|
|
118
|
+
UPDATE: TokenType.UPDATE,
|
|
119
|
+
SET: TokenType.SET,
|
|
120
|
+
DELETE: TokenType.DELETE,
|
|
121
|
+
JOIN: TokenType.JOIN,
|
|
122
|
+
INNER: TokenType.INNER,
|
|
123
|
+
LEFT: TokenType.LEFT,
|
|
124
|
+
RIGHT: TokenType.RIGHT,
|
|
125
|
+
OUTER: TokenType.OUTER,
|
|
126
|
+
ON: TokenType.ON,
|
|
127
|
+
GROUP: TokenType.GROUP,
|
|
128
|
+
BY: TokenType.BY,
|
|
129
|
+
ORDER: TokenType.ORDER,
|
|
130
|
+
LIMIT: TokenType.LIMIT,
|
|
131
|
+
AS: TokenType.AS,
|
|
132
|
+
AND: TokenType.AND,
|
|
133
|
+
OR: TokenType.OR,
|
|
134
|
+
LIKE: TokenType.LIKE,
|
|
135
|
+
TRUE: TokenType.BOOLEAN,
|
|
136
|
+
FALSE: TokenType.BOOLEAN,
|
|
137
|
+
NULL: TokenType.NULL
|
|
140
138
|
}
|
|
141
139
|
return keywords[word.toUpperCase()] || TokenType.IDENTIFIER
|
|
142
140
|
}
|
|
@@ -146,7 +144,7 @@ class SQLLexer {
|
|
|
146
144
|
|
|
147
145
|
while (this.current) {
|
|
148
146
|
this.skipWhitespace()
|
|
149
|
-
|
|
147
|
+
|
|
150
148
|
if (!this.current) break
|
|
151
149
|
|
|
152
150
|
const position = this.position
|
|
@@ -169,7 +167,11 @@ class SQLLexer {
|
|
|
169
167
|
if (/[a-zA-Z_]/.test(this.current)) {
|
|
170
168
|
let value = this.readIdentifier()
|
|
171
169
|
// Support dot notation for nested fields (e.g. address.city → address/city)
|
|
172
|
-
while (
|
|
170
|
+
while (
|
|
171
|
+
this.current === '.' &&
|
|
172
|
+
this.position + 1 < this.input.length &&
|
|
173
|
+
/[a-zA-Z_]/.test(this.input[this.position + 1])
|
|
174
|
+
) {
|
|
173
175
|
this.advance() // skip '.'
|
|
174
176
|
value += '/' + this.readIdentifier()
|
|
175
177
|
}
|
|
@@ -257,7 +259,11 @@ class SQLParser {
|
|
|
257
259
|
|
|
258
260
|
private advance(): void {
|
|
259
261
|
this.position++
|
|
260
|
-
this.current = this.tokens[this.position] || {
|
|
262
|
+
this.current = this.tokens[this.position] || {
|
|
263
|
+
type: TokenType.EOF,
|
|
264
|
+
value: '',
|
|
265
|
+
position: -1
|
|
266
|
+
}
|
|
261
267
|
}
|
|
262
268
|
|
|
263
269
|
private expect(type: TokenType): Token {
|
|
@@ -312,7 +318,7 @@ class SQLParser {
|
|
|
312
318
|
this.advance()
|
|
313
319
|
return op ?? ''
|
|
314
320
|
}
|
|
315
|
-
|
|
321
|
+
|
|
316
322
|
throw new Error(`Unknown operator: ${this.current.type}`)
|
|
317
323
|
}
|
|
318
324
|
|
|
@@ -320,14 +326,14 @@ class SQLParser {
|
|
|
320
326
|
const column = this.expect(TokenType.IDENTIFIER).value
|
|
321
327
|
const operator = this.parseOperator()
|
|
322
328
|
const value = this.parseValue()
|
|
323
|
-
|
|
329
|
+
|
|
324
330
|
return { column, operator, value }
|
|
325
331
|
}
|
|
326
332
|
|
|
327
333
|
private parseWhereClause<T>(): Array<_op<T>> {
|
|
328
334
|
this.expect(TokenType.WHERE)
|
|
329
335
|
const conditions: Array<_op<T>> = []
|
|
330
|
-
|
|
336
|
+
|
|
331
337
|
do {
|
|
332
338
|
const condition = this.parseCondition()
|
|
333
339
|
const op: _op<T> = {
|
|
@@ -336,26 +342,26 @@ class SQLParser {
|
|
|
336
342
|
} as _operand
|
|
337
343
|
} as _op<T>
|
|
338
344
|
conditions.push(op)
|
|
339
|
-
|
|
345
|
+
|
|
340
346
|
if (this.match(TokenType.AND, TokenType.OR)) {
|
|
341
347
|
this.advance()
|
|
342
348
|
} else {
|
|
343
349
|
break
|
|
344
350
|
}
|
|
345
351
|
} while (true)
|
|
346
|
-
|
|
352
|
+
|
|
347
353
|
return conditions
|
|
348
354
|
}
|
|
349
355
|
|
|
350
356
|
private parseSelectClause(): string[] {
|
|
351
357
|
this.expect(TokenType.SELECT)
|
|
352
358
|
const columns: string[] = []
|
|
353
|
-
|
|
359
|
+
|
|
354
360
|
if (this.current.type === TokenType.ASTERISK) {
|
|
355
361
|
this.advance()
|
|
356
362
|
return ['*']
|
|
357
363
|
}
|
|
358
|
-
|
|
364
|
+
|
|
359
365
|
do {
|
|
360
366
|
columns.push(this.expect(TokenType.IDENTIFIER).value)
|
|
361
367
|
if (this.current.type === TokenType.COMMA) {
|
|
@@ -364,7 +370,7 @@ class SQLParser {
|
|
|
364
370
|
break
|
|
365
371
|
}
|
|
366
372
|
} while (true)
|
|
367
|
-
|
|
373
|
+
|
|
368
374
|
return columns
|
|
369
375
|
}
|
|
370
376
|
|
|
@@ -372,131 +378,139 @@ class SQLParser {
|
|
|
372
378
|
const select = this.parseSelectClause()
|
|
373
379
|
this.expect(TokenType.FROM)
|
|
374
380
|
const collection = this.expect(TokenType.IDENTIFIER).value
|
|
375
|
-
|
|
381
|
+
|
|
376
382
|
// Check if this is a JOIN query
|
|
377
|
-
if (
|
|
383
|
+
if (
|
|
384
|
+
this.match(
|
|
385
|
+
TokenType.JOIN,
|
|
386
|
+
TokenType.INNER,
|
|
387
|
+
TokenType.LEFT,
|
|
388
|
+
TokenType.RIGHT,
|
|
389
|
+
TokenType.OUTER
|
|
390
|
+
)
|
|
391
|
+
) {
|
|
378
392
|
return this.parseJoinQuery<T, any>(select, collection)
|
|
379
393
|
}
|
|
380
|
-
|
|
394
|
+
|
|
381
395
|
const query: _storeQuery<T> = {
|
|
382
396
|
$collection: collection,
|
|
383
|
-
$select: select.includes('*') ? undefined : select as Array<keyof T
|
|
397
|
+
$select: select.includes('*') ? undefined : (select as Array<keyof T>),
|
|
384
398
|
$onlyIds: select.includes('_id')
|
|
385
399
|
}
|
|
386
|
-
|
|
400
|
+
|
|
387
401
|
if (this.match(TokenType.WHERE)) {
|
|
388
402
|
query.$ops = this.parseWhereClause<T>()
|
|
389
403
|
}
|
|
390
|
-
|
|
404
|
+
|
|
391
405
|
if (this.match(TokenType.GROUP)) {
|
|
392
406
|
this.advance()
|
|
393
407
|
this.expect(TokenType.BY)
|
|
394
408
|
query.$groupby = this.expect(TokenType.IDENTIFIER).value as keyof T
|
|
395
409
|
}
|
|
396
|
-
|
|
410
|
+
|
|
397
411
|
if (this.match(TokenType.LIMIT)) {
|
|
398
412
|
this.advance()
|
|
399
413
|
query.$limit = parseInt(this.expect(TokenType.NUMBER).value)
|
|
400
414
|
}
|
|
401
|
-
|
|
415
|
+
|
|
402
416
|
return query
|
|
403
417
|
}
|
|
404
418
|
|
|
405
419
|
parseJoinQuery<T extends Record<string, any>, U extends Record<string, any>>(
|
|
406
|
-
select: string[],
|
|
420
|
+
select: string[],
|
|
407
421
|
leftCollection: string
|
|
408
422
|
): _join<T, U> {
|
|
409
423
|
// Parse join type
|
|
410
|
-
let joinMode:
|
|
411
|
-
|
|
424
|
+
let joinMode: 'inner' | 'left' | 'right' | 'outer' = 'inner'
|
|
425
|
+
|
|
412
426
|
if (this.match(TokenType.INNER)) {
|
|
413
427
|
this.advance()
|
|
414
|
-
joinMode =
|
|
428
|
+
joinMode = 'inner'
|
|
415
429
|
} else if (this.match(TokenType.LEFT)) {
|
|
416
430
|
this.advance()
|
|
417
|
-
joinMode =
|
|
431
|
+
joinMode = 'left'
|
|
418
432
|
} else if (this.match(TokenType.RIGHT)) {
|
|
419
433
|
this.advance()
|
|
420
|
-
joinMode =
|
|
434
|
+
joinMode = 'right'
|
|
421
435
|
} else if (this.match(TokenType.OUTER)) {
|
|
422
436
|
this.advance()
|
|
423
|
-
joinMode =
|
|
437
|
+
joinMode = 'outer'
|
|
424
438
|
}
|
|
425
|
-
|
|
439
|
+
|
|
426
440
|
this.expect(TokenType.JOIN)
|
|
427
441
|
const rightCollection = this.expect(TokenType.IDENTIFIER).value
|
|
428
442
|
this.expect(TokenType.ON)
|
|
429
|
-
|
|
443
|
+
|
|
430
444
|
// Parse join conditions
|
|
431
445
|
const onConditions = this.parseJoinConditions<T, U>()
|
|
432
|
-
|
|
446
|
+
|
|
433
447
|
const joinQuery: _join<T, U> = {
|
|
434
448
|
$leftCollection: leftCollection,
|
|
435
449
|
$rightCollection: rightCollection,
|
|
436
450
|
$mode: joinMode,
|
|
437
451
|
$on: onConditions,
|
|
438
|
-
$select: select.includes('*') ? undefined : select as Array<keyof T | keyof U>
|
|
452
|
+
$select: select.includes('*') ? undefined : (select as Array<keyof T | keyof U>)
|
|
439
453
|
}
|
|
440
|
-
|
|
454
|
+
|
|
441
455
|
// Parse additional clauses
|
|
442
456
|
if (this.match(TokenType.WHERE)) {
|
|
443
457
|
// For joins, WHERE conditions would need to be handled differently
|
|
444
458
|
// Skip for now as it's complex with joined tables
|
|
445
459
|
this.parseWhereClause<T>()
|
|
446
460
|
}
|
|
447
|
-
|
|
461
|
+
|
|
448
462
|
if (this.match(TokenType.GROUP)) {
|
|
449
463
|
this.advance()
|
|
450
464
|
this.expect(TokenType.BY)
|
|
451
465
|
joinQuery.$groupby = this.expect(TokenType.IDENTIFIER).value as keyof T | keyof U
|
|
452
466
|
}
|
|
453
|
-
|
|
467
|
+
|
|
454
468
|
if (this.match(TokenType.LIMIT)) {
|
|
455
469
|
this.advance()
|
|
456
470
|
joinQuery.$limit = parseInt(this.expect(TokenType.NUMBER).value)
|
|
457
471
|
}
|
|
458
|
-
|
|
472
|
+
|
|
459
473
|
return joinQuery
|
|
460
474
|
}
|
|
461
475
|
|
|
462
476
|
private parseJoinConditions<T, U>(): _on<T, U> {
|
|
463
477
|
const conditions: _on<T, U> = {}
|
|
464
|
-
|
|
478
|
+
|
|
465
479
|
do {
|
|
466
480
|
// Parse: table1.column = table2.column
|
|
467
481
|
const leftSide = this.parseJoinColumn()
|
|
468
482
|
const operator = this.parseJoinOperator()
|
|
469
483
|
const rightSide = this.parseJoinColumn()
|
|
470
|
-
|
|
484
|
+
|
|
471
485
|
// Build the join condition
|
|
472
486
|
const leftColumn = leftSide.column as keyof T
|
|
473
487
|
const rightColumn = rightSide.column as keyof U
|
|
474
|
-
|
|
488
|
+
|
|
475
489
|
if (!conditions[leftColumn]) {
|
|
476
490
|
conditions[leftColumn] = {} as _joinOperand<U>
|
|
477
491
|
}
|
|
478
|
-
|
|
479
|
-
(conditions[leftColumn] as any)[operator] = rightColumn
|
|
480
|
-
|
|
492
|
+
|
|
493
|
+
;(conditions[leftColumn] as any)[operator] = rightColumn
|
|
494
|
+
|
|
481
495
|
if (this.match(TokenType.AND)) {
|
|
482
496
|
this.advance()
|
|
483
497
|
} else {
|
|
484
498
|
break
|
|
485
499
|
}
|
|
486
500
|
} while (true)
|
|
487
|
-
|
|
501
|
+
|
|
488
502
|
return conditions
|
|
489
503
|
}
|
|
490
504
|
|
|
491
|
-
private parseJoinColumn(): { table?: string
|
|
505
|
+
private parseJoinColumn(): { table?: string; column: string } {
|
|
492
506
|
const identifier = this.expect(TokenType.IDENTIFIER).value
|
|
493
|
-
|
|
507
|
+
|
|
494
508
|
// Check if it's table.column format
|
|
495
509
|
if (this.current.type === TokenType.IDENTIFIER) {
|
|
496
510
|
// This might be a qualified column name, but we'll treat it as simple for now
|
|
497
511
|
return { column: identifier }
|
|
498
512
|
}
|
|
499
|
-
|
|
513
|
+
|
|
500
514
|
return { column: identifier }
|
|
501
515
|
}
|
|
502
516
|
|
|
@@ -515,7 +529,7 @@ class SQLParser {
|
|
|
515
529
|
this.advance()
|
|
516
530
|
return op
|
|
517
531
|
}
|
|
518
|
-
|
|
532
|
+
|
|
519
533
|
throw new Error(`Unknown join operator: ${this.current.type}`)
|
|
520
534
|
}
|
|
521
535
|
|
|
@@ -523,7 +537,7 @@ class SQLParser {
|
|
|
523
537
|
this.expect(TokenType.INSERT)
|
|
524
538
|
this.expect(TokenType.INTO)
|
|
525
539
|
const collection = this.expect(TokenType.IDENTIFIER).value
|
|
526
|
-
|
|
540
|
+
|
|
527
541
|
// Parse column list
|
|
528
542
|
let columns: string[] = []
|
|
529
543
|
if (this.current.type === TokenType.LPAREN) {
|
|
@@ -539,28 +553,28 @@ class SQLParser {
|
|
|
539
553
|
} while (true)
|
|
540
554
|
this.expect(TokenType.RPAREN)
|
|
541
555
|
}
|
|
542
|
-
|
|
556
|
+
|
|
543
557
|
this.expect(TokenType.VALUES)
|
|
544
558
|
this.expect(TokenType.LPAREN)
|
|
545
|
-
|
|
559
|
+
|
|
546
560
|
const values: any = {}
|
|
547
561
|
let valueIndex = 0
|
|
548
|
-
|
|
562
|
+
|
|
549
563
|
do {
|
|
550
564
|
const value = this.parseValue()
|
|
551
565
|
const column = columns[valueIndex] || `col${valueIndex}`
|
|
552
566
|
values[column] = value
|
|
553
567
|
valueIndex++
|
|
554
|
-
|
|
568
|
+
|
|
555
569
|
if (this.current.type === TokenType.COMMA) {
|
|
556
570
|
this.advance()
|
|
557
571
|
} else {
|
|
558
572
|
break
|
|
559
573
|
}
|
|
560
574
|
} while (true)
|
|
561
|
-
|
|
575
|
+
|
|
562
576
|
this.expect(TokenType.RPAREN)
|
|
563
|
-
|
|
577
|
+
|
|
564
578
|
return {
|
|
565
579
|
$collection: collection,
|
|
566
580
|
$values: values as { [K in keyof T]: T[K] }
|
|
@@ -571,27 +585,27 @@ class SQLParser {
|
|
|
571
585
|
this.expect(TokenType.UPDATE)
|
|
572
586
|
const collection = this.expect(TokenType.IDENTIFIER).value
|
|
573
587
|
this.expect(TokenType.SET)
|
|
574
|
-
|
|
588
|
+
|
|
575
589
|
const set: any = {}
|
|
576
|
-
|
|
590
|
+
|
|
577
591
|
do {
|
|
578
592
|
const column = this.expect(TokenType.IDENTIFIER).value
|
|
579
593
|
this.expect(TokenType.EQUALS)
|
|
580
594
|
const value = this.parseValue()
|
|
581
595
|
set[column] = value
|
|
582
|
-
|
|
596
|
+
|
|
583
597
|
if (this.current.type === TokenType.COMMA) {
|
|
584
598
|
this.advance()
|
|
585
599
|
} else {
|
|
586
600
|
break
|
|
587
601
|
}
|
|
588
602
|
} while (true)
|
|
589
|
-
|
|
603
|
+
|
|
590
604
|
const update: _storeUpdate<T> = {
|
|
591
605
|
$collection: collection,
|
|
592
606
|
$set: set as { [K in keyof Partial<T>]: T[K] }
|
|
593
607
|
}
|
|
594
|
-
|
|
608
|
+
|
|
595
609
|
if (this.match(TokenType.WHERE)) {
|
|
596
610
|
const whereQuery: _storeQuery<T> = {
|
|
597
611
|
$collection: collection,
|
|
@@ -599,7 +613,7 @@ class SQLParser {
|
|
|
599
613
|
}
|
|
600
614
|
update.$where = whereQuery
|
|
601
615
|
}
|
|
602
|
-
|
|
616
|
+
|
|
603
617
|
return update
|
|
604
618
|
}
|
|
605
619
|
|
|
@@ -607,31 +621,31 @@ class SQLParser {
|
|
|
607
621
|
this.expect(TokenType.DELETE)
|
|
608
622
|
this.expect(TokenType.FROM)
|
|
609
623
|
const collection = this.expect(TokenType.IDENTIFIER).value
|
|
610
|
-
|
|
624
|
+
|
|
611
625
|
const deleteQuery: _storeDelete<T> = {
|
|
612
626
|
$collection: collection
|
|
613
627
|
}
|
|
614
|
-
|
|
628
|
+
|
|
615
629
|
if (this.match(TokenType.WHERE)) {
|
|
616
630
|
deleteQuery.$ops = this.parseWhereClause<T>()
|
|
617
631
|
}
|
|
618
|
-
|
|
632
|
+
|
|
619
633
|
return deleteQuery
|
|
620
634
|
}
|
|
621
635
|
}
|
|
622
636
|
|
|
623
637
|
// Main SQL to AST converter
|
|
624
638
|
export class Parser {
|
|
625
|
-
static parse<T extends Record<string, any>, U extends Record<string, any> = any>(
|
|
626
|
-
|
|
627
|
-
|
|
639
|
+
static parse<T extends Record<string, any>, U extends Record<string, any> = any>(
|
|
640
|
+
sql: string
|
|
641
|
+
): _storeQuery<T> | _storeInsert<T> | _storeUpdate<T> | _storeDelete<T> | _join<T, U> {
|
|
628
642
|
const lexer = new SQLLexer(sql)
|
|
629
643
|
const tokens = lexer.tokenize()
|
|
630
644
|
const parser = new SQLParser(tokens)
|
|
631
|
-
|
|
645
|
+
|
|
632
646
|
// Determine query type based on first token
|
|
633
647
|
const firstToken = tokens[0]
|
|
634
|
-
|
|
648
|
+
|
|
635
649
|
switch (firstToken.value) {
|
|
636
650
|
case TokenType.CREATE:
|
|
637
651
|
return { $collection: tokens[2].value }
|
|
@@ -657,7 +671,7 @@ export class Parser {
|
|
|
657
671
|
|
|
658
672
|
// Join query builder
|
|
659
673
|
static join<T extends Record<string, any>, U extends Record<string, any>>(
|
|
660
|
-
leftCollection: string,
|
|
674
|
+
leftCollection: string,
|
|
661
675
|
rightCollection: string
|
|
662
676
|
): JoinBuilder<T, U> {
|
|
663
677
|
return new JoinBuilder<T, U>(leftCollection, rightCollection)
|
|
@@ -706,51 +720,58 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
706
720
|
// Convert to SQL string (reverse operation)
|
|
707
721
|
toSQL(): string {
|
|
708
722
|
let sql = 'SELECT '
|
|
709
|
-
|
|
723
|
+
|
|
710
724
|
if (this.queryAst.$select) {
|
|
711
725
|
sql += this.queryAst.$select.join(', ')
|
|
712
726
|
} else {
|
|
713
727
|
sql += '*'
|
|
714
728
|
}
|
|
715
|
-
|
|
729
|
+
|
|
716
730
|
sql += ` FROM ${this.collection}`
|
|
717
|
-
|
|
731
|
+
|
|
718
732
|
if (this.queryAst.$ops && this.queryAst.$ops.length > 0) {
|
|
719
733
|
sql += ' WHERE '
|
|
720
|
-
const conditions = this.queryAst.$ops
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
734
|
+
const conditions = this.queryAst.$ops
|
|
735
|
+
.map((op) => {
|
|
736
|
+
const entries = Object.entries(op)
|
|
737
|
+
return entries
|
|
738
|
+
.map(([column, operand]) => {
|
|
739
|
+
const opEntries = Object.entries(operand as _operand)
|
|
740
|
+
return opEntries
|
|
741
|
+
.map(([operator, value]) => {
|
|
742
|
+
const sqlOp = this.operatorToSQL(operator)
|
|
743
|
+
const sqlValue =
|
|
744
|
+
typeof value === 'string' ? `'${value}'` : value
|
|
745
|
+
return `${column} ${sqlOp} ${sqlValue}`
|
|
746
|
+
})
|
|
747
|
+
.join(' AND ')
|
|
748
|
+
})
|
|
749
|
+
.join(' AND ')
|
|
750
|
+
})
|
|
751
|
+
.join(' AND ')
|
|
731
752
|
sql += conditions
|
|
732
753
|
}
|
|
733
|
-
|
|
754
|
+
|
|
734
755
|
if (this.queryAst.$groupby) {
|
|
735
756
|
sql += ` GROUP BY ${String(this.queryAst.$groupby)}`
|
|
736
757
|
}
|
|
737
|
-
|
|
758
|
+
|
|
738
759
|
if (this.queryAst.$limit) {
|
|
739
760
|
sql += ` LIMIT ${this.queryAst.$limit}`
|
|
740
761
|
}
|
|
741
|
-
|
|
762
|
+
|
|
742
763
|
return sql
|
|
743
764
|
}
|
|
744
765
|
|
|
745
766
|
private operatorToSQL(operator: string): string {
|
|
746
767
|
const opMap: Record<string, string> = {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
768
|
+
$eq: '=',
|
|
769
|
+
$ne: '!=',
|
|
770
|
+
$gt: '>',
|
|
771
|
+
$lt: '<',
|
|
772
|
+
$gte: '>=',
|
|
773
|
+
$lte: '<=',
|
|
774
|
+
$like: 'LIKE'
|
|
754
775
|
}
|
|
755
776
|
return opMap[operator] || '='
|
|
756
777
|
}
|
|
@@ -826,51 +847,55 @@ export class JoinBuilder<T extends Record<string, any>, U extends Record<string,
|
|
|
826
847
|
// Convert to SQL string
|
|
827
848
|
toSQL(): string {
|
|
828
849
|
let sql = 'SELECT '
|
|
829
|
-
|
|
850
|
+
|
|
830
851
|
if (this.joinAst.$select) {
|
|
831
852
|
sql += this.joinAst.$select.join(', ')
|
|
832
853
|
} else {
|
|
833
854
|
sql += '*'
|
|
834
855
|
}
|
|
835
|
-
|
|
856
|
+
|
|
836
857
|
sql += ` FROM ${this.joinAst.$leftCollection}`
|
|
837
|
-
|
|
858
|
+
|
|
838
859
|
// Add join type
|
|
839
860
|
const joinType = this.joinAst.$mode?.toUpperCase() || 'INNER'
|
|
840
861
|
sql += ` ${joinType} JOIN ${this.joinAst.$rightCollection}`
|
|
841
|
-
|
|
862
|
+
|
|
842
863
|
// Add ON conditions
|
|
843
864
|
if (this.joinAst.$on) {
|
|
844
865
|
sql += ' ON '
|
|
845
|
-
const conditions = Object.entries(this.joinAst.$on)
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
866
|
+
const conditions = Object.entries(this.joinAst.$on)
|
|
867
|
+
.map(([leftCol, operand]) => {
|
|
868
|
+
return Object.entries(operand as _joinOperand<U>)
|
|
869
|
+
.map(([operator, rightCol]) => {
|
|
870
|
+
const sqlOp = this.operatorToSQL(operator)
|
|
871
|
+
return `${this.joinAst.$leftCollection}.${leftCol} ${sqlOp} ${this.joinAst.$rightCollection}.${String(rightCol)}`
|
|
872
|
+
})
|
|
873
|
+
.join(' AND ')
|
|
874
|
+
})
|
|
875
|
+
.join(' AND ')
|
|
851
876
|
sql += conditions
|
|
852
877
|
}
|
|
853
|
-
|
|
878
|
+
|
|
854
879
|
if (this.joinAst.$groupby) {
|
|
855
880
|
sql += ` GROUP BY ${String(this.joinAst.$groupby)}`
|
|
856
881
|
}
|
|
857
|
-
|
|
882
|
+
|
|
858
883
|
if (this.joinAst.$limit) {
|
|
859
884
|
sql += ` LIMIT ${this.joinAst.$limit}`
|
|
860
885
|
}
|
|
861
|
-
|
|
886
|
+
|
|
862
887
|
return sql
|
|
863
888
|
}
|
|
864
889
|
|
|
865
890
|
private operatorToSQL(operator: string): string {
|
|
866
891
|
const opMap: Record<string, string> = {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
892
|
+
$eq: '=',
|
|
893
|
+
$ne: '!=',
|
|
894
|
+
$gt: '>',
|
|
895
|
+
$lt: '<',
|
|
896
|
+
$gte: '>=',
|
|
897
|
+
$lte: '<='
|
|
873
898
|
}
|
|
874
899
|
return opMap[operator] || '='
|
|
875
900
|
}
|
|
876
|
-
}
|
|
901
|
+
}
|