@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/src/olap.ts ADDED
@@ -0,0 +1,189 @@
1
+ import { is } from 'drizzle-orm/entity';
2
+ import { sql, Subquery, type SQLWrapper } from 'drizzle-orm';
3
+ import type { AnyPgColumn, PgTable } from 'drizzle-orm/pg-core';
4
+ import type { PgViewBase } from 'drizzle-orm/pg-core/view-base';
5
+ import type { SelectedFields } from 'drizzle-orm/pg-core/query-builders';
6
+ import { SQL } from 'drizzle-orm/sql/sql';
7
+ import { Column, getTableName } from 'drizzle-orm';
8
+ import type { DuckDBDatabase } from './driver.ts';
9
+
10
+ export const countN = (expr: SQLWrapper = sql`*`) =>
11
+ sql<number>`count(${expr})`.mapWith(Number);
12
+
13
+ export const sumN = (expr: SQLWrapper) =>
14
+ sql<number>`sum(${expr})`.mapWith(Number);
15
+
16
+ export const avgN = (expr: SQLWrapper) =>
17
+ sql<number>`avg(${expr})`.mapWith(Number);
18
+
19
+ export const sumDistinctN = (expr: SQLWrapper) =>
20
+ sql<number>`sum(distinct ${expr})`.mapWith(Number);
21
+
22
+ export const percentileCont = (p: number, expr: SQLWrapper) =>
23
+ sql<number>`percentile_cont(${p}) within group (order by ${expr})`.mapWith(
24
+ Number
25
+ );
26
+
27
+ export const median = (expr: SQLWrapper) => percentileCont(0.5, expr);
28
+
29
+ export const anyValue = <T = unknown>(expr: SQLWrapper) =>
30
+ sql<T>`any_value(${expr})`;
31
+
32
+ type PartitionOrder =
33
+ | {
34
+ partitionBy?: SQLWrapper | SQLWrapper[];
35
+ orderBy?: SQLWrapper | SQLWrapper[];
36
+ }
37
+ | undefined;
38
+
39
+ function normalizeArray<T>(value?: T | T[]): T[] {
40
+ if (!value) return [];
41
+ return Array.isArray(value) ? value : [value];
42
+ }
43
+
44
+ function overClause(options?: PartitionOrder) {
45
+ const partitions = normalizeArray(options?.partitionBy);
46
+ const orders = normalizeArray(options?.orderBy);
47
+
48
+ const chunks: SQLWrapper[] = [];
49
+
50
+ if (partitions.length > 0) {
51
+ chunks.push(sql`partition by ${sql.join(partitions, sql`, `)}`);
52
+ }
53
+
54
+ if (orders.length > 0) {
55
+ chunks.push(sql`order by ${sql.join(orders, sql`, `)}`);
56
+ }
57
+
58
+ if (chunks.length === 0) {
59
+ return sql``;
60
+ }
61
+
62
+ return sql`over (${sql.join(chunks, sql` `)})`;
63
+ }
64
+
65
+ export const rowNumber = (options?: PartitionOrder) =>
66
+ sql<number>`row_number() ${overClause(options)}`.mapWith(Number);
67
+
68
+ export const rank = (options?: PartitionOrder) =>
69
+ sql<number>`rank() ${overClause(options)}`.mapWith(Number);
70
+
71
+ export const denseRank = (options?: PartitionOrder) =>
72
+ sql<number>`dense_rank() ${overClause(options)}`.mapWith(Number);
73
+
74
+ export const lag = <T = unknown>(
75
+ expr: SQLWrapper,
76
+ offset = 1,
77
+ defaultValue?: SQLWrapper,
78
+ options?: PartitionOrder
79
+ ) =>
80
+ defaultValue
81
+ ? sql<T>`lag(${expr}, ${offset}, ${defaultValue}) ${overClause(options)}`
82
+ : sql<T>`lag(${expr}, ${offset}) ${overClause(options)}`;
83
+
84
+ export const lead = <T = unknown>(
85
+ expr: SQLWrapper,
86
+ offset = 1,
87
+ defaultValue?: SQLWrapper,
88
+ options?: PartitionOrder
89
+ ) =>
90
+ defaultValue
91
+ ? sql<T>`lead(${expr}, ${offset}, ${defaultValue}) ${overClause(options)}`
92
+ : sql<T>`lead(${expr}, ${offset}) ${overClause(options)}`;
93
+
94
+ type ValueExpr = SQL | SQL.Aliased | AnyPgColumn;
95
+ type GroupKey = ValueExpr;
96
+ type MeasureMap = Record<string, ValueExpr>;
97
+ type NonAggMap = Record<string, ValueExpr>;
98
+
99
+ function keyAlias(key: SQLWrapper, fallback: string): string {
100
+ if (is(key, SQL.Aliased)) {
101
+ return key.fieldAlias ?? fallback;
102
+ }
103
+ if (is(key, Column)) {
104
+ return `${getTableName(key.table)}.${key.name}`;
105
+ }
106
+ return fallback;
107
+ }
108
+
109
+ export class OlapBuilder {
110
+ private source?: PgTable | Subquery | PgViewBase | SQL;
111
+ private keys: GroupKey[] = [];
112
+ private measureMap: MeasureMap = {};
113
+ private nonAggregates: NonAggMap = {};
114
+ private wrapNonAggWithAnyValue = false;
115
+ private orderByClauses: ValueExpr[] = [];
116
+
117
+ constructor(private db: DuckDBDatabase) {}
118
+
119
+ from(source: PgTable | Subquery | PgViewBase | SQL): this {
120
+ this.source = source;
121
+ return this;
122
+ }
123
+
124
+ groupBy(keys: GroupKey[]): this {
125
+ this.keys = keys;
126
+ return this;
127
+ }
128
+
129
+ measures(measures: MeasureMap): this {
130
+ this.measureMap = measures;
131
+ return this;
132
+ }
133
+
134
+ selectNonAggregates(
135
+ fields: NonAggMap,
136
+ options: { anyValue?: boolean } = {}
137
+ ): this {
138
+ this.nonAggregates = fields;
139
+ this.wrapNonAggWithAnyValue = options.anyValue ?? false;
140
+ return this;
141
+ }
142
+
143
+ orderBy(...clauses: ValueExpr[]): this {
144
+ this.orderByClauses = clauses;
145
+ return this;
146
+ }
147
+
148
+ build() {
149
+ if (!this.source) {
150
+ throw new Error('olap: .from() is required');
151
+ }
152
+ if (this.keys.length === 0) {
153
+ throw new Error('olap: .groupBy() is required');
154
+ }
155
+ if (Object.keys(this.measureMap).length === 0) {
156
+ throw new Error('olap: .measures() is required');
157
+ }
158
+
159
+ const selection: Record<string, ValueExpr> = {};
160
+
161
+ this.keys.forEach((key, idx) => {
162
+ const alias = keyAlias(key, `key_${idx}`);
163
+ selection[alias] = key;
164
+ });
165
+
166
+ Object.entries(this.nonAggregates).forEach(([alias, expr]) => {
167
+ selection[alias] = this.wrapNonAggWithAnyValue ? anyValue(expr) : expr;
168
+ });
169
+
170
+ Object.assign(selection, this.measureMap);
171
+
172
+ let query: any = this.db
173
+ .select(selection as SelectedFields)
174
+ .from(this.source!)
175
+ .groupBy(...this.keys);
176
+
177
+ if (this.orderByClauses.length > 0) {
178
+ query = query.orderBy(...this.orderByClauses);
179
+ }
180
+
181
+ return query;
182
+ }
183
+
184
+ run() {
185
+ return this.build();
186
+ }
187
+ }
188
+
189
+ export const olap = (db: DuckDBDatabase) => new OlapBuilder(db);
@@ -6,11 +6,7 @@ import {
6
6
  type SelectedFields,
7
7
  type TableLikeHasEmptySelection,
8
8
  } from 'drizzle-orm/pg-core/query-builders';
9
- import {
10
- PgColumn,
11
- PgTable,
12
- type PgSession,
13
- } from 'drizzle-orm/pg-core';
9
+ import { PgColumn, PgTable, type PgSession } from 'drizzle-orm/pg-core';
14
10
  import { Subquery, ViewBaseConfig, type SQLWrapper } from 'drizzle-orm';
15
11
  import { PgViewBase } from 'drizzle-orm/pg-core/view-base';
16
12
  import type {
@@ -25,7 +21,7 @@ import { getTableColumns, type DrizzleTypeError } from 'drizzle-orm/utils';
25
21
  interface PgViewBaseInternal<
26
22
  TName extends string = string,
27
23
  TExisting extends boolean = boolean,
28
- TSelectedFields extends ColumnsSelection = ColumnsSelection
24
+ TSelectedFields extends ColumnsSelection = ColumnsSelection,
29
25
  > extends PgViewBase<TName, TExisting, TSelectedFields> {
30
26
  [ViewBaseConfig]?: {
31
27
  selectedFields: SelectedFields;
@@ -34,7 +30,7 @@ interface PgViewBaseInternal<
34
30
 
35
31
  export class DuckDBSelectBuilder<
36
32
  TSelection extends SelectedFields | undefined,
37
- TBuilderMode extends 'db' | 'qb' = 'db'
33
+ TBuilderMode extends 'db' | 'qb' = 'db',
38
34
  > extends PgSelectBuilder<TSelection, TBuilderMode> {
39
35
  private _fields: TSelection;
40
36
  private _session: PgSession | undefined;
package/src/session.ts CHANGED
@@ -20,12 +20,18 @@ import { mapResultRow } from './sql/result-mapper.ts';
20
20
  import type { DuckDBDialect } from './dialect.ts';
21
21
  import { TransactionRollbackError } from 'drizzle-orm/errors';
22
22
  import type { DuckDBClientLike, RowData } from './client.ts';
23
- import { executeOnClient, prepareParams } from './client.ts';
23
+ import {
24
+ executeArrowOnClient,
25
+ executeInBatches,
26
+ executeOnClient,
27
+ prepareParams,
28
+ type ExecuteInBatchesOptions,
29
+ } from './client.ts';
24
30
 
25
31
  export type { DuckDBClientLike, RowData } from './client.ts';
26
32
 
27
33
  export class DuckDBPreparedQuery<
28
- T extends PreparedQueryConfig
34
+ T extends PreparedQueryConfig,
29
35
  > extends PgPreparedQuery<T> {
30
36
  static readonly [entityKind]: string = 'DuckDBPreparedQuery';
31
37
 
@@ -37,7 +43,9 @@ export class DuckDBPreparedQuery<
37
43
  private logger: Logger,
38
44
  private fields: SelectedFieldsOrdered | undefined,
39
45
  private _isResponseInArrayMode: boolean,
40
- private customResultMapper: ((rows: unknown[][]) => T['execute']) | undefined,
46
+ private customResultMapper:
47
+ | ((rows: unknown[][]) => T['execute'])
48
+ | undefined,
41
49
  private rewriteArrays: boolean,
42
50
  private rejectStringArrayLiterals: boolean,
43
51
  private warnOnStringArrayLiteral?: (sql: string) => void
@@ -71,17 +79,10 @@ export class DuckDBPreparedQuery<
71
79
 
72
80
  this.logger.logQuery(rewrittenQuery, params);
73
81
 
74
- const {
75
- fields,
76
- joinsNotNullableMap,
77
- customResultMapper,
78
- } = this as typeof this & { joinsNotNullableMap?: Record<string, boolean> };
82
+ const { fields, joinsNotNullableMap, customResultMapper } =
83
+ this as typeof this & { joinsNotNullableMap?: Record<string, boolean> };
79
84
 
80
- const rows = await executeOnClient(
81
- this.client,
82
- rewrittenQuery,
83
- params
84
- );
85
+ const rows = await executeOnClient(this.client, rewrittenQuery, params);
85
86
 
86
87
  if (rows.length === 0 || !fields) {
87
88
  return rows as T['execute'];
@@ -115,7 +116,7 @@ export interface DuckDBSessionOptions {
115
116
 
116
117
  export class DuckDBSession<
117
118
  TFullSchema extends Record<string, unknown> = Record<string, never>,
118
- TSchema extends TablesRelationalConfig = Record<string, never>
119
+ TSchema extends TablesRelationalConfig = Record<string, never>,
119
120
  > extends PgSession<DuckDBQueryResultHKT, TFullSchema, TSchema> {
120
121
  static readonly [entityKind]: string = 'DuckDBSession';
121
122
 
@@ -199,11 +200,67 @@ export class DuckDBSession<
199
200
  []
200
201
  );
201
202
  };
203
+
204
+ executeBatches<T extends RowData = RowData>(
205
+ query: SQL,
206
+ options: ExecuteInBatchesOptions = {}
207
+ ): AsyncGenerator<GenericRowData<T>[], void, void> {
208
+ const builtQuery = this.dialect.sqlToQuery(query);
209
+ const params = prepareParams(builtQuery.params, {
210
+ rejectStringArrayLiterals: this.rejectStringArrayLiterals,
211
+ warnOnStringArrayLiteral: this.rejectStringArrayLiterals
212
+ ? undefined
213
+ : () => this.warnOnStringArrayLiteral(builtQuery.sql),
214
+ });
215
+ const rewrittenQuery = this.rewriteArrays
216
+ ? adaptArrayOperators(builtQuery.sql)
217
+ : builtQuery.sql;
218
+
219
+ if (this.rewriteArrays && rewrittenQuery !== builtQuery.sql) {
220
+ this.logger.logQuery(
221
+ `[duckdb] original query before array rewrite: ${builtQuery.sql}`,
222
+ params
223
+ );
224
+ }
225
+
226
+ this.logger.logQuery(rewrittenQuery, params);
227
+
228
+ return executeInBatches(
229
+ this.client,
230
+ rewrittenQuery,
231
+ params,
232
+ options
233
+ ) as AsyncGenerator<GenericRowData<T>[], void, void>;
234
+ }
235
+
236
+ async executeArrow(query: SQL): Promise<unknown> {
237
+ const builtQuery = this.dialect.sqlToQuery(query);
238
+ const params = prepareParams(builtQuery.params, {
239
+ rejectStringArrayLiterals: this.rejectStringArrayLiterals,
240
+ warnOnStringArrayLiteral: this.rejectStringArrayLiterals
241
+ ? undefined
242
+ : () => this.warnOnStringArrayLiteral(builtQuery.sql),
243
+ });
244
+ const rewrittenQuery = this.rewriteArrays
245
+ ? adaptArrayOperators(builtQuery.sql)
246
+ : builtQuery.sql;
247
+
248
+ if (this.rewriteArrays && rewrittenQuery !== builtQuery.sql) {
249
+ this.logger.logQuery(
250
+ `[duckdb] original query before array rewrite: ${builtQuery.sql}`,
251
+ params
252
+ );
253
+ }
254
+
255
+ this.logger.logQuery(rewrittenQuery, params);
256
+
257
+ return executeArrowOnClient(this.client, rewrittenQuery, params);
258
+ }
202
259
  }
203
260
 
204
261
  type PgTransactionInternals<
205
262
  TFullSchema extends Record<string, unknown> = Record<string, never>,
206
- TSchema extends TablesRelationalConfig = Record<string, never>
263
+ TSchema extends TablesRelationalConfig = Record<string, never>,
207
264
  > = {
208
265
  dialect: DuckDBDialect;
209
266
  session: DuckDBSession<TFullSchema, TSchema>;
@@ -211,13 +268,13 @@ type PgTransactionInternals<
211
268
 
212
269
  type DuckDBTransactionWithInternals<
213
270
  TFullSchema extends Record<string, unknown> = Record<string, never>,
214
- TSchema extends TablesRelationalConfig = Record<string, never>
271
+ TSchema extends TablesRelationalConfig = Record<string, never>,
215
272
  > = PgTransactionInternals<TFullSchema, TSchema> &
216
273
  DuckDBTransaction<TFullSchema, TSchema>;
217
274
 
218
275
  export class DuckDBTransaction<
219
276
  TFullSchema extends Record<string, unknown>,
220
- TSchema extends TablesRelationalConfig
277
+ TSchema extends TablesRelationalConfig,
221
278
  > extends PgTransaction<DuckDBQueryResultHKT, TFullSchema, TSchema> {
222
279
  static readonly [entityKind]: string = 'DuckDBTransaction';
223
280
 
@@ -246,6 +303,19 @@ export class DuckDBTransaction<
246
303
  );
247
304
  }
248
305
 
306
+ executeBatches<T extends RowData = RowData>(
307
+ query: SQL,
308
+ options: ExecuteInBatchesOptions = {}
309
+ ): AsyncGenerator<GenericRowData<T>[], void, void> {
310
+ type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
311
+ return (this as unknown as Tx).session.executeBatches<T>(query, options);
312
+ }
313
+
314
+ executeArrow(query: SQL): Promise<unknown> {
315
+ type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
316
+ return (this as unknown as Tx).session.executeArrow(query);
317
+ }
318
+
249
319
  override async transaction<T>(
250
320
  transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>
251
321
  ): Promise<T> {
@@ -268,7 +338,6 @@ export type GenericTableData<T = RowData> = T[];
268
338
  const arrayLiteralWarning =
269
339
  'Received a stringified Postgres-style array literal. Use duckDbList()/duckDbArray() or pass native arrays instead. You can also set rejectStringArrayLiterals=true to throw.';
270
340
 
271
-
272
341
  export interface DuckDBQueryResultHKT extends PgQueryResultHKT {
273
342
  type: GenericTableData<Assume<this['row'], RowData>>;
274
343
  }
@@ -85,10 +85,7 @@ export function adaptArrayOperators(query: string): string {
85
85
  let idx = rewritten.indexOf(token);
86
86
  while (idx !== -1) {
87
87
  const [leftStart, leftExpr] = walkLeft(rewritten, idx - 1);
88
- const [rightEnd, rightExpr] = walkRight(
89
- rewritten,
90
- idx + token.length
91
- );
88
+ const [rightEnd, rightExpr] = walkRight(rewritten, idx + token.length);
92
89
 
93
90
  const left = leftExpr.trim();
94
91
  const right = rightExpr.trim();
@@ -98,9 +95,7 @@ export function adaptArrayOperators(query: string): string {
98
95
  })`;
99
96
 
100
97
  rewritten =
101
- rewritten.slice(0, leftStart) +
102
- replacement +
103
- rewritten.slice(rightEnd);
98
+ rewritten.slice(0, leftStart) + replacement + rewritten.slice(rightEnd);
104
99
 
105
100
  idx = rewritten.indexOf(token, leftStart + replacement.length);
106
101
  }
@@ -136,7 +131,9 @@ export function queryAdapter(query: string): string {
136
131
  if (noTableProp) {
137
132
  const [, column, alias] = noTableProp;
138
133
  const asAlias = ` as '${column}'`;
139
- return alias ? trimmedField.replace(alias, asAlias) : `${trimmedField}${asAlias}`;
134
+ return alias
135
+ ? trimmedField.replace(alias, asAlias)
136
+ : `${trimmedField}${asAlias}`;
140
137
  }
141
138
 
142
139
  return trimmedField;
@@ -88,7 +88,10 @@ function normalizeTimestampString(
88
88
  return value;
89
89
  }
90
90
 
91
- function normalizeTimestamp(value: unknown, withTimezone: boolean): Date | unknown {
91
+ function normalizeTimestamp(
92
+ value: unknown,
93
+ withTimezone: boolean
94
+ ): Date | unknown {
92
95
  if (value instanceof Date) {
93
96
  return value;
94
97
  }
@@ -96,8 +99,7 @@ function normalizeTimestamp(value: unknown, withTimezone: boolean): Date | unkno
96
99
  const hasOffset =
97
100
  value.endsWith('Z') || /[+-]\d{2}:?\d{2}$/.test(value.trim());
98
101
  const spaced = value.replace(' ', 'T');
99
- const normalized =
100
- withTimezone || hasOffset ? spaced : `${spaced}+00`;
102
+ const normalized = withTimezone || hasOffset ? spaced : `${spaced}+00`;
101
103
  return new Date(normalized);
102
104
  }
103
105
  return value;
@@ -177,9 +179,7 @@ function mapDriverValue(
177
179
  if (normalized instanceof Date) {
178
180
  return normalized;
179
181
  }
180
- return decoder.mapFromDriverValue(
181
- toDecoderInput(decoder, normalized)
182
- );
182
+ return decoder.mapFromDriverValue(toDecoderInput(decoder, normalized));
183
183
  }
184
184
 
185
185
  if (is(decoder, PgDateString)) {
@@ -1,10 +1,4 @@
1
- import {
2
- Column,
3
- SQL,
4
- getTableName,
5
- is,
6
- sql,
7
- } from 'drizzle-orm';
1
+ import { Column, SQL, getTableName, is, sql } from 'drizzle-orm';
8
2
  import type { SelectedFields } from 'drizzle-orm/pg-core';
9
3
 
10
4
  function mapEntries(
@@ -38,8 +32,7 @@ function mapEntries(
38
32
  }
39
33
 
40
34
  if (is(value, SQL) || is(value, Column)) {
41
- const aliased =
42
- is(value, SQL) ? value : sql`${value}`.mapWith(value);
35
+ const aliased = is(value, SQL) ? value : sql`${value}`.mapWith(value);
43
36
  return [key, aliased.as(qualified)];
44
37
  }
45
38