Skip to content

Commit

Permalink
fix: Drizzle transactions not using lock context for React-Native (#470)
Browse files Browse the repository at this point in the history
  • Loading branch information
Chriztiaan authored Jan 17, 2025
1 parent 2289dfb commit 86a753f
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 48 deletions.
5 changes: 5 additions & 0 deletions .changeset/lovely-ligers-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/drizzle-driver': patch
---

Fixed Drizzle transactions breaking for react-native projects, correctly using lock context for transactions.
6 changes: 5 additions & 1 deletion packages/drizzle-driver/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { wrapPowerSyncWithDrizzle, type DrizzleQuery, type PowerSyncSQLiteDatabase } from './sqlite/db';
import {
wrapPowerSyncWithDrizzle,
type DrizzleQuery,
type PowerSyncSQLiteDatabase
} from './sqlite/PowerSyncSQLiteDatabase';
import { toCompilableQuery } from './utils/compilableQuery';
import {
DrizzleAppSchema,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { AbstractPowerSyncDatabase, QueryResult } from '@powersync/common';
import { LockContext, QueryResult } from '@powersync/common';
import { entityKind } from 'drizzle-orm/entity';
import type { Logger } from 'drizzle-orm/logger';
import { NoopLogger } from 'drizzle-orm/logger';
import type { RelationalSchemaConfig, TablesRelationalConfig } from 'drizzle-orm/relations';
import { type Query, sql } from 'drizzle-orm/sql/sql';
import { type Query } from 'drizzle-orm/sql/sql';
import type { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core/dialect';
import type { SelectedFieldsOrdered } from 'drizzle-orm/sqlite-core/query-builders/select.types';
import {
Expand All @@ -13,7 +13,7 @@ import {
SQLiteTransaction,
type SQLiteTransactionConfig
} from 'drizzle-orm/sqlite-core/session';
import { PowerSyncSQLitePreparedQuery } from './sqlite-query';
import { PowerSyncSQLitePreparedQuery } from './PowerSyncSQLitePreparedQuery';

export interface PowerSyncSQLiteSessionOptions {
logger?: Logger;
Expand All @@ -30,19 +30,19 @@ export class PowerSyncSQLiteTransaction<
static readonly [entityKind]: string = 'PowerSyncSQLiteTransaction';
}

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

private logger: Logger;
protected logger: Logger;

constructor(
private db: AbstractPowerSyncDatabase,
dialect: SQLiteAsyncDialect,
private schema: RelationalSchemaConfig<TSchema> | undefined,
options: PowerSyncSQLiteSessionOptions = {}
protected db: LockContext,
protected dialect: SQLiteAsyncDialect,
protected schema: RelationalSchemaConfig<TSchema> | undefined,
protected options: PowerSyncSQLiteSessionOptions = {}
) {
super(dialect);
this.logger = options.logger ?? new NoopLogger();
Expand All @@ -66,33 +66,10 @@ export class PowerSyncSQLiteSession<
);
}

override transaction<T>(
transaction: (tx: PowerSyncSQLiteTransaction<TFullSchema, TSchema>) => T,
config: PowerSyncSQLiteTransactionConfig = {}
transaction<T>(
_transaction: (tx: PowerSyncSQLiteTransaction<TFullSchema, TSchema>) => T,
_config: PowerSyncSQLiteTransactionConfig = {}
): T {
const { accessMode = 'read write' } = config;

if (accessMode === 'read only') {
return this.db.readLock(async () => this.internalTransaction(transaction, config)) as T;
}

return this.db.writeLock(async () => this.internalTransaction(transaction, config)) as T;
}

async internalTransaction<T>(
transaction: (tx: PowerSyncSQLiteTransaction<TFullSchema, TSchema>) => T,
config: PowerSyncSQLiteTransactionConfig = {}
): Promise<T> {
const tx = new PowerSyncSQLiteTransaction('async', (this as any).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;
}
throw new Error('Nested transactions are not supported');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core/db';
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core/dialect';
import type { DrizzleConfig } from 'drizzle-orm/utils';
import { toCompilableQuery } from './../utils/compilableQuery';
import { PowerSyncSQLiteSession, PowerSyncSQLiteTransactionConfig } from './sqlite-session';
import { PowerSyncSQLiteSession } from './PowerSyncSQLiteSession';
import { PowerSyncSQLiteTransactionConfig } from './PowerSyncSQLiteBaseSession';

export type DrizzleQuery<T> = { toSQL(): Query; execute(): Promise<T | T[]> };

Expand Down Expand Up @@ -55,7 +56,7 @@ export class PowerSyncSQLiteDatabase<
this.db = db;
}

override transaction<T>(
transaction<T>(
transaction: (
tx: SQLiteTransaction<'async', QueryResult, TSchema, ExtractTablesWithRelations<TSchema>>
) => Promise<T>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AbstractPowerSyncDatabase, QueryResult } from '@powersync/common';
import { LockContext, QueryResult } from '@powersync/common';
import { Column, DriverValueDecoder, getTableName, SQL } from 'drizzle-orm';
import { entityKind, is } from 'drizzle-orm/entity';
import type { Logger } from 'drizzle-orm/logger';
Expand Down Expand Up @@ -26,7 +26,7 @@ export class PowerSyncSQLitePreparedQuery<
static readonly [entityKind]: string = 'PowerSyncSQLitePreparedQuery';

constructor(
private db: AbstractPowerSyncDatabase,
private db: LockContext,
query: Query,
private logger: Logger,
private fields: SelectedFieldsOrdered | undefined,
Expand Down
70 changes: 70 additions & 0 deletions packages/drizzle-driver/src/sqlite/PowerSyncSQLiteSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { AbstractPowerSyncDatabase, DBAdapter } from '@powersync/common';
import { entityKind } from 'drizzle-orm/entity';
import type { RelationalSchemaConfig, TablesRelationalConfig } from 'drizzle-orm/relations';
import { type Query } from 'drizzle-orm/sql/sql';
import type { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core/dialect';
import type { SelectedFieldsOrdered } from 'drizzle-orm/sqlite-core/query-builders/select.types';
import {
type PreparedQueryConfig as PreparedQueryConfigBase,
type SQLiteExecuteMethod
} from 'drizzle-orm/sqlite-core/session';
import { PowerSyncSQLitePreparedQuery } from './PowerSyncSQLitePreparedQuery';
import {
PowerSyncSQLiteSessionOptions,
PowerSyncSQLiteTransaction,
PowerSyncSQLiteTransactionConfig,
PowerSyncSQLiteBaseSession
} from './PowerSyncSQLiteBaseSession';

export class PowerSyncSQLiteSession<
TFullSchema extends Record<string, unknown>,
TSchema extends TablesRelationalConfig
> extends PowerSyncSQLiteBaseSession<TFullSchema, TSchema> {
static readonly [entityKind]: string = 'PowerSyncSQLiteSession';
protected client: AbstractPowerSyncDatabase;
constructor(
db: AbstractPowerSyncDatabase,
dialect: SQLiteAsyncDialect,
schema: RelationalSchemaConfig<TSchema> | undefined,
options: PowerSyncSQLiteSessionOptions = {}
) {
super(db, dialect, schema, options);
this.client = db;
}

transaction<T>(
transaction: (tx: PowerSyncSQLiteTransaction<TFullSchema, TSchema>) => T,
config: PowerSyncSQLiteTransactionConfig = {}
): T {
const { accessMode = 'read write' } = config;

if (accessMode === 'read only') {
return this.client.readLock(async (ctx) => this.internalTransaction(ctx, transaction, config)) as T;
}

return this.client.writeLock(async (ctx) => this.internalTransaction(ctx, transaction, config)) as T;
}

protected async internalTransaction<T>(
connection: DBAdapter,
fn: (tx: PowerSyncSQLiteTransaction<TFullSchema, TSchema>) => T,
config: PowerSyncSQLiteTransactionConfig = {}
): Promise<T> {
const tx = new PowerSyncSQLiteTransaction<TFullSchema, TSchema>(
'async',
(this as any).dialect,
new PowerSyncSQLiteBaseSession(connection, this.dialect, this.schema, this.options),
this.schema
);

await connection.execute(`begin${config?.behavior ? ' ' + config.behavior : ''}`);
try {
const result = await fn(tx);
await connection.execute(`commit`);
return result;
} catch (err) {
await connection.execute(`rollback`);
throw err;
}
}
}
4 changes: 2 additions & 2 deletions packages/drizzle-driver/tests/setup/db.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Schema, PowerSyncDatabase, column, Table, AbstractPowerSyncDatabase } from '@powersync/web';
import { AbstractPowerSyncDatabase, column, PowerSyncDatabase, Schema, Table } from '@powersync/web';
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { wrapPowerSyncWithDrizzle, PowerSyncSQLiteDatabase } from '../../src/sqlite/db';
import { wrapPowerSyncWithDrizzle } from '../../src/sqlite/PowerSyncSQLiteDatabase';

const users = new Table({
name: column.text
Expand Down
2 changes: 1 addition & 1 deletion packages/drizzle-driver/tests/sqlite/db.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AbstractPowerSyncDatabase } from '@powersync/common';
import { eq, sql } from 'drizzle-orm';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import * as SUT from '../../src/sqlite/db';
import * as SUT from '../../src/sqlite/PowerSyncSQLiteDatabase';
import { DrizzleSchema, drizzleUsers, getDrizzleDb, getPowerSyncDb } from '../setup/db';

describe('Database operations', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AbstractPowerSyncDatabase } from '@powersync/web';
import { Query } from 'drizzle-orm/sql/sql';
import { PowerSyncSQLiteDatabase } from '../../src/sqlite/db';
import { PowerSyncSQLitePreparedQuery } from '../../src/sqlite/sqlite-query';
import { PowerSyncSQLiteDatabase } from '../../src/sqlite/PowerSyncSQLiteDatabase';
import { PowerSyncSQLitePreparedQuery } from '../../src/sqlite/PowerSyncSQLitePreparedQuery';
import { DrizzleSchema, drizzleUsers, getDrizzleDb, getPowerSyncDb } from '../setup/db';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

Expand Down
2 changes: 1 addition & 1 deletion packages/drizzle-driver/tests/sqlite/watch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { PowerSyncDatabase } from '@powersync/web';
import { count, eq, relations, sql } from 'drizzle-orm';
import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as SUT from '../../src/sqlite/db';
import * as SUT from '../../src/sqlite/PowerSyncSQLiteDatabase';

vi.useRealTimers();

Expand Down

0 comments on commit 86a753f

Please sign in to comment.