diff --git a/.changeset/lovely-ligers-sleep.md b/.changeset/lovely-ligers-sleep.md new file mode 100644 index 000000000..d9cc2ed13 --- /dev/null +++ b/.changeset/lovely-ligers-sleep.md @@ -0,0 +1,5 @@ +--- +'@powersync/drizzle-driver': patch +--- + +Fixed Drizzle transactions breaking for react-native projects, correctly using lock context for transactions. diff --git a/packages/drizzle-driver/src/index.ts b/packages/drizzle-driver/src/index.ts index b467ea6ab..ff742fc8b 100644 --- a/packages/drizzle-driver/src/index.ts +++ b/packages/drizzle-driver/src/index.ts @@ -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, diff --git a/packages/drizzle-driver/src/sqlite/sqlite-session.ts b/packages/drizzle-driver/src/sqlite/PowerSyncSQLiteBaseSession.ts similarity index 55% rename from packages/drizzle-driver/src/sqlite/sqlite-session.ts rename to packages/drizzle-driver/src/sqlite/PowerSyncSQLiteBaseSession.ts index 49b5a241c..bc30905f6 100644 --- a/packages/drizzle-driver/src/sqlite/sqlite-session.ts +++ b/packages/drizzle-driver/src/sqlite/PowerSyncSQLiteBaseSession.ts @@ -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 { @@ -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; @@ -30,19 +30,19 @@ export class PowerSyncSQLiteTransaction< static readonly [entityKind]: string = 'PowerSyncSQLiteTransaction'; } -export class PowerSyncSQLiteSession< +export class PowerSyncSQLiteBaseSession< TFullSchema extends Record, 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 | undefined, - options: PowerSyncSQLiteSessionOptions = {} + protected db: LockContext, + protected dialect: SQLiteAsyncDialect, + protected schema: RelationalSchemaConfig | undefined, + protected options: PowerSyncSQLiteSessionOptions = {} ) { super(dialect); this.logger = options.logger ?? new NoopLogger(); @@ -66,33 +66,10 @@ export class PowerSyncSQLiteSession< ); } - override transaction( - transaction: (tx: PowerSyncSQLiteTransaction) => T, - config: PowerSyncSQLiteTransactionConfig = {} + transaction( + _transaction: (tx: PowerSyncSQLiteTransaction) => 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( - transaction: (tx: PowerSyncSQLiteTransaction) => T, - config: PowerSyncSQLiteTransactionConfig = {} - ): Promise { - 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'); } } diff --git a/packages/drizzle-driver/src/sqlite/db.ts b/packages/drizzle-driver/src/sqlite/PowerSyncSQLiteDatabase.ts similarity index 94% rename from packages/drizzle-driver/src/sqlite/db.ts rename to packages/drizzle-driver/src/sqlite/PowerSyncSQLiteDatabase.ts index af7268639..3b4b96abd 100644 --- a/packages/drizzle-driver/src/sqlite/db.ts +++ b/packages/drizzle-driver/src/sqlite/PowerSyncSQLiteDatabase.ts @@ -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 = { toSQL(): Query; execute(): Promise }; @@ -55,7 +56,7 @@ export class PowerSyncSQLiteDatabase< this.db = db; } - override transaction( + transaction( transaction: ( tx: SQLiteTransaction<'async', QueryResult, TSchema, ExtractTablesWithRelations> ) => Promise, diff --git a/packages/drizzle-driver/src/sqlite/sqlite-query.ts b/packages/drizzle-driver/src/sqlite/PowerSyncSQLitePreparedQuery.ts similarity index 98% rename from packages/drizzle-driver/src/sqlite/sqlite-query.ts rename to packages/drizzle-driver/src/sqlite/PowerSyncSQLitePreparedQuery.ts index 80f31fe75..5229caef0 100644 --- a/packages/drizzle-driver/src/sqlite/sqlite-query.ts +++ b/packages/drizzle-driver/src/sqlite/PowerSyncSQLitePreparedQuery.ts @@ -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'; @@ -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, diff --git a/packages/drizzle-driver/src/sqlite/PowerSyncSQLiteSession.ts b/packages/drizzle-driver/src/sqlite/PowerSyncSQLiteSession.ts new file mode 100644 index 000000000..786bda332 --- /dev/null +++ b/packages/drizzle-driver/src/sqlite/PowerSyncSQLiteSession.ts @@ -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, + TSchema extends TablesRelationalConfig +> extends PowerSyncSQLiteBaseSession { + static readonly [entityKind]: string = 'PowerSyncSQLiteSession'; + protected client: AbstractPowerSyncDatabase; + constructor( + db: AbstractPowerSyncDatabase, + dialect: SQLiteAsyncDialect, + schema: RelationalSchemaConfig | undefined, + options: PowerSyncSQLiteSessionOptions = {} + ) { + super(db, dialect, schema, options); + this.client = db; + } + + transaction( + transaction: (tx: PowerSyncSQLiteTransaction) => 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( + connection: DBAdapter, + fn: (tx: PowerSyncSQLiteTransaction) => T, + config: PowerSyncSQLiteTransactionConfig = {} + ): Promise { + const tx = new PowerSyncSQLiteTransaction( + '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; + } + } +} diff --git a/packages/drizzle-driver/tests/setup/db.ts b/packages/drizzle-driver/tests/setup/db.ts index b3807a7e0..b164331a0 100644 --- a/packages/drizzle-driver/tests/setup/db.ts +++ b/packages/drizzle-driver/tests/setup/db.ts @@ -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 diff --git a/packages/drizzle-driver/tests/sqlite/db.test.ts b/packages/drizzle-driver/tests/sqlite/db.test.ts index a7b3f79e9..60e0c7222 100644 --- a/packages/drizzle-driver/tests/sqlite/db.test.ts +++ b/packages/drizzle-driver/tests/sqlite/db.test.ts @@ -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', () => { diff --git a/packages/drizzle-driver/tests/sqlite/sqlite-query.test.ts b/packages/drizzle-driver/tests/sqlite/query.test.ts similarity index 92% rename from packages/drizzle-driver/tests/sqlite/sqlite-query.test.ts rename to packages/drizzle-driver/tests/sqlite/query.test.ts index 9a45db0f5..30e565911 100644 --- a/packages/drizzle-driver/tests/sqlite/sqlite-query.test.ts +++ b/packages/drizzle-driver/tests/sqlite/query.test.ts @@ -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'; diff --git a/packages/drizzle-driver/tests/sqlite/watch.test.ts b/packages/drizzle-driver/tests/sqlite/watch.test.ts index 0fc804f8a..9f0d80ef0 100644 --- a/packages/drizzle-driver/tests/sqlite/watch.test.ts +++ b/packages/drizzle-driver/tests/sqlite/watch.test.ts @@ -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();