drizzle-databend 0.1.13 → 0.1.15

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 CHANGED
@@ -1,23 +1,33 @@
1
1
  # drizzle-databend
2
2
 
3
- A [Drizzle ORM](https://orm.drizzle.team/) driver for [Databend](https://databend.com/). Built on Drizzle's Postgres driver surface (`pg-core`) since Databend supports `$1` positional parameters and double-quote identifier quoting.
3
+ > This driver was created to power [drizzle-cube](https://try.drizzle-cube.dev) (an embeddable semantic layer built on Drizzle) and [drizby](https://github.com/cliftonc/drizby) (an open source BI platform built on drizzle-cube). It enables both projects to query Databend natively via Drizzle ORM.
4
+
5
+ A [Drizzle ORM](https://orm.drizzle.team/) driver for [Databend](https://databend.com/). Built on a standalone `databend-core` dialect module with `?` positional parameters and double-quote identifier quoting.
4
6
 
5
7
  Uses [`databend-driver`](https://github.com/databendlabs/bendsql/tree/main/bindings/nodejs) (NAPI-RS bindings) as the underlying client.
6
8
 
9
+ ### Parameter binding
10
+
11
+ Queries are sent with `?` placeholders and a separate parameter array. On Databend
12
+ server `> 1.2.900` with `databend-driver >= 0.34.0`, parameters are bound **server-side**
13
+ via the `/v1/query` `params` field, so values are never interpolated into the SQL text.
14
+ Against older servers the driver transparently falls back to client-side interpolation
15
+ using its own SQL-standard escaping. Either way the driver owns escaping; this adapter no
16
+ longer pre-escapes values. `databend-driver >= 0.34.0` is required.
17
+
7
18
  ## Install
8
19
 
9
20
  ```sh
10
- bun add drizzle-databend drizzle-orm databend-driver
21
+ npm install drizzle-databend drizzle-orm databend-driver
11
22
  ```
12
23
 
13
24
  ## Quick start
14
25
 
15
26
  ```ts
16
- import { drizzle } from 'drizzle-databend';
17
- import { pgTable, integer, varchar, text } from 'drizzle-orm/pg-core';
27
+ import { drizzle, databendTable, integer, varchar, text } from 'drizzle-databend';
18
28
  import { eq } from 'drizzle-orm';
19
29
 
20
- const users = pgTable('users', {
30
+ const users = databendTable('users', {
21
31
  id: integer('id').notNull(),
22
32
  name: varchar('name', { length: 256 }).notNull(),
23
33
  email: text('email'),
@@ -35,9 +45,18 @@ const rows = await db.select().from(users).where(eq(users.name, 'Alice'));
35
45
  ## Databend-specific column types
36
46
 
37
47
  ```ts
38
- import { databendVariant, databendArray, databendTuple, databendMap, databendTimestamp, databendDate } from 'drizzle-databend';
39
-
40
- const events = pgTable('events', {
48
+ import {
49
+ databendTable,
50
+ databendVariant,
51
+ databendArray,
52
+ databendTuple,
53
+ databendMap,
54
+ databendTimestamp,
55
+ databendDate,
56
+ integer,
57
+ } from 'drizzle-databend';
58
+
59
+ const events = databendTable('events', {
41
60
  id: integer('id').notNull(),
42
61
  payload: databendVariant('payload'), // VARIANT (semi-structured JSON)
43
62
  tags: databendArray('tags', 'VARCHAR'), // ARRAY(VARCHAR)
@@ -117,32 +136,28 @@ bendsql -udatabend -pdatabend
117
136
  ### Commands
118
137
 
119
138
  ```sh
120
- bun install # Install dependencies
121
- bun run build # Build (dist/index.mjs + type declarations)
122
- bun test # Run integration tests (requires running Databend)
139
+ npm install # Install dependencies
140
+ npm run build # Build (dist/index.mjs + type declarations)
141
+ npm run typecheck # Type-check with tsc
142
+ npm run lint # Lint with biome
143
+ npm test # Run all tests (requires running Databend)
123
144
  ```
124
145
 
125
- ### Seed data
126
-
127
- Populate two sample tables (`users` and `events`) for manual testing:
128
-
129
- ```sh
130
- bun run scripts/seed.ts
131
- ```
132
-
133
- This creates 5 users and 10 events with VARIANT payloads and timestamps.
134
-
135
146
  ## Architecture
136
147
 
137
- Built on the same pattern as [drizzle-duckdb](https://github.com/leonardovida/drizzle-duckdb):
138
-
139
- - **`driver.ts`** -- `drizzle()` factory and `DatabendDatabase` extending `PgDatabase`
140
- - **`session.ts`** -- `DatabendSession` and `DatabendPreparedQuery` for query execution
141
- - **`dialect.ts`** -- `DatabendDialect` extending `PgDialect` with Databend-specific migrations and type mapping
142
- - **`client.ts`** -- Low-level execution wrapping `databend-driver`'s `Connection` API
143
- - **`pool.ts`** -- Connection pooling via `Client.getConn()`
144
- - **`columns.ts`** -- Custom column types (VARIANT, ARRAY, TUPLE, MAP, TIMESTAMP, DATE)
145
- - **`sql/result-mapper.ts`** -- Maps Databend results to Drizzle's expected format
148
+ - **`src/databend-core/`** -- Standalone dialect module (ported from drizzle-orm's gel-core)
149
+ - `dialect.ts` -- SQL generation with `?` params and `"` identifier quoting
150
+ - `session.ts` -- Abstract session, prepared query, and transaction base classes
151
+ - `db.ts` -- `DatabendDatabase` with select/insert/update/delete/execute/CTE support
152
+ - `table.ts` -- `databendTable()` table definition function
153
+ - `columns/` -- Column type builders (integer, varchar, boolean, timestamp, etc.)
154
+ - `query-builders/` -- SELECT, INSERT, UPDATE, DELETE query builders
155
+ - **`src/driver.ts`** -- `drizzle()` factory, connection management, pool creation
156
+ - **`src/session.ts`** -- Concrete session with databend-driver query execution
157
+ - **`src/client.ts`** -- Low-level execution wrapping `databend-driver`'s `Connection` API
158
+ - **`src/pool.ts`** -- Connection pooling via `Client.getConn()`
159
+ - **`src/columns.ts`** -- Custom column types (VARIANT, ARRAY, TUPLE, MAP, TIMESTAMP, DATE)
160
+ - **`src/sql/result-mapper.ts`** -- Maps Databend results to Drizzle's expected format
146
161
 
147
162
  ## License
148
163
 
package/dist/client.d.ts CHANGED
@@ -13,11 +13,16 @@ export type ExecuteArraysResult = {
13
13
  };
14
14
  export declare function isPool(client: DatabendClientLike): client is DatabendConnectionPool;
15
15
  /**
16
- * Convert Drizzle param array to a JSON value accepted by databend-driver's Params.
17
- * Databend's Params is serde_json::Value, so we pass an array of JSON-serializable values.
16
+ * Convert a Drizzle param array into the JSON values databend-driver binds to `?`
17
+ * placeholders. Databend's Params is a serde_json::Value, so we pass an array of
18
+ * JSON-serializable values.
18
19
  *
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).
20
+ * As of databend-driver 0.34.0 the driver binds parameters server-side (or, against
21
+ * older servers, interpolates them with its own SQL-standard '' escaping). Either way
22
+ * the driver owns escaping, so we must NOT pre-escape here - doing so would corrupt
23
+ * values via double-escaping. Our only job is to coerce JS types the JSON params array
24
+ * cannot carry verbatim (bigint, Date, structured values) into a form the server binds
25
+ * to the target column.
21
26
  */
22
27
  export declare function prepareParams(params: unknown[], typings?: QueryTypingsValue[]): unknown[];
23
28
  export declare function executeOnClient(client: DatabendClientLike, query: string, params: unknown[], typings?: QueryTypingsValue[]): Promise<RowData[]>;
@@ -11,7 +11,7 @@ export declare class DatabendDatabase {
11
11
  query: any;
12
12
  constructor(dialect: any, session: any, schema?: any);
13
13
  $with(alias: string): {
14
- as(qb: any): WithSubquery<string, Record<string, unknown>>;
14
+ as(qb: any): WithSubquery<string, any>;
15
15
  };
16
16
  $count(source: any, filters?: any): DatabendCountBuilder;
17
17
  with(...queries: any[]): {
@@ -8,7 +8,7 @@ export declare class DatabendDialect {
8
8
  constructor(config?: any);
9
9
  migrate(migrations: MigrationMeta[], session: any, config: MigrationConfig | string): Promise<void>;
10
10
  escapeName(name: string): string;
11
- escapeParam(num: number): string;
11
+ escapeParam(_num: number): string;
12
12
  escapeString(str: string): string;
13
13
  buildWithCTE(queries: any[] | undefined): SQL<unknown> | undefined;
14
14
  buildDeleteQuery({ table, where, withList }: any): SQL<unknown>;
@@ -7,7 +7,7 @@ export declare class QueryBuilder {
7
7
  dialectConfig: any;
8
8
  constructor(dialect?: any);
9
9
  $with(alias: string): {
10
- as(qb: any): WithSubquery<string, Record<string, unknown>>;
10
+ as(qb: any): WithSubquery<string, any>;
11
11
  };
12
12
  with(...queries: any[]): {
13
13
  select: (fields?: any) => DatabendSelectBuilder;
@@ -45,7 +45,7 @@ declare class DatabendSelectQueryBuilderBase extends TypedQueryBuilder<any, any>
45
45
  /** @internal */
46
46
  getSQL(): any;
47
47
  toSQL(): any;
48
- as(alias: string): Subquery<string, Record<string, unknown>>;
48
+ as(alias: string): Subquery<string, any>;
49
49
  /** @internal */
50
50
  getSelectedFields(): any;
51
51
  $dynamic(): this;
package/dist/index.mjs CHANGED
@@ -5,17 +5,18 @@ function isPool(client) {
5
5
  function prepareParams(params, typings) {
6
6
  return params.map((param, i) => {
7
7
  if (param === void 0) return null;
8
- if (param instanceof Date) return param.toISOString().replace(/'/g, "''");
8
+ if (param === null) return null;
9
+ if (param instanceof Date) return param.toISOString();
9
10
  if (typeof param === "bigint") return param.toString();
10
11
  if (typeof param === "string") {
11
12
  const typing = typings?.[i];
12
13
  if (typing === "decimal" && /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(param)) {
13
14
  return Number(param);
14
15
  }
15
- return param.replace(/'/g, "''");
16
+ return param;
16
17
  }
17
- if (typeof param === "object" && param !== null) {
18
- return JSON.stringify(param).replace(/'/g, "''");
18
+ if (typeof param === "object") {
19
+ return JSON.stringify(param);
19
20
  }
20
21
  return param;
21
22
  });
@@ -963,8 +964,8 @@ var DatabendDialect = class {
963
964
  escapeName(name) {
964
965
  return `"${name}"`;
965
966
  }
966
- escapeParam(num) {
967
- return `$${num + 1}`;
967
+ escapeParam(_num) {
968
+ return "?";
968
969
  }
969
970
  escapeString(str) {
970
971
  return `'${str.replace(/'/g, "''")}'`;
@@ -2952,6 +2953,12 @@ import {
2952
2953
  is as is10,
2953
2954
  SQL as SQL8
2954
2955
  } from "drizzle-orm";
2956
+ function getFieldSql(field) {
2957
+ const f = field;
2958
+ if (f.sql instanceof SQL8) return f.sql;
2959
+ if (f._?.sql instanceof SQL8) return f._.sql;
2960
+ throw new Error("Cannot extract SQL from field");
2961
+ }
2955
2962
  function toDecoderInput(decoder, value) {
2956
2963
  void decoder;
2957
2964
  return value;
@@ -3002,11 +3009,12 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
3002
3009
  } else if (is10(field, SQL8)) {
3003
3010
  decoder = field.decoder;
3004
3011
  } else {
3005
- const col = field.sql.queryChunks.find((chunk) => is10(chunk, Column3));
3012
+ const fieldSql = getFieldSql(field);
3013
+ const col = fieldSql.queryChunks.find((chunk) => is10(chunk, Column3));
3006
3014
  if (is10(col, DatabendCustomColumn)) {
3007
3015
  decoder = col;
3008
3016
  } else {
3009
- decoder = field.sql.decoder;
3017
+ decoder = fieldSql.decoder;
3010
3018
  }
3011
3019
  }
3012
3020
  let node = acc;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "module": "./dist/index.mjs",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.ts",
6
- "version": "0.1.13",
6
+ "version": "0.1.15",
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": {
@@ -17,8 +17,8 @@
17
17
  "typecheck": "tsc --noEmit"
18
18
  },
19
19
  "peerDependencies": {
20
- "databend-driver": ">=0.33.0",
21
- "drizzle-orm": "^0.40.0"
20
+ "databend-driver": ">=0.34.0",
21
+ "drizzle-orm": ">=0.40.0"
22
22
  },
23
23
  "peerDependenciesMeta": {
24
24
  "databend-driver": {
@@ -28,8 +28,8 @@
28
28
  "devDependencies": {
29
29
  "@biomejs/biome": "^2.4.7",
30
30
  "@types/node": "^25.5.0",
31
- "databend-driver": "^0.33.6",
32
- "drizzle-orm": "0.40.0",
31
+ "databend-driver": "^0.34.0",
32
+ "drizzle-orm": "0.45.0",
33
33
  "esbuild": "^0.25.0",
34
34
  "typescript": "^5.8.2",
35
35
  "vitest": "^1.6.0"
package/src/client.ts CHANGED
@@ -19,27 +19,40 @@ export function isPool(
19
19
  }
20
20
 
21
21
  /**
22
- * Convert Drizzle param array to a JSON value accepted by databend-driver's Params.
23
- * Databend's Params is serde_json::Value, so we pass an array of JSON-serializable values.
22
+ * Convert a Drizzle param array into the JSON values databend-driver binds to `?`
23
+ * placeholders. Databend's Params is a serde_json::Value, so we pass an array of
24
+ * JSON-serializable values.
24
25
  *
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).
26
+ * As of databend-driver 0.34.0 the driver binds parameters server-side (or, against
27
+ * older servers, interpolates them with its own SQL-standard '' escaping). Either way
28
+ * the driver owns escaping, so we must NOT pre-escape here - doing so would corrupt
29
+ * values via double-escaping. Our only job is to coerce JS types the JSON params array
30
+ * cannot carry verbatim (bigint, Date, structured values) into a form the server binds
31
+ * to the target column.
27
32
  */
28
33
  export function prepareParams(params: unknown[], typings?: QueryTypingsValue[]): unknown[] {
29
34
  return params.map((param, i) => {
30
35
  if (param === undefined) return null;
31
- if (param instanceof Date) return param.toISOString().replace(/'/g, "''");
36
+ if (param === null) return null;
37
+ // JSON has no Date type: send an ISO string for the server to bind to TIMESTAMP.
38
+ if (param instanceof Date) return param.toISOString();
39
+ // JSON cannot carry a bigint: send it as a string for the server to parse.
32
40
  if (typeof param === 'bigint') return param.toString();
33
41
  if (typeof param === 'string') {
42
+ // Drizzle hands decimal/numeric values over as strings. Coerce them to a JS number
43
+ // so the server binds a numeric value rather than a string for numeric columns.
34
44
  const typing = typings?.[i];
35
45
  if (typing === 'decimal' && /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(param)) {
36
46
  return Number(param);
37
47
  }
38
- return param.replace(/'/g, "''");
48
+ return param;
39
49
  }
40
- if (typeof param === 'object' && param !== null) {
41
- return JSON.stringify(param).replace(/'/g, "''");
50
+ // VARIANT / ARRAY / MAP / TUPLE: serialize structured values to a JSON string, which
51
+ // the server parses into the semi-structured column type.
52
+ if (typeof param === 'object') {
53
+ return JSON.stringify(param);
42
54
  }
55
+ // number / boolean pass through unchanged.
43
56
  return param;
44
57
  });
45
58
  }
@@ -97,8 +97,12 @@ export class DatabendDialect {
97
97
  return `"${name}"`;
98
98
  }
99
99
 
100
- escapeParam(num: number): string {
101
- return `$${num + 1}`;
100
+ escapeParam(_num: number): string {
101
+ // Emit `?` rather than `$N`. Databend reads `$N` as a column-position reference
102
+ // (stage column refs), which forces the driver onto its client-side interpolation
103
+ // fallback. `?` parses as a placeholder and is bound server-side, positionally, in
104
+ // encounter order - which matches how Drizzle appends one param per placeholder.
105
+ return '?';
102
106
  }
103
107
 
104
108
  escapeString(str: string): string {
@@ -6,6 +6,7 @@ import {
6
6
  is,
7
7
  type SelectedFieldsOrdered,
8
8
  SQL,
9
+ Subquery,
9
10
  } from 'drizzle-orm';
10
11
  import { DatabendCustomColumn } from '../databend-core/columns/custom.ts';
11
12
  import { DatabendDate } from '../databend-core/columns/date.ts';
@@ -15,6 +16,14 @@ type SQLInternal<T = unknown> = SQL<T> & {
15
16
  decoder: DriverValueDecoder<T, any>;
16
17
  };
17
18
 
19
+ /** Extract SQL from Aliased (has .sql) or Subquery (has _.sql) */
20
+ function getFieldSql(field: unknown): SQL {
21
+ const f = field as any;
22
+ if (f.sql instanceof SQL) return f.sql;
23
+ if (f._?.sql instanceof SQL) return f._.sql;
24
+ throw new Error('Cannot extract SQL from field');
25
+ }
26
+
18
27
  type DecoderInput<TDecoder extends DriverValueDecoder<unknown, unknown>> =
19
28
  Parameters<TDecoder['mapFromDriverValue']>[0];
20
29
 
@@ -113,12 +122,13 @@ export function mapResultRow<TResult>(
113
122
  } else if (is(field, SQL)) {
114
123
  decoder = (field as SQLInternal).decoder;
115
124
  } else {
116
- const col = field.sql.queryChunks.find((chunk) => is(chunk, Column));
125
+ const fieldSql = getFieldSql(field);
126
+ const col = fieldSql.queryChunks.find((chunk) => is(chunk, Column));
117
127
 
118
128
  if (is(col, DatabendCustomColumn)) {
119
129
  decoder = col;
120
130
  } else {
121
- decoder = (field.sql as SQLInternal).decoder;
131
+ decoder = (fieldSql as SQLInternal).decoder;
122
132
  }
123
133
  }
124
134
  let node = acc;