@leonardovida-md/drizzle-neo-duckdb 1.2.1 → 1.2.2
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 +2 -1
- package/dist/duckdb-introspect.mjs +301 -1
- package/dist/index.mjs +301 -1
- package/dist/sql/ast-transformer.d.ts +2 -0
- package/dist/sql/visitors/generate-series-alias.d.ts +13 -0
- package/dist/sql/visitors/union-with-hoister.d.ts +11 -0
- package/package.json +1 -1
- package/src/sql/ast-transformer.ts +28 -2
- package/src/sql/visitors/column-qualifier.ts +21 -0
- package/src/sql/visitors/generate-series-alias.ts +291 -0
- package/src/sql/visitors/union-with-hoister.ts +106 -0
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ pnpm add @leonardovida-md/drizzle-neo-duckdb @duckdb/node-api
|
|
|
40
40
|
```typescript
|
|
41
41
|
import { DuckDBInstance } from '@duckdb/node-api';
|
|
42
42
|
import { drizzle } from '@leonardovida-md/drizzle-neo-duckdb';
|
|
43
|
+
import { sql } from 'drizzle-orm';
|
|
43
44
|
import { integer, text, pgTable } from 'drizzle-orm/pg-core';
|
|
44
45
|
|
|
45
46
|
// Connect to DuckDB
|
|
@@ -149,7 +150,7 @@ const db = drizzle(pool);
|
|
|
149
150
|
## Schema & Types
|
|
150
151
|
|
|
151
152
|
- Use `drizzle-orm/pg-core` for schemas; DuckDB SQL is largely Postgres-compatible.
|
|
152
|
-
- DuckDB-specific helpers: `duckDbList`, `duckDbArray`, `duckDbStruct`, `duckDbMap`, `duckDbJson`, `duckDbTimestamp`, `duckDbDate`, `duckDbTime`.
|
|
153
|
+
- DuckDB-specific helpers: `duckDbList`, `duckDbArray`, `duckDbStruct`, `duckDbMap`, `duckDbJson`, `duckDbBlob`, `duckDbInet`, `duckDbInterval`, `duckDbTimestamp`, `duckDbDate`, `duckDbTime`.
|
|
153
154
|
- Browser-safe imports live under `@leonardovida-md/drizzle-neo-duckdb/helpers` (introspection emits this path).
|
|
154
155
|
|
|
155
156
|
See the [column types](https://leonardovida.github.io/drizzle-neo-duckdb/api/columns) docs for full API.
|
|
@@ -1111,6 +1111,15 @@ function walkOnClause(expr, leftQualifier, rightQualifier, ambiguousColumns) {
|
|
|
1111
1111
|
transformed = true;
|
|
1112
1112
|
}
|
|
1113
1113
|
}
|
|
1114
|
+
if (expr.operator === "=" && leftCol && rightCol && leftColName && rightColName && leftColName !== rightColName) {
|
|
1115
|
+
if (leftQualified && rightUnqualified && !rightColName.includes(".")) {
|
|
1116
|
+
applyQualifier(rightCol, rightQualifier);
|
|
1117
|
+
transformed = true;
|
|
1118
|
+
} else if (leftUnqualified && rightQualified && !leftColName.includes(".")) {
|
|
1119
|
+
applyQualifier(leftCol, leftQualifier);
|
|
1120
|
+
transformed = true;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1114
1123
|
transformed = walkOnClause(isBinaryExpr(expr.left) ? expr.left : expr.left, leftQualifier, rightQualifier, ambiguousColumns) || transformed;
|
|
1115
1124
|
transformed = walkOnClause(isBinaryExpr(expr.right) ? expr.right : expr.right, leftQualifier, rightQualifier, ambiguousColumns) || transformed;
|
|
1116
1125
|
}
|
|
@@ -1319,6 +1328,289 @@ function qualifyJoinColumns(ast) {
|
|
|
1319
1328
|
return transformed;
|
|
1320
1329
|
}
|
|
1321
1330
|
|
|
1331
|
+
// src/sql/visitors/generate-series-alias.ts
|
|
1332
|
+
function getColumnName2(col) {
|
|
1333
|
+
if (typeof col.column === "string") {
|
|
1334
|
+
return col.column;
|
|
1335
|
+
}
|
|
1336
|
+
if (col.column && "expr" in col.column && col.column.expr?.value) {
|
|
1337
|
+
return String(col.column.expr.value);
|
|
1338
|
+
}
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
function isColumnRef(expr) {
|
|
1342
|
+
return typeof expr === "object" && expr !== null && "type" in expr && expr.type === "column_ref";
|
|
1343
|
+
}
|
|
1344
|
+
function isBinaryExpr2(expr) {
|
|
1345
|
+
return !!expr && typeof expr === "object" && "type" in expr && expr.type === "binary_expr";
|
|
1346
|
+
}
|
|
1347
|
+
function getGenerateSeriesAliases(from) {
|
|
1348
|
+
const aliases = new Set;
|
|
1349
|
+
if (!from || !Array.isArray(from))
|
|
1350
|
+
return aliases;
|
|
1351
|
+
for (const f of from) {
|
|
1352
|
+
if ("expr" in f && f.expr && typeof f.expr === "object") {
|
|
1353
|
+
const exprObj = f.expr;
|
|
1354
|
+
if (exprObj.type === "function" && "name" in exprObj) {
|
|
1355
|
+
const nameObj = exprObj.name;
|
|
1356
|
+
const nameParts = nameObj?.name;
|
|
1357
|
+
const fnName = nameParts?.[0]?.value;
|
|
1358
|
+
if (typeof fnName === "string" && fnName.toLowerCase() === "generate_series") {
|
|
1359
|
+
const alias = typeof f.as === "string" ? f.as : null;
|
|
1360
|
+
if (alias && !alias.includes("(")) {
|
|
1361
|
+
aliases.add(alias);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return aliases;
|
|
1368
|
+
}
|
|
1369
|
+
function rewriteAliasColumnRef(col, alias) {
|
|
1370
|
+
col.table = alias;
|
|
1371
|
+
col.column = { expr: { type: "default", value: "generate_series" } };
|
|
1372
|
+
}
|
|
1373
|
+
function walkExpression2(expr, aliases) {
|
|
1374
|
+
if (!expr || typeof expr !== "object")
|
|
1375
|
+
return false;
|
|
1376
|
+
let transformed = false;
|
|
1377
|
+
const exprObj = expr;
|
|
1378
|
+
if (isColumnRef(expr)) {
|
|
1379
|
+
if (!("table" in expr) || !expr.table) {
|
|
1380
|
+
const colName = getColumnName2(expr);
|
|
1381
|
+
if (colName && aliases.has(colName)) {
|
|
1382
|
+
rewriteAliasColumnRef(expr, colName);
|
|
1383
|
+
transformed = true;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return transformed;
|
|
1387
|
+
}
|
|
1388
|
+
if (isBinaryExpr2(expr)) {
|
|
1389
|
+
const binary = expr;
|
|
1390
|
+
transformed = walkExpression2(binary.left, aliases) || transformed;
|
|
1391
|
+
transformed = walkExpression2(binary.right, aliases) || transformed;
|
|
1392
|
+
return transformed;
|
|
1393
|
+
}
|
|
1394
|
+
if (exprObj.type === "unary_expr" && exprObj.expr) {
|
|
1395
|
+
transformed = walkExpression2(exprObj.expr, aliases) || transformed;
|
|
1396
|
+
}
|
|
1397
|
+
if (exprObj.type === "cast" && exprObj.expr) {
|
|
1398
|
+
transformed = walkExpression2(exprObj.expr, aliases) || transformed;
|
|
1399
|
+
}
|
|
1400
|
+
if (exprObj.type === "case") {
|
|
1401
|
+
if (exprObj.expr) {
|
|
1402
|
+
transformed = walkExpression2(exprObj.expr, aliases) || transformed;
|
|
1403
|
+
}
|
|
1404
|
+
if (Array.isArray(exprObj.args)) {
|
|
1405
|
+
for (const whenClause of exprObj.args) {
|
|
1406
|
+
if (whenClause.cond) {
|
|
1407
|
+
transformed = walkExpression2(whenClause.cond, aliases) || transformed;
|
|
1408
|
+
}
|
|
1409
|
+
if (whenClause.result) {
|
|
1410
|
+
transformed = walkExpression2(whenClause.result, aliases) || transformed;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if ("args" in exprObj && exprObj.args) {
|
|
1416
|
+
const args = exprObj.args;
|
|
1417
|
+
if (Array.isArray(args.value)) {
|
|
1418
|
+
for (const arg of args.value) {
|
|
1419
|
+
transformed = walkExpression2(arg, aliases) || transformed;
|
|
1420
|
+
}
|
|
1421
|
+
} else if (args.expr) {
|
|
1422
|
+
transformed = walkExpression2(args.expr, aliases) || transformed;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if ("over" in exprObj && exprObj.over && typeof exprObj.over === "object") {
|
|
1426
|
+
const over = exprObj.over;
|
|
1427
|
+
if (Array.isArray(over.partition)) {
|
|
1428
|
+
for (const part of over.partition) {
|
|
1429
|
+
transformed = walkExpression2(part, aliases) || transformed;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
if (Array.isArray(over.orderby)) {
|
|
1433
|
+
for (const order of over.orderby) {
|
|
1434
|
+
transformed = walkExpression2(order, aliases) || transformed;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if ("ast" in exprObj && exprObj.ast) {
|
|
1439
|
+
const subAst = exprObj.ast;
|
|
1440
|
+
if (subAst.type === "select") {
|
|
1441
|
+
transformed = walkSelect2(subAst) || transformed;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
if (exprObj.type === "expr_list" && Array.isArray(exprObj.value)) {
|
|
1445
|
+
for (const item of exprObj.value) {
|
|
1446
|
+
transformed = walkExpression2(item, aliases) || transformed;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return transformed;
|
|
1450
|
+
}
|
|
1451
|
+
function walkFrom2(from, aliases) {
|
|
1452
|
+
if (!from || !Array.isArray(from))
|
|
1453
|
+
return false;
|
|
1454
|
+
let transformed = false;
|
|
1455
|
+
for (const f of from) {
|
|
1456
|
+
if ("join" in f) {
|
|
1457
|
+
const join = f;
|
|
1458
|
+
transformed = walkExpression2(join.on, aliases) || transformed;
|
|
1459
|
+
}
|
|
1460
|
+
if ("expr" in f && f.expr && "ast" in f.expr) {
|
|
1461
|
+
transformed = walkSelect2(f.expr.ast) || transformed;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return transformed;
|
|
1465
|
+
}
|
|
1466
|
+
function walkSelect2(select) {
|
|
1467
|
+
let transformed = false;
|
|
1468
|
+
const aliases = getGenerateSeriesAliases(select.from);
|
|
1469
|
+
if (select.with) {
|
|
1470
|
+
for (const cte of select.with) {
|
|
1471
|
+
const cteSelect = cte.stmt?.ast ?? cte.stmt;
|
|
1472
|
+
if (cteSelect && cteSelect.type === "select") {
|
|
1473
|
+
transformed = walkSelect2(cteSelect) || transformed;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
transformed = walkFrom2(select.from, aliases) || transformed;
|
|
1478
|
+
transformed = walkExpression2(select.where, aliases) || transformed;
|
|
1479
|
+
if (select.having) {
|
|
1480
|
+
if (Array.isArray(select.having)) {
|
|
1481
|
+
for (const h of select.having) {
|
|
1482
|
+
transformed = walkExpression2(h, aliases) || transformed;
|
|
1483
|
+
}
|
|
1484
|
+
} else {
|
|
1485
|
+
transformed = walkExpression2(select.having, aliases) || transformed;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
if (Array.isArray(select.columns)) {
|
|
1489
|
+
for (const col of select.columns) {
|
|
1490
|
+
if ("expr" in col) {
|
|
1491
|
+
transformed = walkExpression2(col.expr, aliases) || transformed;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
if (Array.isArray(select.groupby)) {
|
|
1496
|
+
for (const g of select.groupby) {
|
|
1497
|
+
transformed = walkExpression2(g, aliases) || transformed;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (Array.isArray(select.orderby)) {
|
|
1501
|
+
for (const order of select.orderby) {
|
|
1502
|
+
if (order.expr) {
|
|
1503
|
+
transformed = walkExpression2(order.expr, aliases) || transformed;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
if (select._orderby) {
|
|
1508
|
+
for (const order of select._orderby) {
|
|
1509
|
+
if (order.expr) {
|
|
1510
|
+
transformed = walkExpression2(order.expr, aliases) || transformed;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (select._next) {
|
|
1515
|
+
transformed = walkSelect2(select._next) || transformed;
|
|
1516
|
+
}
|
|
1517
|
+
return transformed;
|
|
1518
|
+
}
|
|
1519
|
+
function rewriteGenerateSeriesAliases(ast) {
|
|
1520
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
1521
|
+
let transformed = false;
|
|
1522
|
+
for (const stmt of statements) {
|
|
1523
|
+
if (stmt.type === "select") {
|
|
1524
|
+
transformed = walkSelect2(stmt) || transformed;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return transformed;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/sql/visitors/union-with-hoister.ts
|
|
1531
|
+
function getCteName(cte) {
|
|
1532
|
+
const nameObj = cte.name;
|
|
1533
|
+
if (!nameObj)
|
|
1534
|
+
return null;
|
|
1535
|
+
const value = nameObj.value;
|
|
1536
|
+
if (typeof value === "string")
|
|
1537
|
+
return value;
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
function hoistWithInSelect(select) {
|
|
1541
|
+
if (!select.set_op || !select._next)
|
|
1542
|
+
return false;
|
|
1543
|
+
const arms = [];
|
|
1544
|
+
let current = select;
|
|
1545
|
+
while (current && current.type === "select") {
|
|
1546
|
+
arms.push(current);
|
|
1547
|
+
current = current._next;
|
|
1548
|
+
}
|
|
1549
|
+
const mergedWith = [];
|
|
1550
|
+
const seen = new Set;
|
|
1551
|
+
let hasWithBeyondFirst = false;
|
|
1552
|
+
for (const arm of arms) {
|
|
1553
|
+
if (arm.with && arm.with.length > 0) {
|
|
1554
|
+
if (arm !== arms[0]) {
|
|
1555
|
+
hasWithBeyondFirst = true;
|
|
1556
|
+
}
|
|
1557
|
+
for (const cte of arm.with) {
|
|
1558
|
+
const cteName = getCteName(cte);
|
|
1559
|
+
if (!cteName)
|
|
1560
|
+
return false;
|
|
1561
|
+
if (seen.has(cteName)) {
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
seen.add(cteName);
|
|
1565
|
+
mergedWith.push(cte);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (!hasWithBeyondFirst)
|
|
1570
|
+
return false;
|
|
1571
|
+
arms[0].with = mergedWith;
|
|
1572
|
+
if ("parentheses_symbol" in arms[0]) {
|
|
1573
|
+
arms[0].parentheses_symbol = false;
|
|
1574
|
+
}
|
|
1575
|
+
for (let i = 1;i < arms.length; i++) {
|
|
1576
|
+
arms[i].with = null;
|
|
1577
|
+
}
|
|
1578
|
+
return true;
|
|
1579
|
+
}
|
|
1580
|
+
function walkSelect3(select) {
|
|
1581
|
+
let transformed = false;
|
|
1582
|
+
if (select.with) {
|
|
1583
|
+
for (const cte of select.with) {
|
|
1584
|
+
const cteSelect = cte.stmt?.ast ?? cte.stmt;
|
|
1585
|
+
if (cteSelect && cteSelect.type === "select") {
|
|
1586
|
+
transformed = walkSelect3(cteSelect) || transformed;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
if (Array.isArray(select.from)) {
|
|
1591
|
+
for (const from of select.from) {
|
|
1592
|
+
if ("expr" in from && from.expr && "ast" in from.expr) {
|
|
1593
|
+
transformed = walkSelect3(from.expr.ast) || transformed;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
transformed = hoistWithInSelect(select) || transformed;
|
|
1598
|
+
if (select._next) {
|
|
1599
|
+
transformed = walkSelect3(select._next) || transformed;
|
|
1600
|
+
}
|
|
1601
|
+
return transformed;
|
|
1602
|
+
}
|
|
1603
|
+
function hoistUnionWith(ast) {
|
|
1604
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
1605
|
+
let transformed = false;
|
|
1606
|
+
for (const stmt of statements) {
|
|
1607
|
+
if (stmt.type === "select") {
|
|
1608
|
+
transformed = walkSelect3(stmt) || transformed;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return transformed;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1322
1614
|
// src/sql/ast-transformer.ts
|
|
1323
1615
|
var { Parser } = nodeSqlParser;
|
|
1324
1616
|
var parser = new Parser;
|
|
@@ -1353,7 +1645,9 @@ function debugLog(message, payload) {
|
|
|
1353
1645
|
function transformSQL(query) {
|
|
1354
1646
|
const needsArrayTransform = query.includes("@>") || query.includes("<@") || query.includes("&&");
|
|
1355
1647
|
const needsJoinTransform = hasJoin(query) || /\bupdate\b/i.test(query) || /\bdelete\b/i.test(query);
|
|
1356
|
-
|
|
1648
|
+
const needsUnionTransform = /\bunion\b/i.test(query) || /\bintersect\b/i.test(query) || /\bexcept\b/i.test(query);
|
|
1649
|
+
const needsGenerateSeriesTransform = /\bgenerate_series\b/i.test(query);
|
|
1650
|
+
if (!needsArrayTransform && !needsJoinTransform && !needsUnionTransform && !needsGenerateSeriesTransform) {
|
|
1357
1651
|
return { sql: query, transformed: false };
|
|
1358
1652
|
}
|
|
1359
1653
|
return getCachedOrTransform(query, () => {
|
|
@@ -1366,6 +1660,12 @@ function transformSQL(query) {
|
|
|
1366
1660
|
if (needsJoinTransform) {
|
|
1367
1661
|
transformed = qualifyJoinColumns(ast) || transformed;
|
|
1368
1662
|
}
|
|
1663
|
+
if (needsGenerateSeriesTransform) {
|
|
1664
|
+
transformed = rewriteGenerateSeriesAliases(ast) || transformed;
|
|
1665
|
+
}
|
|
1666
|
+
if (needsUnionTransform) {
|
|
1667
|
+
transformed = hoistUnionWith(ast) || transformed;
|
|
1668
|
+
}
|
|
1369
1669
|
if (!transformed) {
|
|
1370
1670
|
debugLog("AST parsed but no transformation applied", {
|
|
1371
1671
|
join: needsJoinTransform
|
package/dist/index.mjs
CHANGED
|
@@ -1162,6 +1162,15 @@ function walkOnClause(expr, leftQualifier, rightQualifier, ambiguousColumns) {
|
|
|
1162
1162
|
transformed = true;
|
|
1163
1163
|
}
|
|
1164
1164
|
}
|
|
1165
|
+
if (expr.operator === "=" && leftCol && rightCol && leftColName && rightColName && leftColName !== rightColName) {
|
|
1166
|
+
if (leftQualified && rightUnqualified && !rightColName.includes(".")) {
|
|
1167
|
+
applyQualifier(rightCol, rightQualifier);
|
|
1168
|
+
transformed = true;
|
|
1169
|
+
} else if (leftUnqualified && rightQualified && !leftColName.includes(".")) {
|
|
1170
|
+
applyQualifier(leftCol, leftQualifier);
|
|
1171
|
+
transformed = true;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1165
1174
|
transformed = walkOnClause(isBinaryExpr(expr.left) ? expr.left : expr.left, leftQualifier, rightQualifier, ambiguousColumns) || transformed;
|
|
1166
1175
|
transformed = walkOnClause(isBinaryExpr(expr.right) ? expr.right : expr.right, leftQualifier, rightQualifier, ambiguousColumns) || transformed;
|
|
1167
1176
|
}
|
|
@@ -1370,6 +1379,289 @@ function qualifyJoinColumns(ast) {
|
|
|
1370
1379
|
return transformed;
|
|
1371
1380
|
}
|
|
1372
1381
|
|
|
1382
|
+
// src/sql/visitors/generate-series-alias.ts
|
|
1383
|
+
function getColumnName2(col) {
|
|
1384
|
+
if (typeof col.column === "string") {
|
|
1385
|
+
return col.column;
|
|
1386
|
+
}
|
|
1387
|
+
if (col.column && "expr" in col.column && col.column.expr?.value) {
|
|
1388
|
+
return String(col.column.expr.value);
|
|
1389
|
+
}
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
function isColumnRef(expr) {
|
|
1393
|
+
return typeof expr === "object" && expr !== null && "type" in expr && expr.type === "column_ref";
|
|
1394
|
+
}
|
|
1395
|
+
function isBinaryExpr2(expr) {
|
|
1396
|
+
return !!expr && typeof expr === "object" && "type" in expr && expr.type === "binary_expr";
|
|
1397
|
+
}
|
|
1398
|
+
function getGenerateSeriesAliases(from) {
|
|
1399
|
+
const aliases = new Set;
|
|
1400
|
+
if (!from || !Array.isArray(from))
|
|
1401
|
+
return aliases;
|
|
1402
|
+
for (const f of from) {
|
|
1403
|
+
if ("expr" in f && f.expr && typeof f.expr === "object") {
|
|
1404
|
+
const exprObj = f.expr;
|
|
1405
|
+
if (exprObj.type === "function" && "name" in exprObj) {
|
|
1406
|
+
const nameObj = exprObj.name;
|
|
1407
|
+
const nameParts = nameObj?.name;
|
|
1408
|
+
const fnName = nameParts?.[0]?.value;
|
|
1409
|
+
if (typeof fnName === "string" && fnName.toLowerCase() === "generate_series") {
|
|
1410
|
+
const alias = typeof f.as === "string" ? f.as : null;
|
|
1411
|
+
if (alias && !alias.includes("(")) {
|
|
1412
|
+
aliases.add(alias);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return aliases;
|
|
1419
|
+
}
|
|
1420
|
+
function rewriteAliasColumnRef(col, alias) {
|
|
1421
|
+
col.table = alias;
|
|
1422
|
+
col.column = { expr: { type: "default", value: "generate_series" } };
|
|
1423
|
+
}
|
|
1424
|
+
function walkExpression2(expr, aliases) {
|
|
1425
|
+
if (!expr || typeof expr !== "object")
|
|
1426
|
+
return false;
|
|
1427
|
+
let transformed = false;
|
|
1428
|
+
const exprObj = expr;
|
|
1429
|
+
if (isColumnRef(expr)) {
|
|
1430
|
+
if (!("table" in expr) || !expr.table) {
|
|
1431
|
+
const colName = getColumnName2(expr);
|
|
1432
|
+
if (colName && aliases.has(colName)) {
|
|
1433
|
+
rewriteAliasColumnRef(expr, colName);
|
|
1434
|
+
transformed = true;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
return transformed;
|
|
1438
|
+
}
|
|
1439
|
+
if (isBinaryExpr2(expr)) {
|
|
1440
|
+
const binary = expr;
|
|
1441
|
+
transformed = walkExpression2(binary.left, aliases) || transformed;
|
|
1442
|
+
transformed = walkExpression2(binary.right, aliases) || transformed;
|
|
1443
|
+
return transformed;
|
|
1444
|
+
}
|
|
1445
|
+
if (exprObj.type === "unary_expr" && exprObj.expr) {
|
|
1446
|
+
transformed = walkExpression2(exprObj.expr, aliases) || transformed;
|
|
1447
|
+
}
|
|
1448
|
+
if (exprObj.type === "cast" && exprObj.expr) {
|
|
1449
|
+
transformed = walkExpression2(exprObj.expr, aliases) || transformed;
|
|
1450
|
+
}
|
|
1451
|
+
if (exprObj.type === "case") {
|
|
1452
|
+
if (exprObj.expr) {
|
|
1453
|
+
transformed = walkExpression2(exprObj.expr, aliases) || transformed;
|
|
1454
|
+
}
|
|
1455
|
+
if (Array.isArray(exprObj.args)) {
|
|
1456
|
+
for (const whenClause of exprObj.args) {
|
|
1457
|
+
if (whenClause.cond) {
|
|
1458
|
+
transformed = walkExpression2(whenClause.cond, aliases) || transformed;
|
|
1459
|
+
}
|
|
1460
|
+
if (whenClause.result) {
|
|
1461
|
+
transformed = walkExpression2(whenClause.result, aliases) || transformed;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if ("args" in exprObj && exprObj.args) {
|
|
1467
|
+
const args = exprObj.args;
|
|
1468
|
+
if (Array.isArray(args.value)) {
|
|
1469
|
+
for (const arg of args.value) {
|
|
1470
|
+
transformed = walkExpression2(arg, aliases) || transformed;
|
|
1471
|
+
}
|
|
1472
|
+
} else if (args.expr) {
|
|
1473
|
+
transformed = walkExpression2(args.expr, aliases) || transformed;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if ("over" in exprObj && exprObj.over && typeof exprObj.over === "object") {
|
|
1477
|
+
const over = exprObj.over;
|
|
1478
|
+
if (Array.isArray(over.partition)) {
|
|
1479
|
+
for (const part of over.partition) {
|
|
1480
|
+
transformed = walkExpression2(part, aliases) || transformed;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (Array.isArray(over.orderby)) {
|
|
1484
|
+
for (const order of over.orderby) {
|
|
1485
|
+
transformed = walkExpression2(order, aliases) || transformed;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if ("ast" in exprObj && exprObj.ast) {
|
|
1490
|
+
const subAst = exprObj.ast;
|
|
1491
|
+
if (subAst.type === "select") {
|
|
1492
|
+
transformed = walkSelect2(subAst) || transformed;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
if (exprObj.type === "expr_list" && Array.isArray(exprObj.value)) {
|
|
1496
|
+
for (const item of exprObj.value) {
|
|
1497
|
+
transformed = walkExpression2(item, aliases) || transformed;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return transformed;
|
|
1501
|
+
}
|
|
1502
|
+
function walkFrom2(from, aliases) {
|
|
1503
|
+
if (!from || !Array.isArray(from))
|
|
1504
|
+
return false;
|
|
1505
|
+
let transformed = false;
|
|
1506
|
+
for (const f of from) {
|
|
1507
|
+
if ("join" in f) {
|
|
1508
|
+
const join = f;
|
|
1509
|
+
transformed = walkExpression2(join.on, aliases) || transformed;
|
|
1510
|
+
}
|
|
1511
|
+
if ("expr" in f && f.expr && "ast" in f.expr) {
|
|
1512
|
+
transformed = walkSelect2(f.expr.ast) || transformed;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return transformed;
|
|
1516
|
+
}
|
|
1517
|
+
function walkSelect2(select) {
|
|
1518
|
+
let transformed = false;
|
|
1519
|
+
const aliases = getGenerateSeriesAliases(select.from);
|
|
1520
|
+
if (select.with) {
|
|
1521
|
+
for (const cte of select.with) {
|
|
1522
|
+
const cteSelect = cte.stmt?.ast ?? cte.stmt;
|
|
1523
|
+
if (cteSelect && cteSelect.type === "select") {
|
|
1524
|
+
transformed = walkSelect2(cteSelect) || transformed;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
transformed = walkFrom2(select.from, aliases) || transformed;
|
|
1529
|
+
transformed = walkExpression2(select.where, aliases) || transformed;
|
|
1530
|
+
if (select.having) {
|
|
1531
|
+
if (Array.isArray(select.having)) {
|
|
1532
|
+
for (const h of select.having) {
|
|
1533
|
+
transformed = walkExpression2(h, aliases) || transformed;
|
|
1534
|
+
}
|
|
1535
|
+
} else {
|
|
1536
|
+
transformed = walkExpression2(select.having, aliases) || transformed;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
if (Array.isArray(select.columns)) {
|
|
1540
|
+
for (const col of select.columns) {
|
|
1541
|
+
if ("expr" in col) {
|
|
1542
|
+
transformed = walkExpression2(col.expr, aliases) || transformed;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (Array.isArray(select.groupby)) {
|
|
1547
|
+
for (const g of select.groupby) {
|
|
1548
|
+
transformed = walkExpression2(g, aliases) || transformed;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (Array.isArray(select.orderby)) {
|
|
1552
|
+
for (const order of select.orderby) {
|
|
1553
|
+
if (order.expr) {
|
|
1554
|
+
transformed = walkExpression2(order.expr, aliases) || transformed;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (select._orderby) {
|
|
1559
|
+
for (const order of select._orderby) {
|
|
1560
|
+
if (order.expr) {
|
|
1561
|
+
transformed = walkExpression2(order.expr, aliases) || transformed;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
if (select._next) {
|
|
1566
|
+
transformed = walkSelect2(select._next) || transformed;
|
|
1567
|
+
}
|
|
1568
|
+
return transformed;
|
|
1569
|
+
}
|
|
1570
|
+
function rewriteGenerateSeriesAliases(ast) {
|
|
1571
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
1572
|
+
let transformed = false;
|
|
1573
|
+
for (const stmt of statements) {
|
|
1574
|
+
if (stmt.type === "select") {
|
|
1575
|
+
transformed = walkSelect2(stmt) || transformed;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return transformed;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// src/sql/visitors/union-with-hoister.ts
|
|
1582
|
+
function getCteName(cte) {
|
|
1583
|
+
const nameObj = cte.name;
|
|
1584
|
+
if (!nameObj)
|
|
1585
|
+
return null;
|
|
1586
|
+
const value = nameObj.value;
|
|
1587
|
+
if (typeof value === "string")
|
|
1588
|
+
return value;
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
function hoistWithInSelect(select) {
|
|
1592
|
+
if (!select.set_op || !select._next)
|
|
1593
|
+
return false;
|
|
1594
|
+
const arms = [];
|
|
1595
|
+
let current = select;
|
|
1596
|
+
while (current && current.type === "select") {
|
|
1597
|
+
arms.push(current);
|
|
1598
|
+
current = current._next;
|
|
1599
|
+
}
|
|
1600
|
+
const mergedWith = [];
|
|
1601
|
+
const seen = new Set;
|
|
1602
|
+
let hasWithBeyondFirst = false;
|
|
1603
|
+
for (const arm of arms) {
|
|
1604
|
+
if (arm.with && arm.with.length > 0) {
|
|
1605
|
+
if (arm !== arms[0]) {
|
|
1606
|
+
hasWithBeyondFirst = true;
|
|
1607
|
+
}
|
|
1608
|
+
for (const cte of arm.with) {
|
|
1609
|
+
const cteName = getCteName(cte);
|
|
1610
|
+
if (!cteName)
|
|
1611
|
+
return false;
|
|
1612
|
+
if (seen.has(cteName)) {
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
seen.add(cteName);
|
|
1616
|
+
mergedWith.push(cte);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (!hasWithBeyondFirst)
|
|
1621
|
+
return false;
|
|
1622
|
+
arms[0].with = mergedWith;
|
|
1623
|
+
if ("parentheses_symbol" in arms[0]) {
|
|
1624
|
+
arms[0].parentheses_symbol = false;
|
|
1625
|
+
}
|
|
1626
|
+
for (let i = 1;i < arms.length; i++) {
|
|
1627
|
+
arms[i].with = null;
|
|
1628
|
+
}
|
|
1629
|
+
return true;
|
|
1630
|
+
}
|
|
1631
|
+
function walkSelect3(select) {
|
|
1632
|
+
let transformed = false;
|
|
1633
|
+
if (select.with) {
|
|
1634
|
+
for (const cte of select.with) {
|
|
1635
|
+
const cteSelect = cte.stmt?.ast ?? cte.stmt;
|
|
1636
|
+
if (cteSelect && cteSelect.type === "select") {
|
|
1637
|
+
transformed = walkSelect3(cteSelect) || transformed;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (Array.isArray(select.from)) {
|
|
1642
|
+
for (const from of select.from) {
|
|
1643
|
+
if ("expr" in from && from.expr && "ast" in from.expr) {
|
|
1644
|
+
transformed = walkSelect3(from.expr.ast) || transformed;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
transformed = hoistWithInSelect(select) || transformed;
|
|
1649
|
+
if (select._next) {
|
|
1650
|
+
transformed = walkSelect3(select._next) || transformed;
|
|
1651
|
+
}
|
|
1652
|
+
return transformed;
|
|
1653
|
+
}
|
|
1654
|
+
function hoistUnionWith(ast) {
|
|
1655
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
1656
|
+
let transformed = false;
|
|
1657
|
+
for (const stmt of statements) {
|
|
1658
|
+
if (stmt.type === "select") {
|
|
1659
|
+
transformed = walkSelect3(stmt) || transformed;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
return transformed;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1373
1665
|
// src/sql/ast-transformer.ts
|
|
1374
1666
|
var { Parser } = nodeSqlParser;
|
|
1375
1667
|
var parser = new Parser;
|
|
@@ -1404,7 +1696,9 @@ function debugLog(message, payload) {
|
|
|
1404
1696
|
function transformSQL(query) {
|
|
1405
1697
|
const needsArrayTransform = query.includes("@>") || query.includes("<@") || query.includes("&&");
|
|
1406
1698
|
const needsJoinTransform = hasJoin(query) || /\bupdate\b/i.test(query) || /\bdelete\b/i.test(query);
|
|
1407
|
-
|
|
1699
|
+
const needsUnionTransform = /\bunion\b/i.test(query) || /\bintersect\b/i.test(query) || /\bexcept\b/i.test(query);
|
|
1700
|
+
const needsGenerateSeriesTransform = /\bgenerate_series\b/i.test(query);
|
|
1701
|
+
if (!needsArrayTransform && !needsJoinTransform && !needsUnionTransform && !needsGenerateSeriesTransform) {
|
|
1408
1702
|
return { sql: query, transformed: false };
|
|
1409
1703
|
}
|
|
1410
1704
|
return getCachedOrTransform(query, () => {
|
|
@@ -1417,6 +1711,12 @@ function transformSQL(query) {
|
|
|
1417
1711
|
if (needsJoinTransform) {
|
|
1418
1712
|
transformed = qualifyJoinColumns(ast) || transformed;
|
|
1419
1713
|
}
|
|
1714
|
+
if (needsGenerateSeriesTransform) {
|
|
1715
|
+
transformed = rewriteGenerateSeriesAliases(ast) || transformed;
|
|
1716
|
+
}
|
|
1717
|
+
if (needsUnionTransform) {
|
|
1718
|
+
transformed = hoistUnionWith(ast) || transformed;
|
|
1719
|
+
}
|
|
1420
1720
|
if (!transformed) {
|
|
1421
1721
|
debugLog("AST parsed but no transformation applied", {
|
|
1422
1722
|
join: needsJoinTransform
|
|
@@ -29,3 +29,5 @@ export declare function getTransformCacheStats(): {
|
|
|
29
29
|
export declare function needsTransformation(query: string): boolean;
|
|
30
30
|
export { transformArrayOperators } from './visitors/array-operators.ts';
|
|
31
31
|
export { qualifyJoinColumns } from './visitors/column-qualifier.ts';
|
|
32
|
+
export { rewriteGenerateSeriesAliases } from './visitors/generate-series-alias.ts';
|
|
33
|
+
export { hoistUnionWith } from './visitors/union-with-hoister.ts';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST visitor to rewrite Postgres style generate_series aliases.
|
|
3
|
+
*
|
|
4
|
+
* Postgres lets you reference a generate_series alias as a column:
|
|
5
|
+
* FROM generate_series(...) AS gs
|
|
6
|
+
* SELECT gs::date
|
|
7
|
+
*
|
|
8
|
+
* DuckDB treats gs as a table alias, and the column is generate_series.
|
|
9
|
+
* This visitor rewrites unqualified column refs that match a
|
|
10
|
+
* generate_series alias to gs.generate_series.
|
|
11
|
+
*/
|
|
12
|
+
import type { AST } from 'node-sql-parser';
|
|
13
|
+
export declare function rewriteGenerateSeriesAliases(ast: AST | AST[]): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST visitor to hoist WITH clauses out of UNION and other set operations.
|
|
3
|
+
*
|
|
4
|
+
* Drizzle can emit SQL like:
|
|
5
|
+
* (with a as (...) select ...) union (with b as (...) select ...)
|
|
6
|
+
*
|
|
7
|
+
* DuckDB 1.4.x has an internal binder bug for this pattern.
|
|
8
|
+
* We merge per arm CTEs into a single top level WITH when names do not collide.
|
|
9
|
+
*/
|
|
10
|
+
import type { AST } from 'node-sql-parser';
|
|
11
|
+
export declare function hoistUnionWith(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.2",
|
|
7
7
|
"description": "A drizzle ORM client for use with DuckDB. Based on drizzle's Postgres client.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"scripts": {
|
|
@@ -17,6 +17,8 @@ import type { AST } from 'node-sql-parser';
|
|
|
17
17
|
|
|
18
18
|
import { transformArrayOperators } from './visitors/array-operators.ts';
|
|
19
19
|
import { qualifyJoinColumns } from './visitors/column-qualifier.ts';
|
|
20
|
+
import { rewriteGenerateSeriesAliases } from './visitors/generate-series-alias.ts';
|
|
21
|
+
import { hoistUnionWith } from './visitors/union-with-hoister.ts';
|
|
20
22
|
|
|
21
23
|
const parser = new Parser();
|
|
22
24
|
|
|
@@ -75,8 +77,18 @@ export function transformSQL(query: string): TransformResult {
|
|
|
75
77
|
query.includes('@>') || query.includes('<@') || query.includes('&&');
|
|
76
78
|
const needsJoinTransform =
|
|
77
79
|
hasJoin(query) || /\bupdate\b/i.test(query) || /\bdelete\b/i.test(query);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
const needsUnionTransform =
|
|
81
|
+
/\bunion\b/i.test(query) ||
|
|
82
|
+
/\bintersect\b/i.test(query) ||
|
|
83
|
+
/\bexcept\b/i.test(query);
|
|
84
|
+
const needsGenerateSeriesTransform = /\bgenerate_series\b/i.test(query);
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
!needsArrayTransform &&
|
|
88
|
+
!needsJoinTransform &&
|
|
89
|
+
!needsUnionTransform &&
|
|
90
|
+
!needsGenerateSeriesTransform
|
|
91
|
+
) {
|
|
80
92
|
return { sql: query, transformed: false };
|
|
81
93
|
}
|
|
82
94
|
|
|
@@ -95,6 +107,14 @@ export function transformSQL(query: string): TransformResult {
|
|
|
95
107
|
transformed = qualifyJoinColumns(ast) || transformed;
|
|
96
108
|
}
|
|
97
109
|
|
|
110
|
+
if (needsGenerateSeriesTransform) {
|
|
111
|
+
transformed = rewriteGenerateSeriesAliases(ast) || transformed;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (needsUnionTransform) {
|
|
115
|
+
transformed = hoistUnionWith(ast) || transformed;
|
|
116
|
+
}
|
|
117
|
+
|
|
98
118
|
if (!transformed) {
|
|
99
119
|
debugLog('AST parsed but no transformation applied', {
|
|
100
120
|
join: needsJoinTransform,
|
|
@@ -135,6 +155,10 @@ export function needsTransformation(query: string): boolean {
|
|
|
135
155
|
query.includes('<@') ||
|
|
136
156
|
query.includes('&&') ||
|
|
137
157
|
lower.includes('join') ||
|
|
158
|
+
lower.includes('union') ||
|
|
159
|
+
lower.includes('intersect') ||
|
|
160
|
+
lower.includes('except') ||
|
|
161
|
+
lower.includes('generate_series') ||
|
|
138
162
|
lower.includes('update') ||
|
|
139
163
|
lower.includes('delete')
|
|
140
164
|
);
|
|
@@ -142,3 +166,5 @@ export function needsTransformation(query: string): boolean {
|
|
|
142
166
|
|
|
143
167
|
export { transformArrayOperators } from './visitors/array-operators.ts';
|
|
144
168
|
export { qualifyJoinColumns } from './visitors/column-qualifier.ts';
|
|
169
|
+
export { rewriteGenerateSeriesAliases } from './visitors/generate-series-alias.ts';
|
|
170
|
+
export { hoistUnionWith } from './visitors/union-with-hoister.ts';
|
|
@@ -181,6 +181,27 @@ function walkOnClause(
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
if (
|
|
185
|
+
expr.operator === '=' &&
|
|
186
|
+
leftCol &&
|
|
187
|
+
rightCol &&
|
|
188
|
+
leftColName &&
|
|
189
|
+
rightColName &&
|
|
190
|
+
leftColName !== rightColName
|
|
191
|
+
) {
|
|
192
|
+
if (leftQualified && rightUnqualified && !rightColName.includes('.')) {
|
|
193
|
+
applyQualifier(rightCol, rightQualifier);
|
|
194
|
+
transformed = true;
|
|
195
|
+
} else if (
|
|
196
|
+
leftUnqualified &&
|
|
197
|
+
rightQualified &&
|
|
198
|
+
!leftColName.includes('.')
|
|
199
|
+
) {
|
|
200
|
+
applyQualifier(leftCol, leftQualifier);
|
|
201
|
+
transformed = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
184
205
|
transformed =
|
|
185
206
|
walkOnClause(
|
|
186
207
|
isBinaryExpr(expr.left as Binary)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST visitor to rewrite Postgres style generate_series aliases.
|
|
3
|
+
*
|
|
4
|
+
* Postgres lets you reference a generate_series alias as a column:
|
|
5
|
+
* FROM generate_series(...) AS gs
|
|
6
|
+
* SELECT gs::date
|
|
7
|
+
*
|
|
8
|
+
* DuckDB treats gs as a table alias, and the column is generate_series.
|
|
9
|
+
* This visitor rewrites unqualified column refs that match a
|
|
10
|
+
* generate_series alias to gs.generate_series.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
AST,
|
|
15
|
+
Binary,
|
|
16
|
+
ColumnRefItem,
|
|
17
|
+
ExpressionValue,
|
|
18
|
+
From,
|
|
19
|
+
Join,
|
|
20
|
+
Select,
|
|
21
|
+
OrderBy,
|
|
22
|
+
Column,
|
|
23
|
+
} from 'node-sql-parser';
|
|
24
|
+
|
|
25
|
+
function getColumnName(col: ColumnRefItem): string | null {
|
|
26
|
+
if (typeof col.column === 'string') {
|
|
27
|
+
return col.column;
|
|
28
|
+
}
|
|
29
|
+
if (col.column && 'expr' in col.column && col.column.expr?.value) {
|
|
30
|
+
return String(col.column.expr.value);
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isColumnRef(expr: ExpressionValue): expr is ColumnRefItem {
|
|
36
|
+
return (
|
|
37
|
+
typeof expr === 'object' &&
|
|
38
|
+
expr !== null &&
|
|
39
|
+
'type' in expr &&
|
|
40
|
+
expr.type === 'column_ref'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isBinaryExpr(
|
|
45
|
+
expr: ExpressionValue | Binary | null | undefined
|
|
46
|
+
): expr is Binary {
|
|
47
|
+
return (
|
|
48
|
+
!!expr &&
|
|
49
|
+
typeof expr === 'object' &&
|
|
50
|
+
'type' in expr &&
|
|
51
|
+
(expr as { type?: string }).type === 'binary_expr'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getGenerateSeriesAliases(from: Select['from']): Set<string> {
|
|
56
|
+
const aliases = new Set<string>();
|
|
57
|
+
if (!from || !Array.isArray(from)) return aliases;
|
|
58
|
+
|
|
59
|
+
for (const f of from) {
|
|
60
|
+
if ('expr' in f && f.expr && typeof f.expr === 'object') {
|
|
61
|
+
const exprObj = f.expr as Record<string, unknown>;
|
|
62
|
+
if (exprObj.type === 'function' && 'name' in exprObj) {
|
|
63
|
+
const nameObj = exprObj.name as Record<string, unknown> | undefined;
|
|
64
|
+
const nameParts = nameObj?.name as
|
|
65
|
+
| Array<Record<string, unknown>>
|
|
66
|
+
| undefined;
|
|
67
|
+
const fnName = nameParts?.[0]?.value;
|
|
68
|
+
if (
|
|
69
|
+
typeof fnName === 'string' &&
|
|
70
|
+
fnName.toLowerCase() === 'generate_series'
|
|
71
|
+
) {
|
|
72
|
+
const alias = typeof f.as === 'string' ? f.as : null;
|
|
73
|
+
if (alias && !alias.includes('(')) {
|
|
74
|
+
aliases.add(alias);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return aliases;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rewriteAliasColumnRef(col: ColumnRefItem, alias: string): void {
|
|
85
|
+
col.table = alias;
|
|
86
|
+
col.column = { expr: { type: 'default', value: 'generate_series' } };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function walkExpression(
|
|
90
|
+
expr: ExpressionValue | null | undefined,
|
|
91
|
+
aliases: Set<string>
|
|
92
|
+
): boolean {
|
|
93
|
+
if (!expr || typeof expr !== 'object') return false;
|
|
94
|
+
|
|
95
|
+
let transformed = false;
|
|
96
|
+
const exprObj = expr as Record<string, unknown>;
|
|
97
|
+
|
|
98
|
+
if (isColumnRef(expr)) {
|
|
99
|
+
if (!('table' in expr) || !expr.table) {
|
|
100
|
+
const colName = getColumnName(expr);
|
|
101
|
+
if (colName && aliases.has(colName)) {
|
|
102
|
+
rewriteAliasColumnRef(expr, colName);
|
|
103
|
+
transformed = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return transformed;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isBinaryExpr(expr)) {
|
|
110
|
+
const binary = expr as Binary;
|
|
111
|
+
transformed =
|
|
112
|
+
walkExpression(binary.left as ExpressionValue, aliases) || transformed;
|
|
113
|
+
transformed =
|
|
114
|
+
walkExpression(binary.right as ExpressionValue, aliases) || transformed;
|
|
115
|
+
return transformed;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (exprObj.type === 'unary_expr' && exprObj.expr) {
|
|
119
|
+
transformed =
|
|
120
|
+
walkExpression(exprObj.expr as ExpressionValue, aliases) || transformed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (exprObj.type === 'cast' && exprObj.expr) {
|
|
124
|
+
transformed =
|
|
125
|
+
walkExpression(exprObj.expr as ExpressionValue, aliases) || transformed;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (exprObj.type === 'case') {
|
|
129
|
+
if (exprObj.expr) {
|
|
130
|
+
transformed =
|
|
131
|
+
walkExpression(exprObj.expr as ExpressionValue, aliases) || transformed;
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(exprObj.args)) {
|
|
134
|
+
for (const whenClause of exprObj.args as Array<Record<string, unknown>>) {
|
|
135
|
+
if (whenClause.cond) {
|
|
136
|
+
transformed =
|
|
137
|
+
walkExpression(whenClause.cond as ExpressionValue, aliases) ||
|
|
138
|
+
transformed;
|
|
139
|
+
}
|
|
140
|
+
if (whenClause.result) {
|
|
141
|
+
transformed =
|
|
142
|
+
walkExpression(whenClause.result as ExpressionValue, aliases) ||
|
|
143
|
+
transformed;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if ('args' in exprObj && exprObj.args) {
|
|
150
|
+
const args = exprObj.args as Record<string, unknown>;
|
|
151
|
+
if (Array.isArray(args.value)) {
|
|
152
|
+
for (const arg of args.value as ExpressionValue[]) {
|
|
153
|
+
transformed = walkExpression(arg, aliases) || transformed;
|
|
154
|
+
}
|
|
155
|
+
} else if (args.expr) {
|
|
156
|
+
transformed =
|
|
157
|
+
walkExpression(args.expr as ExpressionValue, aliases) || transformed;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if ('over' in exprObj && exprObj.over && typeof exprObj.over === 'object') {
|
|
162
|
+
const over = exprObj.over as Record<string, unknown>;
|
|
163
|
+
if (Array.isArray(over.partition)) {
|
|
164
|
+
for (const part of over.partition as ExpressionValue[]) {
|
|
165
|
+
transformed = walkExpression(part, aliases) || transformed;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (Array.isArray(over.orderby)) {
|
|
169
|
+
for (const order of over.orderby as ExpressionValue[]) {
|
|
170
|
+
transformed = walkExpression(order, aliases) || transformed;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if ('ast' in exprObj && exprObj.ast) {
|
|
176
|
+
const subAst = exprObj.ast as Select;
|
|
177
|
+
if (subAst.type === 'select') {
|
|
178
|
+
transformed = walkSelect(subAst) || transformed;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (exprObj.type === 'expr_list' && Array.isArray(exprObj.value)) {
|
|
183
|
+
for (const item of exprObj.value as ExpressionValue[]) {
|
|
184
|
+
transformed = walkExpression(item, aliases) || transformed;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return transformed;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function walkFrom(from: Select['from'], aliases: Set<string>): boolean {
|
|
192
|
+
if (!from || !Array.isArray(from)) return false;
|
|
193
|
+
|
|
194
|
+
let transformed = false;
|
|
195
|
+
|
|
196
|
+
for (const f of from) {
|
|
197
|
+
if ('join' in f) {
|
|
198
|
+
const join = f as Join;
|
|
199
|
+
transformed =
|
|
200
|
+
walkExpression(join.on as ExpressionValue, aliases) || transformed;
|
|
201
|
+
}
|
|
202
|
+
if ('expr' in f && f.expr && 'ast' in f.expr) {
|
|
203
|
+
transformed = walkSelect(f.expr.ast as Select) || transformed;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return transformed;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function walkSelect(select: Select): boolean {
|
|
211
|
+
let transformed = false;
|
|
212
|
+
const aliases = getGenerateSeriesAliases(select.from);
|
|
213
|
+
|
|
214
|
+
if (select.with) {
|
|
215
|
+
for (const cte of select.with) {
|
|
216
|
+
const cteSelect = cte.stmt?.ast ?? cte.stmt;
|
|
217
|
+
if (cteSelect && cteSelect.type === 'select') {
|
|
218
|
+
transformed = walkSelect(cteSelect as Select) || transformed;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
transformed = walkFrom(select.from, aliases) || transformed;
|
|
224
|
+
|
|
225
|
+
transformed = walkExpression(select.where, aliases) || transformed;
|
|
226
|
+
|
|
227
|
+
if (select.having) {
|
|
228
|
+
if (Array.isArray(select.having)) {
|
|
229
|
+
for (const h of select.having) {
|
|
230
|
+
transformed =
|
|
231
|
+
walkExpression(h as ExpressionValue, aliases) || transformed;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
transformed =
|
|
235
|
+
walkExpression(select.having as ExpressionValue, aliases) ||
|
|
236
|
+
transformed;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (Array.isArray(select.columns)) {
|
|
241
|
+
for (const col of select.columns as Column[]) {
|
|
242
|
+
if ('expr' in col) {
|
|
243
|
+
transformed =
|
|
244
|
+
walkExpression(col.expr as ExpressionValue, aliases) || transformed;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (Array.isArray(select.groupby)) {
|
|
250
|
+
for (const g of select.groupby as ExpressionValue[]) {
|
|
251
|
+
transformed = walkExpression(g, aliases) || transformed;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (Array.isArray(select.orderby)) {
|
|
256
|
+
for (const order of select.orderby as OrderBy[]) {
|
|
257
|
+
if (order.expr) {
|
|
258
|
+
transformed =
|
|
259
|
+
walkExpression(order.expr as ExpressionValue, aliases) || transformed;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (select._orderby) {
|
|
265
|
+
for (const order of select._orderby as OrderBy[]) {
|
|
266
|
+
if (order.expr) {
|
|
267
|
+
transformed =
|
|
268
|
+
walkExpression(order.expr as ExpressionValue, aliases) || transformed;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (select._next) {
|
|
274
|
+
transformed = walkSelect(select._next) || transformed;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return transformed;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function rewriteGenerateSeriesAliases(ast: AST | AST[]): boolean {
|
|
281
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
282
|
+
let transformed = false;
|
|
283
|
+
|
|
284
|
+
for (const stmt of statements) {
|
|
285
|
+
if (stmt.type === 'select') {
|
|
286
|
+
transformed = walkSelect(stmt as Select) || transformed;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return transformed;
|
|
291
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST visitor to hoist WITH clauses out of UNION and other set operations.
|
|
3
|
+
*
|
|
4
|
+
* Drizzle can emit SQL like:
|
|
5
|
+
* (with a as (...) select ...) union (with b as (...) select ...)
|
|
6
|
+
*
|
|
7
|
+
* DuckDB 1.4.x has an internal binder bug for this pattern.
|
|
8
|
+
* We merge per arm CTEs into a single top level WITH when names do not collide.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AST, Select, From } from 'node-sql-parser';
|
|
12
|
+
|
|
13
|
+
function getCteName(cte: { name?: unknown }): string | null {
|
|
14
|
+
const nameObj = cte.name as Record<string, unknown> | undefined;
|
|
15
|
+
if (!nameObj) return null;
|
|
16
|
+
const value = nameObj.value;
|
|
17
|
+
if (typeof value === 'string') return value;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hoistWithInSelect(select: Select): boolean {
|
|
22
|
+
if (!select.set_op || !select._next) return false;
|
|
23
|
+
|
|
24
|
+
const arms: Select[] = [];
|
|
25
|
+
let current: Select | null = select;
|
|
26
|
+
while (current && current.type === 'select') {
|
|
27
|
+
arms.push(current);
|
|
28
|
+
current = current._next as Select | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const mergedWith: NonNullable<Select['with']> = [];
|
|
32
|
+
const seen = new Set<string>();
|
|
33
|
+
let hasWithBeyondFirst = false;
|
|
34
|
+
|
|
35
|
+
for (const arm of arms) {
|
|
36
|
+
if (arm.with && arm.with.length > 0) {
|
|
37
|
+
if (arm !== arms[0]) {
|
|
38
|
+
hasWithBeyondFirst = true;
|
|
39
|
+
}
|
|
40
|
+
for (const cte of arm.with) {
|
|
41
|
+
const cteName = getCteName(cte);
|
|
42
|
+
if (!cteName) return false;
|
|
43
|
+
if (seen.has(cteName)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
seen.add(cteName);
|
|
47
|
+
mergedWith.push(cte);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!hasWithBeyondFirst) return false;
|
|
53
|
+
|
|
54
|
+
arms[0].with = mergedWith;
|
|
55
|
+
if ('parentheses_symbol' in arms[0]) {
|
|
56
|
+
(arms[0] as Select & { parentheses_symbol?: boolean }).parentheses_symbol =
|
|
57
|
+
false;
|
|
58
|
+
}
|
|
59
|
+
for (let i = 1; i < arms.length; i++) {
|
|
60
|
+
arms[i].with = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function walkSelect(select: Select): boolean {
|
|
67
|
+
let transformed = false;
|
|
68
|
+
|
|
69
|
+
if (select.with) {
|
|
70
|
+
for (const cte of select.with) {
|
|
71
|
+
const cteSelect = cte.stmt?.ast ?? cte.stmt;
|
|
72
|
+
if (cteSelect && cteSelect.type === 'select') {
|
|
73
|
+
transformed = walkSelect(cteSelect as Select) || transformed;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (Array.isArray(select.from)) {
|
|
79
|
+
for (const from of select.from as From[]) {
|
|
80
|
+
if ('expr' in from && from.expr && 'ast' in from.expr) {
|
|
81
|
+
transformed = walkSelect(from.expr.ast as Select) || transformed;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
transformed = hoistWithInSelect(select) || transformed;
|
|
87
|
+
|
|
88
|
+
if (select._next) {
|
|
89
|
+
transformed = walkSelect(select._next) || transformed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return transformed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function hoistUnionWith(ast: AST | AST[]): boolean {
|
|
96
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
97
|
+
let transformed = false;
|
|
98
|
+
|
|
99
|
+
for (const stmt of statements) {
|
|
100
|
+
if (stmt.type === 'select') {
|
|
101
|
+
transformed = walkSelect(stmt as Select) || transformed;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return transformed;
|
|
106
|
+
}
|