drizzle-databend 0.1.9 → 0.1.11

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/session.d.ts CHANGED
@@ -2,13 +2,13 @@ import { entityKind } from 'drizzle-orm/entity';
2
2
  import type { Logger } from 'drizzle-orm/logger';
3
3
  import { PgTransaction } from 'drizzle-orm/pg-core';
4
4
  import type { SelectedFieldsOrdered } from 'drizzle-orm/pg-core/query-builders/select.types';
5
- import type { PgTransactionConfig, PreparedQueryConfig, PgQueryResultHKT } from 'drizzle-orm/pg-core/session';
5
+ import type { PgQueryResultHKT, PgTransactionConfig, PreparedQueryConfig } 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, type SQL } from 'drizzle-orm/sql/sql';
9
9
  import type { Assume } from 'drizzle-orm/utils';
10
- import type { DatabendDialect } from './dialect.ts';
11
10
  import type { DatabendClientLike, RowData } from './client.ts';
11
+ import type { DatabendDialect } from './dialect.ts';
12
12
  export type { DatabendClientLike, RowData } from './client.ts';
13
13
  export declare class DatabendPreparedQuery<T extends PreparedQueryConfig> extends PgPreparedQuery<T> {
14
14
  private client;
@@ -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,18 @@
3
3
  "module": "./dist/index.mjs",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.ts",
6
- "version": "0.1.9",
6
+ "version": "0.1.11",
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
- "build": "bun build --target=node ./src/index.ts --outfile=./dist/index.mjs --packages=external && bun run build:declarations",
10
+ "build": "esbuild src/index.ts --bundle --format=esm --platform=node --packages=external --outfile=dist/index.mjs && npm run build:declarations",
11
11
  "build:declarations": "tsc --emitDeclarationOnly --project tsconfig.types.json",
12
- "test": "vitest"
12
+ "test": "vitest run",
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",
16
+ "lint": "biome check src/ test/",
17
+ "typecheck": "tsc --noEmit"
13
18
  },
14
19
  "peerDependencies": {
15
20
  "databend-driver": ">=0.33.0",
@@ -21,9 +26,11 @@
21
26
  }
22
27
  },
23
28
  "devDependencies": {
29
+ "@biomejs/biome": "^2.4.7",
30
+ "@types/node": "^25.5.0",
24
31
  "databend-driver": "^0.33.6",
25
- "@types/bun": "^1.2.5",
26
32
  "drizzle-orm": "0.40.0",
33
+ "esbuild": "^0.25.0",
27
34
  "typescript": "^5.8.2",
28
35
  "vitest": "^1.6.0"
29
36
  },
@@ -39,7 +46,6 @@
39
46
  "engines": {
40
47
  "node": ">=18.17"
41
48
  },
42
- "packageManager": "bun@1.3.6",
43
49
  "keywords": [
44
50
  "drizzle",
45
51
  "databend"
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 * 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/columns.ts CHANGED
@@ -1,5 +1,5 @@
1
+
1
2
  import { customType } from 'drizzle-orm/pg-core';
2
- import type { SQL } from 'drizzle-orm';
3
3
 
4
4
  /**
5
5
  * Databend VARIANT column type.
package/src/dialect.ts CHANGED
@@ -1,21 +1,27 @@
1
+ import {
2
+ type DriverValueEncoder,
3
+ type QueryTypingsValue,
4
+ sql,
5
+ } from 'drizzle-orm';
1
6
  import { entityKind, is } from 'drizzle-orm/entity';
2
7
  import type { MigrationConfig, MigrationMeta } from 'drizzle-orm/migrator';
3
8
  import {
9
+ PgBigInt53,
10
+ PgBigInt64,
4
11
  PgDate,
5
12
  PgDateString,
6
13
  PgDialect,
14
+ PgDoublePrecision,
15
+ PgInteger,
7
16
  PgNumeric,
8
- PgSession,
17
+ PgReal,
18
+ type PgSession,
19
+ PgSmallInt,
9
20
  PgTime,
10
21
  PgTimestamp,
11
22
  PgTimestampString,
12
23
  PgUUID,
13
24
  } from 'drizzle-orm/pg-core';
14
- import {
15
- sql,
16
- type DriverValueEncoder,
17
- type QueryTypingsValue,
18
- } from 'drizzle-orm';
19
25
 
20
26
  export class DatabendDialect extends PgDialect {
21
27
  static readonly [entityKind]: string = 'DatabendPgDialect';
@@ -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/driver.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Connection } from 'databend-driver';
1
2
  import { Client } from 'databend-driver';
2
3
  import { entityKind } from 'drizzle-orm/entity';
3
4
  import type { Logger } from 'drizzle-orm/logger';
@@ -5,25 +6,24 @@ import { DefaultLogger } from 'drizzle-orm/logger';
5
6
  import { PgDatabase } from 'drizzle-orm/pg-core/db';
6
7
  import {
7
8
  createTableRelationsHelpers,
8
- extractTablesRelationalConfig,
9
9
  type ExtractTablesWithRelations,
10
+ extractTablesRelationalConfig,
10
11
  type RelationalSchemaConfig,
11
12
  type TablesRelationalConfig,
12
13
  } from 'drizzle-orm/relations';
13
- import { type DrizzleConfig } from 'drizzle-orm/utils';
14
+ import type { DrizzleConfig } from 'drizzle-orm/utils';
15
+ import { closeClientConnection, isPool } from './client.ts';
16
+ import { DatabendDialect } from './dialect.ts';
17
+ import {
18
+ createDatabendConnectionPool,
19
+ type DatabendPoolConfig,
20
+ } from './pool.ts';
14
21
  import type {
15
22
  DatabendClientLike,
16
23
  DatabendQueryResultHKT,
17
24
  DatabendTransaction,
18
25
  } from './session.ts';
19
26
  import { DatabendSession } from './session.ts';
20
- import { DatabendDialect } from './dialect.ts';
21
- import { isPool, closeClientConnection } from './client.ts';
22
- import {
23
- createDatabendConnectionPool,
24
- type DatabendPoolConfig,
25
- } from './pool.ts';
26
- import type { Connection } from 'databend-driver';
27
27
 
28
28
  export interface DatabendDriverOptions {
29
29
  logger?: Logger;
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
- export * from './driver.ts';
2
- export * from './session.ts';
1
+ export * from './client.ts';
3
2
  export * from './columns.ts';
3
+ export * from './driver.ts';
4
4
  export * from './migrator.ts';
5
- export * from './client.ts';
6
5
  export * from './pool.ts';
6
+ export * from './session.ts';
package/src/migrator.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { MigrationConfig } from 'drizzle-orm/migrator';
2
2
  import { readMigrationFiles } from 'drizzle-orm/migrator';
3
- import type { DatabendDatabase } from './driver.ts';
4
3
  import type { PgSession } from 'drizzle-orm/pg-core/session';
4
+ import type { DatabendDatabase } from './driver.ts';
5
5
 
6
6
  export type DatabendMigrationConfig = MigrationConfig | string;
7
7
 
package/src/pool.ts CHANGED
@@ -215,7 +215,9 @@ export function createDatabendConnectionPool(
215
215
  toClose.map((item) => closeClientConnection(item.connection))
216
216
  );
217
217
  total = Math.max(0, total - toClose.length);
218
- toClose.forEach((item) => metadata.delete(item.connection));
218
+ for (const item of toClose) {
219
+ metadata.delete(item.connection);
220
+ }
219
221
 
220
222
  const maxWait = 5000;
221
223
  const start = Date.now();
package/src/session.ts CHANGED
@@ -1,23 +1,22 @@
1
+ import type { Connection } from 'databend-driver';
1
2
  import { entityKind } from 'drizzle-orm/entity';
3
+ import { TransactionRollbackError } from 'drizzle-orm/errors';
2
4
  import type { Logger } from 'drizzle-orm/logger';
3
5
  import { NoopLogger } from 'drizzle-orm/logger';
4
6
  import { PgTransaction } from 'drizzle-orm/pg-core';
5
7
  import type { SelectedFieldsOrdered } from 'drizzle-orm/pg-core/query-builders/select.types';
6
8
  import type {
9
+ PgQueryResultHKT,
7
10
  PgTransactionConfig,
8
11
  PreparedQueryConfig,
9
- PgQueryResultHKT,
10
12
  } from 'drizzle-orm/pg-core/session';
11
13
  import { PgPreparedQuery, PgSession } from 'drizzle-orm/pg-core/session';
12
14
  import type {
13
15
  RelationalSchemaConfig,
14
16
  TablesRelationalConfig,
15
17
  } from 'drizzle-orm/relations';
16
- import { fillPlaceholders, type Query, SQL, sql } from 'drizzle-orm/sql/sql';
18
+ import { fillPlaceholders, type Query, type QueryTypingsValue, type SQL, sql } from 'drizzle-orm/sql/sql';
17
19
  import type { Assume } from 'drizzle-orm/utils';
18
- import { mapResultRow } from './sql/result-mapper.ts';
19
- import { TransactionRollbackError } from 'drizzle-orm/errors';
20
- import type { DatabendDialect } from './dialect.ts';
21
20
  import type {
22
21
  DatabendClientLike,
23
22
  DatabendConnectionPool,
@@ -26,10 +25,10 @@ import type {
26
25
  import {
27
26
  executeArraysOnClient,
28
27
  executeOnClient,
29
- prepareParams,
30
28
  isPool,
31
29
  } from './client.ts';
32
- import type { Connection } from 'databend-driver';
30
+ import type { DatabendDialect } from './dialect.ts';
31
+ import { mapResultRow } from './sql/result-mapper.ts';
33
32
 
34
33
  export type { DatabendClientLike, RowData } from './client.ts';
35
34
 
@@ -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
 
@@ -1,11 +1,11 @@
1
1
  import {
2
+ type AnyColumn,
2
3
  Column,
3
- SQL,
4
+ type DriverValueDecoder,
4
5
  getTableName,
5
6
  is,
6
- type AnyColumn,
7
- type DriverValueDecoder,
8
7
  type SelectedFieldsOrdered,
8
+ SQL,
9
9
  } from 'drizzle-orm';
10
10
  import {
11
11
  PgCustomColumn,
@@ -214,7 +214,6 @@ export function mapResultRow<TResult>(
214
214
  if (nullifyMap[objectName] && nullifyMap[objectName] !== tableName) {
215
215
  nullifyMap[objectName] = false;
216
216
  }
217
- continue;
218
217
  }
219
218
  }
220
219
  return acc;
@@ -1,4 +1,4 @@
1
- import { Column, SQL, getTableName, is, sql } from 'drizzle-orm';
1
+ import { Column, getTableName, is, SQL, sql } from 'drizzle-orm';
2
2
  import type { SelectedFields } from 'drizzle-orm/pg-core';
3
3
 
4
4
  function mapEntries(