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

fix: Drizzle transactions not using lock context for React-Native #470

Merged
merged 3 commits into from
Jan 17, 2025
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 .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
Loading