Skip to content

Commit f8fd814

Browse files
authored
Added executeRaw() (#482)
1 parent db90702 commit f8fd814

25 files changed

+497
-115
lines changed

.changeset/beige-actors-flash.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@powersync/react-native': minor
3+
---
4+
5+
Introduced `executeRaw` member to `RNQSDBAdapter` to match `DBAdapter` interface.
6+
It handles SQLite query results differently to `execute` - to preserve all columns, preventing duplicate column names from being overwritten.
7+
8+
The implementation for RNQS will currently fall back to `execute`, preserving current behavior. Users requiring this functionality should migrate to `@powersync/op-sqlite`.

.changeset/eleven-cups-rescue.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/drizzle-driver': minor
3+
---
4+
5+
Using `executeRaw` internally for queries instead of `execute`. This function processes SQLite query results differently to preserve all columns, preventing duplicate column names from being overwritten.

.changeset/fair-squids-chew.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@powersync/op-sqlite': minor
3+
'@powersync/common': minor
4+
'@powersync/node': minor
5+
'@powersync/web': minor
6+
---
7+
8+
Introduced `executeRaw`, which processes SQLite query results differently to preserve all columns, preventing duplicate column names from being overwritten.

packages/common/src/client/AbstractPowerSyncDatabase.ts

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

663+
async executeRaw(sql: string, parameters?: any[]) {
664+
await this.waitForReady();
665+
return this.database.executeRaw(sql, parameters);
666+
}
667+
663668
/**
664669
* Execute a write query (INSERT/UPDATE/DELETE) multiple times with each parameter set
665670
* and optionally return results.

packages/common/src/db/DBAdapter.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ 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+
/**
48+
* Execute a single write statement and return raw results.
49+
* Unlike `execute`, which returns an object with structured key-value pairs,
50+
* `executeRaw` returns a nested array of raw values, where each row is
51+
* represented as an array of column values without field names.
52+
*
53+
* Example result:
54+
*
55+
* ```[ [ '1', 'list 1', '33', 'Post content', '1' ] ]```
56+
*
57+
* Where as `execute`'s `rows._array` would have been:
58+
*
59+
* ```[ { id: '33', name: 'list 1', content: 'Post content', list_id: '1' } ]```
60+
*/
61+
executeRaw: (query: string, params?: any[] | undefined) => Promise<any[][]>;
4762
}
4863

4964
export interface Transaction extends LockContext {
@@ -95,6 +110,7 @@ export interface DBLockOptions {
95110
export interface DBAdapter extends BaseObserverInterface<DBAdapterListener>, DBGetUtils {
96111
close: () => void | Promise<void>;
97112
execute: (query: string, params?: any[]) => Promise<QueryResult>;
113+
executeRaw: (query: string, params?: any[]) => Promise<any[][]>;
98114
executeBatch: (query: string, params?: any[][]) => Promise<QueryResult>;
99115
name: string;
100116
readLock: <T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) => Promise<T>;

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ export class PowerSyncSQLitePreparedQuery<
5454
}
5555

5656
const rows = (await this.values(placeholderValues)) as unknown[][];
57-
const valueRows = rows.map((row) => Object.values(row));
57+
5858
if (customResultMapper) {
59-
const mapped = customResultMapper(valueRows) as T['all'];
59+
const mapped = customResultMapper(rows) as T['all'];
6060
return mapped;
6161
}
62-
return valueRows.map((row) => mapResultRow(fields!, row, (this as any).joinsNotNullableMap));
62+
return rows.map((row) => mapResultRow(fields!, row, (this as any).joinsNotNullableMap));
6363
}
6464

6565
async get(placeholderValues?: Record<string, unknown>): Promise<T['get']> {
@@ -80,18 +80,17 @@ export class PowerSyncSQLitePreparedQuery<
8080
}
8181

8282
if (customResultMapper) {
83-
const valueRows = rows.map((row) => Object.values(row));
84-
return customResultMapper(valueRows) as T['get'];
83+
return customResultMapper(rows) as T['get'];
8584
}
8685

87-
return mapResultRow(fields!, Object.values(row), joinsNotNullableMap);
86+
return mapResultRow(fields!, row, joinsNotNullableMap);
8887
}
8988

9089
async values(placeholderValues?: Record<string, unknown>): Promise<T['values']> {
9190
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
9291
this.logger.logQuery(this.query.sql, params);
93-
const rs = await this.db.execute(this.query.sql, params);
94-
return rs.rows?._array ?? [];
92+
93+
return await this.db.executeRaw(this.query.sql, params);
9594
}
9695

9796
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: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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+
const usersRelations = relations(drizzleUsers, ({ one, many }) => ({
33+
posts: many(drizzlePosts)
34+
}));
35+
36+
const postsRelations = relations(drizzlePosts, ({ one }) => ({
37+
user: one(drizzleUsers, {
38+
fields: [drizzlePosts.user_id],
39+
references: [drizzleUsers.id]
40+
})
41+
}));
42+
43+
const PsSchema = new Schema({ users, posts });
44+
const DrizzleSchema = { users: drizzleUsers, posts: drizzlePosts, usersRelations, postsRelations };
45+
46+
describe('Relationship tests', () => {
47+
let powerSyncDb: AbstractPowerSyncDatabase;
48+
let db: SUT.PowerSyncSQLiteDatabase<typeof DrizzleSchema>;
49+
50+
beforeEach(async () => {
51+
powerSyncDb = new PowerSyncDatabase({
52+
database: {
53+
dbFilename: 'test.db'
54+
},
55+
schema: PsSchema
56+
});
57+
db = SUT.wrapPowerSyncWithDrizzle(powerSyncDb, { schema: DrizzleSchema, logger: { logQuery: () => {} } });
58+
59+
await powerSyncDb.init();
60+
61+
await db.insert(drizzleUsers).values({ id: '1', name: 'Alice' });
62+
await db.insert(drizzlePosts).values({ id: '33', content: 'Post content', title: 'Post title', user_id: '1' });
63+
});
64+
65+
afterEach(async () => {
66+
await powerSyncDb?.disconnectAndClear();
67+
});
68+
69+
it('should retrieve a user with posts', async () => {
70+
const result = await db.query.users.findMany({ with: { posts: true } });
71+
72+
expect(result).toEqual([
73+
{ id: '1', name: 'Alice', posts: [{ id: '33', content: 'Post content', title: 'Post title', user_id: '1' }] }
74+
]);
75+
});
76+
77+
it('should retrieve a post with its user', async () => {
78+
const result = await db.query.posts.findMany({ with: { user: true } });
79+
80+
expect(result).toEqual([
81+
{
82+
id: '33',
83+
content: 'Post content',
84+
title: 'Post title',
85+
user_id: '1',
86+
user: { id: '1', name: 'Alice' }
87+
}
88+
]);
89+
});
90+
91+
it('should return a user and posts using leftJoin', async () => {
92+
const result = await db
93+
.select()
94+
.from(drizzleUsers)
95+
.leftJoin(drizzlePosts, eq(drizzleUsers.id, drizzlePosts.user_id));
96+
97+
expect(result[0].users).toEqual({ id: '1', name: 'Alice' });
98+
expect(result[0].posts).toEqual({ id: '33', content: 'Post content', title: 'Post title', user_id: '1' });
99+
});
100+
101+
it('should return a user and posts using rightJoin', async () => {
102+
const result = await db
103+
.select()
104+
.from(drizzleUsers)
105+
.rightJoin(drizzlePosts, eq(drizzleUsers.id, drizzlePosts.user_id));
106+
107+
expect(result[0].users).toEqual({ id: '1', name: 'Alice' });
108+
expect(result[0].posts).toEqual({ id: '33', content: 'Post content', title: 'Post title', user_id: '1' });
109+
});
110+
111+
it('should return a user and posts using fullJoin', async () => {
112+
const result = await db
113+
.select()
114+
.from(drizzleUsers)
115+
.fullJoin(drizzlePosts, eq(drizzleUsers.id, drizzlePosts.user_id));
116+
117+
expect(result[0].users).toEqual({ id: '1', name: 'Alice' });
118+
expect(result[0].posts).toEqual({ id: '33', content: 'Post content', title: 'Post title', user_id: '1' });
119+
});
120+
});

packages/node/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
},
5656
"devDependencies": {
5757
"@types/async-lock": "^1.4.0",
58+
"drizzle-orm": "^0.35.2",
59+
"@powersync/drizzle-driver": "workspace:*",
5860
"rollup": "4.14.3",
5961
"typescript": "^5.5.3",
6062
"vitest": "^3.0.5"

packages/node/src/db/AsyncDatabase.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface AsyncDatabaseOpener {
1313

1414
export interface AsyncDatabase {
1515
execute: (query: string, params: any[]) => Promise<ProxiedQueryResult>;
16+
executeRaw: (query: string, params: any[]) => Promise<any[][]>;
1617
executeBatch: (query: string, params: any[][]) => Promise<ProxiedQueryResult>;
1718
close: () => Promise<void>;
1819
// Collect table updates made since the last call to collectCommittedUpdates.

packages/node/src/db/BetterSQLite3DBAdapter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> impl
245245
await connection.execute('BEGIN');
246246
const result = await fn({
247247
execute: (query, params) => connection.execute(query, params),
248+
executeRaw: (query, params) => connection.executeRaw(query, params),
248249
executeBatch: (query, params) => connection.executeBatch(query, params),
249250
get: (query, params) => connection.get(query, params),
250251
getAll: (query, params) => connection.getAll(query, params),
@@ -281,6 +282,10 @@ export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> impl
281282
return this.writeLock((ctx) => ctx.execute(query, params));
282283
}
283284

285+
executeRaw(query: string, params?: any[] | undefined): Promise<any[][]> {
286+
return this.writeLock((ctx) => ctx.executeRaw(query, params));
287+
}
288+
284289
executeBatch(query: string, params?: any[][]): Promise<QueryResult> {
285290
return this.writeTransaction((ctx) => ctx.executeBatch(query, params));
286291
}

packages/node/src/db/RemoteConnection.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export class RemoteConnection implements LockContext {
2929
return RemoteConnection.wrapQueryResult(result);
3030
}
3131

32+
async executeRaw(query: string, params?: any[] | undefined): Promise<any[][]> {
33+
return await this.database.executeRaw(query, params ?? []);
34+
}
35+
3236
async getAll<T>(sql: string, parameters?: any[]): Promise<T[]> {
3337
const res = await this.execute(sql, parameters);
3438
return res.rows?._array ?? [];

packages/node/src/db/SqliteWorker.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ class BlockingAsyncDatabase implements AsyncDatabase {
6666
}
6767
}
6868

69+
async executeRaw(query: string, params: any[]) {
70+
const stmt = this.db.prepare(query);
71+
72+
if (stmt.reader) {
73+
return stmt.raw().all(params);
74+
} else {
75+
stmt.raw().run(params);
76+
return [];
77+
}
78+
}
79+
6980
async executeBatch(query: string, params: any[][]) {
7081
params = params ?? [];
7182

0 commit comments

Comments
 (0)