drizzle-databend 0.1.0

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/driver.ts ADDED
@@ -0,0 +1,268 @@
1
+ import { Client } from 'databend-driver';
2
+ import { entityKind } from 'drizzle-orm/entity';
3
+ import type { Logger } from 'drizzle-orm/logger';
4
+ import { DefaultLogger } from 'drizzle-orm/logger';
5
+ import { PgDatabase } from 'drizzle-orm/pg-core/db';
6
+ import {
7
+ createTableRelationsHelpers,
8
+ extractTablesRelationalConfig,
9
+ type ExtractTablesWithRelations,
10
+ type RelationalSchemaConfig,
11
+ type TablesRelationalConfig,
12
+ } from 'drizzle-orm/relations';
13
+ import { type DrizzleConfig } from 'drizzle-orm/utils';
14
+ import type {
15
+ DatabendClientLike,
16
+ DatabendQueryResultHKT,
17
+ DatabendTransaction,
18
+ } from './session.ts';
19
+ 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
+
28
+ export interface DatabendDriverOptions {
29
+ logger?: Logger;
30
+ }
31
+
32
+ export class DatabendDriver {
33
+ static readonly [entityKind]: string = 'DatabendDriver';
34
+
35
+ constructor(
36
+ private client: DatabendClientLike,
37
+ private dialect: DatabendDialect,
38
+ private options: DatabendDriverOptions = {}
39
+ ) {}
40
+
41
+ createSession(
42
+ schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined
43
+ ): DatabendSession<Record<string, unknown>, TablesRelationalConfig> {
44
+ return new DatabendSession(this.client, this.dialect, schema, {
45
+ logger: this.options.logger,
46
+ });
47
+ }
48
+ }
49
+
50
+ export interface DatabendDrizzleConfig<
51
+ TSchema extends Record<string, unknown> = Record<string, never>,
52
+ > extends DrizzleConfig<TSchema> {
53
+ /** Pool configuration. Use size config or false to disable. */
54
+ pool?: DatabendPoolConfig | false;
55
+ }
56
+
57
+ export interface DatabendDrizzleConfigWithConnection<
58
+ TSchema extends Record<string, unknown> = Record<string, never>,
59
+ > extends DatabendDrizzleConfig<TSchema> {
60
+ /** Connection DSN string */
61
+ connection: string;
62
+ }
63
+
64
+ export interface DatabendDrizzleConfigWithClient<
65
+ TSchema extends Record<string, unknown> = Record<string, never>,
66
+ > extends DatabendDrizzleConfig<TSchema> {
67
+ /** Explicit client (connection or pool) */
68
+ client: DatabendClientLike;
69
+ }
70
+
71
+ function isConfigObject(data: unknown): data is Record<string, unknown> {
72
+ if (typeof data !== 'object' || data === null) return false;
73
+ if (data.constructor?.name !== 'Object') return false;
74
+ return (
75
+ 'connection' in data ||
76
+ 'client' in data ||
77
+ 'pool' in data ||
78
+ 'schema' in data ||
79
+ 'logger' in data
80
+ );
81
+ }
82
+
83
+ function createFromClient<
84
+ TSchema extends Record<string, unknown> = Record<string, never>,
85
+ >(
86
+ client: DatabendClientLike,
87
+ config: DatabendDrizzleConfig<TSchema> = {},
88
+ databendClient?: Client
89
+ ): DatabendDatabase<TSchema, ExtractTablesWithRelations<TSchema>> {
90
+ const dialect = new DatabendDialect();
91
+
92
+ const logger =
93
+ config.logger === true ? new DefaultLogger() : config.logger || undefined;
94
+
95
+ let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
96
+
97
+ if (config.schema) {
98
+ const tablesConfig = extractTablesRelationalConfig(
99
+ config.schema,
100
+ createTableRelationsHelpers
101
+ );
102
+ schema = {
103
+ fullSchema: config.schema,
104
+ schema: tablesConfig.tables,
105
+ tableNamesMap: tablesConfig.tableNamesMap,
106
+ };
107
+ }
108
+
109
+ const driver = new DatabendDriver(client, dialect, { logger });
110
+ const session = driver.createSession(schema);
111
+
112
+ const db = new DatabendDatabase(
113
+ dialect,
114
+ session,
115
+ schema,
116
+ client,
117
+ databendClient
118
+ );
119
+ return db as DatabendDatabase<TSchema, ExtractTablesWithRelations<TSchema>>;
120
+ }
121
+
122
+ async function createFromDsn<
123
+ TSchema extends Record<string, unknown> = Record<string, never>,
124
+ >(
125
+ dsn: string,
126
+ config: DatabendDrizzleConfig<TSchema> = {}
127
+ ): Promise<DatabendDatabase<TSchema, ExtractTablesWithRelations<TSchema>>> {
128
+ const databendClient = new Client(dsn);
129
+
130
+ if (config.pool === false) {
131
+ const connection = await databendClient.getConn();
132
+ return createFromClient(connection, config, databendClient);
133
+ }
134
+
135
+ const poolSize = config.pool?.size ?? 4;
136
+ const pool = createDatabendConnectionPool(databendClient, { size: poolSize });
137
+ return createFromClient(pool, config, databendClient);
138
+ }
139
+
140
+ // Overload 1: DSN string (async, auto-pools)
141
+ export function drizzle<
142
+ TSchema extends Record<string, unknown> = Record<string, never>,
143
+ >(
144
+ dsn: string
145
+ ): Promise<DatabendDatabase<TSchema, ExtractTablesWithRelations<TSchema>>>;
146
+
147
+ // Overload 2: DSN string + config (async, auto-pools)
148
+ export function drizzle<
149
+ TSchema extends Record<string, unknown> = Record<string, never>,
150
+ >(
151
+ dsn: string,
152
+ config: DatabendDrizzleConfig<TSchema>
153
+ ): Promise<DatabendDatabase<TSchema, ExtractTablesWithRelations<TSchema>>>;
154
+
155
+ // Overload 3: Config with connection (async, auto-pools)
156
+ export function drizzle<
157
+ TSchema extends Record<string, unknown> = Record<string, never>,
158
+ >(
159
+ config: DatabendDrizzleConfigWithConnection<TSchema>
160
+ ): Promise<DatabendDatabase<TSchema, ExtractTablesWithRelations<TSchema>>>;
161
+
162
+ // Overload 4: Config with explicit client (sync)
163
+ export function drizzle<
164
+ TSchema extends Record<string, unknown> = Record<string, never>,
165
+ >(
166
+ config: DatabendDrizzleConfigWithClient<TSchema>
167
+ ): DatabendDatabase<TSchema, ExtractTablesWithRelations<TSchema>>;
168
+
169
+ // Overload 5: Explicit client (sync, backward compatible)
170
+ export function drizzle<
171
+ TSchema extends Record<string, unknown> = Record<string, never>,
172
+ >(
173
+ client: DatabendClientLike,
174
+ config?: DatabendDrizzleConfig<TSchema>
175
+ ): DatabendDatabase<TSchema, ExtractTablesWithRelations<TSchema>>;
176
+
177
+ // Implementation
178
+ export function drizzle<
179
+ TSchema extends Record<string, unknown> = Record<string, never>,
180
+ >(
181
+ clientOrConfigOrDsn:
182
+ | string
183
+ | DatabendClientLike
184
+ | DatabendDrizzleConfigWithConnection<TSchema>
185
+ | DatabendDrizzleConfigWithClient<TSchema>,
186
+ config?: DatabendDrizzleConfig<TSchema>
187
+ ):
188
+ | DatabendDatabase<TSchema, ExtractTablesWithRelations<TSchema>>
189
+ | Promise<DatabendDatabase<TSchema, ExtractTablesWithRelations<TSchema>>> {
190
+ // String DSN -> async with auto-pool
191
+ if (typeof clientOrConfigOrDsn === 'string') {
192
+ return createFromDsn(clientOrConfigOrDsn, config);
193
+ }
194
+
195
+ // Config object with connection or client
196
+ if (isConfigObject(clientOrConfigOrDsn)) {
197
+ const configObj = clientOrConfigOrDsn as
198
+ | DatabendDrizzleConfigWithConnection<TSchema>
199
+ | DatabendDrizzleConfigWithClient<TSchema>;
200
+
201
+ if ('connection' in configObj) {
202
+ const connConfig =
203
+ configObj as DatabendDrizzleConfigWithConnection<TSchema>;
204
+ const { connection, ...restConfig } = connConfig;
205
+ return createFromDsn(
206
+ connection,
207
+ restConfig as DatabendDrizzleConfig<TSchema>
208
+ );
209
+ }
210
+
211
+ if ('client' in configObj) {
212
+ const clientConfig = configObj as DatabendDrizzleConfigWithClient<TSchema>;
213
+ const { client: clientValue, ...restConfig } = clientConfig;
214
+ return createFromClient(
215
+ clientValue,
216
+ restConfig as DatabendDrizzleConfig<TSchema>
217
+ );
218
+ }
219
+
220
+ throw new Error(
221
+ 'Invalid drizzle config: either connection or client must be provided'
222
+ );
223
+ }
224
+
225
+ // Direct client (backward compatible)
226
+ return createFromClient(clientOrConfigOrDsn as DatabendClientLike, config);
227
+ }
228
+
229
+ export class DatabendDatabase<
230
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
231
+ TSchema extends TablesRelationalConfig =
232
+ ExtractTablesWithRelations<TFullSchema>,
233
+ > extends PgDatabase<DatabendQueryResultHKT, TFullSchema, TSchema> {
234
+ static readonly [entityKind]: string = 'DatabendDatabase';
235
+
236
+ /** The underlying connection or pool */
237
+ readonly $client: DatabendClientLike;
238
+
239
+ /** The Databend Client instance (when created from DSN) */
240
+ readonly $databendClient?: Client;
241
+
242
+ constructor(
243
+ readonly dialect: DatabendDialect,
244
+ readonly session: DatabendSession<TFullSchema, TSchema>,
245
+ schema: RelationalSchemaConfig<TSchema> | undefined,
246
+ client: DatabendClientLike,
247
+ databendClient?: Client
248
+ ) {
249
+ super(dialect, session, schema);
250
+ this.$client = client;
251
+ this.$databendClient = databendClient;
252
+ }
253
+
254
+ async close(): Promise<void> {
255
+ if (isPool(this.$client) && this.$client.close) {
256
+ await this.$client.close();
257
+ }
258
+ if (!isPool(this.$client)) {
259
+ await closeClientConnection(this.$client as Connection);
260
+ }
261
+ }
262
+
263
+ override async transaction<T>(
264
+ transaction: (tx: DatabendTransaction<TFullSchema, TSchema>) => Promise<T>
265
+ ): Promise<T> {
266
+ return await this.session.transaction<T>(transaction);
267
+ }
268
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './driver.ts';
2
+ export * from './session.ts';
3
+ export * from './columns.ts';
4
+ export * from './migrator.ts';
5
+ export * from './client.ts';
6
+ export * from './pool.ts';
@@ -0,0 +1,22 @@
1
+ import type { MigrationConfig } from 'drizzle-orm/migrator';
2
+ import { readMigrationFiles } from 'drizzle-orm/migrator';
3
+ import type { DatabendDatabase } from './driver.ts';
4
+ import type { PgSession } from 'drizzle-orm/pg-core/session';
5
+
6
+ export type DatabendMigrationConfig = MigrationConfig | string;
7
+
8
+ export async function migrate<TSchema extends Record<string, unknown>>(
9
+ db: DatabendDatabase<TSchema>,
10
+ config: DatabendMigrationConfig
11
+ ) {
12
+ const migrationConfig: MigrationConfig =
13
+ typeof config === 'string' ? { migrationsFolder: config } : config;
14
+
15
+ const migrations = readMigrationFiles(migrationConfig);
16
+
17
+ await db.dialect.migrate(
18
+ migrations,
19
+ db.session as unknown as PgSession,
20
+ migrationConfig
21
+ );
22
+ }
package/src/pool.ts ADDED
@@ -0,0 +1,233 @@
1
+ import type { Client, Connection } from 'databend-driver';
2
+ import { closeClientConnection, type DatabendConnectionPool } from './client.ts';
3
+
4
+ export interface DatabendPoolConfig {
5
+ /** Maximum concurrent connections. Defaults to 4. */
6
+ size?: number;
7
+ }
8
+
9
+ export interface DatabendConnectionPoolOptions {
10
+ /** Maximum concurrent connections. Defaults to 4. */
11
+ size?: number;
12
+ /** Timeout in milliseconds to wait for a connection. Defaults to 30000 (30s). */
13
+ acquireTimeout?: number;
14
+ /** Maximum number of requests waiting for a connection. Defaults to 100. */
15
+ maxWaitingRequests?: number;
16
+ /** Max time (ms) a connection may live before being recycled. */
17
+ maxLifetimeMs?: number;
18
+ /** Max idle time (ms) before an idle connection is discarded. */
19
+ idleTimeoutMs?: number;
20
+ }
21
+
22
+ export function createDatabendConnectionPool(
23
+ client: Client,
24
+ options: DatabendConnectionPoolOptions = {}
25
+ ): DatabendConnectionPool & { size: number } {
26
+ const size = options.size && options.size > 0 ? options.size : 4;
27
+ const acquireTimeout = options.acquireTimeout ?? 30_000;
28
+ const maxWaitingRequests = options.maxWaitingRequests ?? 100;
29
+ const maxLifetimeMs = options.maxLifetimeMs;
30
+ const idleTimeoutMs = options.idleTimeoutMs;
31
+ const metadata = new WeakMap<
32
+ Connection,
33
+ { createdAt: number; lastUsedAt: number }
34
+ >();
35
+
36
+ type PooledConnection = {
37
+ connection: Connection;
38
+ createdAt: number;
39
+ lastUsedAt: number;
40
+ };
41
+
42
+ const idle: PooledConnection[] = [];
43
+ const waiting: Array<{
44
+ resolve: (conn: Connection) => void;
45
+ reject: (error: Error) => void;
46
+ timeoutId: ReturnType<typeof setTimeout>;
47
+ }> = [];
48
+ let total = 0;
49
+ let closed = false;
50
+ let pendingAcquires = 0;
51
+
52
+ const shouldRecycle = (conn: PooledConnection, now: number): boolean => {
53
+ if (maxLifetimeMs !== undefined && now - conn.createdAt >= maxLifetimeMs) {
54
+ return true;
55
+ }
56
+ if (idleTimeoutMs !== undefined && now - conn.lastUsedAt >= idleTimeoutMs) {
57
+ return true;
58
+ }
59
+ return false;
60
+ };
61
+
62
+ const acquire = async (): Promise<Connection> => {
63
+ if (closed) {
64
+ throw new Error('Databend connection pool is closed');
65
+ }
66
+
67
+ while (idle.length > 0) {
68
+ const pooled = idle.pop() as PooledConnection;
69
+ const now = Date.now();
70
+ if (shouldRecycle(pooled, now)) {
71
+ await closeClientConnection(pooled.connection);
72
+ total = Math.max(0, total - 1);
73
+ metadata.delete(pooled.connection);
74
+ continue;
75
+ }
76
+ pooled.lastUsedAt = now;
77
+ metadata.set(pooled.connection, {
78
+ createdAt: pooled.createdAt,
79
+ lastUsedAt: pooled.lastUsedAt,
80
+ });
81
+ return pooled.connection;
82
+ }
83
+
84
+ if (total < size) {
85
+ pendingAcquires += 1;
86
+ total += 1;
87
+ try {
88
+ const connection = await client.getConn();
89
+ if (closed) {
90
+ await closeClientConnection(connection);
91
+ total -= 1;
92
+ throw new Error('Databend connection pool is closed');
93
+ }
94
+ const now = Date.now();
95
+ metadata.set(connection, { createdAt: now, lastUsedAt: now });
96
+ return connection;
97
+ } catch (error) {
98
+ total -= 1;
99
+ throw error;
100
+ } finally {
101
+ pendingAcquires -= 1;
102
+ }
103
+ }
104
+
105
+ if (waiting.length >= maxWaitingRequests) {
106
+ throw new Error(
107
+ `Databend connection pool queue is full (max ${maxWaitingRequests} waiting requests)`
108
+ );
109
+ }
110
+
111
+ return await new Promise((resolve, reject) => {
112
+ const timeoutId = setTimeout(() => {
113
+ const idx = waiting.findIndex((w) => w.timeoutId === timeoutId);
114
+ if (idx !== -1) {
115
+ waiting.splice(idx, 1);
116
+ }
117
+ reject(
118
+ new Error(
119
+ `Databend connection pool acquire timeout after ${acquireTimeout}ms`
120
+ )
121
+ );
122
+ }, acquireTimeout);
123
+
124
+ waiting.push({ resolve, reject, timeoutId });
125
+ });
126
+ };
127
+
128
+ const release = async (connection: Connection): Promise<void> => {
129
+ const waiter = waiting.shift();
130
+ if (waiter) {
131
+ clearTimeout(waiter.timeoutId);
132
+ const now = Date.now();
133
+ const meta =
134
+ metadata.get(connection) ??
135
+ ({ createdAt: now, lastUsedAt: now } as {
136
+ createdAt: number;
137
+ lastUsedAt: number;
138
+ });
139
+
140
+ const expired =
141
+ maxLifetimeMs !== undefined && now - meta.createdAt >= maxLifetimeMs;
142
+
143
+ if (closed) {
144
+ await closeClientConnection(connection);
145
+ total = Math.max(0, total - 1);
146
+ metadata.delete(connection);
147
+ waiter.reject(new Error('Databend connection pool is closed'));
148
+ return;
149
+ }
150
+
151
+ if (expired) {
152
+ await closeClientConnection(connection);
153
+ total = Math.max(0, total - 1);
154
+ metadata.delete(connection);
155
+ try {
156
+ const replacement = await acquire();
157
+ waiter.resolve(replacement);
158
+ } catch (error) {
159
+ waiter.reject(error as Error);
160
+ }
161
+ return;
162
+ }
163
+
164
+ meta.lastUsedAt = now;
165
+ metadata.set(connection, meta);
166
+ waiter.resolve(connection);
167
+ return;
168
+ }
169
+
170
+ if (closed) {
171
+ await closeClientConnection(connection);
172
+ metadata.delete(connection);
173
+ total = Math.max(0, total - 1);
174
+ return;
175
+ }
176
+
177
+ const now = Date.now();
178
+ const existingMeta =
179
+ metadata.get(connection) ??
180
+ ({ createdAt: now, lastUsedAt: now } as {
181
+ createdAt: number;
182
+ lastUsedAt: number;
183
+ });
184
+ existingMeta.lastUsedAt = now;
185
+ metadata.set(connection, existingMeta);
186
+
187
+ if (
188
+ maxLifetimeMs !== undefined &&
189
+ now - existingMeta.createdAt >= maxLifetimeMs
190
+ ) {
191
+ await closeClientConnection(connection);
192
+ total -= 1;
193
+ metadata.delete(connection);
194
+ return;
195
+ }
196
+
197
+ idle.push({
198
+ connection,
199
+ createdAt: existingMeta.createdAt,
200
+ lastUsedAt: existingMeta.lastUsedAt,
201
+ });
202
+ };
203
+
204
+ const close = async (): Promise<void> => {
205
+ closed = true;
206
+
207
+ const waiters = waiting.splice(0, waiting.length);
208
+ for (const waiter of waiters) {
209
+ clearTimeout(waiter.timeoutId);
210
+ waiter.reject(new Error('Databend connection pool is closed'));
211
+ }
212
+
213
+ const toClose = idle.splice(0, idle.length);
214
+ await Promise.allSettled(
215
+ toClose.map((item) => closeClientConnection(item.connection))
216
+ );
217
+ total = Math.max(0, total - toClose.length);
218
+ toClose.forEach((item) => metadata.delete(item.connection));
219
+
220
+ const maxWait = 5000;
221
+ const start = Date.now();
222
+ while (pendingAcquires > 0 && Date.now() - start < maxWait) {
223
+ await new Promise((r) => setTimeout(r, 10));
224
+ }
225
+ };
226
+
227
+ return {
228
+ acquire,
229
+ release,
230
+ close,
231
+ size,
232
+ };
233
+ }