@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/duckdb-introspect.mjs +262 -73
- package/dist/index.mjs +254 -65
- package/dist/sql/ast-transformer.d.ts +16 -0
- package/dist/sql/visitors/column-qualifier.d.ts +5 -0
- package/package.json +2 -2
- package/src/sql/ast-transformer.ts +93 -17
- package/src/sql/visitors/column-qualifier.ts +373 -86
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
|
|
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" && !(
|
|
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
|
|
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
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1114
|
-
|
|
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
|
|
1177
|
+
applyQualifier(expr, defaultQualifier);
|
|
1128
1178
|
transformed = true;
|
|
1129
1179
|
}
|
|
1130
1180
|
return transformed;
|
|
1131
1181
|
}
|
|
1132
|
-
if (
|
|
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
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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 (
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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.
|
|
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
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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
|
-
|
|
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.
|
|
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 --
|
|
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 =
|
|
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
|
-
|
|
33
|
-
|
|
83
|
+
// Use cache for repeated queries
|
|
84
|
+
return getCachedOrTransform(query, () => {
|
|
85
|
+
try {
|
|
86
|
+
const ast = parser.astify(query, { database: 'PostgreSQL' });
|
|
34
87
|
|
|
35
|
-
|
|
88
|
+
let transformed = false;
|
|
36
89
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
90
|
+
if (needsArrayTransform) {
|
|
91
|
+
transformed = transformArrayOperators(ast) || transformed;
|
|
92
|
+
}
|
|
40
93
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
94
|
+
if (needsJoinTransform) {
|
|
95
|
+
transformed = qualifyJoinColumns(ast) || transformed;
|
|
96
|
+
}
|
|
44
97
|
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|