tantan-typeorm-gs 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/bun.lock +300 -0
  4. package/docs/authentication.md +118 -0
  5. package/docs/configuration.md +680 -0
  6. package/docs/contributing.md +177 -0
  7. package/docs/crud.md +351 -0
  8. package/docs/custom-client.md +165 -0
  9. package/docs/development.md +243 -0
  10. package/docs/entities.md +502 -0
  11. package/docs/find-options.md +296 -0
  12. package/docs/google-cloud.md +101 -0
  13. package/docs/installation.md +51 -0
  14. package/docs/integration-testing.md +178 -0
  15. package/docs/limitations.md +223 -0
  16. package/docs/pagination.md +384 -0
  17. package/docs/performance.md +171 -0
  18. package/docs/security.md +153 -0
  19. package/docs/sorting.md +0 -0
  20. package/docs/supported-features.md +310 -0
  21. package/docs/synchronization.md +203 -0
  22. package/package.json +43 -0
  23. package/src/client/index.ts +589 -0
  24. package/src/core/data-source.ts +47 -0
  25. package/src/core/driver.ts +290 -0
  26. package/src/core/error.ts +144 -0
  27. package/src/core/memory.ts +152 -0
  28. package/src/core/query/interpreter.ts +949 -0
  29. package/src/core/query/runner.ts +1297 -0
  30. package/src/core/query/types.ts +155 -0
  31. package/src/core/schema-builder.ts +64 -0
  32. package/src/core/types.ts +113 -0
  33. package/src/core/utils.ts +13 -0
  34. package/src/index.ts +3 -0
  35. package/tantan-typeorm-gs.code-workspace +8 -0
  36. package/test/base/0001-data-source.test.ts +252 -0
  37. package/test/base/0002-operator.test.ts +616 -0
  38. package/test/base/0003-select.test.ts +281 -0
  39. package/test/base/0004-aggregate.test.ts +213 -0
  40. package/test/base/0005-transcation.test.ts +1566 -0
  41. package/test/base/0006-relation.test.ts +1611 -0
  42. package/test/base/0007-logging.test.ts +182 -0
  43. package/test/base/0008-soft-delete.test.ts +649 -0
  44. package/test/google-sheets/0001-client.test.ts +1705 -0
  45. package/test/google-sheets/0002-worksheet-management.test.ts +408 -0
  46. package/test/google-sheets/0003-data-source.test.ts +1935 -0
  47. package/test/google-sheets/0004-transactions.test.ts +952 -0
  48. package/test/google-sheets/0005-operator.test.ts +1124 -0
  49. package/test/google-sheets/0006-object-criteria.test.ts +283 -0
  50. package/test/google-sheets/0007-performance.test.ts +532 -0
  51. package/test/public-api.test.ts +34 -0
  52. package/tsconfig.build.json +21 -0
  53. package/tsconfig.json +34 -0
@@ -0,0 +1,949 @@
1
+ import type {
2
+ AggregateColumn,
3
+ AggregateFunction,
4
+ DeleteQuery,
5
+ FromQuery,
6
+ InsertQuery,
7
+ JoinCondition,
8
+ JoinQuery,
9
+ JoinType,
10
+ OrderBy,
11
+ OrderDirection,
12
+ Pagination,
13
+ SelectColumn,
14
+ UpdateQuery,
15
+ WhereExpression,
16
+ WhereOperator
17
+ } from "./types.js";
18
+ import { GoogleSheetsParseError, GoogleSheetsUnsupportedOperationError } from "../error.js";
19
+
20
+ export class GoogleSheetsQueryInterpreter
21
+ {
22
+ parseInsertQuery(query: string): InsertQuery
23
+ {
24
+ const match = query.match(/^INSERT\s+INTO\s+"([^"]+)"\s*\((.+?)\)\s*VALUES\s*(.+)$/i);
25
+
26
+ if (!match?.[1] || !match[2] || !match[3])
27
+ {
28
+ throw new GoogleSheetsParseError(`Unable to parse INSERT query: ${query}`);
29
+ }
30
+
31
+ const columns = match[2].split(',').map((column) => column.trim().replace(/^"|"$/g, ''));
32
+ const valuesPart = match[3].trim();
33
+ const rows: string[][] = [];
34
+
35
+ for (const rowMatch of valuesPart.matchAll(/\(([^()]*)\)/g))
36
+ {
37
+ const row = rowMatch[1];
38
+
39
+ if (row === undefined) continue;
40
+
41
+ rows.push(row.split(',').map((value) => value.trim()));
42
+ }
43
+
44
+ if (rows.length === 0) throw new GoogleSheetsParseError(`Unable to parse INSERT query: ${query}`);
45
+
46
+ for (const values of rows)
47
+ {
48
+ if (columns.length !== values.length)
49
+ {
50
+ throw new GoogleSheetsParseError('INSERT columns and values count do not match.')
51
+ }
52
+ }
53
+
54
+ return { columns, values: rows };
55
+ }
56
+
57
+ parseUpdateQuery(query: string): UpdateQuery
58
+ {
59
+ const match = query.match(/^UPDATE\s+"([^"]+)"\s+SET\s+(.+?)\s+WHERE\s+([\s\S]+)$/i);
60
+
61
+ if (!match?.[1] || !match[2] || !match[3])
62
+ {
63
+ throw new GoogleSheetsParseError(`Unable to parse UPDATE query: ${query}`)
64
+ }
65
+
66
+ const assignments =
67
+ match[2]
68
+ .split(',')
69
+ .map(
70
+ (assignment) =>
71
+ {
72
+ const assignmentMatch =
73
+ assignment
74
+ .match(/^\s*"([^"]+)"\s*=\s*(CURRENT_TIMESTAMP|NULL|:[a-zA-Z_][a-zA-Z0-9_]*)\s*$/i);
75
+
76
+ if (!assignmentMatch?.[1] || !assignmentMatch[2])
77
+ {
78
+ throw new GoogleSheetsParseError(`Unable to parse UPDATE assignment: ${assignment}`)
79
+ }
80
+
81
+ return { column: assignmentMatch[1], value: assignmentMatch[2] };
82
+ },
83
+ );
84
+
85
+ return {
86
+ table: match[1],
87
+ assignments,
88
+ where: this.parseWhereExpressionPart(match[3].trim()),
89
+ };
90
+ }
91
+
92
+ parseDeleteQuery(query: string): DeleteQuery
93
+ {
94
+ const match = query.match(/^DELETE\s+FROM\s+"([^"]+)"\s+WHERE\s+([\s\S]+)$/i);
95
+
96
+ if (!match?.[1] || !match[2]) throw new GoogleSheetsParseError(`Unable to parse DELETE query: ${query}`)
97
+
98
+ return { table: match[1], where: this.parseWhereExpressionPart(match[2].trim()) };
99
+ }
100
+
101
+ parseSelectColumns(query: string): SelectColumn[]
102
+ {
103
+ const selectMatch = query.match(/^SELECT\s+(.+?)\s+FROM\s+/i);
104
+
105
+ if (!selectMatch?.[1]) throw new GoogleSheetsParseError('Unable to parse SELECT clause.')
106
+
107
+ return selectMatch[1]
108
+ .split(',')
109
+ .map(
110
+ (expression) =>
111
+ {
112
+ const normalizedExpression = expression.trim();
113
+
114
+ /**
115
+ * Literal:
116
+ *
117
+ * 1 AS "count"
118
+ */
119
+ const literalMatch = normalizedExpression.match(/^(\d+)\s+AS\s+"([^"]+)"$/i);
120
+
121
+ if (literalMatch?.[1] && literalMatch[2])
122
+ {
123
+ return {
124
+ tableAlias: '',
125
+ column: literalMatch[1],
126
+ alias: literalMatch[2],
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Aggregate:
132
+ *
133
+ * COUNT(*)
134
+ * COUNT("product"."category")
135
+ * SUM("product"."price")
136
+ * AVG("product"."price")
137
+ * MIN("product"."price")
138
+ * MAX("product"."price")
139
+ *
140
+ * AS "alias"
141
+ */
142
+ const aggregateMatch =
143
+ normalizedExpression.match(
144
+ /^(COUNT|SUM|AVG|MIN|MAX)\s*\(\s*(\*|"[^"]+"\."[^"]+")\s*\)\s+AS\s+"([^"]+)"$/i,
145
+ );
146
+
147
+ if (aggregateMatch?.[1] && aggregateMatch[2] && aggregateMatch[3])
148
+ {
149
+ let tableAlias = '';
150
+
151
+ const argumentMatch = aggregateMatch[2].match(/^"([^"]+)"\."([^"]+)"$/i);
152
+
153
+ if (argumentMatch?.[1]) tableAlias = argumentMatch[1];
154
+
155
+ return {
156
+ tableAlias,
157
+ column: aggregateMatch[2],
158
+ alias: aggregateMatch[3],
159
+ };
160
+ }
161
+
162
+ /**
163
+ * DISTINCT column without alias:
164
+ *
165
+ * DISTINCT "distinctAlias"."user_id"
166
+ */
167
+
168
+ const distinctMatch = normalizedExpression.match(/^DISTINCT\s+"([^"]+)"\."([^"]+)"$/i);
169
+
170
+ if (distinctMatch?.[1] && distinctMatch[2])
171
+ {
172
+ return {
173
+ tableAlias: distinctMatch[1],
174
+ column: distinctMatch[2],
175
+ alias: distinctMatch[2],
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Column without alias:
181
+ *
182
+ * "distinctAlias"."user_id"
183
+ */
184
+
185
+ const columnMatch = normalizedExpression.match(/^"([^"]+)"\."([^"]+)"$/i);
186
+
187
+ if (columnMatch?.[1] && columnMatch[2])
188
+ {
189
+ return {
190
+ tableAlias: columnMatch[1],
191
+ column: columnMatch[2],
192
+ alias: columnMatch[2],
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Normal column:
198
+ *
199
+ * "product"."id" AS "id"
200
+ *
201
+ * DISTINCT "product"."id" AS "id"
202
+ */
203
+
204
+ const match = normalizedExpression.match(/^(?:DISTINCT\s+)?"([^"]+)"\."([^"]+)"\s+AS\s+"([^"]+)"$/i);
205
+
206
+ if (!match?.[1] || !match[2] || !match[3])
207
+ {
208
+ throw new GoogleSheetsParseError(`Unable to parse SELECT expression: ${expression}`)
209
+ }
210
+
211
+ return {
212
+ tableAlias: match[1],
213
+ column: match[2],
214
+ alias: match[3],
215
+ };
216
+ },
217
+ );
218
+ }
219
+
220
+ parseFrom(query: string): FromQuery
221
+ {
222
+ const match = query.match(/\bFROM\s+"([^"]+)"\s+"([^"]+)"/i);
223
+
224
+ if (!match?.[1] || !match[2]) throw new GoogleSheetsParseError(`Unable to parse FROM clause: ${query}`)
225
+
226
+ return { table: match[1], alias: match[2] };
227
+ }
228
+
229
+ parseJoins(query: string): JoinQuery[]
230
+ {
231
+ const joins: JoinQuery[] = [];
232
+
233
+ const regex =
234
+ /\b(LEFT|INNER)\s+JOIN\s+"([^"]+)"\s+"([^"]+)"\s+ON\s+([\s\S]*?)(?=\s+(?:LEFT|INNER)\s+JOIN\b|\s+WHERE\b|\s+ORDER\s+BY\b|\s+LIMIT\b|\s+OFFSET\b|$)/gi;
235
+
236
+ let match: RegExpExecArray | null;
237
+
238
+ while ((match = regex.exec(query)) !== null)
239
+ {
240
+ const [, type, table, alias, onExpression] = match;
241
+
242
+ if (!type || !table || !alias || !onExpression) continue;
243
+
244
+ const conditions = this.parseJoinConditions(onExpression);
245
+
246
+ if (conditions.length === 0) continue;
247
+
248
+ joins.push({
249
+ type: type.toUpperCase() as JoinType,
250
+ table,
251
+ alias,
252
+ conditions,
253
+ });
254
+ }
255
+
256
+ return joins;
257
+ }
258
+
259
+ private parseJoinConditions(expression: string): JoinCondition[]
260
+ {
261
+ let normalized = expression.trim();
262
+
263
+ /**
264
+ * Remove wrapping parentheses.
265
+ *
266
+ * (
267
+ * "a"."x" = :param
268
+ * AND
269
+ * "a"."y" = "b"."y"
270
+ * )
271
+ */
272
+ while (normalized.startsWith('(') && normalized.endsWith(')'))
273
+ {
274
+ normalized = normalized.slice(1, -1).trim();
275
+ }
276
+
277
+ const parts = normalized.split(/\s+AND\s+/i);
278
+ const conditions: JoinCondition[] = [];
279
+
280
+ for (const part of parts)
281
+ {
282
+ const condition = part.trim();
283
+
284
+ /**
285
+ * Column = Column
286
+ *
287
+ * "a"."x" = "b"."y"
288
+ */
289
+
290
+ const columnMatch = condition.match(/^"([^"]+)"\."([^"]+)"\s*=\s*"([^"]+)"\."([^"]+)"$/i);
291
+
292
+ if (columnMatch?.[1] && columnMatch[2] && columnMatch[3] && columnMatch[4])
293
+ {
294
+ conditions.push({
295
+ left: {
296
+ type: 'column',
297
+ tableAlias: columnMatch[1],
298
+ column: columnMatch[2],
299
+ },
300
+ operator: '=',
301
+ right: {
302
+ type: 'column',
303
+ tableAlias: columnMatch[3],
304
+ column: columnMatch[4],
305
+ },
306
+ });
307
+
308
+ continue;
309
+ }
310
+
311
+ /**
312
+ * Column = Parameter
313
+ *
314
+ * "a"."x" = :orm_param_0
315
+ */
316
+ const parameterMatch = condition.match(/^"([^"]+)"\."([^"]+)"\s*=\s*(:[a-zA-Z_][a-zA-Z0-9_]*)$/i);
317
+
318
+ if (parameterMatch?.[1] && parameterMatch[2] && parameterMatch[3])
319
+ {
320
+ conditions.push({
321
+ left: {
322
+ type: 'column',
323
+ tableAlias: parameterMatch[1],
324
+ column: parameterMatch[2],
325
+ },
326
+ operator: '=',
327
+ right: {
328
+ type: 'parameter',
329
+ value: parameterMatch[3],
330
+ },
331
+ });
332
+
333
+ continue;
334
+ }
335
+
336
+ /**
337
+ * Column IS NULL
338
+ *
339
+ * ("a"."deletedAt" IS NULL)
340
+ */
341
+ const nullMatch =
342
+ condition.match(
343
+ /^\("([^"]+)"\."([^"]+)"\s+IS\s+(NOT\s+)?NULL\)$/i,
344
+ );
345
+
346
+ if (nullMatch?.[1] && nullMatch[2])
347
+ {
348
+ conditions.push({
349
+ left: {
350
+ type: 'column',
351
+ tableAlias: nullMatch[1],
352
+ column: nullMatch[2],
353
+ },
354
+ operator: nullMatch[3]
355
+ ? 'IS NOT NULL'
356
+ : 'IS NULL',
357
+ });
358
+
359
+ continue;
360
+ }
361
+
362
+
363
+ throw new GoogleSheetsParseError(`Unable to parse JOIN condition: ${condition}`)
364
+ }
365
+
366
+ return conditions;
367
+ }
368
+
369
+ parseAggregate(query: string): AggregateColumn | undefined
370
+ {
371
+ const match =
372
+ query.match(/^SELECT\s+(COUNT|SUM|AVG|MIN|MAX)\s*\(\s*([^)]+)\s*\)\s+AS\s+"([^"]+)"\s+FROM\s+/i);
373
+
374
+ if (!match?.[1] || !match[2] || !match[3]) return undefined;
375
+
376
+ return {
377
+ function: match[1].toUpperCase() as AggregateFunction,
378
+ argument: match[2].trim(),
379
+ alias: match[3],
380
+ };
381
+ }
382
+
383
+ parseAggregates(query: string): AggregateColumn[]
384
+ {
385
+ const selectMatch = query.match(/^SELECT\s+(.+?)\s+FROM\s+/i);
386
+
387
+ if (!selectMatch?.[1]) return [];
388
+
389
+ const expressions = selectMatch[1].split(',').map((expression) => expression.trim());
390
+
391
+ const aggregates: AggregateColumn[] = [];
392
+
393
+ for (const expression of expressions)
394
+ {
395
+ const match = expression.match(/^(COUNT|SUM|AVG|MIN|MAX)\s*\(\s*([^)]+)\s*\)\s+AS\s+"([^"]+)"$/i);
396
+
397
+ if (!match?.[1] || !match[2] || !match[3]) continue;
398
+
399
+ aggregates.push({
400
+ function: match[1].toUpperCase() as AggregateFunction,
401
+ argument: match[2].trim(),
402
+ alias: match[3],
403
+ });
404
+ }
405
+
406
+ return aggregates;
407
+ }
408
+
409
+ parseWhereExpression(query: string): WhereExpression | undefined
410
+ {
411
+ const whereMatch = query.match(/\bWHERE\s+(.+?)(?=\s+ORDER\s+BY|\s+LIMIT\s+\d+|\s+OFFSET\s+\d+|$)/i);
412
+
413
+ if (!whereMatch?.[1]) return undefined;
414
+
415
+ return this.parseWhereExpressionPart(whereMatch[1].trim());
416
+ }
417
+
418
+ private parseWhereExpressionPart(expression: string): WhereExpression
419
+ {
420
+ const normalizedExpression = this.unwrapOuterParentheses(expression.trim());
421
+ const orExpressions = this.splitLogicalExpression(normalizedExpression, 'OR');
422
+
423
+ if (orExpressions.length > 1)
424
+ {
425
+ return {
426
+ type: 'or',
427
+ expressions: orExpressions.map((part) => this.parseWhereAndExpression(part)),
428
+ };
429
+ }
430
+
431
+ return this.parseWhereAndExpression(normalizedExpression);
432
+ }
433
+
434
+ private parseWhereAndExpression(expression: string): WhereExpression
435
+ {
436
+ const normalizedExpression = this.unwrapOuterParentheses(expression.trim());
437
+
438
+ const notMatch = normalizedExpression.match(/^NOT\s*(\(.*\))$/is);
439
+
440
+ if (notMatch?.[1])
441
+ {
442
+ return {
443
+ type: 'not',
444
+ expression: this.parseWhereExpressionPart(notMatch[1])
445
+ };
446
+ }
447
+
448
+ const andExpressions = this.splitLogicalExpression(normalizedExpression, 'AND');
449
+
450
+ if (andExpressions.length === 1) return this.parseSingleWhereCondition(andExpressions[0]!);
451
+
452
+ return {
453
+ type: 'and',
454
+ expressions: andExpressions.map((part) => this.parseWhereExpressionPart(part))
455
+ };
456
+ }
457
+
458
+ private parseSingleWhereCondition(expression: string): WhereExpression
459
+ {
460
+ const normalizedExpression = this.unwrapOuterParentheses(expression.trim());
461
+
462
+ /**
463
+ * IS NULL / IS NOT NULL
464
+ */
465
+ const nullMatch = normalizedExpression.match(/^(?:"([^"]+)"\.)?"([^"]+)"\s+IS\s+(NOT\s+)?NULL$/i);
466
+
467
+ if (nullMatch?.[2])
468
+ {
469
+ return {
470
+ type: 'condition',
471
+ condition: {
472
+ tableAlias: nullMatch[1] ?? '',
473
+ column: nullMatch[2],
474
+ operator: nullMatch[3] ? 'IS NOT NULL' : 'IS NULL',
475
+ parameters: [],
476
+ },
477
+ };
478
+ }
479
+
480
+ /**
481
+ * IN / NOT IN
482
+ */
483
+ const inMatch = normalizedExpression.match(/^(?:"([^"]+)"\.)?"([^"]+)"\s+(NOT\s+)?IN\s*\((.+)\)$/i);
484
+
485
+ if (inMatch?.[2] && inMatch[4])
486
+ {
487
+ return {
488
+ type: 'condition',
489
+ condition: {
490
+ tableAlias: inMatch[1] ?? '',
491
+ column: inMatch[2],
492
+ operator: inMatch[3] ? 'NOT IN' : 'IN',
493
+ parameters: inMatch[4].split(',').map(parameter => parameter.trim()),
494
+ },
495
+ };
496
+ }
497
+
498
+ /**
499
+ * BETWEEN
500
+ */
501
+ const betweenMatch =
502
+ normalizedExpression.match(
503
+ /^(?:"([^"]+)"\.)?"([^"]+)"\s+BETWEEN\s+(:[a-zA-Z_][a-zA-Z0-9_]*)\s+AND\s+(:[a-zA-Z_][a-zA-Z0-9_]*)$/i
504
+ );
505
+
506
+ if (betweenMatch?.[2] && betweenMatch[3] && betweenMatch[4])
507
+ {
508
+ return {
509
+ type: 'condition',
510
+ condition: {
511
+ tableAlias: betweenMatch[1] ?? '',
512
+ column: betweenMatch[2],
513
+ operator: 'BETWEEN',
514
+ parameters: [
515
+ betweenMatch[3],
516
+ betweenMatch[4],
517
+ ],
518
+ },
519
+ };
520
+ }
521
+
522
+ /**
523
+ * ANY
524
+ *
525
+ * "User"."id" = ANY(:orm_param_0)
526
+ */
527
+ const anyMatch =
528
+ normalizedExpression.match(
529
+ /^(?:"([^"]+)"\.)?"([^"]+)"\s*=\s*ANY\s*\(\s*(:[a-zA-Z_][a-zA-Z0-9_]*)\s*\)$/i
530
+ );
531
+
532
+ if (anyMatch?.[2] && anyMatch[3])
533
+ {
534
+ return {
535
+ type: 'condition',
536
+ condition: {
537
+ tableAlias: anyMatch[1] ?? '',
538
+ column: anyMatch[2],
539
+ operator: 'ANY',
540
+ parameters: [anyMatch[3]],
541
+ },
542
+ };
543
+ }
544
+
545
+ /**
546
+ * LIKE / NOT LIKE
547
+ *
548
+ * ILIKE / NOT ILIKE
549
+ */
550
+ const likeMatch =
551
+ normalizedExpression.match(
552
+ /^(?:"([^"]+)"\.)?"([^"]+)"\s+(NOT\s+)?(I?LIKE)\s+(:[a-zA-Z_][a-zA-Z0-9_]*)$/i
553
+ );
554
+
555
+ if (likeMatch?.[2] && likeMatch[4] && likeMatch[5])
556
+ {
557
+ const likeOperator = likeMatch[4].toUpperCase();
558
+
559
+ let operator: WhereOperator;
560
+
561
+ if (likeOperator === 'LIKE') operator = likeMatch[3] ? 'NOT LIKE' : 'LIKE';
562
+ else operator = likeMatch[3] ? 'NOT ILIKE' : 'ILIKE';
563
+
564
+ return {
565
+ type: 'condition',
566
+ condition: {
567
+ tableAlias: likeMatch[1] ?? '',
568
+ column: likeMatch[2],
569
+ operator,
570
+ parameters: [likeMatch[5]],
571
+ },
572
+ };
573
+ }
574
+
575
+ /**
576
+ * =, !=, <>, >, >=, <, <=
577
+ */
578
+ const comparisonMatch =
579
+ normalizedExpression.match(
580
+ /^(?:"([^"]+)"\.)?"([^"]+)"\s*(>=|<=|!=|<>|=|>|<)\s*(:[a-zA-Z_][a-zA-Z0-9_]*)$/i,
581
+ );
582
+
583
+ if (comparisonMatch?.[2] && comparisonMatch[3] && comparisonMatch[4])
584
+ {
585
+ return {
586
+ type: 'condition',
587
+ condition: {
588
+ tableAlias: comparisonMatch[1] ?? '',
589
+ column: comparisonMatch[2],
590
+ operator: comparisonMatch[3] as WhereOperator,
591
+ parameters: [comparisonMatch[4]],
592
+ },
593
+ };
594
+ }
595
+
596
+ throw new GoogleSheetsParseError(`Unable to parse WHERE expression: ${expression}`)
597
+ }
598
+
599
+ private splitLogicalExpression(expression: string, operator: 'AND' | 'OR'): string[]
600
+ {
601
+ const parts: string[] = [];
602
+
603
+ let depth = 0;
604
+ let start = 0;
605
+
606
+ for (let index = 0; index < expression.length; index++)
607
+ {
608
+ const character = expression[index];
609
+
610
+ if (character === '(')
611
+ {
612
+ depth++;
613
+ continue;
614
+ }
615
+
616
+ if (character === ')')
617
+ {
618
+ depth--;
619
+ continue;
620
+ }
621
+
622
+ if (depth !== 0) continue;
623
+
624
+ const remaining = expression.slice(index);
625
+
626
+ const match = remaining.match(new RegExp(`^${operator}\\b`, 'i'));
627
+
628
+ if (!match) continue;
629
+
630
+ /**
631
+ * BETWEEN contains its own AND:
632
+ *
633
+ * id BETWEEN :min AND :max
634
+ *
635
+ * That AND is not a logical AND.
636
+ */
637
+
638
+ if (operator === 'AND')
639
+ {
640
+ const before = expression.slice(start, index).trim();
641
+
642
+ if (/\bBETWEEN\s+:[a-zA-Z_][a-zA-Z0-9_]*$/i.test(before)) continue;
643
+ }
644
+
645
+ parts.push(expression.slice(start, index).trim(),
646
+ );
647
+
648
+ index += match[0].length - 1;
649
+ start = index + 1;
650
+ }
651
+
652
+ parts.push(expression.slice(start).trim());
653
+
654
+ return parts.filter((part) => part.length > 0);
655
+ }
656
+
657
+ private unwrapOuterParentheses(expression: string): string
658
+ {
659
+ let result = expression.trim();
660
+
661
+ while (result.startsWith('(') && result.endsWith(')') && this.isWrappedByOuterParentheses(result))
662
+ {
663
+ result = result.slice(1, -1).trim();
664
+ }
665
+
666
+ return result;
667
+ }
668
+
669
+ private isWrappedByOuterParentheses(expression: string): boolean
670
+ {
671
+ let depth = 0;
672
+
673
+ for (let index = 0; index < expression.length; index++)
674
+ {
675
+ const character = expression[index];
676
+
677
+ if (character === '(') depth++;
678
+ else if (character === ')')
679
+ {
680
+ depth--;
681
+
682
+ if (depth === 0 && index !== expression.length - 1) return false;
683
+ }
684
+ }
685
+
686
+ return depth === 0;
687
+ }
688
+
689
+ extractColumnName(expression: string): string
690
+ {
691
+ const match = expression.match(/^"[^"]+"\."([^"]+)"$/i);
692
+
693
+ if (match?.[1]) return match[1];
694
+
695
+ return expression.replace(/^"|"$/g, '');
696
+ }
697
+
698
+ evaluateAggregate(rows: Record<string, unknown>[], aggregate: AggregateColumn): number | null
699
+ {
700
+ if (aggregate.function === 'COUNT')
701
+ {
702
+ /**
703
+ * COUNT(*) / COUNT(1)
704
+ *
705
+ * TypeORM uses COUNT(1) for repository.count().
706
+ * Both forms count rows, not a column value.
707
+ */
708
+
709
+ if (aggregate.argument === '*' || /^\d+$/.test(aggregate.argument)) return rows.length;
710
+ }
711
+
712
+ const columnMatch = aggregate.argument.match(/^"?([^".]+)"?\."?([^".]+)"?$/);
713
+ const tableAlias = columnMatch?.[1];
714
+ const column = columnMatch?.[2] ?? this.extractColumnName(aggregate.argument);
715
+
716
+ const getValue = (row: Record<string, unknown>): unknown =>
717
+ {
718
+ if (tableAlias)
719
+ {
720
+ const tableRow = row[tableAlias];
721
+
722
+ if (tableRow && typeof tableRow === 'object') return (tableRow as Record<string, unknown>)[column];
723
+
724
+ return undefined;
725
+ }
726
+
727
+ return row[column];
728
+ };
729
+
730
+ const values = rows
731
+ .map((row) => getValue(row))
732
+ .filter((value): value is string | number => value !== null && value !== undefined);
733
+
734
+ if (aggregate.function === 'COUNT') return values.length;
735
+ if (values.length === 0) return null;
736
+
737
+ const numericValues = values
738
+ .map((value) => typeof value === 'number' ? value : Number(value))
739
+ .filter((value) => Number.isFinite(value));
740
+
741
+ if (numericValues.length === 0) return null;
742
+
743
+ switch (aggregate.function)
744
+ {
745
+ case 'SUM': return numericValues.reduce((sum, value) => sum + value, 0);
746
+ case 'AVG': return numericValues.reduce((sum, value) => sum + value, 0) / numericValues.length;
747
+ case 'MIN': return Math.min(...numericValues);
748
+ case 'MAX': return Math.max(...numericValues);
749
+ default: throw new GoogleSheetsUnsupportedOperationError(`Unsupported aggregate function: ${aggregate.function}`)
750
+ }
751
+ }
752
+
753
+ evaluateWhere(value: unknown, operator: WhereOperator, parameters: unknown[]): boolean
754
+ {
755
+ switch (operator)
756
+ {
757
+ case '=': return this.compareValues(value, parameters[0]);
758
+ case '!=':
759
+ case '<>':
760
+ return value !== parameters[0];
761
+ case '>':
762
+ return value !== undefined &&
763
+ value !== null &&
764
+ parameters[0] !== undefined &&
765
+ parameters[0] !== null &&
766
+ value > parameters[0];
767
+ case '>=':
768
+ return value !== undefined &&
769
+ value !== null &&
770
+ parameters[0] !== undefined &&
771
+ parameters[0] !== null &&
772
+ value >= parameters[0];
773
+ case '<':
774
+ return value !== undefined &&
775
+ value !== null &&
776
+ parameters[0] !== undefined &&
777
+ parameters[0] !== null &&
778
+ value < parameters[0];
779
+ case '<=':
780
+ return value !== undefined &&
781
+ value !== null &&
782
+ parameters[0] !== undefined &&
783
+ parameters[0] !== null &&
784
+ value <= parameters[0];
785
+ case 'IS NULL': return (value === null || value === undefined);
786
+ case 'IS NOT NULL': return (value !== null && value !== undefined);
787
+ case 'IN': return parameters.some((parameter) => this.compareValues(value, parameter));
788
+ case 'NOT IN': return !parameters.some((parameter) => this.compareValues(value, parameter));
789
+ case 'ANY':
790
+ {
791
+ const values = parameters[0];
792
+
793
+ if (!Array.isArray(values)) return false;
794
+
795
+ return values.some(
796
+ (parameter) => this.compareValues(value, parameter),
797
+ );
798
+ }
799
+ case 'LIKE': return this.evaluateLike(value, parameters[0], false);
800
+ case 'NOT LIKE': return !this.evaluateLike(value, parameters[0], false);
801
+ case 'ILIKE': return this.evaluateLike(value, parameters[0], true);
802
+ case 'NOT ILIKE': return !this.evaluateLike(value, parameters[0], true);
803
+ case 'BETWEEN':
804
+ return value !== undefined &&
805
+ value !== null &&
806
+ parameters[0] !== undefined &&
807
+ parameters[0] !== null &&
808
+ parameters[1] !== undefined &&
809
+ parameters[1] !== null &&
810
+ value >= parameters[0] &&
811
+ value <= parameters[1];
812
+ default: throw new GoogleSheetsUnsupportedOperationError(`Unsupported operator: ${operator}`)
813
+ }
814
+ }
815
+
816
+ private evaluateLike(value: unknown, pattern: unknown, caseInsensitive: boolean): boolean
817
+ {
818
+ if (typeof value !== 'string' || typeof pattern !== 'string') return false;
819
+
820
+ const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
821
+
822
+ const regexPattern = `^${escapedPattern.replaceAll('%', '.*').replaceAll('_', '.')}$`;
823
+
824
+ return new RegExp(regexPattern, caseInsensitive ? 'i' : '').test(value);
825
+ }
826
+
827
+ evaluateWhereExpression(
828
+ expression: WhereExpression,
829
+ resolveValue: (
830
+ tableAlias: string,
831
+ column: string,
832
+ ) => unknown,
833
+ resolveParameter: (
834
+ parameter: string,
835
+ ) => unknown,
836
+ ): boolean
837
+ {
838
+ switch (expression.type)
839
+ {
840
+ case 'condition':
841
+ {
842
+ const condition = expression.condition;
843
+
844
+ const parameters = condition.parameters.map((parameter) => resolveParameter(parameter)).flat();
845
+
846
+ return this.evaluateWhere(
847
+ resolveValue(
848
+ condition.tableAlias,
849
+ condition.column,
850
+ ),
851
+ condition.operator,
852
+ parameters,
853
+ );
854
+ }
855
+
856
+ case 'and':
857
+ return expression.expressions.every(
858
+ (expression) => this.evaluateWhereExpression(expression, resolveValue, resolveParameter)
859
+ );
860
+
861
+ case 'or':
862
+ return expression.expressions.some(
863
+ (expression) => this.evaluateWhereExpression(expression, resolveValue, resolveParameter)
864
+ );
865
+
866
+ case 'not': return !this.evaluateWhereExpression(expression.expression, resolveValue, resolveParameter);
867
+ default: throw new GoogleSheetsUnsupportedOperationError(`Unsupported operator: ${expression}`)
868
+ }
869
+ }
870
+
871
+ private compareValues(left: unknown, right: unknown): boolean
872
+ {
873
+ if (left === null || left === undefined || right === null || right === undefined)
874
+ {
875
+ return left === right;
876
+ }
877
+
878
+ if (typeof left === 'number' && typeof right === 'string')
879
+ {
880
+ return left === Number(right);
881
+ }
882
+
883
+ if (typeof left === 'string' && typeof right === 'number')
884
+ {
885
+ return Number(left) === right;
886
+ }
887
+
888
+ return left === right;
889
+ }
890
+
891
+ parseOrderBy(query: string): OrderBy[]
892
+ {
893
+ const match = query.match(/\bORDER\s+BY\s+(.+?)(?:\s+LIMIT\s+\d+|\s+OFFSET\s+\d+|$)/i);
894
+
895
+ if (!match?.[1]) return [];
896
+
897
+ return match[1]
898
+ .split(',')
899
+ .map(
900
+ (expression) =>
901
+ {
902
+ const trimmedExpression = expression.trim();
903
+
904
+ // ORDER BY "table"."column" ASC
905
+ const columnMatch = trimmedExpression.match(/^"([^"]+)"\."([^"]+)"(?:\s+(ASC|DESC))?$/i);
906
+
907
+ if (columnMatch?.[1] && columnMatch[2])
908
+ {
909
+ return {
910
+ tableAlias: columnMatch[1],
911
+ column: columnMatch[2],
912
+ direction: (columnMatch[3]?.toUpperCase() ?? 'ASC') as OrderDirection,
913
+ };
914
+ }
915
+
916
+ // ORDER BY "table_column" ASC
917
+ const aliasMatch = trimmedExpression.match(/^"([^"]+)"(?:\s+(ASC|DESC))?$/i);
918
+
919
+ if (aliasMatch?.[1])
920
+ {
921
+ const alias = aliasMatch[1];
922
+ const separatorIndex = alias.lastIndexOf('_');
923
+
924
+ if (separatorIndex > 0)
925
+ {
926
+ return {
927
+ tableAlias: alias.slice(0, separatorIndex),
928
+ column: alias.slice(separatorIndex + 1),
929
+ direction: (aliasMatch[2]?.toUpperCase() ?? 'ASC') as OrderDirection
930
+ };
931
+ }
932
+ }
933
+
934
+ throw new GoogleSheetsParseError(`Unable to parse ORDER BY expression: ${expression}`)
935
+ },
936
+ );
937
+ }
938
+
939
+ parsePagination(query: string): Pagination
940
+ {
941
+ const limitMatch = query.match(/\bLIMIT\s+(\d+)/i);
942
+ const offsetMatch = query.match(/\bOFFSET\s+(\d+)/i);
943
+
944
+ return {
945
+ limit: limitMatch ? Number(limitMatch[1]) : undefined,
946
+ offset: offsetMatch ? Number(offsetMatch[1]) : undefined,
947
+ };
948
+ }
949
+ }