Skip to content

Commit 6877d2c

Browse files
committed
Introduced executeRaw to DBAdapter and WASQLiteConnection.
1 parent aa84a6c commit 6877d2c

File tree

14 files changed

+230
-44
lines changed

14 files changed

+230
-44
lines changed

packages/common/src/client/AbstractPowerSyncDatabase.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,11 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
603603
return this.database.execute(sql, parameters);
604604
}
605605

606+
async executeRaw(sql: string, parameters?: any[]) {
607+
await this.waitForReady();
608+
return this.database.executeRaw(sql, parameters);
609+
}
610+
606611
/**
607612
* Execute a write query (INSERT/UPDATE/DELETE) multiple times with each parameter set
608613
* and optionally return results.

packages/common/src/db/DBAdapter.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export interface DBGetUtils {
4444
export interface LockContext extends DBGetUtils {
4545
/** Execute a single write statement. */
4646
execute: (query: string, params?: any[] | undefined) => Promise<QueryResult>;
47+
/** Execute a single write statement and return raw results. */
48+
executeRaw: (query: string, params?: any[] | undefined) => Promise<any[][]>;
4749
}
4850

4951
export interface Transaction extends LockContext {
@@ -95,6 +97,7 @@ export interface DBLockOptions {
9597
export interface DBAdapter extends BaseObserverInterface<DBAdapterListener>, DBGetUtils {
9698
close: () => void;
9799
execute: (query: string, params?: any[]) => Promise<QueryResult>;
100+
executeRaw: (query: string, params?: any[]) => Promise<any[][]>;
98101
executeBatch: (query: string, params?: any[][]) => Promise<QueryResult>;
99102
name: string;
100103
readLock: <T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) => Promise<T>;
@@ -103,7 +106,7 @@ export interface DBAdapter extends BaseObserverInterface<DBAdapterListener>, DBG
103106
writeTransaction: <T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) => Promise<T>;
104107
/**
105108
* This method refreshes the schema information across all connections. This is for advanced use cases, and should generally not be needed.
106-
*/
109+
*/
107110
refreshSchema: () => Promise<void>;
108111
}
109112

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ export class PowerSyncSQLitePreparedQuery<
9090
async values(placeholderValues?: Record<string, unknown>): Promise<T['values']> {
9191
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
9292
this.logger.logQuery(this.query.sql, params);
93-
const rs = await this.db.execute(this.query.sql, params);
94-
return rs.rows?._array ?? [];
93+
94+
return await this.db.executeRaw(this.query.sql, params);
9595
}
9696

9797
isResponseInArrayMode(): boolean {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ describe('PowerSyncSQLitePreparedQuery', () => {
5656
const preparedQuery = new PowerSyncSQLitePreparedQuery(powerSyncDb, query, loggerMock, undefined, 'all', false);
5757

5858
const values = await preparedQuery.values();
59+
5960
expect(values).toEqual([
60-
{ id: '1', name: 'Alice' },
61-
{ id: '2', name: 'Bob' }
61+
['1', 'Alice'],
62+
['2', 'Bob']
6263
]);
6364
});
6465
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { AbstractPowerSyncDatabase, column, Schema, Table } from '@powersync/common';
2+
import { PowerSyncDatabase } from '@powersync/web';
3+
import { eq, relations } from 'drizzle-orm';
4+
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
5+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6+
import * as SUT from '../../src/sqlite/PowerSyncSQLiteDatabase';
7+
8+
const users = new Table({
9+
name: column.text
10+
});
11+
12+
const posts = new Table({
13+
content: column.text,
14+
title: column.text,
15+
user_id: column.text
16+
});
17+
18+
const drizzleUsers = sqliteTable('users', {
19+
id: text('id').primaryKey().notNull(),
20+
name: text('name').notNull()
21+
});
22+
23+
const drizzlePosts = sqliteTable('posts', {
24+
id: text('id').primaryKey().notNull(),
25+
content: text('content').notNull(),
26+
title: text('title').notNull(),
27+
user_id: text('user_id')
28+
.notNull()
29+
.references(() => drizzleUsers.id)
30+
});
31+
32+
// Define relationships
33+
const usersRelations = relations(drizzleUsers, ({ one, many }) => ({
34+
posts: many(drizzlePosts) // One user has many posts
35+
}));
36+
37+
const postsRelations = relations(drizzlePosts, ({ one }) => ({
38+
user: one(drizzleUsers, {
39+
fields: [drizzlePosts.user_id], // Foreign key in posts
40+
references: [drizzleUsers.id] // Primary key in users
41+
})
42+
}));
43+
44+
const PsSchema = new Schema({ users, posts });
45+
// const DrizzleSchema = { users: drizzleUsers, posts: drizzlePosts };
46+
const DrizzleSchema = { users: drizzleUsers, posts: drizzlePosts, usersRelations, postsRelations };
47+
48+
describe('Relationship tests', () => {
49+
let powerSyncDb: AbstractPowerSyncDatabase;
50+
let db: SUT.PowerSyncSQLiteDatabase<typeof DrizzleSchema>;
51+
52+
beforeEach(async () => {
53+
powerSyncDb = new PowerSyncDatabase({
54+
database: {
55+
dbFilename: 'test.db'
56+
},
57+
schema: PsSchema
58+
});
59+
db = SUT.wrapPowerSyncWithDrizzle(powerSyncDb, { schema: DrizzleSchema, logger: { logQuery: () => {} } });
60+
61+
await powerSyncDb.init();
62+
63+
await db.insert(drizzleUsers).values({ id: '1', name: 'Alice' });
64+
await db.insert(drizzlePosts).values({ id: '33', content: 'Post content', title: 'Post title', user_id: '1' });
65+
});
66+
67+
afterEach(async () => {
68+
await powerSyncDb?.disconnectAndClear();
69+
});
70+
71+
it('should retrieve a user with posts', async () => {
72+
const result = await db.query.users.findMany({ with: { posts: true } });
73+
74+
expect(result).toEqual([
75+
{ id: '1', name: 'Alice', posts: [{ id: '33', content: 'Post content', title: 'Post title', user_id: '1' }] }
76+
]);
77+
});
78+
79+
it('should retrieve a post with its user', async () => {
80+
const result = await db.query.posts.findMany({ with: { user: true } });
81+
82+
expect(result).toEqual([
83+
{
84+
id: '33',
85+
content: 'Post content',
86+
title: 'Post title',
87+
user_id: '1',
88+
user: { id: '1', name: 'Alice' }
89+
}
90+
]);
91+
});
92+
93+
it('should return a user and posts using leftJoin', async () => {
94+
const result = await db
95+
.select()
96+
.from(drizzleUsers)
97+
.leftJoin(drizzlePosts, eq(drizzleUsers.id, drizzlePosts.user_id));
98+
99+
expect(result[0].users).toEqual({ id: '1', name: 'Alice' });
100+
expect(result[0].posts).toEqual({ id: '33', content: 'Post content', title: 'Post title', user_id: '1' });
101+
});
102+
103+
it('should return a user and posts using rightJoin', async () => {
104+
const result = await db
105+
.select()
106+
.from(drizzleUsers)
107+
.rightJoin(drizzlePosts, eq(drizzleUsers.id, drizzlePosts.user_id));
108+
109+
expect(result[0].users).toEqual({ id: '1', name: 'Alice' });
110+
expect(result[0].posts).toEqual({ id: '33', content: 'Post content', title: 'Post title', user_id: '1' });
111+
});
112+
113+
it('should return a user and posts using fullJoin', async () => {
114+
const result = await db
115+
.select()
116+
.from(drizzleUsers)
117+
.fullJoin(drizzlePosts, eq(drizzleUsers.id, drizzlePosts.user_id));
118+
119+
expect(result[0].users).toEqual({ id: '1', name: 'Alice' });
120+
expect(result[0].posts).toEqual({ id: '33', content: 'Post content', title: 'Post title', user_id: '1' });
121+
});
122+
});

packages/powersync-op-sqlite/src/db/OPSQLiteConnection.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ export class OPSQLiteConnection extends BaseObserver<DBAdapterListener> {
9797
};
9898
}
9999

100+
async executeRaw(query: string, params?: any[]): Promise<any[][]> {
101+
// TODO CL: Test this
102+
return await this.DB.executeRaw(query, params);
103+
}
104+
100105
async executeBatch(query: string, params: any[][] = []): Promise<QueryResult> {
101106
const tuple: SQLBatchTuple[] = [[query, params[0]]];
102107
params.slice(1).forEach((p) => tuple.push([query, p]));

packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
1-
import {
2-
BaseObserver,
3-
DBAdapter,
4-
DBAdapterListener,
5-
DBLockOptions,
6-
QueryResult,
7-
Transaction
8-
} from '@powersync/common';
9-
import {
10-
ANDROID_DATABASE_PATH,
11-
getDylibPath,
12-
IOS_LIBRARY_PATH,
13-
open,
14-
type DB
15-
} from '@op-engineering/op-sqlite';
1+
import { BaseObserver, DBAdapter, DBAdapterListener, DBLockOptions, QueryResult, Transaction } from '@powersync/common';
2+
import { ANDROID_DATABASE_PATH, getDylibPath, IOS_LIBRARY_PATH, open, type DB } from '@op-engineering/op-sqlite';
163
import Lock from 'async-lock';
174
import { OPSQLiteConnection } from './OPSQLiteConnection';
185
import { Platform } from 'react-native';
@@ -247,6 +234,10 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
247234
return this.writeLock((ctx) => ctx.execute(query, params));
248235
}
249236

237+
executeRaw(query: string, params?: any[]) {
238+
return this.writeLock((ctx) => ctx.executeRaw(query, params));
239+
}
240+
250241
async executeBatch(query: string, params: any[][] = []): Promise<QueryResult> {
251242
return this.writeLock((ctx) => ctx.executeBatch(query, params));
252243
}
@@ -274,6 +265,7 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
274265
await connection.execute('BEGIN');
275266
const result = await fn({
276267
execute: (query, params) => connection.execute(query, params),
268+
executeRaw: (query, params) => connection.executeRaw(query, params),
277269
get: (query, params) => connection.get(query, params),
278270
getAll: (query, params) => connection.getAll(query, params),
279271
getOptional: (query, params) => connection.getOptional(query, params),
@@ -292,7 +284,7 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
292284
await this.initialized;
293285
await this.writeConnection!.refreshSchema();
294286

295-
if(this.readConnections) {
287+
if (this.readConnections) {
296288
for (let readConnection of this.readConnections) {
297289
await readConnection.connection.refreshSchema();
298290
}

packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ export class RNQSDBAdapter extends BaseObserver<DBAdapterListener> implements DB
6262
return this.baseDB.execute(query, params);
6363
}
6464

65+
executeRaw(query: string, params?: any[]): Promise<any[][]> {
66+
throw new Error('Method not implemented.');
67+
}
68+
6569
async executeBatch(query: string, params: any[][] = []): Promise<QueryResult> {
6670
const commands: any[] = [];
6771

@@ -85,6 +89,10 @@ export class RNQSDBAdapter extends BaseObserver<DBAdapterListener> implements DB
8589
return this.baseDB.readLock((ctx) => ctx.execute(sql, params));
8690
}
8791

92+
private readOnlyExecuteRaw(sql: string, params?: any[]) {
93+
return this.baseDB.readLock((ctx) => ctx.execute(sql, params));
94+
}
95+
8896
/**
8997
* Adds DB get utils to lock contexts and transaction contexts
9098
* @param tx

packages/web/src/db/adapters/AsyncDatabaseConnection.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface AsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOption
2121
init(): Promise<void>;
2222
close(): Promise<void>;
2323
execute(sql: string, params?: any[]): Promise<ProxiedQueryResult>;
24+
executeRaw(sql: string, params?: any[]): Promise<any[][]>;
2425
executeBatch(sql: string, params?: any[]): Promise<ProxiedQueryResult>;
2526
registerOnTableChange(callback: OnTableChangeCallback): Promise<() => void>;
2627
getConfig(): Promise<Config>;

packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ export class LockedAsyncDatabaseAdapter
138138
return this.writeLock((ctx) => ctx.execute(query, params));
139139
}
140140

141+
async executeRaw(query: string, params?: any[] | undefined): Promise<any[][]> {
142+
return this.writeLock((ctx) => ctx.executeRaw(query, params));
143+
}
144+
141145
async executeBatch(query: string, params?: any[][]): Promise<QueryResult> {
142146
return this.writeLock((ctx) => this._executeBatch(query, params));
143147
}
@@ -169,12 +173,16 @@ export class LockedAsyncDatabaseAdapter
169173

170174
async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
171175
await this.waitForInitialized();
172-
return this.acquireLock(async () => fn(this.generateDBHelpers({ execute: this._execute })));
176+
return this.acquireLock(async () =>
177+
fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw }))
178+
);
173179
}
174180

175181
async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
176182
await this.waitForInitialized();
177-
return this.acquireLock(async () => fn(this.generateDBHelpers({ execute: this._execute })));
183+
return this.acquireLock(async () =>
184+
fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw }))
185+
);
178186
}
179187

180188
protected acquireLock(callback: () => Promise<any>): Promise<any> {
@@ -283,6 +291,14 @@ export class LockedAsyncDatabaseAdapter
283291
};
284292
};
285293

294+
/**
295+
* Wraps the worker executeRaw function, awaiting for it to be available
296+
*/
297+
private _executeRaw = async (sql: string, bindings?: any[]): Promise<any[][]> => {
298+
await this.waitForInitialized();
299+
return await this.baseDB.executeRaw(sql, bindings);
300+
};
301+
286302
/**
287303
* Wraps the worker executeBatch function, awaiting for it to be available
288304
*/

0 commit comments

Comments
 (0)