@leonardovida-md/drizzle-neo-duckdb 1.0.1 → 1.0.3

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.
@@ -311,8 +311,78 @@ import { TransactionRollbackError } from "drizzle-orm/errors";
311
311
 
312
312
  // src/client.ts
313
313
  import {
314
- listValue
314
+ listValue as listValue2,
315
+ timestampValue as timestampValue2
315
316
  } from "@duckdb/node-api";
317
+
318
+ // src/value-wrappers.ts
319
+ import {
320
+ listValue,
321
+ arrayValue,
322
+ structValue,
323
+ mapValue,
324
+ blobValue,
325
+ timestampValue,
326
+ timestampTZValue
327
+ } from "@duckdb/node-api";
328
+ var DUCKDB_VALUE_MARKER = Symbol.for("drizzle-duckdb:value");
329
+ function dateToMicros(value) {
330
+ if (value instanceof Date) {
331
+ return BigInt(value.getTime()) * 1000n;
332
+ }
333
+ let normalized = value;
334
+ if (!value.includes("T") && value.includes(" ")) {
335
+ normalized = value.replace(" ", "T");
336
+ }
337
+ if (!normalized.endsWith("Z") && !/[+-]\d{2}:?\d{2}$/.test(normalized)) {
338
+ normalized += "Z";
339
+ }
340
+ const date = new Date(normalized);
341
+ if (isNaN(date.getTime())) {
342
+ throw new Error(`Invalid timestamp string: ${value}`);
343
+ }
344
+ return BigInt(date.getTime()) * 1000n;
345
+ }
346
+ function toUint8Array(data) {
347
+ return data instanceof Uint8Array && !(data instanceof Buffer) ? data : new Uint8Array(data);
348
+ }
349
+ function convertStructEntries(data, toValue) {
350
+ const entries = {};
351
+ for (const [key, val] of Object.entries(data)) {
352
+ entries[key] = toValue(val);
353
+ }
354
+ return entries;
355
+ }
356
+ function convertMapEntries(data, toValue) {
357
+ return Object.entries(data).map(([key, val]) => ({
358
+ key,
359
+ value: toValue(val)
360
+ }));
361
+ }
362
+ function wrapperToNodeApiValue(wrapper, toValue) {
363
+ switch (wrapper.kind) {
364
+ case "list":
365
+ return listValue(wrapper.data.map(toValue));
366
+ case "array":
367
+ return arrayValue(wrapper.data.map(toValue));
368
+ case "struct":
369
+ return structValue(convertStructEntries(wrapper.data, toValue));
370
+ case "map":
371
+ return mapValue(convertMapEntries(wrapper.data, toValue));
372
+ case "timestamp":
373
+ return wrapper.withTimezone ? timestampTZValue(dateToMicros(wrapper.data)) : timestampValue(dateToMicros(wrapper.data));
374
+ case "blob":
375
+ return blobValue(toUint8Array(wrapper.data));
376
+ case "json":
377
+ return JSON.stringify(wrapper.data);
378
+ default: {
379
+ const _exhaustive = wrapper;
380
+ throw new Error(`Unknown wrapper kind: ${_exhaustive.kind}`);
381
+ }
382
+ }
383
+ }
384
+
385
+ // src/client.ts
316
386
  function isPgArrayLiteral(value) {
317
387
  return value.startsWith("{") && value.endsWith("}");
318
388
  }
@@ -341,30 +411,78 @@ function prepareParams(params, options = {}) {
341
411
  });
342
412
  }
343
413
  function toNodeApiValue(value) {
414
+ if (value == null)
415
+ return null;
416
+ const t = typeof value;
417
+ if (t === "string" || t === "number" || t === "bigint" || t === "boolean") {
418
+ return value;
419
+ }
420
+ if (t === "object" && DUCKDB_VALUE_MARKER in value) {
421
+ return wrapperToNodeApiValue(value, toNodeApiValue);
422
+ }
344
423
  if (Array.isArray(value)) {
345
- return listValue(value.map((inner) => toNodeApiValue(inner)));
424
+ return listValue2(value.map((inner) => toNodeApiValue(inner)));
425
+ }
426
+ if (value instanceof Date) {
427
+ return timestampValue2(BigInt(value.getTime()) * 1000n);
346
428
  }
347
429
  return value;
348
430
  }
349
- async function executeOnClient(client, query, params) {
350
- const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
351
- const result = await client.run(query, values);
352
- const rows = await result.getRowsJS();
353
- const columns = result.columnNames();
431
+ function deduplicateColumns(columns) {
354
432
  const seen = {};
355
- const uniqueColumns = columns.map((col) => {
433
+ return columns.map((col) => {
356
434
  const count = seen[col] ?? 0;
357
435
  seen[col] = count + 1;
358
436
  return count === 0 ? col : `${col}_${count}`;
359
437
  });
360
- return (rows ?? []).map((vals) => {
438
+ }
439
+ function mapRowsToObjects(columns, rows) {
440
+ return rows.map((vals) => {
361
441
  const obj = {};
362
- uniqueColumns.forEach((col, idx) => {
442
+ columns.forEach((col, idx) => {
363
443
  obj[col] = vals[idx];
364
444
  });
365
445
  return obj;
366
446
  });
367
447
  }
448
+ async function executeOnClient(client, query, params) {
449
+ const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
450
+ const result = await client.run(query, values);
451
+ const rows = await result.getRowsJS();
452
+ const columns = result.deduplicatedColumnNames?.() ?? result.columnNames();
453
+ const uniqueColumns = deduplicateColumns(columns);
454
+ return rows ? mapRowsToObjects(uniqueColumns, rows) : [];
455
+ }
456
+ async function* executeInBatches(client, query, params, options = {}) {
457
+ const rowsPerChunk = options.rowsPerChunk && options.rowsPerChunk > 0 ? options.rowsPerChunk : 1e5;
458
+ const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
459
+ const result = await client.stream(query, values);
460
+ const columns = result.deduplicatedColumnNames?.() ?? result.columnNames();
461
+ const uniqueColumns = deduplicateColumns(columns);
462
+ let buffer = [];
463
+ for await (const chunk of result.yieldRowsJs()) {
464
+ const objects = mapRowsToObjects(uniqueColumns, chunk);
465
+ for (const row of objects) {
466
+ buffer.push(row);
467
+ if (buffer.length >= rowsPerChunk) {
468
+ yield buffer;
469
+ buffer = [];
470
+ }
471
+ }
472
+ }
473
+ if (buffer.length > 0) {
474
+ yield buffer;
475
+ }
476
+ }
477
+ async function executeArrowOnClient(client, query, params) {
478
+ const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
479
+ const result = await client.run(query, values);
480
+ const maybeArrow = result.toArrow ?? result.getArrowTable;
481
+ if (typeof maybeArrow === "function") {
482
+ return await maybeArrow.call(result);
483
+ }
484
+ return result.getColumnsObjectJS();
485
+ }
368
486
 
369
487
  // src/session.ts
370
488
  class DuckDBPreparedQuery extends PgPreparedQuery {
@@ -405,11 +523,7 @@ class DuckDBPreparedQuery extends PgPreparedQuery {
405
523
  this.logger.logQuery(`[duckdb] original query before array rewrite: ${this.queryString}`, params);
406
524
  }
407
525
  this.logger.logQuery(rewrittenQuery, params);
408
- const {
409
- fields,
410
- joinsNotNullableMap,
411
- customResultMapper
412
- } = this;
526
+ const { fields, joinsNotNullableMap, customResultMapper } = this;
413
527
  const rows = await executeOnClient(this.client, rewrittenQuery, params);
414
528
  if (rows.length === 0 || !fields) {
415
529
  return rows;
@@ -469,6 +583,32 @@ class DuckDBSession extends PgSession {
469
583
  this.logger.logQuery(`[duckdb] ${arrayLiteralWarning}
470
584
  query: ${query}`, []);
471
585
  };
586
+ executeBatches(query, options = {}) {
587
+ const builtQuery = this.dialect.sqlToQuery(query);
588
+ const params = prepareParams(builtQuery.params, {
589
+ rejectStringArrayLiterals: this.rejectStringArrayLiterals,
590
+ warnOnStringArrayLiteral: this.rejectStringArrayLiterals ? undefined : () => this.warnOnStringArrayLiteral(builtQuery.sql)
591
+ });
592
+ const rewrittenQuery = this.rewriteArrays ? adaptArrayOperators(builtQuery.sql) : builtQuery.sql;
593
+ if (this.rewriteArrays && rewrittenQuery !== builtQuery.sql) {
594
+ this.logger.logQuery(`[duckdb] original query before array rewrite: ${builtQuery.sql}`, params);
595
+ }
596
+ this.logger.logQuery(rewrittenQuery, params);
597
+ return executeInBatches(this.client, rewrittenQuery, params, options);
598
+ }
599
+ async executeArrow(query) {
600
+ const builtQuery = this.dialect.sqlToQuery(query);
601
+ const params = prepareParams(builtQuery.params, {
602
+ rejectStringArrayLiterals: this.rejectStringArrayLiterals,
603
+ warnOnStringArrayLiteral: this.rejectStringArrayLiterals ? undefined : () => this.warnOnStringArrayLiteral(builtQuery.sql)
604
+ });
605
+ const rewrittenQuery = this.rewriteArrays ? adaptArrayOperators(builtQuery.sql) : builtQuery.sql;
606
+ if (this.rewriteArrays && rewrittenQuery !== builtQuery.sql) {
607
+ this.logger.logQuery(`[duckdb] original query before array rewrite: ${builtQuery.sql}`, params);
608
+ }
609
+ this.logger.logQuery(rewrittenQuery, params);
610
+ return executeArrowOnClient(this.client, rewrittenQuery, params);
611
+ }
472
612
  }
473
613
 
474
614
  class DuckDBTransaction extends PgTransaction {
@@ -492,6 +632,12 @@ class DuckDBTransaction extends PgTransaction {
492
632
  setTransaction(config) {
493
633
  return this.session.execute(sql`set transaction ${this.getTransactionConfigSQL(config)}`);
494
634
  }
635
+ executeBatches(query, options = {}) {
636
+ return this.session.executeBatches(query, options);
637
+ }
638
+ executeArrow(query) {
639
+ return this.session.executeArrow(query);
640
+ }
495
641
  async transaction(transaction) {
496
642
  const nestedTx = new DuckDBTransaction(this.dialect, this.session, this.schema, this.nestedIndex + 1);
497
643
  return transaction(nestedTx);
@@ -590,13 +736,7 @@ import { PgViewBase } from "drizzle-orm/pg-core/view-base";
590
736
  import { SQL as SQL4 } from "drizzle-orm/sql/sql";
591
737
 
592
738
  // src/sql/selection.ts
593
- import {
594
- Column as Column2,
595
- SQL as SQL3,
596
- getTableName as getTableName2,
597
- is as is3,
598
- sql as sql3
599
- } from "drizzle-orm";
739
+ import { Column as Column2, SQL as SQL3, getTableName as getTableName2, is as is3, sql as sql3 } from "drizzle-orm";
600
740
  function mapEntries(obj, prefix, fullJoin = false) {
601
741
  return Object.fromEntries(Object.entries(obj).filter(([key]) => key !== "enableRLS").map(([key, value]) => {
602
742
  const qualified = prefix ? `${prefix}.${key}` : key;
@@ -737,6 +877,12 @@ class DuckDBDatabase extends PgDatabase {
737
877
  dialect: this.dialect
738
878
  });
739
879
  }
880
+ executeBatches(query, options = {}) {
881
+ return this.session.executeBatches(query, options);
882
+ }
883
+ executeArrow(query) {
884
+ return this.session.executeArrow(query);
885
+ }
740
886
  async transaction(transaction) {
741
887
  return await this.session.transaction(transaction);
742
888
  }
@@ -747,12 +893,13 @@ import { sql as sql4 } from "drizzle-orm";
747
893
  var SYSTEM_SCHEMAS = new Set(["information_schema", "pg_catalog"]);
748
894
  var DEFAULT_IMPORT_BASE = "@leonardovida-md/drizzle-neo-duckdb";
749
895
  async function introspect(db, opts = {}) {
750
- const schemas = await resolveSchemas(db, opts.schemas);
896
+ const database = await resolveDatabase(db, opts.database, opts.allDatabases);
897
+ const schemas = await resolveSchemas(db, database, opts.schemas);
751
898
  const includeViews = opts.includeViews ?? false;
752
- const tables = await loadTables(db, schemas, includeViews);
753
- const columns = await loadColumns(db, schemas);
754
- const constraints = await loadConstraints(db, schemas);
755
- const indexes = await loadIndexes(db, schemas);
899
+ const tables = await loadTables(db, database, schemas, includeViews);
900
+ const columns = await loadColumns(db, database, schemas);
901
+ const constraints = await loadConstraints(db, database, schemas);
902
+ const indexes = await loadIndexes(db, database, schemas);
756
903
  const grouped = buildTables(tables, columns, constraints, indexes);
757
904
  const schemaTs = emitSchema(grouped, {
758
905
  useCustomTimeTypes: opts.useCustomTimeTypes ?? true,
@@ -766,27 +913,41 @@ async function introspect(db, opts = {}) {
766
913
  }
767
914
  };
768
915
  }
769
- async function resolveSchemas(db, targetSchemas) {
916
+ async function resolveDatabase(db, targetDatabase, allDatabases) {
917
+ if (allDatabases) {
918
+ return null;
919
+ }
920
+ if (targetDatabase) {
921
+ return targetDatabase;
922
+ }
923
+ const rows = await db.execute(sql4`SELECT current_database() as current_database`);
924
+ return rows[0]?.current_database ?? null;
925
+ }
926
+ async function resolveSchemas(db, database, targetSchemas) {
770
927
  if (targetSchemas?.length) {
771
928
  return targetSchemas;
772
929
  }
773
- const rows = await db.execute(sql4`select schema_name from information_schema.schemata`);
930
+ const databaseFilter = database ? sql4`catalog_name = ${database}` : sql4`1 = 1`;
931
+ const rows = await db.execute(sql4`SELECT schema_name FROM information_schema.schemata WHERE ${databaseFilter}`);
774
932
  return rows.map((row) => row.schema_name).filter((name) => !SYSTEM_SCHEMAS.has(name));
775
933
  }
776
- async function loadTables(db, schemas, includeViews) {
934
+ async function loadTables(db, database, schemas, includeViews) {
777
935
  const schemaFragments = schemas.map((schema) => sql4`${schema}`);
936
+ const databaseFilter = database ? sql4`table_catalog = ${database}` : sql4`1 = 1`;
778
937
  return await db.execute(sql4`
779
- select table_schema as schema_name, table_name, table_type
780
- from information_schema.tables
781
- where table_schema in (${sql4.join(schemaFragments, sql4.raw(", "))})
782
- and ${includeViews ? sql4`1 = 1` : sql4`table_type = 'BASE TABLE'`}
783
- order by table_schema, table_name
938
+ SELECT table_schema as schema_name, table_name, table_type
939
+ FROM information_schema.tables
940
+ WHERE ${databaseFilter}
941
+ AND table_schema IN (${sql4.join(schemaFragments, sql4.raw(", "))})
942
+ AND ${includeViews ? sql4`1 = 1` : sql4`table_type = 'BASE TABLE'`}
943
+ ORDER BY table_schema, table_name
784
944
  `);
785
945
  }
786
- async function loadColumns(db, schemas) {
946
+ async function loadColumns(db, database, schemas) {
787
947
  const schemaFragments = schemas.map((schema) => sql4`${schema}`);
948
+ const databaseFilter = database ? sql4`database_name = ${database}` : sql4`1 = 1`;
788
949
  return await db.execute(sql4`
789
- select
950
+ SELECT
790
951
  schema_name,
791
952
  table_name,
792
953
  column_name,
@@ -798,15 +959,17 @@ async function loadColumns(db, schemas) {
798
959
  numeric_precision,
799
960
  numeric_scale,
800
961
  internal
801
- from duckdb_columns()
802
- where schema_name in (${sql4.join(schemaFragments, sql4.raw(", "))})
803
- order by schema_name, table_name, column_index
962
+ FROM duckdb_columns()
963
+ WHERE ${databaseFilter}
964
+ AND schema_name IN (${sql4.join(schemaFragments, sql4.raw(", "))})
965
+ ORDER BY schema_name, table_name, column_index
804
966
  `);
805
967
  }
806
- async function loadConstraints(db, schemas) {
968
+ async function loadConstraints(db, database, schemas) {
807
969
  const schemaFragments = schemas.map((schema) => sql4`${schema}`);
970
+ const databaseFilter = database ? sql4`database_name = ${database}` : sql4`1 = 1`;
808
971
  return await db.execute(sql4`
809
- select
972
+ SELECT
810
973
  schema_name,
811
974
  table_name,
812
975
  constraint_name,
@@ -815,23 +978,26 @@ async function loadConstraints(db, schemas) {
815
978
  constraint_column_names,
816
979
  referenced_table,
817
980
  referenced_column_names
818
- from duckdb_constraints()
819
- where schema_name in (${sql4.join(schemaFragments, sql4.raw(", "))})
820
- order by schema_name, table_name, constraint_index
981
+ FROM duckdb_constraints()
982
+ WHERE ${databaseFilter}
983
+ AND schema_name IN (${sql4.join(schemaFragments, sql4.raw(", "))})
984
+ ORDER BY schema_name, table_name, constraint_index
821
985
  `);
822
986
  }
823
- async function loadIndexes(db, schemas) {
987
+ async function loadIndexes(db, database, schemas) {
824
988
  const schemaFragments = schemas.map((schema) => sql4`${schema}`);
989
+ const databaseFilter = database ? sql4`database_name = ${database}` : sql4`1 = 1`;
825
990
  return await db.execute(sql4`
826
- select
991
+ SELECT
827
992
  schema_name,
828
993
  table_name,
829
994
  index_name,
830
995
  is_unique,
831
996
  expressions
832
- from duckdb_indexes()
833
- where schema_name in (${sql4.join(schemaFragments, sql4.raw(", "))})
834
- order by schema_name, table_name, index_name
997
+ FROM duckdb_indexes()
998
+ WHERE ${databaseFilter}
999
+ AND schema_name IN (${sql4.join(schemaFragments, sql4.raw(", "))})
1000
+ ORDER BY schema_name, table_name, index_name
835
1001
  `);
836
1002
  }
837
1003
  function buildTables(tables, columns, constraints, indexes) {
@@ -1026,7 +1192,9 @@ function mapDuckDbType(column, imports, options) {
1026
1192
  }
1027
1193
  if (upper === "BIGINT" || upper === "INT8" || upper === "UBIGINT") {
1028
1194
  imports.pgCore.add("bigint");
1029
- return { builder: `bigint(${columnName(column.name)})` };
1195
+ return {
1196
+ builder: `bigint(${columnName(column.name)}, { mode: 'number' })`
1197
+ };
1030
1198
  }
1031
1199
  const decimalMatch = /^DECIMAL\((\d+),(\d+)\)/i.exec(upper);
1032
1200
  const numericMatch = /^NUMERIC\((\d+),(\d+)\)/i.exec(upper);
@@ -1278,6 +1446,7 @@ function renderImports(imports, importBasePath) {
1278
1446
  function parseArgs(argv) {
1279
1447
  const options = {
1280
1448
  outFile: path.resolve(process.cwd(), "drizzle/schema.ts"),
1449
+ allDatabases: false,
1281
1450
  includeViews: false,
1282
1451
  useCustomTimeTypes: true
1283
1452
  };
@@ -1287,6 +1456,13 @@ function parseArgs(argv) {
1287
1456
  case "--url":
1288
1457
  options.url = argv[++i];
1289
1458
  break;
1459
+ case "--database":
1460
+ case "--db":
1461
+ options.database = argv[++i];
1462
+ break;
1463
+ case "--all-databases":
1464
+ options.allDatabases = true;
1465
+ break;
1290
1466
  case "--schema":
1291
1467
  case "--schemas":
1292
1468
  options.schemas = argv[++i]?.split(",").map((s) => s.trim()).filter(Boolean);
@@ -1325,11 +1501,27 @@ Usage:
1325
1501
 
1326
1502
  Options:
1327
1503
  --url DuckDB database path (e.g. :memory:, ./local.duckdb, md:)
1504
+ --database, --db Database/catalog to introspect (default: current database)
1505
+ --all-databases Introspect all attached databases (not just current)
1328
1506
  --schema Comma separated schema list (defaults to all non-system schemas)
1329
1507
  --out Output file (default: ./drizzle/schema.ts)
1330
1508
  --include-views Include views in the generated schema
1331
1509
  --use-pg-time Use pg-core timestamp/date/time instead of DuckDB custom helpers
1332
1510
  --import-base Override import path for duckdb helpers (default: package name)
1511
+
1512
+ Database Filtering:
1513
+ By default, only tables from the current database are introspected. This prevents
1514
+ returning tables from all attached databases in MotherDuck workspaces.
1515
+
1516
+ Use --database to specify a different database, or --all-databases to introspect
1517
+ all attached databases.
1518
+
1519
+ Examples:
1520
+ # Local DuckDB file
1521
+ bun x duckdb-introspect --url ./my-database.duckdb --out ./schema.ts
1522
+
1523
+ # MotherDuck (requires MOTHERDUCK_TOKEN env var)
1524
+ MOTHERDUCK_TOKEN=xxx bun x duckdb-introspect --url md: --database my_cloud_db --out ./schema.ts
1333
1525
  `);
1334
1526
  }
1335
1527
  async function main() {
@@ -1344,6 +1536,8 @@ async function main() {
1344
1536
  const db = drizzle(connection);
1345
1537
  try {
1346
1538
  const result = await introspect(db, {
1539
+ database: options.database,
1540
+ allDatabases: options.allDatabases,
1347
1541
  schemas: options.schemas,
1348
1542
  includeViews: options.includeViews,
1349
1543
  useCustomTimeTypes: options.useCustomTimeTypes,
package/dist/index.d.ts CHANGED
@@ -3,3 +3,6 @@ export * from './session.ts';
3
3
  export * from './columns.ts';
4
4
  export * from './migrator.ts';
5
5
  export * from './introspect.ts';
6
+ export * from './client.ts';
7
+ export * from './olap.ts';
8
+ export * from './value-wrappers.ts';