Skip to content
Open
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
53 changes: 53 additions & 0 deletions drizzle-orm/src/expo-sqlite/async/driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { SQLiteDatabase, SQLiteRunResult } from 'expo-sqlite';
import { entityKind } from '~/entity.ts';
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 { ExpoSQLiteAsyncSession } from './session.ts';

export class ExpoSQLiteAsyncDatabase<TSchema extends Record<string, unknown> = Record<string, never>>
extends BaseSQLiteDatabase<'async', SQLiteRunResult, TSchema>
{
static override readonly [entityKind]: string = 'ExpoSQLiteAsyncDatabase';
}

export function drizzle<TSchema extends Record<string, unknown> = Record<string, never>>(
client: SQLiteDatabase,
config: DrizzleConfig<TSchema> = {},
): ExpoSQLiteAsyncDatabase<TSchema> & {
$client: SQLiteDatabase;
} {
const dialect = new SQLiteAsyncDialect({ casing: config.casing });
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 ExpoSQLiteAsyncSession(client, dialect, schema, { logger });
const db = new ExpoSQLiteAsyncDatabase('async', dialect, session, schema) as ExpoSQLiteAsyncDatabase<TSchema>;
(<any> db).$client = client;

return db as any;
}
2 changes: 2 additions & 0 deletions drizzle-orm/src/expo-sqlite/async/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/expo-sqlite/async/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 { ExpoSQLiteAsyncDatabase } 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: ExpoSQLiteAsyncDatabase<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: ExpoSQLiteAsyncDatabase<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 as any).then(() => {
dispatch({ type: 'migrated', payload: true });
}).catch((error) => {
dispatch({ type: 'error', payload: error as Error });
});
}, []);

return state;
};
188 changes: 188 additions & 0 deletions drizzle-orm/src/expo-sqlite/async/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import type { SQLiteDatabase, SQLiteRunResult, SQLiteStatement } from 'expo-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,
type SQLiteExecuteMethod,
SQLitePreparedQuery,
SQLiteSession,
type SQLiteTransactionConfig,
} from '~/sqlite-core/session.ts';
import { mapResultRow } from '~/utils.ts';

export interface ExpoSQLiteAsyncSessionOptions {
logger?: Logger;
}

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

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

private logger: Logger;

constructor(
private client: SQLiteDatabase,
dialect: SQLiteAsyncDialect,
private schema: RelationalSchemaConfig<TSchema> | undefined,
options: ExpoSQLiteAsyncSessionOptions = {},
) {
super(dialect);
this.logger = options.logger ?? new NoopLogger();
}

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

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

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

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

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

constructor(
private stmt: SQLiteStatement,
query: Query,
private logger: Logger,
private fields: SelectedFieldsOrdered | undefined,
executeMethod: SQLiteExecuteMethod,
private _isResponseInArrayMode: boolean,
private customResultMapper?: (rows: unknown[][]) => unknown,
) {
super('async', executeMethod, query);
}

async run(placeholderValues?: Record<string, unknown>): Promise<SQLiteRunResult> {
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
this.logger.logQuery(this.query.sql, params);
const result = await this.stmt.executeAsync(params as any[]);
return {
changes: result.changes,
lastInsertRowId: result.lastInsertRowId,
};
}

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

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 params = fillPlaceholders(this.query.params, placeholderValues ?? {});
this.logger.logQuery(this.query.sql, params);

const { fields, stmt, joinsNotNullableMap, customResultMapper } = this;
if (!fields && !customResultMapper) {
const result = await stmt.executeAsync(params as any[]);
return result.getFirstAsync();
}

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

async values(placeholderValues?: Record<string, unknown>): Promise<T['values']> {
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
this.logger.logQuery(this.query.sql, params);
const result = await this.stmt.executeForRawResultAsync(params as any[]);
return result.getAllAsync();
}

/** @internal */
isResponseInArrayMode(): boolean {
return this._isResponseInArrayMode;
}
}