@delma/fylo 2.0.1 → 2.1.1

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