Skip to content
Open
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
4 changes: 2 additions & 2 deletions crates/bindings-typescript/src/server/sys.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ declare module 'spacetime:[email protected]' {

declare module 'spacetime:[email protected]' {
export type ModuleHooks = {
__call_view__(id: u32, sender: u256, args: Uint8Array): Uint8Array;
__call_view_anon__(id: u32, args: Uint8Array): Uint8Array;
__call_view__(id: u32, sender: u256, args: Uint8Array): Uint8Array | object;
__call_view_anon__(id: u32, args: Uint8Array): Uint8Array | object;
};

export function register_hooks(hooks: ModuleHooks);
Expand Down
2 changes: 2 additions & 0 deletions crates/codegen/examples/regen-typescript-moduledef.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use fs_err as fs;
use regex::Regex;
use spacetimedb_codegen::{generate, typescript, OutputFile};
use spacetimedb_lib::db::raw_def::v9::ViewResultHeader;
use spacetimedb_lib::{RawModuleDef, RawModuleDefV8};
use spacetimedb_schema::def::ModuleDef;
use std::path::Path;
Expand All @@ -20,6 +21,7 @@ macro_rules! regex_replace {
fn main() -> anyhow::Result<()> {
let module = RawModuleDefV8::with_builder(|module| {
module.add_type::<RawModuleDef>();
module.add_type::<ViewResultHeader>();
});

let dir = &Path::new(concat!(
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/host/v8/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,7 @@ fn common_call<'scope, R, O, F>(
budget: FunctionBudget,
op: O,
call: F,
) -> ExecutionResult<Result<R, ExecutionError>>
) -> ExecutionResult<R, ExecutionError>
where
O: InstanceOp,
F: FnOnce(&mut PinScope<'scope, '_>, O) -> Result<R, ErrorOrException<ExceptionThrown>>,
Expand Down
7 changes: 3 additions & 4 deletions crates/core/src/host/v8/syscall/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use bytes::Bytes;
use spacetimedb_lib::{RawModuleDef, VersionTuple};
use v8::{callback_scope, Context, FixedArray, Local, Module, PinScope};

use crate::host::v8::de::scratch_buf;
use crate::host::v8::error::{ErrorOrException, ExcResult, ExceptionThrown, Throwable, TypeError};
use crate::host::wasm_common::abi::parse_abi_version;
use crate::host::wasm_common::module_host_actor::{AnonymousViewOp, ReducerOp, ReducerResult, ViewOp};
use crate::host::wasm_common::module_host_actor::{AnonymousViewOp, ReducerOp, ReducerResult, ViewOp, ViewReturnData};

mod hooks;
mod v1;
Expand Down Expand Up @@ -84,7 +83,7 @@ pub(super) fn call_call_view(
scope: &mut PinScope<'_, '_>,
hooks: &HookFunctions<'_>,
op: ViewOp<'_>,
) -> Result<Bytes, ErrorOrException<ExceptionThrown>> {
) -> Result<ViewReturnData, ErrorOrException<ExceptionThrown>> {
match hooks.abi {
AbiVersion::V1 => v1::call_call_view(scope, hooks, op),
}
Expand All @@ -97,7 +96,7 @@ pub(super) fn call_call_view_anon(
scope: &mut PinScope<'_, '_>,
hooks: &HookFunctions<'_>,
op: AnonymousViewOp<'_>,
) -> Result<Bytes, ErrorOrException<ExceptionThrown>> {
) -> Result<ViewReturnData, ErrorOrException<ExceptionThrown>> {
match hooks.abi {
AbiVersion::V1 => v1::call_call_view_anon(scope, hooks, op),
}
Expand Down
78 changes: 68 additions & 10 deletions crates/core/src/host/v8/syscall/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::host::v8::{
TerminationError, Throwable,
};
use crate::host::wasm_common::instrumentation::span;
use crate::host::wasm_common::module_host_actor::{AnonymousViewOp, ReducerOp, ReducerResult, ViewOp};
use crate::host::wasm_common::module_host_actor::{AnonymousViewOp, ReducerOp, ReducerResult, ViewOp, ViewReturnData};
use crate::host::wasm_common::{err_to_errno_and_log, RowIterIdx, TimingSpan, TimingSpanIdx};
use crate::host::AbiCall;
use anyhow::Context;
Expand Down Expand Up @@ -420,7 +420,7 @@ pub(super) fn call_call_view(
scope: &mut PinScope<'_, '_>,
hooks: &HookFunctions<'_>,
op: ViewOp<'_>,
) -> Result<Bytes, ErrorOrException<ExceptionThrown>> {
) -> Result<ViewReturnData, ErrorOrException<ExceptionThrown>> {
let fun = hooks.call_view.context("`__call_view__` was never defined")?;

let ViewOp {
Expand All @@ -441,19 +441,46 @@ pub(super) fn call_call_view(
// Call the function.
let ret = call_free_fun(scope, fun, args)?;

// Deserialize the user result.
let ret = cast!(scope, ret, v8::Uint8Array, "bytes return from `__call_view__`").map_err(|e| e.throw(scope))?;
// The original version returned a byte array with the encoded rows.
if ret.is_typed_array() && ret.is_uint8_array() {
// This is the original format, which just returns the raw bytes.
let ret =
cast!(scope, ret, v8::Uint8Array, "bytes return from `__call_view_anon__`").map_err(|e| e.throw(scope))?;
let bytes = ret.get_contents(&mut []);

return Ok(ViewReturnData::Rows(Bytes::copy_from_slice(bytes)));
};

// The newer version returns an object with a `data` field containing the bytes.
let ret = cast!(scope, ret, v8::Object, "object return from `__call_view_anon__`").map_err(|e| e.throw(scope))?;

let Some(data_key) = v8::String::new(scope, "data") else {
return Err(ErrorOrException::Err(anyhow::anyhow!("error creating a v8 string")));
};
let Some(data_val) = ret.get(scope, data_key.into()) else {
return Err(ErrorOrException::Err(anyhow::anyhow!(
"data key not found in return object"
)));
};

let ret = cast!(
scope,
data_val,
v8::Uint8Array,
"bytes in the `data` field returned from `__call_view_anon__`"
)
.map_err(|e| e.throw(scope))?;
let bytes = ret.get_contents(&mut []);

Ok(Bytes::copy_from_slice(bytes))
Ok(ViewReturnData::HeaderFirst(Bytes::copy_from_slice(bytes)))
}

/// Calls the `__call_view_anon__` function `fun`.
pub(super) fn call_call_view_anon(
scope: &mut PinScope<'_, '_>,
hooks: &HookFunctions<'_>,
op: AnonymousViewOp<'_>,
) -> Result<Bytes, ErrorOrException<ExceptionThrown>> {
) -> Result<ViewReturnData, ErrorOrException<ExceptionThrown>> {
let fun = hooks.call_view_anon.context("`__call_view__` was never defined")?;

let AnonymousViewOp {
Expand All @@ -472,12 +499,43 @@ pub(super) fn call_call_view_anon(
// Call the function.
let ret = call_free_fun(scope, fun, args)?;

// Deserialize the user result.
let ret =
cast!(scope, ret, v8::Uint8Array, "bytes return from `__call_view_anon__`").map_err(|e| e.throw(scope))?;
if ret.is_typed_array() && ret.is_uint8_array() {
// This is the original format, which just returns the raw bytes.
let ret =
cast!(scope, ret, v8::Uint8Array, "bytes return from `__call_view_anon__`").map_err(|e| e.throw(scope))?;
let bytes = ret.get_contents(&mut []);

// We are pretending this was sent with the new format.
return Ok(ViewReturnData::Rows(Bytes::copy_from_slice(bytes)));
};

let ret = cast!(
scope,
ret,
v8::Object,
"bytes or object return from `__call_view_anon__`"
)
.map_err(|e| e.throw(scope))?;

let Some(data_key) = v8::String::new(scope, "data") else {
return Err(ErrorOrException::Err(anyhow::anyhow!("error creating a v8 string")));
};
let Some(data_val) = ret.get(scope, data_key.into()) else {
return Err(ErrorOrException::Err(anyhow::anyhow!(
"data key not found in return object"
)));
};

let ret = cast!(
scope,
data_val,
v8::Uint8Array,
"bytes in the `data` field returned from `__call_view_anon__`"
)
.map_err(|e| e.throw(scope))?;
let bytes = ret.get_contents(&mut []);

Ok(Bytes::copy_from_slice(bytes))
Ok(ViewReturnData::HeaderFirst(Bytes::copy_from_slice(bytes)))
}

/// Calls the registered `__describe_module__` function hook.
Expand Down
26 changes: 21 additions & 5 deletions crates/core/src/host/wasm_common/module_host_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,17 +150,27 @@ pub enum ExecutionError {
}

#[derive(derive_more::AsRef)]
pub struct ExecutionResult<T> {
pub struct ExecutionResult<T, E> {
#[as_ref]
pub stats: ExecutionStats,
pub call_result: T,
pub call_result: Result<T, E>,
}

pub type ReducerExecuteResult = ExecutionResult<Result<(), ExecutionError>>;
// pub type ReducerExecuteResult = ExecutionResult<Result<(), ExecutionError>>;
pub type ReducerExecuteResult = ExecutionResult<(), ExecutionError>;

pub type ViewExecuteResult = ExecutionResult<Result<Bytes, ExecutionError>>;
// The original version of views used a different return format (it returned the rows directly).
// The newer version uses ViewReturnData to represent the different formats.
pub enum ViewReturnData {
// This view returns a Vec of rows (bsatn encoded).
Rows(Bytes),
// This view returns a ViewResultHeader, potentially followed by more data.
HeaderFirst(Bytes),
}

pub type ViewExecuteResult = ExecutionResult<ViewReturnData, ExecutionError>;

pub type ProcedureExecuteResult = ExecutionResult<anyhow::Result<Bytes>>;
pub type ProcedureExecuteResult = ExecutionResult<Bytes, anyhow::Error>;

pub struct WasmModuleHostActor<T: WasmModule> {
module: T::InstancePre,
Expand Down Expand Up @@ -980,6 +990,9 @@ impl InstanceCommon {
}
// Materialize anonymous view
(Ok(bytes), None) => {
let ViewReturnData::Rows(bytes) = bytes else {
unimplemented!("View returned a non-rows format");
};
stdb.materialize_anonymous_view(&mut tx, table_id, row_type, bytes, self.info.module_def.typespace())
.inspect_err(|err| {
log::error!("Fatal error materializing view `{view_name}`: {err}");
Expand All @@ -989,6 +1002,9 @@ impl InstanceCommon {
}
// Materialize sender view
(Ok(bytes), Some(sender)) => {
let ViewReturnData::Rows(bytes) = bytes else {
unimplemented!("View returned a non-rows format");
};
stdb.materialize_view(
&mut tx,
table_id,
Expand Down
18 changes: 14 additions & 4 deletions crates/core/src/host/wasmtime/wasmtime_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::host::instance_env::{InstanceEnv, TxSlot};
use crate::host::module_common::run_describer;
use crate::host::wasm_common::module_host_actor::{
AnonymousViewOp, DescribeError, ExecutionError, ExecutionStats, InitializationError, InstanceOp, ViewOp,
ViewReturnData,
};
use crate::host::wasm_common::*;
use crate::replica_context::ReplicaContext;
Expand Down Expand Up @@ -109,6 +110,17 @@ fn handle_result_sink_code(code: i32, result: Vec<u8>) -> Result<Vec<u8>, Execut
}
}

/// Handle the return code from a view function using a result sink.
/// For views, we treat the return code 2 as a successful return using the header format.
fn handle_view_result_sink_code(code: i32, result: Vec<u8>) -> Result<ViewReturnData, ExecutionError> {
match code {
0 => Ok(ViewReturnData::Rows(result.into())),
2 => Ok(ViewReturnData::HeaderFirst(result.into())),
CALL_FAILURE => Err(ExecutionError::User(string_from_utf8_lossy_owned(result).into())),
_ => Err(ExecutionError::Recoverable(anyhow::anyhow!("unknown return code"))),
}
}

const CALL_FAILURE: i32 = HOST_CALL_FAILURE.get() as i32;

/// Invoke `typed_func` and assert that it doesn't yield.
Expand Down Expand Up @@ -451,8 +463,7 @@ impl module_host_actor::WasmInstance for WasmtimeInstance {

let call_result = call_result
.map_err(ExecutionError::Trap)
.and_then(|code| handle_result_sink_code(code, result_bytes))
.map(|r| r.into());
.and_then(|code| handle_view_result_sink_code(code, result_bytes));

module_host_actor::ViewExecuteResult { stats, call_result }
}
Expand Down Expand Up @@ -490,8 +501,7 @@ impl module_host_actor::WasmInstance for WasmtimeInstance {

let call_result = call_result
.map_err(ExecutionError::Trap)
.and_then(|code| handle_result_sink_code(code, result_bytes))
.map(|r| r.into());
.and_then(|code| handle_view_result_sink_code(code, result_bytes));

module_host_actor::ViewExecuteResult { stats, call_result }
}
Expand Down
25 changes: 25 additions & 0 deletions crates/lib/src/db/raw_def/v9.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,31 @@ pub struct RawViewDefV9 {
pub return_type: AlgebraicType,
}

#[derive(Debug, Clone, SpacetimeType)]
#[sats(crate = crate)]
pub enum ViewResultHeader {
// This means the row data will follow, as a bsatn-encoded Vec<RowType>.
// We could make RowData contain an Vec<u8> of the bytes, but that forces us to make an extra copy when we serialize and
// when we deserialize.
RowData,
// This means we the view wants to return the results of the sql query.
RawSql(String),
// This means we the view wants to return the results of a sql query.
// Any query parameters will follow the header.
ParameterizedQuery(ParameterizedQueryHeader),
}

#[derive(Debug, Clone, SpacetimeType)]
// A header for a parameterized query. This should be followed by a bsatn encoding of any parameters.
#[sats(crate = crate)]
pub struct ParameterizedQueryHeader {
// The sql query template. Add details on parameter syntax when it is supported.
pub template: String,
// If set, these are the types of the parameters.
// This is optional to support parameter inference in the future.
pub parameter_types: Option<ProductType>,
}

/// A reducer definition.
#[derive(Debug, Clone, SpacetimeType)]
#[sats(crate = crate)]
Expand Down
Loading