Skip to content

Commit 5c91ebb

Browse files
fix: drizzle locks
1 parent d3808db commit 5c91ebb

File tree

6 files changed

+531
-25
lines changed

6 files changed

+531
-25
lines changed

.changeset/nice-mangos-fail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/drizzle-driver': patch
3+
---
4+
5+
Fixed issue where read queries would not use a readLock.

packages/drizzle-driver/src/sqlite/PowerSyncSQLiteBaseSession.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LockContext, QueryResult } from '@powersync/common';
1+
import { 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';
@@ -7,13 +7,13 @@ 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 {
10-
type PreparedQueryConfig as PreparedQueryConfigBase,
11-
type SQLiteExecuteMethod,
1210
SQLiteSession,
1311
SQLiteTransaction,
12+
type PreparedQueryConfig as PreparedQueryConfigBase,
13+
type SQLiteExecuteMethod,
1414
type SQLiteTransactionConfig
1515
} from 'drizzle-orm/sqlite-core/session';
16-
import { PowerSyncSQLitePreparedQuery } from './PowerSyncSQLitePreparedQuery.js';
16+
import { ContextProvider, PowerSyncSQLitePreparedQuery } from './PowerSyncSQLitePreparedQuery.js';
1717

1818
export interface PowerSyncSQLiteSessionOptions {
1919
logger?: Logger;
@@ -39,7 +39,7 @@ export class PowerSyncSQLiteBaseSession<
3939
protected logger: Logger;
4040

4141
constructor(
42-
protected db: LockContext,
42+
protected contextProvider: ContextProvider,
4343
protected dialect: SQLiteAsyncDialect,
4444
protected schema: RelationalSchemaConfig<TSchema> | undefined,
4545
protected options: PowerSyncSQLiteSessionOptions = {}
@@ -56,7 +56,7 @@ export class PowerSyncSQLiteBaseSession<
5656
customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown
5757
): PowerSyncSQLitePreparedQuery<T> {
5858
return new PowerSyncSQLitePreparedQuery(
59-
this.db,
59+
this.contextProvider,
6060
query,
6161
this.logger,
6262
fields,

packages/drizzle-driver/src/sqlite/PowerSyncSQLitePreparedQuery.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
import { LockContext, QueryResult } from '@powersync/common';
2-
import { Column, DriverValueDecoder, getTableName, SQL } from 'drizzle-orm';
2+
import { Column, DriverValueDecoder, SQL, getTableName } from 'drizzle-orm';
33
import { entityKind, is } from 'drizzle-orm/entity';
44
import type { Logger } from 'drizzle-orm/logger';
55
import { fillPlaceholders, type Query } from 'drizzle-orm/sql/sql';
66
import { SQLiteColumn } from 'drizzle-orm/sqlite-core';
77
import type { SelectedFieldsOrdered } from 'drizzle-orm/sqlite-core/query-builders/select.types';
88
import {
9+
SQLitePreparedQuery,
910
type PreparedQueryConfig as PreparedQueryConfigBase,
10-
type SQLiteExecuteMethod,
11-
SQLitePreparedQuery
11+
type SQLiteExecuteMethod
1212
} from 'drizzle-orm/sqlite-core/session';
1313

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

16+
/**
17+
* Callback which uses a LockContext for database operations.
18+
*/
19+
export type LockCallback<T> = (ctx: LockContext) => Promise<T>;
20+
21+
/**
22+
* Provider for specific database contexts.
23+
* Handlers are provided a context to the provided callback.
24+
* This does not necessarily need to acquire a database lock for each call.
25+
* Calls might use the same lock context for multiple operations.
26+
* The read/write context may relate to a single read OR write context.
27+
*/
28+
export type ContextProvider = {
29+
useReadContext: <T>(fn: LockCallback<T>) => Promise<T>;
30+
useWriteContext: <T>(fn: LockCallback<T>) => Promise<T>;
31+
};
32+
1633
export class PowerSyncSQLitePreparedQuery<
1734
T extends PreparedQueryConfig = PreparedQueryConfig
1835
> extends SQLitePreparedQuery<{
@@ -26,7 +43,7 @@ export class PowerSyncSQLitePreparedQuery<
2643
static readonly [entityKind]: string = 'PowerSyncSQLitePreparedQuery';
2744

2845
constructor(
29-
private db: LockContext,
46+
private contextProvider: ContextProvider,
3047
query: Query,
3148
private logger: Logger,
3249
private fields: SelectedFieldsOrdered | undefined,
@@ -40,17 +57,23 @@ export class PowerSyncSQLitePreparedQuery<
4057
async run(placeholderValues?: Record<string, unknown>): Promise<QueryResult> {
4158
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
4259
this.logger.logQuery(this.query.sql, params);
43-
const rs = await this.db.execute(this.query.sql, params);
44-
return rs;
60+
/**
61+
* Run operations are teated as potential mutations, so they use the write context.
62+
*/
63+
return this.contextProvider.useWriteContext(async (ctx) => {
64+
const rs = await ctx.execute(this.query.sql, params);
65+
return rs;
66+
});
4567
}
4668

4769
async all(placeholderValues?: Record<string, unknown>): Promise<T['all']> {
4870
const { fields, query, logger, customResultMapper } = this;
4971
if (!fields && !customResultMapper) {
5072
const params = fillPlaceholders(query.params, placeholderValues ?? {});
5173
logger.logQuery(query.sql, params);
52-
const rs = await this.db.execute(this.query.sql, params);
53-
return rs.rows?._array ?? [];
74+
return await this.contextProvider.useReadContext(async (ctx) => {
75+
return await ctx.getAll(this.query.sql, params);
76+
});
5477
}
5578

5679
const rows = (await this.values(placeholderValues)) as unknown[][];
@@ -69,7 +92,9 @@ export class PowerSyncSQLitePreparedQuery<
6992
const { fields, customResultMapper } = this;
7093
const joinsNotNullableMap = (this as any).joinsNotNullableMap;
7194
if (!fields && !customResultMapper) {
72-
return this.db.get(this.query.sql, params);
95+
return this.contextProvider.useReadContext(async (ctx) => {
96+
return await ctx.get(this.query.sql, params);
97+
});
7398
}
7499

75100
const rows = (await this.values(placeholderValues)) as unknown[][];
@@ -90,7 +115,9 @@ export class PowerSyncSQLitePreparedQuery<
90115
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
91116
this.logger.logQuery(this.query.sql, params);
92117

93-
return await this.db.executeRaw(this.query.sql, params);
118+
return await this.contextProvider.useReadContext(async (ctx) => {
119+
return await ctx.executeRaw(this.query.sql, params);
120+
});
94121
}
95122

96123
isResponseInArrayMode(): boolean {

packages/drizzle-driver/src/sqlite/PowerSyncSQLiteSession.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AbstractPowerSyncDatabase, DBAdapter } from '@powersync/common';
1+
import { AbstractPowerSyncDatabase, LockContext } from '@powersync/common';
22
import { entityKind } from 'drizzle-orm/entity';
33
import type { RelationalSchemaConfig, TablesRelationalConfig } from 'drizzle-orm/relations';
44
import type { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core/dialect';
@@ -21,7 +21,16 @@ export class PowerSyncSQLiteSession<
2121
schema: RelationalSchemaConfig<TSchema> | undefined,
2222
options: PowerSyncSQLiteSessionOptions = {}
2323
) {
24-
super(db, dialect, schema, options);
24+
super(
25+
// Top level operations use the respective locks.
26+
{
27+
useReadContext: (callback) => db.readLock(callback),
28+
useWriteContext: (callback) => db.writeLock(callback)
29+
},
30+
dialect,
31+
schema,
32+
options
33+
);
2534
this.client = db;
2635
}
2736

@@ -39,14 +48,23 @@ export class PowerSyncSQLiteSession<
3948
}
4049

4150
protected async internalTransaction<T>(
42-
connection: DBAdapter,
51+
connection: LockContext,
4352
fn: (tx: PowerSyncSQLiteTransaction<TFullSchema, TSchema>) => T,
4453
config: PowerSyncSQLiteTransactionConfig = {}
4554
): Promise<T> {
4655
const tx = new PowerSyncSQLiteTransaction<TFullSchema, TSchema>(
4756
'async',
4857
(this as any).dialect,
49-
new PowerSyncSQLiteBaseSession(connection, this.dialect, this.schema, this.options),
58+
new PowerSyncSQLiteBaseSession(
59+
{
60+
// We already have a fixed context here. We need to use it for both "read" and "write" operations.
61+
useReadContext: (callback) => callback(connection),
62+
useWriteContext: (callback) => callback(connection)
63+
},
64+
this.dialect,
65+
this.schema,
66+
this.options
67+
),
5068
this.schema
5169
);
5270

0 commit comments

Comments
 (0)