Skip to content

Commit 4568c34

Browse files
committed
feat(rivetkit): add synchronous sqlite queries
1 parent eb2bfca commit 4568c34

20 files changed

Lines changed: 773 additions & 13 deletions

File tree

docs/content/docs/sqlite.mdx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@ const rows = await c.db.execute(
8383
);
8484
```
8585

86+
### Synchronous queries in Node.js
87+
88+
The Node.js native runtime also provides `c.db.executeSync(...)` for integrations that require an immediate result. It accepts the same SQL and parameters as `execute(...)`.
89+
90+
```ts @nocheck
91+
const rows = c.db.executeSync(
92+
"SELECT id, title FROM todos WHERE title LIKE ?",
93+
`%${query}%`,
94+
);
95+
```
96+
97+
**Prefer `await c.db.execute(...)` for normal use.** `executeSync(...)` blocks the Node.js event loop until SQLite finishes, so a slow query also prevents the actor from handling other JavaScript work. It is unavailable in WebAssembly runtimes.
98+
8699
### Transactions
87100

88101
Use transactions when multiple writes must succeed or fail together.

rivetkit-typescript/packages/rivetkit-napi/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,19 +379,24 @@ export declare class JsNativeDatabase {
379379
run(sql: string, params?: Array<JsBindParam> | undefined | null): Promise<ExecuteResult>
380380
query(sql: string, params?: Array<JsBindParam> | undefined | null): Promise<QueryResult>
381381
execute(sql: string, params?: Array<JsBindParam> | undefined | null): Promise<NativeExecuteResult>
382+
executeSync(sql: string, params?: Array<JsBindParam> | undefined | null): NativeExecuteResult
382383
executeBatch(statements: Array<JsSqliteBatchStatement>): Promise<Array<NativeExecuteResult>>
383384
exec(sql: string): Promise<QueryResult>
385+
execSync(sql: string): QueryResult
384386
close(): Promise<void>
385387
beginTransaction(timeoutMs?: number | undefined | null, name?: string | undefined | null): Promise<JsSqliteTransaction>
386388
}
387389
export declare class JsSqliteTransaction {
388390
execute(sql: string, params?: Array<JsBindParam> | undefined | null): Promise<NativeExecuteResult>
391+
executeSync(sql: string, params?: Array<JsBindParam> | undefined | null): NativeExecuteResult
389392
exec(sql: string): Promise<QueryResult>
393+
execSync(sql: string): QueryResult
390394
commit(): Promise<void>
391395
rollback(): Promise<void>
392396
}
393397
export declare class JsActorStateTransaction {
394398
execute(sql: string, params?: Array<JsBindParam> | undefined | null): Promise<NativeExecuteResult>
399+
executeSync(sql: string, params?: Array<JsBindParam> | undefined | null): NativeExecuteResult
395400
commit(payload: StateDeltaPayload): Promise<void>
396401
rollback(): Promise<void>
397402
}

rivetkit-typescript/packages/rivetkit-napi/src/database.rs

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::time::Duration;
1+
use std::{future::Future, sync::mpsc, time::Duration};
22

33
use crate::actor_context::{StateDeltaPayload, state_deltas_from_payload};
44
use napi::bindgen_prelude::Buffer;
@@ -176,6 +176,17 @@ impl JsNativeDatabase {
176176
Ok(core_execute_result_to_js(result))
177177
}
178178

179+
#[napi]
180+
pub fn execute_sync(
181+
&self,
182+
sql: String,
183+
params: Option<Vec<JsBindParam>>,
184+
) -> napi::Result<NativeExecuteResult> {
185+
let params = params.map(js_bind_params_to_core).transpose()?;
186+
let db = self.db.clone();
187+
wait_for_runtime(async move { db.execute(sql, params).await }).map(core_execute_result_to_js)
188+
}
189+
179190
#[napi]
180191
pub async fn execute_batch(
181192
&self,
@@ -196,6 +207,12 @@ impl JsNativeDatabase {
196207
Ok(core_query_result_to_js(result))
197208
}
198209

210+
#[napi]
211+
pub fn exec_sync(&self, sql: String) -> napi::Result<QueryResult> {
212+
let db = self.db.clone();
213+
wait_for_runtime(async move { db.exec(sql).await }).map(core_query_result_to_js)
214+
}
215+
199216
#[napi]
200217
pub async fn close(&self) -> napi::Result<()> {
201218
self.db.close().await.map_err(crate::napi_anyhow_error)
@@ -233,6 +250,18 @@ impl JsSqliteTransaction {
233250
.map_err(crate::napi_anyhow_error)
234251
}
235252

253+
#[napi]
254+
pub fn execute_sync(
255+
&self,
256+
sql: String,
257+
params: Option<Vec<JsBindParam>>,
258+
) -> napi::Result<NativeExecuteResult> {
259+
let params = params.map(js_bind_params_to_core).transpose()?;
260+
let transaction = self.transaction.clone();
261+
wait_for_runtime(async move { transaction.execute(sql, params).await })
262+
.map(core_execute_result_to_js)
263+
}
264+
236265
#[napi]
237266
pub async fn exec(&self, sql: String) -> napi::Result<QueryResult> {
238267
self.transaction
@@ -242,6 +271,12 @@ impl JsSqliteTransaction {
242271
.map_err(crate::napi_anyhow_error)
243272
}
244273

274+
#[napi]
275+
pub fn exec_sync(&self, sql: String) -> napi::Result<QueryResult> {
276+
let transaction = self.transaction.clone();
277+
wait_for_runtime(async move { transaction.exec(sql).await }).map(core_query_result_to_js)
278+
}
279+
245280
#[napi]
246281
pub async fn commit(&self) -> napi::Result<()> {
247282
self.transaction
@@ -275,6 +310,18 @@ impl JsActorStateTransaction {
275310
.map_err(crate::napi_anyhow_error)
276311
}
277312

313+
#[napi]
314+
pub fn execute_sync(
315+
&self,
316+
sql: String,
317+
params: Option<Vec<JsBindParam>>,
318+
) -> napi::Result<NativeExecuteResult> {
319+
let params = params.map(js_bind_params_to_core).transpose()?;
320+
let transaction = self.transaction.clone();
321+
wait_for_runtime(async move { transaction.execute(sql, params).await })
322+
.map(core_execute_result_to_js)
323+
}
324+
278325
#[napi]
279326
pub async fn commit(&self, payload: StateDeltaPayload) -> napi::Result<()> {
280327
self.transaction
@@ -292,6 +339,38 @@ impl JsActorStateTransaction {
292339
}
293340
}
294341

342+
fn wait_for_runtime<T, F>(future: F) -> napi::Result<T>
343+
where
344+
T: Send + 'static,
345+
F: Future<Output = anyhow::Result<T>> + Send + 'static,
346+
{
347+
let runtime = tokio::runtime::Handle::try_current().map_err(|error| {
348+
napi_anyhow_error(
349+
crate::NapiInvalidState {
350+
state: "runtime".to_owned(),
351+
reason: format!("cannot run synchronous SQLite query: {error}"),
352+
}
353+
.build(),
354+
)
355+
})?;
356+
let (sender, receiver) = mpsc::sync_channel(1);
357+
runtime.spawn(async move {
358+
let _ = sender.send(future.await);
359+
});
360+
receiver
361+
.recv()
362+
.map_err(|error| {
363+
napi_anyhow_error(
364+
crate::NapiInvalidState {
365+
state: "runtime".to_owned(),
366+
reason: format!("synchronous SQLite query ended without a result: {error}"),
367+
}
368+
.build(),
369+
)
370+
})?
371+
.map_err(crate::napi_anyhow_error)
372+
}
373+
295374
pub(crate) fn transaction_timeout(timeout_ms: f64) -> napi::Result<Duration> {
296375
if !timeout_ms.is_finite() || timeout_ms <= 0.0 {
297376
return Err(napi_anyhow_error(
@@ -388,3 +467,20 @@ fn column_value_to_json(value: ColumnValue) -> serde_json::Value {
388467
}
389468
}
390469
}
470+
471+
#[cfg(test)]
472+
mod tests {
473+
#[test]
474+
fn synchronous_wait_uses_the_active_multithreaded_runtime() {
475+
let runtime = tokio::runtime::Builder::new_multi_thread()
476+
.enable_all()
477+
.build()
478+
.expect("runtime should build");
479+
let _guard = runtime.enter();
480+
481+
let result = super::wait_for_runtime(async { Ok::<_, anyhow::Error>(42) })
482+
.expect("future should complete");
483+
484+
assert_eq!(result, 42);
485+
}
486+
}

rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/actor-db-raw.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,32 @@ export const dbActorRaw = actor({
254254
);
255255
return results[0].count;
256256
},
257+
synchronousQueries: async (c, value: string) => {
258+
c.db.executeSync(
259+
"INSERT INTO test_data (value, payload, created_at) VALUES (?, ?, ?)",
260+
value,
261+
"",
262+
Date.now(),
263+
);
264+
const selected = c.db.executeSync<{ value: string }>(
265+
"SELECT value FROM test_data WHERE value = ?",
266+
value,
267+
);
268+
const transactionCount = await c.db.transaction((tx) => {
269+
if (!tx.executeSync) {
270+
throw new Error(
271+
"synchronous transaction queries are unavailable",
272+
);
273+
}
274+
return tx.executeSync<{ count: number }>(
275+
"SELECT COUNT(*) AS count FROM test_data",
276+
)[0]?.count;
277+
});
278+
return {
279+
value: selected[0]?.value,
280+
transactionCount,
281+
};
282+
},
257283
insertMany: async (c, count: number) => {
258284
if (count <= 0) {
259285
return { count: 0 };

rivetkit-typescript/packages/rivetkit/src/agent-os/actor/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { AgentOsOptions, MountConfig } from "@rivet-dev/agent-os-core";
22
import { AgentOs, createInMemoryFileSystem } from "@rivet-dev/agent-os-core";
33
import { type ActorDefinition, actor, event } from "@/actor/mod";
4-
import type { DatabaseProvider, RawAccess } from "@/common/database/config";
4+
import type {
5+
DatabaseProvider,
6+
SynchronousRawAccess,
7+
} from "@/common/database/config";
58
import { db } from "@/common/database/mod";
69
import {
710
type AgentOsActorConfig,
@@ -146,7 +149,7 @@ export function agentOs<TConnParams = undefined>(
146149
undefined,
147150
AgentOsActorVars,
148151
undefined,
149-
DatabaseProvider<RawAccess>,
152+
DatabaseProvider<SynchronousRawAccess>,
150153
{
151154
sessionEvent: typeof sessionEventToken;
152155
permissionRequest: typeof permissionRequestToken;
@@ -182,7 +185,7 @@ export function agentOs<TConnParams = undefined>(
182185
undefined,
183186
AgentOsActorVars,
184187
undefined,
185-
DatabaseProvider<RawAccess>,
188+
DatabaseProvider<SynchronousRawAccess>,
186189
{
187190
sessionEvent: typeof sessionEventToken;
188191
permissionRequest: typeof permissionRequestToken;

rivetkit-typescript/packages/rivetkit/src/agent-os/actor/preview.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import crypto from "node:crypto";
22
import type { RequestContext } from "@/actor/config";
3-
import type { DatabaseProvider, RawAccess } from "@/common/database/config";
3+
import type {
4+
DatabaseProvider,
5+
SynchronousRawAccess,
6+
} from "@/common/database/config";
47
import type { AgentOsActorConfig } from "../config";
58
import type {
69
AgentOsActionContext,
@@ -46,7 +49,7 @@ type AgentOsRequestContext<TConnParams> = RequestContext<
4649
undefined,
4750
AgentOsActorVars,
4851
undefined,
49-
DatabaseProvider<RawAccess>
52+
DatabaseProvider<SynchronousRawAccess>
5053
>;
5154

5255
export function buildOnRequestHandler<TConnParams>(

rivetkit-typescript/packages/rivetkit/src/common/database/config.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,15 @@ export interface SqliteDatabase {
8484
sql: string,
8585
callback?: (row: unknown[], columns: string[]) => void,
8686
): Promise<void>;
87+
execSync?(
88+
sql: string,
89+
callback?: (row: unknown[], columns: string[]) => void,
90+
): void;
8791
execute(sql: string, params?: SqliteBindings): Promise<SqliteExecuteResult>;
92+
executeSync?(
93+
sql: string,
94+
params?: SqliteBindings,
95+
): SqliteExecuteResult;
8896
executeBatch(
8997
statements: SqliteBatchStatement[],
9098
): Promise<SqliteExecuteResult[]>;
@@ -106,7 +114,15 @@ export interface SqliteTransactionDatabase {
106114
sql: string,
107115
callback?: (row: unknown[], columns: string[]) => void,
108116
): Promise<void>;
117+
execSync?(
118+
sql: string,
119+
callback?: (row: unknown[], columns: string[]) => void,
120+
): void;
109121
execute(sql: string, params?: SqliteBindings): Promise<SqliteExecuteResult>;
122+
executeSync?(
123+
sql: string,
124+
params?: SqliteBindings,
125+
): SqliteExecuteResult;
110126
commit(): Promise<void>;
111127
rollback(): Promise<void>;
112128
}
@@ -204,11 +220,23 @@ type ExecuteFunction = <
204220
...args: unknown[]
205221
) => Promise<TRow[]>;
206222

223+
type ExecuteSyncFunction = <
224+
TRow extends Record<string, unknown> = Record<string, unknown>,
225+
>(
226+
query: string,
227+
...args: unknown[]
228+
) => TRow[];
229+
207230
export type RawAccess = {
208231
/**
209232
* Executes a raw SQL query.
210233
*/
211234
execute: ExecuteFunction;
235+
/**
236+
* Executes a raw SQL query synchronously when supported by the runtime.
237+
* This blocks the Node.js event loop. Prefer `execute` for normal use.
238+
*/
239+
executeSync?: ExecuteSyncFunction;
212240
/** Runs a callback in an isolated SQLite transaction. */
213241
transaction: <T>(
214242
callback: (tx: RawAccess) => Promise<T> | T,
@@ -226,3 +254,8 @@ export type RawAccess = {
226254
*/
227255
close: () => Promise<void>;
228256
};
257+
258+
/** Raw database access with synchronous queries provided by the Node.js runtime. */
259+
export type SynchronousRawAccess = RawAccess & {
260+
executeSync: ExecuteSyncFunction;
261+
};

0 commit comments

Comments
 (0)