drizzle-databend 0.1.8 → 0.1.10

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/dist/client.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Connection } from 'databend-driver';
2
+ import type { QueryTypingsValue } from 'drizzle-orm';
2
3
  export interface DatabendConnectionPool {
3
4
  acquire(): Promise<Connection>;
4
5
  release(connection: Connection): void | Promise<void>;
@@ -14,9 +15,12 @@ export declare function isPool(client: DatabendClientLike): client is DatabendCo
14
15
  /**
15
16
  * Convert Drizzle param array to a JSON value accepted by databend-driver's Params.
16
17
  * Databend's Params is serde_json::Value, so we pass an array of JSON-serializable values.
18
+ *
19
+ * The databend-driver does client-side parameter substitution with no string escaping,
20
+ * so we must pre-escape single quotes here (SQL standard '' escaping).
17
21
  */
18
- export declare function prepareParams(params: unknown[]): unknown[];
19
- export declare function executeOnClient(client: DatabendClientLike, query: string, params: unknown[]): Promise<RowData[]>;
20
- export declare function executeArraysOnClient(client: DatabendClientLike, query: string, params: unknown[]): Promise<ExecuteArraysResult>;
21
- export declare function execOnClient(client: DatabendClientLike, query: string, params: unknown[]): Promise<number>;
22
+ export declare function prepareParams(params: unknown[], typings?: QueryTypingsValue[]): unknown[];
23
+ export declare function executeOnClient(client: DatabendClientLike, query: string, params: unknown[], typings?: QueryTypingsValue[]): Promise<RowData[]>;
24
+ export declare function executeArraysOnClient(client: DatabendClientLike, query: string, params: unknown[], typings?: QueryTypingsValue[]): Promise<ExecuteArraysResult>;
25
+ export declare function execOnClient(client: DatabendClientLike, query: string, params: unknown[], typings?: QueryTypingsValue[]): Promise<number>;
22
26
  export declare function closeClientConnection(connection: Connection): Promise<void>;
package/dist/index.mjs CHANGED
@@ -182,17 +182,47 @@ import { TransactionRollbackError } from "drizzle-orm/errors";
182
182
  function isPool(client) {
183
183
  return typeof client.acquire === "function";
184
184
  }
185
- function prepareParams(params) {
186
- return params.map((param) => {
185
+ function prepareParams(params, typings) {
186
+ return params.map((param, i) => {
187
187
  if (param === undefined)
188
188
  return null;
189
189
  if (param instanceof Date)
190
- return param.toISOString();
190
+ return param.toISOString().replace(/'/g, "''");
191
191
  if (typeof param === "bigint")
192
192
  return param.toString();
193
+ if (typeof param === "string") {
194
+ const typing = typings?.[i];
195
+ if (typing === "decimal" && /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(param)) {
196
+ return Number(param);
197
+ }
198
+ return param.replace(/'/g, "''");
199
+ }
200
+ if (typeof param === "object" && param !== null) {
201
+ return JSON.stringify(param).replace(/'/g, "''");
202
+ }
193
203
  return param;
194
204
  });
195
205
  }
206
+ function isTransientError(error) {
207
+ if (!(error instanceof Error))
208
+ return false;
209
+ const msg = error.message?.toLowerCase() ?? "";
210
+ return msg.includes("connection closed") || msg.includes("econnreset") || msg.includes("epipe") || msg.includes("socket hang up") || msg.includes("connection refused");
211
+ }
212
+ async function withRetry(fn, maxRetries = 2) {
213
+ let lastError;
214
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
215
+ try {
216
+ return await fn();
217
+ } catch (error) {
218
+ lastError = error;
219
+ if (!isTransientError(error) || attempt === maxRetries)
220
+ throw error;
221
+ await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempt)));
222
+ }
223
+ }
224
+ throw lastError;
225
+ }
196
226
  function deduplicateColumns(columns) {
197
227
  const counts = new Map;
198
228
  let hasDuplicates = false;
@@ -214,16 +244,18 @@ function deduplicateColumns(columns) {
214
244
  return count === 0 ? column : `${column}_${count}`;
215
245
  });
216
246
  }
217
- async function executeOnClient(client, query, params) {
247
+ async function executeOnClient(client, query, params, typings) {
218
248
  if (isPool(client)) {
219
- const connection = await client.acquire();
220
- try {
221
- return await executeOnClient(connection, query, params);
222
- } finally {
223
- await client.release(connection);
224
- }
249
+ return withRetry(async () => {
250
+ const connection = await client.acquire();
251
+ try {
252
+ return await executeOnClient(connection, query, params, typings);
253
+ } finally {
254
+ await client.release(connection);
255
+ }
256
+ });
225
257
  }
226
- const prepared = prepareParams(params);
258
+ const prepared = prepareParams(params, typings);
227
259
  const paramValue = prepared.length > 0 ? prepared : undefined;
228
260
  const rows = await client.queryAll(query, paramValue);
229
261
  if (!rows || rows.length === 0) {
@@ -231,16 +263,18 @@ async function executeOnClient(client, query, params) {
231
263
  }
232
264
  return rows.map((r) => r.data());
233
265
  }
234
- async function executeArraysOnClient(client, query, params) {
266
+ async function executeArraysOnClient(client, query, params, typings) {
235
267
  if (isPool(client)) {
236
- const connection = await client.acquire();
237
- try {
238
- return await executeArraysOnClient(connection, query, params);
239
- } finally {
240
- await client.release(connection);
241
- }
268
+ return withRetry(async () => {
269
+ const connection = await client.acquire();
270
+ try {
271
+ return await executeArraysOnClient(connection, query, params, typings);
272
+ } finally {
273
+ await client.release(connection);
274
+ }
275
+ });
242
276
  }
243
- const prepared = prepareParams(params);
277
+ const prepared = prepareParams(params, typings);
244
278
  const paramValue = prepared.length > 0 ? prepared : undefined;
245
279
  const iter = await client.queryIter(query, paramValue);
246
280
  const schema = iter.schema();
@@ -257,16 +291,18 @@ async function executeArraysOnClient(client, query, params) {
257
291
  }
258
292
  return { columns, rows };
259
293
  }
260
- async function execOnClient(client, query, params) {
294
+ async function execOnClient(client, query, params, typings) {
261
295
  if (isPool(client)) {
262
- const connection = await client.acquire();
263
- try {
264
- return await execOnClient(connection, query, params);
265
- } finally {
266
- await client.release(connection);
267
- }
296
+ return withRetry(async () => {
297
+ const connection = await client.acquire();
298
+ try {
299
+ return await execOnClient(connection, query, params, typings);
300
+ } finally {
301
+ await client.release(connection);
302
+ }
303
+ });
268
304
  }
269
- const prepared = prepareParams(params);
305
+ const prepared = prepareParams(params, typings);
270
306
  const paramValue = prepared.length > 0 ? prepared : undefined;
271
307
  return await client.exec(query, paramValue);
272
308
  }
@@ -285,8 +321,9 @@ class DatabendPreparedQuery extends PgPreparedQuery {
285
321
  fields;
286
322
  _isResponseInArrayMode;
287
323
  customResultMapper;
324
+ typings;
288
325
  static [entityKind] = "DatabendPreparedQuery";
289
- constructor(client, queryString, params, logger, fields, _isResponseInArrayMode, customResultMapper) {
326
+ constructor(client, queryString, params, logger, fields, _isResponseInArrayMode, customResultMapper, typings) {
290
327
  super({ sql: queryString, params });
291
328
  this.client = client;
292
329
  this.queryString = queryString;
@@ -295,19 +332,20 @@ class DatabendPreparedQuery extends PgPreparedQuery {
295
332
  this.fields = fields;
296
333
  this._isResponseInArrayMode = _isResponseInArrayMode;
297
334
  this.customResultMapper = customResultMapper;
335
+ this.typings = typings;
298
336
  }
299
337
  async execute(placeholderValues = {}) {
300
- const params = prepareParams(fillPlaceholders(this.params, placeholderValues));
338
+ const params = fillPlaceholders(this.params, placeholderValues);
301
339
  this.logger.logQuery(this.queryString, params);
302
- const { fields, joinsNotNullableMap, customResultMapper } = this;
340
+ const { fields, joinsNotNullableMap, customResultMapper, typings } = this;
303
341
  if (fields) {
304
- const { rows: rows2 } = await executeArraysOnClient(this.client, this.queryString, params);
342
+ const { rows: rows2 } = await executeArraysOnClient(this.client, this.queryString, params, typings);
305
343
  if (rows2.length === 0) {
306
344
  return [];
307
345
  }
308
346
  return customResultMapper ? customResultMapper(rows2) : rows2.map((row) => mapResultRow(fields, row, joinsNotNullableMap));
309
347
  }
310
- const rows = await executeOnClient(this.client, this.queryString, params);
348
+ const rows = await executeOnClient(this.client, this.queryString, params, typings);
311
349
  return rows;
312
350
  }
313
351
  all(placeholderValues = {}) {
@@ -335,7 +373,7 @@ class DatabendSession extends PgSession {
335
373
  this.logger = options.logger ?? new NoopLogger;
336
374
  }
337
375
  prepareQuery(query, fields, name, isResponseInArrayMode, customResultMapper) {
338
- return new DatabendPreparedQuery(this.client, query.sql, query.params, this.logger, fields, isResponseInArrayMode, customResultMapper);
376
+ return new DatabendPreparedQuery(this.client, query.sql, query.params, this.logger, fields, isResponseInArrayMode, customResultMapper, query.typings);
339
377
  }
340
378
  async transaction(transaction, config) {
341
379
  let pinnedConnection;
@@ -426,10 +464,16 @@ class DatabendTransaction extends PgTransaction {
426
464
  // src/dialect.ts
427
465
  import { entityKind as entityKind2, is as is2 } from "drizzle-orm/entity";
428
466
  import {
467
+ PgBigInt53,
468
+ PgBigInt64,
429
469
  PgDate as PgDate2,
430
470
  PgDateString as PgDateString2,
431
471
  PgDialect,
472
+ PgDoublePrecision,
473
+ PgInteger,
432
474
  PgNumeric,
475
+ PgReal,
476
+ PgSmallInt,
433
477
  PgTime as PgTime2,
434
478
  PgTimestamp as PgTimestamp2,
435
479
  PgTimestampString as PgTimestampString2,
@@ -475,7 +519,7 @@ class DatabendDialect extends PgDialect {
475
519
  });
476
520
  }
477
521
  prepareTyping(encoder) {
478
- if (is2(encoder, PgNumeric)) {
522
+ if (is2(encoder, PgNumeric) || is2(encoder, PgInteger) || is2(encoder, PgSmallInt) || is2(encoder, PgReal) || is2(encoder, PgDoublePrecision) || is2(encoder, PgBigInt53) || is2(encoder, PgBigInt64)) {
479
523
  return "decimal";
480
524
  } else if (is2(encoder, PgTime2)) {
481
525
  return "time";
package/dist/session.d.ts CHANGED
@@ -5,7 +5,7 @@ import type { SelectedFieldsOrdered } from 'drizzle-orm/pg-core/query-builders/s
5
5
  import type { PgTransactionConfig, PreparedQueryConfig, PgQueryResultHKT } from 'drizzle-orm/pg-core/session';
6
6
  import { PgPreparedQuery, PgSession } from 'drizzle-orm/pg-core/session';
7
7
  import type { RelationalSchemaConfig, TablesRelationalConfig } from 'drizzle-orm/relations';
8
- import { type Query, SQL } from 'drizzle-orm/sql/sql';
8
+ import { type Query, type QueryTypingsValue, SQL } from 'drizzle-orm/sql/sql';
9
9
  import type { Assume } from 'drizzle-orm/utils';
10
10
  import type { DatabendDialect } from './dialect.ts';
11
11
  import type { DatabendClientLike, RowData } from './client.ts';
@@ -18,8 +18,9 @@ export declare class DatabendPreparedQuery<T extends PreparedQueryConfig> extend
18
18
  private fields;
19
19
  private _isResponseInArrayMode;
20
20
  private customResultMapper;
21
+ private typings?;
21
22
  static readonly [entityKind]: string;
22
- constructor(client: DatabendClientLike, queryString: string, params: unknown[], logger: Logger, fields: SelectedFieldsOrdered | undefined, _isResponseInArrayMode: boolean, customResultMapper: ((rows: unknown[][]) => T['execute']) | undefined);
23
+ constructor(client: DatabendClientLike, queryString: string, params: unknown[], logger: Logger, fields: SelectedFieldsOrdered | undefined, _isResponseInArrayMode: boolean, customResultMapper: ((rows: unknown[][]) => T['execute']) | undefined, typings?: QueryTypingsValue[] | undefined);
23
24
  execute(placeholderValues?: Record<string, unknown> | undefined): Promise<T['execute']>;
24
25
  all(placeholderValues?: Record<string, unknown> | undefined): Promise<T['all']>;
25
26
  isResponseInArrayMode(): boolean;
package/package.json CHANGED
@@ -3,13 +3,16 @@
3
3
  "module": "./dist/index.mjs",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.ts",
6
- "version": "0.1.8",
6
+ "version": "0.1.10",
7
7
  "description": "A drizzle ORM driver for use with Databend. Based on drizzle's Postgres driver surface.",
8
8
  "type": "module",
9
9
  "scripts": {
10
10
  "build": "bun build --target=node ./src/index.ts --outfile=./dist/index.mjs --packages=external && bun run build:declarations",
11
11
  "build:declarations": "tsc --emitDeclarationOnly --project tsconfig.types.json",
12
- "test": "vitest"
12
+ "test": "vitest",
13
+ "db:start": "docker start databend 2>/dev/null || docker run -d --name databend -p 8000:8000 datafuselabs/databend",
14
+ "db:stop": "docker stop databend",
15
+ "db:restart": "docker restart databend"
13
16
  },
14
17
  "peerDependencies": {
15
18
  "databend-driver": ">=0.33.0",
package/src/client.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Connection } from 'databend-driver';
2
+ import type { QueryTypingsValue } from 'drizzle-orm';
2
3
 
3
4
  export interface DatabendConnectionPool {
4
5
  acquire(): Promise<Connection>;
@@ -20,16 +21,50 @@ export function isPool(
20
21
  /**
21
22
  * Convert Drizzle param array to a JSON value accepted by databend-driver's Params.
22
23
  * Databend's Params is serde_json::Value, so we pass an array of JSON-serializable values.
24
+ *
25
+ * The databend-driver does client-side parameter substitution with no string escaping,
26
+ * so we must pre-escape single quotes here (SQL standard '' escaping).
23
27
  */
24
- export function prepareParams(params: unknown[]): unknown[] {
25
- return params.map((param) => {
28
+ export function prepareParams(params: unknown[], typings?: QueryTypingsValue[]): unknown[] {
29
+ return params.map((param, i) => {
26
30
  if (param === undefined) return null;
27
- if (param instanceof Date) return param.toISOString();
31
+ if (param instanceof Date) return param.toISOString().replace(/'/g, "''");
28
32
  if (typeof param === 'bigint') return param.toString();
33
+ if (typeof param === 'string') {
34
+ const typing = typings?.[i];
35
+ if (typing === 'decimal' && /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(param)) {
36
+ return Number(param);
37
+ }
38
+ return param.replace(/'/g, "''");
39
+ }
40
+ if (typeof param === 'object' && param !== null) {
41
+ return JSON.stringify(param).replace(/'/g, "''");
42
+ }
29
43
  return param;
30
44
  });
31
45
  }
32
46
 
47
+ function isTransientError(error: unknown): boolean {
48
+ if (!(error instanceof Error)) return false;
49
+ const msg = error.message?.toLowerCase() ?? '';
50
+ return msg.includes('connection closed') || msg.includes('econnreset') ||
51
+ msg.includes('epipe') || msg.includes('socket hang up') ||
52
+ msg.includes('connection refused');
53
+ }
54
+
55
+ async function withRetry<T>(fn: () => Promise<T>, maxRetries = 2): Promise<T> {
56
+ let lastError: unknown;
57
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
58
+ try { return await fn(); }
59
+ catch (error) {
60
+ lastError = error;
61
+ if (!isTransientError(error) || attempt === maxRetries) throw error;
62
+ await new Promise(r => setTimeout(r, 100 * Math.pow(2, attempt)));
63
+ }
64
+ }
65
+ throw lastError;
66
+ }
67
+
33
68
  function deduplicateColumns(columns: string[]): string[] {
34
69
  const counts = new Map<string, number>();
35
70
  let hasDuplicates = false;
@@ -58,18 +93,21 @@ function deduplicateColumns(columns: string[]): string[] {
58
93
  export async function executeOnClient(
59
94
  client: DatabendClientLike,
60
95
  query: string,
61
- params: unknown[]
96
+ params: unknown[],
97
+ typings?: QueryTypingsValue[]
62
98
  ): Promise<RowData[]> {
63
99
  if (isPool(client)) {
64
- const connection = await client.acquire();
65
- try {
66
- return await executeOnClient(connection, query, params);
67
- } finally {
68
- await client.release(connection);
69
- }
100
+ return withRetry(async () => {
101
+ const connection = await client.acquire();
102
+ try {
103
+ return await executeOnClient(connection, query, params, typings);
104
+ } finally {
105
+ await client.release(connection);
106
+ }
107
+ });
70
108
  }
71
109
 
72
- const prepared = prepareParams(params);
110
+ const prepared = prepareParams(params, typings);
73
111
  const paramValue = prepared.length > 0 ? prepared : undefined;
74
112
  const rows = await client.queryAll(query, paramValue);
75
113
 
@@ -83,18 +121,21 @@ export async function executeOnClient(
83
121
  export async function executeArraysOnClient(
84
122
  client: DatabendClientLike,
85
123
  query: string,
86
- params: unknown[]
124
+ params: unknown[],
125
+ typings?: QueryTypingsValue[]
87
126
  ): Promise<ExecuteArraysResult> {
88
127
  if (isPool(client)) {
89
- const connection = await client.acquire();
90
- try {
91
- return await executeArraysOnClient(connection, query, params);
92
- } finally {
93
- await client.release(connection);
94
- }
128
+ return withRetry(async () => {
129
+ const connection = await client.acquire();
130
+ try {
131
+ return await executeArraysOnClient(connection, query, params, typings);
132
+ } finally {
133
+ await client.release(connection);
134
+ }
135
+ });
95
136
  }
96
137
 
97
- const prepared = prepareParams(params);
138
+ const prepared = prepareParams(params, typings);
98
139
  const paramValue = prepared.length > 0 ? prepared : undefined;
99
140
  const iter = await client.queryIter(query, paramValue);
100
141
  const schema = iter.schema();
@@ -115,18 +156,21 @@ export async function executeArraysOnClient(
115
156
  export async function execOnClient(
116
157
  client: DatabendClientLike,
117
158
  query: string,
118
- params: unknown[]
159
+ params: unknown[],
160
+ typings?: QueryTypingsValue[]
119
161
  ): Promise<number> {
120
162
  if (isPool(client)) {
121
- const connection = await client.acquire();
122
- try {
123
- return await execOnClient(connection, query, params);
124
- } finally {
125
- await client.release(connection);
126
- }
163
+ return withRetry(async () => {
164
+ const connection = await client.acquire();
165
+ try {
166
+ return await execOnClient(connection, query, params, typings);
167
+ } finally {
168
+ await client.release(connection);
169
+ }
170
+ });
127
171
  }
128
172
 
129
- const prepared = prepareParams(params);
173
+ const prepared = prepareParams(params, typings);
130
174
  const paramValue = prepared.length > 0 ? prepared : undefined;
131
175
  return await client.exec(query, paramValue);
132
176
  }
package/src/dialect.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import { entityKind, is } from 'drizzle-orm/entity';
2
2
  import type { MigrationConfig, MigrationMeta } from 'drizzle-orm/migrator';
3
3
  import {
4
+ PgBigInt53,
5
+ PgBigInt64,
4
6
  PgDate,
5
7
  PgDateString,
6
8
  PgDialect,
9
+ PgDoublePrecision,
10
+ PgInteger,
7
11
  PgNumeric,
12
+ PgReal,
8
13
  PgSession,
14
+ PgSmallInt,
9
15
  PgTime,
10
16
  PgTimestamp,
11
17
  PgTimestampString,
@@ -92,7 +98,11 @@ export class DatabendDialect extends PgDialect {
92
98
  override prepareTyping(
93
99
  encoder: DriverValueEncoder<unknown, unknown>
94
100
  ): QueryTypingsValue {
95
- if (is(encoder, PgNumeric)) {
101
+ if (
102
+ is(encoder, PgNumeric) || is(encoder, PgInteger) || is(encoder, PgSmallInt)
103
+ || is(encoder, PgReal) || is(encoder, PgDoublePrecision)
104
+ || is(encoder, PgBigInt53) || is(encoder, PgBigInt64)
105
+ ) {
96
106
  return 'decimal';
97
107
  } else if (is(encoder, PgTime)) {
98
108
  return 'time';
package/src/session.ts CHANGED
@@ -13,7 +13,7 @@ import type {
13
13
  RelationalSchemaConfig,
14
14
  TablesRelationalConfig,
15
15
  } from 'drizzle-orm/relations';
16
- import { fillPlaceholders, type Query, SQL, sql } from 'drizzle-orm/sql/sql';
16
+ import { fillPlaceholders, type Query, type QueryTypingsValue, SQL, sql } from 'drizzle-orm/sql/sql';
17
17
  import type { Assume } from 'drizzle-orm/utils';
18
18
  import { mapResultRow } from './sql/result-mapper.ts';
19
19
  import { TransactionRollbackError } from 'drizzle-orm/errors';
@@ -26,7 +26,6 @@ import type {
26
26
  import {
27
27
  executeArraysOnClient,
28
28
  executeOnClient,
29
- prepareParams,
30
29
  isPool,
31
30
  } from './client.ts';
32
31
  import type { Connection } from 'databend-driver';
@@ -47,7 +46,8 @@ export class DatabendPreparedQuery<
47
46
  private _isResponseInArrayMode: boolean,
48
47
  private customResultMapper:
49
48
  | ((rows: unknown[][]) => T['execute'])
50
- | undefined
49
+ | undefined,
50
+ private typings?: QueryTypingsValue[]
51
51
  ) {
52
52
  super({ sql: queryString, params });
53
53
  }
@@ -55,19 +55,18 @@ export class DatabendPreparedQuery<
55
55
  async execute(
56
56
  placeholderValues: Record<string, unknown> | undefined = {}
57
57
  ): Promise<T['execute']> {
58
- const params = prepareParams(
59
- fillPlaceholders(this.params, placeholderValues)
60
- );
58
+ const params = fillPlaceholders(this.params, placeholderValues);
61
59
  this.logger.logQuery(this.queryString, params);
62
60
 
63
- const { fields, joinsNotNullableMap, customResultMapper } =
61
+ const { fields, joinsNotNullableMap, customResultMapper, typings } =
64
62
  this as typeof this & { joinsNotNullableMap?: Record<string, boolean> };
65
63
 
66
64
  if (fields) {
67
65
  const { rows } = await executeArraysOnClient(
68
66
  this.client,
69
67
  this.queryString,
70
- params
68
+ params,
69
+ typings
71
70
  );
72
71
 
73
72
  if (rows.length === 0) {
@@ -81,7 +80,7 @@ export class DatabendPreparedQuery<
81
80
  );
82
81
  }
83
82
 
84
- const rows = await executeOnClient(this.client, this.queryString, params);
83
+ const rows = await executeOnClient(this.client, this.queryString, params, typings);
85
84
 
86
85
  return rows as T['execute'];
87
86
  }
@@ -137,7 +136,8 @@ export class DatabendSession<
137
136
  this.logger,
138
137
  fields,
139
138
  isResponseInArrayMode,
140
- customResultMapper
139
+ customResultMapper,
140
+ (query as any).typings
141
141
  );
142
142
  }
143
143