@leonardovida-md/drizzle-neo-duckdb 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -63,4 +63,12 @@ export interface IntrospectResult {
63
63
  }
64
64
  export declare const DEFAULT_IMPORT_BASE = "@leonardovida-md/drizzle-neo-duckdb/helpers";
65
65
  export declare function introspect(db: DuckDBDatabase, opts?: IntrospectOptions): Promise<IntrospectResult>;
66
+ export declare function buildDefault(defaultValue: string | null): string;
67
+ export declare function parseStructFields(inner: string): Array<{
68
+ name: string;
69
+ type: string;
70
+ }>;
71
+ export declare function parseMapValue(raw: string): string;
72
+ export declare function splitTopLevel(input: string, delimiter: string): string[];
73
+ export declare function toIdentifier(name: string): string;
66
74
  export {};
package/dist/pool.d.ts CHANGED
@@ -16,6 +16,14 @@ export declare function resolvePoolSize(pool: DuckDBPoolConfig | PoolPreset | fa
16
16
  export interface DuckDBConnectionPoolOptions {
17
17
  /** Maximum concurrent connections. Defaults to 4. */
18
18
  size?: number;
19
+ /** Timeout in milliseconds to wait for a connection. Defaults to 30000 (30s). */
20
+ acquireTimeout?: number;
21
+ /** Maximum number of requests waiting for a connection. Defaults to 100. */
22
+ maxWaitingRequests?: number;
23
+ /** Max time (ms) a connection may live before being recycled. */
24
+ maxLifetimeMs?: number;
25
+ /** Max idle time (ms) before an idle connection is discarded. */
26
+ idleTimeoutMs?: number;
19
27
  }
20
28
  export declare function createDuckDBConnectionPool(instance: DuckDBInstance, options?: DuckDBConnectionPoolOptions): DuckDBConnectionPool & {
21
29
  size: number;
package/dist/session.d.ts CHANGED
@@ -44,12 +44,17 @@ export declare class DuckDBSession<TFullSchema extends Record<string, unknown> =
44
44
  private rewriteArrays;
45
45
  private rejectStringArrayLiterals;
46
46
  private hasWarnedArrayLiteral;
47
+ private rollbackOnly;
47
48
  constructor(client: DuckDBClientLike, dialect: DuckDBDialect, schema: RelationalSchemaConfig<TSchema> | undefined, options?: DuckDBSessionOptions);
48
49
  prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(query: Query, fields: SelectedFieldsOrdered | undefined, name: string | undefined, isResponseInArrayMode: boolean, customResultMapper?: (rows: unknown[][]) => T['execute']): PgPreparedQuery<T>;
49
- transaction<T>(transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>): Promise<T>;
50
+ execute<T>(query: SQL): Promise<T>;
51
+ all<T = unknown>(query: SQL): Promise<T[]>;
52
+ transaction<T>(transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>, config?: PgTransactionConfig): Promise<T>;
50
53
  private warnOnStringArrayLiteral;
51
54
  executeBatches<T extends RowData = RowData>(query: SQL, options?: ExecuteInBatchesOptions): AsyncGenerator<GenericRowData<T>[], void, void>;
52
55
  executeArrow(query: SQL): Promise<unknown>;
56
+ markRollbackOnly(): void;
57
+ isRollbackOnly(): boolean;
53
58
  }
54
59
  export declare class DuckDBTransaction<TFullSchema extends Record<string, unknown>, TSchema extends TablesRelationalConfig> extends PgTransaction<DuckDBQueryResultHKT, TFullSchema, TSchema> {
55
60
  static readonly [entityKind]: string;
@@ -59,6 +64,7 @@ export declare class DuckDBTransaction<TFullSchema extends Record<string, unknow
59
64
  executeBatches<T extends RowData = RowData>(query: SQL, options?: ExecuteInBatchesOptions): AsyncGenerator<GenericRowData<T>[], void, void>;
60
65
  executeArrow(query: SQL): Promise<unknown>;
61
66
  transaction<T>(transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>): Promise<T>;
67
+ private runNestedWithoutSavepoint;
62
68
  }
63
69
  export type GenericRowData<T extends RowData = RowData> = T;
64
70
  export type GenericTableData<T = RowData> = T[];
@@ -1 +1,2 @@
1
+ export declare function scrubForRewrite(query: string): string;
1
2
  export declare function adaptArrayOperators(query: string): string;
@@ -1,2 +1,9 @@
1
1
  import { type AnyColumn, type SelectedFieldsOrdered } from 'drizzle-orm';
2
+ export declare function normalizeInet(value: unknown): unknown;
3
+ export declare function normalizeTimestampString(value: unknown, withTimezone: boolean): string | unknown;
4
+ export declare function normalizeTimestamp(value: unknown, withTimezone: boolean): Date | unknown;
5
+ export declare function normalizeDateString(value: unknown): string | unknown;
6
+ export declare function normalizeDateValue(value: unknown): Date | unknown;
7
+ export declare function normalizeTime(value: unknown): string | unknown;
8
+ export declare function normalizeInterval(value: unknown): string | unknown;
2
9
  export declare function mapResultRow<TResult>(columns: SelectedFieldsOrdered<AnyColumn>, row: unknown[], joinsNotNullableMap: Record<string, boolean> | undefined): TResult;
@@ -23,7 +23,7 @@ export interface StructValueWrapper extends DuckDBValueWrapper<'struct', Record<
23
23
  export interface MapValueWrapper extends DuckDBValueWrapper<'map', Record<string, unknown>> {
24
24
  readonly valueType?: string;
25
25
  }
26
- export interface TimestampValueWrapper extends DuckDBValueWrapper<'timestamp', Date | string> {
26
+ export interface TimestampValueWrapper extends DuckDBValueWrapper<'timestamp', Date | string | number | bigint> {
27
27
  readonly withTimezone: boolean;
28
28
  readonly precision?: number;
29
29
  }
@@ -37,6 +37,6 @@ export declare function wrapList(data: unknown[], elementType?: string): ListVal
37
37
  export declare function wrapArray(data: unknown[], elementType?: string, fixedLength?: number): ArrayValueWrapper;
38
38
  export declare function wrapStruct(data: Record<string, unknown>, schema?: Record<string, string>): StructValueWrapper;
39
39
  export declare function wrapMap(data: Record<string, unknown>, valueType?: string): MapValueWrapper;
40
- export declare function wrapTimestamp(data: Date | string, withTimezone: boolean, precision?: number): TimestampValueWrapper;
40
+ export declare function wrapTimestamp(data: Date | string | number | bigint, withTimezone: boolean, precision?: number): TimestampValueWrapper;
41
41
  export declare function wrapBlob(data: Buffer | Uint8Array): BlobValueWrapper;
42
42
  export declare function wrapJson(data: unknown): JsonValueWrapper;
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.1.0",
6
+ "version": "1.1.1",
7
7
  "description": "A drizzle ORM client for use with DuckDB. Based on drizzle's Postgres client.",
8
8
  "type": "module",
9
9
  "scripts": {
@@ -12,6 +12,7 @@ interface CliOptions {
12
12
  allDatabases: boolean;
13
13
  schemas?: string[];
14
14
  outFile: string;
15
+ outMeta?: string;
15
16
  includeViews: boolean;
16
17
  useCustomTimeTypes: boolean;
17
18
  importBasePath?: string;
@@ -20,6 +21,7 @@ interface CliOptions {
20
21
  function parseArgs(argv: string[]): CliOptions {
21
22
  const options: CliOptions = {
22
23
  outFile: path.resolve(process.cwd(), 'drizzle/schema.ts'),
24
+ outMeta: undefined,
23
25
  allDatabases: false,
24
26
  includeViews: false,
25
27
  useCustomTimeTypes: true,
@@ -52,6 +54,14 @@ function parseArgs(argv: string[]): CliOptions {
52
54
  argv[++i] ?? 'drizzle/schema.ts'
53
55
  );
54
56
  break;
57
+ case '--out-json':
58
+ case '--outJson':
59
+ case '--json':
60
+ options.outMeta = path.resolve(
61
+ process.cwd(),
62
+ argv[++i] ?? 'drizzle/schema.meta.json'
63
+ );
64
+ break;
55
65
  case '--include-views':
56
66
  case '--includeViews':
57
67
  options.includeViews = true;
@@ -88,6 +98,7 @@ Options:
88
98
  --all-databases Introspect all attached databases (not just current)
89
99
  --schema Comma separated schema list (defaults to all non-system schemas)
90
100
  --out Output file (default: ./drizzle/schema.ts)
101
+ --json Optional JSON metadata output (default: ./drizzle/schema.meta.json)
91
102
  --include-views Include views in the generated schema
92
103
  --use-pg-time Use pg-core timestamp/date/time instead of DuckDB custom helpers
93
104
  --import-base Override import path for duckdb helpers (default: package name)
@@ -136,8 +147,19 @@ async function main() {
136
147
 
137
148
  await mkdir(path.dirname(options.outFile), { recursive: true });
138
149
  await writeFile(options.outFile, result.files.schemaTs, 'utf8');
150
+ if (options.outMeta) {
151
+ await mkdir(path.dirname(options.outMeta), { recursive: true });
152
+ await writeFile(
153
+ options.outMeta,
154
+ JSON.stringify(result.files.metaJson, null, 2),
155
+ 'utf8'
156
+ );
157
+ }
139
158
 
140
159
  console.log(`Wrote schema to ${options.outFile}`);
160
+ if (options.outMeta) {
161
+ console.log(`Wrote metadata to ${options.outMeta}`);
162
+ }
141
163
  } finally {
142
164
  if (
143
165
  'closeSync' in connection &&
@@ -145,6 +167,11 @@ async function main() {
145
167
  ) {
146
168
  connection.closeSync();
147
169
  }
170
+ if ('closeSync' in instance && typeof instance.closeSync === 'function') {
171
+ instance.closeSync();
172
+ } else if ('close' in instance && typeof instance.close === 'function') {
173
+ await instance.close();
174
+ }
148
175
  }
149
176
  }
150
177
 
package/src/client.ts CHANGED
@@ -49,17 +49,20 @@ export function prepareParams(
49
49
  options: PrepareParamsOptions = {}
50
50
  ): unknown[] {
51
51
  return params.map((param) => {
52
- if (typeof param === 'string' && isPgArrayLiteral(param)) {
53
- if (options.rejectStringArrayLiterals) {
54
- throw new Error(
55
- 'Stringified array literals are not supported. Use duckDbList()/duckDbArray() or pass native arrays.'
56
- );
52
+ if (typeof param === 'string') {
53
+ const trimmed = param.trim();
54
+ if (trimmed && isPgArrayLiteral(trimmed)) {
55
+ if (options.rejectStringArrayLiterals) {
56
+ throw new Error(
57
+ 'Stringified array literals are not supported. Use duckDbList()/duckDbArray() or pass native arrays.'
58
+ );
59
+ }
60
+
61
+ if (options.warnOnStringArrayLiteral) {
62
+ options.warnOnStringArrayLiteral();
63
+ }
64
+ return parsePgArrayLiteral(trimmed);
57
65
  }
58
-
59
- if (options.warnOnStringArrayLiteral) {
60
- options.warnOnStringArrayLiteral();
61
- }
62
- return parsePgArrayLiteral(param);
63
66
  }
64
67
  return param;
65
68
  });
package/src/columns.ts CHANGED
@@ -63,7 +63,7 @@ type StructColType = `STRUCT (${string})`;
63
63
 
64
64
  type Primitive = AnyColType | ListColType | ArrayColType | StructColType;
65
65
 
66
- function coerceArrayString(value: string): unknown[] | undefined {
66
+ export function coerceArrayString(value: string): unknown[] | undefined {
67
67
  const trimmed = value.trim();
68
68
  if (!trimmed) {
69
69
  return [];
@@ -86,7 +86,7 @@ function coerceArrayString(value: string): unknown[] | undefined {
86
86
  return undefined;
87
87
  }
88
88
 
89
- function formatLiteral(value: unknown, typeHint?: string): string {
89
+ export function formatLiteral(value: unknown, typeHint?: string): string {
90
90
  if (value === null || value === undefined) {
91
91
  return 'NULL';
92
92
  }
@@ -123,7 +123,7 @@ function formatLiteral(value: unknown, typeHint?: string): string {
123
123
  return `'${escaped}'`;
124
124
  }
125
125
 
126
- function buildListLiteral(values: unknown[], elementType?: string): SQL {
126
+ export function buildListLiteral(values: unknown[], elementType?: string): SQL {
127
127
  if (values.length === 0) {
128
128
  return sql`[]`;
129
129
  }
@@ -135,7 +135,7 @@ function buildListLiteral(values: unknown[], elementType?: string): SQL {
135
135
  return sql`list_value(${sql.join(chunks, sql.raw(', '))})`;
136
136
  }
137
137
 
138
- function buildStructLiteral(
138
+ export function buildStructLiteral(
139
139
  value: Record<string, unknown>,
140
140
  schema?: Record<string, Primitive>
141
141
  ): SQL {
@@ -154,7 +154,7 @@ function buildStructLiteral(
154
154
  return sql`struct_pack(${sql.join(parts, sql.raw(', '))})`;
155
155
  }
156
156
 
157
- function buildMapLiteral(
157
+ export function buildMapLiteral(
158
158
  value: Record<string, unknown>,
159
159
  valueType?: string
160
160
  ): SQL {
@@ -188,11 +188,11 @@ export const duckDbList = <TData = unknown>(
188
188
  }
189
189
  if (typeof value === 'string') {
190
190
  const parsed = coerceArrayString(value);
191
- if (parsed) {
191
+ if (parsed !== undefined) {
192
192
  return parsed as TData[];
193
193
  }
194
194
  }
195
- return [] as TData[];
195
+ return value as unknown as TData[];
196
196
  },
197
197
  })(name);
198
198
 
@@ -219,11 +219,11 @@ export const duckDbArray = <TData = unknown>(
219
219
  }
220
220
  if (typeof value === 'string') {
221
221
  const parsed = coerceArrayString(value);
222
- if (parsed) {
222
+ if (parsed !== undefined) {
223
223
  return parsed as TData[];
224
224
  }
225
225
  }
226
- return [] as TData[];
226
+ return value as unknown as TData[];
227
227
  },
228
228
  })(name);
229
229
 
package/src/dialect.ts CHANGED
@@ -19,18 +19,64 @@ import {
19
19
  type QueryTypingsValue,
20
20
  } from 'drizzle-orm';
21
21
 
22
+ const enum SavepointSupport {
23
+ Unknown = 0,
24
+ Yes = 1,
25
+ No = 2,
26
+ }
27
+
22
28
  export class DuckDBDialect extends PgDialect {
23
29
  static readonly [entityKind]: string = 'DuckDBPgDialect';
30
+ // Track if PG JSON columns were detected during the current query preparation.
31
+ // Reset before each query via DuckDBSession to keep detection per-query.
24
32
  private hasPgJsonColumn = false;
33
+ // Track savepoint support per-dialect instance to avoid cross-contamination
34
+ // when multiple database connections with different capabilities exist.
35
+ private savepointsSupported: SavepointSupport = SavepointSupport.Unknown;
36
+
37
+ /**
38
+ * Reset the PG JSON detection flag. Should be called before preparing a new query.
39
+ */
40
+ resetPgJsonFlag(): void {
41
+ this.hasPgJsonColumn = false;
42
+ }
43
+
44
+ /**
45
+ * Mark that a PG JSON/JSONB column was detected during query preparation.
46
+ */
47
+ markPgJsonDetected(): void {
48
+ this.hasPgJsonColumn = true;
49
+ }
25
50
 
26
51
  assertNoPgJsonColumns(): void {
27
52
  if (this.hasPgJsonColumn) {
28
53
  throw new Error(
29
- 'Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDBs native JSON type.'
54
+ "Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDB's native JSON type."
30
55
  );
31
56
  }
32
57
  }
33
58
 
59
+ /**
60
+ * Check if savepoints are known to be unsupported for this dialect instance.
61
+ */
62
+ areSavepointsUnsupported(): boolean {
63
+ return this.savepointsSupported === SavepointSupport.No;
64
+ }
65
+
66
+ /**
67
+ * Mark that savepoints are supported for this dialect instance.
68
+ */
69
+ markSavepointsSupported(): void {
70
+ this.savepointsSupported = SavepointSupport.Yes;
71
+ }
72
+
73
+ /**
74
+ * Mark that savepoints are not supported for this dialect instance.
75
+ */
76
+ markSavepointsUnsupported(): void {
77
+ this.savepointsSupported = SavepointSupport.No;
78
+ }
79
+
34
80
  override async migrate(
35
81
  migrations: MigrationMeta[],
36
82
  session: PgSession,
@@ -117,8 +163,10 @@ export class DuckDBDialect extends PgDialect {
117
163
  encoder: DriverValueEncoder<unknown, unknown>
118
164
  ): QueryTypingsValue {
119
165
  if (is(encoder, PgJsonb) || is(encoder, PgJson)) {
120
- this.hasPgJsonColumn = true;
121
- return 'none';
166
+ this.markPgJsonDetected();
167
+ throw new Error(
168
+ "Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDB's native JSON type."
169
+ );
122
170
  } else if (is(encoder, PgNumeric)) {
123
171
  return 'decimal';
124
172
  } else if (is(encoder, PgTime)) {
package/src/driver.ts CHANGED
@@ -24,7 +24,7 @@ import { DuckDBDialect } from './dialect.ts';
24
24
  import { DuckDBSelectBuilder } from './select-builder.ts';
25
25
  import { aliasFields } from './sql/selection.ts';
26
26
  import type { ExecuteInBatchesOptions, RowData } from './client.ts';
27
- import { isPool } from './client.ts';
27
+ import { closeClientConnection, isPool } from './client.ts';
28
28
  import {
29
29
  createDuckDBConnectionPool,
30
30
  resolvePoolSize,
@@ -290,7 +290,20 @@ export class DuckDBDatabase<
290
290
  if (isPool(this.$client) && this.$client.close) {
291
291
  await this.$client.close();
292
292
  }
293
- // Note: DuckDBInstance doesn't have a close method in the current API
293
+ if (!isPool(this.$client)) {
294
+ await closeClientConnection(this.$client);
295
+ }
296
+ if (this.$instance) {
297
+ const maybeClosable = this.$instance as unknown as {
298
+ close?: () => Promise<void> | void;
299
+ closeSync?: () => void;
300
+ };
301
+ if (typeof maybeClosable.close === 'function') {
302
+ await maybeClosable.close();
303
+ } else if (typeof maybeClosable.closeSync === 'function') {
304
+ maybeClosable.closeSync();
305
+ }
306
+ }
294
307
  }
295
308
 
296
309
  select(): DuckDBSelectBuilder<undefined>;
package/src/introspect.ts CHANGED
@@ -535,7 +535,7 @@ function emitColumn(
535
535
  return builder;
536
536
  }
537
537
 
538
- function buildDefault(defaultValue: string | null): string {
538
+ export function buildDefault(defaultValue: string | null): string {
539
539
  if (!defaultValue) {
540
540
  return '';
541
541
  }
@@ -707,6 +707,22 @@ function mapDuckDbType(
707
707
  return { builder: `text(${columnName(column.name)}) /* JSON */` };
708
708
  }
709
709
 
710
+ if (upper.startsWith('ENUM')) {
711
+ imports.pgCore.add('text');
712
+ const enumLiteral = raw.replace(/^ENUM\s*/i, '').trim();
713
+ return {
714
+ builder: `text(${columnName(column.name)}) /* ENUM ${enumLiteral} */`,
715
+ };
716
+ }
717
+
718
+ if (upper.startsWith('UNION')) {
719
+ imports.pgCore.add('text');
720
+ const unionLiteral = raw.replace(/^UNION\s*/i, '').trim();
721
+ return {
722
+ builder: `text(${columnName(column.name)}) /* UNION ${unionLiteral} */`,
723
+ };
724
+ }
725
+
710
726
  if (upper === 'INET') {
711
727
  imports.local.add('duckDbInet');
712
728
  return { builder: `duckDbInet(${columnName(column.name)})` };
@@ -788,15 +804,16 @@ function mapDuckDbType(
788
804
  }
789
805
 
790
806
  // Fallback: keep as text to avoid runtime failures.
807
+ // Unknown types are mapped to text with a comment indicating the original type.
791
808
  imports.pgCore.add('text');
792
809
  return {
793
810
  builder: `text(${columnName(
794
811
  column.name
795
- )}) /* TODO: verify type ${upper} */`,
812
+ )}) /* unsupported DuckDB type: ${upper} */`,
796
813
  };
797
814
  }
798
815
 
799
- function parseStructFields(
816
+ export function parseStructFields(
800
817
  inner: string
801
818
  ): Array<{ name: string; type: string }> {
802
819
  const result: Array<{ name: string; type: string }> = [];
@@ -813,7 +830,7 @@ function parseStructFields(
813
830
  return result;
814
831
  }
815
832
 
816
- function parseMapValue(raw: string): string {
833
+ export function parseMapValue(raw: string): string {
817
834
  const inner = raw.replace(/^MAP\(/i, '').replace(/\)$/, '');
818
835
  const parts = splitTopLevel(inner, ',');
819
836
  if (parts.length < 2) {
@@ -822,7 +839,7 @@ function parseMapValue(raw: string): string {
822
839
  return parts[1]?.trim() ?? 'TEXT';
823
840
  }
824
841
 
825
- function splitTopLevel(input: string, delimiter: string): string[] {
842
+ export function splitTopLevel(input: string, delimiter: string): string[] {
826
843
  const parts: string[] = [];
827
844
  let depth = 0;
828
845
  let current = '';
@@ -847,7 +864,7 @@ function tableKey(schema: string, table: string): string {
847
864
  return `${schema}.${table}`;
848
865
  }
849
866
 
850
- function toIdentifier(name: string): string {
867
+ export function toIdentifier(name: string): string {
851
868
  const cleaned = name.replace(/[^A-Za-z0-9_]/g, '_');
852
869
  const parts = cleaned.split('_').filter(Boolean);
853
870
  const base = parts