@leonardovida-md/drizzle-neo-duckdb 1.0.2 → 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.
@@ -61,5 +61,6 @@ export interface IntrospectResult {
61
61
  relationsTs?: string;
62
62
  };
63
63
  }
64
+ export declare const DEFAULT_IMPORT_BASE = "@leonardovida-md/drizzle-neo-duckdb/helpers";
64
65
  export declare function introspect(db: DuckDBDatabase, opts?: IntrospectOptions): Promise<IntrospectResult>;
65
66
  export {};
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/pool.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { DuckDBInstance } from '@duckdb/node-api';
2
+ import { type DuckDBConnectionPool } from './client.ts';
3
+ /** Pool size presets for different MotherDuck instance types */
4
+ export type PoolPreset = 'pulse' | 'standard' | 'jumbo' | 'mega' | 'giga' | 'local' | 'memory';
5
+ /** Pool sizes optimized for each MotherDuck instance type */
6
+ export declare const POOL_PRESETS: Record<PoolPreset, number>;
7
+ export interface DuckDBPoolConfig {
8
+ /** Maximum concurrent connections. Defaults to 4. */
9
+ size?: number;
10
+ }
11
+ /**
12
+ * Resolve pool configuration to a concrete size.
13
+ * Returns false if pooling is disabled.
14
+ */
15
+ export declare function resolvePoolSize(pool: DuckDBPoolConfig | PoolPreset | false | undefined): number | false;
16
+ export interface DuckDBConnectionPoolOptions {
17
+ /** Maximum concurrent connections. Defaults to 4. */
18
+ size?: number;
19
+ }
20
+ export declare function createDuckDBConnectionPool(instance: DuckDBInstance, options?: DuckDBConnectionPoolOptions): DuckDBConnectionPool & {
21
+ size: number;
22
+ };
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;
@@ -1,2 +1 @@
1
1
  export declare function adaptArrayOperators(query: string): string;
2
- export declare function queryAdapter(query: string): string;
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { aliasFields } from './sql/selection.ts';
2
- export { adaptArrayOperators, queryAdapter } from './sql/query-rewriters.ts';
2
+ export { adaptArrayOperators } from './sql/query-rewriters.ts';
3
3
  export { mapResultRow } from './sql/result-mapper.ts';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * DuckDB wrapper value helpers that are safe for client-side bundles.
3
+ * These utilities only tag values; conversion to native bindings lives
4
+ * in value-wrappers.ts to avoid pulling @duckdb/node-api into browsers.
5
+ */
6
+ export declare const DUCKDB_VALUE_MARKER: unique symbol;
7
+ export type DuckDBValueKind = 'list' | 'array' | 'struct' | 'map' | 'timestamp' | 'blob' | 'json';
8
+ export interface DuckDBValueWrapper<TKind extends DuckDBValueKind = DuckDBValueKind, TData = unknown> {
9
+ readonly [DUCKDB_VALUE_MARKER]: true;
10
+ readonly kind: TKind;
11
+ readonly data: TData;
12
+ }
13
+ export interface ListValueWrapper extends DuckDBValueWrapper<'list', unknown[]> {
14
+ readonly elementType?: string;
15
+ }
16
+ export interface ArrayValueWrapper extends DuckDBValueWrapper<'array', unknown[]> {
17
+ readonly elementType?: string;
18
+ readonly fixedLength?: number;
19
+ }
20
+ export interface StructValueWrapper extends DuckDBValueWrapper<'struct', Record<string, unknown>> {
21
+ readonly schema?: Record<string, string>;
22
+ }
23
+ export interface MapValueWrapper extends DuckDBValueWrapper<'map', Record<string, unknown>> {
24
+ readonly valueType?: string;
25
+ }
26
+ export interface TimestampValueWrapper extends DuckDBValueWrapper<'timestamp', Date | string> {
27
+ readonly withTimezone: boolean;
28
+ readonly precision?: number;
29
+ }
30
+ export interface BlobValueWrapper extends DuckDBValueWrapper<'blob', Buffer | Uint8Array> {
31
+ }
32
+ export interface JsonValueWrapper extends DuckDBValueWrapper<'json', unknown> {
33
+ }
34
+ export type AnyDuckDBValueWrapper = ListValueWrapper | ArrayValueWrapper | StructValueWrapper | MapValueWrapper | TimestampValueWrapper | BlobValueWrapper | JsonValueWrapper;
35
+ export declare function isDuckDBWrapper(value: unknown): value is AnyDuckDBValueWrapper;
36
+ export declare function wrapList(data: unknown[], elementType?: string): ListValueWrapper;
37
+ export declare function wrapArray(data: unknown[], elementType?: string, fixedLength?: number): ArrayValueWrapper;
38
+ export declare function wrapStruct(data: Record<string, unknown>, schema?: Record<string, string>): StructValueWrapper;
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;
41
+ export declare function wrapBlob(data: Buffer | Uint8Array): BlobValueWrapper;
42
+ export declare function wrapJson(data: unknown): JsonValueWrapper;
@@ -0,0 +1,8 @@
1
+ import { type DuckDBValue } from '@duckdb/node-api';
2
+ import { type AnyDuckDBValueWrapper } from './value-wrappers-core.ts';
3
+ /**
4
+ * Convert a wrapper to a DuckDB Node API value.
5
+ * Uses exhaustive switch for compile-time safety.
6
+ */
7
+ export declare function wrapperToNodeApiValue(wrapper: AnyDuckDBValueWrapper, toValue: (v: unknown) => DuckDBValue): DuckDBValue;
8
+ export { DUCKDB_VALUE_MARKER, isDuckDBWrapper, wrapArray, wrapBlob, wrapJson, wrapList, wrapMap, wrapStruct, wrapTimestamp, type AnyDuckDBValueWrapper, type DuckDBValueWrapper, type ArrayValueWrapper, type BlobValueWrapper, type JsonValueWrapper, type ListValueWrapper, type MapValueWrapper, type StructValueWrapper, type TimestampValueWrapper, type DuckDBValueKind, } from './value-wrappers-core.ts';
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.2",
6
+ "version": "1.1.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": {
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",
10
+ "build": "bun build --target=node ./src/index.ts --outfile=./dist/index.mjs --packages=external && bun build --target=node ./src/helpers.ts --outfile=./dist/helpers.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",
@@ -44,6 +48,10 @@
44
48
  "types": "./dist/index.d.ts",
45
49
  "import": "./dist/index.mjs"
46
50
  },
51
+ "./helpers": {
52
+ "types": "./dist/helpers.d.ts",
53
+ "import": "./dist/helpers.mjs"
54
+ },
47
55
  "./package.json": "./package.json"
48
56
  },
49
57
  "sideEffects": false,
@@ -40,11 +40,17 @@ function parseArgs(argv: string[]): CliOptions {
40
40
  break;
41
41
  case '--schema':
42
42
  case '--schemas':
43
- options.schemas = argv[++i]?.split(',').map((s) => s.trim()).filter(Boolean);
43
+ options.schemas = argv[++i]
44
+ ?.split(',')
45
+ .map((s) => s.trim())
46
+ .filter(Boolean);
44
47
  break;
45
48
  case '--out':
46
49
  case '--outFile':
47
- options.outFile = path.resolve(process.cwd(), argv[++i] ?? 'drizzle/schema.ts');
50
+ options.outFile = path.resolve(
51
+ process.cwd(),
52
+ argv[++i] ?? 'drizzle/schema.ts'
53
+ );
48
54
  break;
49
55
  case '--include-views':
50
56
  case '--includeViews':
@@ -133,7 +139,10 @@ async function main() {
133
139
 
134
140
  console.log(`Wrote schema to ${options.outFile}`);
135
141
  } finally {
136
- if ('closeSync' in connection && typeof connection.closeSync === 'function') {
142
+ if (
143
+ 'closeSync' in connection &&
144
+ typeof connection.closeSync === 'function'
145
+ ) {
137
146
  connection.closeSync();
138
147
  }
139
148
  }
package/src/client.ts CHANGED
@@ -1,12 +1,30 @@
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
- export type DuckDBClientLike = DuckDBConnection;
13
+ export type DuckDBClientLike = DuckDBConnection | DuckDBConnectionPool;
8
14
  export type RowData = Record<string, unknown>;
9
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
+
10
28
  export interface PrepareParamsOptions {
11
29
  rejectStringArrayLiterals?: boolean;
12
30
  warnOnStringArrayLiteral?: () => void;
@@ -26,8 +44,6 @@ function parsePgArrayLiteral(value: string): unknown {
26
44
  }
27
45
  }
28
46
 
29
- let warnedArrayLiteral = false;
30
-
31
47
  export function prepareParams(
32
48
  params: unknown[],
33
49
  options: PrepareParamsOptions = {}
@@ -40,8 +56,7 @@ export function prepareParams(
40
56
  );
41
57
  }
42
58
 
43
- if (!warnedArrayLiteral && options.warnOnStringArrayLiteral) {
44
- warnedArrayLiteral = true;
59
+ if (options.warnOnStringArrayLiteral) {
45
60
  options.warnOnStringArrayLiteral();
46
61
  }
47
62
  return parsePgArrayLiteral(param);
@@ -50,13 +65,62 @@ export function prepareParams(
50
65
  });
51
66
  }
52
67
 
68
+ /**
69
+ * Convert a value to DuckDB Node API value.
70
+ * Handles wrapper types and plain values for backward compatibility.
71
+ * Optimized for the common case (primitives) in the hot path.
72
+ */
53
73
  function toNodeApiValue(value: unknown): DuckDBValue {
74
+ // Fast path 1: null/undefined
75
+ if (value == null) return null;
76
+
77
+ // Fast path 2: primitives (most common)
78
+ const t = typeof value;
79
+ if (t === 'string' || t === 'number' || t === 'bigint' || t === 'boolean') {
80
+ return value as DuckDBValue;
81
+ }
82
+
83
+ // Fast path 3: pre-wrapped DuckDB value (Symbol check ~2-3ns)
84
+ if (t === 'object' && DUCKDB_VALUE_MARKER in (value as object)) {
85
+ return wrapperToNodeApiValue(
86
+ value as AnyDuckDBValueWrapper,
87
+ toNodeApiValue
88
+ );
89
+ }
90
+
91
+ // Legacy path: plain arrays (backward compatibility)
54
92
  if (Array.isArray(value)) {
55
93
  return listValue(value.map((inner) => toNodeApiValue(inner)));
56
94
  }
95
+
96
+ // Date conversion to timestamp
97
+ if (value instanceof Date) {
98
+ return timestampValue(BigInt(value.getTime()) * 1000n);
99
+ }
100
+
101
+ // Fallback for unknown objects
57
102
  return value as DuckDBValue;
58
103
  }
59
104
 
105
+ function deduplicateColumns(columns: string[]): string[] {
106
+ const seen: Record<string, number> = {};
107
+ return columns.map((col) => {
108
+ const count = seen[col] ?? 0;
109
+ seen[col] = count + 1;
110
+ return count === 0 ? col : `${col}_${count}`;
111
+ });
112
+ }
113
+
114
+ function mapRowsToObjects(columns: string[], rows: unknown[][]): RowData[] {
115
+ return rows.map((vals) => {
116
+ const obj: Record<string, unknown> = {};
117
+ columns.forEach((col, idx) => {
118
+ obj[col] = vals[idx];
119
+ });
120
+ return obj;
121
+ }) as RowData[];
122
+ }
123
+
60
124
  export async function closeClientConnection(
61
125
  connection: DuckDBConnection
62
126
  ): Promise<void> {
@@ -65,10 +129,7 @@ export async function closeClientConnection(
65
129
  return;
66
130
  }
67
131
 
68
- if (
69
- 'closeSync' in connection &&
70
- typeof connection.closeSync === 'function'
71
- ) {
132
+ if ('closeSync' in connection && typeof connection.closeSync === 'function') {
72
133
  connection.closeSync();
73
134
  return;
74
135
  }
@@ -86,25 +147,119 @@ export async function executeOnClient(
86
147
  query: string,
87
148
  params: unknown[]
88
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
+
89
159
  const values =
90
160
  params.length > 0
91
161
  ? (params.map((param) => toNodeApiValue(param)) as DuckDBValue[])
92
162
  : undefined;
93
163
  const result = await client.run(query, values);
94
164
  const rows = await result.getRowsJS();
95
- const columns = result.columnNames();
96
- const seen: Record<string, number> = {};
97
- const uniqueColumns = columns.map((col) => {
98
- const count = seen[col] ?? 0;
99
- seen[col] = count + 1;
100
- return count === 0 ? col : `${col}_${count}`;
101
- });
165
+ const columns =
166
+ // prefer deduplicated names when available (Node API >=1.4.2)
167
+ result.deduplicatedColumnNames?.() ?? result.columnNames();
168
+ const uniqueColumns = deduplicateColumns(columns);
102
169
 
103
- return (rows ?? []).map((vals) => {
104
- const obj: Record<string, unknown> = {};
105
- uniqueColumns.forEach((col, idx) => {
106
- obj[col] = vals[idx];
107
- });
108
- return obj;
109
- }) as RowData[];
170
+ return rows ? mapRowsToObjects(uniqueColumns, rows) : [];
171
+ }
172
+
173
+ export interface ExecuteInBatchesOptions {
174
+ rowsPerChunk?: number;
175
+ }
176
+
177
+ /**
178
+ * Stream results from DuckDB in batches to avoid fully materializing rows in JS.
179
+ */
180
+ export async function* executeInBatches(
181
+ client: DuckDBClientLike,
182
+ query: string,
183
+ params: unknown[],
184
+ options: ExecuteInBatchesOptions = {}
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
+
196
+ const rowsPerChunk =
197
+ options.rowsPerChunk && options.rowsPerChunk > 0
198
+ ? options.rowsPerChunk
199
+ : 100_000;
200
+ const values =
201
+ params.length > 0
202
+ ? (params.map((param) => toNodeApiValue(param)) as DuckDBValue[])
203
+ : undefined;
204
+
205
+ const result = await client.stream(query, values);
206
+ const columns =
207
+ // prefer deduplicated names when available (Node API >=1.4.2)
208
+ result.deduplicatedColumnNames?.() ?? result.columnNames();
209
+ const uniqueColumns = deduplicateColumns(columns);
210
+
211
+ let buffer: RowData[] = [];
212
+
213
+ for await (const chunk of result.yieldRowsJs()) {
214
+ const objects = mapRowsToObjects(uniqueColumns, chunk);
215
+ for (const row of objects) {
216
+ buffer.push(row);
217
+ if (buffer.length >= rowsPerChunk) {
218
+ yield buffer;
219
+ buffer = [];
220
+ }
221
+ }
222
+ }
223
+
224
+ if (buffer.length > 0) {
225
+ yield buffer;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Return columnar results when the underlying node-api exposes an Arrow/columnar API.
231
+ * Falls back to column-major JS arrays when Arrow is unavailable.
232
+ */
233
+ export async function executeArrowOnClient(
234
+ client: DuckDBClientLike,
235
+ query: string,
236
+ params: unknown[]
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
+
247
+ const values =
248
+ params.length > 0
249
+ ? (params.map((param) => toNodeApiValue(param)) as DuckDBValue[])
250
+ : undefined;
251
+ const result = await client.run(query, values);
252
+
253
+ // Runtime detection for Arrow API support (optional method, not in base type)
254
+ const maybeArrow =
255
+ (result as unknown as { toArrow?: () => Promise<unknown> }).toArrow ??
256
+ (result as unknown as { getArrowTable?: () => Promise<unknown> })
257
+ .getArrowTable;
258
+
259
+ if (typeof maybeArrow === 'function') {
260
+ return await maybeArrow.call(result);
261
+ }
262
+
263
+ // Fallback: return column-major JS arrays to avoid per-row object creation.
264
+ return result.getColumnsObjectJS();
110
265
  }