@delma/fylo 2.1.0 → 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.
- package/README.md +27 -0
- package/dist/adapters/cipher.js +155 -0
- package/dist/adapters/cipher.js.map +1 -0
- package/dist/core/collection.js +6 -0
- package/dist/core/collection.js.map +1 -0
- package/{src/core/directory.ts → dist/core/directory.js} +28 -35
- package/dist/core/directory.js.map +1 -0
- package/dist/core/doc-id.js +15 -0
- package/dist/core/doc-id.js.map +1 -0
- package/dist/core/extensions.js +16 -0
- package/dist/core/extensions.js.map +1 -0
- package/dist/core/format.js +355 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/parser.js +764 -0
- package/dist/core/parser.js.map +1 -0
- package/dist/core/query.js +47 -0
- package/dist/core/query.js.map +1 -0
- package/dist/engines/s3-files/documents.js +62 -0
- package/dist/engines/s3-files/documents.js.map +1 -0
- package/dist/engines/s3-files/filesystem.js +165 -0
- package/dist/engines/s3-files/filesystem.js.map +1 -0
- package/dist/engines/s3-files/query.js +235 -0
- package/dist/engines/s3-files/query.js.map +1 -0
- package/dist/engines/s3-files/types.js +2 -0
- package/dist/engines/s3-files/types.js.map +1 -0
- package/dist/engines/s3-files.js +629 -0
- package/dist/engines/s3-files.js.map +1 -0
- package/dist/engines/types.js +2 -0
- package/dist/engines/types.js.map +1 -0
- package/dist/index.js +562 -0
- package/dist/index.js.map +1 -0
- package/dist/sync.js +18 -0
- package/dist/sync.js.map +1 -0
- package/{src → dist}/types/fylo.d.ts +14 -1
- package/package.json +2 -2
- package/.env.example +0 -16
- package/.github/copilot-instructions.md +0 -3
- package/.github/prompts/release.prompt.md +0 -10
- package/.github/workflows/ci.yml +0 -37
- package/.github/workflows/publish.yml +0 -91
- package/.prettierrc +0 -7
- package/AGENTS.md +0 -3
- package/CLAUDE.md +0 -3
- package/eslint.config.js +0 -32
- package/src/CLI +0 -39
- package/src/adapters/cipher.ts +0 -180
- package/src/core/collection.ts +0 -5
- package/src/core/extensions.ts +0 -21
- package/src/core/format.ts +0 -457
- package/src/core/parser.ts +0 -901
- package/src/core/query.ts +0 -53
- package/src/engines/s3-files/documents.ts +0 -65
- package/src/engines/s3-files/filesystem.ts +0 -172
- package/src/engines/s3-files/query.ts +0 -291
- package/src/engines/s3-files/types.ts +0 -42
- package/src/engines/s3-files.ts +0 -769
- package/src/engines/types.ts +0 -21
- package/src/index.ts +0 -632
- package/src/sync.ts +0 -58
- package/tests/collection/truncate.test.js +0 -36
- package/tests/data.js +0 -97
- package/tests/helpers/root.js +0 -7
- package/tests/integration/aws-s3-files.canary.test.js +0 -22
- package/tests/integration/create.test.js +0 -39
- package/tests/integration/delete.test.js +0 -97
- package/tests/integration/edge-cases.test.js +0 -162
- package/tests/integration/encryption.test.js +0 -148
- package/tests/integration/export.test.js +0 -46
- package/tests/integration/join-modes.test.js +0 -154
- package/tests/integration/nested.test.js +0 -144
- package/tests/integration/operators.test.js +0 -136
- package/tests/integration/read.test.js +0 -123
- package/tests/integration/rollback.test.js +0 -30
- package/tests/integration/s3-files.performance.test.js +0 -75
- package/tests/integration/s3-files.test.js +0 -205
- package/tests/integration/sync.test.js +0 -154
- package/tests/integration/update.test.js +0 -105
- package/tests/mocks/cipher.js +0 -40
- package/tests/schemas/album.d.ts +0 -5
- package/tests/schemas/album.json +0 -5
- package/tests/schemas/comment.d.ts +0 -7
- package/tests/schemas/comment.json +0 -7
- package/tests/schemas/photo.d.ts +0 -7
- package/tests/schemas/photo.json +0 -7
- package/tests/schemas/post.d.ts +0 -6
- package/tests/schemas/post.json +0 -6
- package/tests/schemas/tip.d.ts +0 -7
- package/tests/schemas/tip.json +0 -7
- package/tests/schemas/todo.d.ts +0 -6
- package/tests/schemas/todo.json +0 -6
- package/tests/schemas/user.d.ts +0 -23
- package/tests/schemas/user.json +0 -23
- package/tsconfig.json +0 -21
- package/tsconfig.typecheck.json +0 -31
- /package/{src → dist}/types/bun-runtime.d.ts +0 -0
- /package/{src → dist}/types/index.d.ts +0 -0
- /package/{src → dist}/types/node-runtime.d.ts +0 -0
- /package/{src → dist}/types/query.d.ts +0 -0
- /package/{src → dist}/types/vendor-modules.d.ts +0 -0
package/src/core/parser.ts
DELETED
|
@@ -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
|
-
}
|