@leonardovida-md/drizzle-neo-duckdb 1.2.1 → 1.3.0

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 CHANGED
@@ -19,6 +19,8 @@ Works with local DuckDB files, in-memory databases, and [MotherDuck](https://mot
19
19
 
20
20
  > **Status:** Experimental. Core query building, migrations, and type inference work well. Some DuckDB-specific types and edge cases are still being refined.
21
21
 
22
+ > **Note:** The NPM package is `@leonardovida-md/drizzle-neo-duckdb` while the repository is `drizzle-duckdb`. This is due to a migration to preserve the existing NPM package name.
23
+
22
24
  Docs tip: every docs page has a **Markdown (raw)** button for LLM-friendly source.
23
25
 
24
26
  ## Installation
@@ -40,6 +42,7 @@ pnpm add @leonardovida-md/drizzle-neo-duckdb @duckdb/node-api
40
42
  ```typescript
41
43
  import { DuckDBInstance } from '@duckdb/node-api';
42
44
  import { drizzle } from '@leonardovida-md/drizzle-neo-duckdb';
45
+ import { sql } from 'drizzle-orm';
43
46
  import { integer, text, pgTable } from 'drizzle-orm/pg-core';
44
47
 
45
48
  // Connect to DuckDB
@@ -149,7 +152,7 @@ const db = drizzle(pool);
149
152
  ## Schema & Types
150
153
 
151
154
  - 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`.
155
+ - DuckDB-specific helpers: `duckDbList`, `duckDbArray`, `duckDbStruct`, `duckDbMap`, `duckDbJson`, `duckDbBlob`, `duckDbInet`, `duckDbInterval`, `duckDbTimestamp`, `duckDbDate`, `duckDbTime`.
153
156
  - Browser-safe imports live under `@leonardovida-md/drizzle-neo-duckdb/helpers` (introspection emits this path).
154
157
 
155
158
  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
- if (!needsArrayTransform && !needsJoinTransform) {
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
- if (!needsArrayTransform && !needsJoinTransform) {
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.1",
6
+ "version": "1.3.0",
7
7
  "description": "A drizzle ORM client for use with DuckDB. Based on drizzle's Postgres client.",
8
8
  "type": "module",
9
9
  "scripts": {
@@ -62,7 +62,7 @@
62
62
  "engines": {
63
63
  "node": ">=18.17"
64
64
  },
65
- "packageManager": "bun@1.2.2",
65
+ "packageManager": "bun@1.3.6",
66
66
  "keywords": [
67
67
  "drizzle",
68
68
  "duckdb"
@@ -79,7 +79,6 @@
79
79
  "dist/**/*.d.ts"
80
80
  ],
81
81
  "dependencies": {
82
- "@duckdb/node-bindings-darwin-arm64": "^1.4.2-r.1",
83
82
  "node-sql-parser": "^5.3.13"
84
83
  }
85
84
  }
package/src/driver.ts CHANGED
@@ -269,8 +269,8 @@ export function drizzle<
269
269
 
270
270
  export class DuckDBDatabase<
271
271
  TFullSchema extends Record<string, unknown> = Record<string, never>,
272
- TSchema extends
273
- TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
272
+ TSchema extends TablesRelationalConfig =
273
+ ExtractTablesWithRelations<TFullSchema>,
274
274
  > extends PgDatabase<DuckDBQueryResultHKT, TFullSchema, TSchema> {
275
275
  static readonly [entityKind]: string = 'DuckDBDatabase';
276
276
 
@@ -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
- if (!needsArrayTransform && !needsJoinTransform) {
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
+ }
@@ -23,35 +23,47 @@ export interface DuckDBValueWrapper<
23
23
  readonly data: TData;
24
24
  }
25
25
 
26
- export interface ListValueWrapper
27
- extends DuckDBValueWrapper<'list', unknown[]> {
26
+ export interface ListValueWrapper extends DuckDBValueWrapper<
27
+ 'list',
28
+ unknown[]
29
+ > {
28
30
  readonly elementType?: string;
29
31
  }
30
32
 
31
- export interface ArrayValueWrapper
32
- extends DuckDBValueWrapper<'array', unknown[]> {
33
+ export interface ArrayValueWrapper extends DuckDBValueWrapper<
34
+ 'array',
35
+ unknown[]
36
+ > {
33
37
  readonly elementType?: string;
34
38
  readonly fixedLength?: number;
35
39
  }
36
40
 
37
- export interface StructValueWrapper
38
- extends DuckDBValueWrapper<'struct', Record<string, unknown>> {
41
+ export interface StructValueWrapper extends DuckDBValueWrapper<
42
+ 'struct',
43
+ Record<string, unknown>
44
+ > {
39
45
  readonly schema?: Record<string, string>;
40
46
  }
41
47
 
42
- export interface MapValueWrapper
43
- extends DuckDBValueWrapper<'map', Record<string, unknown>> {
48
+ export interface MapValueWrapper extends DuckDBValueWrapper<
49
+ 'map',
50
+ Record<string, unknown>
51
+ > {
44
52
  readonly valueType?: string;
45
53
  }
46
54
 
47
- export interface TimestampValueWrapper
48
- extends DuckDBValueWrapper<'timestamp', Date | string | number | bigint> {
55
+ export interface TimestampValueWrapper extends DuckDBValueWrapper<
56
+ 'timestamp',
57
+ Date | string | number | bigint
58
+ > {
49
59
  readonly withTimezone: boolean;
50
60
  readonly precision?: number;
51
61
  }
52
62
 
53
- export interface BlobValueWrapper
54
- extends DuckDBValueWrapper<'blob', Buffer | Uint8Array> {}
63
+ export interface BlobValueWrapper extends DuckDBValueWrapper<
64
+ 'blob',
65
+ Buffer | Uint8Array
66
+ > {}
55
67
 
56
68
  export interface JsonValueWrapper extends DuckDBValueWrapper<'json', unknown> {}
57
69