Skip to content

Commit 63a191a

Browse files
committed
Run queries on read-only connections when we can.
1 parent 41fd49d commit 63a191a

File tree

4 files changed

+99
-9
lines changed

4 files changed

+99
-9
lines changed

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { LockContext, QueryResult } from '@powersync/common';
2+
import type { WithCacheConfig } from 'drizzle-orm/cache/core/types';
23
import { entityKind } from 'drizzle-orm/entity';
34
import type { Logger } from 'drizzle-orm/logger';
45
import { NoopLogger } from 'drizzle-orm/logger';
@@ -14,6 +15,7 @@ import {
1415
type SQLiteTransactionConfig
1516
} from 'drizzle-orm/sqlite-core/session';
1617
import { PowerSyncSQLitePreparedQuery } from './PowerSyncSQLitePreparedQuery.js';
18+
import { QueryContext } from './QueryContext.js';
1719

1820
export interface PowerSyncSQLiteSessionOptions {
1921
logger?: Logger;
@@ -39,7 +41,7 @@ export class PowerSyncSQLiteBaseSession<
3941
protected logger: Logger;
4042

4143
constructor(
42-
protected db: LockContext,
44+
protected db: QueryContext,
4345
protected dialect: SQLiteAsyncDialect,
4446
protected schema: RelationalSchemaConfig<TSchema> | undefined,
4547
protected options: PowerSyncSQLiteSessionOptions = {}
@@ -53,7 +55,12 @@ export class PowerSyncSQLiteBaseSession<
5355
fields: SelectedFieldsOrdered | undefined,
5456
executeMethod: SQLiteExecuteMethod,
5557
isResponseInArrayMode: boolean,
56-
customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown
58+
customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown,
59+
queryMetadata?: {
60+
type: 'select' | 'update' | 'delete' | 'insert';
61+
tables: string[];
62+
},
63+
cacheConfig?: WithCacheConfig
5764
): PowerSyncSQLitePreparedQuery<T> {
5865
return new PowerSyncSQLitePreparedQuery(
5966
this.db,
@@ -62,7 +69,10 @@ export class PowerSyncSQLiteBaseSession<
6269
fields,
6370
executeMethod,
6471
isResponseInArrayMode,
65-
customResultMapper
72+
customResultMapper,
73+
undefined, // cache not supported yet
74+
queryMetadata,
75+
cacheConfig
6676
);
6777
}
6878

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

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { LockContext, QueryResult } from '@powersync/common';
1+
import { QueryResult } from '@powersync/common';
22
import { Column, DriverValueDecoder, getTableName, SQL } from 'drizzle-orm';
3+
import type { Cache } from 'drizzle-orm/cache/core';
4+
import type { WithCacheConfig } from 'drizzle-orm/cache/core/types';
35
import { entityKind, is } from 'drizzle-orm/entity';
46
import type { Logger } from 'drizzle-orm/logger';
57
import { fillPlaceholders, type Query } from 'drizzle-orm/sql/sql';
@@ -10,6 +12,7 @@ import {
1012
type SQLiteExecuteMethod,
1113
SQLitePreparedQuery
1214
} from 'drizzle-orm/sqlite-core/session';
15+
import { QueryContext } from './QueryContext.js';
1316

1417
type PreparedQueryConfig = Omit<PreparedQueryConfigBase, 'statement' | 'run'>;
1518

@@ -25,16 +28,27 @@ export class PowerSyncSQLitePreparedQuery<
2528
}> {
2629
static readonly [entityKind]: string = 'PowerSyncSQLitePreparedQuery';
2730

31+
private readOnly = false;
32+
2833
constructor(
29-
private db: LockContext,
34+
private db: QueryContext,
3035
query: Query,
3136
private logger: Logger,
3237
private fields: SelectedFieldsOrdered | undefined,
3338
executeMethod: SQLiteExecuteMethod,
3439
private _isResponseInArrayMode: boolean,
35-
private customResultMapper?: (rows: unknown[][]) => unknown
40+
private customResultMapper?: (rows: unknown[][]) => unknown,
41+
cache?: Cache | undefined,
42+
queryMetadata?:
43+
| {
44+
type: 'select' | 'update' | 'delete' | 'insert';
45+
tables: string[];
46+
}
47+
| undefined,
48+
cacheConfig?: WithCacheConfig | undefined
3649
) {
37-
super('async', executeMethod, query);
50+
super('async', executeMethod, query, cache, queryMetadata, cacheConfig);
51+
this.readOnly = queryMetadata?.type == 'select';
3852
}
3953

4054
async run(placeholderValues?: Record<string, unknown>): Promise<QueryResult> {
@@ -90,6 +104,13 @@ export class PowerSyncSQLitePreparedQuery<
90104
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
91105
this.logger.logQuery(this.query.sql, params);
92106

107+
// When calling on the database (not in a transaction), and this is a select/read-only query,
108+
// use a read context for the query.
109+
if (this.readOnly) {
110+
return this.db.getAllRaw(this.query.sql, params);
111+
}
112+
113+
// This uses a write lock, unless we're already in a read transaction
93114
return await this.db.executeRaw(this.query.sql, params);
94115
}
95116

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
PowerSyncSQLiteTransaction,
99
PowerSyncSQLiteTransactionConfig
1010
} from './PowerSyncSQLiteBaseSession.js';
11+
import { DatabaseQueryContext, LockQueryContext } from './QueryContext.js';
1112

1213
export class PowerSyncSQLiteSession<
1314
TFullSchema extends Record<string, unknown>,
@@ -21,7 +22,7 @@ export class PowerSyncSQLiteSession<
2122
schema: RelationalSchemaConfig<TSchema> | undefined,
2223
options: PowerSyncSQLiteSessionOptions = {}
2324
) {
24-
super(db, dialect, schema, options);
25+
super(new DatabaseQueryContext(db), dialect, schema, options);
2526
this.client = db;
2627
}
2728

@@ -46,7 +47,7 @@ export class PowerSyncSQLiteSession<
4647
const tx = new PowerSyncSQLiteTransaction<TFullSchema, TSchema>(
4748
'async',
4849
(this as any).dialect,
49-
new PowerSyncSQLiteBaseSession(connection, this.dialect, this.schema, this.options),
50+
new PowerSyncSQLiteBaseSession(new LockQueryContext(connection), this.dialect, this.schema, this.options),
5051
this.schema
5152
);
5253

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { AbstractPowerSyncDatabase, LockContext, QueryResult } from '@powersync/common';
2+
3+
/**
4+
* Like LockContext, but only includes the specific methods needed for Drizzle.
5+
*
6+
* We extend it by adding getAllRaw, to support read-only queries with executeRaw.
7+
*/
8+
export interface QueryContext {
9+
execute(query: string, params?: any[] | undefined): Promise<QueryResult>;
10+
executeRaw(query: string, params?: any[] | undefined): Promise<any[][]>;
11+
12+
get<T>(sql: string, parameters?: any[]): Promise<T>;
13+
getAll<T>(sql: string, parameters?: any[]): Promise<T[]>;
14+
/**
15+
* Like executeRaw, but for read-only queries.
16+
*/
17+
getAllRaw(query: string, params?: any[] | undefined): Promise<any[][]>;
18+
}
19+
20+
export class DatabaseQueryContext implements QueryContext {
21+
constructor(private db: AbstractPowerSyncDatabase) {}
22+
execute(query: string, params?: any[] | undefined): Promise<QueryResult> {
23+
return this.db.execute(query, params);
24+
}
25+
executeRaw(query: string, params?: any[] | undefined) {
26+
return this.db.executeRaw(query, params);
27+
}
28+
get<T>(sql: string, parameters?: any[]) {
29+
return this.db.get<T>(sql, parameters);
30+
}
31+
getAll<T>(sql: string, parameters?: any[]) {
32+
return this.db.getAll<T>(sql, parameters);
33+
}
34+
getAllRaw(query: string, params?: any[] | undefined) {
35+
return this.db.readLock(async (ctx) => {
36+
return ctx.executeRaw(query, params);
37+
});
38+
}
39+
}
40+
41+
export class LockQueryContext implements QueryContext {
42+
constructor(private ctx: LockContext) {}
43+
execute(query: string, params?: any[] | undefined): Promise<QueryResult> {
44+
return this.ctx.execute(query, params);
45+
}
46+
executeRaw(query: string, params?: any[] | undefined) {
47+
return this.ctx.executeRaw(query, params);
48+
}
49+
get<T>(sql: string, parameters?: any[]) {
50+
return this.ctx.get<T>(sql, parameters);
51+
}
52+
getAll<T>(sql: string, parameters?: any[]) {
53+
return this.ctx.getAll<T>(sql, parameters);
54+
}
55+
getAllRaw(query: string, params?: any[] | undefined) {
56+
return this.ctx.executeRaw(query, params);
57+
}
58+
}

0 commit comments

Comments
 (0)