forge-sql-orm 2.1.15 → 2.1.16

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 (48) hide show
  1. package/README.md +4 -0
  2. package/dist/core/ForgeSQLQueryBuilder.d.ts +2 -3
  3. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  4. package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
  5. package/dist/core/ForgeSQLSelectOperations.d.ts +2 -1
  6. package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
  7. package/dist/core/ForgeSQLSelectOperations.js.map +1 -1
  8. package/dist/core/Rovo.d.ts +40 -0
  9. package/dist/core/Rovo.d.ts.map +1 -1
  10. package/dist/core/Rovo.js +164 -138
  11. package/dist/core/Rovo.js.map +1 -1
  12. package/dist/index.d.ts +1 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
  17. package/dist/lib/drizzle/extensions/additionalActions.js +72 -22
  18. package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -1
  19. package/dist/utils/cacheTableUtils.d.ts +11 -0
  20. package/dist/utils/cacheTableUtils.d.ts.map +1 -0
  21. package/dist/utils/cacheTableUtils.js +450 -0
  22. package/dist/utils/cacheTableUtils.js.map +1 -0
  23. package/dist/utils/cacheUtils.d.ts.map +1 -1
  24. package/dist/utils/cacheUtils.js +3 -22
  25. package/dist/utils/cacheUtils.js.map +1 -1
  26. package/dist/utils/forgeDriver.d.ts.map +1 -1
  27. package/dist/utils/forgeDriver.js +5 -12
  28. package/dist/utils/forgeDriver.js.map +1 -1
  29. package/dist/utils/metadataContextUtils.d.ts.map +1 -1
  30. package/dist/utils/metadataContextUtils.js +53 -31
  31. package/dist/utils/metadataContextUtils.js.map +1 -1
  32. package/dist/utils/sqlUtils.d.ts +1 -0
  33. package/dist/utils/sqlUtils.d.ts.map +1 -1
  34. package/dist/utils/sqlUtils.js +217 -119
  35. package/dist/utils/sqlUtils.js.map +1 -1
  36. package/dist/webtriggers/applyMigrationsWebTrigger.js +1 -1
  37. package/package.json +9 -9
  38. package/src/core/ForgeSQLQueryBuilder.ts +2 -2
  39. package/src/core/ForgeSQLSelectOperations.ts +2 -1
  40. package/src/core/Rovo.ts +209 -167
  41. package/src/index.ts +1 -3
  42. package/src/lib/drizzle/extensions/additionalActions.ts +98 -42
  43. package/src/utils/cacheTableUtils.ts +511 -0
  44. package/src/utils/cacheUtils.ts +3 -25
  45. package/src/utils/forgeDriver.ts +5 -11
  46. package/src/utils/metadataContextUtils.ts +49 -26
  47. package/src/utils/sqlUtils.ts +298 -142
  48. package/src/webtriggers/applyMigrationsWebTrigger.ts +1 -1
@@ -536,6 +536,96 @@ async function handleNonCachedQuery(
536
536
  }
537
537
  }
538
538
 
539
+ /**
540
+ * Creates a select query builder with field aliasing and optional caching support.
541
+ *
542
+ * @param db - The database instance
543
+ * @param fields - The fields to select with aliases
544
+ * @param selectFn - Function to create the base select query
545
+ * @param useCache - Whether to enable caching for this query
546
+ * @param options - ForgeSQL ORM options
547
+ * @param cacheTtl - Optional cache TTL override
548
+ * @returns Select query builder with aliasing and optional caching
549
+ */
550
+ /**
551
+ * Creates a catch handler for Promise-like objects
552
+ */
553
+ function createCatchHandler(receiver: any): (onrejected: any) => Promise<any> {
554
+ return (onrejected: any) => receiver.then(undefined, onrejected);
555
+ }
556
+
557
+ /**
558
+ * Creates a finally handler for Promise-like objects
559
+ */
560
+ function createFinallyHandler(receiver: any): (onfinally: any) => Promise<any> {
561
+ return (onfinally: any) => {
562
+ const handleFinally = (value: any) => Promise.resolve(value).finally(onfinally);
563
+ const handleReject = (reason: any) => Promise.reject(reason).finally(onfinally);
564
+ return receiver.then(handleFinally, handleReject);
565
+ };
566
+ }
567
+
568
+ /**
569
+ * Creates a then handler for cached queries
570
+ */
571
+ function createCachedThenHandler(
572
+ target: any,
573
+ options: ForgeSqlOrmOptions,
574
+ cacheTtl: number | undefined,
575
+ selections: any,
576
+ aliasMap: any,
577
+ ): (onfulfilled?: any, onrejected?: any) => Promise<any> {
578
+ return (onfulfilled?: any, onrejected?: any) => {
579
+ const ttl = cacheTtl ?? options.cacheTTL ?? 120;
580
+ return handleCachedQuery(target, options, ttl, selections, aliasMap, onfulfilled, onrejected);
581
+ };
582
+ }
583
+
584
+ /**
585
+ * Creates a then handler for non-cached queries
586
+ */
587
+ function createNonCachedThenHandler(
588
+ target: any,
589
+ options: ForgeSqlOrmOptions,
590
+ selections: any,
591
+ aliasMap: any,
592
+ ): (onfulfilled?: any, onrejected?: any) => Promise<any> {
593
+ return (onfulfilled?: any, onrejected?: any) => {
594
+ return handleNonCachedQuery(target, options, selections, aliasMap, onfulfilled, onrejected);
595
+ };
596
+ }
597
+
598
+ /**
599
+ * Creates an execute handler that transforms results
600
+ */
601
+ function createExecuteHandler(
602
+ target: any,
603
+ selections: any,
604
+ aliasMap: any,
605
+ ): (...args: any[]) => Promise<any> {
606
+ return async (...args: any[]) => {
607
+ const rows = await target.execute(...args);
608
+ return applyFromDriverTransform(rows, selections, aliasMap);
609
+ };
610
+ }
611
+
612
+ /**
613
+ * Creates a function call handler that wraps results
614
+ */
615
+ function createFunctionCallHandler(
616
+ value: Function,
617
+ target: any,
618
+ wrapBuilder: (rawBuilder: any) => any,
619
+ ): (...args: any[]) => any {
620
+ return (...args: any[]) => {
621
+ const result = value.apply(target, args);
622
+ if (typeof result === "object" && result !== null && "execute" in result) {
623
+ return wrapBuilder(result);
624
+ }
625
+ return result;
626
+ };
627
+ }
628
+
539
629
  /**
540
630
  * Creates a select query builder with field aliasing and optional caching support.
541
631
  *
@@ -562,61 +652,27 @@ function createAliasedSelectBuilder<TSelection extends SelectedFields>(
562
652
  return new Proxy(rawBuilder, {
563
653
  get(target, prop, receiver) {
564
654
  if (prop === "execute") {
565
- return async (...args: any[]) => {
566
- const rows = await target.execute(...args);
567
- return applyFromDriverTransform(rows, selections, aliasMap);
568
- };
655
+ return createExecuteHandler(target, selections, aliasMap);
569
656
  }
570
657
 
571
658
  if (prop === "then") {
572
- return (onfulfilled?: any, onrejected?: any) => {
573
- if (useCache) {
574
- const ttl = cacheTtl ?? options.cacheTTL ?? 120;
575
- return handleCachedQuery(
576
- target,
577
- options,
578
- ttl,
579
- selections,
580
- aliasMap,
581
- onfulfilled,
582
- onrejected,
583
- );
584
- } else {
585
- return handleNonCachedQuery(
586
- target,
587
- options,
588
- selections,
589
- aliasMap,
590
- onfulfilled,
591
- onrejected,
592
- );
593
- }
594
- };
659
+ return useCache
660
+ ? createCachedThenHandler(target, options, cacheTtl, selections, aliasMap)
661
+ : createNonCachedThenHandler(target, options, selections, aliasMap);
595
662
  }
663
+
596
664
  if (prop === "catch") {
597
- return (onrejected: any) => (receiver as any).then(undefined, onrejected);
665
+ return createCatchHandler(receiver);
598
666
  }
599
667
 
600
668
  if (prop === "finally") {
601
- return (onfinally: any) =>
602
- (receiver as any).then(
603
- (value: any) => Promise.resolve(value).finally(onfinally),
604
- (reason: any) => Promise.reject(reason).finally(onfinally),
605
- );
669
+ return createFinallyHandler(receiver);
606
670
  }
607
671
 
608
672
  const value = Reflect.get(target, prop, receiver);
609
673
 
610
674
  if (typeof value === "function") {
611
- return (...args: any[]) => {
612
- const result = value.apply(target, args);
613
-
614
- if (typeof result === "object" && result !== null && "execute" in result) {
615
- return wrapBuilder(result);
616
- }
617
-
618
- return result;
619
- };
675
+ return createFunctionCallHandler(value, target, wrapBuilder);
620
676
  }
621
677
 
622
678
  return value;
@@ -0,0 +1,511 @@
1
+ import { Parser } from "node-sql-parser";
2
+ import { ForgeSqlOrmOptions } from "../core/ForgeSQLQueryBuilder";
3
+
4
+ /**
5
+ * Extracts table name from object value.
6
+ */
7
+ function extractTableNameFromObject(value: any, context?: string): string | null {
8
+ // If it's an array, skip it (not a table name)
9
+ if (Array.isArray(value)) {
10
+ return null;
11
+ }
12
+ // Handle backticks_quote_string type only for node.table context
13
+ if (
14
+ context?.includes("node.table") &&
15
+ value.type === "backticks_quote_string" &&
16
+ typeof value.value === "string"
17
+ ) {
18
+ return value.value === "dual" ? null : value.value.toLowerCase();
19
+ }
20
+ // Try value.name first (most common)
21
+ if (typeof value.name === "string") {
22
+ return value.name === "dual" ? null : value.name.toLowerCase();
23
+ }
24
+ // Try value.table if it's a nested structure
25
+ if (value.table) {
26
+ return normalizeTableName(value.table, context);
27
+ }
28
+ // Log when we encounter an object that we can't extract table name from
29
+ // eslint-disable-next-line no-console
30
+ console.warn(
31
+ `[cacheTableUtils] Unable to extract table name from object:`,
32
+ JSON.stringify(value, null, 2),
33
+ context ? `(context: ${context})` : "",
34
+ );
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Helper function to safely convert to string and lowercase.
40
+ */
41
+ function normalizeTableName(value: any, context?: string): string | null {
42
+ if (!value) {
43
+ return null;
44
+ }
45
+ // If it's already a string, use it
46
+ if (typeof value === "string") {
47
+ return value === "dual" ? null : value.toLowerCase();
48
+ }
49
+ // If it's an object, try to extract name from various properties
50
+ if (typeof value === "object") {
51
+ return extractTableNameFromObject(value, context);
52
+ }
53
+ // For other types (number, boolean, etc.), log and don't treat as table name
54
+ // eslint-disable-next-line no-console
55
+ console.warn(
56
+ `[cacheTableUtils] Unexpected table name type:`,
57
+ typeof value,
58
+ value,
59
+ context ? `(context: ${context})` : "",
60
+ );
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Checks if a node is a column reference alias.
66
+ */
67
+ function isColumnRefAlias(node: any): boolean {
68
+ return node.type === "column_ref" && !node.table;
69
+ }
70
+
71
+ /**
72
+ * Checks if a node is an explicit alias (has 'as' property but is not a table node).
73
+ */
74
+ function isExplicitAlias(node: any): boolean {
75
+ return Boolean(node.as && node.type !== "table" && node.type !== "dual" && !node.table);
76
+ }
77
+
78
+ /**
79
+ * Checks if a node has a short name that is likely an alias.
80
+ */
81
+ function isShortNameAlias(node: any): boolean {
82
+ if (!node.name || node.table || node.type === "table" || node.type === "dual") {
83
+ return false;
84
+ }
85
+ const nameStr = typeof node.name === "string" ? node.name : node.name?.name || node.name?.value;
86
+ return typeof nameStr === "string" && nameStr.length <= 2;
87
+ }
88
+
89
+ /**
90
+ * Checks if a node is likely an alias (not a real table).
91
+ */
92
+ function isLikelyAlias(node: any): boolean {
93
+ return isColumnRefAlias(node) || isExplicitAlias(node) || isShortNameAlias(node);
94
+ }
95
+
96
+ /**
97
+ * Extracts table name from table node.
98
+ *
99
+ * @param node - AST node with table information
100
+ * @returns Table name in lowercase or null if not applicable
101
+ */
102
+ function extractTableName(node: any): string | null {
103
+ if (!node) {
104
+ return null;
105
+ }
106
+
107
+ // Early return for likely aliases
108
+ if (isLikelyAlias(node)) {
109
+ return null;
110
+ }
111
+
112
+ // Handle table node directly
113
+ if (node.type === "table" || node.type === "dual") {
114
+ const fromTable = node.table
115
+ ? normalizeTableName(node.table, `node.type=${node.type}, node.table`)
116
+ : null;
117
+ if (fromTable) {
118
+ return fromTable;
119
+ }
120
+ const fromName = node.name
121
+ ? normalizeTableName(node.name, `node.type=${node.type}, node.name`)
122
+ : null;
123
+ if (fromName) {
124
+ return fromName;
125
+ }
126
+ return null;
127
+ }
128
+
129
+ // Handle table reference in different formats
130
+ if (node.table) {
131
+ const tableName = normalizeTableName(node.table, `node.table (type: ${node.type})`);
132
+ if (tableName) {
133
+ return tableName;
134
+ }
135
+ }
136
+
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * Processes and adds table name to the set if valid.
142
+ */
143
+ function processTableName(node: any, tableName: string, tables: Set<string>): void {
144
+ // Filter out a_ prefixed names (field aliases)
145
+ if (tableName.startsWith("a_")) {
146
+ return;
147
+ }
148
+
149
+ // Filter out short names that are likely table aliases (u, us, o, oi, etc.)
150
+ // Only filter if it's not a real table node (type === "table" or "dual")
151
+ const isRealTableNode = node.type === "table" || node.type === "dual";
152
+ if (!isRealTableNode && tableName.length <= 2) {
153
+ return;
154
+ }
155
+
156
+ tables.add(tableName);
157
+ }
158
+
159
+ /**
160
+ * Extracts table name from node and adds to set if valid.
161
+ */
162
+ function extractAndAddTableName(node: any, tables: Set<string>): void {
163
+ const tableName = extractTableName(node);
164
+ if (tableName && tableName.length > 0) {
165
+ processTableName(node, tableName, tables);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Processes CTE (Common Table Expressions) - WITH clause.
171
+ */
172
+ function processCTE(node: any, tables: Set<string>): void {
173
+ if (!node.with && !node.with_list) {
174
+ return;
175
+ }
176
+ const withClauses = node.with_list || (Array.isArray(node.with) ? node.with : [node.with]);
177
+ withClauses.forEach((cte: any) => {
178
+ if (cte?.stmt) {
179
+ extractTablesFromNode(cte.stmt, tables);
180
+ }
181
+ if (cte?.as?.stmt) {
182
+ extractTablesFromNode(cte.as.stmt, tables);
183
+ }
184
+ });
185
+ }
186
+
187
+ /**
188
+ * Processes FROM and JOIN clauses.
189
+ */
190
+ function processFromAndJoin(node: any, tables: Set<string>): void {
191
+ if (node.from) {
192
+ if (Array.isArray(node.from)) {
193
+ node.from.forEach((item: any) => extractTablesFromNode(item, tables));
194
+ } else {
195
+ extractTablesFromNode(node.from, tables);
196
+ }
197
+ }
198
+
199
+ if (node.join) {
200
+ if (Array.isArray(node.join)) {
201
+ node.join.forEach((item: any) => extractTablesFromNode(item, tables));
202
+ } else {
203
+ extractTablesFromNode(node.join, tables);
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Processes SELECT columns that may contain subqueries.
210
+ */
211
+ function processSelectColumns(node: any, tables: Set<string>): void {
212
+ const columns = node.columns || node.select;
213
+ if (!columns) {
214
+ return;
215
+ }
216
+
217
+ if (Array.isArray(columns)) {
218
+ columns.forEach((col: any) => {
219
+ if (!col) return;
220
+
221
+ // If the column itself is a subquery
222
+ if (col.type === "subquery" || col.type === "select") {
223
+ extractTablesFromNode(col, tables);
224
+ }
225
+
226
+ // Process expression (may contain subqueries)
227
+ if (col.expr) {
228
+ extractTablesFromNode(col.expr, tables);
229
+ }
230
+
231
+ // Process AST (alternative structure for subqueries)
232
+ if (col.ast) {
233
+ extractTablesFromNode(col.ast, tables);
234
+ }
235
+ });
236
+ } else if (typeof columns === "object") {
237
+ extractTablesFromNode(columns, tables);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Processes ORDER BY or GROUP BY clause.
243
+ */
244
+ function processOrderByOrGroupBy(clause: any, tables: Set<string>): void {
245
+ if (!clause) {
246
+ return;
247
+ }
248
+ if (Array.isArray(clause)) {
249
+ clause.forEach((item: any) => {
250
+ if (item?.expr) {
251
+ extractTablesFromNode(item.expr, tables);
252
+ }
253
+ extractTablesFromNode(item, tables);
254
+ });
255
+ } else {
256
+ extractTablesFromNode(clause, tables);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Processes UNION operations.
262
+ */
263
+ function processUnionNode(unionNode: any, tables: Set<string>): void {
264
+ if (!unionNode) {
265
+ return;
266
+ }
267
+
268
+ const isUnionType =
269
+ unionNode.type === "select" ||
270
+ unionNode.type === "union" ||
271
+ unionNode.type === "union_all" ||
272
+ unionNode.type === "union_distinct" ||
273
+ unionNode.type === "intersect" ||
274
+ unionNode.type === "except" ||
275
+ unionNode.type === "minus";
276
+
277
+ if (isUnionType) {
278
+ extractTablesFromNode(unionNode, tables);
279
+ } else if (unionNode.select) {
280
+ extractTablesFromNode(unionNode.select, tables);
281
+ } else if (unionNode.ast) {
282
+ extractTablesFromNode(unionNode.ast, tables);
283
+ } else {
284
+ extractTablesFromNode(unionNode, tables);
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Processes UNION/UNION ALL/UNION DISTINCT/INTERSECT/EXCEPT/MINUS clauses.
290
+ */
291
+ function processUnion(node: any, tables: Set<string>): void {
292
+ if (!node.union) {
293
+ return;
294
+ }
295
+
296
+ if (Array.isArray(node.union)) {
297
+ node.union.forEach((unionNode: any) => processUnionNode(unionNode, tables));
298
+ } else if (typeof node.union === "object") {
299
+ processUnionNode(node.union, tables);
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Processes UNION/INTERSECT/EXCEPT operation nodes.
305
+ */
306
+ function processUnionOperation(node: any, tables: Set<string>): void {
307
+ const isUnionOperation =
308
+ node.type === "union" ||
309
+ node.type === "union_all" ||
310
+ node.type === "union_distinct" ||
311
+ node.type === "intersect" ||
312
+ node.type === "except" ||
313
+ node.type === "minus";
314
+
315
+ if (!isUnionOperation) {
316
+ return;
317
+ }
318
+
319
+ if (node.left) {
320
+ extractTablesFromNode(node.left, tables);
321
+ }
322
+ if (node.right) {
323
+ extractTablesFromNode(node.right, tables);
324
+ }
325
+ extractTablesFromNode(node, tables);
326
+ }
327
+
328
+ /**
329
+ * Processes _next property (alternative UNION structure).
330
+ */
331
+ function processNext(node: any, tables: Set<string>): void {
332
+ if (!node._next) {
333
+ return;
334
+ }
335
+ if (Array.isArray(node._next)) {
336
+ node._next.forEach((nextNode: any) => extractTablesFromNode(nextNode, tables));
337
+ } else {
338
+ extractTablesFromNode(node._next, tables);
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Recursively processes all object properties for any remaining nested structures.
344
+ */
345
+ function processRecursively(node: any, tables: Set<string>): void {
346
+ const isLikelyAlias =
347
+ (node.type === "column_ref" && !node.table) ||
348
+ (node.name &&
349
+ !node.table &&
350
+ node.type !== "table" &&
351
+ node.type !== "dual" &&
352
+ node.name.length <= 2);
353
+
354
+ if (isLikelyAlias || Array.isArray(node)) {
355
+ return;
356
+ }
357
+
358
+ Object.values(node).forEach((value) => {
359
+ if (value && typeof value === "object") {
360
+ if (Array.isArray(value)) {
361
+ value.forEach((item: any) => {
362
+ if (item && typeof item === "object") {
363
+ extractTablesFromNode(item, tables);
364
+ }
365
+ });
366
+ } else {
367
+ extractTablesFromNode(value, tables);
368
+ }
369
+ }
370
+ });
371
+ }
372
+
373
+ /**
374
+ * Recursively extracts table names from SQL AST node.
375
+ * Handles regular tables, CTEs, subqueries, and complex query structures.
376
+ *
377
+ * @param node - AST node to extract tables from
378
+ * @param tables - Accumulator set for table names
379
+ */
380
+ function extractTablesFromNode(node: any, tables: Set<string>): void {
381
+ if (!node || typeof node !== "object") {
382
+ return;
383
+ }
384
+
385
+ // Extract table name if node is a table type
386
+ extractAndAddTableName(node, tables);
387
+
388
+ // Handle CTE (Common Table Expressions) - WITH clause
389
+ processCTE(node, tables);
390
+
391
+ // Extract tables from FROM and JOIN clauses
392
+ processFromAndJoin(node, tables);
393
+
394
+ // Handle subqueries explicitly
395
+ if (node.type === "subquery" || node.type === "select") {
396
+ if (node.ast) {
397
+ extractTablesFromNode(node.ast, tables);
398
+ }
399
+ if (node.from) {
400
+ extractTablesFromNode(node.from, tables);
401
+ }
402
+ }
403
+
404
+ // Extract tables from WHERE clause (may contain subqueries)
405
+ if (node.where) {
406
+ extractTablesFromNode(node.where, tables);
407
+ }
408
+
409
+ // Extract tables from SELECT columns (may contain subqueries)
410
+ processSelectColumns(node, tables);
411
+
412
+ // Extract tables from HAVING clause (may contain subqueries)
413
+ if (node.having) {
414
+ extractTablesFromNode(node.having, tables);
415
+ }
416
+
417
+ // Extract tables from ORDER BY clause (may contain subqueries)
418
+ processOrderByOrGroupBy(node.orderby || node.order_by, tables);
419
+
420
+ // Extract tables from GROUP BY clause (may contain subqueries)
421
+ processOrderByOrGroupBy(node.groupby || node.group_by, tables);
422
+
423
+ // Extract tables from UPDATE statement
424
+ if (node.type === "update" && node.table) {
425
+ extractTablesFromNode(node.table, tables);
426
+ }
427
+
428
+ // Extract tables from INSERT statement
429
+ if (node.type === "insert" && node.table) {
430
+ extractTablesFromNode(node.table, tables);
431
+ }
432
+
433
+ // Extract tables from DELETE statement
434
+ if (node.type === "delete" && node.from) {
435
+ extractTablesFromNode(node.from, tables);
436
+ }
437
+
438
+ // Extract tables from UNION operations
439
+ processUnion(node, tables);
440
+
441
+ // Handle node types for UNION/INTERSECT/EXCEPT operations
442
+ processUnionOperation(node, tables);
443
+
444
+ // Handle _next property (alternative UNION structure)
445
+ processNext(node, tables);
446
+
447
+ // Recursively process all object properties
448
+ processRecursively(node, tables);
449
+ }
450
+
451
+ /**
452
+ * Extracts all table names from SQL query using node-sql-parser, with regex fallback.
453
+ * Returns them as comma-separated string in format `table1`,`table2`.
454
+ *
455
+ * @param sql - SQL query string
456
+ * @param options - ForgeSQL ORM options for logging
457
+ * @returns Comma-separated string of unique table names in backticks
458
+ */
459
+ export function extractBacktickedValues(sql: string, options: ForgeSqlOrmOptions): string {
460
+ // Try to use node-sql-parser first
461
+ try {
462
+ const parser = new Parser();
463
+ const ast = parser.astify(sql.trim());
464
+
465
+ const tables = new Set<string>();
466
+
467
+ // Handle both single statement and multiple statements
468
+ const statements = Array.isArray(ast) ? ast : [ast];
469
+ statements.forEach((statement) => {
470
+ extractTablesFromNode(statement, tables);
471
+ });
472
+
473
+ if (tables.size > 0) {
474
+ // Sort to ensure consistent order for the same input
475
+ const backtickedValues = Array.from(tables)
476
+ .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base", numeric: true }))
477
+ .map((table) => `\`${table}\``)
478
+ .join(",");
479
+ if (options.logCache) {
480
+ // eslint-disable-next-line no-console
481
+ console.warn(`Extracted backticked values: ${backtickedValues}`);
482
+ }
483
+ return backtickedValues;
484
+ }
485
+ } catch (error) {
486
+ if (options.logCache) {
487
+ // eslint-disable-next-line no-console
488
+ console.error(
489
+ `Error extracting backticked values: ${error}. Using regex-based extraction instead.`,
490
+ );
491
+ }
492
+ // If parsing fails, fall back to regex-based extraction
493
+ // This handles cases where node-sql-parser doesn't support the SQL syntax
494
+ }
495
+
496
+ // Fallback to regex-based extraction (original logic)
497
+ const regex = /`([^`]+)`/g;
498
+ const matches = new Set<string>();
499
+ let match;
500
+
501
+ while ((match = regex.exec(sql.toLowerCase())) !== null) {
502
+ if (!match[1].startsWith("a_")) {
503
+ matches.add(`\`${match[1]}\``);
504
+ }
505
+ }
506
+
507
+ // Sort to ensure consistent order for the same input
508
+ return Array.from(matches)
509
+ .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base", numeric: true }))
510
+ .join(",");
511
+ }