Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SQLite] Add OP-SQLite driver #1824

Merged
merged 5 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions drizzle-orm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@cloudflare/workers-types": ">=3",
"@libsql/client": "*",
"@neondatabase/serverless": ">=0.1",
"@op-engineering/op-sqlite": ">=2",
"@opentelemetry/api": "^1.4.1",
"@planetscale/database": ">=1",
"@types/better-sqlite3": "*",
Expand Down Expand Up @@ -128,6 +129,9 @@
"expo-sqlite": {
"optional": true
},
"@op-engineering/op-sqlite": {
"optional": true
},
"react": {
"optional": true
},
Expand All @@ -140,6 +144,7 @@
"@cloudflare/workers-types": "^4.20230904.0",
"@libsql/client": "^0.1.6",
"@neondatabase/serverless": "^0.4.24",
"@op-engineering/op-sqlite": "^2.0.16",
"@opentelemetry/api": "^1.4.1",
"@originjs/vite-plugin-commonjs": "^1.0.3",
"@planetscale/database": "^1.16.0",
Expand Down
45 changes: 45 additions & 0 deletions drizzle-orm/src/op-sqlite/driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { OPSQLiteConnection, QueryResult } from '@op-engineering/op-sqlite';
import { DefaultLogger } from '~/logger.ts';
import {
createTableRelationsHelpers,
extractTablesRelationalConfig,
type RelationalSchemaConfig,
type TablesRelationalConfig,
} from '~/relations.ts';
import { BaseSQLiteDatabase } from '~/sqlite-core/db.ts';
import { SQLiteAsyncDialect } from '~/sqlite-core/dialect.ts';
import type { DrizzleConfig } from '~/utils.ts';
import { OPSQLiteSession } from './session.ts';

export type OPSQLiteDatabase<
TSchema extends Record<string, unknown> = Record<string, never>,
> = BaseSQLiteDatabase<'async', QueryResult, TSchema>;

export function drizzle<TSchema extends Record<string, unknown> = Record<string, never>>(
client: OPSQLiteConnection,
config: DrizzleConfig<TSchema> = {},
): OPSQLiteDatabase<TSchema> {
const dialect = new SQLiteAsyncDialect();
let logger;
if (config.logger === true) {
logger = new DefaultLogger();
} else if (config.logger !== false) {
logger = config.logger;
}

let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
if (config.schema) {
const tablesConfig = extractTablesRelationalConfig(
config.schema,
createTableRelationsHelpers,
);
schema = {
fullSchema: config.schema,
schema: tablesConfig.tables,
tableNamesMap: tablesConfig.tableNamesMap,
};
}

const session = new OPSQLiteSession(client, dialect, schema, { logger });
return new BaseSQLiteDatabase('async', dialect, session, schema) as OPSQLiteDatabase<TSchema>;
}
2 changes: 2 additions & 0 deletions drizzle-orm/src/op-sqlite/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './driver.ts';
export * from './session.ts';
99 changes: 99 additions & 0 deletions drizzle-orm/src/op-sqlite/migrator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useEffect, useReducer } from "react";
import type { MigrationMeta } from '~/migrator.ts';
import type { OPSQLiteDatabase } from './driver.ts';

interface MigrationConfig {
journal: {
entries: { idx: number; when: number; tag: string; breakpoints: boolean }[];
};
migrations: Record<string, string>;
}

async function readMigrationFiles({ journal, migrations }: MigrationConfig): Promise<MigrationMeta[]> {
const migrationQueries: MigrationMeta[] = [];

for await (const journalEntry of journal.entries) {
const query = migrations[`m${journalEntry.idx.toString().padStart(4, '0')}`];

if (!query) {
throw new Error(`Missing migration: ${journalEntry.tag}`);
}

try {
const result = query.split('--> statement-breakpoint').map((it) => {
return it;
});

migrationQueries.push({
sql: result,
bps: journalEntry.breakpoints,
folderMillis: journalEntry.when,
hash: '',
});
} catch {
throw new Error(`Failed to parse migration: ${journalEntry.tag}`);
}
}

return migrationQueries;
}

export async function migrate<TSchema extends Record<string, unknown>>(
db: OPSQLiteDatabase<TSchema>,
config: MigrationConfig,
) {
const migrations = await readMigrationFiles(config);
return db.dialect.migrate(migrations, db.session);
}

interface State {
success: boolean;
error?: Error;
}

type Action =
| { type: 'migrating' }
| { type: 'migrated'; payload: true }
| { type: 'error'; payload: Error }

export const useMigrations = (db: OPSQLiteDatabase<any>, migrations: {
journal: {
entries: { idx: number; when: number; tag: string; breakpoints: boolean }[];
};
migrations: Record<string, string>;
}): State => {
const initialState: State = {
success: false,
error: undefined,
}

const fetchReducer = (state: State, action: Action): State => {
switch (action.type) {
case 'migrating': {
return { ...initialState }
}
case 'migrated': {
return { ...initialState, success: action.payload }
}
case 'error': {
return { ...initialState, error: action.payload }
}
default: {
return state
}
}
}

const [state, dispatch] = useReducer(fetchReducer, initialState);

useEffect(() => {
dispatch({ type: 'migrating' })
migrate(db, migrations).then(() => {
dispatch({ type: 'migrated', payload: true })
}).catch((error) => {
dispatch({ type: 'error', payload: error as Error })
});
}, []);

return state;
}
158 changes: 158 additions & 0 deletions drizzle-orm/src/op-sqlite/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import type { OPSQLiteConnection, QueryResult } from '@op-engineering/op-sqlite';
import { entityKind } from '~/entity.ts';
import type { Logger } from '~/logger.ts';
import { NoopLogger } from '~/logger.ts';
import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
import { fillPlaceholders, type Query, sql } from '~/sql/sql.ts';
import type { SQLiteAsyncDialect } from '~/sqlite-core/dialect.ts';
import { SQLiteTransaction } from '~/sqlite-core/index.ts';
import type { SelectedFieldsOrdered } from '~/sqlite-core/query-builders/select.types.ts';
import {
type PreparedQueryConfig as PreparedQueryConfigBase,
SQLitePreparedQuery,
type SQLiteExecuteMethod,
SQLiteSession,
type SQLiteTransactionConfig,
} from '~/sqlite-core/session.ts';
import { mapResultRow } from '~/utils.ts';

export interface OPSQLiteSessionOptions {
logger?: Logger;
}

type PreparedQueryConfig = Omit<PreparedQueryConfigBase, 'statement' | 'run'>;

export class OPSQLiteSession<
TFullSchema extends Record<string, unknown>,
TSchema extends TablesRelationalConfig,
> extends SQLiteSession<'async', QueryResult, TFullSchema, TSchema> {
static readonly [entityKind]: string = 'OPSQLiteSession';

private logger: Logger;

constructor(
private client: OPSQLiteConnection,
dialect: SQLiteAsyncDialect,
private schema: RelationalSchemaConfig<TSchema> | undefined,
options: OPSQLiteSessionOptions = {},

) {
super(dialect);
this.logger = options.logger ?? new NoopLogger();
}

prepareQuery<T extends Omit<PreparedQueryConfig, 'run'>>(
query: Query,
fields: SelectedFieldsOrdered | undefined,
executeMethod: SQLiteExecuteMethod,
customResultMapper?: (rows: unknown[][]) => unknown,
): OPSQLitePreparedQuery<T> {
return new OPSQLitePreparedQuery(this.client, query, this.logger, fields, executeMethod, customResultMapper);
}

override transaction<T>(
transaction: (tx: OPSQLiteTransaction<TFullSchema, TSchema>) => T,
config: SQLiteTransactionConfig = {},
): T {
const tx = new OPSQLiteTransaction('async', this.dialect, this, this.schema);
this.run(sql.raw(`begin${config?.behavior ? ' ' + config.behavior : ''}`));
try {
const result = transaction(tx);
this.run(sql`commit`);
return result;
} catch (err) {
this.run(sql`rollback`);
throw err;
}
}
}

export class OPSQLiteTransaction<
TFullSchema extends Record<string, unknown>,
TSchema extends TablesRelationalConfig,
> extends SQLiteTransaction<'async', QueryResult, TFullSchema, TSchema> {
static readonly [entityKind]: string = 'OPSQLiteTransaction';

override transaction<T>(transaction: (tx: OPSQLiteTransaction<TFullSchema, TSchema>) => T): T {
const savepointName = `sp${this.nestedIndex}`;
const tx = new OPSQLiteTransaction('async', this.dialect, this.session, this.schema, this.nestedIndex + 1);
this.session.run(sql.raw(`savepoint ${savepointName}`));
try {
const result = transaction(tx);
this.session.run(sql.raw(`release savepoint ${savepointName}`));
return result;
} catch (err) {
this.session.run(sql.raw(`rollback to savepoint ${savepointName}`));
throw err;
}
}
}

export class OPSQLitePreparedQuery<T extends PreparedQueryConfig = PreparedQueryConfig> extends SQLitePreparedQuery<
{ type: 'async'; run: QueryResult; all: T['all']; get: T['get']; values: T['values']; execute: T['execute'] }
> {
static readonly [entityKind]: string = 'OPSQLitePreparedQuery';

constructor(
private client: OPSQLiteConnection,
query: Query,
private logger: Logger,
private fields: SelectedFieldsOrdered | undefined,
executeMethod: SQLiteExecuteMethod,
private customResultMapper?: (rows: unknown[][]) => unknown,
) {
super('sync', executeMethod, query);
}

run(placeholderValues?: Record<string, unknown>): Promise<QueryResult> {
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
this.logger.logQuery(this.query.sql, params);

return this.client.executeAsync(this.query.sql, params);
}

async all(placeholderValues?: Record<string, unknown>): Promise<T['all']> {
const { fields, joinsNotNullableMap, query, logger, customResultMapper, client } = this;
if (!fields && !customResultMapper) {
const params = fillPlaceholders(query.params, placeholderValues ?? {});
logger.logQuery(query.sql, params);

return client.execute(query.sql, params).rows?._array || [];
}

const rows = await this.values(placeholderValues) as unknown[][];
if (customResultMapper) {
return customResultMapper(rows) as T['all'];
}
return rows.map((row) => mapResultRow(fields!, row, joinsNotNullableMap));
}

async get(placeholderValues?: Record<string, unknown>): Promise<T['get']> {
const { fields, joinsNotNullableMap, customResultMapper, query, logger, client } = this;
const params = fillPlaceholders(query.params, placeholderValues ?? {});
logger.logQuery(query.sql, params);
if (!fields && !customResultMapper) {
const rows = client.execute(query.sql, params).rows?._array || [];
return rows[0];
}

const rows = await this.values(placeholderValues) as unknown[][];
const row = rows[0];

if (!row) {
return undefined;
}

if (customResultMapper) {
return customResultMapper(rows) as T['get'];
}

return mapResultRow(fields!, row, joinsNotNullableMap);
}

values(placeholderValues?: Record<string, unknown>): Promise<T['values']> {
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
this.logger.logQuery(this.query.sql, params);
return this.client.executeRawAsync(this.query.sql, params);
}
}
8 changes: 5 additions & 3 deletions drizzle-orm/src/sqlite-core/dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,11 +771,13 @@ export class SQLiteAsyncDialect extends SQLiteDialect {
async migrate(
migrations: MigrationMeta[],
session: SQLiteSession<'async', unknown, Record<string, unknown>, TablesRelationalConfig>,
config: string | MigrationConfig,
config?: string | MigrationConfig,
): Promise<void> {
const migrationsTable = typeof config === 'string'
const migrationsTable = config === undefined
? '__drizzle_migrations'
: config.migrationsTable ?? '__drizzle_migrations';
: typeof config === 'string'
? '__drizzle_migrations'
: config.migrationsTable ?? '__drizzle_migrations';

const migrationTableCreate = sql`
CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} (
Expand Down
Loading
Loading