@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.
- package/README.md +36 -18
- package/dist/client.d.ts +12 -0
- package/dist/columns.d.ts +18 -10
- package/dist/driver.d.ts +4 -0
- package/dist/duckdb-introspect.mjs +245 -51
- package/dist/index.d.ts +3 -0
- package/dist/index.mjs +435 -65
- package/dist/introspect.d.ts +12 -0
- package/dist/olap.d.ts +46 -0
- package/dist/session.d.ts +5 -0
- package/dist/value-wrappers.d.ts +104 -0
- package/package.json +7 -3
- package/src/bin/duckdb-introspect.ts +40 -3
- package/src/client.ts +135 -18
- package/src/columns.ts +65 -36
- package/src/dialect.ts +2 -2
- package/src/driver.ts +20 -6
- package/src/index.ts +3 -0
- package/src/introspect.ts +97 -33
- package/src/migrator.ts +1 -3
- package/src/olap.ts +189 -0
- package/src/select-builder.ts +3 -7
- package/src/session.ts +87 -18
- package/src/sql/query-rewriters.ts +5 -8
- package/src/sql/result-mapper.ts +6 -6
- package/src/sql/selection.ts +2 -9
- package/src/value-wrappers.ts +324 -0
package/dist/olap.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Subquery, type SQLWrapper } from 'drizzle-orm';
|
|
2
|
+
import type { AnyPgColumn, PgTable } from 'drizzle-orm/pg-core';
|
|
3
|
+
import type { PgViewBase } from 'drizzle-orm/pg-core/view-base';
|
|
4
|
+
import { SQL } from 'drizzle-orm/sql/sql';
|
|
5
|
+
import type { DuckDBDatabase } from './driver.ts';
|
|
6
|
+
export declare const countN: (expr?: SQLWrapper) => SQL<number>;
|
|
7
|
+
export declare const sumN: (expr: SQLWrapper) => SQL<number>;
|
|
8
|
+
export declare const avgN: (expr: SQLWrapper) => SQL<number>;
|
|
9
|
+
export declare const sumDistinctN: (expr: SQLWrapper) => SQL<number>;
|
|
10
|
+
export declare const percentileCont: (p: number, expr: SQLWrapper) => SQL<number>;
|
|
11
|
+
export declare const median: (expr: SQLWrapper) => SQL<number>;
|
|
12
|
+
export declare const anyValue: <T = unknown>(expr: SQLWrapper) => SQL<T>;
|
|
13
|
+
type PartitionOrder = {
|
|
14
|
+
partitionBy?: SQLWrapper | SQLWrapper[];
|
|
15
|
+
orderBy?: SQLWrapper | SQLWrapper[];
|
|
16
|
+
} | undefined;
|
|
17
|
+
export declare const rowNumber: (options?: PartitionOrder) => SQL<number>;
|
|
18
|
+
export declare const rank: (options?: PartitionOrder) => SQL<number>;
|
|
19
|
+
export declare const denseRank: (options?: PartitionOrder) => SQL<number>;
|
|
20
|
+
export declare const lag: <T = unknown>(expr: SQLWrapper, offset?: number, defaultValue?: SQLWrapper, options?: PartitionOrder) => SQL<T>;
|
|
21
|
+
export declare const lead: <T = unknown>(expr: SQLWrapper, offset?: number, defaultValue?: SQLWrapper, options?: PartitionOrder) => SQL<T>;
|
|
22
|
+
type ValueExpr = SQL | SQL.Aliased | AnyPgColumn;
|
|
23
|
+
type GroupKey = ValueExpr;
|
|
24
|
+
type MeasureMap = Record<string, ValueExpr>;
|
|
25
|
+
type NonAggMap = Record<string, ValueExpr>;
|
|
26
|
+
export declare class OlapBuilder {
|
|
27
|
+
private db;
|
|
28
|
+
private source?;
|
|
29
|
+
private keys;
|
|
30
|
+
private measureMap;
|
|
31
|
+
private nonAggregates;
|
|
32
|
+
private wrapNonAggWithAnyValue;
|
|
33
|
+
private orderByClauses;
|
|
34
|
+
constructor(db: DuckDBDatabase);
|
|
35
|
+
from(source: PgTable | Subquery | PgViewBase | SQL): this;
|
|
36
|
+
groupBy(keys: GroupKey[]): this;
|
|
37
|
+
measures(measures: MeasureMap): this;
|
|
38
|
+
selectNonAggregates(fields: NonAggMap, options?: {
|
|
39
|
+
anyValue?: boolean;
|
|
40
|
+
}): this;
|
|
41
|
+
orderBy(...clauses: ValueExpr[]): this;
|
|
42
|
+
build(): any;
|
|
43
|
+
run(): any;
|
|
44
|
+
}
|
|
45
|
+
export declare const olap: (db: DuckDBDatabase) => OlapBuilder;
|
|
46
|
+
export {};
|
package/dist/session.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { type Query, SQL } from 'drizzle-orm/sql/sql';
|
|
|
9
9
|
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
|
+
import { type ExecuteInBatchesOptions } from './client.ts';
|
|
12
13
|
export type { DuckDBClientLike, RowData } from './client.ts';
|
|
13
14
|
export declare class DuckDBPreparedQuery<T extends PreparedQueryConfig> extends PgPreparedQuery<T> {
|
|
14
15
|
private client;
|
|
@@ -47,12 +48,16 @@ export declare class DuckDBSession<TFullSchema extends Record<string, unknown> =
|
|
|
47
48
|
prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(query: Query, fields: SelectedFieldsOrdered | undefined, name: string | undefined, isResponseInArrayMode: boolean, customResultMapper?: (rows: unknown[][]) => T['execute']): PgPreparedQuery<T>;
|
|
48
49
|
transaction<T>(transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>): Promise<T>;
|
|
49
50
|
private warnOnStringArrayLiteral;
|
|
51
|
+
executeBatches<T extends RowData = RowData>(query: SQL, options?: ExecuteInBatchesOptions): AsyncGenerator<GenericRowData<T>[], void, void>;
|
|
52
|
+
executeArrow(query: SQL): Promise<unknown>;
|
|
50
53
|
}
|
|
51
54
|
export declare class DuckDBTransaction<TFullSchema extends Record<string, unknown>, TSchema extends TablesRelationalConfig> extends PgTransaction<DuckDBQueryResultHKT, TFullSchema, TSchema> {
|
|
52
55
|
static readonly [entityKind]: string;
|
|
53
56
|
rollback(): never;
|
|
54
57
|
getTransactionConfigSQL(config: PgTransactionConfig): SQL;
|
|
55
58
|
setTransaction(config: PgTransactionConfig): Promise<void>;
|
|
59
|
+
executeBatches<T extends RowData = RowData>(query: SQL, options?: ExecuteInBatchesOptions): AsyncGenerator<GenericRowData<T>[], void, void>;
|
|
60
|
+
executeArrow(query: SQL): Promise<unknown>;
|
|
56
61
|
transaction<T>(transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>): Promise<T>;
|
|
57
62
|
}
|
|
58
63
|
export type GenericRowData<T extends RowData = RowData> = T;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type DuckDBValue } from '@duckdb/node-api';
|
|
2
|
+
/**
|
|
3
|
+
* Symbol used to identify wrapped DuckDB values for native binding.
|
|
4
|
+
* Uses Symbol.for() to ensure cross-module compatibility.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DUCKDB_VALUE_MARKER: unique symbol;
|
|
7
|
+
/**
|
|
8
|
+
* Type identifier for each wrapper kind.
|
|
9
|
+
*/
|
|
10
|
+
export type DuckDBValueKind = 'list' | 'array' | 'struct' | 'map' | 'timestamp' | 'blob' | 'json';
|
|
11
|
+
/**
|
|
12
|
+
* Base interface for all tagged DuckDB value wrappers.
|
|
13
|
+
*/
|
|
14
|
+
export interface DuckDBValueWrapper<TKind extends DuckDBValueKind = DuckDBValueKind, TData = unknown> {
|
|
15
|
+
readonly [DUCKDB_VALUE_MARKER]: true;
|
|
16
|
+
readonly kind: TKind;
|
|
17
|
+
readonly data: TData;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* List wrapper - maps to DuckDBListValue
|
|
21
|
+
*/
|
|
22
|
+
export interface ListValueWrapper extends DuckDBValueWrapper<'list', unknown[]> {
|
|
23
|
+
readonly elementType?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Array wrapper (fixed size) - maps to DuckDBArrayValue
|
|
27
|
+
*/
|
|
28
|
+
export interface ArrayValueWrapper extends DuckDBValueWrapper<'array', unknown[]> {
|
|
29
|
+
readonly elementType?: string;
|
|
30
|
+
readonly fixedLength?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Struct wrapper - maps to DuckDBStructValue
|
|
34
|
+
*/
|
|
35
|
+
export interface StructValueWrapper extends DuckDBValueWrapper<'struct', Record<string, unknown>> {
|
|
36
|
+
readonly schema?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Map wrapper - maps to DuckDBMapValue
|
|
40
|
+
*/
|
|
41
|
+
export interface MapValueWrapper extends DuckDBValueWrapper<'map', Record<string, unknown>> {
|
|
42
|
+
readonly valueType?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Timestamp wrapper - maps to DuckDBTimestampValue or DuckDBTimestampTZValue
|
|
46
|
+
*/
|
|
47
|
+
export interface TimestampValueWrapper extends DuckDBValueWrapper<'timestamp', Date | string> {
|
|
48
|
+
readonly withTimezone: boolean;
|
|
49
|
+
readonly precision?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Blob wrapper - maps to DuckDBBlobValue
|
|
53
|
+
*/
|
|
54
|
+
export interface BlobValueWrapper extends DuckDBValueWrapper<'blob', Buffer | Uint8Array> {
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* JSON wrapper - delays JSON.stringify() to binding time.
|
|
58
|
+
* DuckDB stores JSON as VARCHAR internally.
|
|
59
|
+
*/
|
|
60
|
+
export interface JsonValueWrapper extends DuckDBValueWrapper<'json', unknown> {
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Union of all wrapper types for exhaustive type checking.
|
|
64
|
+
*/
|
|
65
|
+
export type AnyDuckDBValueWrapper = ListValueWrapper | ArrayValueWrapper | StructValueWrapper | MapValueWrapper | TimestampValueWrapper | BlobValueWrapper | JsonValueWrapper;
|
|
66
|
+
/**
|
|
67
|
+
* Type guard to check if a value is a tagged DuckDB wrapper.
|
|
68
|
+
* Optimized for fast detection in the hot path.
|
|
69
|
+
*/
|
|
70
|
+
export declare function isDuckDBWrapper(value: unknown): value is AnyDuckDBValueWrapper;
|
|
71
|
+
/**
|
|
72
|
+
* Create a list wrapper for variable-length lists.
|
|
73
|
+
*/
|
|
74
|
+
export declare function wrapList(data: unknown[], elementType?: string): ListValueWrapper;
|
|
75
|
+
/**
|
|
76
|
+
* Create an array wrapper for fixed-length arrays.
|
|
77
|
+
*/
|
|
78
|
+
export declare function wrapArray(data: unknown[], elementType?: string, fixedLength?: number): ArrayValueWrapper;
|
|
79
|
+
/**
|
|
80
|
+
* Create a struct wrapper for named field structures.
|
|
81
|
+
*/
|
|
82
|
+
export declare function wrapStruct(data: Record<string, unknown>, schema?: Record<string, string>): StructValueWrapper;
|
|
83
|
+
/**
|
|
84
|
+
* Create a map wrapper for key-value maps.
|
|
85
|
+
*/
|
|
86
|
+
export declare function wrapMap(data: Record<string, unknown>, valueType?: string): MapValueWrapper;
|
|
87
|
+
/**
|
|
88
|
+
* Create a timestamp wrapper.
|
|
89
|
+
*/
|
|
90
|
+
export declare function wrapTimestamp(data: Date | string, withTimezone: boolean, precision?: number): TimestampValueWrapper;
|
|
91
|
+
/**
|
|
92
|
+
* Create a blob wrapper for binary data.
|
|
93
|
+
*/
|
|
94
|
+
export declare function wrapBlob(data: Buffer | Uint8Array): BlobValueWrapper;
|
|
95
|
+
/**
|
|
96
|
+
* Create a JSON wrapper that delays JSON.stringify() to binding time.
|
|
97
|
+
* This ensures consistent handling with other wrapped types.
|
|
98
|
+
*/
|
|
99
|
+
export declare function wrapJson(data: unknown): JsonValueWrapper;
|
|
100
|
+
/**
|
|
101
|
+
* Convert a wrapper to a DuckDB Node API value.
|
|
102
|
+
* Uses exhaustive switch for compile-time safety.
|
|
103
|
+
*/
|
|
104
|
+
export declare function wrapperToNodeApiValue(wrapper: AnyDuckDBValueWrapper, toValue: (v: unknown) => DuckDBValue): DuckDBValue;
|
package/package.json
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
"module": "./dist/index.mjs",
|
|
4
4
|
"main": "./dist/index.mjs",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.3",
|
|
7
7
|
"description": "A drizzle ORM client for use with DuckDB. Based on drizzle's Postgres client.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "bun build --target=node ./src/index.ts --outfile=./dist/index.mjs --packages=external && bun build --target=node ./src/bin/duckdb-introspect.ts --outfile=./dist/duckdb-introspect.mjs --packages=external && bun run build:declarations",
|
|
11
11
|
"build:declarations": "tsc --emitDeclarationOnly --project tsconfig.types.json",
|
|
12
12
|
"test": "vitest",
|
|
13
|
-
"t": "vitest --watch --ui"
|
|
13
|
+
"t": "vitest --watch --ui",
|
|
14
|
+
"bench": "vitest bench --runInBand test/perf",
|
|
15
|
+
"perf:run": "bun run scripts/run-perf.ts",
|
|
16
|
+
"perf:compare": "bun run scripts/compare-perf.ts"
|
|
14
17
|
},
|
|
15
18
|
"bin": {
|
|
16
19
|
"duckdb-introspect": "dist/duckdb-introspect.mjs"
|
|
@@ -33,7 +36,8 @@
|
|
|
33
36
|
"prettier": "^3.5.3",
|
|
34
37
|
"typescript": "^5.8.2",
|
|
35
38
|
"uuid": "^10.0.0",
|
|
36
|
-
"vitest": "^1.6.0"
|
|
39
|
+
"vitest": "^1.6.0",
|
|
40
|
+
"tinybench": "^2.7.1"
|
|
37
41
|
},
|
|
38
42
|
"repository": {
|
|
39
43
|
"type": "git",
|
|
@@ -8,6 +8,8 @@ import { introspect } from '../introspect.ts';
|
|
|
8
8
|
|
|
9
9
|
interface CliOptions {
|
|
10
10
|
url?: string;
|
|
11
|
+
database?: string;
|
|
12
|
+
allDatabases: boolean;
|
|
11
13
|
schemas?: string[];
|
|
12
14
|
outFile: string;
|
|
13
15
|
includeViews: boolean;
|
|
@@ -18,6 +20,7 @@ interface CliOptions {
|
|
|
18
20
|
function parseArgs(argv: string[]): CliOptions {
|
|
19
21
|
const options: CliOptions = {
|
|
20
22
|
outFile: path.resolve(process.cwd(), 'drizzle/schema.ts'),
|
|
23
|
+
allDatabases: false,
|
|
21
24
|
includeViews: false,
|
|
22
25
|
useCustomTimeTypes: true,
|
|
23
26
|
};
|
|
@@ -28,13 +31,26 @@ function parseArgs(argv: string[]): CliOptions {
|
|
|
28
31
|
case '--url':
|
|
29
32
|
options.url = argv[++i];
|
|
30
33
|
break;
|
|
34
|
+
case '--database':
|
|
35
|
+
case '--db':
|
|
36
|
+
options.database = argv[++i];
|
|
37
|
+
break;
|
|
38
|
+
case '--all-databases':
|
|
39
|
+
options.allDatabases = true;
|
|
40
|
+
break;
|
|
31
41
|
case '--schema':
|
|
32
42
|
case '--schemas':
|
|
33
|
-
options.schemas = argv[++i]
|
|
43
|
+
options.schemas = argv[++i]
|
|
44
|
+
?.split(',')
|
|
45
|
+
.map((s) => s.trim())
|
|
46
|
+
.filter(Boolean);
|
|
34
47
|
break;
|
|
35
48
|
case '--out':
|
|
36
49
|
case '--outFile':
|
|
37
|
-
options.outFile = path.resolve(
|
|
50
|
+
options.outFile = path.resolve(
|
|
51
|
+
process.cwd(),
|
|
52
|
+
argv[++i] ?? 'drizzle/schema.ts'
|
|
53
|
+
);
|
|
38
54
|
break;
|
|
39
55
|
case '--include-views':
|
|
40
56
|
case '--includeViews':
|
|
@@ -68,11 +84,27 @@ Usage:
|
|
|
68
84
|
|
|
69
85
|
Options:
|
|
70
86
|
--url DuckDB database path (e.g. :memory:, ./local.duckdb, md:)
|
|
87
|
+
--database, --db Database/catalog to introspect (default: current database)
|
|
88
|
+
--all-databases Introspect all attached databases (not just current)
|
|
71
89
|
--schema Comma separated schema list (defaults to all non-system schemas)
|
|
72
90
|
--out Output file (default: ./drizzle/schema.ts)
|
|
73
91
|
--include-views Include views in the generated schema
|
|
74
92
|
--use-pg-time Use pg-core timestamp/date/time instead of DuckDB custom helpers
|
|
75
93
|
--import-base Override import path for duckdb helpers (default: package name)
|
|
94
|
+
|
|
95
|
+
Database Filtering:
|
|
96
|
+
By default, only tables from the current database are introspected. This prevents
|
|
97
|
+
returning tables from all attached databases in MotherDuck workspaces.
|
|
98
|
+
|
|
99
|
+
Use --database to specify a different database, or --all-databases to introspect
|
|
100
|
+
all attached databases.
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
# Local DuckDB file
|
|
104
|
+
bun x duckdb-introspect --url ./my-database.duckdb --out ./schema.ts
|
|
105
|
+
|
|
106
|
+
# MotherDuck (requires MOTHERDUCK_TOKEN env var)
|
|
107
|
+
MOTHERDUCK_TOKEN=xxx bun x duckdb-introspect --url md: --database my_cloud_db --out ./schema.ts
|
|
76
108
|
`);
|
|
77
109
|
}
|
|
78
110
|
|
|
@@ -94,6 +126,8 @@ async function main() {
|
|
|
94
126
|
|
|
95
127
|
try {
|
|
96
128
|
const result = await introspect(db, {
|
|
129
|
+
database: options.database,
|
|
130
|
+
allDatabases: options.allDatabases,
|
|
97
131
|
schemas: options.schemas,
|
|
98
132
|
includeViews: options.includeViews,
|
|
99
133
|
useCustomTimeTypes: options.useCustomTimeTypes,
|
|
@@ -105,7 +139,10 @@ async function main() {
|
|
|
105
139
|
|
|
106
140
|
console.log(`Wrote schema to ${options.outFile}`);
|
|
107
141
|
} finally {
|
|
108
|
-
if (
|
|
142
|
+
if (
|
|
143
|
+
'closeSync' in connection &&
|
|
144
|
+
typeof connection.closeSync === 'function'
|
|
145
|
+
) {
|
|
109
146
|
connection.closeSync();
|
|
110
147
|
}
|
|
111
148
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
listValue,
|
|
3
|
+
timestampValue,
|
|
3
4
|
type DuckDBConnection,
|
|
4
5
|
type DuckDBValue,
|
|
5
6
|
} from '@duckdb/node-api';
|
|
7
|
+
import {
|
|
8
|
+
DUCKDB_VALUE_MARKER,
|
|
9
|
+
wrapperToNodeApiValue,
|
|
10
|
+
type AnyDuckDBValueWrapper,
|
|
11
|
+
} from './value-wrappers.ts';
|
|
6
12
|
|
|
7
13
|
export type DuckDBClientLike = DuckDBConnection;
|
|
8
14
|
export type RowData = Record<string, unknown>;
|
|
@@ -50,13 +56,62 @@ export function prepareParams(
|
|
|
50
56
|
});
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Convert a value to DuckDB Node API value.
|
|
61
|
+
* Handles wrapper types and plain values for backward compatibility.
|
|
62
|
+
* Optimized for the common case (primitives) in the hot path.
|
|
63
|
+
*/
|
|
53
64
|
function toNodeApiValue(value: unknown): DuckDBValue {
|
|
65
|
+
// Fast path 1: null/undefined
|
|
66
|
+
if (value == null) return null;
|
|
67
|
+
|
|
68
|
+
// Fast path 2: primitives (most common)
|
|
69
|
+
const t = typeof value;
|
|
70
|
+
if (t === 'string' || t === 'number' || t === 'bigint' || t === 'boolean') {
|
|
71
|
+
return value as DuckDBValue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Fast path 3: pre-wrapped DuckDB value (Symbol check ~2-3ns)
|
|
75
|
+
if (t === 'object' && DUCKDB_VALUE_MARKER in (value as object)) {
|
|
76
|
+
return wrapperToNodeApiValue(
|
|
77
|
+
value as AnyDuckDBValueWrapper,
|
|
78
|
+
toNodeApiValue
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Legacy path: plain arrays (backward compatibility)
|
|
54
83
|
if (Array.isArray(value)) {
|
|
55
84
|
return listValue(value.map((inner) => toNodeApiValue(inner)));
|
|
56
85
|
}
|
|
86
|
+
|
|
87
|
+
// Date conversion to timestamp
|
|
88
|
+
if (value instanceof Date) {
|
|
89
|
+
return timestampValue(BigInt(value.getTime()) * 1000n);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Fallback for unknown objects
|
|
57
93
|
return value as DuckDBValue;
|
|
58
94
|
}
|
|
59
95
|
|
|
96
|
+
function deduplicateColumns(columns: string[]): string[] {
|
|
97
|
+
const seen: Record<string, number> = {};
|
|
98
|
+
return columns.map((col) => {
|
|
99
|
+
const count = seen[col] ?? 0;
|
|
100
|
+
seen[col] = count + 1;
|
|
101
|
+
return count === 0 ? col : `${col}_${count}`;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function mapRowsToObjects(columns: string[], rows: unknown[][]): RowData[] {
|
|
106
|
+
return rows.map((vals) => {
|
|
107
|
+
const obj: Record<string, unknown> = {};
|
|
108
|
+
columns.forEach((col, idx) => {
|
|
109
|
+
obj[col] = vals[idx];
|
|
110
|
+
});
|
|
111
|
+
return obj;
|
|
112
|
+
}) as RowData[];
|
|
113
|
+
}
|
|
114
|
+
|
|
60
115
|
export async function closeClientConnection(
|
|
61
116
|
connection: DuckDBConnection
|
|
62
117
|
): Promise<void> {
|
|
@@ -65,10 +120,7 @@ export async function closeClientConnection(
|
|
|
65
120
|
return;
|
|
66
121
|
}
|
|
67
122
|
|
|
68
|
-
if (
|
|
69
|
-
'closeSync' in connection &&
|
|
70
|
-
typeof connection.closeSync === 'function'
|
|
71
|
-
) {
|
|
123
|
+
if ('closeSync' in connection && typeof connection.closeSync === 'function') {
|
|
72
124
|
connection.closeSync();
|
|
73
125
|
return;
|
|
74
126
|
}
|
|
@@ -92,19 +144,84 @@ export async function executeOnClient(
|
|
|
92
144
|
: undefined;
|
|
93
145
|
const result = await client.run(query, values);
|
|
94
146
|
const rows = await result.getRowsJS();
|
|
95
|
-
const columns =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
seen[col] = count + 1;
|
|
100
|
-
return count === 0 ? col : `${col}_${count}`;
|
|
101
|
-
});
|
|
147
|
+
const columns =
|
|
148
|
+
// prefer deduplicated names when available (Node API >=1.4.2)
|
|
149
|
+
result.deduplicatedColumnNames?.() ?? result.columnNames();
|
|
150
|
+
const uniqueColumns = deduplicateColumns(columns);
|
|
102
151
|
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
152
|
+
return rows ? mapRowsToObjects(uniqueColumns, rows) : [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface ExecuteInBatchesOptions {
|
|
156
|
+
rowsPerChunk?: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Stream results from DuckDB in batches to avoid fully materializing rows in JS.
|
|
161
|
+
*/
|
|
162
|
+
export async function* executeInBatches(
|
|
163
|
+
client: DuckDBClientLike,
|
|
164
|
+
query: string,
|
|
165
|
+
params: unknown[],
|
|
166
|
+
options: ExecuteInBatchesOptions = {}
|
|
167
|
+
): AsyncGenerator<RowData[], void, void> {
|
|
168
|
+
const rowsPerChunk =
|
|
169
|
+
options.rowsPerChunk && options.rowsPerChunk > 0
|
|
170
|
+
? options.rowsPerChunk
|
|
171
|
+
: 100_000;
|
|
172
|
+
const values =
|
|
173
|
+
params.length > 0
|
|
174
|
+
? (params.map((param) => toNodeApiValue(param)) as DuckDBValue[])
|
|
175
|
+
: undefined;
|
|
176
|
+
|
|
177
|
+
const result = await client.stream(query, values);
|
|
178
|
+
const columns =
|
|
179
|
+
// prefer deduplicated names when available (Node API >=1.4.2)
|
|
180
|
+
result.deduplicatedColumnNames?.() ?? result.columnNames();
|
|
181
|
+
const uniqueColumns = deduplicateColumns(columns);
|
|
182
|
+
|
|
183
|
+
let buffer: RowData[] = [];
|
|
184
|
+
|
|
185
|
+
for await (const chunk of result.yieldRowsJs()) {
|
|
186
|
+
const objects = mapRowsToObjects(uniqueColumns, chunk);
|
|
187
|
+
for (const row of objects) {
|
|
188
|
+
buffer.push(row);
|
|
189
|
+
if (buffer.length >= rowsPerChunk) {
|
|
190
|
+
yield buffer;
|
|
191
|
+
buffer = [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (buffer.length > 0) {
|
|
197
|
+
yield buffer;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Return columnar results when the underlying node-api exposes an Arrow/columnar API.
|
|
203
|
+
* Falls back to column-major JS arrays when Arrow is unavailable.
|
|
204
|
+
*/
|
|
205
|
+
export async function executeArrowOnClient(
|
|
206
|
+
client: DuckDBClientLike,
|
|
207
|
+
query: string,
|
|
208
|
+
params: unknown[]
|
|
209
|
+
): Promise<unknown> {
|
|
210
|
+
const values =
|
|
211
|
+
params.length > 0
|
|
212
|
+
? (params.map((param) => toNodeApiValue(param)) as DuckDBValue[])
|
|
213
|
+
: undefined;
|
|
214
|
+
const result = await client.run(query, values);
|
|
215
|
+
|
|
216
|
+
const maybeArrow =
|
|
217
|
+
(result as unknown as { toArrow?: () => Promise<unknown> }).toArrow ??
|
|
218
|
+
(result as unknown as { getArrowTable?: () => Promise<unknown> })
|
|
219
|
+
.getArrowTable;
|
|
220
|
+
|
|
221
|
+
if (typeof maybeArrow === 'function') {
|
|
222
|
+
return await maybeArrow.call(result);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Fallback: return column-major JS arrays to avoid per-row object creation.
|
|
226
|
+
return result.getColumnsObjectJS();
|
|
110
227
|
}
|