Skip to content

Commit

Permalink
Merge pull request #1984 from drizzle-team/beta
Browse files Browse the repository at this point in the history
Beta
  • Loading branch information
AndriiSherman committed Mar 8, 2024
2 parents 9322cf2 + d9db4a3 commit bfc757f
Show file tree
Hide file tree
Showing 9 changed files with 2,551 additions and 1,001 deletions.
23 changes: 23 additions & 0 deletions changelogs/drizzle-orm/0.30.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## New Features

### 馃帀 OP-SQLite driver Support

Usage Example

```ts
import { open } from '@op-engineering/op-sqlite';
import { drizzle } from 'drizzle-orm/op-sqlite';

const opsqlite = open({
name: 'myDB',
});
const db = drizzle(opsqlite);

await db.select().from(users);
```

For more usage and setup details, please check our [op-sqlite docs](http://orm.drizzle.team/docs/get-started-sqlite#op-sqlite)

### Bug fixes

- Migration hook fixed for Expo driver
8 changes: 6 additions & 2 deletions drizzle-orm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "drizzle-orm",
"version": "0.30.0",
"version": "0.30.1",
"description": "Drizzle ORM package for SQL databases",
"type": "module",
"scripts": {
Expand Down Expand Up @@ -42,12 +42,12 @@
"bugs": {
"url": "https://github.com/drizzle-team/drizzle-orm/issues"
},
"homepage": "https://github.com/drizzle-team/drizzle-orm#readme",
"peerDependencies": {
"@aws-sdk/client-rds-data": ">=3",
"@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 +128,9 @@
"expo-sqlite": {
"optional": true
},
"@op-engineering/op-sqlite": {
"optional": true
},
"react": {
"optional": true
},
Expand All @@ -140,6 +143,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
14 changes: 6 additions & 8 deletions drizzle-orm/src/expo-sqlite/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,12 @@ export const useMigrations = (db: ExpoSQLiteDatabase<any>, migrations: {
const [state, dispatch] = useReducer(fetchReducer, initialState);

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

return state;
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);
}
}

0 comments on commit bfc757f

Please sign in to comment.