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 +45 -30
- package/dist/client.d.ts +9 -4
- package/dist/databend-core/db.d.ts +1 -1
- package/dist/databend-core/dialect.d.ts +1 -1
- package/dist/databend-core/query-builders/query-builder.d.ts +1 -1
- package/dist/databend-core/query-builders/select.d.ts +1 -1
- package/dist/index.mjs +16 -8
- package/package.json +5 -5
- package/src/client.ts +21 -8
- package/src/databend-core/dialect.ts +6 -2
- package/src/sql/result-mapper.ts +12 -2
package/README.md
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
# drizzle-databend
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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 =
|
|
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 {
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
-
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
- **`
|
|
145
|
-
- **`
|
|
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
|
|
17
|
-
* Databend's Params is serde_json::Value, so we pass an array of
|
|
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
|
-
*
|
|
20
|
-
*
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
16
|
+
return param;
|
|
16
17
|
}
|
|
17
|
-
if (typeof param === "object"
|
|
18
|
-
return JSON.stringify(param)
|
|
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(
|
|
967
|
-
return
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
21
|
-
"drizzle-orm": "
|
|
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.
|
|
32
|
-
"drizzle-orm": "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
|
|
23
|
-
* Databend's Params is serde_json::Value, so we pass an array of
|
|
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
|
-
*
|
|
26
|
-
*
|
|
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
|
|
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
|
|
48
|
+
return param;
|
|
39
49
|
}
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
101
|
-
|
|
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 {
|
package/src/sql/result-mapper.ts
CHANGED
|
@@ -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
|
|
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 = (
|
|
131
|
+
decoder = (fieldSql as SQLInternal).decoder;
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
134
|
let node = acc;
|