@leonardovida-md/drizzle-neo-duckdb 1.0.3 → 1.1.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 +20 -5
- package/dist/client.d.ts +7 -1
- package/dist/columns.d.ts +1 -1
- package/dist/driver.d.ts +33 -1
- package/dist/duckdb-introspect.mjs +211 -37
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.mjs +319 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +202 -36
- package/dist/introspect.d.ts +1 -0
- package/dist/pool.d.ts +22 -0
- package/dist/sql/query-rewriters.d.ts +0 -1
- package/dist/utils.d.ts +1 -1
- package/dist/value-wrappers-core.d.ts +42 -0
- package/dist/value-wrappers.d.ts +2 -98
- package/package.json +6 -2
- package/src/client.ts +43 -5
- package/src/columns.ts +1 -1
- package/src/driver.ts +191 -7
- package/src/helpers.ts +18 -0
- package/src/index.ts +1 -0
- package/src/introspect.ts +24 -23
- package/src/migrator.ts +1 -1
- package/src/olap.ts +1 -0
- package/src/pool.ts +104 -0
- package/src/session.ts +36 -10
- package/src/sql/query-rewriters.ts +2 -49
- package/src/utils.ts +1 -1
- package/src/value-wrappers-core.ts +156 -0
- package/src/value-wrappers.ts +44 -213
package/src/client.ts
CHANGED
|
@@ -10,9 +10,21 @@ import {
|
|
|
10
10
|
type AnyDuckDBValueWrapper,
|
|
11
11
|
} from './value-wrappers.ts';
|
|
12
12
|
|
|
13
|
-
export type DuckDBClientLike = DuckDBConnection;
|
|
13
|
+
export type DuckDBClientLike = DuckDBConnection | DuckDBConnectionPool;
|
|
14
14
|
export type RowData = Record<string, unknown>;
|
|
15
15
|
|
|
16
|
+
export interface DuckDBConnectionPool {
|
|
17
|
+
acquire(): Promise<DuckDBConnection>;
|
|
18
|
+
release(connection: DuckDBConnection): void | Promise<void>;
|
|
19
|
+
close?(): Promise<void> | void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isPool(
|
|
23
|
+
client: DuckDBClientLike
|
|
24
|
+
): client is DuckDBConnectionPool {
|
|
25
|
+
return typeof (client as DuckDBConnectionPool).acquire === 'function';
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
export interface PrepareParamsOptions {
|
|
17
29
|
rejectStringArrayLiterals?: boolean;
|
|
18
30
|
warnOnStringArrayLiteral?: () => void;
|
|
@@ -32,8 +44,6 @@ function parsePgArrayLiteral(value: string): unknown {
|
|
|
32
44
|
}
|
|
33
45
|
}
|
|
34
46
|
|
|
35
|
-
let warnedArrayLiteral = false;
|
|
36
|
-
|
|
37
47
|
export function prepareParams(
|
|
38
48
|
params: unknown[],
|
|
39
49
|
options: PrepareParamsOptions = {}
|
|
@@ -46,8 +56,7 @@ export function prepareParams(
|
|
|
46
56
|
);
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
if (
|
|
50
|
-
warnedArrayLiteral = true;
|
|
59
|
+
if (options.warnOnStringArrayLiteral) {
|
|
51
60
|
options.warnOnStringArrayLiteral();
|
|
52
61
|
}
|
|
53
62
|
return parsePgArrayLiteral(param);
|
|
@@ -138,6 +147,15 @@ export async function executeOnClient(
|
|
|
138
147
|
query: string,
|
|
139
148
|
params: unknown[]
|
|
140
149
|
): Promise<RowData[]> {
|
|
150
|
+
if (isPool(client)) {
|
|
151
|
+
const connection = await client.acquire();
|
|
152
|
+
try {
|
|
153
|
+
return await executeOnClient(connection, query, params);
|
|
154
|
+
} finally {
|
|
155
|
+
await client.release(connection);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
141
159
|
const values =
|
|
142
160
|
params.length > 0
|
|
143
161
|
? (params.map((param) => toNodeApiValue(param)) as DuckDBValue[])
|
|
@@ -165,6 +183,16 @@ export async function* executeInBatches(
|
|
|
165
183
|
params: unknown[],
|
|
166
184
|
options: ExecuteInBatchesOptions = {}
|
|
167
185
|
): AsyncGenerator<RowData[], void, void> {
|
|
186
|
+
if (isPool(client)) {
|
|
187
|
+
const connection = await client.acquire();
|
|
188
|
+
try {
|
|
189
|
+
yield* executeInBatches(connection, query, params, options);
|
|
190
|
+
return;
|
|
191
|
+
} finally {
|
|
192
|
+
await client.release(connection);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
168
196
|
const rowsPerChunk =
|
|
169
197
|
options.rowsPerChunk && options.rowsPerChunk > 0
|
|
170
198
|
? options.rowsPerChunk
|
|
@@ -207,12 +235,22 @@ export async function executeArrowOnClient(
|
|
|
207
235
|
query: string,
|
|
208
236
|
params: unknown[]
|
|
209
237
|
): Promise<unknown> {
|
|
238
|
+
if (isPool(client)) {
|
|
239
|
+
const connection = await client.acquire();
|
|
240
|
+
try {
|
|
241
|
+
return await executeArrowOnClient(connection, query, params);
|
|
242
|
+
} finally {
|
|
243
|
+
await client.release(connection);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
210
247
|
const values =
|
|
211
248
|
params.length > 0
|
|
212
249
|
? (params.map((param) => toNodeApiValue(param)) as DuckDBValue[])
|
|
213
250
|
: undefined;
|
|
214
251
|
const result = await client.run(query, values);
|
|
215
252
|
|
|
253
|
+
// Runtime detection for Arrow API support (optional method, not in base type)
|
|
216
254
|
const maybeArrow =
|
|
217
255
|
(result as unknown as { toArrow?: () => Promise<unknown> }).toArrow ??
|
|
218
256
|
(result as unknown as { getArrowTable?: () => Promise<unknown> })
|
package/src/columns.ts
CHANGED
package/src/driver.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DuckDBInstance } from '@duckdb/node-api';
|
|
1
2
|
import { entityKind } from 'drizzle-orm/entity';
|
|
2
3
|
import type { Logger } from 'drizzle-orm/logger';
|
|
3
4
|
import { DefaultLogger } from 'drizzle-orm/logger';
|
|
@@ -23,6 +24,13 @@ import { DuckDBDialect } from './dialect.ts';
|
|
|
23
24
|
import { DuckDBSelectBuilder } from './select-builder.ts';
|
|
24
25
|
import { aliasFields } from './sql/selection.ts';
|
|
25
26
|
import type { ExecuteInBatchesOptions, RowData } from './client.ts';
|
|
27
|
+
import { isPool } from './client.ts';
|
|
28
|
+
import {
|
|
29
|
+
createDuckDBConnectionPool,
|
|
30
|
+
resolvePoolSize,
|
|
31
|
+
type DuckDBPoolConfig,
|
|
32
|
+
type PoolPreset,
|
|
33
|
+
} from './pool.ts';
|
|
26
34
|
|
|
27
35
|
export interface PgDriverOptions {
|
|
28
36
|
logger?: Logger;
|
|
@@ -50,18 +58,57 @@ export class DuckDBDriver {
|
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
/** Connection configuration when using path-based connection */
|
|
62
|
+
export interface DuckDBConnectionConfig {
|
|
63
|
+
/** Database path: ':memory:', './file.duckdb', 'md:', 'md:database' */
|
|
64
|
+
path: string;
|
|
65
|
+
/** DuckDB instance options (e.g., motherduck_token) */
|
|
66
|
+
options?: Record<string, string>;
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
export interface DuckDBDrizzleConfig<
|
|
54
70
|
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
55
71
|
> extends DrizzleConfig<TSchema> {
|
|
56
72
|
rewriteArrays?: boolean;
|
|
57
73
|
rejectStringArrayLiterals?: boolean;
|
|
74
|
+
/** Pool configuration. Use preset name, size config, or false to disable. */
|
|
75
|
+
pool?: DuckDBPoolConfig | PoolPreset | false;
|
|
58
76
|
}
|
|
59
77
|
|
|
60
|
-
export
|
|
78
|
+
export interface DuckDBDrizzleConfigWithConnection<
|
|
79
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
80
|
+
> extends DuckDBDrizzleConfig<TSchema> {
|
|
81
|
+
/** Connection string or config object */
|
|
82
|
+
connection: string | DuckDBConnectionConfig;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface DuckDBDrizzleConfigWithClient<
|
|
86
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
87
|
+
> extends DuckDBDrizzleConfig<TSchema> {
|
|
88
|
+
/** Explicit client (connection or pool) */
|
|
89
|
+
client: DuckDBClientLike;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Check if a value looks like a config object (not a client) */
|
|
93
|
+
function isConfigObject(data: unknown): data is Record<string, unknown> {
|
|
94
|
+
if (typeof data !== 'object' || data === null) return false;
|
|
95
|
+
if (data.constructor?.name !== 'Object') return false;
|
|
96
|
+
return (
|
|
97
|
+
'connection' in data ||
|
|
98
|
+
'client' in data ||
|
|
99
|
+
'pool' in data ||
|
|
100
|
+
'schema' in data ||
|
|
101
|
+
'logger' in data
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Internal: create database from a client (connection or pool) */
|
|
106
|
+
function createFromClient<
|
|
61
107
|
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
62
108
|
>(
|
|
63
109
|
client: DuckDBClientLike,
|
|
64
|
-
config: DuckDBDrizzleConfig<TSchema> = {}
|
|
110
|
+
config: DuckDBDrizzleConfig<TSchema> = {},
|
|
111
|
+
instance?: DuckDBInstance
|
|
65
112
|
): DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>> {
|
|
66
113
|
const dialect = new DuckDBDialect();
|
|
67
114
|
|
|
@@ -89,10 +136,125 @@ export function drizzle<
|
|
|
89
136
|
});
|
|
90
137
|
const session = driver.createSession(schema);
|
|
91
138
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
139
|
+
const db = new DuckDBDatabase(dialect, session, schema, client, instance);
|
|
140
|
+
return db as DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Internal: create database from a connection string */
|
|
144
|
+
async function createFromConnectionString<
|
|
145
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
146
|
+
>(
|
|
147
|
+
path: string,
|
|
148
|
+
instanceOptions: Record<string, string> | undefined,
|
|
149
|
+
config: DuckDBDrizzleConfig<TSchema> = {}
|
|
150
|
+
): Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>> {
|
|
151
|
+
const instance = await DuckDBInstance.create(path, instanceOptions);
|
|
152
|
+
const poolSize = resolvePoolSize(config.pool);
|
|
153
|
+
|
|
154
|
+
if (poolSize === false) {
|
|
155
|
+
const connection = await instance.connect();
|
|
156
|
+
return createFromClient(connection, config, instance);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const pool = createDuckDBConnectionPool(instance, { size: poolSize });
|
|
160
|
+
return createFromClient(pool, config, instance);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Overload 1: Connection string (async, auto-pools)
|
|
164
|
+
export function drizzle<
|
|
165
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
166
|
+
>(
|
|
167
|
+
connectionString: string
|
|
168
|
+
): Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>>;
|
|
169
|
+
|
|
170
|
+
// Overload 2: Connection string + config (async, auto-pools)
|
|
171
|
+
export function drizzle<
|
|
172
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
173
|
+
>(
|
|
174
|
+
connectionString: string,
|
|
175
|
+
config: DuckDBDrizzleConfig<TSchema>
|
|
176
|
+
): Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>>;
|
|
177
|
+
|
|
178
|
+
// Overload 3: Config with connection (async, auto-pools)
|
|
179
|
+
export function drizzle<
|
|
180
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
181
|
+
>(
|
|
182
|
+
config: DuckDBDrizzleConfigWithConnection<TSchema>
|
|
183
|
+
): Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>>;
|
|
184
|
+
|
|
185
|
+
// Overload 4: Config with explicit client (sync)
|
|
186
|
+
export function drizzle<
|
|
187
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
188
|
+
>(
|
|
189
|
+
config: DuckDBDrizzleConfigWithClient<TSchema>
|
|
190
|
+
): DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>;
|
|
191
|
+
|
|
192
|
+
// Overload 5: Explicit client (sync, backward compatible)
|
|
193
|
+
export function drizzle<
|
|
194
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
195
|
+
>(
|
|
196
|
+
client: DuckDBClientLike,
|
|
197
|
+
config?: DuckDBDrizzleConfig<TSchema>
|
|
198
|
+
): DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>;
|
|
199
|
+
|
|
200
|
+
// Implementation
|
|
201
|
+
export function drizzle<
|
|
202
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
203
|
+
>(
|
|
204
|
+
clientOrConfigOrPath:
|
|
205
|
+
| string
|
|
206
|
+
| DuckDBClientLike
|
|
207
|
+
| DuckDBDrizzleConfigWithConnection<TSchema>
|
|
208
|
+
| DuckDBDrizzleConfigWithClient<TSchema>,
|
|
209
|
+
config?: DuckDBDrizzleConfig<TSchema>
|
|
210
|
+
):
|
|
211
|
+
| DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>
|
|
212
|
+
| Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>> {
|
|
213
|
+
// String path -> async with auto-pool
|
|
214
|
+
if (typeof clientOrConfigOrPath === 'string') {
|
|
215
|
+
return createFromConnectionString(clientOrConfigOrPath, undefined, config);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Config object with connection or client
|
|
219
|
+
if (isConfigObject(clientOrConfigOrPath)) {
|
|
220
|
+
const configObj = clientOrConfigOrPath as
|
|
221
|
+
| DuckDBDrizzleConfigWithConnection<TSchema>
|
|
222
|
+
| DuckDBDrizzleConfigWithClient<TSchema>;
|
|
223
|
+
|
|
224
|
+
if ('connection' in configObj) {
|
|
225
|
+
const connConfig =
|
|
226
|
+
configObj as DuckDBDrizzleConfigWithConnection<TSchema>;
|
|
227
|
+
const { connection, ...restConfig } = connConfig;
|
|
228
|
+
if (typeof connection === 'string') {
|
|
229
|
+
return createFromConnectionString(
|
|
230
|
+
connection,
|
|
231
|
+
undefined,
|
|
232
|
+
restConfig as DuckDBDrizzleConfig<TSchema>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return createFromConnectionString(
|
|
236
|
+
connection.path,
|
|
237
|
+
connection.options,
|
|
238
|
+
restConfig as DuckDBDrizzleConfig<TSchema>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if ('client' in configObj) {
|
|
243
|
+
const clientConfig = configObj as DuckDBDrizzleConfigWithClient<TSchema>;
|
|
244
|
+
const { client: clientValue, ...restConfig } = clientConfig;
|
|
245
|
+
return createFromClient(
|
|
246
|
+
clientValue,
|
|
247
|
+
restConfig as DuckDBDrizzleConfig<TSchema>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
throw new Error(
|
|
252
|
+
'Invalid drizzle config: either connection or client must be provided'
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Direct client (backward compatible)
|
|
257
|
+
return createFromClient(clientOrConfigOrPath as DuckDBClientLike, config);
|
|
96
258
|
}
|
|
97
259
|
|
|
98
260
|
export class DuckDBDatabase<
|
|
@@ -102,12 +264,33 @@ export class DuckDBDatabase<
|
|
|
102
264
|
> extends PgDatabase<DuckDBQueryResultHKT, TFullSchema, TSchema> {
|
|
103
265
|
static readonly [entityKind]: string = 'DuckDBDatabase';
|
|
104
266
|
|
|
267
|
+
/** The underlying connection or pool */
|
|
268
|
+
readonly $client: DuckDBClientLike;
|
|
269
|
+
|
|
270
|
+
/** The DuckDB instance (when created from connection string) */
|
|
271
|
+
readonly $instance?: DuckDBInstance;
|
|
272
|
+
|
|
105
273
|
constructor(
|
|
106
274
|
readonly dialect: DuckDBDialect,
|
|
107
275
|
readonly session: DuckDBSession<TFullSchema, TSchema>,
|
|
108
|
-
schema: RelationalSchemaConfig<TSchema> | undefined
|
|
276
|
+
schema: RelationalSchemaConfig<TSchema> | undefined,
|
|
277
|
+
client: DuckDBClientLike,
|
|
278
|
+
instance?: DuckDBInstance
|
|
109
279
|
) {
|
|
110
280
|
super(dialect, session, schema);
|
|
281
|
+
this.$client = client;
|
|
282
|
+
this.$instance = instance;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Close the database connection pool and instance.
|
|
287
|
+
* Should be called when shutting down the application.
|
|
288
|
+
*/
|
|
289
|
+
async close(): Promise<void> {
|
|
290
|
+
if (isPool(this.$client) && this.$client.close) {
|
|
291
|
+
await this.$client.close();
|
|
292
|
+
}
|
|
293
|
+
// Note: DuckDBInstance doesn't have a close method in the current API
|
|
111
294
|
}
|
|
112
295
|
|
|
113
296
|
select(): DuckDBSelectBuilder<undefined>;
|
|
@@ -119,6 +302,7 @@ export class DuckDBDatabase<
|
|
|
119
302
|
): DuckDBSelectBuilder<SelectedFields | undefined> {
|
|
120
303
|
const selectedFields = fields ? aliasFields(fields) : undefined;
|
|
121
304
|
|
|
305
|
+
// Cast needed: DuckDBSession is compatible but types don't align exactly with PgSession
|
|
122
306
|
return new DuckDBSelectBuilder({
|
|
123
307
|
fields: selectedFields ?? undefined,
|
|
124
308
|
session: this.session as unknown as PgSession<DuckDBQueryResultHKT>,
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Client-safe entrypoint exposing schema builder utilities without pulling
|
|
2
|
+
// the DuckDB Node API bindings. Intended for generated schemas and browser use.
|
|
3
|
+
export {
|
|
4
|
+
duckDbList,
|
|
5
|
+
duckDbArray,
|
|
6
|
+
duckDbMap,
|
|
7
|
+
duckDbStruct,
|
|
8
|
+
duckDbJson,
|
|
9
|
+
duckDbBlob,
|
|
10
|
+
duckDbInet,
|
|
11
|
+
duckDbInterval,
|
|
12
|
+
duckDbTimestamp,
|
|
13
|
+
duckDbDate,
|
|
14
|
+
duckDbTime,
|
|
15
|
+
duckDbArrayContains,
|
|
16
|
+
duckDbArrayContained,
|
|
17
|
+
duckDbArrayOverlaps,
|
|
18
|
+
} from './columns.ts';
|
package/src/index.ts
CHANGED
package/src/introspect.ts
CHANGED
|
@@ -108,7 +108,8 @@ type ImportBuckets = {
|
|
|
108
108
|
local: Set<string>;
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
-
const DEFAULT_IMPORT_BASE =
|
|
111
|
+
export const DEFAULT_IMPORT_BASE =
|
|
112
|
+
'@leonardovida-md/drizzle-neo-duckdb/helpers';
|
|
112
113
|
|
|
113
114
|
export async function introspect(
|
|
114
115
|
db: DuckDBDatabase,
|
|
@@ -649,6 +650,28 @@ function mapDuckDbType(
|
|
|
649
650
|
return { builder: `doublePrecision(${columnName(column.name)})` };
|
|
650
651
|
}
|
|
651
652
|
|
|
653
|
+
const arrayMatch = /^(.*)\[(\d+)\]$/.exec(upper);
|
|
654
|
+
if (arrayMatch) {
|
|
655
|
+
imports.local.add('duckDbArray');
|
|
656
|
+
const [, base, length] = arrayMatch;
|
|
657
|
+
return {
|
|
658
|
+
builder: `duckDbArray(${columnName(
|
|
659
|
+
column.name
|
|
660
|
+
)}, ${JSON.stringify(base)}, ${Number(length)})`,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const listMatch = /^(.*)\[\]$/.exec(upper);
|
|
665
|
+
if (listMatch) {
|
|
666
|
+
imports.local.add('duckDbList');
|
|
667
|
+
const [, base] = listMatch;
|
|
668
|
+
return {
|
|
669
|
+
builder: `duckDbList(${columnName(
|
|
670
|
+
column.name
|
|
671
|
+
)}, ${JSON.stringify(base)})`,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
652
675
|
if (upper.startsWith('CHAR(') || upper === 'CHAR') {
|
|
653
676
|
imports.pgCore.add('char');
|
|
654
677
|
const length = column.characterLength;
|
|
@@ -699,28 +722,6 @@ function mapDuckDbType(
|
|
|
699
722
|
return { builder: `duckDbBlob(${columnName(column.name)})` };
|
|
700
723
|
}
|
|
701
724
|
|
|
702
|
-
const arrayMatch = /^(.*)\[(\d+)\]$/.exec(upper);
|
|
703
|
-
if (arrayMatch) {
|
|
704
|
-
imports.local.add('duckDbArray');
|
|
705
|
-
const [, base, length] = arrayMatch;
|
|
706
|
-
return {
|
|
707
|
-
builder: `duckDbArray(${columnName(
|
|
708
|
-
column.name
|
|
709
|
-
)}, ${JSON.stringify(base)}, ${Number(length)})`,
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const listMatch = /^(.*)\[\]$/.exec(upper);
|
|
714
|
-
if (listMatch) {
|
|
715
|
-
imports.local.add('duckDbList');
|
|
716
|
-
const [, base] = listMatch;
|
|
717
|
-
return {
|
|
718
|
-
builder: `duckDbList(${columnName(
|
|
719
|
-
column.name
|
|
720
|
-
)}, ${JSON.stringify(base)})`,
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
|
|
724
725
|
if (upper.startsWith('STRUCT')) {
|
|
725
726
|
imports.local.add('duckDbStruct');
|
|
726
727
|
const inner = upper.replace(/^STRUCT\s*\(/i, '').replace(/\)$/, '');
|
package/src/migrator.ts
CHANGED
|
@@ -14,9 +14,9 @@ export async function migrate<TSchema extends Record<string, unknown>>(
|
|
|
14
14
|
|
|
15
15
|
const migrations = readMigrationFiles(migrationConfig);
|
|
16
16
|
|
|
17
|
+
// Cast needed: Drizzle's internal PgSession type differs from exported type
|
|
17
18
|
await db.dialect.migrate(
|
|
18
19
|
migrations,
|
|
19
|
-
// Need to work around omitted internal types from drizzle...
|
|
20
20
|
db.session as unknown as PgSession,
|
|
21
21
|
migrationConfig
|
|
22
22
|
);
|
package/src/olap.ts
CHANGED
|
@@ -169,6 +169,7 @@ export class OlapBuilder {
|
|
|
169
169
|
|
|
170
170
|
Object.assign(selection, this.measureMap);
|
|
171
171
|
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Drizzle's query builder types don't allow reassignment after groupBy
|
|
172
173
|
let query: any = this.db
|
|
173
174
|
.select(selection as SelectedFields)
|
|
174
175
|
.from(this.source!)
|
package/src/pool.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { DuckDBConnection, DuckDBInstance } from '@duckdb/node-api';
|
|
2
|
+
import { closeClientConnection, type DuckDBConnectionPool } from './client.ts';
|
|
3
|
+
|
|
4
|
+
/** Pool size presets for different MotherDuck instance types */
|
|
5
|
+
export type PoolPreset =
|
|
6
|
+
| 'pulse'
|
|
7
|
+
| 'standard'
|
|
8
|
+
| 'jumbo'
|
|
9
|
+
| 'mega'
|
|
10
|
+
| 'giga'
|
|
11
|
+
| 'local'
|
|
12
|
+
| 'memory';
|
|
13
|
+
|
|
14
|
+
/** Pool sizes optimized for each MotherDuck instance type */
|
|
15
|
+
export const POOL_PRESETS: Record<PoolPreset, number> = {
|
|
16
|
+
pulse: 4, // Auto-scaling, ad-hoc analytics
|
|
17
|
+
standard: 6, // Balanced ETL/ELT workloads
|
|
18
|
+
jumbo: 8, // Complex queries, high-volume
|
|
19
|
+
mega: 12, // Large-scale transformations
|
|
20
|
+
giga: 16, // Maximum parallelism
|
|
21
|
+
local: 8, // Local DuckDB file
|
|
22
|
+
memory: 4, // In-memory testing
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface DuckDBPoolConfig {
|
|
26
|
+
/** Maximum concurrent connections. Defaults to 4. */
|
|
27
|
+
size?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve pool configuration to a concrete size.
|
|
32
|
+
* Returns false if pooling is disabled.
|
|
33
|
+
*/
|
|
34
|
+
export function resolvePoolSize(
|
|
35
|
+
pool: DuckDBPoolConfig | PoolPreset | false | undefined
|
|
36
|
+
): number | false {
|
|
37
|
+
if (pool === false) return false;
|
|
38
|
+
if (pool === undefined) return 4;
|
|
39
|
+
if (typeof pool === 'string') return POOL_PRESETS[pool];
|
|
40
|
+
return pool.size ?? 4;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DuckDBConnectionPoolOptions {
|
|
44
|
+
/** Maximum concurrent connections. Defaults to 4. */
|
|
45
|
+
size?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createDuckDBConnectionPool(
|
|
49
|
+
instance: DuckDBInstance,
|
|
50
|
+
options: DuckDBConnectionPoolOptions = {}
|
|
51
|
+
): DuckDBConnectionPool & { size: number } {
|
|
52
|
+
const size = options.size && options.size > 0 ? options.size : 4;
|
|
53
|
+
const idle: DuckDBConnection[] = [];
|
|
54
|
+
const waiting: Array<(conn: DuckDBConnection) => void> = [];
|
|
55
|
+
let total = 0;
|
|
56
|
+
let closed = false;
|
|
57
|
+
|
|
58
|
+
const acquire = async (): Promise<DuckDBConnection> => {
|
|
59
|
+
if (closed) {
|
|
60
|
+
throw new Error('DuckDB connection pool is closed');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (idle.length > 0) {
|
|
64
|
+
return idle.pop() as DuckDBConnection;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (total < size) {
|
|
68
|
+
total += 1;
|
|
69
|
+
return await DuckDBConnection.create(instance);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return await new Promise((resolve) => {
|
|
73
|
+
waiting.push(resolve);
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const release = async (connection: DuckDBConnection): Promise<void> => {
|
|
78
|
+
if (closed) {
|
|
79
|
+
await closeClientConnection(connection);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const waiter = waiting.shift();
|
|
84
|
+
if (waiter) {
|
|
85
|
+
waiter(connection);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
idle.push(connection);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const close = async (): Promise<void> => {
|
|
93
|
+
closed = true;
|
|
94
|
+
const toClose = idle.splice(0, idle.length);
|
|
95
|
+
await Promise.all(toClose.map((conn) => closeClientConnection(conn)));
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
acquire,
|
|
100
|
+
release,
|
|
101
|
+
close,
|
|
102
|
+
size,
|
|
103
|
+
};
|
|
104
|
+
}
|
package/src/session.ts
CHANGED
|
@@ -19,7 +19,11 @@ import { adaptArrayOperators } from './sql/query-rewriters.ts';
|
|
|
19
19
|
import { mapResultRow } from './sql/result-mapper.ts';
|
|
20
20
|
import type { DuckDBDialect } from './dialect.ts';
|
|
21
21
|
import { TransactionRollbackError } from 'drizzle-orm/errors';
|
|
22
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
DuckDBClientLike,
|
|
24
|
+
DuckDBConnectionPool,
|
|
25
|
+
RowData,
|
|
26
|
+
} from './client.ts';
|
|
23
27
|
import {
|
|
24
28
|
executeArrowOnClient,
|
|
25
29
|
executeInBatches,
|
|
@@ -27,6 +31,8 @@ import {
|
|
|
27
31
|
prepareParams,
|
|
28
32
|
type ExecuteInBatchesOptions,
|
|
29
33
|
} from './client.ts';
|
|
34
|
+
import { isPool } from './client.ts';
|
|
35
|
+
import type { DuckDBConnection } from '@duckdb/node-api';
|
|
30
36
|
|
|
31
37
|
export type { DuckDBClientLike, RowData } from './client.ts';
|
|
32
38
|
|
|
@@ -165,8 +171,18 @@ export class DuckDBSession<
|
|
|
165
171
|
override async transaction<T>(
|
|
166
172
|
transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>
|
|
167
173
|
): Promise<T> {
|
|
174
|
+
let pinnedConnection: DuckDBConnection | undefined;
|
|
175
|
+
let pool: DuckDBConnectionPool | undefined;
|
|
176
|
+
|
|
177
|
+
let clientForTx: DuckDBClientLike = this.client;
|
|
178
|
+
if (isPool(this.client)) {
|
|
179
|
+
pool = this.client;
|
|
180
|
+
pinnedConnection = await pool.acquire();
|
|
181
|
+
clientForTx = pinnedConnection;
|
|
182
|
+
}
|
|
183
|
+
|
|
168
184
|
const session = new DuckDBSession(
|
|
169
|
-
|
|
185
|
+
clientForTx,
|
|
170
186
|
this.dialect,
|
|
171
187
|
this.schema,
|
|
172
188
|
this.options
|
|
@@ -178,15 +194,21 @@ export class DuckDBSession<
|
|
|
178
194
|
this.schema
|
|
179
195
|
);
|
|
180
196
|
|
|
181
|
-
await tx.execute(sql`BEGIN TRANSACTION;`);
|
|
182
|
-
|
|
183
197
|
try {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
198
|
+
await tx.execute(sql`BEGIN TRANSACTION;`);
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const result = await transaction(tx);
|
|
202
|
+
await tx.execute(sql`commit`);
|
|
203
|
+
return result;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
await tx.execute(sql`rollback`);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
if (pinnedConnection && pool) {
|
|
210
|
+
await pool.release(pinnedConnection);
|
|
211
|
+
}
|
|
190
212
|
}
|
|
191
213
|
}
|
|
192
214
|
|
|
@@ -297,6 +319,7 @@ export class DuckDBTransaction<
|
|
|
297
319
|
}
|
|
298
320
|
|
|
299
321
|
setTransaction(config: PgTransactionConfig): Promise<void> {
|
|
322
|
+
// Cast needed: PgTransaction doesn't expose dialect/session properties in public API
|
|
300
323
|
type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
|
|
301
324
|
return (this as unknown as Tx).session.execute(
|
|
302
325
|
sql`set transaction ${this.getTransactionConfigSQL(config)}`
|
|
@@ -307,11 +330,13 @@ export class DuckDBTransaction<
|
|
|
307
330
|
query: SQL,
|
|
308
331
|
options: ExecuteInBatchesOptions = {}
|
|
309
332
|
): AsyncGenerator<GenericRowData<T>[], void, void> {
|
|
333
|
+
// Cast needed: PgTransaction doesn't expose session property in public API
|
|
310
334
|
type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
|
|
311
335
|
return (this as unknown as Tx).session.executeBatches<T>(query, options);
|
|
312
336
|
}
|
|
313
337
|
|
|
314
338
|
executeArrow(query: SQL): Promise<unknown> {
|
|
339
|
+
// Cast needed: PgTransaction doesn't expose session property in public API
|
|
315
340
|
type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
|
|
316
341
|
return (this as unknown as Tx).session.executeArrow(query);
|
|
317
342
|
}
|
|
@@ -319,6 +344,7 @@ export class DuckDBTransaction<
|
|
|
319
344
|
override async transaction<T>(
|
|
320
345
|
transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>
|
|
321
346
|
): Promise<T> {
|
|
347
|
+
// Cast needed: PgTransaction doesn't expose dialect/session properties in public API
|
|
322
348
|
type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
|
|
323
349
|
const nestedTx = new DuckDBTransaction<TFullSchema, TSchema>(
|
|
324
350
|
(this as unknown as Tx).dialect,
|