@delma/fylo 1.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.
Files changed (60) hide show
  1. package/.env.example +16 -0
  2. package/.github/copilot-instructions.md +113 -0
  3. package/.github/prompts/issue.prompt.md +19 -0
  4. package/.github/prompts/pr.prompt.md +18 -0
  5. package/.github/prompts/release.prompt.md +49 -0
  6. package/.github/prompts/review-pr.prompt.md +19 -0
  7. package/.github/prompts/sync-main.prompt.md +14 -0
  8. package/.github/workflows/ci.yml +37 -0
  9. package/.github/workflows/publish.yml +101 -0
  10. package/.prettierrc +7 -0
  11. package/LICENSE +21 -0
  12. package/README.md +230 -0
  13. package/eslint.config.js +28 -0
  14. package/package.json +51 -0
  15. package/src/CLI +37 -0
  16. package/src/adapters/cipher.ts +174 -0
  17. package/src/adapters/redis.ts +71 -0
  18. package/src/adapters/s3.ts +67 -0
  19. package/src/core/directory.ts +418 -0
  20. package/src/core/extensions.ts +19 -0
  21. package/src/core/format.ts +486 -0
  22. package/src/core/parser.ts +876 -0
  23. package/src/core/query.ts +48 -0
  24. package/src/core/walker.ts +167 -0
  25. package/src/index.ts +1088 -0
  26. package/src/types/fylo.d.ts +139 -0
  27. package/src/types/index.d.ts +3 -0
  28. package/src/types/query.d.ts +73 -0
  29. package/tests/collection/truncate.test.ts +56 -0
  30. package/tests/data.ts +110 -0
  31. package/tests/index.ts +19 -0
  32. package/tests/integration/create.test.ts +57 -0
  33. package/tests/integration/delete.test.ts +147 -0
  34. package/tests/integration/edge-cases.test.ts +232 -0
  35. package/tests/integration/encryption.test.ts +176 -0
  36. package/tests/integration/export.test.ts +61 -0
  37. package/tests/integration/join-modes.test.ts +221 -0
  38. package/tests/integration/nested.test.ts +212 -0
  39. package/tests/integration/operators.test.ts +167 -0
  40. package/tests/integration/read.test.ts +203 -0
  41. package/tests/integration/rollback.test.ts +105 -0
  42. package/tests/integration/update.test.ts +130 -0
  43. package/tests/mocks/cipher.ts +55 -0
  44. package/tests/mocks/redis.ts +13 -0
  45. package/tests/mocks/s3.ts +114 -0
  46. package/tests/schemas/album.d.ts +5 -0
  47. package/tests/schemas/album.json +5 -0
  48. package/tests/schemas/comment.d.ts +7 -0
  49. package/tests/schemas/comment.json +7 -0
  50. package/tests/schemas/photo.d.ts +7 -0
  51. package/tests/schemas/photo.json +7 -0
  52. package/tests/schemas/post.d.ts +6 -0
  53. package/tests/schemas/post.json +6 -0
  54. package/tests/schemas/tip.d.ts +7 -0
  55. package/tests/schemas/tip.json +7 -0
  56. package/tests/schemas/todo.d.ts +6 -0
  57. package/tests/schemas/todo.json +6 -0
  58. package/tests/schemas/user.d.ts +23 -0
  59. package/tests/schemas/user.json +23 -0
  60. package/tsconfig.json +19 -0
@@ -0,0 +1,876 @@
1
+
2
+
3
+ // Token types for SQL lexing
4
+ enum TokenType {
5
+ CREATE = 'CREATE',
6
+ DROP = 'DROP',
7
+ SELECT = 'SELECT',
8
+ FROM = 'FROM',
9
+ WHERE = 'WHERE',
10
+ INSERT = 'INSERT',
11
+ INTO = 'INTO',
12
+ VALUES = 'VALUES',
13
+ UPDATE = 'UPDATE',
14
+ SET = 'SET',
15
+ DELETE = 'DELETE',
16
+ JOIN = 'JOIN',
17
+ INNER = 'INNER',
18
+ LEFT = 'LEFT',
19
+ RIGHT = 'RIGHT',
20
+ OUTER = 'OUTER',
21
+ ON = 'ON',
22
+ GROUP = 'GROUP',
23
+ BY = 'BY',
24
+ ORDER = 'ORDER',
25
+ LIMIT = 'LIMIT',
26
+ AS = 'AS',
27
+ AND = 'AND',
28
+ OR = 'OR',
29
+ EQUALS = '=',
30
+ NOT_EQUALS = '!=',
31
+ GREATER_THAN = '>',
32
+ LESS_THAN = '<',
33
+ GREATER_EQUAL = '>=',
34
+ LESS_EQUAL = '<=',
35
+ LIKE = 'LIKE',
36
+ IDENTIFIER = 'IDENTIFIER',
37
+ STRING = 'STRING',
38
+ NUMBER = 'NUMBER',
39
+ BOOLEAN = 'BOOLEAN',
40
+ NULL = 'NULL',
41
+ COMMA = ',',
42
+ SEMICOLON = ';',
43
+ LPAREN = '(',
44
+ RPAREN = ')',
45
+ ASTERISK = '*',
46
+ EOF = 'EOF'
47
+ }
48
+
49
+ interface Token {
50
+ type: TokenType
51
+ value: string
52
+ position: number
53
+ }
54
+
55
+ // SQL Lexer
56
+ class SQLLexer {
57
+ private input: string
58
+ private position: number = 0
59
+ private current: string | null = null
60
+
61
+ constructor(input: string) {
62
+ this.input = input.trim()
63
+ this.current = this.input[0] || null
64
+ }
65
+
66
+ private advance(): void {
67
+ this.position++
68
+ this.current = this.position < this.input.length ? this.input[this.position] : null
69
+ }
70
+
71
+ private skipWhitespace(): void {
72
+ while (this.current && /\s/.test(this.current)) {
73
+ this.advance()
74
+ }
75
+ }
76
+
77
+ private readString(): string {
78
+ let result = ''
79
+ const quote = this.current
80
+ this.advance() // Skip opening quote
81
+
82
+ while (this.current && this.current !== quote) {
83
+ result += this.current
84
+ this.advance()
85
+ }
86
+
87
+ if (this.current === quote) {
88
+ this.advance() // Skip closing quote
89
+ }
90
+
91
+ return result
92
+ }
93
+
94
+ private readNumber(): string {
95
+ let result = ''
96
+ while (this.current && /[\d.]/.test(this.current)) {
97
+ result += this.current
98
+ this.advance()
99
+ }
100
+ return result
101
+ }
102
+
103
+ private readIdentifier(): string {
104
+ let result = ''
105
+ while (this.current && /[a-zA-Z0-9_\-]/.test(this.current)) {
106
+ result += this.current
107
+ this.advance()
108
+ }
109
+ return result
110
+ }
111
+
112
+ private getKeywordType(word: string): TokenType {
113
+ const keywords: Record<string, TokenType> = {
114
+ 'SELECT': TokenType.SELECT,
115
+ 'FROM': TokenType.FROM,
116
+ 'WHERE': TokenType.WHERE,
117
+ 'INSERT': TokenType.INSERT,
118
+ 'INTO': TokenType.INTO,
119
+ 'VALUES': TokenType.VALUES,
120
+ 'UPDATE': TokenType.UPDATE,
121
+ 'SET': TokenType.SET,
122
+ 'DELETE': TokenType.DELETE,
123
+ 'JOIN': TokenType.JOIN,
124
+ 'INNER': TokenType.INNER,
125
+ 'LEFT': TokenType.LEFT,
126
+ 'RIGHT': TokenType.RIGHT,
127
+ 'OUTER': TokenType.OUTER,
128
+ 'ON': TokenType.ON,
129
+ 'GROUP': TokenType.GROUP,
130
+ 'BY': TokenType.BY,
131
+ 'ORDER': TokenType.ORDER,
132
+ 'LIMIT': TokenType.LIMIT,
133
+ 'AS': TokenType.AS,
134
+ 'AND': TokenType.AND,
135
+ 'OR': TokenType.OR,
136
+ 'LIKE': TokenType.LIKE,
137
+ 'TRUE': TokenType.BOOLEAN,
138
+ 'FALSE': TokenType.BOOLEAN,
139
+ 'NULL': TokenType.NULL
140
+ }
141
+ return keywords[word.toUpperCase()] || TokenType.IDENTIFIER
142
+ }
143
+
144
+ tokenize(): Token[] {
145
+ const tokens: Token[] = []
146
+
147
+ while (this.current) {
148
+ this.skipWhitespace()
149
+
150
+ if (!this.current) break
151
+
152
+ const position = this.position
153
+
154
+ // String literals
155
+ if (this.current === "'" || this.current === '"') {
156
+ const value = this.readString()
157
+ tokens.push({ type: TokenType.STRING, value, position })
158
+ continue
159
+ }
160
+
161
+ // Numbers
162
+ if (/\d/.test(this.current)) {
163
+ const value = this.readNumber()
164
+ tokens.push({ type: TokenType.NUMBER, value, position })
165
+ continue
166
+ }
167
+
168
+ // Identifiers and keywords
169
+ if (/[a-zA-Z_]/.test(this.current)) {
170
+ let value = this.readIdentifier()
171
+ // Support dot notation for nested fields (e.g. address.city → address/city)
172
+ while (this.current === '.' && this.position + 1 < this.input.length && /[a-zA-Z_]/.test(this.input[this.position + 1])) {
173
+ this.advance() // skip '.'
174
+ value += '/' + this.readIdentifier()
175
+ }
176
+ const type = this.getKeywordType(value)
177
+ tokens.push({ type, value, position })
178
+ continue
179
+ }
180
+
181
+ // Operators and punctuation
182
+ switch (this.current) {
183
+ case '=':
184
+ tokens.push({ type: TokenType.EQUALS, value: '=', position })
185
+ this.advance()
186
+ break
187
+ case '!':
188
+ if (this.input[this.position + 1] === '=') {
189
+ tokens.push({ type: TokenType.NOT_EQUALS, value: '!=', position })
190
+ this.advance()
191
+ this.advance()
192
+ } else {
193
+ this.advance()
194
+ }
195
+ break
196
+ case '>':
197
+ if (this.input[this.position + 1] === '=') {
198
+ tokens.push({ type: TokenType.GREATER_EQUAL, value: '>=', position })
199
+ this.advance()
200
+ this.advance()
201
+ } else {
202
+ tokens.push({ type: TokenType.GREATER_THAN, value: '>', position })
203
+ this.advance()
204
+ }
205
+ break
206
+ case '<':
207
+ if (this.input[this.position + 1] === '=') {
208
+ tokens.push({ type: TokenType.LESS_EQUAL, value: '<=', position })
209
+ this.advance()
210
+ this.advance()
211
+ } else {
212
+ tokens.push({ type: TokenType.LESS_THAN, value: '<', position })
213
+ this.advance()
214
+ }
215
+ break
216
+ case ',':
217
+ tokens.push({ type: TokenType.COMMA, value: ',', position })
218
+ this.advance()
219
+ break
220
+ case ';':
221
+ tokens.push({ type: TokenType.SEMICOLON, value: ';', position })
222
+ this.advance()
223
+ break
224
+ case '(':
225
+ tokens.push({ type: TokenType.LPAREN, value: '(', position })
226
+ this.advance()
227
+ break
228
+ case ')':
229
+ tokens.push({ type: TokenType.RPAREN, value: ')', position })
230
+ this.advance()
231
+ break
232
+ case '*':
233
+ tokens.push({ type: TokenType.ASTERISK, value: '*', position })
234
+ this.advance()
235
+ break
236
+ default:
237
+ this.advance()
238
+ break
239
+ }
240
+ }
241
+
242
+ tokens.push({ type: TokenType.EOF, value: '', position: this.position })
243
+ return tokens
244
+ }
245
+ }
246
+
247
+ // SQL Parser
248
+ class SQLParser {
249
+ private tokens: Token[]
250
+ private position: number = 0
251
+ private current: Token
252
+
253
+ constructor(tokens: Token[]) {
254
+ this.tokens = tokens
255
+ this.current = tokens[0]
256
+ }
257
+
258
+ private advance(): void {
259
+ this.position++
260
+ this.current = this.tokens[this.position] || { type: TokenType.EOF, value: '', position: -1 }
261
+ }
262
+
263
+ private expect(type: TokenType): Token {
264
+ if (this.current.type !== type) {
265
+ throw new Error('Invalid SQL syntax')
266
+ }
267
+ const token = this.current
268
+ this.advance()
269
+ return token
270
+ }
271
+
272
+ private match(...types: TokenType[]): boolean {
273
+ return types.includes(this.current.type)
274
+ }
275
+
276
+ private parseValue(): any {
277
+ if (this.current.type === TokenType.STRING) {
278
+ const value = this.current.value
279
+ this.advance()
280
+ return value
281
+ }
282
+ if (this.current.type === TokenType.NUMBER) {
283
+ const value = parseFloat(this.current.value)
284
+ this.advance()
285
+ return value
286
+ }
287
+ if (this.current.type === TokenType.BOOLEAN) {
288
+ const value = this.current.value.toLowerCase() === 'true'
289
+ this.advance()
290
+ return value
291
+ }
292
+ if (this.current.type === TokenType.NULL) {
293
+ this.advance()
294
+ return null
295
+ }
296
+ throw new Error(`Unexpected value type: ${this.current.type}`)
297
+ }
298
+
299
+ private parseOperator(): string {
300
+ const operatorMap: Partial<Record<TokenType, string>> = {
301
+ [TokenType.EQUALS]: '$eq',
302
+ [TokenType.NOT_EQUALS]: '$ne',
303
+ [TokenType.GREATER_THAN]: '$gt',
304
+ [TokenType.LESS_THAN]: '$lt',
305
+ [TokenType.GREATER_EQUAL]: '$gte',
306
+ [TokenType.LESS_EQUAL]: '$lte',
307
+ [TokenType.LIKE]: '$like'
308
+ }
309
+
310
+ if (operatorMap[this.current.type]) {
311
+ const op = operatorMap[this.current.type]
312
+ this.advance()
313
+ return op ?? ''
314
+ }
315
+
316
+ throw new Error(`Unknown operator: ${this.current.type}`)
317
+ }
318
+
319
+ private parseCondition(): _condition {
320
+ const column = this.expect(TokenType.IDENTIFIER).value
321
+ const operator = this.parseOperator()
322
+ const value = this.parseValue()
323
+
324
+ return { column, operator, value }
325
+ }
326
+
327
+ private parseWhereClause<T>(): Array<_op<T>> {
328
+ this.expect(TokenType.WHERE)
329
+ const conditions: Array<_op<T>> = []
330
+
331
+ do {
332
+ const condition = this.parseCondition()
333
+ const op: _op<T> = {
334
+ [condition.column as keyof T]: {
335
+ [condition.operator]: condition.value
336
+ } as _operand
337
+ } as _op<T>
338
+ conditions.push(op)
339
+
340
+ if (this.match(TokenType.AND, TokenType.OR)) {
341
+ this.advance()
342
+ } else {
343
+ break
344
+ }
345
+ } while (true)
346
+
347
+ return conditions
348
+ }
349
+
350
+ private parseSelectClause(): string[] {
351
+ this.expect(TokenType.SELECT)
352
+ const columns: string[] = []
353
+
354
+ if (this.current.type === TokenType.ASTERISK) {
355
+ this.advance()
356
+ return ['*']
357
+ }
358
+
359
+ do {
360
+ columns.push(this.expect(TokenType.IDENTIFIER).value)
361
+ if (this.current.type === TokenType.COMMA) {
362
+ this.advance()
363
+ } else {
364
+ break
365
+ }
366
+ } while (true)
367
+
368
+ return columns
369
+ }
370
+
371
+ parseSelect<T extends Record<string, any>>(): _storeQuery<T> | _join<T, any> {
372
+ const select = this.parseSelectClause()
373
+ this.expect(TokenType.FROM)
374
+ const collection = this.expect(TokenType.IDENTIFIER).value
375
+
376
+ // Check if this is a JOIN query
377
+ if (this.match(TokenType.JOIN, TokenType.INNER, TokenType.LEFT, TokenType.RIGHT, TokenType.OUTER)) {
378
+ return this.parseJoinQuery<T, any>(select, collection)
379
+ }
380
+
381
+ const query: _storeQuery<T> = {
382
+ $collection: collection,
383
+ $select: select.includes('*') ? undefined : select as Array<keyof T>,
384
+ $onlyIds: select.includes('_id')
385
+ }
386
+
387
+ if (this.match(TokenType.WHERE)) {
388
+ query.$ops = this.parseWhereClause<T>()
389
+ }
390
+
391
+ if (this.match(TokenType.GROUP)) {
392
+ this.advance()
393
+ this.expect(TokenType.BY)
394
+ query.$groupby = this.expect(TokenType.IDENTIFIER).value as keyof T
395
+ }
396
+
397
+ if (this.match(TokenType.LIMIT)) {
398
+ this.advance()
399
+ query.$limit = parseInt(this.expect(TokenType.NUMBER).value)
400
+ }
401
+
402
+ return query
403
+ }
404
+
405
+ parseJoinQuery<T extends Record<string, any>, U extends Record<string, any>>(
406
+ select: string[],
407
+ leftCollection: string
408
+ ): _join<T, U> {
409
+ // Parse join type
410
+ let joinMode: "inner" | "left" | "right" | "outer" = "inner"
411
+
412
+ if (this.match(TokenType.INNER)) {
413
+ this.advance()
414
+ joinMode = "inner"
415
+ } else if (this.match(TokenType.LEFT)) {
416
+ this.advance()
417
+ joinMode = "left"
418
+ } else if (this.match(TokenType.RIGHT)) {
419
+ this.advance()
420
+ joinMode = "right"
421
+ } else if (this.match(TokenType.OUTER)) {
422
+ this.advance()
423
+ joinMode = "outer"
424
+ }
425
+
426
+ this.expect(TokenType.JOIN)
427
+ const rightCollection = this.expect(TokenType.IDENTIFIER).value
428
+ this.expect(TokenType.ON)
429
+
430
+ // Parse join conditions
431
+ const onConditions = this.parseJoinConditions<T, U>()
432
+
433
+ const joinQuery: _join<T, U> = {
434
+ $leftCollection: leftCollection,
435
+ $rightCollection: rightCollection,
436
+ $mode: joinMode,
437
+ $on: onConditions,
438
+ $select: select.includes('*') ? undefined : select as Array<keyof T | keyof U>
439
+ }
440
+
441
+ // Parse additional clauses
442
+ if (this.match(TokenType.WHERE)) {
443
+ // For joins, WHERE conditions would need to be handled differently
444
+ // Skip for now as it's complex with joined tables
445
+ this.parseWhereClause<T>()
446
+ }
447
+
448
+ if (this.match(TokenType.GROUP)) {
449
+ this.advance()
450
+ this.expect(TokenType.BY)
451
+ joinQuery.$groupby = this.expect(TokenType.IDENTIFIER).value as keyof T | keyof U
452
+ }
453
+
454
+ if (this.match(TokenType.LIMIT)) {
455
+ this.advance()
456
+ joinQuery.$limit = parseInt(this.expect(TokenType.NUMBER).value)
457
+ }
458
+
459
+ return joinQuery
460
+ }
461
+
462
+ private parseJoinConditions<T, U>(): _on<T, U> {
463
+ const conditions: _on<T, U> = {}
464
+
465
+ do {
466
+ // Parse: table1.column = table2.column
467
+ const leftSide = this.parseJoinColumn()
468
+ const operator = this.parseJoinOperator()
469
+ const rightSide = this.parseJoinColumn()
470
+
471
+ // Build the join condition
472
+ const leftColumn = leftSide.column as keyof T
473
+ const rightColumn = rightSide.column as keyof U
474
+
475
+ if (!conditions[leftColumn]) {
476
+ conditions[leftColumn] = {} as _joinOperand<U>
477
+ }
478
+
479
+ (conditions[leftColumn] as any)[operator] = rightColumn
480
+
481
+ if (this.match(TokenType.AND)) {
482
+ this.advance()
483
+ } else {
484
+ break
485
+ }
486
+ } while (true)
487
+
488
+ return conditions
489
+ }
490
+
491
+ private parseJoinColumn(): { table?: string, column: string } {
492
+ const identifier = this.expect(TokenType.IDENTIFIER).value
493
+
494
+ // Check if it's table.column format
495
+ if (this.current.type === TokenType.IDENTIFIER) {
496
+ // This might be a qualified column name, but we'll treat it as simple for now
497
+ return { column: identifier }
498
+ }
499
+
500
+ return { column: identifier }
501
+ }
502
+
503
+ private parseJoinOperator(): string {
504
+ const operatorMap: Record<string, string> = {
505
+ [TokenType.EQUALS]: '$eq',
506
+ [TokenType.NOT_EQUALS]: '$ne',
507
+ [TokenType.GREATER_THAN]: '$gt',
508
+ [TokenType.LESS_THAN]: '$lt',
509
+ [TokenType.GREATER_EQUAL]: '$gte',
510
+ [TokenType.LESS_EQUAL]: '$lte'
511
+ }
512
+
513
+ if (operatorMap[this.current.type]) {
514
+ const op = operatorMap[this.current.type]
515
+ this.advance()
516
+ return op
517
+ }
518
+
519
+ throw new Error(`Unknown join operator: ${this.current.type}`)
520
+ }
521
+
522
+ parseInsert<T extends Record<string, any>>(): _storeInsert<T> {
523
+ this.expect(TokenType.INSERT)
524
+ this.expect(TokenType.INTO)
525
+ const collection = this.expect(TokenType.IDENTIFIER).value
526
+
527
+ // Parse column list
528
+ let columns: string[] = []
529
+ if (this.current.type === TokenType.LPAREN) {
530
+ this.advance()
531
+ do {
532
+ columns.push(this.expect(TokenType.IDENTIFIER).value)
533
+ // @ts-expect-error - current may be undefined at end of input
534
+ if (this.current.type === TokenType.COMMA) {
535
+ this.advance()
536
+ } else {
537
+ break
538
+ }
539
+ } while (true)
540
+ this.expect(TokenType.RPAREN)
541
+ }
542
+
543
+ this.expect(TokenType.VALUES)
544
+ this.expect(TokenType.LPAREN)
545
+
546
+ const values: any = {}
547
+ let valueIndex = 0
548
+
549
+ do {
550
+ const value = this.parseValue()
551
+ const column = columns[valueIndex] || `col${valueIndex}`
552
+ values[column] = value
553
+ valueIndex++
554
+
555
+ if (this.current.type === TokenType.COMMA) {
556
+ this.advance()
557
+ } else {
558
+ break
559
+ }
560
+ } while (true)
561
+
562
+ this.expect(TokenType.RPAREN)
563
+
564
+ return {
565
+ $collection: collection,
566
+ $values: values as { [K in keyof T]: T[K] }
567
+ }
568
+ }
569
+
570
+ parseUpdate<T extends Record<string, any>>(): _storeUpdate<T> {
571
+ this.expect(TokenType.UPDATE)
572
+ const collection = this.expect(TokenType.IDENTIFIER).value
573
+ this.expect(TokenType.SET)
574
+
575
+ const set: any = {}
576
+
577
+ do {
578
+ const column = this.expect(TokenType.IDENTIFIER).value
579
+ this.expect(TokenType.EQUALS)
580
+ const value = this.parseValue()
581
+ set[column] = value
582
+
583
+ if (this.current.type === TokenType.COMMA) {
584
+ this.advance()
585
+ } else {
586
+ break
587
+ }
588
+ } while (true)
589
+
590
+ const update: _storeUpdate<T> = {
591
+ $collection: collection,
592
+ $set: set as { [K in keyof Partial<T>]: T[K] }
593
+ }
594
+
595
+ if (this.match(TokenType.WHERE)) {
596
+ const whereQuery: _storeQuery<T> = {
597
+ $collection: collection,
598
+ $ops: this.parseWhereClause<T>()
599
+ }
600
+ update.$where = whereQuery
601
+ }
602
+
603
+ return update
604
+ }
605
+
606
+ parseDelete<T extends Record<string, any>>(): _storeDelete<T> {
607
+ this.expect(TokenType.DELETE)
608
+ this.expect(TokenType.FROM)
609
+ const collection = this.expect(TokenType.IDENTIFIER).value
610
+
611
+ const deleteQuery: _storeDelete<T> = {
612
+ $collection: collection
613
+ }
614
+
615
+ if (this.match(TokenType.WHERE)) {
616
+ deleteQuery.$ops = this.parseWhereClause<T>()
617
+ }
618
+
619
+ return deleteQuery
620
+ }
621
+ }
622
+
623
+ // Main SQL to AST converter
624
+ export class Parser {
625
+ static parse<T extends Record<string, any>, U extends Record<string, any> = any>(sql: string):
626
+ _storeQuery<T> | _storeInsert<T> | _storeUpdate<T> | _storeDelete<T> | _join<T, U> {
627
+
628
+ const lexer = new SQLLexer(sql)
629
+ const tokens = lexer.tokenize()
630
+ const parser = new SQLParser(tokens)
631
+
632
+ // Determine query type based on first token
633
+ const firstToken = tokens[0]
634
+
635
+ switch (firstToken.value) {
636
+ case TokenType.CREATE:
637
+ return { $collection: tokens[2].value }
638
+ case TokenType.SELECT:
639
+ return parser.parseSelect<T>()
640
+ case TokenType.INSERT:
641
+ return parser.parseInsert<T>()
642
+ case TokenType.UPDATE:
643
+ return parser.parseUpdate<T>()
644
+ case TokenType.DELETE:
645
+ return parser.parseDelete<T>()
646
+ case TokenType.DROP:
647
+ return { $collection: tokens[2].value }
648
+ default:
649
+ throw new Error(`Unsupported SQL statement type: ${firstToken.value}`)
650
+ }
651
+ }
652
+
653
+ // Bun SQL inspired query builder methods
654
+ static query<T extends Record<string, any>>(collection: string): QueryBuilder<T> {
655
+ return new QueryBuilder<T>(collection)
656
+ }
657
+
658
+ // Join query builder
659
+ static join<T extends Record<string, any>, U extends Record<string, any>>(
660
+ leftCollection: string,
661
+ rightCollection: string
662
+ ): JoinBuilder<T, U> {
663
+ return new JoinBuilder<T, U>(leftCollection, rightCollection)
664
+ }
665
+ }
666
+
667
+ // Bun SQL inspired query builder
668
+ export class QueryBuilder<T extends Record<string, any>> {
669
+ private collection: string
670
+ private queryAst: Partial<_storeQuery<T>> = {}
671
+
672
+ constructor(collection: string) {
673
+ this.collection = collection
674
+ this.queryAst.$collection = collection
675
+ }
676
+
677
+ select(...columns: Array<keyof T>): this {
678
+ this.queryAst.$select = columns
679
+ return this
680
+ }
681
+
682
+ where(conditions: Array<_op<T>>): this {
683
+ this.queryAst.$ops = conditions
684
+ return this
685
+ }
686
+
687
+ limit(count: number): this {
688
+ this.queryAst.$limit = count
689
+ return this
690
+ }
691
+
692
+ groupBy(column: keyof T): this {
693
+ this.queryAst.$groupby = column
694
+ return this
695
+ }
696
+
697
+ onlyIds(): this {
698
+ this.queryAst.$onlyIds = true
699
+ return this
700
+ }
701
+
702
+ build(): _storeQuery<T> {
703
+ return this.queryAst as _storeQuery<T>
704
+ }
705
+
706
+ // Convert to SQL string (reverse operation)
707
+ toSQL(): string {
708
+ let sql = 'SELECT '
709
+
710
+ if (this.queryAst.$select) {
711
+ sql += this.queryAst.$select.join(', ')
712
+ } else {
713
+ sql += '*'
714
+ }
715
+
716
+ sql += ` FROM ${this.collection}`
717
+
718
+ if (this.queryAst.$ops && this.queryAst.$ops.length > 0) {
719
+ sql += ' WHERE '
720
+ const conditions = this.queryAst.$ops.map(op => {
721
+ const entries = Object.entries(op)
722
+ return entries.map(([column, operand]) => {
723
+ const opEntries = Object.entries(operand as _operand)
724
+ return opEntries.map(([operator, value]) => {
725
+ const sqlOp = this.operatorToSQL(operator)
726
+ const sqlValue = typeof value === 'string' ? `'${value}'` : value
727
+ return `${column} ${sqlOp} ${sqlValue}`
728
+ }).join(' AND ')
729
+ }).join(' AND ')
730
+ }).join(' AND ')
731
+ sql += conditions
732
+ }
733
+
734
+ if (this.queryAst.$groupby) {
735
+ sql += ` GROUP BY ${String(this.queryAst.$groupby)}`
736
+ }
737
+
738
+ if (this.queryAst.$limit) {
739
+ sql += ` LIMIT ${this.queryAst.$limit}`
740
+ }
741
+
742
+ return sql
743
+ }
744
+
745
+ private operatorToSQL(operator: string): string {
746
+ const opMap: Record<string, string> = {
747
+ '$eq': '=',
748
+ '$ne': '!=',
749
+ '$gt': '>',
750
+ '$lt': '<',
751
+ '$gte': '>=',
752
+ '$lte': '<=',
753
+ '$like': 'LIKE'
754
+ }
755
+ return opMap[operator] || '='
756
+ }
757
+ }
758
+
759
+ // Join query builder
760
+ export class JoinBuilder<T extends Record<string, any>, U extends Record<string, any>> {
761
+ private joinAst: Partial<_join<T, U>> = {}
762
+
763
+ constructor(leftCollection: string, rightCollection: string) {
764
+ this.joinAst.$leftCollection = leftCollection
765
+ this.joinAst.$rightCollection = rightCollection
766
+ this.joinAst.$mode = 'inner' // default
767
+ }
768
+
769
+ select(...columns: Array<keyof T | keyof U>): this {
770
+ this.joinAst.$select = columns
771
+ return this
772
+ }
773
+
774
+ innerJoin(): this {
775
+ this.joinAst.$mode = 'inner'
776
+ return this
777
+ }
778
+
779
+ leftJoin(): this {
780
+ this.joinAst.$mode = 'left'
781
+ return this
782
+ }
783
+
784
+ rightJoin(): this {
785
+ this.joinAst.$mode = 'right'
786
+ return this
787
+ }
788
+
789
+ outerJoin(): this {
790
+ this.joinAst.$mode = 'outer'
791
+ return this
792
+ }
793
+
794
+ on(conditions: _on<T, U>): this {
795
+ this.joinAst.$on = conditions
796
+ return this
797
+ }
798
+
799
+ limit(count: number): this {
800
+ this.joinAst.$limit = count
801
+ return this
802
+ }
803
+
804
+ groupBy(column: keyof T | keyof U): this {
805
+ this.joinAst.$groupby = column
806
+ return this
807
+ }
808
+
809
+ onlyIds(): this {
810
+ this.joinAst.$onlyIds = true
811
+ return this
812
+ }
813
+
814
+ rename(mapping: Record<keyof Partial<T> | keyof Partial<U>, string>): this {
815
+ this.joinAst.$rename = mapping
816
+ return this
817
+ }
818
+
819
+ build(): _join<T, U> {
820
+ if (!this.joinAst.$on) {
821
+ throw new Error('JOIN query must have ON conditions')
822
+ }
823
+ return this.joinAst as _join<T, U>
824
+ }
825
+
826
+ // Convert to SQL string
827
+ toSQL(): string {
828
+ let sql = 'SELECT '
829
+
830
+ if (this.joinAst.$select) {
831
+ sql += this.joinAst.$select.join(', ')
832
+ } else {
833
+ sql += '*'
834
+ }
835
+
836
+ sql += ` FROM ${this.joinAst.$leftCollection}`
837
+
838
+ // Add join type
839
+ const joinType = this.joinAst.$mode?.toUpperCase() || 'INNER'
840
+ sql += ` ${joinType} JOIN ${this.joinAst.$rightCollection}`
841
+
842
+ // Add ON conditions
843
+ if (this.joinAst.$on) {
844
+ sql += ' ON '
845
+ const conditions = Object.entries(this.joinAst.$on).map(([leftCol, operand]) => {
846
+ return Object.entries(operand as _joinOperand<U>).map(([operator, rightCol]) => {
847
+ const sqlOp = this.operatorToSQL(operator)
848
+ return `${this.joinAst.$leftCollection}.${leftCol} ${sqlOp} ${this.joinAst.$rightCollection}.${String(rightCol)}`
849
+ }).join(' AND ')
850
+ }).join(' AND ')
851
+ sql += conditions
852
+ }
853
+
854
+ if (this.joinAst.$groupby) {
855
+ sql += ` GROUP BY ${String(this.joinAst.$groupby)}`
856
+ }
857
+
858
+ if (this.joinAst.$limit) {
859
+ sql += ` LIMIT ${this.joinAst.$limit}`
860
+ }
861
+
862
+ return sql
863
+ }
864
+
865
+ private operatorToSQL(operator: string): string {
866
+ const opMap: Record<string, string> = {
867
+ '$eq': '=',
868
+ '$ne': '!=',
869
+ '$gt': '>',
870
+ '$lt': '<',
871
+ '$gte': '>=',
872
+ '$lte': '<=',
873
+ }
874
+ return opMap[operator] || '='
875
+ }
876
+ }