Skip to content

Commit 86a753f

Browse files
authored
fix: Drizzle transactions not using lock context for React-Native (#470)
1 parent 2289dfb commit 86a753f

10 files changed

+105
-48
lines changed

.changeset/lovely-ligers-sleep.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/drizzle-driver': patch
3+
---
4+
5+
Fixed Drizzle transactions breaking for react-native projects, correctly using lock context for transactions.

packages/drizzle-driver/src/index.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { wrapPowerSyncWithDrizzle, type DrizzleQuery, type PowerSyncSQLiteDatabase } from './sqlite/db';
1+
import {
2+
wrapPowerSyncWithDrizzle,
3+
type DrizzleQuery,
4+
type PowerSyncSQLiteDatabase
5+
} from './sqlite/PowerSyncSQLiteDatabase';
26
import { toCompilableQuery } from './utils/compilableQuery';
37
import {
48
DrizzleAppSchema,
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { AbstractPowerSyncDatabase, QueryResult } from '@powersync/common';
1+
import { LockContext, QueryResult } from '@powersync/common';
22
import { entityKind } from 'drizzle-orm/entity';
33
import type { Logger } from 'drizzle-orm/logger';
44
import { NoopLogger } from 'drizzle-orm/logger';
55
import type { RelationalSchemaConfig, TablesRelationalConfig } from 'drizzle-orm/relations';
6-
import { type Query, sql } from 'drizzle-orm/sql/sql';
6+
import { type Query } from 'drizzle-orm/sql/sql';
77
import type { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core/dialect';
88
import type { SelectedFieldsOrdered } from 'drizzle-orm/sqlite-core/query-builders/select.types';
99
import {
@@ -13,7 +13,7 @@ import {
1313
SQLiteTransaction,
1414
type SQLiteTransactionConfig
1515
} from 'drizzle-orm/sqlite-core/session';
16-
import { PowerSyncSQLitePreparedQuery } from './sqlite-query';
16+
import { PowerSyncSQLitePreparedQuery } from './PowerSyncSQLitePreparedQuery';
1717

1818
export interface PowerSyncSQLiteSessionOptions {
1919
logger?: Logger;
@@ -30,19 +30,19 @@ export class PowerSyncSQLiteTransaction<
3030
static readonly [entityKind]: string = 'PowerSyncSQLiteTransaction';
3131
}
3232

33-
export class PowerSyncSQLiteSession<
33+
export class PowerSyncSQLiteBaseSession<
3434
TFullSchema extends Record<string, unknown>,
3535
TSchema extends TablesRelationalConfig
3636
> extends SQLiteSession<'async', QueryResult, TFullSchema, TSchema> {
37-
static readonly [entityKind]: string = 'PowerSyncSQLiteSession';
37+
static readonly [entityKind]: string = 'PowerSyncSQLiteBaseSession';
3838

39-
private logger: Logger;
39+
protected logger: Logger;
4040

4141
constructor(
42-
private db: AbstractPowerSyncDatabase,
43-
dialect: SQLiteAsyncDialect,
44-
private schema: RelationalSchemaConfig<TSchema> | undefined,
45-
options: PowerSyncSQLiteSessionOptions = {}
42+
protected db: LockContext,
43+
protected dialect: SQLiteAsyncDialect,
44+
protected schema: RelationalSchemaConfig<TSchema> | undefined,
45+
protected options: PowerSyncSQLiteSessionOptions = {}
4646
) {
4747
super(dialect);
4848
this.logger = options.logger ?? new NoopLogger();
@@ -66,33 +66,10 @@ export class PowerSyncSQLiteSession<
6666
);
6767
}
6868

69-
override transaction<T>(
70-
transaction: (tx: PowerSyncSQLiteTransaction<TFullSchema, TSchema>) => T,
71-
config: PowerSyncSQLiteTransactionConfig = {}
69+
transaction<T>(
70+
_transaction: (tx: PowerSyncSQLiteTransaction<TFullSchema, TSchema>) => T,
71+
_config: PowerSyncSQLiteTransactionConfig = {}
7272
): T {
73-
const { accessMode = 'read write' } = config;
74-
75-
if (accessMode === 'read only') {
76-
return this.db.readLock(async () => this.internalTransaction(transaction, config)) as T;
77-
}
78-
79-
return this.db.writeLock(async () => this.internalTransaction(transaction, config)) as T;
80-
}
81-
82-
async internalTransaction<T>(
83-
transaction: (tx: PowerSyncSQLiteTransaction<TFullSchema, TSchema>) => T,
84-
config: PowerSyncSQLiteTransactionConfig = {}
85-
): Promise<T> {
86-
const tx = new PowerSyncSQLiteTransaction('async', (this as any).dialect, this, this.schema);
87-
88-
await this.run(sql.raw(`begin${config?.behavior ? ' ' + config.behavior : ''}`));
89-
try {
90-
const result = await transaction(tx);
91-
await this.run(sql`commit`);
92-
return result;
93-
} catch (err) {
94-
await this.run(sql`rollback`);
95-
throw err;
96-
}
73+
throw new Error('Nested transactions are not supported');
9774
}
9875
}

packages/drizzle-driver/src/sqlite/db.ts renamed to packages/drizzle-driver/src/sqlite/PowerSyncSQLiteDatabase.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core/db';
1919
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core/dialect';
2020
import type { DrizzleConfig } from 'drizzle-orm/utils';
2121
import { toCompilableQuery } from './../utils/compilableQuery';
22-
import { PowerSyncSQLiteSession, PowerSyncSQLiteTransactionConfig } from './sqlite-session';
22+
import { PowerSyncSQLiteSession } from './PowerSyncSQLiteSession';
23+
import { PowerSyncSQLiteTransactionConfig } from './PowerSyncSQLiteBaseSession';
2324

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

@@ -55,7 +56,7 @@ export class PowerSyncSQLiteDatabase<
5556
this.db = db;
5657
}
5758

58-
override transaction<T>(
59+
transaction<T>(
5960
transaction: (
6061
tx: SQLiteTransaction<'async', QueryResult, TSchema, ExtractTablesWithRelations<TSchema>>
6162
) => Promise<T>,

packages/drizzle-driver/src/sqlite/sqlite-query.ts renamed to packages/drizzle-driver/src/sqlite/PowerSyncSQLitePreparedQuery.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AbstractPowerSyncDatabase, QueryResult } from '@powersync/common';
1+
import { LockContext, QueryResult } from '@powersync/common';
22
import { Column, DriverValueDecoder, getTableName, SQL } from 'drizzle-orm';
33
import { entityKind, is } from 'drizzle-orm/entity';
44
import type { Logger } from 'drizzle-orm/logger';
@@ -26,7 +26,7 @@ export class PowerSyncSQLitePreparedQuery<
2626
static readonly [entityKind]: string = 'PowerSyncSQLitePreparedQuery';
2727

2828
constructor(
29-
private db: AbstractPowerSyncDatabase,
29+
private db: LockContext,
3030
query: Query,
3131
private logger: Logger,
3232
private fields: SelectedFieldsOrdered | undefined,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { AbstractPowerSyncDatabase, DBAdapter } from '@powersync/common';
2+
import { entityKind } from 'drizzle-orm/entity';
3+
import type { RelationalSchemaConfig, TablesRelationalConfig } from 'drizzle-orm/relations';
4+
import { type Query } from 'drizzle-orm/sql/sql';
5+
import type { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core/dialect';
6+
import type { SelectedFieldsOrdered } from 'drizzle-orm/sqlite-core/query-builders/select.types';
7+
import {
8+
type PreparedQueryConfig as PreparedQueryConfigBase,
9+
type SQLiteExecuteMethod
10+
} from 'drizzle-orm/sqlite-core/session';
11+
import { PowerSyncSQLitePreparedQuery } from './PowerSyncSQLitePreparedQuery';
12+
import {
13+
PowerSyncSQLiteSessionOptions,
14+
PowerSyncSQLiteTransaction,
15+
PowerSyncSQLiteTransactionConfig,
16+
PowerSyncSQLiteBaseSession
17+
} from './PowerSyncSQLiteBaseSession';
18+
19+
export class PowerSyncSQLiteSession<
20+
TFullSchema extends Record<string, unknown>,
21+
TSchema extends TablesRelationalConfig
22+
> extends PowerSyncSQLiteBaseSession<TFullSchema, TSchema> {
23+
static readonly [entityKind]: string = 'PowerSyncSQLiteSession';
24+
protected client: AbstractPowerSyncDatabase;
25+
constructor(
26+
db: AbstractPowerSyncDatabase,
27+
dialect: SQLiteAsyncDialect,
28+
schema: RelationalSchemaConfig<TSchema> | undefined,
29+
options: PowerSyncSQLiteSessionOptions = {}
30+
) {
31+
super(db, dialect, schema, options);
32+
this.client = db;
33+
}
34+
35+
transaction<T>(
36+
transaction: (tx: PowerSyncSQLiteTransaction<TFullSchema, TSchema>) => T,
37+
config: PowerSyncSQLiteTransactionConfig = {}
38+
): T {
39+
const { accessMode = 'read write' } = config;
40+
41+
if (accessMode === 'read only') {
42+
return this.client.readLock(async (ctx) => this.internalTransaction(ctx, transaction, config)) as T;
43+
}
44+
45+
return this.client.writeLock(async (ctx) => this.internalTransaction(ctx, transaction, config)) as T;
46+
}
47+
48+
protected async internalTransaction<T>(
49+
connection: DBAdapter,
50+
fn: (tx: PowerSyncSQLiteTransaction<TFullSchema, TSchema>) => T,
51+
config: PowerSyncSQLiteTransactionConfig = {}
52+
): Promise<T> {
53+
const tx = new PowerSyncSQLiteTransaction<TFullSchema, TSchema>(
54+
'async',
55+
(this as any).dialect,
56+
new PowerSyncSQLiteBaseSession(connection, this.dialect, this.schema, this.options),
57+
this.schema
58+
);
59+
60+
await connection.execute(`begin${config?.behavior ? ' ' + config.behavior : ''}`);
61+
try {
62+
const result = await fn(tx);
63+
await connection.execute(`commit`);
64+
return result;
65+
} catch (err) {
66+
await connection.execute(`rollback`);
67+
throw err;
68+
}
69+
}
70+
}

packages/drizzle-driver/tests/setup/db.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Schema, PowerSyncDatabase, column, Table, AbstractPowerSyncDatabase } from '@powersync/web';
1+
import { AbstractPowerSyncDatabase, column, PowerSyncDatabase, Schema, Table } from '@powersync/web';
22
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
3-
import { wrapPowerSyncWithDrizzle, PowerSyncSQLiteDatabase } from '../../src/sqlite/db';
3+
import { wrapPowerSyncWithDrizzle } from '../../src/sqlite/PowerSyncSQLiteDatabase';
44

55
const users = new Table({
66
name: column.text

packages/drizzle-driver/tests/sqlite/db.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AbstractPowerSyncDatabase } from '@powersync/common';
22
import { eq, sql } from 'drizzle-orm';
33
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4-
import * as SUT from '../../src/sqlite/db';
4+
import * as SUT from '../../src/sqlite/PowerSyncSQLiteDatabase';
55
import { DrizzleSchema, drizzleUsers, getDrizzleDb, getPowerSyncDb } from '../setup/db';
66

77
describe('Database operations', () => {

packages/drizzle-driver/tests/sqlite/sqlite-query.test.ts renamed to packages/drizzle-driver/tests/sqlite/query.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AbstractPowerSyncDatabase } from '@powersync/web';
22
import { Query } from 'drizzle-orm/sql/sql';
3-
import { PowerSyncSQLiteDatabase } from '../../src/sqlite/db';
4-
import { PowerSyncSQLitePreparedQuery } from '../../src/sqlite/sqlite-query';
3+
import { PowerSyncSQLiteDatabase } from '../../src/sqlite/PowerSyncSQLiteDatabase';
4+
import { PowerSyncSQLitePreparedQuery } from '../../src/sqlite/PowerSyncSQLitePreparedQuery';
55
import { DrizzleSchema, drizzleUsers, getDrizzleDb, getPowerSyncDb } from '../setup/db';
66
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
77

packages/drizzle-driver/tests/sqlite/watch.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { PowerSyncDatabase } from '@powersync/web';
33
import { count, eq, relations, sql } from 'drizzle-orm';
44
import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
55
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6-
import * as SUT from '../../src/sqlite/db';
6+
import * as SUT from '../../src/sqlite/PowerSyncSQLiteDatabase';
77

88
vi.useRealTimers();
99

0 commit comments

Comments
 (0)