@leonardovida-md/drizzle-neo-duckdb 1.1.3 → 1.2.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.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * DuckDB-native array operators. Generate DuckDB-compatible SQL directly
3
+ * without query rewriting.
4
+ */
5
+ import { type SQL, type SQLWrapper } from 'drizzle-orm';
6
+ export declare function arrayHasAll<T>(column: SQLWrapper, values: T[] | SQLWrapper): SQL;
7
+ export declare function arrayHasAny<T>(column: SQLWrapper, values: T[] | SQLWrapper): SQL;
8
+ export declare function arrayContainedBy<T>(column: SQLWrapper, values: T[] | SQLWrapper): SQL;
package/dist/options.d.ts CHANGED
@@ -1,6 +1,3 @@
1
- export type RewriteArraysMode = 'auto' | 'always' | 'never';
2
- export type RewriteArraysOption = boolean | RewriteArraysMode;
3
- export declare function resolveRewriteArraysOption(value?: RewriteArraysOption): RewriteArraysMode;
4
1
  export type PrepareCacheOption = boolean | number | {
5
2
  size?: number;
6
3
  };
package/dist/session.d.ts CHANGED
@@ -10,7 +10,7 @@ import type { Assume } from 'drizzle-orm/utils';
10
10
  import type { DuckDBDialect } from './dialect.ts';
11
11
  import type { DuckDBClientLike, RowData } from './client.ts';
12
12
  import { type ExecuteBatchesRawChunk, type ExecuteInBatchesOptions } from './client.ts';
13
- import type { PreparedStatementCacheConfig, RewriteArraysMode } from './options.ts';
13
+ import type { PreparedStatementCacheConfig } from './options.ts';
14
14
  export type { DuckDBClientLike, RowData } from './client.ts';
15
15
  export declare class DuckDBPreparedQuery<T extends PreparedQueryConfig> extends PgPreparedQuery<T> {
16
16
  private client;
@@ -21,19 +21,17 @@ export declare class DuckDBPreparedQuery<T extends PreparedQueryConfig> extends
21
21
  private fields;
22
22
  private _isResponseInArrayMode;
23
23
  private customResultMapper;
24
- private rewriteArraysMode;
25
24
  private rejectStringArrayLiterals;
26
25
  private prepareCache;
27
26
  private warnOnStringArrayLiteral?;
28
27
  static readonly [entityKind]: string;
29
- constructor(client: DuckDBClientLike, dialect: DuckDBDialect, queryString: string, params: unknown[], logger: Logger, fields: SelectedFieldsOrdered | undefined, _isResponseInArrayMode: boolean, customResultMapper: ((rows: unknown[][]) => T['execute']) | undefined, rewriteArraysMode: RewriteArraysMode, rejectStringArrayLiterals: boolean, prepareCache: PreparedStatementCacheConfig | undefined, warnOnStringArrayLiteral?: ((sql: string) => void) | undefined);
28
+ constructor(client: DuckDBClientLike, dialect: DuckDBDialect, queryString: string, params: unknown[], logger: Logger, fields: SelectedFieldsOrdered | undefined, _isResponseInArrayMode: boolean, customResultMapper: ((rows: unknown[][]) => T['execute']) | undefined, rejectStringArrayLiterals: boolean, prepareCache: PreparedStatementCacheConfig | undefined, warnOnStringArrayLiteral?: ((sql: string) => void) | undefined);
30
29
  execute(placeholderValues?: Record<string, unknown> | undefined): Promise<T['execute']>;
31
30
  all(placeholderValues?: Record<string, unknown> | undefined): Promise<T['all']>;
32
31
  isResponseInArrayMode(): boolean;
33
32
  }
34
33
  export interface DuckDBSessionOptions {
35
34
  logger?: Logger;
36
- rewriteArrays?: RewriteArraysMode;
37
35
  rejectStringArrayLiterals?: boolean;
38
36
  prepareCache?: PreparedStatementCacheConfig;
39
37
  }
@@ -44,7 +42,6 @@ export declare class DuckDBSession<TFullSchema extends Record<string, unknown> =
44
42
  static readonly [entityKind]: string;
45
43
  protected dialect: DuckDBDialect;
46
44
  private logger;
47
- private rewriteArraysMode;
48
45
  private rejectStringArrayLiterals;
49
46
  private prepareCache;
50
47
  private hasWarnedArrayLiteral;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * AST-based SQL transformer for DuckDB compatibility.
3
+ *
4
+ * Transforms:
5
+ * - Array operators: @>, <@, && -> array_has_all(), array_has_any()
6
+ * - JOIN column qualification: "col" = "col" -> "left"."col" = "right"."col"
7
+ */
8
+ export type TransformResult = {
9
+ sql: string;
10
+ transformed: boolean;
11
+ };
12
+ export declare function transformSQL(query: string): TransformResult;
13
+ export declare function needsTransformation(query: string): boolean;
14
+ export { transformArrayOperators } from './visitors/array-operators.ts';
15
+ export { qualifyJoinColumns } from './visitors/column-qualifier.ts';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * AST visitor to transform Postgres array operators to DuckDB functions.
3
+ */
4
+ import type { AST } from 'node-sql-parser';
5
+ export declare function transformArrayOperators(ast: AST | AST[]): boolean;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * AST visitor to qualify unqualified column references in JOIN ON clauses.
3
+ */
4
+ import type { AST } from 'node-sql-parser';
5
+ export declare function qualifyJoinColumns(ast: AST | AST[]): boolean;
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  export { aliasFields } from './sql/selection.ts';
2
- export { adaptArrayOperators } from './sql/query-rewriters.ts';
3
2
  export { mapResultRow } from './sql/result-mapper.ts';
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.3",
6
+ "version": "1.2.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": {
@@ -77,5 +77,9 @@
77
77
  "src/**/*.ts",
78
78
  "dist/*.mjs",
79
79
  "dist/**/*.d.ts"
80
- ]
80
+ ],
81
+ "dependencies": {
82
+ "@duckdb/node-bindings-darwin-arm64": "^1.4.2-r.1",
83
+ "node-sql-parser": "^5.3.13"
84
+ }
81
85
  }
package/src/columns.ts CHANGED
@@ -237,7 +237,12 @@ export const duckDbMap = <TData extends Record<string, any>>(
237
237
  dataType() {
238
238
  return `MAP (STRING, ${valueType})`;
239
239
  },
240
- toDriver(value: TData): MapValueWrapper {
240
+ toDriver(value: TData) {
241
+ // Use SQL literals for empty maps due to DuckDB type inference issues
242
+ // with mapValue() when there are no entries to infer types from
243
+ if (Object.keys(value).length === 0) {
244
+ return buildMapLiteral(value, valueType);
245
+ }
241
246
  return wrapMap(value, valueType);
242
247
  },
243
248
  fromDriver(value: TData | MapValueWrapper): TData {
package/src/dialect.ts CHANGED
@@ -15,9 +15,13 @@ import {
15
15
  } from 'drizzle-orm/pg-core';
16
16
  import {
17
17
  sql,
18
+ SQL,
18
19
  type DriverValueEncoder,
19
20
  type QueryTypingsValue,
20
21
  } from 'drizzle-orm';
22
+ import type { QueryWithTypings } from 'drizzle-orm/sql/sql';
23
+
24
+ import { transformSQL } from './sql/ast-transformer.ts';
21
25
 
22
26
  const enum SavepointSupport {
23
27
  Unknown = 0,
@@ -181,4 +185,20 @@ export class DuckDBDialect extends PgDialect {
181
185
  return 'none';
182
186
  }
183
187
  }
188
+
189
+ override sqlToQuery(
190
+ sqlObj: SQL,
191
+ invokeSource?: 'indexes' | undefined
192
+ ): QueryWithTypings {
193
+ // First, let the parent generate the SQL string
194
+ const result = super.sqlToQuery(sqlObj, invokeSource);
195
+
196
+ // Apply AST-based transformations for DuckDB compatibility
197
+ const transformed = transformSQL(result.sql);
198
+
199
+ return {
200
+ ...result,
201
+ sql: transformed.sql,
202
+ };
203
+ }
184
204
  }
package/src/driver.ts CHANGED
@@ -37,16 +37,12 @@ import {
37
37
  } from './pool.ts';
38
38
  import {
39
39
  resolvePrepareCacheOption,
40
- resolveRewriteArraysOption,
41
40
  type PreparedStatementCacheConfig,
42
41
  type PrepareCacheOption,
43
- type RewriteArraysMode,
44
- type RewriteArraysOption,
45
42
  } from './options.ts';
46
43
 
47
44
  export interface PgDriverOptions {
48
45
  logger?: Logger;
49
- rewriteArrays?: RewriteArraysMode;
50
46
  rejectStringArrayLiterals?: boolean;
51
47
  prepareCache?: PreparedStatementCacheConfig;
52
48
  }
@@ -65,7 +61,6 @@ export class DuckDBDriver {
65
61
  ): DuckDBSession<Record<string, unknown>, TablesRelationalConfig> {
66
62
  return new DuckDBSession(this.client, this.dialect, schema, {
67
63
  logger: this.options.logger,
68
- rewriteArrays: this.options.rewriteArrays ?? 'auto',
69
64
  rejectStringArrayLiterals: this.options.rejectStringArrayLiterals,
70
65
  prepareCache: this.options.prepareCache,
71
66
  });
@@ -83,7 +78,6 @@ export interface DuckDBConnectionConfig {
83
78
  export interface DuckDBDrizzleConfig<
84
79
  TSchema extends Record<string, unknown> = Record<string, never>,
85
80
  > extends DrizzleConfig<TSchema> {
86
- rewriteArrays?: RewriteArraysOption;
87
81
  rejectStringArrayLiterals?: boolean;
88
82
  prepareCache?: PrepareCacheOption;
89
83
  /** Pool configuration. Use preset name, size config, or false to disable. */
@@ -126,7 +120,6 @@ function createFromClient<
126
120
  instance?: DuckDBInstance
127
121
  ): DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>> {
128
122
  const dialect = new DuckDBDialect();
129
- const rewriteArraysMode = resolveRewriteArraysOption(config.rewriteArrays);
130
123
  const prepareCache = resolvePrepareCacheOption(config.prepareCache);
131
124
 
132
125
  const logger =
@@ -148,7 +141,6 @@ function createFromClient<
148
141
 
149
142
  const driver = new DuckDBDriver(client, dialect, {
150
143
  logger,
151
- rewriteArrays: rewriteArraysMode,
152
144
  rejectStringArrayLiterals: config.rejectStringArrayLiterals,
153
145
  prepareCache,
154
146
  });
package/src/index.ts CHANGED
@@ -8,3 +8,4 @@ export * from './pool.ts';
8
8
  export * from './olap.ts';
9
9
  export * from './value-wrappers.ts';
10
10
  export * from './options.ts';
11
+ export * from './operators.ts';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * DuckDB-native array operators. Generate DuckDB-compatible SQL directly
3
+ * without query rewriting.
4
+ */
5
+
6
+ import { sql, type SQL, type SQLWrapper } from 'drizzle-orm';
7
+
8
+ export function arrayHasAll<T>(
9
+ column: SQLWrapper,
10
+ values: T[] | SQLWrapper
11
+ ): SQL {
12
+ return sql`array_has_all(${column}, ${values})`;
13
+ }
14
+
15
+ export function arrayHasAny<T>(
16
+ column: SQLWrapper,
17
+ values: T[] | SQLWrapper
18
+ ): SQL {
19
+ return sql`array_has_any(${column}, ${values})`;
20
+ }
21
+
22
+ export function arrayContainedBy<T>(
23
+ column: SQLWrapper,
24
+ values: T[] | SQLWrapper
25
+ ): SQL {
26
+ return sql`array_has_all(${values}, ${column})`;
27
+ }
package/src/options.ts CHANGED
@@ -1,18 +1,3 @@
1
- export type RewriteArraysMode = 'auto' | 'always' | 'never';
2
-
3
- export type RewriteArraysOption = boolean | RewriteArraysMode;
4
-
5
- const DEFAULT_REWRITE_ARRAYS_MODE: RewriteArraysMode = 'auto';
6
-
7
- export function resolveRewriteArraysOption(
8
- value?: RewriteArraysOption
9
- ): RewriteArraysMode {
10
- if (value === undefined) return DEFAULT_REWRITE_ARRAYS_MODE;
11
- if (value === true) return 'auto';
12
- if (value === false) return 'never';
13
- return value;
14
- }
15
-
16
1
  export type PrepareCacheOption = boolean | number | { size?: number };
17
2
 
18
3
  export interface PreparedStatementCacheConfig {
package/src/session.ts CHANGED
@@ -15,10 +15,6 @@ import type {
15
15
  } from 'drizzle-orm/relations';
16
16
  import { fillPlaceholders, type Query, SQL, sql } from 'drizzle-orm/sql/sql';
17
17
  import type { Assume } from 'drizzle-orm/utils';
18
- import {
19
- adaptArrayOperators,
20
- qualifyJoinColumns,
21
- } from './sql/query-rewriters.ts';
22
18
  import { mapResultRow } from './sql/result-mapper.ts';
23
19
  import { TransactionRollbackError } from 'drizzle-orm/errors';
24
20
  import type { DuckDBDialect } from './dialect.ts';
@@ -39,10 +35,7 @@ import {
39
35
  } from './client.ts';
40
36
  import { isPool } from './client.ts';
41
37
  import type { DuckDBConnection } from '@duckdb/node-api';
42
- import type {
43
- PreparedStatementCacheConfig,
44
- RewriteArraysMode,
45
- } from './options.ts';
38
+ import type { PreparedStatementCacheConfig } from './options.ts';
46
39
 
47
40
  export type { DuckDBClientLike, RowData } from './client.ts';
48
41
 
@@ -56,34 +49,6 @@ function isSavepointSyntaxError(error: unknown): boolean {
56
49
  );
57
50
  }
58
51
 
59
- function rewriteQuery(
60
- mode: RewriteArraysMode,
61
- query: string
62
- ): { sql: string; rewritten: boolean } {
63
- if (mode === 'never') {
64
- return { sql: query, rewritten: false };
65
- }
66
-
67
- let result = query;
68
- let wasRewritten = false;
69
-
70
- // Rewrite Postgres array operators to DuckDB functions
71
- const arrayRewritten = adaptArrayOperators(result);
72
- if (arrayRewritten !== result) {
73
- result = arrayRewritten;
74
- wasRewritten = true;
75
- }
76
-
77
- // Qualify unqualified column references in JOIN ON clauses
78
- const joinQualified = qualifyJoinColumns(result);
79
- if (joinQualified !== result) {
80
- result = joinQualified;
81
- wasRewritten = true;
82
- }
83
-
84
- return { sql: result, rewritten: wasRewritten };
85
- }
86
-
87
52
  export class DuckDBPreparedQuery<
88
53
  T extends PreparedQueryConfig,
89
54
  > extends PgPreparedQuery<T> {
@@ -100,7 +65,6 @@ export class DuckDBPreparedQuery<
100
65
  private customResultMapper:
101
66
  | ((rows: unknown[][]) => T['execute'])
102
67
  | undefined,
103
- private rewriteArraysMode: RewriteArraysMode,
104
68
  private rejectStringArrayLiterals: boolean,
105
69
  private prepareCache: PreparedStatementCacheConfig | undefined,
106
70
  private warnOnStringArrayLiteral?: (sql: string) => void
@@ -121,19 +85,7 @@ export class DuckDBPreparedQuery<
121
85
  : undefined,
122
86
  }
123
87
  );
124
- const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
125
- this.rewriteArraysMode,
126
- this.queryString
127
- );
128
-
129
- if (didRewrite) {
130
- this.logger.logQuery(
131
- `[duckdb] original query before array rewrite: ${this.queryString}`,
132
- params
133
- );
134
- }
135
-
136
- this.logger.logQuery(rewrittenQuery, params);
88
+ this.logger.logQuery(this.queryString, params);
137
89
 
138
90
  const { fields, joinsNotNullableMap, customResultMapper } =
139
91
  this as typeof this & { joinsNotNullableMap?: Record<string, boolean> };
@@ -141,7 +93,7 @@ export class DuckDBPreparedQuery<
141
93
  if (fields) {
142
94
  const { rows } = await executeArraysOnClient(
143
95
  this.client,
144
- rewrittenQuery,
96
+ this.queryString,
145
97
  params,
146
98
  { prepareCache: this.prepareCache }
147
99
  );
@@ -157,7 +109,7 @@ export class DuckDBPreparedQuery<
157
109
  );
158
110
  }
159
111
 
160
- const rows = await executeOnClient(this.client, rewrittenQuery, params, {
112
+ const rows = await executeOnClient(this.client, this.queryString, params, {
161
113
  prepareCache: this.prepareCache,
162
114
  });
163
115
 
@@ -177,7 +129,6 @@ export class DuckDBPreparedQuery<
177
129
 
178
130
  export interface DuckDBSessionOptions {
179
131
  logger?: Logger;
180
- rewriteArrays?: RewriteArraysMode;
181
132
  rejectStringArrayLiterals?: boolean;
182
133
  prepareCache?: PreparedStatementCacheConfig;
183
134
  }
@@ -190,7 +141,6 @@ export class DuckDBSession<
190
141
 
191
142
  protected override dialect: DuckDBDialect;
192
143
  private logger: Logger;
193
- private rewriteArraysMode: RewriteArraysMode;
194
144
  private rejectStringArrayLiterals: boolean;
195
145
  private prepareCache: PreparedStatementCacheConfig | undefined;
196
146
  private hasWarnedArrayLiteral = false;
@@ -205,12 +155,10 @@ export class DuckDBSession<
205
155
  super(dialect);
206
156
  this.dialect = dialect;
207
157
  this.logger = options.logger ?? new NoopLogger();
208
- this.rewriteArraysMode = options.rewriteArrays ?? 'auto';
209
158
  this.rejectStringArrayLiterals = options.rejectStringArrayLiterals ?? false;
210
159
  this.prepareCache = options.prepareCache;
211
160
  this.options = {
212
161
  ...options,
213
- rewriteArrays: this.rewriteArraysMode,
214
162
  prepareCache: this.prepareCache,
215
163
  };
216
164
  }
@@ -232,7 +180,6 @@ export class DuckDBSession<
232
180
  fields,
233
181
  isResponseInArrayMode,
234
182
  customResultMapper,
235
- this.rewriteArraysMode,
236
183
  this.rejectStringArrayLiterals,
237
184
  this.prepareCache,
238
185
  this.rejectStringArrayLiterals ? undefined : this.warnOnStringArrayLiteral
@@ -326,23 +273,12 @@ export class DuckDBSession<
326
273
  ? undefined
327
274
  : () => this.warnOnStringArrayLiteral(builtQuery.sql),
328
275
  });
329
- const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
330
- this.rewriteArraysMode,
331
- builtQuery.sql
332
- );
333
276
 
334
- if (didRewrite) {
335
- this.logger.logQuery(
336
- `[duckdb] original query before array rewrite: ${builtQuery.sql}`,
337
- params
338
- );
339
- }
340
-
341
- this.logger.logQuery(rewrittenQuery, params);
277
+ this.logger.logQuery(builtQuery.sql, params);
342
278
 
343
279
  return executeInBatches(
344
280
  this.client,
345
- rewrittenQuery,
281
+ builtQuery.sql,
346
282
  params,
347
283
  options
348
284
  ) as AsyncGenerator<GenericRowData<T>[], void, void>;
@@ -361,21 +297,10 @@ export class DuckDBSession<
361
297
  ? undefined
362
298
  : () => this.warnOnStringArrayLiteral(builtQuery.sql),
363
299
  });
364
- const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
365
- this.rewriteArraysMode,
366
- builtQuery.sql
367
- );
368
300
 
369
- if (didRewrite) {
370
- this.logger.logQuery(
371
- `[duckdb] original query before array rewrite: ${builtQuery.sql}`,
372
- params
373
- );
374
- }
301
+ this.logger.logQuery(builtQuery.sql, params);
375
302
 
376
- this.logger.logQuery(rewrittenQuery, params);
377
-
378
- return executeInBatchesRaw(this.client, rewrittenQuery, params, options);
303
+ return executeInBatchesRaw(this.client, builtQuery.sql, params, options);
379
304
  }
380
305
 
381
306
  async executeArrow(query: SQL): Promise<unknown> {
@@ -388,21 +313,10 @@ export class DuckDBSession<
388
313
  ? undefined
389
314
  : () => this.warnOnStringArrayLiteral(builtQuery.sql),
390
315
  });
391
- const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
392
- this.rewriteArraysMode,
393
- builtQuery.sql
394
- );
395
-
396
- if (didRewrite) {
397
- this.logger.logQuery(
398
- `[duckdb] original query before array rewrite: ${builtQuery.sql}`,
399
- params
400
- );
401
- }
402
316
 
403
- this.logger.logQuery(rewrittenQuery, params);
317
+ this.logger.logQuery(builtQuery.sql, params);
404
318
 
405
- return executeArrowOnClient(this.client, rewrittenQuery, params);
319
+ return executeArrowOnClient(this.client, builtQuery.sql, params);
406
320
  }
407
321
 
408
322
  markRollbackOnly(): void {
@@ -0,0 +1,68 @@
1
+ /**
2
+ * AST-based SQL transformer for DuckDB compatibility.
3
+ *
4
+ * Transforms:
5
+ * - Array operators: @>, <@, && -> array_has_all(), array_has_any()
6
+ * - JOIN column qualification: "col" = "col" -> "left"."col" = "right"."col"
7
+ */
8
+
9
+ import nodeSqlParser from 'node-sql-parser';
10
+ const { Parser } = nodeSqlParser;
11
+ import type { AST } from 'node-sql-parser';
12
+
13
+ import { transformArrayOperators } from './visitors/array-operators.ts';
14
+ import { qualifyJoinColumns } from './visitors/column-qualifier.ts';
15
+
16
+ const parser = new Parser();
17
+
18
+ export type TransformResult = {
19
+ sql: string;
20
+ transformed: boolean;
21
+ };
22
+
23
+ export function transformSQL(query: string): TransformResult {
24
+ const needsArrayTransform =
25
+ query.includes('@>') || query.includes('<@') || query.includes('&&');
26
+ const needsJoinTransform = query.toLowerCase().includes('join');
27
+
28
+ if (!needsArrayTransform && !needsJoinTransform) {
29
+ return { sql: query, transformed: false };
30
+ }
31
+
32
+ try {
33
+ const ast = parser.astify(query, { database: 'PostgreSQL' });
34
+
35
+ let transformed = false;
36
+
37
+ if (needsArrayTransform) {
38
+ transformed = transformArrayOperators(ast) || transformed;
39
+ }
40
+
41
+ if (needsJoinTransform) {
42
+ transformed = qualifyJoinColumns(ast) || transformed;
43
+ }
44
+
45
+ if (!transformed) {
46
+ return { sql: query, transformed: false };
47
+ }
48
+
49
+ const transformedSql = parser.sqlify(ast, { database: 'PostgreSQL' });
50
+
51
+ return { sql: transformedSql, transformed: true };
52
+ } catch {
53
+ return { sql: query, transformed: false };
54
+ }
55
+ }
56
+
57
+ export function needsTransformation(query: string): boolean {
58
+ const lower = query.toLowerCase();
59
+ return (
60
+ query.includes('@>') ||
61
+ query.includes('<@') ||
62
+ query.includes('&&') ||
63
+ lower.includes('join')
64
+ );
65
+ }
66
+
67
+ export { transformArrayOperators } from './visitors/array-operators.ts';
68
+ export { qualifyJoinColumns } from './visitors/column-qualifier.ts';