Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions crates/wasm-rquickjs/skeleton/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions crates/wasm-rquickjs/skeleton/Cargo.toml_
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,16 @@ golem-wasi-http = { version = "0.2.0", features = ["async"], optional = true }
wasi-logging = { version = "0.0.1", optional = true }

# SQLite
rusqlite = { version = "0.38", default-features = false, features = ["bundled", "wasm32-wasi-vfs", "functions", "session", "backup"], optional = true }
rusqlite = { version = "0.38", default-features = false, features = ["bundled", "wasm32-wasi-vfs", "functions", "session", "backup", "serialize"], optional = true }

# Golem
golem-rust = { version = "1.7", default-features = false, optional = true }

# Patch libsqlite3-sys to fix wasm32-wasi cross-compilation with session/preupdate_hook features.
# Upstream buildtime_bindgen produces incomplete bindings when cross-compiling to wasm32-wasip1.
# This fork uses pre-built session-enabled bindings instead.
# Patch rusqlite and libsqlite3-sys together for wasm32-wasi: the fork carries
# both the serialize/deserialize flag typing fix and the session-enabled WASI
# bindings.
[patch.crates-io]
rusqlite = { git = "https://github.com/golemcloud/rusqlite", branch = "v0.38.0-patched" }
libsqlite3-sys = { git = "https://github.com/golemcloud/rusqlite", branch = "v0.38.0-patched" }

[package.metadata.component.bindings]
Expand Down
42 changes: 33 additions & 9 deletions crates/wasm-rquickjs/skeleton/src/builtin/sqlite.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import {
create_session, session_changeset, session_patchset, session_close,
apply_changeset,
get_constants,
enable_defensive, location, set_authorizer, native_backup
enable_defensive, location, set_authorizer, native_backup,
serialize_database, restore_database
} from '__wasm_rquickjs_builtin/sqlite_native';

const constants = get_constants();
export const constants = get_constants();
const _connIdSymbol = Symbol('connId');

function assertBoolean(value, name) {
Expand Down Expand Up @@ -326,7 +327,7 @@ class _DatabaseSyncImpl {
}
}

function DatabaseSync(path, options) {
export function DatabaseSync(path, options) {
if (!new.target) {
const err = new TypeError('Cannot call constructor without `new`');
err.code = 'ERR_CONSTRUCT_CALL_REQUIRED';
Expand All @@ -336,7 +337,7 @@ function DatabaseSync(path, options) {
}
DatabaseSync.prototype = _DatabaseSyncImpl.prototype;

class Session {
export class Session {
#sessionId;
#connId;
#closed = false;
Expand Down Expand Up @@ -379,7 +380,7 @@ class Session {

const _stmtSecret = Symbol('stmtSecret');

class StatementSync {
export class StatementSync {
#connId;
#sql;
#options;
Expand Down Expand Up @@ -506,7 +507,7 @@ class StatementSync {
}
}

class SQLTagStore {
export class SQLTagStore {
#db;
#maxSize;
#cache;
Expand Down Expand Up @@ -585,7 +586,7 @@ class SQLTagStore {
}
}

async function backup(sourceDb, path, options = {}) {
export async function backup(sourceDb, path, options = {}) {
if (!(sourceDb instanceof DatabaseSync)) {
throw new TypeError('sourceDb must be a DatabaseSync instance');
}
Expand All @@ -603,5 +604,28 @@ async function backup(sourceDb, path, options = {}) {
return native_backup(connId, path, source, target, rate);
}

export { DatabaseSync, StatementSync, Session, SQLTagStore, constants, backup };
export default { DatabaseSync, StatementSync, Session, SQLTagStore, constants, backup };
export const serializeDatabase = serialize_database;
export const restoreDatabase = restore_database;

function _getConnId(db) {
if (!(db instanceof DatabaseSync)) {
throw new TypeError('db must be a DatabaseSync instance');
}
const connId = db[_connIdSymbol];
if (connId === null || connId === undefined) throwDbNotOpen('ERR_SQLITE_ERROR');
return connId;
}

export function serializeDatabaseSync(db) {
return serialize_database(_getConnId(db));
}

export function restoreDatabaseSync(db, bytes) {
return restore_database(_getConnId(db), bytes);
}

export function isAutocommitDatabaseSync(db) {
return is_autocommit(_getConnId(db));
}

export default { DatabaseSync, StatementSync, Session, SQLTagStore, constants, backup, serializeDatabase, restoreDatabase, serializeDatabaseSync, restoreDatabaseSync, isAutocommitDatabaseSync };
86 changes: 86 additions & 0 deletions crates/wasm-rquickjs/skeleton/src/builtin/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1830,6 +1830,78 @@ fn auth_action_args<'a>(action: &'a rusqlite::hooks::AuthAction<'a>) -> (&'a str
}
}

fn serialize_database_impl<'js>(
ctx: rquickjs::Ctx<'js>,
conn_id: u32,
) -> rquickjs::Result<rquickjs::Value<'js>> {
let table = lock_conn_table(&ctx)?;
let conn = table
.connections
.get(&conn_id)
.ok_or_else(|| rquickjs::Exception::throw_message(&ctx, "database is not open"))?;

let data = conn
.serialize(rusqlite::MAIN_DB)
.map_err(|e| sqlite_error(&ctx, &e))?;

let bytes: &[u8] = &data;
let typed_array = rquickjs::TypedArray::<u8>::new(ctx.clone(), bytes)
.map_err(|_| rquickjs::Exception::throw_message(&ctx, "failed to create Uint8Array"))?;
Ok(typed_array.into_value())
}

fn restore_database_impl<'js>(
ctx: rquickjs::Ctx<'js>,
conn_id: u32,
bytes: rquickjs::TypedArray<'js, u8>,
) -> rquickjs::Result<()> {
let mut table = lock_conn_table(&ctx)?;
let conn = table
.connections
.get_mut(&conn_id)
.ok_or_else(|| rquickjs::Exception::throw_message(&ctx, "database is not open"))?;

if conn
.is_readonly(rusqlite::MAIN_DB)
.map_err(|e| sqlite_error(&ctx, &e))?
{
return Err(throw_coded_error(
&ctx,
"Error",
"cannot restore a read-only database",
"ERR_SQLITE_ERROR",
));
}

if !conn.is_autocommit() {
return Err(throw_coded_error(
&ctx,
"Error",
"cannot restore database with an open transaction",
"ERR_SQLITE_ERROR",
));
}

let data = bytes.as_bytes().ok_or_else(|| {
rquickjs::Exception::throw_message(&ctx, "failed to read Uint8Array bytes")
})?;

// Use deserialize_read_exact into a temp in-memory connection, then backup into dest
let mut temp_conn =
rusqlite::Connection::open_in_memory().map_err(|e| sqlite_error(&ctx, &e))?;
temp_conn
.deserialize_read_exact(rusqlite::MAIN_DB, data, data.len(), false)
.map_err(|e| sqlite_error(&ctx, &e))?;

let backup = rusqlite::backup::Backup::new(&temp_conn, conn)
.map_err(|e| sqlite_error(&ctx, &e))?;
backup
.run_to_completion(100, Duration::ZERO, None)
.map_err(|e| sqlite_error(&ctx, &e))?;

Ok(())
}

fn native_backup_impl<'js>(
ctx: rquickjs::Ctx<'js>,
conn_id: u32,
Expand Down Expand Up @@ -2157,6 +2229,20 @@ pub mod native_module {
) -> rquickjs::Result<i32> {
super::native_backup_impl(ctx, conn_id, path, source_db, target_db, rate)
}

#[rquickjs::function]
pub fn serialize_database<'js>(ctx: Ctx<'js>, conn_id: u32) -> rquickjs::Result<Value<'js>> {
super::serialize_database_impl(ctx, conn_id)
}

#[rquickjs::function]
pub fn restore_database<'js>(
ctx: Ctx<'js>,
conn_id: u32,
bytes: rquickjs::TypedArray<'js, u8>,
) -> rquickjs::Result<()> {
super::restore_database_impl(ctx, conn_id, bytes)
}
}

pub const SQLITE_JS: &str = include_str!("sqlite.js");
Expand Down
9 changes: 7 additions & 2 deletions crates/wasm-rquickjs/src/optimize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,15 @@ pub async fn optimize_component(
config.wasm_component_model(true);
let engine = Engine::new(&config)?;

let mut wasi_builder = wasmtime_wasi::WasiCtxBuilder::new();
// Forward guest stdout/stderr so Wizer failures preserve the original JS/Rust
// diagnostics instead of collapsing into a bare trap.
wasi_builder.inherit_stdout().inherit_stderr();

#[cfg(feature = "use-golem-wasmtime")]
let (wasi, io_ctx) = wasmtime_wasi::WasiCtxBuilder::new().build();
let (wasi, io_ctx) = wasi_builder.build();
#[cfg(not(feature = "use-golem-wasmtime"))]
let wasi = wasmtime_wasi::WasiCtxBuilder::new().build();
let wasi = wasi_builder.build();

let mut store = Store::new(
&engine,
Expand Down
1 change: 1 addition & 0 deletions crates/wasm-rquickjs/src/skeleton.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ pub fn copy_skeleton_sources(output: &Utf8Path) -> anyhow::Result<()> {
output,
)?;
}

Ok(())
}

Expand Down
Loading
Loading