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