forge-sql-orm 2.1.14 → 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.
- package/README.md +294 -20
- package/dist/core/ForgeSQLORM.d.ts +16 -7
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.js +73 -15
- package/dist/core/ForgeSQLORM.js.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +15 -7
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts +2 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.js.map +1 -1
- package/dist/core/Rovo.d.ts +40 -0
- package/dist/core/Rovo.d.ts.map +1 -1
- package/dist/core/Rovo.js +164 -138
- package/dist/core/Rovo.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.js +72 -22
- package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -1
- package/dist/utils/cacheTableUtils.d.ts +11 -0
- package/dist/utils/cacheTableUtils.d.ts.map +1 -0
- package/dist/utils/cacheTableUtils.js +450 -0
- package/dist/utils/cacheTableUtils.js.map +1 -0
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/cacheUtils.js +3 -22
- package/dist/utils/cacheUtils.js.map +1 -1
- package/dist/utils/forgeDriver.d.ts +3 -2
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/forgeDriver.js +24 -27
- package/dist/utils/forgeDriver.js.map +1 -1
- package/dist/utils/metadataContextUtils.d.ts +27 -1
- package/dist/utils/metadataContextUtils.d.ts.map +1 -1
- package/dist/utils/metadataContextUtils.js +237 -10
- package/dist/utils/metadataContextUtils.js.map +1 -1
- package/dist/utils/sqlUtils.d.ts +1 -0
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/utils/sqlUtils.js +217 -119
- package/dist/utils/sqlUtils.js.map +1 -1
- package/dist/webtriggers/applyMigrationsWebTrigger.js +1 -1
- package/dist/webtriggers/index.d.ts +1 -0
- package/dist/webtriggers/index.d.ts.map +1 -1
- package/dist/webtriggers/index.js +1 -0
- package/dist/webtriggers/index.js.map +1 -1
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +60 -0
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -0
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.js +55 -0
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.js.map +1 -0
- package/package.json +11 -10
- package/src/core/ForgeSQLORM.ts +78 -14
- package/src/core/ForgeSQLQueryBuilder.ts +15 -5
- package/src/core/ForgeSQLSelectOperations.ts +2 -1
- package/src/core/Rovo.ts +209 -167
- package/src/index.ts +1 -3
- package/src/lib/drizzle/extensions/additionalActions.ts +98 -42
- package/src/utils/cacheTableUtils.ts +511 -0
- package/src/utils/cacheUtils.ts +3 -25
- package/src/utils/forgeDriver.ts +38 -29
- package/src/utils/metadataContextUtils.ts +290 -10
- package/src/utils/sqlUtils.ts +298 -142
- package/src/webtriggers/applyMigrationsWebTrigger.ts +1 -1
- package/src/webtriggers/index.ts +1 -0
- package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +69 -0
|
@@ -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
|
|
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
|
|
573
|
-
|
|
574
|
-
|
|
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 (
|
|
665
|
+
return createCatchHandler(receiver);
|
|
598
666
|
}
|
|
599
667
|
|
|
600
668
|
if (prop === "finally") {
|
|
601
|
-
return (
|
|
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 (
|
|
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
|
+
}
|