@leonardovida-md/drizzle-neo-duckdb 1.2.0 → 1.2.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/dist/index.mjs CHANGED
@@ -1065,22 +1065,30 @@ function getTableSource(from) {
1065
1065
  if ("table" in from && from.table) {
1066
1066
  return {
1067
1067
  name: from.table,
1068
- alias: from.as ?? null
1068
+ alias: from.as ?? null,
1069
+ schema: "db" in from ? from.db ?? null : null
1069
1070
  };
1070
1071
  }
1071
1072
  if ("expr" in from && from.as) {
1072
1073
  return {
1073
1074
  name: from.as,
1074
- alias: from.as
1075
+ alias: from.as,
1076
+ schema: null
1075
1077
  };
1076
1078
  }
1077
1079
  return null;
1078
1080
  }
1079
1081
  function getQualifier(source) {
1080
- return source.alias ?? source.name;
1082
+ return {
1083
+ table: source.alias ?? source.name,
1084
+ schema: source.schema
1085
+ };
1081
1086
  }
1082
1087
  function isUnqualifiedColumnRef(expr) {
1083
- return typeof expr === "object" && expr !== null && "type" in expr && expr.type === "column_ref" && !(("table" in expr) && expr.table);
1088
+ return typeof expr === "object" && expr !== null && "type" in expr && expr.type === "column_ref" && (!("table" in expr) || !expr.table);
1089
+ }
1090
+ function isQualifiedColumnRef(expr) {
1091
+ return typeof expr === "object" && expr !== null && "type" in expr && expr.type === "column_ref" && "table" in expr && !!expr.table;
1084
1092
  }
1085
1093
  function getColumnName(col) {
1086
1094
  if (typeof col.column === "string") {
@@ -1091,29 +1099,71 @@ function getColumnName(col) {
1091
1099
  }
1092
1100
  return null;
1093
1101
  }
1094
- function walkOnClause(expr, leftSource, rightSource, ambiguousColumns) {
1102
+ function applyQualifier(col, qualifier) {
1103
+ col.table = qualifier.table;
1104
+ if (!("schema" in col) || !col.schema) {
1105
+ col.schema = qualifier.schema;
1106
+ }
1107
+ }
1108
+ function unwrapColumnRef(expr) {
1109
+ if (!expr || typeof expr !== "object")
1110
+ return null;
1111
+ if ("type" in expr && expr.type === "column_ref") {
1112
+ return expr;
1113
+ }
1114
+ if ("expr" in expr && expr.expr) {
1115
+ return unwrapColumnRef(expr.expr);
1116
+ }
1117
+ if ("ast" in expr && expr.ast && typeof expr.ast === "object") {
1118
+ return null;
1119
+ }
1120
+ if ("args" in expr && expr.args) {
1121
+ const args = expr.args;
1122
+ if (args.expr) {
1123
+ return unwrapColumnRef(args.expr);
1124
+ }
1125
+ if (args.value && args.value.length === 1) {
1126
+ return unwrapColumnRef(args.value[0]);
1127
+ }
1128
+ }
1129
+ return null;
1130
+ }
1131
+ function isBinaryExpr(expr) {
1132
+ return !!expr && typeof expr === "object" && "type" in expr && expr.type === "binary_expr";
1133
+ }
1134
+ function walkOnClause(expr, leftQualifier, rightQualifier, ambiguousColumns) {
1095
1135
  if (!expr || typeof expr !== "object")
1096
1136
  return false;
1097
1137
  let transformed = false;
1098
- if (expr.type === "binary_expr") {
1099
- if (expr.operator === "=") {
1100
- const left = expr.left;
1101
- const right = expr.right;
1102
- if (isUnqualifiedColumnRef(left) && isUnqualifiedColumnRef(right)) {
1103
- const leftColName = getColumnName(left);
1104
- const rightColName = getColumnName(right);
1105
- if (leftColName && rightColName && leftColName === rightColName) {
1106
- left.table = leftSource;
1107
- right.table = rightSource;
1108
- ambiguousColumns.add(leftColName);
1109
- transformed = true;
1110
- }
1138
+ if (isBinaryExpr(expr)) {
1139
+ const left = expr.left;
1140
+ const right = expr.right;
1141
+ const leftCol = unwrapColumnRef(left);
1142
+ const rightCol = unwrapColumnRef(right);
1143
+ const leftUnqualified = leftCol ? isUnqualifiedColumnRef(leftCol) : false;
1144
+ const rightUnqualified = rightCol ? isUnqualifiedColumnRef(rightCol) : false;
1145
+ const leftQualified = leftCol ? isQualifiedColumnRef(leftCol) : false;
1146
+ const rightQualified = rightCol ? isQualifiedColumnRef(rightCol) : false;
1147
+ const leftColName = leftCol ? getColumnName(leftCol) : null;
1148
+ const rightColName = rightCol ? getColumnName(rightCol) : null;
1149
+ if (expr.operator === "=" && leftColName && rightColName && leftColName === rightColName) {
1150
+ if (leftUnqualified && rightUnqualified) {
1151
+ applyQualifier(leftCol, leftQualifier);
1152
+ applyQualifier(rightCol, rightQualifier);
1153
+ ambiguousColumns.add(leftColName);
1154
+ transformed = true;
1155
+ } else if (leftQualified && rightUnqualified) {
1156
+ applyQualifier(rightCol, rightQualifier);
1157
+ ambiguousColumns.add(rightColName);
1158
+ transformed = true;
1159
+ } else if (leftUnqualified && rightQualified) {
1160
+ applyQualifier(leftCol, leftQualifier);
1161
+ ambiguousColumns.add(leftColName);
1162
+ transformed = true;
1111
1163
  }
1112
1164
  }
1113
- if (expr.operator === "AND" || expr.operator === "OR") {
1114
- transformed = walkOnClause(expr.left, leftSource, rightSource, ambiguousColumns) || transformed;
1115
- transformed = walkOnClause(expr.right, leftSource, rightSource, ambiguousColumns) || transformed;
1116
- }
1165
+ transformed = walkOnClause(isBinaryExpr(expr.left) ? expr.left : expr.left, leftQualifier, rightQualifier, ambiguousColumns) || transformed;
1166
+ transformed = walkOnClause(isBinaryExpr(expr.right) ? expr.right : expr.right, leftQualifier, rightQualifier, ambiguousColumns) || transformed;
1117
1167
  }
1118
1168
  return transformed;
1119
1169
  }
@@ -1124,12 +1174,12 @@ function qualifyAmbiguousInExpression(expr, defaultQualifier, ambiguousColumns)
1124
1174
  if (isUnqualifiedColumnRef(expr)) {
1125
1175
  const colName = getColumnName(expr);
1126
1176
  if (colName && ambiguousColumns.has(colName)) {
1127
- expr.table = defaultQualifier;
1177
+ applyQualifier(expr, defaultQualifier);
1128
1178
  transformed = true;
1129
1179
  }
1130
1180
  return transformed;
1131
1181
  }
1132
- if ("type" in expr && expr.type === "binary_expr") {
1182
+ if (isBinaryExpr(expr)) {
1133
1183
  const binary = expr;
1134
1184
  transformed = qualifyAmbiguousInExpression(binary.left, defaultQualifier, ambiguousColumns) || transformed;
1135
1185
  transformed = qualifyAmbiguousInExpression(binary.right, defaultQualifier, ambiguousColumns) || transformed;
@@ -1146,48 +1196,117 @@ function qualifyAmbiguousInExpression(expr, defaultQualifier, ambiguousColumns)
1146
1196
  transformed = qualifyAmbiguousInExpression(args.expr, defaultQualifier, ambiguousColumns) || transformed;
1147
1197
  }
1148
1198
  }
1199
+ if ("over" in expr && expr.over && typeof expr.over === "object") {
1200
+ const over = expr.over;
1201
+ if (Array.isArray(over.partition)) {
1202
+ for (const part of over.partition) {
1203
+ transformed = qualifyAmbiguousInExpression(part, defaultQualifier, ambiguousColumns) || transformed;
1204
+ }
1205
+ }
1206
+ if (Array.isArray(over.orderby)) {
1207
+ for (const order of over.orderby) {
1208
+ transformed = qualifyAmbiguousInExpression(order, defaultQualifier, ambiguousColumns) || transformed;
1209
+ }
1210
+ }
1211
+ }
1149
1212
  return transformed;
1150
1213
  }
1214
+ function hasUnqualifiedColumns(expr) {
1215
+ if (!expr || typeof expr !== "object")
1216
+ return false;
1217
+ if ("type" in expr && expr.type === "binary_expr") {
1218
+ const left = expr.left;
1219
+ const right = expr.right;
1220
+ const leftCol = unwrapColumnRef(left);
1221
+ const rightCol = unwrapColumnRef(right);
1222
+ if (isUnqualifiedColumnRef(left) || isUnqualifiedColumnRef(right) || leftCol && isUnqualifiedColumnRef(leftCol) || rightCol && isUnqualifiedColumnRef(rightCol)) {
1223
+ return true;
1224
+ }
1225
+ if (isBinaryExpr(expr.left) && hasUnqualifiedColumns(expr.left))
1226
+ return true;
1227
+ if (isBinaryExpr(expr.right) && hasUnqualifiedColumns(expr.right))
1228
+ return true;
1229
+ }
1230
+ if ("args" in expr && expr.args) {
1231
+ const args = expr.args;
1232
+ if (args.expr && isUnqualifiedColumnRef(args.expr))
1233
+ return true;
1234
+ if (args.value) {
1235
+ for (const arg of args.value) {
1236
+ if (isUnqualifiedColumnRef(arg))
1237
+ return true;
1238
+ }
1239
+ }
1240
+ }
1241
+ return false;
1242
+ }
1151
1243
  function walkSelect(select) {
1152
1244
  let transformed = false;
1153
1245
  const ambiguousColumns = new Set;
1154
1246
  if (Array.isArray(select.from) && select.from.length >= 2) {
1155
1247
  const firstSource = getTableSource(select.from[0]);
1156
- const defaultQualifier = firstSource ? getQualifier(firstSource) : "";
1248
+ const defaultQualifier = firstSource ? getQualifier(firstSource) : null;
1157
1249
  let prevSource = firstSource;
1250
+ let hasAnyUnqualified = false;
1158
1251
  for (const from of select.from) {
1159
1252
  if ("join" in from) {
1160
1253
  const join = from;
1161
- const currentSource = getTableSource(join);
1162
- if (join.on && prevSource && currentSource) {
1163
- const leftQualifier = getQualifier(prevSource);
1164
- const rightQualifier = getQualifier(currentSource);
1165
- transformed = walkOnClause(join.on, leftQualifier, rightQualifier, ambiguousColumns) || transformed;
1166
- }
1167
- prevSource = currentSource;
1168
- } else {
1169
- const source = getTableSource(from);
1170
- if (source) {
1171
- prevSource = source;
1254
+ if (join.on && hasUnqualifiedColumns(join.on)) {
1255
+ hasAnyUnqualified = true;
1256
+ break;
1172
1257
  }
1173
1258
  }
1174
- if ("expr" in from && from.expr && "ast" in from.expr) {
1175
- transformed = walkSelect(from.expr.ast) || transformed;
1176
- }
1177
1259
  }
1178
- if (ambiguousColumns.size > 0 && defaultQualifier) {
1179
- if (Array.isArray(select.columns)) {
1180
- for (const col of select.columns) {
1181
- if ("expr" in col) {
1182
- transformed = qualifyAmbiguousInExpression(col.expr, defaultQualifier, ambiguousColumns) || transformed;
1260
+ if (!hasAnyUnqualified) {
1261
+ for (const from of select.from) {
1262
+ if ("expr" in from && from.expr && "ast" in from.expr) {
1263
+ transformed = walkSelect(from.expr.ast) || transformed;
1264
+ }
1265
+ }
1266
+ } else {
1267
+ for (const from of select.from) {
1268
+ if ("join" in from) {
1269
+ const join = from;
1270
+ const currentSource = getTableSource(join);
1271
+ if (join.on && prevSource && currentSource) {
1272
+ const leftQualifier = getQualifier(prevSource);
1273
+ const rightQualifier = getQualifier(currentSource);
1274
+ transformed = walkOnClause(join.on, leftQualifier, rightQualifier, ambiguousColumns) || transformed;
1275
+ }
1276
+ if (join.using && prevSource && currentSource) {
1277
+ for (const usingCol of join.using) {
1278
+ if (typeof usingCol === "string") {
1279
+ ambiguousColumns.add(usingCol);
1280
+ } else if ("value" in usingCol) {
1281
+ ambiguousColumns.add(String(usingCol.value));
1282
+ }
1283
+ }
1183
1284
  }
1285
+ prevSource = currentSource;
1286
+ } else {
1287
+ const source = getTableSource(from);
1288
+ if (source) {
1289
+ prevSource = source;
1290
+ }
1291
+ }
1292
+ if ("expr" in from && from.expr && "ast" in from.expr) {
1293
+ transformed = walkSelect(from.expr.ast) || transformed;
1184
1294
  }
1185
1295
  }
1186
- transformed = qualifyAmbiguousInExpression(select.where, defaultQualifier, ambiguousColumns) || transformed;
1187
- if (Array.isArray(select.orderby)) {
1188
- for (const order of select.orderby) {
1189
- if (order.expr) {
1190
- transformed = qualifyAmbiguousInExpression(order.expr, defaultQualifier, ambiguousColumns) || transformed;
1296
+ if (ambiguousColumns.size > 0 && defaultQualifier) {
1297
+ if (Array.isArray(select.columns)) {
1298
+ for (const col of select.columns) {
1299
+ if ("expr" in col) {
1300
+ transformed = qualifyAmbiguousInExpression(col.expr, defaultQualifier, ambiguousColumns) || transformed;
1301
+ }
1302
+ }
1303
+ }
1304
+ transformed = qualifyAmbiguousInExpression(select.where, defaultQualifier, ambiguousColumns) || transformed;
1305
+ if (Array.isArray(select.orderby)) {
1306
+ for (const order of select.orderby) {
1307
+ if (order.expr) {
1308
+ transformed = qualifyAmbiguousInExpression(order.expr, defaultQualifier, ambiguousColumns) || transformed;
1309
+ }
1191
1310
  }
1192
1311
  }
1193
1312
  }
@@ -1212,6 +1331,40 @@ function qualifyJoinColumns(ast) {
1212
1331
  for (const stmt of statements) {
1213
1332
  if (stmt.type === "select") {
1214
1333
  transformed = walkSelect(stmt) || transformed;
1334
+ } else if (stmt.type === "insert") {
1335
+ const insert = stmt;
1336
+ if (insert.values && typeof insert.values === "object" && "type" in insert.values && insert.values.type === "select") {
1337
+ transformed = walkSelect(insert.values) || transformed;
1338
+ }
1339
+ } else if (stmt.type === "update") {
1340
+ const update = stmt;
1341
+ const mainSource = update.table?.[0] ? getTableSource(update.table[0]) : null;
1342
+ const defaultQualifier = mainSource ? getQualifier(mainSource) : null;
1343
+ const fromSources = update.from ?? [];
1344
+ const firstFrom = fromSources[0] ? getTableSource(fromSources[0]) : null;
1345
+ if (update.where && defaultQualifier && firstFrom) {
1346
+ const ambiguous = new Set;
1347
+ transformed = walkOnClause(update.where, defaultQualifier, getQualifier(firstFrom), ambiguous) || transformed;
1348
+ transformed = qualifyAmbiguousInExpression(update.where, defaultQualifier, ambiguous) || transformed;
1349
+ }
1350
+ if (Array.isArray(update.returning) && defaultQualifier) {
1351
+ for (const ret of update.returning) {
1352
+ transformed = qualifyAmbiguousInExpression(ret, defaultQualifier, new Set) || transformed;
1353
+ }
1354
+ }
1355
+ } else if (stmt.type === "delete") {
1356
+ const del = stmt;
1357
+ const mainSource = del.table?.[0] ? getTableSource(del.table[0]) : null;
1358
+ const defaultQualifier = mainSource ? getQualifier(mainSource) : null;
1359
+ const fromSources = del.from ?? [];
1360
+ const firstFrom = fromSources[0] ? getTableSource(fromSources[0]) : null;
1361
+ if (del.where && defaultQualifier && firstFrom) {
1362
+ const ambiguous = new Set;
1363
+ transformed = walkOnClause(del.where, defaultQualifier, getQualifier(firstFrom), ambiguous) || transformed;
1364
+ transformed = qualifyAmbiguousInExpression(del.where, defaultQualifier, ambiguous) || transformed;
1365
+ } else if (del.where && defaultQualifier) {
1366
+ transformed = qualifyAmbiguousInExpression(del.where, defaultQualifier, new Set) || transformed;
1367
+ }
1215
1368
  }
1216
1369
  }
1217
1370
  return transformed;
@@ -1220,29 +1373,65 @@ function qualifyJoinColumns(ast) {
1220
1373
  // src/sql/ast-transformer.ts
1221
1374
  var { Parser } = nodeSqlParser;
1222
1375
  var parser = new Parser;
1376
+ var CACHE_SIZE = 500;
1377
+ var transformCache = new Map;
1378
+ function getCachedOrTransform(query, transform) {
1379
+ const cached = transformCache.get(query);
1380
+ if (cached) {
1381
+ transformCache.delete(query);
1382
+ transformCache.set(query, cached);
1383
+ return cached;
1384
+ }
1385
+ const result = transform();
1386
+ if (transformCache.size >= CACHE_SIZE) {
1387
+ const oldestKey = transformCache.keys().next().value;
1388
+ if (oldestKey) {
1389
+ transformCache.delete(oldestKey);
1390
+ }
1391
+ }
1392
+ transformCache.set(query, result);
1393
+ return result;
1394
+ }
1395
+ var DEBUG_ENV = "DRIZZLE_DUCKDB_DEBUG_AST";
1396
+ function hasJoin(query) {
1397
+ return /\bjoin\b/i.test(query);
1398
+ }
1399
+ function debugLog(message, payload) {
1400
+ if (process?.env?.[DEBUG_ENV]) {
1401
+ console.debug("[duckdb-ast]", message, payload ?? "");
1402
+ }
1403
+ }
1223
1404
  function transformSQL(query) {
1224
1405
  const needsArrayTransform = query.includes("@>") || query.includes("<@") || query.includes("&&");
1225
- const needsJoinTransform = query.toLowerCase().includes("join");
1406
+ const needsJoinTransform = hasJoin(query) || /\bupdate\b/i.test(query) || /\bdelete\b/i.test(query);
1226
1407
  if (!needsArrayTransform && !needsJoinTransform) {
1227
1408
  return { sql: query, transformed: false };
1228
1409
  }
1229
- try {
1230
- const ast = parser.astify(query, { database: "PostgreSQL" });
1231
- let transformed = false;
1232
- if (needsArrayTransform) {
1233
- transformed = transformArrayOperators(ast) || transformed;
1234
- }
1235
- if (needsJoinTransform) {
1236
- transformed = qualifyJoinColumns(ast) || transformed;
1237
- }
1238
- if (!transformed) {
1410
+ return getCachedOrTransform(query, () => {
1411
+ try {
1412
+ const ast = parser.astify(query, { database: "PostgreSQL" });
1413
+ let transformed = false;
1414
+ if (needsArrayTransform) {
1415
+ transformed = transformArrayOperators(ast) || transformed;
1416
+ }
1417
+ if (needsJoinTransform) {
1418
+ transformed = qualifyJoinColumns(ast) || transformed;
1419
+ }
1420
+ if (!transformed) {
1421
+ debugLog("AST parsed but no transformation applied", {
1422
+ join: needsJoinTransform
1423
+ });
1424
+ return { sql: query, transformed: false };
1425
+ }
1426
+ const transformedSql = parser.sqlify(ast, { database: "PostgreSQL" });
1427
+ return { sql: transformedSql, transformed: true };
1428
+ } catch (err) {
1429
+ debugLog("AST transform failed; returning original SQL", {
1430
+ error: err.message
1431
+ });
1239
1432
  return { sql: query, transformed: false };
1240
1433
  }
1241
- const transformedSql = parser.sqlify(ast, { database: "PostgreSQL" });
1242
- return { sql: transformedSql, transformed: true };
1243
- } catch {
1244
- return { sql: query, transformed: false };
1245
- }
1434
+ });
1246
1435
  }
1247
1436
 
1248
1437
  // src/dialect.ts
@@ -4,12 +4,28 @@
4
4
  * Transforms:
5
5
  * - Array operators: @>, <@, && -> array_has_all(), array_has_any()
6
6
  * - JOIN column qualification: "col" = "col" -> "left"."col" = "right"."col"
7
+ *
8
+ * Performance optimizations:
9
+ * - LRU cache for transformed queries (avoids re-parsing identical queries)
10
+ * - Smart heuristics to skip JOIN qualification when not needed
11
+ * - Early exit when no transformation is required
7
12
  */
8
13
  export type TransformResult = {
9
14
  sql: string;
10
15
  transformed: boolean;
11
16
  };
12
17
  export declare function transformSQL(query: string): TransformResult;
18
+ /**
19
+ * Clear the transformation cache. Useful for testing or memory management.
20
+ */
21
+ export declare function clearTransformCache(): void;
22
+ /**
23
+ * Get current cache statistics for monitoring.
24
+ */
25
+ export declare function getTransformCacheStats(): {
26
+ size: number;
27
+ maxSize: number;
28
+ };
13
29
  export declare function needsTransformation(query: string): boolean;
14
30
  export { transformArrayOperators } from './visitors/array-operators.ts';
15
31
  export { qualifyJoinColumns } from './visitors/column-qualifier.ts';
@@ -1,5 +1,10 @@
1
1
  /**
2
2
  * AST visitor to qualify unqualified column references in JOIN ON clauses.
3
+ *
4
+ * Performance optimizations:
5
+ * - Early exit when no unqualified columns found in ON clause
6
+ * - Skip processing if all columns are already qualified
7
+ * - Minimal tree traversal when possible
3
8
  */
4
9
  import type { AST } from 'node-sql-parser';
5
10
  export declare function qualifyJoinColumns(ast: AST | AST[]): boolean;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "module": "./dist/index.mjs",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.ts",
6
- "version": "1.2.0",
6
+ "version": "1.2.1",
7
7
  "description": "A drizzle ORM client for use with DuckDB. Based on drizzle's Postgres client.",
8
8
  "type": "module",
9
9
  "scripts": {
@@ -11,7 +11,7 @@
11
11
  "build:declarations": "tsc --emitDeclarationOnly --project tsconfig.types.json",
12
12
  "test": "vitest",
13
13
  "t": "vitest --watch --ui",
14
- "bench": "vitest bench --runInBand test/perf",
14
+ "bench": "vitest bench --run test/perf --pool=threads --poolOptions.threads.singleThread=true --no-file-parallelism",
15
15
  "perf:run": "bun run scripts/run-perf.ts",
16
16
  "perf:compare": "bun run scripts/compare-perf.ts"
17
17
  },
@@ -4,6 +4,11 @@
4
4
  * Transforms:
5
5
  * - Array operators: @>, <@, && -> array_has_all(), array_has_any()
6
6
  * - JOIN column qualification: "col" = "col" -> "left"."col" = "right"."col"
7
+ *
8
+ * Performance optimizations:
9
+ * - LRU cache for transformed queries (avoids re-parsing identical queries)
10
+ * - Smart heuristics to skip JOIN qualification when not needed
11
+ * - Early exit when no transformation is required
7
12
  */
8
13
 
9
14
  import nodeSqlParser from 'node-sql-parser';
@@ -20,38 +25,107 @@ export type TransformResult = {
20
25
  transformed: boolean;
21
26
  };
22
27
 
28
+ // LRU cache for transformed SQL queries
29
+ // Key: original SQL, Value: transformed result
30
+ const CACHE_SIZE = 500;
31
+ const transformCache = new Map<string, TransformResult>();
32
+
33
+ function getCachedOrTransform(
34
+ query: string,
35
+ transform: () => TransformResult
36
+ ): TransformResult {
37
+ const cached = transformCache.get(query);
38
+ if (cached) {
39
+ // Move to end for LRU behavior
40
+ transformCache.delete(query);
41
+ transformCache.set(query, cached);
42
+ return cached;
43
+ }
44
+
45
+ const result = transform();
46
+
47
+ // Add to cache with LRU eviction
48
+ if (transformCache.size >= CACHE_SIZE) {
49
+ // Delete oldest entry (first key in Map iteration order)
50
+ const oldestKey = transformCache.keys().next().value;
51
+ if (oldestKey) {
52
+ transformCache.delete(oldestKey);
53
+ }
54
+ }
55
+ transformCache.set(query, result);
56
+
57
+ return result;
58
+ }
59
+
60
+ const DEBUG_ENV = 'DRIZZLE_DUCKDB_DEBUG_AST';
61
+
62
+ function hasJoin(query: string): boolean {
63
+ return /\bjoin\b/i.test(query);
64
+ }
65
+
66
+ function debugLog(message: string, payload?: unknown): void {
67
+ if (process?.env?.[DEBUG_ENV]) {
68
+ // eslint-disable-next-line no-console
69
+ console.debug('[duckdb-ast]', message, payload ?? '');
70
+ }
71
+ }
72
+
23
73
  export function transformSQL(query: string): TransformResult {
24
74
  const needsArrayTransform =
25
75
  query.includes('@>') || query.includes('<@') || query.includes('&&');
26
- const needsJoinTransform = query.toLowerCase().includes('join');
76
+ const needsJoinTransform =
77
+ hasJoin(query) || /\bupdate\b/i.test(query) || /\bdelete\b/i.test(query);
27
78
 
28
79
  if (!needsArrayTransform && !needsJoinTransform) {
29
80
  return { sql: query, transformed: false };
30
81
  }
31
82
 
32
- try {
33
- const ast = parser.astify(query, { database: 'PostgreSQL' });
83
+ // Use cache for repeated queries
84
+ return getCachedOrTransform(query, () => {
85
+ try {
86
+ const ast = parser.astify(query, { database: 'PostgreSQL' });
34
87
 
35
- let transformed = false;
88
+ let transformed = false;
36
89
 
37
- if (needsArrayTransform) {
38
- transformed = transformArrayOperators(ast) || transformed;
39
- }
90
+ if (needsArrayTransform) {
91
+ transformed = transformArrayOperators(ast) || transformed;
92
+ }
40
93
 
41
- if (needsJoinTransform) {
42
- transformed = qualifyJoinColumns(ast) || transformed;
43
- }
94
+ if (needsJoinTransform) {
95
+ transformed = qualifyJoinColumns(ast) || transformed;
96
+ }
44
97
 
45
- if (!transformed) {
98
+ if (!transformed) {
99
+ debugLog('AST parsed but no transformation applied', {
100
+ join: needsJoinTransform,
101
+ });
102
+ return { sql: query, transformed: false };
103
+ }
104
+
105
+ const transformedSql = parser.sqlify(ast, { database: 'PostgreSQL' });
106
+
107
+ return { sql: transformedSql, transformed: true };
108
+ } catch (err) {
109
+ debugLog('AST transform failed; returning original SQL', {
110
+ error: (err as Error).message,
111
+ });
46
112
  return { sql: query, transformed: false };
47
113
  }
114
+ });
115
+ }
48
116
 
49
- const transformedSql = parser.sqlify(ast, { database: 'PostgreSQL' });
117
+ /**
118
+ * Clear the transformation cache. Useful for testing or memory management.
119
+ */
120
+ export function clearTransformCache(): void {
121
+ transformCache.clear();
122
+ }
50
123
 
51
- return { sql: transformedSql, transformed: true };
52
- } catch {
53
- return { sql: query, transformed: false };
54
- }
124
+ /**
125
+ * Get current cache statistics for monitoring.
126
+ */
127
+ export function getTransformCacheStats(): { size: number; maxSize: number } {
128
+ return { size: transformCache.size, maxSize: CACHE_SIZE };
55
129
  }
56
130
 
57
131
  export function needsTransformation(query: string): boolean {
@@ -60,7 +134,9 @@ export function needsTransformation(query: string): boolean {
60
134
  query.includes('@>') ||
61
135
  query.includes('<@') ||
62
136
  query.includes('&&') ||
63
- lower.includes('join')
137
+ lower.includes('join') ||
138
+ lower.includes('update') ||
139
+ lower.includes('delete')
64
140
  );
65
141
  }
66
142