Skip to content

Commit 5de28fd

Browse files
committed
feat(db0): add db0 driver for drizzle-orm
1 parent a086f59 commit 5de28fd

16 files changed

Lines changed: 1342 additions & 168 deletions

File tree

drizzle-orm/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@
7272
"sql.js": ">=1",
7373
"sqlite3": ">=5",
7474
"gel": ">=2",
75-
"@upstash/redis": ">=1.34.7"
75+
"@upstash/redis": ">=1.34.7",
76+
"db0": ">=0.2.0"
7677
},
7778
"peerDependenciesMeta": {
7879
"mysql2": {
@@ -161,6 +162,9 @@
161162
},
162163
"@upstash/redis": {
163164
"optional": true
165+
},
166+
"db0": {
167+
"optional": true
164168
}
165169
},
166170
"devDependencies": {
@@ -188,6 +192,7 @@
188192
"better-sqlite3": "^11.9.1",
189193
"bun-types": "^1.2.0",
190194
"cpy": "^10.1.0",
195+
"db0": "^0.2.3",
191196
"expo-sqlite": "^14.0.0",
192197
"gel": "^2.0.0",
193198
"glob": "^11.0.1",

drizzle-orm/src/db0/driver.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { Database } from 'db0';
2+
import type { DrizzleConfig } from '~/utils.ts';
3+
import { isConfig } from '~/utils.ts';
4+
import { constructPg, type Db0PgDatabase } from './pg/index.ts';
5+
import { constructSqlite, type Db0SQLiteDatabase } from './sqlite/index.ts';
6+
7+
export type Db0Database = Db0SQLiteDatabase<any> | Db0PgDatabase<any>;
8+
9+
export function drizzle<TSchema extends Record<string, unknown> = Record<string, never>>(
10+
...params:
11+
| [Database]
12+
| [Database, DrizzleConfig<TSchema>]
13+
| [DrizzleConfig<TSchema> & { client: Database }]
14+
): Db0Database & { $client: Database } {
15+
if (isConfig(params[0])) {
16+
const { client, ...drizzleConfig } = params[0] as DrizzleConfig<TSchema> & { client: Database };
17+
return drizzleInternal(client, drizzleConfig);
18+
}
19+
20+
return drizzleInternal(params[0] as Database, params[1] as DrizzleConfig<TSchema> | undefined);
21+
}
22+
23+
function drizzleInternal<TSchema extends Record<string, unknown> = Record<string, never>>(
24+
client: Database,
25+
config: DrizzleConfig<TSchema> = {},
26+
): Db0Database & { $client: Database } {
27+
const dialect = client.dialect;
28+
29+
switch (dialect) {
30+
case 'sqlite':
31+
case 'libsql':
32+
return constructSqlite(client, config) as any;
33+
case 'postgresql':
34+
return constructPg(client, config) as any;
35+
case 'mysql':
36+
throw new Error('drizzle-orm/db0: MySQL support is not yet implemented');
37+
default:
38+
throw new Error(`drizzle-orm/db0: Unsupported db0 dialect: ${dialect}`);
39+
}
40+
}
41+
42+
export namespace drizzle {
43+
export function mock<TSchema extends Record<string, unknown> = Record<string, never>>(
44+
config?: DrizzleConfig<TSchema>,
45+
): Db0Database & { $client: '$client is not available on drizzle.mock()' } {
46+
return constructSqlite({} as any, config) as any;
47+
}
48+
}

drizzle-orm/src/db0/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export { drizzle, type Db0Database } from './driver.ts';
2+
export {
3+
constructPg,
4+
Db0PgDatabase,
5+
Db0PgPreparedQuery,
6+
type Db0PgQueryResult,
7+
type Db0PgQueryResultHKT,
8+
Db0PgSession,
9+
type Db0PgSessionOptions,
10+
Db0PgTransaction,
11+
} from './pg/index.ts';
12+
export {
13+
constructSqlite,
14+
Db0SQLiteDatabase,
15+
Db0SQLitePreparedQuery,
16+
Db0SQLiteSession,
17+
Db0SQLiteTransaction,
18+
type Db0RunResult,
19+
type Db0SQLiteSessionOptions,
20+
} from './sqlite/index.ts';

drizzle-orm/src/db0/pg/driver.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { Database } from 'db0';
2+
import { entityKind } from '~/entity.ts';
3+
import { DefaultLogger } from '~/logger.ts';
4+
import { PgDatabase } from '~/pg-core/db.ts';
5+
import { PgDialect } from '~/pg-core/dialect.ts';
6+
import {
7+
createTableRelationsHelpers,
8+
extractTablesRelationalConfig,
9+
type RelationalSchemaConfig,
10+
type TablesRelationalConfig,
11+
} from '~/relations.ts';
12+
import type { DrizzleConfig } from '~/utils.ts';
13+
import { type Db0PgQueryResultHKT, Db0PgSession, type Db0PgSessionOptions } from './session.ts';
14+
15+
export class Db0PgDatabase<
16+
TSchema extends Record<string, unknown> = Record<string, never>,
17+
> extends PgDatabase<Db0PgQueryResultHKT, TSchema> {
18+
static override readonly [entityKind]: string = 'Db0PgDatabase';
19+
}
20+
21+
export function constructPg<TSchema extends Record<string, unknown> = Record<string, never>>(
22+
client: Database,
23+
config: DrizzleConfig<TSchema> = {},
24+
): Db0PgDatabase<TSchema> & { $client: Database } {
25+
const dialect = new PgDialect({ casing: config.casing });
26+
let logger;
27+
if (config.logger === true) {
28+
logger = new DefaultLogger();
29+
} else if (config.logger !== false) {
30+
logger = config.logger;
31+
}
32+
33+
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
34+
if (config.schema) {
35+
const tablesConfig = extractTablesRelationalConfig(config.schema, createTableRelationsHelpers);
36+
schema = {
37+
fullSchema: config.schema,
38+
schema: tablesConfig.tables,
39+
tableNamesMap: tablesConfig.tableNamesMap,
40+
};
41+
}
42+
43+
const sessionOptions: Db0PgSessionOptions = { logger, cache: config.cache };
44+
const session = new Db0PgSession(client, dialect, schema, sessionOptions);
45+
const db = new Db0PgDatabase(dialect, session, schema as any) as Db0PgDatabase<TSchema>;
46+
(<any>db).$client = client;
47+
48+
return db as any;
49+
}

drizzle-orm/src/db0/pg/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export { constructPg, Db0PgDatabase } from './driver.ts';
2+
export * from './migrator.ts';
3+
export {
4+
Db0PgPreparedQuery,
5+
type Db0PgQueryResult,
6+
type Db0PgQueryResultHKT,
7+
Db0PgSession,
8+
type Db0PgSessionOptions,
9+
Db0PgTransaction,
10+
} from './session.ts';

drizzle-orm/src/db0/pg/migrator.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { MigrationConfig } from '~/migrator.ts';
2+
import { readMigrationFiles } from '~/migrator.ts';
3+
import type { Db0PgDatabase } from './driver.ts';
4+
5+
export async function migrate<TSchema extends Record<string, unknown>>(
6+
db: Db0PgDatabase<TSchema>,
7+
config: MigrationConfig,
8+
) {
9+
const migrations = readMigrationFiles(config);
10+
await db.dialect.migrate(migrations, db.session, config);
11+
}

drizzle-orm/src/db0/pg/session.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import type { Database, Primitive } from 'db0';
2+
import { type Cache, NoopCache } from '~/cache/core/cache.ts';
3+
import type { WithCacheConfig } from '~/cache/core/types.ts';
4+
import { entityKind } from '~/entity.ts';
5+
import type { Logger } from '~/logger.ts';
6+
import { NoopLogger } from '~/logger.ts';
7+
import type { PgDialect } from '~/pg-core/dialect.ts';
8+
import { PgTransaction } from '~/pg-core/index.ts';
9+
import type { SelectedFieldsOrdered } from '~/pg-core/query-builders/select.types.ts';
10+
import type { PgQueryResultHKT, PgTransactionConfig, PreparedQueryConfig } from '~/pg-core/session.ts';
11+
import { PgPreparedQuery, PgSession } from '~/pg-core/session.ts';
12+
import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
13+
import { fillPlaceholders, type Query, type SQL, sql } from '~/sql/sql.ts';
14+
import { mapResultRow } from '~/utils.ts';
15+
16+
export interface Db0PgSessionOptions {
17+
logger?: Logger;
18+
cache?: Cache;
19+
}
20+
21+
export interface Db0PgQueryResult {
22+
rows: unknown[];
23+
rowCount: number;
24+
}
25+
26+
export class Db0PgPreparedQuery<T extends PreparedQueryConfig = PreparedQueryConfig> extends PgPreparedQuery<T> {
27+
static override readonly [entityKind]: string = 'Db0PgPreparedQuery';
28+
29+
constructor(
30+
private client: Database,
31+
private queryString: string,
32+
private params: unknown[],
33+
private logger: Logger,
34+
cache: Cache,
35+
queryMetadata: { type: 'select' | 'update' | 'delete' | 'insert'; tables: string[] } | undefined,
36+
cacheConfig: WithCacheConfig | undefined,
37+
private fields: SelectedFieldsOrdered | undefined,
38+
name: string | undefined,
39+
private _isResponseInArrayMode: boolean,
40+
private customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => T['execute'],
41+
) {
42+
super({ sql: queryString, params }, cache, queryMetadata, cacheConfig);
43+
}
44+
45+
async execute(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['execute']> {
46+
const params = fillPlaceholders(this.params, placeholderValues) as Primitive[];
47+
this.logger.logQuery(this.queryString, params);
48+
49+
const { fields, client, customResultMapper, queryString, joinsNotNullableMap } = this;
50+
51+
if (!fields && !customResultMapper) {
52+
return await this.queryWithCache(queryString, params, async () => {
53+
const stmt = client.prepare(queryString);
54+
const rows = await stmt.all(...params) as unknown[];
55+
return { rows, rowCount: rows.length } as T['execute'];
56+
});
57+
}
58+
59+
// db0 doesn't have array mode, so we get objects and convert to arrays
60+
return await this.queryWithCache(queryString, params, async () => {
61+
const stmt = client.prepare(queryString);
62+
const rows = await stmt.all(...params) as Record<string, unknown>[];
63+
const arrayRows = rows.map((row) => Object.values(row));
64+
65+
if (customResultMapper) {
66+
return customResultMapper(arrayRows);
67+
}
68+
69+
return arrayRows.map((row) => mapResultRow<T['execute']>(fields!, row, joinsNotNullableMap));
70+
});
71+
}
72+
73+
async all(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['all']> {
74+
const params = fillPlaceholders(this.params, placeholderValues) as Primitive[];
75+
this.logger.logQuery(this.queryString, params);
76+
return await this.queryWithCache(this.queryString, params, async () => {
77+
const stmt = this.client.prepare(this.queryString);
78+
return stmt.all(...params) as Promise<T['all']>;
79+
});
80+
}
81+
82+
/** @internal */
83+
isResponseInArrayMode(): boolean {
84+
return this._isResponseInArrayMode;
85+
}
86+
}
87+
88+
export class Db0PgSession<
89+
TFullSchema extends Record<string, unknown>,
90+
TSchema extends TablesRelationalConfig,
91+
> extends PgSession<Db0PgQueryResultHKT, TFullSchema, TSchema> {
92+
static override readonly [entityKind]: string = 'Db0PgSession';
93+
94+
private logger: Logger;
95+
private cache: Cache;
96+
97+
constructor(
98+
private client: Database,
99+
dialect: PgDialect,
100+
private schema: RelationalSchemaConfig<TSchema> | undefined,
101+
private options: Db0PgSessionOptions = {},
102+
) {
103+
super(dialect);
104+
this.logger = options.logger ?? new NoopLogger();
105+
this.cache = options.cache ?? new NoopCache();
106+
}
107+
108+
prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
109+
query: Query,
110+
fields: SelectedFieldsOrdered | undefined,
111+
name: string | undefined,
112+
isResponseInArrayMode: boolean,
113+
customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => T['execute'],
114+
queryMetadata?: { type: 'select' | 'update' | 'delete' | 'insert'; tables: string[] },
115+
cacheConfig?: WithCacheConfig,
116+
): PgPreparedQuery<T> {
117+
return new Db0PgPreparedQuery(
118+
this.client,
119+
query.sql,
120+
query.params,
121+
this.logger,
122+
this.cache,
123+
queryMetadata,
124+
cacheConfig,
125+
fields,
126+
name,
127+
isResponseInArrayMode,
128+
customResultMapper,
129+
);
130+
}
131+
132+
override async transaction<T>(
133+
transaction: (tx: Db0PgTransaction<TFullSchema, TSchema>) => Promise<T>,
134+
config?: PgTransactionConfig,
135+
): Promise<T> {
136+
const tx = new Db0PgTransaction<TFullSchema, TSchema>(this.dialect, this, this.schema);
137+
138+
let beginSql = 'begin';
139+
if (config) {
140+
const chunks: string[] = [];
141+
if (config.isolationLevel) {
142+
chunks.push(`isolation level ${config.isolationLevel}`);
143+
}
144+
if (config.accessMode) {
145+
chunks.push(config.accessMode);
146+
}
147+
if (typeof config.deferrable === 'boolean') {
148+
chunks.push(config.deferrable ? 'deferrable' : 'not deferrable');
149+
}
150+
if (chunks.length > 0) {
151+
beginSql = `begin ${chunks.join(' ')}`;
152+
}
153+
}
154+
155+
await this.execute(sql.raw(beginSql));
156+
try {
157+
const result = await transaction(tx);
158+
await this.execute(sql`commit`);
159+
return result;
160+
} catch (err) {
161+
await this.execute(sql`rollback`);
162+
throw err;
163+
}
164+
}
165+
166+
override async count(countSql: SQL): Promise<number> {
167+
const res = await this.execute<Db0PgQueryResult>(countSql);
168+
return Number((res.rows[0] as Record<string, unknown>)['count']);
169+
}
170+
}
171+
172+
export class Db0PgTransaction<
173+
TFullSchema extends Record<string, unknown>,
174+
TSchema extends TablesRelationalConfig,
175+
> extends PgTransaction<Db0PgQueryResultHKT, TFullSchema, TSchema> {
176+
static override readonly [entityKind]: string = 'Db0PgTransaction';
177+
178+
override async transaction<T>(
179+
transaction: (tx: Db0PgTransaction<TFullSchema, TSchema>) => Promise<T>,
180+
): Promise<T> {
181+
const savepointName = `sp${this.nestedIndex + 1}`;
182+
const tx = new Db0PgTransaction<TFullSchema, TSchema>(this.dialect, this.session, this.schema, this.nestedIndex + 1);
183+
await tx.execute(sql.raw(`savepoint ${savepointName}`));
184+
try {
185+
const result = await transaction(tx);
186+
await tx.execute(sql.raw(`release savepoint ${savepointName}`));
187+
return result;
188+
} catch (err) {
189+
await tx.execute(sql.raw(`rollback to savepoint ${savepointName}`));
190+
throw err;
191+
}
192+
}
193+
}
194+
195+
export interface Db0PgQueryResultHKT extends PgQueryResultHKT {
196+
type: Db0PgQueryResult;
197+
}

0 commit comments

Comments
 (0)