diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 2785a84d95a..51182d64a68 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -92,6 +92,11 @@ jobs: env: DOTNET_INSTALL_DIR: ~/.dotnet + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + - name: Build working-directory: crates/bench/ run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3eb7e9cada..291a17ad58b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,6 +80,13 @@ jobs: with: run_install: true + # Go is required for Go module compilation tests. + - name: Set up Go + if: runner.os == 'Linux' + uses: actions/setup-go@v5 + with: + go-version: '1.25' + # Install emscripten for C++ module compilation tests. - name: Install emscripten (Linux) if: runner.os == 'Linux' diff --git a/.github/workflows/go-sdk-test.yml b/.github/workflows/go-sdk-test.yml new file mode 100644 index 00000000000..b02e4d66231 --- /dev/null +++ b/.github/workflows/go-sdk-test.yml @@ -0,0 +1,36 @@ +name: Go SDK Tests + +on: + pull_request: + paths: + - 'sdks/go/**' + - 'crates/codegen/src/go.rs' + - '.github/workflows/go-sdk-test.yml' + push: + branches: + - master + paths: + - 'sdks/go/**' + - 'crates/codegen/src/go.rs' + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || format('sha-{0}', github.sha) }} + cancel-in-progress: true + +jobs: + go-unit-tests: + name: Go SDK Unit Tests + runs-on: spacetimedb-new-runner-2 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache-dependency-path: sdks/go/go.sum + - name: Run Go tests + working-directory: sdks/go + run: go test -v -count=1 ./... + - name: Run Go vet + working-directory: sdks/go + run: go vet ./... diff --git a/.gitignore b/.gitignore index debd5385bfd..2f62ea10b04 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ yarn-error.log* lerna-debug.log* .pnpm-debug.log* +### Go ### +*generated.go + # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/crates/bench/benches/generic.rs b/crates/bench/benches/generic.rs index 1013566df8b..3f30aaf9cce 100644 --- a/crates/bench/benches/generic.rs +++ b/crates/bench/benches/generic.rs @@ -11,7 +11,7 @@ use spacetimedb_bench::{ }; use spacetimedb_lib::sats::AlgebraicType; use spacetimedb_primitives::ColId; -use spacetimedb_testing::modules::{Csharp, Rust}; +use spacetimedb_testing::modules::{Csharp, GoBenchmarks, Rust}; #[cfg(target_env = "msvc")] #[global_allocator] @@ -32,12 +32,14 @@ fn criterion_benchmark(c: &mut Criterion) { bench_suite::(c, true).unwrap(); bench_suite::(c, true).unwrap(); bench_suite::>(c, true).unwrap(); - bench_suite::>(c, true).unwrap(); + // bench_suite::>(c, true).unwrap(); + bench_suite::>(c, true).unwrap(); bench_suite::(c, false).unwrap(); bench_suite::(c, false).unwrap(); bench_suite::>(c, false).unwrap(); - bench_suite::>(c, false).unwrap(); + // bench_suite::>(c, false).unwrap(); + bench_suite::>(c, false).unwrap(); } #[inline(never)] diff --git a/crates/cli/src/spacetime_config.rs b/crates/cli/src/spacetime_config.rs index 9c49ed5f22c..ba1a7c1b2a4 100644 --- a/crates/cli/src/spacetime_config.rs +++ b/crates/cli/src/spacetime_config.rs @@ -851,6 +851,7 @@ impl SpacetimeConfig { "typescript" => package_manager.map(|pm| pm.run_dev_command()).unwrap_or("npm run dev"), "rust" => "cargo run", "csharp" | "c#" => "dotnet run", + "go" | "golang" => "go run .", _ => "npm run dev", // default fallback }; Self { @@ -1156,6 +1157,11 @@ pub fn detect_client_command(project_dir: &Path) -> Option<(String, Option anyhow::Result &'static str { Language::Csharp => "csharp", Language::TypeScript => "typescript", Language::UnrealCpp => "unrealcpp", + Language::Go => "go", } } @@ -417,6 +421,7 @@ pub fn default_out_dir_for_language(lang: Language) -> Option { Language::Rust | Language::TypeScript => Some(PathBuf::from("src/module_bindings")), Language::Csharp => Some(PathBuf::from("module_bindings")), Language::UnrealCpp => None, + Language::Go => Some(PathBuf::from("module_bindings/")), } } @@ -517,6 +522,7 @@ pub async fn run_prepared_generate_configs( } Language::Rust => &Rust, Language::TypeScript => &TypeScript, + Language::Go => &Go, }; for OutputFile { filename, code } in generate(&module, gen_lang, &options) { @@ -679,11 +685,13 @@ pub enum Language { Rust, #[serde(alias = "uecpp", alias = "ue5cpp", alias = "unreal")] UnrealCpp, + #[serde(alias = "golang")] + Go, } impl clap::ValueEnum for Language { fn value_variants<'a>() -> &'a [Self] { - &[Self::Csharp, Self::TypeScript, Self::Rust, Self::UnrealCpp] + &[Self::Csharp, Self::TypeScript, Self::Rust, Self::UnrealCpp, Self::Go] } fn to_possible_value(&self) -> Option { Some(match self { @@ -691,6 +699,7 @@ impl clap::ValueEnum for Language { Self::TypeScript => clap::builder::PossibleValue::new("typescript").aliases(["ts", "TS"]), Self::Rust => clap::builder::PossibleValue::new("rust").aliases(["rs", "RS"]), Self::UnrealCpp => PossibleValue::new("unrealcpp").aliases(["uecpp", "ue5cpp", "unreal"]), + Self::Go => PossibleValue::new("go").aliases(["golang"]), }) } } @@ -703,6 +712,7 @@ impl Language { Language::Csharp => "C#", Language::TypeScript => "TypeScript", Language::UnrealCpp => "Unreal C++", + Language::Go => "Go", } } @@ -716,6 +726,16 @@ impl Language { Language::UnrealCpp => { // TODO: implement formatting. } + Language::Go => { + let files: Vec<_> = generated_files.iter().map(|f| f.as_os_str()).collect(); + let status = std::process::Command::new("gofmt") + .arg("-w") + .args(&files) + .status(); + if let Err(e) = status { + eprintln!("Warning: gofmt not available: {e}"); + } + } } Ok(()) @@ -1382,6 +1402,14 @@ mod tests { serde_json::from_value::(serde_json::Value::String("unreal".into())).unwrap(), Language::UnrealCpp ); + assert_eq!( + serde_json::from_value::(serde_json::Value::String("go".into())).unwrap(), + Language::Go + ); + assert_eq!( + serde_json::from_value::(serde_json::Value::String("golang".into())).unwrap(), + Language::Go + ); // Invalid language should error assert!(serde_json::from_value::(serde_json::Value::String("java".into())).is_err()); diff --git a/crates/cli/src/subcommands/init.rs b/crates/cli/src/subcommands/init.rs index 58d567ef85d..6777fc2658e 100644 --- a/crates/cli/src/subcommands/init.rs +++ b/crates/cli/src/subcommands/init.rs @@ -62,6 +62,7 @@ pub enum ServerLanguage { Csharp, TypeScript, Cpp, + Go, } impl ServerLanguage { @@ -71,6 +72,7 @@ impl ServerLanguage { ServerLanguage::Csharp => "csharp", ServerLanguage::TypeScript => "typescript", ServerLanguage::Cpp => "cpp", + ServerLanguage::Go => "go", } } @@ -80,6 +82,7 @@ impl ServerLanguage { "csharp" | "c#" => Ok(Some(ServerLanguage::Csharp)), "typescript" => Ok(Some(ServerLanguage::TypeScript)), "cpp" | "c++" | "cxx" => Ok(Some(ServerLanguage::Cpp)), + "go" | "golang" => Ok(Some(ServerLanguage::Go)), _ => Err(anyhow!("Unknown server language: {}", s)), } } @@ -90,6 +93,7 @@ pub enum ClientLanguage { Rust, Csharp, TypeScript, + Go, } impl ClientLanguage { @@ -98,6 +102,7 @@ impl ClientLanguage { ClientLanguage::Rust => "rust", ClientLanguage::Csharp => "csharp", ClientLanguage::TypeScript => "typescript", + ClientLanguage::Go => "go", } } @@ -106,6 +111,7 @@ impl ClientLanguage { "rust" => Ok(Some(ClientLanguage::Rust)), "csharp" | "c#" => Ok(Some(ClientLanguage::Csharp)), "typescript" => Ok(Some(ClientLanguage::TypeScript)), + "go" | "golang" => Ok(Some(ClientLanguage::Go)), _ => Err(anyhow!("Unknown client language: {}", s)), } } @@ -172,7 +178,7 @@ pub fn cli() -> clap::Command { .action(clap::ArgAction::SetTrue), ) .arg(Arg::new("lang").long("lang").value_name("LANG").help( - "Server language: rust, csharp, typescript, cpp (it can only be used when --template is not specified)", + "Server language: rust, csharp, typescript, cpp, go (it can only be used when --template is not specified)", )) .arg( Arg::new("template") @@ -851,7 +857,7 @@ async fn get_template_config_interactive( } } else if client_selection == none_index { // Ask for server language only - let server_lang_choices = vec!["Rust", "C#", "TypeScript"]; + let server_lang_choices = vec!["Rust", "C#", "TypeScript", "Go"]; let server_selection = Select::with_theme(&theme) .with_prompt("Select server language") .items(&server_lang_choices) @@ -862,6 +868,7 @@ async fn get_template_config_interactive( 0 => Some(ServerLanguage::Rust), 1 => Some(ServerLanguage::Csharp), 2 => Some(ServerLanguage::TypeScript), + 3 => Some(ServerLanguage::Go), _ => unreachable!("Invalid server language selection"), }; @@ -1054,6 +1061,23 @@ fn update_cargo_toml_name(dir: &Path, package_name: &str) -> anyhow::Result<()> Ok(()) } +fn update_go_mod(dir: &Path, project_name: &str) -> anyhow::Result<()> { + let go_mod_path = dir.join("go.mod"); + if go_mod_path.exists() { + let content = fs::read_to_string(&go_mod_path)?; + let updated = content.replace( + "example.com/my-spacetimedb-module", + &format!("example.com/{}", project_name), + ); + let updated = updated.replace( + "example.com/my-spacetimedb-client", + &format!("example.com/{}", project_name), + ); + fs::write(&go_mod_path, updated)?; + } + Ok(()) +} + pub fn update_csproj_server_to_nuget(dir: &Path) -> anyhow::Result<()> { if let Some(csproj_path) = find_first_csproj(dir)? { let original = @@ -1327,6 +1351,9 @@ fn init_builtin(config: &TemplateConfig, project_path: &Path, is_server_only: bo Some(ClientLanguage::Csharp) => { update_csproj_client_to_nuget(project_path)?; } + Some(ClientLanguage::Go) => { + update_go_mod(project_path, &config.project_name)?; + } None => {} } } @@ -1357,6 +1384,9 @@ fn init_builtin(config: &TemplateConfig, project_path: &Path, is_server_only: bo Some(ServerLanguage::Cpp) => { // No name update needed for C++ at the moment } + Some(ServerLanguage::Go) => { + update_go_mod(&server_dir, &config.project_name)?; + } None => {} } @@ -1419,6 +1449,11 @@ fn init_empty(config: &TemplateConfig, project_path: &Path) -> anyhow::Result<() let server_dir = project_path.join("spacetimedb"); init_empty_cpp_server(&server_dir, &config.project_name)?; } + Some(ServerLanguage::Go) => { + println!("Setting up Go server..."); + let server_dir = project_path.join("spacetimedb"); + init_empty_go_server(&server_dir, &config.project_name)?; + } None => {} } @@ -1445,6 +1480,12 @@ fn init_empty_cpp_server(server_dir: &Path, _project_name: &str) -> anyhow::Resu init_cpp_project(server_dir) } +fn init_empty_go_server(server_dir: &Path, project_name: &str) -> anyhow::Result<()> { + init_go_project(server_dir)?; + update_go_mod(server_dir, project_name)?; + Ok(()) +} + fn print_next_steps(config: &TemplateConfig, _project_path: &Path) -> anyhow::Result<()> { println!(); println!("{}", "Next steps:".bold()); @@ -1511,6 +1552,24 @@ fn print_next_steps(config: &TemplateConfig, _project_path: &Path) -> anyhow::Re } println!(" cargo run"); } + (TemplateType::Builtin, Some(ServerLanguage::Go), Some(ClientLanguage::Go)) => { + println!( + " spacetime publish --module-path spacetimedb {}{}", + if config.use_local { "--server local " } else { "" }, + config.project_name + ); + println!(" go run ."); + } + (TemplateType::Empty, _, Some(ClientLanguage::Go)) => { + if config.server_lang.is_some() { + println!( + " spacetime publish --module-path spacetimedb {}{}", + if config.use_local { "--server local " } else { "" }, + config.project_name + ); + } + println!(" go run ."); + } (_, _, _) => { println!(" # Follow the template's README for setup instructions"); } @@ -1610,6 +1669,23 @@ fn check_for_git() -> bool { false } +fn check_for_go() -> bool { + let exe_name = if std::env::consts::OS == "windows" { + "go.exe" + } else { + "go" + }; + if find_executable(exe_name).is_some() { + return true; + } + println!( + "{}", + "Warning: You have created a Go project, but Go is not installed. Visit https://go.dev/dl/ for installation instructions.\n" + .yellow() + ); + false +} + pub async fn exec(mut config: Config, args: &ArgMatches) -> anyhow::Result { let options = InitOptions::from_args(args); let is_interactive = !options.non_interactive; @@ -1742,6 +1818,42 @@ pub fn init_cpp_project(project_path: &Path) -> anyhow::Result<()> { Ok(()) } +pub fn init_go_project(project_path: &Path) -> anyhow::Result<()> { + let export_files = vec![ + ( + include_str!("../../../../templates/basic-go/spacetimedb/go.mod"), + "go.mod", + ), + ( + include_str!("../../../../templates/basic-go/spacetimedb/main.go"), + "main.go", + ), + ( + include_str!("../../../../templates/basic-go/spacetimedb/types.go"), + "types.go", + ), + ( + include_str!("../../../../templates/basic-go/spacetimedb/reducers.go"), + "reducers.go", + ), + ( + include_str!("../../../../templates/basic-go/spacetimedb/stdb_generated.go"), + "stdb_generated.go", + ), + ]; + + for data_file in export_files { + let path = project_path.join(data_file.1); + create_directory(path.parent().unwrap())?; + std::fs::write(path, data_file.0)?; + } + + check_for_go(); + check_for_git(); + + Ok(()) +} + pub async fn exec_init_rust(args: &ArgMatches) -> anyhow::Result<()> { let project_path = args.get_one::("project-path").unwrap(); init_rust_project(project_path)?; @@ -1766,6 +1878,18 @@ pub async fn exec_init_csharp(args: &ArgMatches) -> anyhow::Result<()> { Ok(()) } +pub async fn exec_init_go(args: &ArgMatches) -> anyhow::Result<()> { + let project_path = args.get_one::("project-path").unwrap(); + init_go_project(project_path)?; + + println!( + "{}", + format!("Project successfully created at path: {}", project_path.display()).green() + ); + + Ok(()) +} + fn create_directory(path: &Path) -> anyhow::Result<()> { std::fs::create_dir_all(path).context("Failed to create directory") } diff --git a/crates/cli/src/tasks/go.rs b/crates/cli/src/tasks/go.rs new file mode 100644 index 00000000000..4f4818de4ef --- /dev/null +++ b/crates/cli/src/tasks/go.rs @@ -0,0 +1,123 @@ +use anyhow::{anyhow, Context}; +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::detect::find_executable; + +pub(crate) fn build_go(project_path: &Path, build_debug: bool) -> anyhow::Result { + // Verify go is available + let go_found = find_executable("go").is_some(); + if !go_found { + return Err(anyhow!( + "`go` not found in PATH. \ + Install Go 1.24+ from https://go.dev/dl/" + )); + } + + // Ensure the project path exists + fs::metadata(project_path).with_context(|| { + format!( + "The provided project path '{}' does not exist.", + project_path.display() + ) + })?; + + // Ensure go.mod exists + if !project_path.join("go.mod").exists() { + return Err(anyhow!( + "No go.mod found in '{}'. Is this a Go module?", + project_path.display() + )); + } + + // Create output directory + let target_dir = project_path.join("target"); + fs::create_dir_all(&target_dir).with_context(|| { + format!("Failed to create target directory '{}'", target_dir.display()) + })?; + + let output_path = target_dir.join("module.wasm"); + + // Verify stdb-gen is available + let stdb_gen_found = find_executable("stdb-gen").is_some(); + if !stdb_gen_found { + return Err(anyhow!( + "`stdb-gen` not found in PATH. \ + Install it with: go install go.digitalxero.dev/stdb-gen@latest" + )); + } + + // Run stdb-gen upgrade to ensure latest codegen templates + eprintln!("Running stdb-gen upgrade..."); + duct::cmd!("stdb-gen", "upgrade") + .dir(project_path) + .run() + .with_context(|| "Failed to run stdb-gen upgrade.")?; + + // Run go generate to produce stdb_generated.go (code generation step) + eprintln!("Running go generate..."); + duct::cmd!("go", "generate", "./...") + .dir(project_path) + .run() + .with_context(|| "Failed to run go generate. Ensure stdb-gen is available.")?; + + // Build with standard Go compiler + // GOOS=wasip1 GOARCH=wasm produces WASI Preview 1 compatible WASM + // -buildmode=c-shared produces a reactor module (exports _initialize, keeps runtime alive) + // For release: strip debug info with -ldflags "-s -w" + eprintln!("Building Go module..."); + + let mut build_args = vec![ + "build".to_string(), + "-trimpath".to_string(), + "-tags=netgo,osusergo".to_string(), + "-buildmode=c-shared".to_string(), + ]; + + if !build_debug { + build_args.push("-ldflags=-s -w -extldflags -static".to_string()); + } + + build_args.push("-o".to_string()); + build_args.push(output_path.to_str().unwrap().to_string()); + build_args.push(".".to_string()); + + let mut cmd = duct::cmd("go", &build_args) + .env("GOOS", "wasip1") + .env("GOARCH", "wasm") + .env("GODEBUG", "asyncpreemptoff=1") + .dir(project_path); + + cmd.run() + .with_context(|| "Failed to build Go module")?; + + if !output_path.exists() { + return Err(anyhow!( + "Go build succeeded but output file '{}' not found.", + output_path.display() + )); + } + + Ok(output_path) +} + +pub(crate) fn gofmt(files: impl IntoIterator) -> anyhow::Result<()> { + let files: Vec = files.into_iter().collect(); + if files.is_empty() { + return Ok(()); + } + + let gofmt_found = find_executable("gofmt").is_some(); + if !gofmt_found { + eprintln!("Warning: `gofmt` not found in PATH, skipping formatting of generated Go files."); + return Ok(()); + } + + for file in &files { + duct::cmd!("gofmt", "-w", file.to_str().unwrap()) + .run() + .with_context(|| format!("Failed to format '{}'", file.display()))?; + } + + Ok(()) +} diff --git a/crates/cli/src/tasks/mod.rs b/crates/cli/src/tasks/mod.rs index 16414efbe97..b7d71c68a03 100644 --- a/crates/cli/src/tasks/mod.rs +++ b/crates/cli/src/tasks/mod.rs @@ -4,6 +4,7 @@ use crate::util::{self, ModuleLanguage}; use self::cpp::build_cpp; use self::csharp::build_csharp; +use self::go::build_go; use self::javascript::build_javascript; use self::rust::build_rust; @@ -25,6 +26,7 @@ pub fn build( ModuleLanguage::Csharp => build_csharp(project_path, build_debug), ModuleLanguage::Javascript => build_javascript(project_path, build_debug), ModuleLanguage::Cpp => build_cpp(project_path, build_debug), + ModuleLanguage::Go => build_go(project_path, build_debug), }?; if lang == ModuleLanguage::Javascript { @@ -59,5 +61,6 @@ pub fn build( pub mod cpp; pub mod csharp; +pub mod go; pub mod javascript; pub mod rust; diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 492068cba99..afe2b48a53c 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -207,10 +207,11 @@ pub enum ModuleLanguage { Rust, Javascript, Cpp, + Go, } impl clap::ValueEnum for ModuleLanguage { fn value_variants<'a>() -> &'a [Self] { - &[Self::Csharp, Self::Rust, Self::Javascript, Self::Cpp] + &[Self::Csharp, Self::Rust, Self::Javascript, Self::Cpp, Self::Go] } fn to_possible_value(&self) -> Option { match self { @@ -227,6 +228,7 @@ impl clap::ValueEnum for ModuleLanguage { "es", ])), Self::Cpp => Some(clap::builder::PossibleValue::new("cpp").aliases(["c++", "cxx", "C++", "Cpp"])), + Self::Go => Some(clap::builder::PossibleValue::new("go").aliases(["golang", "Go"])), } } } @@ -271,6 +273,8 @@ pub fn detect_module_language(path_to_module: &Path) -> anyhow::Result OutputFile { + let mut output = CodeIndenter::new(String::new(), INDENT); + let out = &mut output; + + print_go_header(out); + + let type_name = table.accessor_name.deref().to_case(Case::Pascal); + let product_def = module.typespace_for_generate()[table.product_type_ref] + .as_product() + .unwrap(); + + // Table handle type + writeln!(out, "// {type_name}TableHandle provides access to the {type_name} table."); + writeln!(out, "type {type_name}TableHandle struct {{"); + out.with_indent(|out| { + writeln!(out, "conn stdb.TableAccessor"); + }); + writeln!(out, "}}"); + writeln!(out); + + // Count method + writeln!(out, "func (t *{type_name}TableHandle) Count() int {{"); + out.with_indent(|out| { + writeln!(out, "return t.conn.TableRowCount(\"{}\") ", table.name); + }); + writeln!(out, "}}"); + writeln!(out); + + // Iter method + writeln!( + out, + "func (t *{type_name}TableHandle) Iter(fn func(row *{type_name}) bool) {{" + ); + out.with_indent(|out| { + writeln!(out, "t.conn.TableIter(\"{}\", func(reader stdb.Reader) bool {{", table.name); + out.with_indent(|out| { + writeln!(out, "row, err := Read{type_name}(reader)"); + writeln!(out, "if err != nil {{"); + out.with_indent(|out| { + writeln!(out, "return false"); + }); + writeln!(out, "}}"); + writeln!(out, "return fn(row)"); + }); + writeln!(out, "}})"); + }); + writeln!(out, "}}"); + writeln!(out); + + // Generate unique column find methods + for (col_name, col_type) in iter_unique_cols( + module.typespace_for_generate(), + &schema, + product_def, + ) { + let col_name_pascal = col_name.deref().to_case(Case::Pascal); + let col_type_str = ty_fmt(module, col_type); + + writeln!( + out, + "func (t *{type_name}TableHandle) FindBy{col_name_pascal}(val {col_type_str}) (*{type_name}, bool) {{" + ); + out.with_indent(|out| { + writeln!( + out, + "var result *{type_name}" + ); + writeln!(out, "found := false"); + writeln!(out, "t.Iter(func(row *{type_name}) bool {{"); + out.with_indent(|out| { + writeln!(out, "if row.{col_name_pascal} == val {{"); + out.with_indent(|out| { + writeln!(out, "result = row"); + writeln!(out, "found = true"); + writeln!(out, "return false"); + }); + writeln!(out, "}}"); + writeln!(out, "return true"); + }); + writeln!(out, "}})"); + writeln!(out, "return result, found"); + }); + writeln!(out, "}}"); + writeln!(out); + } + + // OnInsert callback + writeln!( + out, + "func (t *{type_name}TableHandle) OnInsert(cb func(ctx stdb.EventContext, row *{type_name})) stdb.CallbackId {{" + ); + out.with_indent(|out| { + writeln!( + out, + "return t.conn.OnInsert(\"{}\", func(ctx stdb.EventContext, reader stdb.Reader) {{", + table.name + ); + out.with_indent(|out| { + writeln!(out, "row, err := Read{type_name}(reader)"); + writeln!(out, "if err != nil {{"); + out.with_indent(|out| { + writeln!(out, "return"); + }); + writeln!(out, "}}"); + writeln!(out, "cb(ctx, row)"); + }); + writeln!(out, "}})"); + }); + writeln!(out, "}}"); + writeln!(out); + + // OnDelete callback + writeln!( + out, + "func (t *{type_name}TableHandle) OnDelete(cb func(ctx stdb.EventContext, row *{type_name})) stdb.CallbackId {{" + ); + out.with_indent(|out| { + writeln!( + out, + "return t.conn.OnDelete(\"{}\", func(ctx stdb.EventContext, reader stdb.Reader) {{", + table.name + ); + out.with_indent(|out| { + writeln!(out, "row, err := Read{type_name}(reader)"); + writeln!(out, "if err != nil {{"); + out.with_indent(|out| { + writeln!(out, "return"); + }); + writeln!(out, "}}"); + writeln!(out, "cb(ctx, row)"); + }); + writeln!(out, "}})"); + }); + writeln!(out, "}}"); + + let filename = format!( + "{}_table.go", + table.accessor_name.deref().to_case(Case::Snake) + ); + + OutputFile { + filename, + code: output.into_inner(), + } + } + + fn generate_type_files(&self, module: &ModuleDef, typ: &TypeDef) -> Vec { + let name = collect_case(Case::Pascal, typ.accessor_name.name_segments()); + let filename = format!("{}_type.go", collect_case(Case::Snake, typ.accessor_name.name_segments())); + let code = match &module.typespace_for_generate()[typ.ty] { + AlgebraicTypeDef::Product(prod) => gen_go_product(module, &name, prod), + AlgebraicTypeDef::Sum(sum) => gen_go_sum(module, &name, sum), + AlgebraicTypeDef::PlainEnum(plain_enum) => gen_go_plain_enum(&name, plain_enum), + }; + + vec![OutputFile { filename, code }] + } + + fn generate_reducer_file(&self, module: &ModuleDef, reducer: &ReducerDef) -> OutputFile { + let mut output = CodeIndenter::new(String::new(), INDENT); + let out = &mut output; + + print_go_header(out); + + let func_name = reducer.accessor_name.deref().to_case(Case::Pascal); + + if is_reducer_invokable(reducer) { + // Generate the call method on RemoteReducers + write!(out, "func (r *RemoteReducers) {func_name}("); + for (i, (arg_name, arg_type)) in reducer.params_for_generate.elements.iter().enumerate() + { + if i != 0 { + write!(out, ", "); + } + let arg_name_camel = arg_name.deref().to_case(Case::Camel); + write!(out, "{arg_name_camel} {}", ty_fmt(module, arg_type)); + } + writeln!(out, ") {{"); + out.with_indent(|out| { + // Build args for the BSATN call + writeln!(out, "args := func(w stdb.Writer) {{"); + out.with_indent(|out| { + for (arg_name, arg_type) in reducer.params_for_generate.elements.iter() { + let arg_name_camel = arg_name.deref().to_case(Case::Camel); + write_bsatn_encode(out, module, &arg_name_camel, arg_type); + } + }); + writeln!(out, "}}"); + writeln!(out, "r.conn.CallReducer(\"{}\", args)", reducer.name); + }); + writeln!(out, "}}"); + writeln!(out); + } + + // Generate the callback registration method + write!( + out, + "func (r *RemoteReducers) On{func_name}(cb func(ctx stdb.ReducerEventContext" + ); + for (arg_name, arg_type) in reducer.params_for_generate.elements.iter() { + let arg_name_camel = arg_name.deref().to_case(Case::Camel); + write!(out, ", {} {}", arg_name_camel, ty_fmt(module, arg_type)); + } + writeln!(out, ")) stdb.CallbackId {{"); + out.with_indent(|out| { + writeln!( + out, + "return r.conn.OnReducer(\"{}\", func(ctx stdb.ReducerEventContext, reader stdb.Reader) {{", + reducer.name + ); + out.with_indent(|out| { + for (arg_name, arg_type) in reducer.params_for_generate.elements.iter() { + let arg_name_camel = arg_name.deref().to_case(Case::Camel); + write_bsatn_decode(out, module, &arg_name_camel, arg_type); + } + write!(out, "cb(ctx"); + for (arg_name, _) in reducer.params_for_generate.elements.iter() { + let arg_name_camel = arg_name.deref().to_case(Case::Camel); + write!(out, ", {arg_name_camel}"); + } + writeln!(out, ")"); + }); + writeln!(out, "}})"); + }); + writeln!(out, "}}"); + + let filename = format!( + "{}_reducer.go", + reducer.accessor_name.deref().to_case(Case::Snake) + ); + + OutputFile { + filename, + code: output.into_inner(), + } + } + + fn generate_procedure_file(&self, module: &ModuleDef, procedure: &ProcedureDef) -> OutputFile { + let mut output = CodeIndenter::new(String::new(), INDENT); + let out = &mut output; + + print_go_header(out); + + let func_name = procedure.accessor_name.deref().to_case(Case::Pascal); + let return_type_str = ty_fmt(module, &procedure.return_type_for_generate); + + // Generate the call method on RemoteProcedures + write!(out, "func (p *RemoteProcedures) {func_name}("); + for (i, (arg_name, arg_type)) in procedure.params_for_generate.elements.iter().enumerate() { + if i != 0 { + write!(out, ", "); + } + let arg_name_camel = arg_name.deref().to_case(Case::Camel); + write!(out, "{arg_name_camel} {}", ty_fmt(module, arg_type)); + } + if !procedure.params_for_generate.elements.is_empty() { + write!(out, ", "); + } + writeln!(out, "cb func(ctx stdb.ProcedureEventContext, result {return_type_str}, err error)) {{"); + out.with_indent(|out| { + writeln!(out, "args := func(w stdb.Writer) {{"); + out.with_indent(|out| { + for (arg_name, arg_type) in procedure.params_for_generate.elements.iter() { + let arg_name_camel = arg_name.deref().to_case(Case::Camel); + write_bsatn_encode(out, module, &arg_name_camel, arg_type); + } + }); + writeln!(out, "}}"); + writeln!( + out, + "p.conn.CallProcedure(\"{}\", args, func(ctx stdb.ProcedureEventContext, reader stdb.Reader) {{", + procedure.name + ); + out.with_indent(|out| { + writeln!(out, "// Decode the return value"); + let return_var = "result"; + write_bsatn_decode(out, module, return_var, &procedure.return_type_for_generate); + writeln!(out, "cb(ctx, {return_var}, nil)"); + }); + writeln!(out, "}})"); + }); + writeln!(out, "}}"); + + let filename = format!( + "{}_procedure.go", + procedure.accessor_name.deref().to_case(Case::Snake) + ); + + OutputFile { + filename, + code: output.into_inner(), + } + } + + fn generate_global_files(&self, module: &ModuleDef, options: &CodegenOptions) -> Vec { + let mut output = CodeIndenter::new(String::new(), INDENT); + let out = &mut output; + + print_auto_generated_file_comment(out); + print_auto_generated_version_comment(out); + writeln!(out, "package module_bindings"); + writeln!(out); + writeln!(out, "import ("); + out.with_indent(|out| { + writeln!(out, "{SDK_IMPORT}"); + }); + writeln!(out, ")"); + writeln!(out); + + // RemoteTables struct + writeln!(out, "// RemoteTables provides access to all tables in the module."); + writeln!(out, "type RemoteTables struct {{"); + out.with_indent(|out| { + for (_, accessor_name, _) in iter_table_names_and_types(module, options.visibility) { + let table_name_pascal = accessor_name.deref().to_case(Case::Pascal); + writeln!(out, "{table_name_pascal} *{table_name_pascal}TableHandle"); + } + }); + writeln!(out, "}}"); + writeln!(out); + + // RemoteReducers struct + writeln!(out, "// RemoteReducers provides access to all reducers in the module."); + writeln!(out, "type RemoteReducers struct {{"); + out.with_indent(|out| { + writeln!(out, "conn stdb.DbConnection"); + }); + writeln!(out, "}}"); + writeln!(out); + + // RemoteProcedures struct + writeln!(out, "// RemoteProcedures provides access to all procedures in the module."); + writeln!(out, "type RemoteProcedures struct {{"); + out.with_indent(|out| { + writeln!(out, "conn stdb.DbConnection"); + }); + writeln!(out, "}}"); + writeln!(out); + + // Reducer enum type + writeln!(out, "// Reducer identifies a reducer in the module."); + writeln!(out, "type Reducer uint32"); + writeln!(out); + writeln!(out, "const ("); + out.with_indent(|out| { + let mut first = true; + for reducer in iter_reducers(module, options.visibility) { + let reducer_name = reducer.accessor_name.deref().to_case(Case::Pascal); + if first { + writeln!(out, "Reducer{reducer_name} Reducer = iota"); + first = false; + } else { + writeln!(out, "Reducer{reducer_name}"); + } + } + }); + writeln!(out, ")"); + writeln!(out); + + // DbConnection struct + writeln!(out, "// DbConnection represents a connection to a SpacetimeDB module."); + writeln!(out, "type DbConnection struct {{"); + out.with_indent(|out| { + writeln!(out, "stdb.DbConnection"); + writeln!(out, "Db *RemoteTables"); + writeln!(out, "Reducers *RemoteReducers"); + writeln!(out, "Procedures *RemoteProcedures"); + }); + writeln!(out, "}}"); + writeln!(out); + + // NewDbConnection function + writeln!(out, "// NewDbConnection creates a new DbConnectionBuilder for connecting to a SpacetimeDB module."); + writeln!(out, "func NewDbConnection() stdb.DbConnectionBuilder {{"); + out.with_indent(|out| { + writeln!(out, "return stdb.NewDbConnectionBuilder()"); + }); + writeln!(out, "}}"); + + vec![OutputFile { + filename: "module_bindings.go".to_owned(), + code: output.into_inner(), + }] + } +} + +// --- Helper functions --- + +/// Print the Go file header with auto-generated comment, package declaration, and import. +fn print_go_header(out: &mut CodeIndenter) { + print_auto_generated_file_comment(out); + writeln!(out, "package module_bindings"); + writeln!(out); + writeln!(out, "import ("); + out.with_indent(|out| { + writeln!(out, "{SDK_IMPORT}"); + }); + writeln!(out, ")"); + writeln!(out); +} + +/// Format an `AlgebraicTypeUse` as a Go type string. +fn ty_fmt<'a>(module: &'a ModuleDef, ty: &'a AlgebraicTypeUse) -> impl fmt::Display + 'a { + fmt_fn(move |f| match ty { + AlgebraicTypeUse::Identity => f.write_str("stdb.Identity"), + AlgebraicTypeUse::ConnectionId => f.write_str("stdb.ConnectionId"), + AlgebraicTypeUse::ScheduleAt => f.write_str("stdb.ScheduleAt"), + AlgebraicTypeUse::Timestamp => f.write_str("stdb.Timestamp"), + AlgebraicTypeUse::TimeDuration => f.write_str("stdb.TimeDuration"), + AlgebraicTypeUse::Uuid => f.write_str("stdb.Uuid"), + AlgebraicTypeUse::Unit => f.write_str("struct{}"), + AlgebraicTypeUse::Option(inner_ty) => write!(f, "*{}", ty_fmt(module, inner_ty)), + AlgebraicTypeUse::Result { ok_ty, err_ty } => { + write!( + f, + "stdb.Result[{}, {}]", + ty_fmt(module, ok_ty), + ty_fmt(module, err_ty) + ) + } + AlgebraicTypeUse::Array(elem_ty) => { + // Special case: []byte for Array(U8) + if matches!(elem_ty.as_ref(), AlgebraicTypeUse::Primitive(PrimitiveType::U8)) { + f.write_str("[]byte") + } else { + write!(f, "[]{}", ty_fmt(module, elem_ty)) + } + } + AlgebraicTypeUse::String => f.write_str("string"), + AlgebraicTypeUse::Ref(r) => f.write_str(&type_ref_name(module, *r)), + AlgebraicTypeUse::Primitive(prim) => f.write_str(match prim { + PrimitiveType::Bool => "bool", + PrimitiveType::I8 => "int8", + PrimitiveType::U8 => "uint8", + PrimitiveType::I16 => "int16", + PrimitiveType::U16 => "uint16", + PrimitiveType::I32 => "int32", + PrimitiveType::U32 => "uint32", + PrimitiveType::I64 => "int64", + PrimitiveType::U64 => "uint64", + PrimitiveType::I128 => "stdb.Int128", + PrimitiveType::U128 => "stdb.Uint128", + PrimitiveType::I256 => "stdb.Int256", + PrimitiveType::U256 => "stdb.Uint256", + PrimitiveType::F32 => "float32", + PrimitiveType::F64 => "float64", + }), + AlgebraicTypeUse::Never => unimplemented!("never types are not yet supported in Go output"), + }) +} + +/// Generate a Go product type (struct). +fn gen_go_product(module: &ModuleDef, name: &str, product: &ProductTypeDef) -> String { + let mut output = CodeIndenter::new(String::new(), INDENT); + let out = &mut output; + + print_go_header(out); + + // Struct definition + writeln!(out, "type {name} struct {{"); + out.with_indent(|out| { + for (field_name, field_type) in product.elements.iter() { + let go_field_name = field_name.deref().to_case(Case::Pascal); + let go_type = ty_fmt(module, field_type); + let tag_name = field_name.deref(); + writeln!(out, "{go_field_name} {go_type} `stdb:\"{tag_name}\"`"); + } + }); + writeln!(out, "}}"); + writeln!(out); + + // WriteBsatn method + writeln!(out, "func (v *{name}) WriteBsatn(w stdb.Writer) {{"); + out.with_indent(|out| { + for (field_name, field_type) in product.elements.iter() { + let go_field_name = field_name.deref().to_case(Case::Pascal); + let accessor = format!("v.{go_field_name}"); + write_bsatn_encode(out, module, &accessor, field_type); + } + }); + writeln!(out, "}}"); + writeln!(out); + + // Read function + writeln!(out, "func Read{name}(r stdb.Reader) (*{name}, error) {{"); + out.with_indent(|out| { + writeln!(out, "var v {name}"); + writeln!(out, "var err error"); + for (field_name, field_type) in product.elements.iter() { + let go_field_name = field_name.deref().to_case(Case::Pascal); + write_bsatn_field_decode(out, module, &go_field_name, field_type); + } + writeln!(out, "return &v, nil"); + }); + writeln!(out, "}}"); + + output.into_inner() +} + +/// Generate a Go sum type (tagged union via interface). +fn gen_go_sum(module: &ModuleDef, name: &str, sum: &SumTypeDef) -> String { + let mut output = CodeIndenter::new(String::new(), INDENT); + let out = &mut output; + + print_go_header(out); + + let marker_method = format!("is{name}"); + + // Interface definition + writeln!(out, "type {name} interface {{"); + out.with_indent(|out| { + writeln!(out, "{marker_method}()"); + writeln!(out, "Tag() uint8"); + }); + writeln!(out, "}}"); + writeln!(out); + + // Generate variant types + for (tag, (variant_name, variant_type)) in sum.variants.iter().enumerate() { + let variant_pascal = variant_name.deref().to_case(Case::Pascal); + let full_name = format!("{name}{variant_pascal}"); + + match variant_type { + AlgebraicTypeUse::Unit => { + writeln!(out, "type {full_name} struct{{}}"); + } + _ => { + writeln!(out, "type {full_name} struct {{"); + out.with_indent(|out| { + writeln!(out, "Value {}", ty_fmt(module, variant_type)); + }); + writeln!(out, "}}"); + } + } + writeln!(out); + + writeln!(out, "func ({full_name}) {marker_method}() {{}}"); + writeln!(out, "func ({full_name}) Tag() uint8 {{ return {tag} }}"); + writeln!(out); + } + + // Write function + writeln!(out, "func Write{name}(w stdb.Writer, val {name}) {{"); + out.with_indent(|out| { + writeln!(out, "w.PutU8(val.Tag())"); + writeln!(out, "switch v := val.(type) {{"); + for (variant_name, variant_type) in sum.variants.iter() { + let variant_pascal = variant_name.deref().to_case(Case::Pascal); + let full_name = format!("{name}{variant_pascal}"); + + writeln!(out, "case {full_name}:"); + if !matches!(variant_type, AlgebraicTypeUse::Unit) { + out.with_indent(|out| { + write_bsatn_encode(out, module, "v.Value", variant_type); + }); + } + } + writeln!(out, "}}"); + }); + writeln!(out, "}}"); + writeln!(out); + + // Read function + writeln!(out, "func Read{name}(r stdb.Reader) ({name}, error) {{"); + out.with_indent(|out| { + writeln!(out, "tag, err := r.GetU8()"); + writeln!(out, "if err != nil {{"); + out.with_indent(|out| { + writeln!(out, "return nil, err"); + }); + writeln!(out, "}}"); + writeln!(out, "switch tag {{"); + for (tag, (variant_name, variant_type)) in sum.variants.iter().enumerate() { + let variant_pascal = variant_name.deref().to_case(Case::Pascal); + let full_name = format!("{name}{variant_pascal}"); + + writeln!(out, "case {tag}:"); + out.with_indent(|out| { + if matches!(variant_type, AlgebraicTypeUse::Unit) { + writeln!(out, "return {full_name}{{}}, nil"); + } else { + write_bsatn_decode(out, module, "val", variant_type); + writeln!(out, "return {full_name}{{Value: val}}, nil"); + } + }); + } + writeln!(out, "default:"); + out.with_indent(|out| { + writeln!( + out, + "return nil, stdb.ErrUnknownTag(\"{name}\", tag)" + ); + }); + writeln!(out, "}}"); + }); + writeln!(out, "}}"); + + output.into_inner() +} + +/// Generate a Go plain enum (all unit variants). +fn gen_go_plain_enum(name: &str, plain_enum: &PlainEnumTypeDef) -> String { + let mut output = CodeIndenter::new(String::new(), INDENT); + let out = &mut output; + + print_go_header(out); + + writeln!(out, "type {name} uint8"); + writeln!(out); + writeln!(out, "const ("); + out.with_indent(|out| { + for (i, variant) in plain_enum.variants.iter().enumerate() { + let variant_pascal = variant.deref().to_case(Case::Pascal); + if i == 0 { + writeln!(out, "{name}{variant_pascal} {name} = iota"); + } else { + writeln!(out, "{name}{variant_pascal}"); + } + } + }); + writeln!(out, ")"); + writeln!(out); + + // String method + writeln!(out, "func (e {name}) String() string {{"); + out.with_indent(|out| { + writeln!(out, "switch e {{"); + for variant in plain_enum.variants.iter() { + let variant_pascal = variant.deref().to_case(Case::Pascal); + let variant_str = variant.deref(); + writeln!(out, "case {name}{variant_pascal}:"); + out.with_indent(|out| { + writeln!(out, "return \"{variant_str}\""); + }); + } + writeln!(out, "default:"); + out.with_indent(|out| { + writeln!(out, "return \"unknown\""); + }); + writeln!(out, "}}"); + }); + writeln!(out, "}}"); + writeln!(out); + + // WriteBsatn method + writeln!(out, "func (e {name}) WriteBsatn(w stdb.Writer) {{"); + out.with_indent(|out| { + writeln!(out, "w.PutU8(uint8(e))"); + }); + writeln!(out, "}}"); + writeln!(out); + + // Read function + writeln!(out, "func Read{name}(r stdb.Reader) ({name}, error) {{"); + out.with_indent(|out| { + writeln!(out, "val, err := r.GetU8()"); + writeln!(out, "if err != nil {{"); + out.with_indent(|out| { + writeln!(out, "return 0, err"); + }); + writeln!(out, "}}"); + writeln!(out, "return {name}(val), nil"); + }); + writeln!(out, "}}"); + + output.into_inner() +} + +/// Write BSATN encoding for a given Go expression and type. +fn write_bsatn_encode( + out: &mut CodeIndenter, + module: &ModuleDef, + expr: &str, + ty: &AlgebraicTypeUse, +) { + match ty { + AlgebraicTypeUse::Primitive(prim) => { + let method = match prim { + PrimitiveType::Bool => "PutBool", + PrimitiveType::I8 => "PutI8", + PrimitiveType::U8 => "PutU8", + PrimitiveType::I16 => "PutI16", + PrimitiveType::U16 => "PutU16", + PrimitiveType::I32 => "PutI32", + PrimitiveType::U32 => "PutU32", + PrimitiveType::I64 => "PutI64", + PrimitiveType::U64 => "PutU64", + PrimitiveType::I128 => "PutI128", + PrimitiveType::U128 => "PutU128", + PrimitiveType::I256 => "PutI256", + PrimitiveType::U256 => "PutU256", + PrimitiveType::F32 => "PutF32", + PrimitiveType::F64 => "PutF64", + }; + writeln!(out, "w.{method}({expr})"); + } + AlgebraicTypeUse::String => { + writeln!(out, "w.PutString({expr})"); + } + AlgebraicTypeUse::Identity => { + writeln!(out, "{expr}.WriteBsatn(w)"); + } + AlgebraicTypeUse::ConnectionId => { + writeln!(out, "{expr}.WriteBsatn(w)"); + } + AlgebraicTypeUse::Timestamp => { + writeln!(out, "{expr}.WriteBsatn(w)"); + } + AlgebraicTypeUse::TimeDuration => { + writeln!(out, "{expr}.WriteBsatn(w)"); + } + AlgebraicTypeUse::ScheduleAt => { + writeln!(out, "{expr}.WriteBsatn(w)"); + } + AlgebraicTypeUse::Uuid => { + writeln!(out, "{expr}.WriteBsatn(w)"); + } + AlgebraicTypeUse::Unit => { + // Nothing to write for unit + } + AlgebraicTypeUse::Ref(r) => { + let type_def = &module.typespace_for_generate()[r]; + match type_def { + AlgebraicTypeDef::Product(_) => { + writeln!(out, "{expr}.WriteBsatn(w)"); + } + AlgebraicTypeDef::Sum(_) => { + let type_name = type_ref_name(module, *r); + writeln!(out, "Write{type_name}(w, {expr})"); + } + AlgebraicTypeDef::PlainEnum(_) => { + writeln!(out, "{expr}.WriteBsatn(w)"); + } + } + } + AlgebraicTypeUse::Array(elem_ty) => { + if matches!(elem_ty.as_ref(), AlgebraicTypeUse::Primitive(PrimitiveType::U8)) { + writeln!(out, "w.PutBytes({expr})"); + } else { + writeln!(out, "w.PutU32(uint32(len({expr})))"); + writeln!(out, "for _, item := range {expr} {{"); + out.with_indent(|out| { + write_bsatn_encode(out, module, "item", elem_ty); + }); + writeln!(out, "}}"); + } + } + AlgebraicTypeUse::Option(inner_ty) => { + writeln!(out, "if {expr} != nil {{"); + out.with_indent(|out| { + writeln!(out, "w.PutU8(1)"); + write_bsatn_encode(out, module, &format!("*{expr}"), inner_ty); + }); + writeln!(out, "}} else {{"); + out.with_indent(|out| { + writeln!(out, "w.PutU8(0)"); + }); + writeln!(out, "}}"); + } + AlgebraicTypeUse::Result { .. } => { + writeln!(out, "{expr}.WriteBsatn(w)"); + } + AlgebraicTypeUse::Never => unimplemented!("never types are not yet supported in Go output"), + } +} + +/// Write BSATN decoding for a type, binding the result to a variable name. +fn write_bsatn_decode( + out: &mut CodeIndenter, + module: &ModuleDef, + var_name: &str, + ty: &AlgebraicTypeUse, +) { + match ty { + AlgebraicTypeUse::Primitive(prim) => { + let method = match prim { + PrimitiveType::Bool => "GetBool", + PrimitiveType::I8 => "GetI8", + PrimitiveType::U8 => "GetU8", + PrimitiveType::I16 => "GetI16", + PrimitiveType::U16 => "GetU16", + PrimitiveType::I32 => "GetI32", + PrimitiveType::U32 => "GetU32", + PrimitiveType::I64 => "GetI64", + PrimitiveType::U64 => "GetU64", + PrimitiveType::I128 => "GetI128", + PrimitiveType::U128 => "GetU128", + PrimitiveType::I256 => "GetI256", + PrimitiveType::U256 => "GetU256", + PrimitiveType::F32 => "GetF32", + PrimitiveType::F64 => "GetF64", + }; + writeln!(out, "{var_name}, _ := r.{method}()"); + } + AlgebraicTypeUse::String => { + writeln!(out, "{var_name}, _ := r.GetString()"); + } + AlgebraicTypeUse::Identity => { + writeln!(out, "{var_name}, _ := stdb.ReadIdentity(r)"); + } + AlgebraicTypeUse::ConnectionId => { + writeln!(out, "{var_name}, _ := stdb.ReadConnectionId(r)"); + } + AlgebraicTypeUse::Timestamp => { + writeln!(out, "{var_name}, _ := stdb.ReadTimestamp(r)"); + } + AlgebraicTypeUse::TimeDuration => { + writeln!(out, "{var_name}, _ := stdb.ReadTimeDuration(r)"); + } + AlgebraicTypeUse::ScheduleAt => { + writeln!(out, "{var_name}, _ := stdb.ReadScheduleAt(r)"); + } + AlgebraicTypeUse::Uuid => { + writeln!(out, "{var_name}, _ := stdb.ReadUuid(r)"); + } + AlgebraicTypeUse::Unit => { + writeln!(out, "{var_name} := struct{{}}{{}}"); + } + AlgebraicTypeUse::Ref(r) => { + let type_name = type_ref_name(module, *r); + let type_def = &module.typespace_for_generate()[r]; + match type_def { + AlgebraicTypeDef::Product(_) => { + writeln!(out, "{var_name}, _ := Read{type_name}(r)"); + } + AlgebraicTypeDef::Sum(_) => { + writeln!(out, "{var_name}, _ := Read{type_name}(r)"); + } + AlgebraicTypeDef::PlainEnum(_) => { + writeln!(out, "{var_name}, _ := Read{type_name}(r)"); + } + } + } + AlgebraicTypeUse::Array(elem_ty) => { + if matches!(elem_ty.as_ref(), AlgebraicTypeUse::Primitive(PrimitiveType::U8)) { + writeln!(out, "{var_name}, _ := r.GetBytes()"); + } else { + let elem_type_str = ty_fmt(module, elem_ty); + writeln!(out, "{var_name}Len, _ := r.GetU32()"); + writeln!( + out, + "{var_name} := make([]{elem_type_str}, {var_name}Len)" + ); + writeln!(out, "for i := uint32(0); i < {var_name}Len; i++ {{"); + out.with_indent(|out| { + write_bsatn_decode(out, module, "elem", elem_ty); + writeln!(out, "{var_name}[i] = elem"); + }); + writeln!(out, "}}"); + } + } + AlgebraicTypeUse::Option(inner_ty) => { + let inner_type_str = ty_fmt(module, inner_ty); + writeln!(out, "{var_name}Tag, _ := r.GetU8()"); + writeln!(out, "var {var_name} *{inner_type_str}"); + writeln!(out, "if {var_name}Tag == 1 {{"); + out.with_indent(|out| { + write_bsatn_decode(out, module, &format!("{var_name}Val"), inner_ty); + writeln!(out, "{var_name} = &{var_name}Val"); + }); + writeln!(out, "}}"); + } + AlgebraicTypeUse::Result { .. } => { + writeln!(out, "{var_name}, _ := stdb.ReadResult(r)"); + } + AlgebraicTypeUse::Never => unimplemented!("never types are not yet supported in Go output"), + } +} + +/// Write BSATN field decoding for a struct field (with error handling). +fn write_bsatn_field_decode( + out: &mut CodeIndenter, + module: &ModuleDef, + field_name: &str, + ty: &AlgebraicTypeUse, +) { + match ty { + AlgebraicTypeUse::Primitive(prim) => { + let method = match prim { + PrimitiveType::Bool => "GetBool", + PrimitiveType::I8 => "GetI8", + PrimitiveType::U8 => "GetU8", + PrimitiveType::I16 => "GetI16", + PrimitiveType::U16 => "GetU16", + PrimitiveType::I32 => "GetI32", + PrimitiveType::U32 => "GetU32", + PrimitiveType::I64 => "GetI64", + PrimitiveType::U64 => "GetU64", + PrimitiveType::I128 => "GetI128", + PrimitiveType::U128 => "GetU128", + PrimitiveType::I256 => "GetI256", + PrimitiveType::U256 => "GetU256", + PrimitiveType::F32 => "GetF32", + PrimitiveType::F64 => "GetF64", + }; + writeln!( + out, + "if v.{field_name}, err = r.{method}(); err != nil {{ return nil, err }}" + ); + } + AlgebraicTypeUse::String => { + writeln!( + out, + "if v.{field_name}, err = r.GetString(); err != nil {{ return nil, err }}" + ); + } + AlgebraicTypeUse::Identity => { + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "val, readErr := stdb.ReadIdentity(r)"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "v.{field_name} = val"); + }); + writeln!(out, "}}"); + } + AlgebraicTypeUse::ConnectionId => { + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "val, readErr := stdb.ReadConnectionId(r)"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "v.{field_name} = val"); + }); + writeln!(out, "}}"); + } + AlgebraicTypeUse::Timestamp => { + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "val, readErr := stdb.ReadTimestamp(r)"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "v.{field_name} = val"); + }); + writeln!(out, "}}"); + } + AlgebraicTypeUse::TimeDuration => { + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "val, readErr := stdb.ReadTimeDuration(r)"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "v.{field_name} = val"); + }); + writeln!(out, "}}"); + } + AlgebraicTypeUse::ScheduleAt => { + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "val, readErr := stdb.ReadScheduleAt(r)"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "v.{field_name} = val"); + }); + writeln!(out, "}}"); + } + AlgebraicTypeUse::Uuid => { + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "val, readErr := stdb.ReadUuid(r)"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "v.{field_name} = val"); + }); + writeln!(out, "}}"); + } + AlgebraicTypeUse::Unit => { + // Nothing to read for unit + } + AlgebraicTypeUse::Ref(r) => { + let type_name = type_ref_name(module, *r); + let type_def = &module.typespace_for_generate()[r]; + match type_def { + AlgebraicTypeDef::Product(_) => { + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "val, readErr := Read{type_name}(r)"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "v.{field_name} = *val"); + }); + writeln!(out, "}}"); + } + AlgebraicTypeDef::Sum(_) => { + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "val, readErr := Read{type_name}(r)"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "v.{field_name} = val"); + }); + writeln!(out, "}}"); + } + AlgebraicTypeDef::PlainEnum(_) => { + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "val, readErr := Read{type_name}(r)"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "v.{field_name} = val"); + }); + writeln!(out, "}}"); + } + } + } + AlgebraicTypeUse::Array(elem_ty) => { + if matches!(elem_ty.as_ref(), AlgebraicTypeUse::Primitive(PrimitiveType::U8)) { + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "val, readErr := r.GetBytes()"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "v.{field_name} = val"); + }); + writeln!(out, "}}"); + } else { + let elem_type_str = ty_fmt(module, elem_ty); + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "arrLen, readErr := r.GetU32()"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "v.{field_name} = make([]{elem_type_str}, arrLen)"); + writeln!(out, "for i := uint32(0); i < arrLen; i++ {{"); + out.with_indent(|out| { + write_bsatn_field_decode_inner(out, module, &format!("{field_name}[i]"), elem_ty); + }); + writeln!(out, "}}"); + }); + writeln!(out, "}}"); + } + } + AlgebraicTypeUse::Option(inner_ty) => { + let inner_type_str = ty_fmt(module, inner_ty); + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "optTag, readErr := r.GetU8()"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "if optTag == 1 {{"); + out.with_indent(|out| { + writeln!(out, "var optVal {inner_type_str}"); + writeln!(out, "_ = optVal"); + write_bsatn_field_decode_inner(out, module, "optVal", inner_ty); + writeln!(out, "v.{field_name} = &optVal"); + }); + writeln!(out, "}}"); + }); + writeln!(out, "}}"); + } + AlgebraicTypeUse::Result { .. } => { + writeln!(out, "{{"); + out.with_indent(|out| { + writeln!(out, "val, readErr := stdb.ReadResult(r)"); + writeln!(out, "if readErr != nil {{ return nil, readErr }}"); + writeln!(out, "v.{field_name} = val"); + }); + writeln!(out, "}}"); + } + AlgebraicTypeUse::Never => unimplemented!("never types are not yet supported in Go output"), + } +} + +/// Helper for inner array/option field decoding (without the "v." prefix pattern). +fn write_bsatn_field_decode_inner( + out: &mut CodeIndenter, + module: &ModuleDef, + target: &str, + ty: &AlgebraicTypeUse, +) { + match ty { + AlgebraicTypeUse::Primitive(prim) => { + let method = match prim { + PrimitiveType::Bool => "GetBool", + PrimitiveType::I8 => "GetI8", + PrimitiveType::U8 => "GetU8", + PrimitiveType::I16 => "GetI16", + PrimitiveType::U16 => "GetU16", + PrimitiveType::I32 => "GetI32", + PrimitiveType::U32 => "GetU32", + PrimitiveType::I64 => "GetI64", + PrimitiveType::U64 => "GetU64", + PrimitiveType::I128 => "GetI128", + PrimitiveType::U128 => "GetU128", + PrimitiveType::I256 => "GetI256", + PrimitiveType::U256 => "GetU256", + PrimitiveType::F32 => "GetF32", + PrimitiveType::F64 => "GetF64", + }; + writeln!(out, "if val, readErr := r.{method}(); readErr != nil {{ return nil, readErr }} else {{ v.{target} = val }}"); + } + AlgebraicTypeUse::String => { + writeln!(out, "if val, readErr := r.GetString(); readErr != nil {{ return nil, readErr }} else {{ v.{target} = val }}"); + } + AlgebraicTypeUse::Ref(r) => { + let type_name = type_ref_name(module, *r); + let type_def = &module.typespace_for_generate()[r]; + match type_def { + AlgebraicTypeDef::Product(_) => { + writeln!(out, "if val, readErr := Read{type_name}(r); readErr != nil {{ return nil, readErr }} else {{ v.{target} = *val }}"); + } + _ => { + writeln!(out, "if val, readErr := Read{type_name}(r); readErr != nil {{ return nil, readErr }} else {{ v.{target} = val }}"); + } + } + } + _ => { + // Fallback: use a block-scoped decode + writeln!(out, "// TODO: complex nested decode for {target}"); + } + } +} diff --git a/crates/codegen/src/lib.rs b/crates/codegen/src/lib.rs index 28d4fb8a5a4..d1e99cee99c 100644 --- a/crates/codegen/src/lib.rs +++ b/crates/codegen/src/lib.rs @@ -3,12 +3,14 @@ use spacetimedb_schema::schema::{Schema, TableSchema}; mod code_indenter; pub mod cpp; pub mod csharp; +pub mod go; pub mod rust; pub mod typescript; pub mod unrealcpp; mod util; pub use self::csharp::Csharp; +pub use self::go::Go; pub use self::rust::Rust; pub use self::typescript::TypeScript; pub use self::unrealcpp::UnrealCpp; diff --git a/crates/codegen/tests/codegen.rs b/crates/codegen/tests/codegen.rs index 06dc3ebe8fc..94b6198bab0 100644 --- a/crates/codegen/tests/codegen.rs +++ b/crates/codegen/tests/codegen.rs @@ -1,4 +1,4 @@ -use spacetimedb_codegen::{generate, CodegenOptions, Csharp, Rust, TypeScript}; +use spacetimedb_codegen::{generate, CodegenOptions, Csharp, Go, Rust, TypeScript}; use spacetimedb_data_structures::map::HashMap; use spacetimedb_schema::def::ModuleDef; use spacetimedb_testing::modules::{CompilationMode, CompiledModule}; @@ -38,4 +38,5 @@ declare_tests! { test_codegen_csharp => Csharp { namespace: "SpacetimeDB" }, test_codegen_typescript => TypeScript, test_codegen_rust => Rust, + test_codegen_go => Go, } diff --git a/crates/core/src/host/wasmtime/mod.rs b/crates/core/src/host/wasmtime/mod.rs index 8bf760ae64b..55144721578 100644 --- a/crates/core/src/host/wasmtime/mod.rs +++ b/crates/core/src/host/wasmtime/mod.rs @@ -15,6 +15,7 @@ pub use wasmtime_module::{WasmtimeInstance, WasmtimeModule}; #[cfg(unix)] mod pooling_stack_creator; mod wasm_instance_env; +mod wasi_stubs; mod wasmtime_module; pub struct WasmtimeRuntime { diff --git a/crates/core/src/host/wasmtime/wasi_stubs.rs b/crates/core/src/host/wasmtime/wasi_stubs.rs new file mode 100644 index 00000000000..116646183ce --- /dev/null +++ b/crates/core/src/host/wasmtime/wasi_stubs.rs @@ -0,0 +1,272 @@ +//! WASI Preview 1 stub implementations for languages that compile to `wasip1`. +//! +//! Languages like Go (used for the Go SDK) compile to `wasip1`, which requires +//! WASI imports from `wasi_snapshot_preview1`. SpacetimeDB does not provide a full +//! WASI implementation, so we provide minimal stubs that allow the module to run. +//! +//! The C++ SDK handles this differently by embedding WASI shims in the compiled module +//! (see `crates/bindings-cpp/src/abi/wasi_shims.cpp`). Go uses `//go:wasmimport` +//! for WASI functions which must be satisfied by the host. + +use super::wasm_instance_env::WasmInstanceEnv; +use super::{Mem, MemView}; +use wasmtime::{Caller, Linker}; + +const WASI_MODULE: &str = "wasi_snapshot_preview1"; + +// WASI errno codes +const ERRNO_SUCCESS: i32 = 0; +const ERRNO_BADF: i32 = 8; +const ERRNO_NOSYS: i32 = 52; + +pub(super) fn link_wasi_stubs(linker: &mut Linker) -> anyhow::Result<()> { + // fd_write(fd: i32, iovs_ptr: i32, iovs_len: i32, nwritten_ptr: i32) -> errno + // + // Redirect stdout/stderr writes to the host logger. + linker.func_wrap( + WASI_MODULE, + "fd_write", + |mut caller: Caller<'_, WasmInstanceEnv>, fd: i32, iovs_ptr: i32, iovs_len: i32, nwritten_ptr: i32| -> i32 { + if fd != 1 && fd != 2 { + return ERRNO_BADF; + } + + let mem = match get_memory(&mut caller) { + Some(m) => m, + None => return ERRNO_BADF, + }; + let (mem_view, _) = mem.view_and_store_mut(&mut caller); + + let mut total: u32 = 0; + let mut message = Vec::new(); + for i in 0..iovs_len { + let iov_offset = iovs_ptr as u32 + (i as u32) * 8; + let buf_ptr = match read_u32(mem_view, iov_offset) { + Some(v) => v, + None => return ERRNO_BADF, + }; + let buf_len = match read_u32(mem_view, iov_offset + 4) { + Some(v) => v, + None => return ERRNO_BADF, + }; + if let Ok(bytes) = mem_view.deref_slice(buf_ptr, buf_len) { + message.extend_from_slice(bytes); + total += buf_len; + } + } + + // Write nwritten + if let Ok(dest) = mem_view.deref_slice_mut(nwritten_ptr as u32, 4) { + dest.copy_from_slice(&total.to_le_bytes()); + } + + if !message.is_empty() { + let msg = String::from_utf8_lossy(&message); + if fd == 2 { + log::warn!("[wasm/wasi] {}", msg.trim_end()); + } else { + log::info!("[wasm/wasi] {}", msg.trim_end()); + } + } + + ERRNO_SUCCESS + }, + )?; + + // proc_exit(code: i32) -> ! + linker.func_wrap( + WASI_MODULE, + "proc_exit", + |_caller: Caller<'_, WasmInstanceEnv>, _code: i32| { + // No-op. Only called on fatal errors. + }, + )?; + + // poll_oneoff(in_ptr: i32, out_ptr: i32, nsubscriptions: i32, nevents_ptr: i32) -> errno + linker.func_wrap( + WASI_MODULE, + "poll_oneoff", + |mut caller: Caller<'_, WasmInstanceEnv>, + _in_ptr: i32, + _out_ptr: i32, + nsubscriptions: i32, + nevents_ptr: i32| + -> i32 { + if let Some(mem) = get_memory(&mut caller) { + let (mem_view, _) = mem.view_and_store_mut(&mut caller); + if let Ok(dest) = mem_view.deref_slice_mut(nevents_ptr as u32, 4) { + dest.copy_from_slice(&(nsubscriptions as u32).to_le_bytes()); + } + } + ERRNO_SUCCESS + }, + )?; + + // clock_time_get(id: i32, precision: i64, time_ptr: i32) -> errno + // + // Returns the current time in nanoseconds. Go's runtime requires this to return + // non-zero values — it panics with "nanotime returning zero" otherwise. + // Clock IDs: 0 = REALTIME, 1 = MONOTONIC. + linker.func_wrap( + WASI_MODULE, + "clock_time_get", + |mut caller: Caller<'_, WasmInstanceEnv>, _id: i32, _precision: i64, time_ptr: i32| -> i32 { + if let Some(mem) = get_memory(&mut caller) { + let (mem_view, _) = mem.view_and_store_mut(&mut caller); + if let Ok(dest) = mem_view.deref_slice_mut(time_ptr as u32, 8) { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() as u64; + dest.copy_from_slice(&nanos.to_le_bytes()); + } + } + ERRNO_SUCCESS + }, + )?; + + // args_sizes_get(argc_ptr: i32, argv_buf_size_ptr: i32) -> errno + linker.func_wrap( + WASI_MODULE, + "args_sizes_get", + |mut caller: Caller<'_, WasmInstanceEnv>, argc_ptr: i32, argv_buf_size_ptr: i32| -> i32 { + if let Some(mem) = get_memory(&mut caller) { + let (mem_view, _) = mem.view_and_store_mut(&mut caller); + if let Ok(dest) = mem_view.deref_slice_mut(argc_ptr as u32, 4) { + dest.copy_from_slice(&0u32.to_le_bytes()); + } + if let Ok(dest) = mem_view.deref_slice_mut(argv_buf_size_ptr as u32, 4) { + dest.copy_from_slice(&0u32.to_le_bytes()); + } + } + ERRNO_SUCCESS + }, + )?; + + // args_get(argv_ptr: i32, argv_buf_ptr: i32) -> errno + linker.func_wrap( + WASI_MODULE, + "args_get", + |_caller: Caller<'_, WasmInstanceEnv>, _argv_ptr: i32, _argv_buf_ptr: i32| -> i32 { ERRNO_SUCCESS }, + )?; + + // random_get(buf_ptr: i32, buf_len: i32) -> errno + // + // Fill buffer with random bytes using getrandom (available via std). + linker.func_wrap( + WASI_MODULE, + "random_get", + |mut caller: Caller<'_, WasmInstanceEnv>, buf_ptr: i32, buf_len: i32| -> i32 { + if let Some(mem) = get_memory(&mut caller) { + let (mem_view, _) = mem.view_and_store_mut(&mut caller); + if let Ok(dest) = mem_view.deref_slice_mut(buf_ptr as u32, buf_len as u32) { + // Use a simple counter-based fill. For WASM module use this is adequate — + // modules should not rely on WASI random_get for cryptographic purposes. + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + let mut hasher = DefaultHasher::new(); + std::time::SystemTime::now().hash(&mut hasher); + let mut state = hasher.finish(); + for byte in dest.iter_mut() { + state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + *byte = (state >> 33) as u8; + } + } + } + ERRNO_SUCCESS + }, + )?; + + // Additional stubs that some WASI-targeting compilers may need. + + linker.func_wrap( + WASI_MODULE, + "environ_sizes_get", + |mut caller: Caller<'_, WasmInstanceEnv>, count_ptr: i32, size_ptr: i32| -> i32 { + if let Some(mem) = get_memory(&mut caller) { + let (mem_view, _) = mem.view_and_store_mut(&mut caller); + if let Ok(dest) = mem_view.deref_slice_mut(count_ptr as u32, 4) { + dest.copy_from_slice(&0u32.to_le_bytes()); + } + if let Ok(dest) = mem_view.deref_slice_mut(size_ptr as u32, 4) { + dest.copy_from_slice(&0u32.to_le_bytes()); + } + } + ERRNO_SUCCESS + }, + )?; + + linker.func_wrap( + WASI_MODULE, + "environ_get", + |_caller: Caller<'_, WasmInstanceEnv>, _environ_ptr: i32, _environ_buf_ptr: i32| -> i32 { ERRNO_SUCCESS }, + )?; + + linker.func_wrap(WASI_MODULE, "fd_close", |_caller: Caller<'_, WasmInstanceEnv>, _fd: i32| -> i32 { + ERRNO_BADF + })?; + + linker.func_wrap( + WASI_MODULE, + "fd_seek", + |_caller: Caller<'_, WasmInstanceEnv>, _fd: i32, _offset: i64, _whence: i32, _newoffset_ptr: i32| -> i32 { + ERRNO_NOSYS + }, + )?; + + linker.func_wrap( + WASI_MODULE, + "fd_read", + |_caller: Caller<'_, WasmInstanceEnv>, _fd: i32, _iovs_ptr: i32, _iovs_len: i32, _nread_ptr: i32| -> i32 { + ERRNO_NOSYS + }, + )?; + + linker.func_wrap( + WASI_MODULE, + "fd_fdstat_get", + |_caller: Caller<'_, WasmInstanceEnv>, _fd: i32, _stat_ptr: i32| -> i32 { ERRNO_BADF }, + )?; + + linker.func_wrap( + WASI_MODULE, + "fd_prestat_get", + |_caller: Caller<'_, WasmInstanceEnv>, _fd: i32, _prestat_ptr: i32| -> i32 { ERRNO_BADF }, + )?; + + linker.func_wrap( + WASI_MODULE, + "fd_prestat_dir_name", + |_caller: Caller<'_, WasmInstanceEnv>, _fd: i32, _path_ptr: i32, _path_len: i32| -> i32 { ERRNO_BADF }, + )?; + + // sched_yield() -> errno + // + // Yield the processor. Standard Go's WASM runtime calls this during goroutine scheduling. + linker.func_wrap( + WASI_MODULE, + "sched_yield", + |_caller: Caller<'_, WasmInstanceEnv>| -> i32 { ERRNO_SUCCESS }, + )?; + + // fd_fdstat_set_flags(fd: i32, flags: i32) -> errno + // + // Set file descriptor flags. Standard Go's WASM runtime may call this during initialization. + linker.func_wrap( + WASI_MODULE, + "fd_fdstat_set_flags", + |_caller: Caller<'_, WasmInstanceEnv>, _fd: i32, _flags: i32| -> i32 { ERRNO_NOSYS }, + )?; + + Ok(()) +} + +fn get_memory(caller: &mut Caller<'_, WasmInstanceEnv>) -> Option { + let memory = caller.get_export("memory")?.into_memory()?; + Some(Mem { memory }) +} + +fn read_u32(mem: &MemView, offset: u32) -> Option { + let bytes = mem.deref_slice(offset, 4).ok()?; + Some(u32::from_le_bytes(bytes.try_into().ok()?)) +} diff --git a/crates/core/src/host/wasmtime/wasmtime_module.rs b/crates/core/src/host/wasmtime/wasmtime_module.rs index 48ac0fe80e2..f68b5758b10 100644 --- a/crates/core/src/host/wasmtime/wasmtime_module.rs +++ b/crates/core/src/host/wasmtime/wasmtime_module.rs @@ -67,6 +67,12 @@ impl WasmtimeModule { } } abi_funcs!(link_functions, link_async_functions); + + // WASI Preview 1 stubs for languages (e.g. Go) that compile to `wasip1`. + // These modules import WASI functions from `wasi_snapshot_preview1`. + // We provide no-op / minimal stubs so the module can be instantiated. + super::wasi_stubs::link_wasi_stubs(linker)?; + Ok(()) } } @@ -233,6 +239,19 @@ impl module_host_actor::WasmInstancePre for WasmtimeModule { set_store_fuel(&mut store, FunctionBudget::DEFAULT_BUDGET.into()); store.set_epoch_deadline(EPOCH_TICKS_PER_SECOND); + // WASI modules export `_initialize` (reactors) or `_start` (commands) + // which must be called before any other exports to set up the language runtime. + // Go compiles to a WASI reactor with `_initialize` via `-buildmode=c-shared`. + for wasi_init in ["_initialize", "_start"] { + if let Ok(init) = instance.get_typed_func::<(), ()>(&mut store, wasi_init) { + call_sync_typed_func(&init, &mut store, ()).map_err(|err| InitializationError::RuntimeError { + err, + func: wasi_init.to_owned(), + })?; + break; + } + } + for preinit in &func_names.preinits { let func = instance.get_typed_func::<(), ()>(&mut store, preinit).unwrap(); call_sync_typed_func(&func, &mut store, ()).map_err(|err| InitializationError::RuntimeError { diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 3aca5a2794e..fa6a9518adc 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -122,6 +122,22 @@ macro_rules! require_dotnet { }; } +#[macro_export] +macro_rules! require_go { + () => { + if !$crate::allow_go() { + #[allow(clippy::disallowed_macros)] + { + eprintln!("Skipping Go test (SMOKETESTS_GO env var is not set)"); + } + return; + } + if !$crate::have_go() { + panic!("go not found in PATH"); + } + }; +} + #[macro_export] macro_rules! require_psql { () => { @@ -261,6 +277,29 @@ pub fn allow_dotnet() -> bool { } } +/// Returns true if go is available on the system. +pub fn have_go() -> bool { + static HAVE_GO: OnceLock = OnceLock::new(); + *HAVE_GO.get_or_init(|| { + Command::new("go") + .args(["version"]) + .output() + .map(|output| output.status.success()) + .unwrap_or(false) + }) +} + +/// Returns true if tests are configured to allow Go +pub fn allow_go() -> bool { + static ALLOW_GO: OnceLock = OnceLock::new(); + *ALLOW_GO.get_or_init(|| match std::env::var("SMOKETESTS_GO").as_deref() { + Err(_) => true, + Ok("" | "0") => false, + Ok(v) if v.eq_ignore_ascii_case("false") => false, + Ok(_) => true, + }) +} + /// Returns true if psql (PostgreSQL client) is available on the system. pub fn have_psql() -> bool { static HAVE_PSQL: OnceLock = OnceLock::new(); diff --git a/crates/smoketests/tests/smoketests/go_module.rs b/crates/smoketests/tests/smoketests/go_module.rs new file mode 100644 index 00000000000..86b28ba401d --- /dev/null +++ b/crates/smoketests/tests/smoketests/go_module.rs @@ -0,0 +1,10 @@ +#![allow(clippy::disallowed_macros)] +use spacetimedb_smoketests::require_go; + +/// Ensure that go is detected correctly. +/// Full Go module build test will be added when Go modules are integrated. +#[test] +fn test_build_go_module() { + require_go!(); + assert!(spacetimedb_smoketests::have_go()); +} diff --git a/crates/smoketests/tests/smoketests/mod.rs b/crates/smoketests/tests/smoketests/mod.rs index 1f7e53a7230..5a33eead182 100644 --- a/crates/smoketests/tests/smoketests/mod.rs +++ b/crates/smoketests/tests/smoketests/mod.rs @@ -17,6 +17,7 @@ mod dml; mod domains; mod fail_initial_publish; mod filtering; +mod go_module; mod http_egress; mod module_nested_op; mod modules; diff --git a/crates/testing/src/modules.rs b/crates/testing/src/modules.rs index 629ee5c1577..a5832100d66 100644 --- a/crates/testing/src/modules.rs +++ b/crates/testing/src/modules.rs @@ -350,3 +350,31 @@ impl ModuleLanguage for Cpp { &MODULE } } + +pub struct Go; + +impl ModuleLanguage for Go { + const NAME: &'static str = "go"; + + fn get_module() -> &'static CompiledModule { + lazy_static::lazy_static! { + pub static ref MODULE: CompiledModule = CompiledModule::compile("sdk-test-go", COMPILATION_MODE); + } + + &MODULE + } +} + +pub struct GoBenchmarks; + +impl ModuleLanguage for GoBenchmarks { + const NAME: &'static str = "go"; + + fn get_module() -> &'static CompiledModule { + lazy_static::lazy_static! { + pub static ref MODULE: CompiledModule = CompiledModule::compile("benchmarks-go", COMPILATION_MODE); + } + + &MODULE + } +} diff --git a/crates/testing/tests/standalone_integration_test.rs b/crates/testing/tests/standalone_integration_test.rs index 27aff909972..03979c637f6 100644 --- a/crates/testing/tests/standalone_integration_test.rs +++ b/crates/testing/tests/standalone_integration_test.rs @@ -1,8 +1,8 @@ use serial_test::serial; use spacetimedb_lib::sats::{product, AlgebraicValue}; use spacetimedb_testing::modules::{ - CompilationMode, CompiledModule, Cpp, Csharp, LogLevel, LoggerRecord, ModuleHandle, ModuleLanguage, Rust, - TypeScript, DEFAULT_CONFIG, IN_MEMORY_CONFIG, + CompilationMode, CompiledModule, Cpp, Csharp, GoBenchmarks, LogLevel, LoggerRecord, ModuleHandle, + ModuleLanguage, Rust, TypeScript, DEFAULT_CONFIG, IN_MEMORY_CONFIG, }; use std::{ future::Future, @@ -113,10 +113,14 @@ fn test_calling_a_reducer_cpp() { #[test] #[serial] -fn test_calling_a_reducer_with_private_table() { +fn test_calling_a_reducer_go() { + test_calling_a_reducer_in_module("module-test-go"); +} + +fn test_calling_a_reducer_with_private_table_in_module(module_name: &'static str) { init(); - CompiledModule::compile("module-test", CompilationMode::Debug).with_module_async( + CompiledModule::compile(module_name, CompilationMode::Debug).with_module_async( DEFAULT_CONFIG, |module| async move { module @@ -136,13 +140,25 @@ fn test_calling_a_reducer_with_private_table() { ); } +#[test] +#[serial] +fn test_calling_a_reducer_with_private_table() { + test_calling_a_reducer_with_private_table_in_module("module-test"); +} + +#[test] +#[serial] +fn test_calling_a_reducer_with_private_table_go() { + test_calling_a_reducer_with_private_table_in_module("module-test-go"); +} + async fn read_log_skip_repeating(module: &ModuleHandle) -> String { let logs = read_logs(module).await; let mut logs = logs .into_iter() // Filter out log lines from the `repeating_test` reducer, // which runs frequently enough to appear in our logs after we've slept a second. - .filter(|line| !line.starts_with("Timestamp: Timestamp { __timestamp_micros_since_unix_epoch__: ")) + .filter(|line| !line.starts_with("Timestamp: ")) .collect::>(); if logs.len() != 1 { @@ -187,6 +203,12 @@ fn test_calling_a_procedure() { test_calling_a_procedure_in_module("module-test"); } +#[test] +#[serial] +fn test_calling_a_procedure_go() { + test_calling_a_procedure_in_module("module-test-go"); +} + fn test_calling_with_tx_in_module(module_name: &'static str) { init(); @@ -221,6 +243,12 @@ fn test_calling_with_tx() { test_calling_with_tx_in_module("module-test"); } +#[test] +#[serial] +fn test_calling_with_tx_go() { + test_calling_with_tx_in_module("module-test-go"); +} + /// Invoke the `module-test` module, /// use `caller` to invoke its `test` reducer, /// and assert that its logs look right. @@ -240,8 +268,11 @@ fn test_calling_with_tx() { /// TestF::Baz("buzz".to_string()), /// ] /// ``` -fn test_call_query_macro_with_caller>(caller: impl FnOnce(ModuleHandle) -> F) { - CompiledModule::compile("module-test", CompilationMode::Debug).with_module_async( +fn test_call_query_macro_with_caller_in_module>( + module_name: &str, + caller: impl FnOnce(ModuleHandle) -> F, +) { + CompiledModule::compile(module_name, CompilationMode::Debug).with_module_async( DEFAULT_CONFIG, |module| async move { caller(module.clone()).await; @@ -273,12 +304,10 @@ fn test_call_query_macro_with_caller>(caller: impl FnOnce ); } -/// Call the `module-test` module's `test` reducer with a variety of ways of passing arguments. -#[test] -#[serial] -fn test_call_query_macro() { +/// Call a module-test's `test` reducer with a variety of ways of passing arguments. +fn test_call_query_macro_in_module(module_name: &str) { // Hand-written JSON. This will fail if the JSON encoding of `ClientMessage` changes. - test_call_query_macro_with_caller(|module| async move { + test_call_query_macro_with_caller_in_module(module_name, |module| async move { // Note that JSON doesn't allow multiline strings, so the encoded args string must be on one line! let json = r#" { "CallReducer": { @@ -300,24 +329,34 @@ fn test_call_query_macro() { ]; // JSON via the `Serialize` path. - test_call_query_macro_with_caller(|module| async move { + test_call_query_macro_with_caller_in_module(module_name, |module| async move { module.call_reducer_json("test", args_pv).await.unwrap(); }); // BSATN via the `Serialize` path. - test_call_query_macro_with_caller(|module| async move { + test_call_query_macro_with_caller_in_module(module_name, |module| async move { module.call_reducer_binary("test", args_pv).await.unwrap(); }); } #[test] #[serial] -/// This test runs the index scan workloads in the `perf-test` module. +fn test_call_query_macro() { + test_call_query_macro_in_module("module-test"); +} + +#[test] +#[serial] +fn test_call_query_macro_go() { + test_call_query_macro_in_module("module-test-go"); +} + +/// Run the index scan workloads in a perf-test module. /// Timing spans should be < 1ms if the correct index was used. /// Otherwise these workloads will degenerate into full table scans. -fn test_index_scans() { +fn test_index_scans_in_module(module_name: &'static str) { init(); - CompiledModule::compile("perf-test", CompilationMode::Release).with_module_async( + CompiledModule::compile(module_name, CompilationMode::Release).with_module_async( IN_MEMORY_CONFIG, |module| async move { let no_args = &product![]; @@ -362,6 +401,18 @@ fn test_index_scans() { ); } +#[test] +#[serial] +fn test_index_scans() { + test_index_scans_in_module("perf-test"); +} + +#[test] +#[serial] +fn test_index_scans_go() { + test_index_scans_in_module("perf-test-go"); +} + async fn bench_call(module: &ModuleHandle, call: &str, count: &u32) -> Duration { let now = Instant::now(); @@ -425,6 +476,12 @@ fn test_calling_bench_db_circles_cpp() { test_calling_bench_db_circles::(); } +#[test] +#[serial] +fn test_calling_bench_db_circles_go() { + test_calling_bench_db_circles::(); +} + fn test_calling_bench_db_ia_loop() { L::get_module().with_module_async(DEFAULT_CONFIG, |module| async move { #[rustfmt::skip] @@ -465,3 +522,9 @@ fn test_calling_bench_db_ia_loop_typescript() { fn test_calling_bench_db_ia_loop_cpp() { test_calling_bench_db_ia_loop::(); } + +#[test] +#[serial] +fn test_calling_bench_db_ia_loop_go() { + test_calling_bench_db_ia_loop::(); +} diff --git a/modules/benchmarks-go/benchmarks-go b/modules/benchmarks-go/benchmarks-go new file mode 100755 index 00000000000..4f9798c8d75 Binary files /dev/null and b/modules/benchmarks-go/benchmarks-go differ diff --git a/modules/benchmarks-go/circles.go b/modules/benchmarks-go/circles.go new file mode 100644 index 00000000000..c2082af657c --- /dev/null +++ b/modules/benchmarks-go/circles.go @@ -0,0 +1,223 @@ +package main + +import ( + "fmt" + "math" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/log" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/reducer" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// ---------- helper struct ---------- + +type Vector2 struct { + X float32 + Y float32 +} + +// ---------- table schemas ---------- + +//stdb:table name=entity access=public +type Entity struct { + Id uint32 `stdb:"primarykey,autoinc"` + Position Vector2 + Mass uint32 +} + +//stdb:table name=circle access=public +type Circle struct { + EntityId uint32 `stdb:"primarykey"` + PlayerId uint32 `stdb:"index=btree"` + Direction Vector2 + Magnitude float32 + LastSplitTime types.Timestamp +} + +//stdb:table name=food access=public +type Food struct { + EntityId uint32 `stdb:"primarykey"` +} + +// ---------- helper functions ---------- + +func massToRadius(mass uint32) float32 { + return float32(math.Sqrt(float64(mass))) +} + +func isOverlapping(entity1, entity2 Entity) bool { + entity1Radius := massToRadius(entity1.Mass) + entity2Radius := massToRadius(entity2.Mass) + dx := entity1.Position.X - entity2.Position.X + dy := entity1.Position.Y - entity2.Position.Y + distance := float32(math.Sqrt(float64(dx*dx + dy*dy))) + maxRadius := entity1Radius + if entity2Radius > maxRadius { + maxRadius = entity2Radius + } + return distance < maxRadius +} + +// ---------- logger ---------- + +var circlesLogger log.Logger + +func init() { + circlesLogger = log.NewLogger("circles") +} + +// ---------- bulk insert functions ---------- + +//stdb:reducer name=insert_bulk_entity +func insertBulkEntity(ctx reducer.ReducerContext, count uint32) { + for i := uint32(0); i < count; i++ { + EntityTable.Insert(Entity{ + Id: 0, + Position: Vector2{X: 0, Y: 0}, + Mass: 0, + }) + } + circlesLogger.Info(fmt.Sprintf("INSERT ENTITY: %d", count)) +} + +//stdb:reducer name=insert_bulk_circle +func insertBulkCircle(ctx reducer.ReducerContext, count uint32) { + for i := uint32(0); i < count; i++ { + CircleTable.Insert(Circle{ + EntityId: i, + PlayerId: i, + Direction: Vector2{X: 0, Y: 0}, + Magnitude: 0, + LastSplitTime: types.NewTimestamp(0), + }) + } + circlesLogger.Info(fmt.Sprintf("INSERT CIRCLE: %d", count)) +} + +//stdb:reducer name=insert_bulk_food +func insertBulkFood(ctx reducer.ReducerContext, count uint32) { + for i := uint32(1); i <= count; i++ { + FoodTable.Insert(Food{ + EntityId: i, + }) + } + circlesLogger.Info(fmt.Sprintf("INSERT FOOD: %d", count)) +} + +// ---------- join query functions ---------- + +// crossJoinAll simulates: SELECT * FROM Circle, Entity, Food +// +//stdb:reducer name=cross_join_all +func crossJoinAll(ctx reducer.ReducerContext, expected uint32) { + var count uint32 + + circleIter, err := CircleTable.Scan() + if err != nil { + circlesLogger.Error(fmt.Sprintf("failed to scan circles: %v", err)) + return + } + defer circleIter.Close() + + for circle, ok := circleIter.Next(); ok; circle, ok = circleIter.Next() { + _ = circle + + entityIter, err := EntityTable.Scan() + if err != nil { + circlesLogger.Error(fmt.Sprintf("failed to scan entities: %v", err)) + return + } + + for entity, ok := entityIter.Next(); ok; entity, ok = entityIter.Next() { + _ = entity + + foodIter, err := FoodTable.Scan() + if err != nil { + circlesLogger.Error(fmt.Sprintf("failed to scan foods: %v", err)) + entityIter.Close() + return + } + + for food, ok := foodIter.Next(); ok; food, ok = foodIter.Next() { + _ = food + count++ + } + foodIter.Close() + } + entityIter.Close() + } + + circlesLogger.Info(fmt.Sprintf("CROSS JOIN ALL: %d, processed: %d", expected, count)) +} + +// crossJoinCircleFood simulates: +// SELECT * FROM Circle JOIN Entity USING(entity_id), Food JOIN Entity USING(entity_id) +// +//stdb:reducer name=cross_join_circle_food +func crossJoinCircleFood(ctx reducer.ReducerContext, expected uint32) { + var count uint32 + + circleIter, err := CircleTable.Scan() + if err != nil { + circlesLogger.Error(fmt.Sprintf("failed to scan circles: %v", err)) + return + } + defer circleIter.Close() + + for circle, ok := circleIter.Next(); ok; circle, ok = circleIter.Next() { + circleEntity, found, err := EntityTable.FindById(circle.EntityId) + if err != nil { + circlesLogger.Error(fmt.Sprintf("failed to find entity: %v", err)) + return + } + if !found { + continue + } + + foodIter, err := FoodTable.Scan() + if err != nil { + circlesLogger.Error(fmt.Sprintf("failed to scan foods: %v", err)) + return + } + + for food, ok := foodIter.Next(); ok; food, ok = foodIter.Next() { + count++ + foodEntity, found, err := EntityTable.FindById(food.EntityId) + if err != nil { + circlesLogger.Error(fmt.Sprintf("failed to find food entity: %v", err)) + foodIter.Close() + return + } + if !found { + circlesLogger.Error(fmt.Sprintf("Entity not found: %d", food.EntityId)) + foodIter.Close() + return + } + _ = isOverlapping(circleEntity, foodEntity) + } + foodIter.Close() + } + + circlesLogger.Info(fmt.Sprintf("CROSS JOIN CIRCLE FOOD: %d, processed: %d", expected, count)) +} + +// ---------- game init/run functions ---------- + +//stdb:reducer name=init_game_circles +func initGameCircles(ctx reducer.ReducerContext, initialLoad uint32) { + biggestTable := initialLoad * 100 + bigTable := initialLoad * 50 + smallTable := initialLoad + + insertBulkFood(ctx, biggestTable) + insertBulkEntity(ctx, bigTable) + insertBulkCircle(ctx, smallTable) +} + +//stdb:reducer name=run_game_circles +func runGameCircles(ctx reducer.ReducerContext, initialLoad uint32) { + smallTable := initialLoad + + crossJoinCircleFood(ctx, smallTable) + crossJoinAll(ctx, smallTable) +} diff --git a/modules/benchmarks-go/go.mod b/modules/benchmarks-go/go.mod new file mode 100644 index 00000000000..85c7912d567 --- /dev/null +++ b/modules/benchmarks-go/go.mod @@ -0,0 +1,7 @@ +module github.com/clockworklabs/SpacetimeDB/modules/benchmarks-go + +go 1.23 + +require github.com/clockworklabs/SpacetimeDB/sdks/go v0.0.0 + +replace github.com/clockworklabs/SpacetimeDB/sdks/go => ../../sdks/go diff --git a/modules/benchmarks-go/ia_loop.go b/modules/benchmarks-go/ia_loop.go new file mode 100644 index 00000000000..152065fbfb6 --- /dev/null +++ b/modules/benchmarks-go/ia_loop.go @@ -0,0 +1,298 @@ +package main + +import ( + "fmt" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/log" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/reducer" +) + +// AgentAction is a simple enum for enemy AI agent actions. +// +//stdb:enum variants=Inactive,Idle,Evading,Investigating,Retreating,Fighting +type AgentAction uint8 + +const ( + AgentActionInactive AgentAction = iota + AgentActionIdle + AgentActionEvading + AgentActionInvestigating + AgentActionRetreating + AgentActionFighting +) + +// SmallHexTile is a helper struct for herd cache locations. +type SmallHexTile struct { + X int32 + Z int32 + Dimension uint32 +} + +//stdb:table name=velocity access=public +type Velocity struct { + EntityId uint32 `stdb:"primarykey"` + X float32 + Y float32 + Z float32 +} + +//stdb:table name=position access=public +type Position struct { + EntityId uint32 `stdb:"primarykey"` + X float32 + Y float32 + Z float32 + Vx float32 + Vy float32 + Vz float32 +} + +//stdb:table name=game_enemy_ai_agent_state access=public +type GameEnemyAiAgentState struct { + EntityId uint64 `stdb:"primarykey"` + LastMoveTimestamps []uint64 + NextActionTimestamp uint64 + Action AgentAction +} + +//stdb:table name=game_targetable_state access=public +type GameTargetableState struct { + EntityId uint64 `stdb:"primarykey"` + Quad int64 +} + +//stdb:table name=game_live_targetable_state access=public +type GameLiveTargetableState struct { + EntityId uint64 `stdb:"unique"` + Quad int64 `stdb:"index=btree"` +} + +//stdb:table name=game_mobile_entity_state access=public +type GameMobileEntityState struct { + EntityId uint64 `stdb:"primarykey"` + LocationX int32 `stdb:"index=btree"` + LocationY int32 + Timestamp uint64 +} + +//stdb:table name=game_enemy_state access=public +type GameEnemyState struct { + EntityId uint64 `stdb:"primarykey"` + HerdId int32 +} + +//stdb:table name=game_herd_cache access=public +type GameHerdCache struct { + Id int32 `stdb:"primarykey"` + DimensionId uint32 + CurrentPopulation int32 + Location SmallHexTile + MaxPopulation int32 + SpawnEagerness float32 + RoamingDistance int32 +} + +var iaLogger log.Logger + +func init() { + iaLogger = log.NewLogger("ia_loop") +} + +//stdb:reducer name=insert_bulk_position +func insertBulkPosition(ctx reducer.ReducerContext, count uint32) { + for i := uint32(0); i < count; i++ { + PositionTable.Insert(Position{ + EntityId: i, + X: float32(i), + Y: float32(i + 10), + Z: 0.0, + Vx: 1.0, + Vy: 2.0, + Vz: 0.5, + }) + } + iaLogger.Info(fmt.Sprintf("INSERT POSITION: %d", count)) +} + +//stdb:reducer name=insert_bulk_velocity +func insertBulkVelocity(ctx reducer.ReducerContext, count uint32) { + for i := uint32(0); i < count; i++ { + VelocityTable.Insert(Velocity{ + EntityId: i, + X: 0.1, + Y: 0.2, + Z: 0.3, + }) + } + iaLogger.Info(fmt.Sprintf("INSERT VELOCITY: %d", count)) +} + +//stdb:reducer name=insert_world +func insertWorld(ctx reducer.ReducerContext, players uint64) { + for i := uint64(0); i < players; i++ { + GameEnemyAiAgentStateTable.Insert(GameEnemyAiAgentState{ + EntityId: i, + LastMoveTimestamps: []uint64{0, 0, 0, 0, 0}, + NextActionTimestamp: 100 + i, + Action: AgentActionIdle, + }) + + GameLiveTargetableStateTable.Insert(GameLiveTargetableState{ + EntityId: i, + Quad: int64(i) % 4, + }) + + GameTargetableStateTable.Insert(GameTargetableState{ + EntityId: i, + Quad: int64(i) % 4, + }) + + GameMobileEntityStateTable.Insert(GameMobileEntityState{ + EntityId: i, + LocationX: int32(i), + LocationY: int32(i * 2), + Timestamp: 1000, + }) + + GameEnemyStateTable.Insert(GameEnemyState{ + EntityId: i, + HerdId: int32(i) % 10, + }) + } + + // Insert 10 herds + for h := int32(0); h < 10; h++ { + GameHerdCacheTable.Insert(GameHerdCache{ + Id: h, + DimensionId: 0, + CurrentPopulation: int32(players / 10), + Location: SmallHexTile{ + X: h * 10, + Z: h * 20, + Dimension: 0, + }, + MaxPopulation: 100, + SpawnEagerness: 0.5, + RoamingDistance: 10, + }) + } + + iaLogger.Info(fmt.Sprintf("INSERT WORLD PLAYERS: %d", players)) +} + +//stdb:reducer name=update_position_all +func updatePositionAll(ctx reducer.ReducerContext, expected uint32) { + count := uint32(0) + iter, err := PositionTable.Scan() + if err != nil { + return + } + defer iter.Close() + + for { + pos, ok := iter.Next() + if !ok { + break + } + pos.X += pos.Vx + pos.Y += pos.Vy + pos.Z += pos.Vz + PositionTable.UpdateByEntityId(pos) + count++ + } + iaLogger.Info(fmt.Sprintf("UPDATE POSITION ALL: %d, processed: %d", expected, count)) +} + +//stdb:reducer name=update_position_with_velocity +func updatePositionWithVelocity(ctx reducer.ReducerContext, expected uint32) { + count := uint32(0) + iter, err := PositionTable.Scan() + if err != nil { + return + } + defer iter.Close() + + for { + pos, ok := iter.Next() + if !ok { + break + } + vel, found, err := VelocityTable.FindByEntityId(pos.EntityId) + if err != nil || !found { + continue + } + pos.X += vel.X + pos.Y += vel.Y + pos.Z += vel.Z + PositionTable.UpdateByEntityId(pos) + count++ + } + iaLogger.Info(fmt.Sprintf("UPDATE POSITION BY VELOCITY: %d, processed: %d", expected, count)) +} + +//stdb:reducer name=game_loop_enemy_ia +func gameLoopEnemyIA(ctx reducer.ReducerContext, players uint64) { + count := uint64(0) + iter, err := GameEnemyAiAgentStateTable.Scan() + if err != nil { + return + } + defer iter.Close() + + for { + agent, ok := iter.Next() + if !ok { + break + } + _ = agent + + targetable, found, err := GameTargetableStateTable.FindByEntityId(agent.EntityId) + if err != nil || !found { + continue + } + _ = targetable + + liveTargetable, found, err := GameLiveTargetableStateTable.FindByEntityId(agent.EntityId) + if err != nil || !found { + continue + } + _ = liveTargetable + + mobile, found, err := GameMobileEntityStateTable.FindByEntityId(agent.EntityId) + if err != nil || !found { + continue + } + _ = mobile + + enemy, found, err := GameEnemyStateTable.FindByEntityId(agent.EntityId) + if err != nil || !found { + continue + } + + herd, found, err := GameHerdCacheTable.FindById(enemy.HerdId) + if err != nil || !found { + continue + } + _ = herd + + count++ + } + iaLogger.Info(fmt.Sprintf("ENEMY IA LOOP PLAYERS: %d, processed: %d", players, count)) +} + +//stdb:reducer name=init_game_ia_loop +func initGameIALoop(ctx reducer.ReducerContext, initialLoad uint32) { + bigTable := initialLoad * 50 + smallTable := uint64(initialLoad) + + insertBulkPosition(ctx, bigTable) + insertBulkVelocity(ctx, bigTable) + updatePositionAll(ctx, bigTable) + updatePositionWithVelocity(ctx, bigTable) + insertWorld(ctx, smallTable) +} + +//stdb:reducer name=run_game_ia_loop +func runGameIALoop(ctx reducer.ReducerContext, initialLoad uint32) { + gameLoopEnemyIA(ctx, uint64(initialLoad)) +} diff --git a/modules/benchmarks-go/main.go b/modules/benchmarks-go/main.go new file mode 100644 index 00000000000..e4bb606a3cd --- /dev/null +++ b/modules/benchmarks-go/main.go @@ -0,0 +1,5 @@ +package main + +//go:generate stdb-gen + +func main() {} diff --git a/modules/benchmarks-go/synthetic.go b/modules/benchmarks-go/synthetic.go new file mode 100644 index 00000000000..100e833b1cd --- /dev/null +++ b/modules/benchmarks-go/synthetic.go @@ -0,0 +1,657 @@ +// STDB module used for benchmarks. +// +// This file is tightly bound to the `benchmarks` crate (`crates/bench`). +// +// The various tables in this file need to remain synced with `crates/bench/src/schemas.rs`. +// Field orders, names, and types should be the same. +// +// We instantiate multiple copies of each table. These should be identical +// aside from indexing strategy. Table names must match the template: +// +// `{IndexStrategy}{TableName}`, in PascalCase. +// +// The reducers need to remain synced with `crates/bench/src/spacetime_module.rs`. +// Reducer names must match the template: +// +// `{operation}_{index_strategy}_{table_name}`, in snake_case. +// +// The three index strategies are: +// - `unique`: a single unique key, declared first in the struct. +// - `no_index`: no indexes. +// - `btree_each_column`: one index for each column. +package main + +import ( + "fmt" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/log" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/reducer" +) + +// ---------- schemas ---------- + +// u32_u64_str schema: (id u32, age u64, name string) + +//stdb:table name=unique_0_u32_u64_str access=public +type Unique0U32U64Str struct { + Id uint32 `stdb:"primarykey"` + Age uint64 + Name string +} + +//stdb:table name=no_index_u32_u64_str access=public +type NoIndexU32U64Str struct { + Id uint32 + Age uint64 + Name string +} + +//stdb:table name=btree_each_column_u32_u64_str access=public +type BtreeEachColumnU32U64Str struct { + Id uint32 `stdb:"index=btree"` + Age uint64 `stdb:"index=btree"` + Name string `stdb:"index=btree"` +} + +// u32_u64_u64 schema: (id u32, x u64, y u64) + +//stdb:table name=unique_0_u32_u64_u64 access=public +type Unique0U32U64U64 struct { + Id uint32 `stdb:"primarykey"` + X uint64 + Y uint64 +} + +//stdb:table name=no_index_u32_u64_u64 access=public +type NoIndexU32U64U64 struct { + Id uint32 + X uint64 + Y uint64 +} + +//stdb:table name=btree_each_column_u32_u64_u64 access=public +type BtreeEachColumnU32U64U64 struct { + Id uint32 `stdb:"index=btree"` + X uint64 `stdb:"index=btree"` + Y uint64 `stdb:"index=btree"` +} + +// ---------- logger ---------- + +var benchLogger log.Logger + +func init() { + benchLogger = log.NewLogger("benchmarks") +} + +// ---------- empty ---------- + +//stdb:reducer name=empty +func empty(_ reducer.ReducerContext) {} + +// ---------- insert ---------- + +//stdb:reducer name=insert_unique_0_u32_u64_str +func insertUnique0U32U64Str(_ reducer.ReducerContext, id uint32, age uint64, name string) { + Unique0U32U64StrTable.Insert(Unique0U32U64Str{Id: id, Age: age, Name: name}) +} + +//stdb:reducer name=insert_no_index_u32_u64_str +func insertNoIndexU32U64Str(_ reducer.ReducerContext, id uint32, age uint64, name string) { + NoIndexU32U64StrTable.Insert(NoIndexU32U64Str{Id: id, Age: age, Name: name}) +} + +//stdb:reducer name=insert_btree_each_column_u32_u64_str +func insertBtreeEachColumnU32U64Str(_ reducer.ReducerContext, id uint32, age uint64, name string) { + BtreeEachColumnU32U64StrTable.Insert(BtreeEachColumnU32U64Str{Id: id, Age: age, Name: name}) +} + +//stdb:reducer name=insert_unique_0_u32_u64_u64 +func insertUnique0U32U64U64(_ reducer.ReducerContext, id uint32, x uint64, y uint64) { + Unique0U32U64U64Table.Insert(Unique0U32U64U64{Id: id, X: x, Y: y}) +} + +//stdb:reducer name=insert_no_index_u32_u64_u64 +func insertNoIndexU32U64U64(_ reducer.ReducerContext, id uint32, x uint64, y uint64) { + NoIndexU32U64U64Table.Insert(NoIndexU32U64U64{Id: id, X: x, Y: y}) +} + +//stdb:reducer name=insert_btree_each_column_u32_u64_u64 +func insertBtreeEachColumnU32U64U64(_ reducer.ReducerContext, id uint32, x uint64, y uint64) { + BtreeEachColumnU32U64U64Table.Insert(BtreeEachColumnU32U64U64{Id: id, X: x, Y: y}) +} + +// ---------- insert bulk ---------- + +//stdb:reducer name=insert_bulk_unique_0_u32_u64_str +func insertBulkUnique0U32U64Str(_ reducer.ReducerContext, people []Unique0U32U64Str) { + for _, row := range people { + Unique0U32U64StrTable.Insert(row) + } +} + +//stdb:reducer name=insert_bulk_no_index_u32_u64_str +func insertBulkNoIndexU32U64Str(_ reducer.ReducerContext, people []NoIndexU32U64Str) { + for _, row := range people { + NoIndexU32U64StrTable.Insert(row) + } +} + +//stdb:reducer name=insert_bulk_btree_each_column_u32_u64_str +func insertBulkBtreeEachColumnU32U64Str(_ reducer.ReducerContext, people []BtreeEachColumnU32U64Str) { + for _, row := range people { + BtreeEachColumnU32U64StrTable.Insert(row) + } +} + +//stdb:reducer name=insert_bulk_unique_0_u32_u64_u64 +func insertBulkUnique0U32U64U64(_ reducer.ReducerContext, locs []Unique0U32U64U64) { + for _, row := range locs { + Unique0U32U64U64Table.Insert(row) + } +} + +//stdb:reducer name=insert_bulk_no_index_u32_u64_u64 +func insertBulkNoIndexU32U64U64(_ reducer.ReducerContext, locs []NoIndexU32U64U64) { + for _, row := range locs { + NoIndexU32U64U64Table.Insert(row) + } +} + +//stdb:reducer name=insert_bulk_btree_each_column_u32_u64_u64 +func insertBulkBtreeEachColumnU32U64U64(_ reducer.ReducerContext, locs []BtreeEachColumnU32U64U64) { + for _, row := range locs { + BtreeEachColumnU32U64U64Table.Insert(row) + } +} + +// ---------- update ---------- + +//stdb:reducer name=update_bulk_unique_0_u32_u64_u64 +func updateBulkUnique0U32U64U64(_ reducer.ReducerContext, rowCount uint32) { + iter, err := Unique0U32U64U64Table.Scan() + if err != nil { + panic(err) + } + defer iter.Close() + + var hit uint32 + for { + row, ok := iter.Next() + if !ok { + break + } + if hit >= rowCount { + break + } + hit++ + Unique0U32U64U64Table.UpdateById(Unique0U32U64U64{ + Id: row.Id, + X: row.X + 1, // wrapping add + Y: row.Y, + }) + } + if hit != rowCount { + panic("not enough rows to perform requested amount of updates") + } +} + +//stdb:reducer name=update_bulk_unique_0_u32_u64_str +func updateBulkUnique0U32U64Str(_ reducer.ReducerContext, rowCount uint32) { + iter, err := Unique0U32U64StrTable.Scan() + if err != nil { + panic(err) + } + defer iter.Close() + + var hit uint32 + for { + row, ok := iter.Next() + if !ok { + break + } + if hit >= rowCount { + break + } + hit++ + Unique0U32U64StrTable.UpdateById(Unique0U32U64Str{ + Id: row.Id, + Age: row.Age + 1, // wrapping add + Name: row.Name, + }) + } + if hit != rowCount { + panic("not enough rows to perform requested amount of updates") + } +} + +// ---------- iterate ---------- + +//stdb:reducer name=iterate_unique_0_u32_u64_str +func iterateUnique0U32U64Str(_ reducer.ReducerContext) { + iter, err := Unique0U32U64StrTable.Scan() + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + // Access each field to prevent optimization. + _ = row.Id + _ = row.Age + _ = row.Name + } +} + +//stdb:reducer name=iterate_unique_0_u32_u64_u64 +func iterateUnique0U32U64U64(_ reducer.ReducerContext) { + iter, err := Unique0U32U64U64Table.Scan() + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + // Access each field to prevent optimization. + _ = row.Id + _ = row.X + _ = row.Y + } +} + +// ---------- filter by id ---------- + +//stdb:reducer name=filter_unique_0_u32_u64_str_by_id +func filterUnique0U32U64StrById(_ reducer.ReducerContext, id uint32) { + row, found, err := Unique0U32U64StrTable.FindById(id) + if err != nil { + panic(err) + } + if found { + _ = row + } +} + +//stdb:reducer name=filter_no_index_u32_u64_str_by_id +func filterNoIndexU32U64StrById(_ reducer.ReducerContext, id uint32) { + iter, err := NoIndexU32U64StrTable.Scan() + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + if row.Id == id { + _ = row + } + } +} + +//stdb:reducer name=filter_btree_each_column_u32_u64_str_by_id +func filterBtreeEachColumnU32U64StrById(_ reducer.ReducerContext, id uint32) { + iter, err := BtreeEachColumnU32U64StrTable.FilterById(id) + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + _ = row + } +} + +//stdb:reducer name=filter_unique_0_u32_u64_u64_by_id +func filterUnique0U32U64U64ById(_ reducer.ReducerContext, id uint32) { + row, found, err := Unique0U32U64U64Table.FindById(id) + if err != nil { + panic(err) + } + if found { + _ = row + } +} + +//stdb:reducer name=filter_no_index_u32_u64_u64_by_id +func filterNoIndexU32U64U64ById(_ reducer.ReducerContext, id uint32) { + iter, err := NoIndexU32U64U64Table.Scan() + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + if row.Id == id { + _ = row + } + } +} + +//stdb:reducer name=filter_btree_each_column_u32_u64_u64_by_id +func filterBtreeEachColumnU32U64U64ById(_ reducer.ReducerContext, id uint32) { + iter, err := BtreeEachColumnU32U64U64Table.FilterById(id) + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + _ = row + } +} + +// ---------- filter by name ---------- + +//stdb:reducer name=filter_unique_0_u32_u64_str_by_name +func filterUnique0U32U64StrByName(_ reducer.ReducerContext, name string) { + iter, err := Unique0U32U64StrTable.Scan() + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + if row.Name == name { + _ = row + } + } +} + +//stdb:reducer name=filter_no_index_u32_u64_str_by_name +func filterNoIndexU32U64StrByName(_ reducer.ReducerContext, name string) { + iter, err := NoIndexU32U64StrTable.Scan() + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + if row.Name == name { + _ = row + } + } +} + +//stdb:reducer name=filter_btree_each_column_u32_u64_str_by_name +func filterBtreeEachColumnU32U64StrByName(_ reducer.ReducerContext, name string) { + iter, err := BtreeEachColumnU32U64StrTable.FilterByName(name) + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + _ = row + } +} + +// ---------- filter by x ---------- + +//stdb:reducer name=filter_unique_0_u32_u64_u64_by_x +func filterUnique0U32U64U64ByX(_ reducer.ReducerContext, x uint64) { + iter, err := Unique0U32U64U64Table.Scan() + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + if row.X == x { + _ = row + } + } +} + +//stdb:reducer name=filter_no_index_u32_u64_u64_by_x +func filterNoIndexU32U64U64ByX(_ reducer.ReducerContext, x uint64) { + iter, err := NoIndexU32U64U64Table.Scan() + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + if row.X == x { + _ = row + } + } +} + +//stdb:reducer name=filter_btree_each_column_u32_u64_u64_by_x +func filterBtreeEachColumnU32U64U64ByX(_ reducer.ReducerContext, x uint64) { + iter, err := BtreeEachColumnU32U64U64Table.FilterByX(x) + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + _ = row + } +} + +// ---------- filter by y ---------- + +//stdb:reducer name=filter_unique_0_u32_u64_u64_by_y +func filterUnique0U32U64U64ByY(_ reducer.ReducerContext, y uint64) { + iter, err := Unique0U32U64U64Table.Scan() + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + if row.Y == y { + _ = row + } + } +} + +//stdb:reducer name=filter_no_index_u32_u64_u64_by_y +func filterNoIndexU32U64U64ByY(_ reducer.ReducerContext, y uint64) { + iter, err := NoIndexU32U64U64Table.Scan() + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + if row.Y == y { + _ = row + } + } +} + +//stdb:reducer name=filter_btree_each_column_u32_u64_u64_by_y +func filterBtreeEachColumnU32U64U64ByY(_ reducer.ReducerContext, y uint64) { + iter, err := BtreeEachColumnU32U64U64Table.FilterByY(y) + if err != nil { + panic(err) + } + defer iter.Close() + + for { + row, ok := iter.Next() + if !ok { + break + } + _ = row + } +} + +// ---------- delete ---------- + +//stdb:reducer name=delete_unique_0_u32_u64_str_by_id +func deleteUnique0U32U64StrById(_ reducer.ReducerContext, id uint32) { + Unique0U32U64StrTable.DeleteById(id) +} + +//stdb:reducer name=delete_unique_0_u32_u64_u64_by_id +func deleteUnique0U32U64U64ById(_ reducer.ReducerContext, id uint32) { + Unique0U32U64U64Table.DeleteById(id) +} + +// ---------- clear table ---------- + +//stdb:reducer name=clear_table_unique_0_u32_u64_str +func clearTableUnique0U32U64Str(_ reducer.ReducerContext) { + panic("unimplemented") +} + +//stdb:reducer name=clear_table_no_index_u32_u64_str +func clearTableNoIndexU32U64Str(_ reducer.ReducerContext) { + panic("unimplemented") +} + +//stdb:reducer name=clear_table_btree_each_column_u32_u64_str +func clearTableBtreeEachColumnU32U64Str(_ reducer.ReducerContext) { + panic("unimplemented") +} + +//stdb:reducer name=clear_table_unique_0_u32_u64_u64 +func clearTableUnique0U32U64U64(_ reducer.ReducerContext) { + panic("unimplemented") +} + +//stdb:reducer name=clear_table_no_index_u32_u64_u64 +func clearTableNoIndexU32U64U64(_ reducer.ReducerContext) { + panic("unimplemented") +} + +//stdb:reducer name=clear_table_btree_each_column_u32_u64_u64 +func clearTableBtreeEachColumnU32U64U64(_ reducer.ReducerContext) { + panic("unimplemented") +} + +// ---------- count ---------- + +//stdb:reducer name=count_unique_0_u32_u64_str +func countUnique0U32U64Str(_ reducer.ReducerContext) { + n, err := Unique0U32U64StrTable.Count() + if err != nil { + panic(err) + } + benchLogger.Info(fmt.Sprintf("COUNT: %d", n)) +} + +//stdb:reducer name=count_no_index_u32_u64_str +func countNoIndexU32U64Str(_ reducer.ReducerContext) { + n, err := NoIndexU32U64StrTable.Count() + if err != nil { + panic(err) + } + benchLogger.Info(fmt.Sprintf("COUNT: %d", n)) +} + +//stdb:reducer name=count_btree_each_column_u32_u64_str +func countBtreeEachColumnU32U64Str(_ reducer.ReducerContext) { + n, err := BtreeEachColumnU32U64StrTable.Count() + if err != nil { + panic(err) + } + benchLogger.Info(fmt.Sprintf("COUNT: %d", n)) +} + +//stdb:reducer name=count_unique_0_u32_u64_u64 +func countUnique0U32U64U64(_ reducer.ReducerContext) { + n, err := Unique0U32U64U64Table.Count() + if err != nil { + panic(err) + } + benchLogger.Info(fmt.Sprintf("COUNT: %d", n)) +} + +//stdb:reducer name=count_no_index_u32_u64_u64 +func countNoIndexU32U64U64(_ reducer.ReducerContext) { + n, err := NoIndexU32U64U64Table.Count() + if err != nil { + panic(err) + } + benchLogger.Info(fmt.Sprintf("COUNT: %d", n)) +} + +//stdb:reducer name=count_btree_each_column_u32_u64_u64 +func countBtreeEachColumnU32U64U64(_ reducer.ReducerContext) { + n, err := BtreeEachColumnU32U64U64Table.Count() + if err != nil { + panic(err) + } + benchLogger.Info(fmt.Sprintf("COUNT: %d", n)) +} + +// ---------- module-specific ---------- + +//stdb:reducer name=fn_with_1_args +func fnWith1Args(_ reducer.ReducerContext, _ string) {} + +//stdb:reducer name=fn_with_32_args +func fnWith32Args( + _ reducer.ReducerContext, + _, _, _, _, _, _, _, _ string, + _, _, _, _, _, _, _, _ string, + _, _, _, _, _, _, _, _ string, + _, _, _, _, _, _, _, _ string, +) { +} + +//stdb:reducer name=print_many_things +func printManyThings(_ reducer.ReducerContext, n uint32) { + for i := uint32(0); i < n; i++ { + benchLogger.Info("hello again!") + } +} diff --git a/modules/module-test-go/go.mod b/modules/module-test-go/go.mod new file mode 100644 index 00000000000..89114c7c523 --- /dev/null +++ b/modules/module-test-go/go.mod @@ -0,0 +1,7 @@ +module github.com/clockworklabs/SpacetimeDB/modules/module-test-go + +go 1.23 + +require github.com/clockworklabs/SpacetimeDB/sdks/go v0.0.0 + +replace github.com/clockworklabs/SpacetimeDB/sdks/go => ../../sdks/go diff --git a/modules/module-test-go/main.go b/modules/module-test-go/main.go new file mode 100644 index 00000000000..e4bb606a3cd --- /dev/null +++ b/modules/module-test-go/main.go @@ -0,0 +1,5 @@ +package main + +//go:generate stdb-gen + +func main() {} diff --git a/modules/module-test-go/procedures.go b/modules/module-test-go/procedures.go new file mode 100644 index 00000000000..5d8fa8c3136 --- /dev/null +++ b/modules/module-test-go/procedures.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "time" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/server" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +//stdb:procedure +func sleepOneSecond(ctx server.ProcedureContext) { + prevTime := ctx.Timestamp() + target := types.NewTimestamp(prevTime.Microseconds() + int64(time.Second/time.Microsecond)) + ctx.SleepUntil(target) + newTime := ctx.Timestamp() + actualDelta := newTime.Microseconds() - prevTime.Microseconds() + logger.Info(fmt.Sprintf("Slept from %v to %v, a total of %d microseconds", prevTime, newTime, actualDelta)) +} + +//stdb:procedure +func returnValue(_ server.ProcedureContext, foo uint64) Baz { + return Baz{Field: fmt.Sprintf("%d", foo)} +} + +//stdb:procedure name=with_tx +func withTx(ctx server.ProcedureContext) { + ctx.WithTx(func() { + sayHelloInTx() + }) +} + +// sayHelloInTx is the same logic as sayHello but callable within a procedure transaction. +func sayHelloInTx() { + iter, err := PersonTable.Scan() + if err != nil { + panic(fmt.Sprintf("Scan error: %v", err)) + } + defer iter.Close() + for person, ok := iter.Next(); ok; person, ok = iter.Next() { + logger.Info(fmt.Sprintf("Hello, %s!", person.Name)) + } + logger.Info("Hello, World!") +} + +//stdb:procedure name=get_my_schema_via_http +func getMySchemaViaHttp(ctx server.ProcedureContext) string { + moduleIdentity := ctx.Identity() + uri := fmt.Sprintf("http://localhost:3000/v1/database/%s/schema?version=9", moduleIdentity) + _, body, err := ctx.HttpGet(uri) + if err != nil { + panic(fmt.Sprintf("HTTP error: %v", err)) + } + return string(body) +} diff --git a/modules/module-test-go/reducers.go b/modules/module-test-go/reducers.go new file mode 100644 index 00000000000..ff5be6ab9c4 --- /dev/null +++ b/modules/module-test-go/reducers.go @@ -0,0 +1,240 @@ +package main + +import ( + "fmt" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/server" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/log" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +var logger = log.NewLogger("module-test-go") + +//stdb:init +func initReducer(ctx server.ReducerContext) { + RepeatingTestArgTable.Insert(RepeatingTestArg{ + ScheduledId: 0, + ScheduledAt: types.ScheduleAtInterval{Value: types.NewTimeDuration(1000 * 1000)}, // 1000ms in micros + PrevTime: ctx.Timestamp(), + }) +} + +//stdb:reducer +func repeatingTest(ctx server.ReducerContext, arg RepeatingTestArg) { + deltaTime := ctx.Timestamp().Microseconds() - arg.PrevTime.Microseconds() + logger.Trace(fmt.Sprintf("Timestamp: %v, Delta time: %d", ctx.Timestamp(), deltaTime)) +} + +//stdb:reducer +func add(ctx server.ReducerContext, name string, age uint8) { + _ = ctx + PersonTable.Insert(Person{Id: 0, Name: name, Age: age}) +} + +//stdb:reducer name=say_hello +func sayHello(ctx server.ReducerContext) { + iter, err := PersonTable.Scan() + if err != nil { + panic(fmt.Sprintf("Scan error: %v", err)) + } + defer iter.Close() + for person, ok := iter.Next(); ok; person, ok = iter.Next() { + logger.Info(fmt.Sprintf("Hello, %s!", person.Name)) + } + logger.Info("Hello, World!") +} + +//stdb:reducer name=list_over_age +func listOverAge(ctx server.ReducerContext, age uint8) { + _ = ctx + iter, err := PersonTable.Scan() + if err != nil { + panic(fmt.Sprintf("Scan error: %v", err)) + } + defer iter.Close() + for person, ok := iter.Next(); ok; person, ok = iter.Next() { + if person.Age >= age { + logger.Info(fmt.Sprintf("%s has age %d >= %d", person.Name, person.Age, age)) + } + } +} + +//stdb:reducer name=log_module_identity +func logModuleIdentity(ctx server.ReducerContext) { + logger.Info(fmt.Sprintf("Module identity: %v", ctx.Identity())) +} + +//stdb:reducer +func test(ctx server.ReducerContext, arg TestAlias, arg2 TestB, arg3 TestC, arg4 TestF) error { + logger.Info("BEGIN") + logger.Info(fmt.Sprintf("sender: %v", ctx.Sender())) + logger.Info(fmt.Sprintf("timestamp: %v", ctx.Timestamp())) + logger.Info(fmt.Sprintf(`bar: "%s"`, arg2.Foo)) + + switch arg3 { + case TestCFoo: + logger.Info("Foo") + case TestCBar: + logger.Info("Bar") + } + + switch v := arg4.(type) { + case TestFFoo: + logger.Info("Foo") + case TestFBar: + logger.Info("Bar") + case TestFBaz: + logger.Info(v.Value) + } + + for i := uint32(0); i < 1000; i++ { + TestATable.Insert(TestA{ + X: i + arg.X, + Y: i + arg.Y, + Z: "Yo", + }) + } + + rowCountBeforeDelete, err := TestATable.Count() + if err != nil { + return fmt.Errorf("Count error: %w", err) + } + logger.Info(fmt.Sprintf("Row count before delete: %d", rowCountBeforeDelete)) + + var numDeleted uint32 + for row := uint32(5); row < 10; row++ { + numDeleted += TestATable.DeleteByX(row) + } + + rowCountAfterDelete, err := TestATable.Count() + if err != nil { + return fmt.Errorf("Count error: %w", err) + } + + if rowCountBeforeDelete != rowCountAfterDelete+uint64(numDeleted) { + logger.Error(fmt.Sprintf( + "Started with %d rows, deleted %d, and wound up with %d rows... huh?", + rowCountBeforeDelete, numDeleted, rowCountAfterDelete, + )) + } + + inserted := TestETable.Insert(TestE{Id: 0, Name: "Tyler"}) + logger.Info(fmt.Sprintf(`Inserted: TestE { id: %d, name: "%s" }`, inserted.Id, inserted.Name)) + + logger.Info(fmt.Sprintf("Row count after delete: %d", rowCountAfterDelete)) + + otherRowCount, err := TestATable.Count() + if err != nil { + return fmt.Errorf("Count error: %w", err) + } + logger.Info(fmt.Sprintf("Row count filtered by condition: %d", otherRowCount)) + + logger.Info("MultiColumn") + + for i := int64(0); i < 1000; i++ { + PointsTable.Insert(Point{ + X: i + int64(arg.X), + Y: i + int64(arg.Y), + }) + } + + pointIter, err := PointsTable.Scan() + if err != nil { + return fmt.Errorf("Scan error: %w", err) + } + defer pointIter.Close() + multiRowCount := 0 + for point, ok := pointIter.Next(); ok; point, ok = pointIter.Next() { + if point.X >= 0 && point.Y <= 200 { + multiRowCount++ + } + } + logger.Info(fmt.Sprintf("Row count filtered by multi-column condition: %d", multiRowCount)) + + logger.Info("END") + return nil +} + +//stdb:reducer name=add_player +func addPlayer(ctx server.ReducerContext, name string) error { + _ = ctx + // Insert always creates a new row since id is auto_inc with value 0. + inserted := TestETable.Insert(TestE{Id: 0, Name: name}) + + // Update the same row (no-op, but verifies UpdateBy works). + TestETable.UpdateById(inserted) + + return nil +} + +//stdb:reducer name=delete_player +func deletePlayer(ctx server.ReducerContext, id uint64) error { + _ = ctx + deleted := TestETable.DeleteById(id) + if deleted == 0 { + return fmt.Errorf("No TestE row with id %d", id) + } + return nil +} + +//stdb:reducer name=delete_players_by_name +func deletePlayersByName(ctx server.ReducerContext, name string) error { + _ = ctx + deleted := TestETable.DeleteByName(name) + if deleted == 0 { + return fmt.Errorf("No TestE row with name %q", name) + } + logger.Info(fmt.Sprintf("Deleted %d player(s) with name %q", deleted, name)) + return nil +} + +//stdb:connect +func clientConnected(_ server.ReducerContext) {} + +//stdb:reducer name=add_private +func addPrivate(ctx server.ReducerContext, name string) { + _ = ctx + PrivateTableTable.Insert(PrivateTable{Name: name}) +} + +//stdb:reducer name=query_private +func queryPrivate(ctx server.ReducerContext) { + _ = ctx + iter, err := PrivateTableTable.Scan() + if err != nil { + panic(fmt.Sprintf("Scan error: %v", err)) + } + defer iter.Close() + for person, ok := iter.Next(); ok; person, ok = iter.Next() { + logger.Info(fmt.Sprintf("Private, %s!", person.Name)) + } + logger.Info("Private, World!") +} + +//stdb:reducer name=test_btree_index_args +func testBtreeIndexArgs(ctx server.ReducerContext) { + _ = ctx + // This reducer tests that various index operations compile and work. + + // Single-column string index on test_e.name + str := "String" + _, _ = TestETable.FilterByName(str) + _, _ = TestETable.FilterByName("str") + + TestETable.DeleteByName(str) + TestETable.DeleteByName("str") + + // Multi-column index on points (x, y) + _, _ = PointsTable.FilterByX(int64(0)) + _, _ = PointsTable.FilterByXAndY(int64(0), int64(1)) +} + +//stdb:reducer name=assert_caller_identity_is_module_identity +func assertCallerIdentityIsModuleIdentity(ctx server.ReducerContext) { + caller := ctx.Sender() + owner := ctx.Identity() + if caller != owner { + panic(fmt.Sprintf("Caller %v is not the owner %v", caller, owner)) + } + logger.Info(fmt.Sprintf("Called by the owner %v", owner)) +} diff --git a/modules/module-test-go/tables.go b/modules/module-test-go/tables.go new file mode 100644 index 00000000000..af41dbd1221 --- /dev/null +++ b/modules/module-test-go/tables.go @@ -0,0 +1,90 @@ +package main + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/types" + +// Person table — PK auto_inc id, btree on age, public. +//stdb:table name=person access=public index=person_age_idx_btree:2 +type Person struct { + Id uint32 `stdb:"primarykey,autoinc"` + Name string + Age uint8 +} + +// RemoveTable — default (no feature flag) version. +//stdb:table name=table_to_remove access=private +type RemoveTable struct { + Id uint32 +} + +// TestA table — btree on x, private. +//stdb:table name=test_a access=private +type TestA struct { + X uint32 `stdb:"index=btree"` + Y uint32 + Z string +} + +// TestD table — optional TestC field, public. +//stdb:table name=test_d access=public +type TestD struct { + TestC *TestC +} + +// TestE table — PK auto_inc id, btree on name, private. +//stdb:table name=test_e access=private +type TestE struct { + Id uint64 `stdb:"primarykey,autoinc"` + Name string `stdb:"index=btree"` +} + +// TestFoobar table — field of sum type Foobar, public. +//stdb:table name=test_f access=public +type TestFoobar struct { + Field Foobar +} + +// PrivateTable — private table with a name field. +//stdb:table name=private_table access=private +type PrivateTable struct { + Name string +} + +// Point table — multi-column btree on (x, y), private. +//stdb:table name=points access=private index=points_multi_column_index_idx_btree:0,1 +type Point struct { + X int64 + Y int64 +} + +// PkMultiIdentity — PK on id, unique auto_inc on other, private. +//stdb:table name=pk_multi_identity access=private +type PkMultiIdentity struct { + Id uint32 `stdb:"primarykey"` + Other uint32 `stdb:"unique,autoinc"` +} + +// RepeatingTestArg — scheduled table, PK auto_inc scheduled_id. +//stdb:table name=repeating_test_arg access=private +//stdb:schedule table=repeating_test_arg function=repeating_test +type RepeatingTestArg struct { + ScheduledId uint64 `stdb:"primarykey,autoinc"` + ScheduledAt types.ScheduleAt + PrevTime types.Timestamp +} + +// HasSpecialStuff — table with Identity and ConnectionId fields. +//stdb:table name=has_special_stuff access=private +type HasSpecialStuff struct { + Identity types.Identity + ConnectionId types.ConnectionId +} + +// Player — PK on identity, auto_inc unique player_id, unique name, public. +// Used by both the "player" and "logged_out_player" tables. +//stdb:table name=player access=public +//stdb:table name=logged_out_player access=public +type Player struct { + Identity types.Identity `stdb:"primarykey"` + PlayerId uint64 `stdb:"autoinc,unique"` + Name string `stdb:"unique"` +} diff --git a/modules/module-test-go/types.go b/modules/module-test-go/types.go new file mode 100644 index 00000000000..86d3c680560 --- /dev/null +++ b/modules/module-test-go/types.go @@ -0,0 +1,71 @@ +package main + +// TestB is a product type used in the test reducer. +type TestB struct { + Foo string +} + +// TestC is a simple enum with namespace scope "Namespace.TestC". +//stdb:enum variants=Foo,Bar scope=Namespace +type TestC uint8 + +const ( + TestCFoo TestC = 0 + TestCBar TestC = 1 +) + +// Baz is a product type used in the Foobar sum type. +type Baz struct { + Field string +} + +// Foobar is a sum type: Baz(Baz), Bar, Har(u32). +//stdb:sumtype +type Foobar interface { + foobarTag() uint8 +} + +//stdb:variant of=Foobar name=Baz +type FoobarBaz struct { + Value Baz +} + +func (FoobarBaz) foobarTag() uint8 { return 0 } + +//stdb:variant of=Foobar name=Bar +type FoobarBar struct{} + +func (FoobarBar) foobarTag() uint8 { return 1 } + +//stdb:variant of=Foobar name=Har +type FoobarHar struct { + Value uint32 +} + +func (FoobarHar) foobarTag() uint8 { return 2 } + +// TestF is a sum type with namespace scope "Namespace.TestF": Foo, Bar, Baz(String). +//stdb:sumtype scope=Namespace +type TestF interface { + testFTag() uint8 +} + +//stdb:variant of=TestF name=Foo +type TestFFoo struct{} + +func (TestFFoo) testFTag() uint8 { return 0 } + +//stdb:variant of=TestF name=Bar +type TestFBar struct{} + +func (TestFBar) testFTag() uint8 { return 1 } + +//stdb:variant of=TestF name=Baz +type TestFBaz struct { + Value string +} + +func (TestFBaz) testFTag() uint8 { return 2 } + +// TestAlias is an alias for TestA. +type TestAlias = TestA diff --git a/modules/module-test-go/views.go b/modules/module-test-go/views.go new file mode 100644 index 00000000000..f0c631611c0 --- /dev/null +++ b/modules/module-test-go/views.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/server" +) + +//stdb:view name=my_player public=true +func myPlayerView(ctx server.ViewContext) *Player { + player, found, err := PlayerTable.FindByIdentity(ctx.Sender()) + if err != nil || !found { + return nil + } + return &player +} diff --git a/modules/perf-test-go/go.mod b/modules/perf-test-go/go.mod new file mode 100644 index 00000000000..9e5a5c5b692 --- /dev/null +++ b/modules/perf-test-go/go.mod @@ -0,0 +1,7 @@ +module github.com/clockworklabs/SpacetimeDB/modules/perf-test-go + +go 1.23 + +require github.com/clockworklabs/SpacetimeDB/sdks/go v0.0.0 + +replace github.com/clockworklabs/SpacetimeDB/sdks/go => ../../sdks/go diff --git a/modules/perf-test-go/main.go b/modules/perf-test-go/main.go new file mode 100644 index 00000000000..e4bb606a3cd --- /dev/null +++ b/modules/perf-test-go/main.go @@ -0,0 +1,5 @@ +package main + +//go:generate stdb-gen + +func main() {} diff --git a/modules/perf-test-go/perf-test-go b/modules/perf-test-go/perf-test-go new file mode 100755 index 00000000000..7c64a5ecf9d Binary files /dev/null and b/modules/perf-test-go/perf-test-go differ diff --git a/modules/perf-test-go/reducers.go b/modules/perf-test-go/reducers.go new file mode 100644 index 00000000000..5e1d02123a4 --- /dev/null +++ b/modules/perf-test-go/reducers.go @@ -0,0 +1,116 @@ +package main + +import ( + "fmt" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/server" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/sys" +) + +const ( + numChunks uint64 = 1000 + rowsPerChunk uint64 = 1200 + testID uint64 = 989987 + testChunk uint64 = testID / rowsPerChunk +) + +//stdb:reducer name=load_location_table +func loadLocationTable(_ server.ReducerContext) { + for chunk := uint64(0); chunk < numChunks; chunk++ { + for i := uint64(0); i < rowsPerChunk; i++ { + id := chunk*1200 + i + x := int32(0) + z := int32(chunk) + dimension := uint32(id) + LocationTable.Insert(Location{ + Id: id, + Chunk: chunk, + X: x, + Z: z, + Dimension: dimension, + }) + } + } +} + +//stdb:reducer name=test_index_scan_on_id +func testIndexScanOnId(_ server.ReducerContext) { + timerId := sys.ConsoleTimerStart("Index scan on {id}") + location, found, err := LocationTable.FindById(testID) + _ = sys.ConsoleTimerEnd(timerId) + if err != nil { + panic(fmt.Sprintf("FindBy error: %v", err)) + } + if !found { + panic(fmt.Sprintf("location with id %d not found", testID)) + } + if location.Id != testID { + panic(fmt.Sprintf("expected id %d, got %d", testID, location.Id)) + } +} + +//stdb:reducer name=test_index_scan_on_chunk +func testIndexScanOnChunk(_ server.ReducerContext) { + timerId := sys.ConsoleTimerStart("Index scan on {chunk}") + iter, err := LocationTable.FilterByChunk(testChunk) + _ = sys.ConsoleTimerEnd(timerId) + if err != nil { + panic(fmt.Sprintf("FilterBy error: %v", err)) + } + count := uint64(0) + for { + _, ok := iter.Next() + if !ok { + break + } + count++ + } + if count != rowsPerChunk { + panic(fmt.Sprintf("expected %d rows, got %d", rowsPerChunk, count)) + } +} + +//stdb:reducer name=test_index_scan_on_x_z_dimension +func testIndexScanOnXZDimension(_ server.ReducerContext) { + z := int32(testChunk) + dimension := uint32(testID) + timerId := sys.ConsoleTimerStart("Index scan on {x, z, dimension}") + iter, err := LocationTable.FilterByXAndZAndDimension(int32(0), z, dimension) + _ = sys.ConsoleTimerEnd(timerId) + if err != nil { + panic(fmt.Sprintf("FilterByMultiColumn error: %v", err)) + } + count := 0 + for { + _, ok := iter.Next() + if !ok { + break + } + count++ + } + if count != 1 { + panic(fmt.Sprintf("expected 1 row, got %d", count)) + } +} + +//stdb:reducer name=test_index_scan_on_x_z +func testIndexScanOnXZ(_ server.ReducerContext) { + z := int32(testChunk) + timerId := sys.ConsoleTimerStart("Index scan on {x, z}") + iter, err := LocationTable.FilterByXAndZ(int32(0), z) + _ = sys.ConsoleTimerEnd(timerId) + if err != nil { + panic(fmt.Sprintf("FilterByMultiColumn error: %v", err)) + } + count := uint64(0) + for { + _, ok := iter.Next() + if !ok { + break + } + count++ + } + if count != rowsPerChunk { + panic(fmt.Sprintf("expected %d rows, got %d", rowsPerChunk, count)) + } +} diff --git a/modules/perf-test-go/tables.go b/modules/perf-test-go/tables.go new file mode 100644 index 00000000000..db143a1bab7 --- /dev/null +++ b/modules/perf-test-go/tables.go @@ -0,0 +1,10 @@ +package main + +//stdb:table name=location access=private index=location_coordinates_idx_btree:2,3,4 +type Location struct { + Id uint64 `stdb:"primarykey"` + Chunk uint64 `stdb:"index=btree"` + X int32 `stdb:"index=btree"` + Z int32 + Dimension uint32 +} diff --git a/modules/sdk-test-connect-disconnect-go/go.mod b/modules/sdk-test-connect-disconnect-go/go.mod new file mode 100644 index 00000000000..8a40b41d658 --- /dev/null +++ b/modules/sdk-test-connect-disconnect-go/go.mod @@ -0,0 +1,7 @@ +module github.com/clockworklabs/SpacetimeDB/modules/sdk-test-connect-disconnect-go + +go 1.23 + +require github.com/clockworklabs/SpacetimeDB/sdks/go v0.0.0 + +replace github.com/clockworklabs/SpacetimeDB/sdks/go => ../../sdks/go diff --git a/modules/sdk-test-connect-disconnect-go/init.go b/modules/sdk-test-connect-disconnect-go/init.go new file mode 100644 index 00000000000..d330e3b59c4 --- /dev/null +++ b/modules/sdk-test-connect-disconnect-go/init.go @@ -0,0 +1,26 @@ +package main + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/reducer" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +//stdb:table name=connected access=public +type Connected struct { + Identity types.Identity `stdb:"identity"` +} + +//stdb:table name=disconnected access=public +type Disconnected struct { + Identity types.Identity `stdb:"identity"` +} + +//stdb:connect +func onConnect(ctx reducer.ReducerContext) { + ConnectedTable.Insert(Connected{Identity: ctx.Sender()}) +} + +//stdb:disconnect +func onDisconnect(ctx reducer.ReducerContext) { + DisconnectedTable.Insert(Disconnected{Identity: ctx.Sender()}) +} diff --git a/modules/sdk-test-connect-disconnect-go/main.go b/modules/sdk-test-connect-disconnect-go/main.go new file mode 100644 index 00000000000..e4bb606a3cd --- /dev/null +++ b/modules/sdk-test-connect-disconnect-go/main.go @@ -0,0 +1,5 @@ +package main + +//go:generate stdb-gen + +func main() {} diff --git a/modules/sdk-test-event-table-go/go.mod b/modules/sdk-test-event-table-go/go.mod new file mode 100644 index 00000000000..8a4a59e99d2 --- /dev/null +++ b/modules/sdk-test-event-table-go/go.mod @@ -0,0 +1,7 @@ +module github.com/clockworklabs/SpacetimeDB/modules/sdk-test-event-table-go + +go 1.23 + +require github.com/clockworklabs/SpacetimeDB/sdks/go v0.0.0 + +replace github.com/clockworklabs/SpacetimeDB/sdks/go => ../../sdks/go diff --git a/modules/sdk-test-event-table-go/main.go b/modules/sdk-test-event-table-go/main.go new file mode 100644 index 00000000000..e4bb606a3cd --- /dev/null +++ b/modules/sdk-test-event-table-go/main.go @@ -0,0 +1,5 @@ +package main + +//go:generate stdb-gen + +func main() {} diff --git a/modules/sdk-test-event-table-go/reducers.go b/modules/sdk-test-event-table-go/reducers.go new file mode 100644 index 00000000000..f7df9eed0dd --- /dev/null +++ b/modules/sdk-test-event-table-go/reducers.go @@ -0,0 +1,20 @@ +package main + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/server" +) + +//stdb:reducer +func emitTestEvent(_ server.ReducerContext, name string, value uint64) { + TestEventTable.Insert(TestEvent{Name: name, Value: value}) +} + +//stdb:reducer +func emitMultipleTestEvents(_ server.ReducerContext) { + TestEventTable.Insert(TestEvent{Name: "a", Value: 1}) + TestEventTable.Insert(TestEvent{Name: "b", Value: 2}) + TestEventTable.Insert(TestEvent{Name: "c", Value: 3}) +} + +//stdb:reducer +func noop(_ server.ReducerContext) {} diff --git a/modules/sdk-test-event-table-go/sdk-test-event-table-go b/modules/sdk-test-event-table-go/sdk-test-event-table-go new file mode 100755 index 00000000000..c232fc5d598 Binary files /dev/null and b/modules/sdk-test-event-table-go/sdk-test-event-table-go differ diff --git a/modules/sdk-test-event-table-go/tables.go b/modules/sdk-test-event-table-go/tables.go new file mode 100644 index 00000000000..9660996e645 --- /dev/null +++ b/modules/sdk-test-event-table-go/tables.go @@ -0,0 +1,7 @@ +package main + +//stdb:table name=test_event access=public event=true +type TestEvent struct { + Name string + Value uint64 +} diff --git a/modules/sdk-test-go/go.mod b/modules/sdk-test-go/go.mod new file mode 100644 index 00000000000..ffb63d815c4 --- /dev/null +++ b/modules/sdk-test-go/go.mod @@ -0,0 +1,7 @@ +module github.com/clockworklabs/SpacetimeDB/modules/sdk-test-go + +go 1.23 + +require github.com/clockworklabs/SpacetimeDB/sdks/go v0.0.0 + +replace github.com/clockworklabs/SpacetimeDB/sdks/go => ../../sdks/go diff --git a/modules/sdk-test-go/main.go b/modules/sdk-test-go/main.go new file mode 100644 index 00000000000..e4bb606a3cd --- /dev/null +++ b/modules/sdk-test-go/main.go @@ -0,0 +1,5 @@ +package main + +//go:generate stdb-gen + +func main() {} diff --git a/modules/sdk-test-go/module.wasm b/modules/sdk-test-go/module.wasm new file mode 100644 index 00000000000..a6fe47ab560 Binary files /dev/null and b/modules/sdk-test-go/module.wasm differ diff --git a/modules/sdk-test-go/reducers.go b/modules/sdk-test-go/reducers.go new file mode 100644 index 00000000000..b52a950ab1a --- /dev/null +++ b/modules/sdk-test-go/reducers.go @@ -0,0 +1,1197 @@ +package main + +import ( + "fmt" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/server" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// --------------------------------------------------------------------------- +// One* insert reducers -- each inserts a single-field row. +// --------------------------------------------------------------------------- + +//stdb:reducer +func insertOneU8(_ server.ReducerContext, n uint8) { + OneU8Table.Insert(OneU8{N: n}) +} + +//stdb:reducer +func insertOneU16(_ server.ReducerContext, n uint16) { + OneU16Table.Insert(OneU16{N: n}) +} + +//stdb:reducer +func insertOneU32(_ server.ReducerContext, n uint32) { + OneU32Table.Insert(OneU32{N: n}) +} + +//stdb:reducer +func insertOneU64(_ server.ReducerContext, n uint64) { + OneU64Table.Insert(OneU64{N: n}) +} + +//stdb:reducer +func insertOneU128(_ server.ReducerContext, n types.Uint128) { + OneU128Table.Insert(OneU128{N: n}) +} + +//stdb:reducer +func insertOneU256(_ server.ReducerContext, n types.Uint256) { + OneU256Table.Insert(OneU256{N: n}) +} + +//stdb:reducer +func insertOneI8(_ server.ReducerContext, n int8) { + OneI8Table.Insert(OneI8{N: n}) +} + +//stdb:reducer +func insertOneI16(_ server.ReducerContext, n int16) { + OneI16Table.Insert(OneI16{N: n}) +} + +//stdb:reducer +func insertOneI32(_ server.ReducerContext, n int32) { + OneI32Table.Insert(OneI32{N: n}) +} + +//stdb:reducer +func insertOneI64(_ server.ReducerContext, n int64) { + OneI64Table.Insert(OneI64{N: n}) +} + +//stdb:reducer +func insertOneI128(_ server.ReducerContext, n types.Int128) { + OneI128Table.Insert(OneI128{N: n}) +} + +//stdb:reducer +func insertOneI256(_ server.ReducerContext, n types.Int256) { + OneI256Table.Insert(OneI256{N: n}) +} + +//stdb:reducer +func insertOneBool(_ server.ReducerContext, b bool) { + OneBoolTable.Insert(OneBool{B: b}) +} + +//stdb:reducer +func insertOneF32(_ server.ReducerContext, f float32) { + OneF32Table.Insert(OneF32{F: f}) +} + +//stdb:reducer +func insertOneF64(_ server.ReducerContext, f float64) { + OneF64Table.Insert(OneF64{F: f}) +} + +//stdb:reducer +func insertOneString(_ server.ReducerContext, s string) { + OneStringTable.Insert(OneString{S: s}) +} + +//stdb:reducer +func insertOneIdentity(_ server.ReducerContext, i types.Identity) { + OneIdentityTable.Insert(OneIdentity{I: i}) +} + +//stdb:reducer +func insertOneConnectionId(_ server.ReducerContext, a types.ConnectionId) { + OneConnectionIdTable.Insert(OneConnectionId{A: a}) +} + +//stdb:reducer +func insertOneUuid(_ server.ReducerContext, u types.Uuid) { + OneUuidTable.Insert(OneUuid{U: u}) +} + +//stdb:reducer +func insertOneTimestamp(_ server.ReducerContext, t types.Timestamp) { + OneTimestampTable.Insert(OneTimestamp{T: t}) +} + +//stdb:reducer +func insertOneSimpleEnum(_ server.ReducerContext, e SimpleEnum) { + OneSimpleEnumTable.Insert(OneSimpleEnum{E: e}) +} + +//stdb:reducer +func insertOneEnumWithPayload(_ server.ReducerContext, e EnumWithPayload) { + OneEnumWithPayloadTable.Insert(OneEnumWithPayload{E: e}) +} + +//stdb:reducer +func insertOneUnitStruct(_ server.ReducerContext, s UnitStruct) { + OneUnitStructTable.Insert(OneUnitStruct{S: s}) +} + +//stdb:reducer +func insertOneByteStruct(_ server.ReducerContext, s ByteStruct) { + OneByteStructTable.Insert(OneByteStruct{S: s}) +} + +//stdb:reducer +func insertOneEveryPrimitiveStruct(_ server.ReducerContext, s EveryPrimitiveStruct) { + OneEveryPrimitiveStructTable.Insert(OneEveryPrimitiveStruct{S: s}) +} + +//stdb:reducer +func insertOneEveryVecStruct(_ server.ReducerContext, s EveryVecStruct) { + OneEveryVecStructTable.Insert(OneEveryVecStruct{S: s}) +} + +// --------------------------------------------------------------------------- +// Vec* insert reducers -- each inserts a row containing a slice. +// --------------------------------------------------------------------------- + +//stdb:reducer +func insertVecU8(_ server.ReducerContext, n []uint8) { + VecU8Table.Insert(VecU8{N: n}) +} + +//stdb:reducer +func insertVecU16(_ server.ReducerContext, n []uint16) { + VecU16Table.Insert(VecU16{N: n}) +} + +//stdb:reducer +func insertVecU32(_ server.ReducerContext, n []uint32) { + VecU32Table.Insert(VecU32{N: n}) +} + +//stdb:reducer +func insertVecU64(_ server.ReducerContext, n []uint64) { + VecU64Table.Insert(VecU64{N: n}) +} + +//stdb:reducer +func insertVecU128(_ server.ReducerContext, n []types.Uint128) { + VecU128Table.Insert(VecU128{N: n}) +} + +//stdb:reducer +func insertVecU256(_ server.ReducerContext, n []types.Uint256) { + VecU256Table.Insert(VecU256{N: n}) +} + +//stdb:reducer +func insertVecI8(_ server.ReducerContext, n []int8) { + VecI8Table.Insert(VecI8{N: n}) +} + +//stdb:reducer +func insertVecI16(_ server.ReducerContext, n []int16) { + VecI16Table.Insert(VecI16{N: n}) +} + +//stdb:reducer +func insertVecI32(_ server.ReducerContext, n []int32) { + VecI32Table.Insert(VecI32{N: n}) +} + +//stdb:reducer +func insertVecI64(_ server.ReducerContext, n []int64) { + VecI64Table.Insert(VecI64{N: n}) +} + +//stdb:reducer +func insertVecI128(_ server.ReducerContext, n []types.Int128) { + VecI128Table.Insert(VecI128{N: n}) +} + +//stdb:reducer +func insertVecI256(_ server.ReducerContext, n []types.Int256) { + VecI256Table.Insert(VecI256{N: n}) +} + +//stdb:reducer +func insertVecBool(_ server.ReducerContext, b []bool) { + VecBoolTable.Insert(VecBool{B: b}) +} + +//stdb:reducer +func insertVecF32(_ server.ReducerContext, f []float32) { + VecF32Table.Insert(VecF32{F: f}) +} + +//stdb:reducer +func insertVecF64(_ server.ReducerContext, f []float64) { + VecF64Table.Insert(VecF64{F: f}) +} + +//stdb:reducer +func insertVecString(_ server.ReducerContext, s []string) { + VecStringTable.Insert(VecString{S: s}) +} + +//stdb:reducer +func insertVecIdentity(_ server.ReducerContext, i []types.Identity) { + VecIdentityTable.Insert(VecIdentity{I: i}) +} + +//stdb:reducer +func insertVecConnectionId(_ server.ReducerContext, a []types.ConnectionId) { + VecConnectionIdTable.Insert(VecConnectionId{A: a}) +} + +//stdb:reducer +func insertVecUuid(_ server.ReducerContext, u []types.Uuid) { + VecUuidTable.Insert(VecUuid{U: u}) +} + +//stdb:reducer +func insertVecTimestamp(_ server.ReducerContext, t []types.Timestamp) { + VecTimestampTable.Insert(VecTimestamp{T: t}) +} + +//stdb:reducer +func insertVecSimpleEnum(_ server.ReducerContext, e []SimpleEnum) { + VecSimpleEnumTable.Insert(VecSimpleEnum{E: e}) +} + +//stdb:reducer +func insertVecEnumWithPayload(_ server.ReducerContext, e []EnumWithPayload) { + VecEnumWithPayloadTable.Insert(VecEnumWithPayload{E: e}) +} + +//stdb:reducer +func insertVecUnitStruct(_ server.ReducerContext, s []UnitStruct) { + VecUnitStructTable.Insert(VecUnitStruct{S: s}) +} + +//stdb:reducer +func insertVecByteStruct(_ server.ReducerContext, s []ByteStruct) { + VecByteStructTable.Insert(VecByteStruct{S: s}) +} + +//stdb:reducer +func insertVecEveryPrimitiveStruct(_ server.ReducerContext, s []EveryPrimitiveStruct) { + VecEveryPrimitiveStructTable.Insert(VecEveryPrimitiveStruct{S: s}) +} + +//stdb:reducer +func insertVecEveryVecStruct(_ server.ReducerContext, s []EveryVecStruct) { + VecEveryVecStructTable.Insert(VecEveryVecStruct{S: s}) +} + +// --------------------------------------------------------------------------- +// Option* insert reducers -- each inserts a row with an optional value. +// --------------------------------------------------------------------------- + +//stdb:reducer +func insertOptionI32(_ server.ReducerContext, n *int32) { + OptionI32Table.Insert(OptionI32{N: n}) +} + +//stdb:reducer +func insertOptionString(_ server.ReducerContext, s *string) { + OptionStringTable.Insert(OptionString{S: s}) +} + +//stdb:reducer +func insertOptionIdentity(_ server.ReducerContext, i *types.Identity) { + OptionIdentityTable.Insert(OptionIdentity{I: i}) +} + +//stdb:reducer +func insertOptionUuid(_ server.ReducerContext, u *types.Uuid) { + OptionUuidTable.Insert(OptionUuid{U: u}) +} + +//stdb:reducer +func insertOptionSimpleEnum(_ server.ReducerContext, e *SimpleEnum) { + OptionSimpleEnumTable.Insert(OptionSimpleEnum{E: e}) +} + +//stdb:reducer +func insertOptionEveryPrimitiveStruct(_ server.ReducerContext, s *EveryPrimitiveStruct) { + OptionEveryPrimitiveStructTable.Insert(OptionEveryPrimitiveStruct{S: s}) +} + +//stdb:reducer +func insertOptionVecOptionI32(_ server.ReducerContext, v *[]*int32) { + OptionVecOptionI32Table.Insert(OptionVecOptionI32{V: v}) +} + +// --------------------------------------------------------------------------- +// Result* insert reducers +// --------------------------------------------------------------------------- + +//stdb:reducer +func insertResultI32String(_ server.ReducerContext, r ResultI32StringValue) { + ResultI32StringTable.Insert(ResultI32String{R: r}) +} + +//stdb:reducer +func insertResultStringI32(_ server.ReducerContext, r ResultStringI32Value) { + ResultStringI32Table.Insert(ResultStringI32{R: r}) +} + +//stdb:reducer +func insertResultIdentityString(_ server.ReducerContext, r ResultIdentityStringValue) { + ResultIdentityStringTable.Insert(ResultIdentityString{R: r}) +} + +//stdb:reducer +func insertResultSimpleEnumI32(_ server.ReducerContext, r ResultSimpleEnumI32Value) { + ResultSimpleEnumI32Table.Insert(ResultSimpleEnumI32{R: r}) +} + +//stdb:reducer +func insertResultEveryPrimitiveStructString(_ server.ReducerContext, r ResultEveryPrimitiveStructStringValue) { + ResultEveryPrimitiveStructStringTable.Insert(ResultEveryPrimitiveStructString{R: r}) +} + +//stdb:reducer +func insertResultVecI32String(_ server.ReducerContext, r ResultVecI32StringValue) { + ResultVecI32StringTable.Insert(ResultVecI32String{R: r}) +} + +// --------------------------------------------------------------------------- +// Unique* CRUD reducers -- insert, update (delete+insert), delete by unique field. +// --------------------------------------------------------------------------- + +//stdb:reducer +func insertUniqueU8(_ server.ReducerContext, n uint8, data int32) { + UniqueU8Table.Insert(UniqueU8{N: n, Data: data}) +} + +//stdb:reducer +func updateUniqueU8(_ server.ReducerContext, n uint8, data int32) { + UniqueU8Table.DeleteByN(n) + UniqueU8Table.Insert(UniqueU8{N: n, Data: data}) +} + +//stdb:reducer +func deleteUniqueU8(_ server.ReducerContext, n uint8) { + UniqueU8Table.DeleteByN(n) +} + +//stdb:reducer +func insertUniqueU16(_ server.ReducerContext, n uint16, data int32) { + UniqueU16Table.Insert(UniqueU16{N: n, Data: data}) +} + +//stdb:reducer +func updateUniqueU16(_ server.ReducerContext, n uint16, data int32) { + UniqueU16Table.DeleteByN(n) + UniqueU16Table.Insert(UniqueU16{N: n, Data: data}) +} + +//stdb:reducer +func deleteUniqueU16(_ server.ReducerContext, n uint16) { + UniqueU16Table.DeleteByN(n) +} + +//stdb:reducer +func insertUniqueU32(_ server.ReducerContext, n uint32, data int32) { + UniqueU32Table.Insert(UniqueU32{N: n, Data: data}) +} + +//stdb:reducer +func updateUniqueU32(_ server.ReducerContext, n uint32, data int32) { + UniqueU32Table.DeleteByN(n) + UniqueU32Table.Insert(UniqueU32{N: n, Data: data}) +} + +//stdb:reducer +func deleteUniqueU32(_ server.ReducerContext, n uint32) { + UniqueU32Table.DeleteByN(n) +} + +//stdb:reducer +func insertUniqueU64(_ server.ReducerContext, n uint64, data int32) { + UniqueU64Table.Insert(UniqueU64{N: n, Data: data}) +} + +//stdb:reducer +func updateUniqueU64(_ server.ReducerContext, n uint64, data int32) { + UniqueU64Table.DeleteByN(n) + UniqueU64Table.Insert(UniqueU64{N: n, Data: data}) +} + +//stdb:reducer +func deleteUniqueU64(_ server.ReducerContext, n uint64) { + UniqueU64Table.DeleteByN(n) +} + +//stdb:reducer +func insertUniqueU128(_ server.ReducerContext, n types.Uint128, data int32) { + UniqueU128Table.Insert(UniqueU128{N: n, Data: data}) +} + +//stdb:reducer +func updateUniqueU128(_ server.ReducerContext, n types.Uint128, data int32) { + UniqueU128Table.DeleteByN(n) + UniqueU128Table.Insert(UniqueU128{N: n, Data: data}) +} + +//stdb:reducer +func deleteUniqueU128(_ server.ReducerContext, n types.Uint128) { + UniqueU128Table.DeleteByN(n) +} + +//stdb:reducer +func insertUniqueU256(_ server.ReducerContext, n types.Uint256, data int32) { + UniqueU256Table.Insert(UniqueU256{N: n, Data: data}) +} + +//stdb:reducer +func updateUniqueU256(_ server.ReducerContext, n types.Uint256, data int32) { + UniqueU256Table.DeleteByN(n) + UniqueU256Table.Insert(UniqueU256{N: n, Data: data}) +} + +//stdb:reducer +func deleteUniqueU256(_ server.ReducerContext, n types.Uint256) { + UniqueU256Table.DeleteByN(n) +} + +//stdb:reducer +func insertUniqueI8(_ server.ReducerContext, n int8, data int32) { + UniqueI8Table.Insert(UniqueI8{N: n, Data: data}) +} + +//stdb:reducer +func updateUniqueI8(_ server.ReducerContext, n int8, data int32) { + UniqueI8Table.DeleteByN(n) + UniqueI8Table.Insert(UniqueI8{N: n, Data: data}) +} + +//stdb:reducer +func deleteUniqueI8(_ server.ReducerContext, n int8) { + UniqueI8Table.DeleteByN(n) +} + +//stdb:reducer +func insertUniqueI16(_ server.ReducerContext, n int16, data int32) { + UniqueI16Table.Insert(UniqueI16{N: n, Data: data}) +} + +//stdb:reducer +func updateUniqueI16(_ server.ReducerContext, n int16, data int32) { + UniqueI16Table.DeleteByN(n) + UniqueI16Table.Insert(UniqueI16{N: n, Data: data}) +} + +//stdb:reducer +func deleteUniqueI16(_ server.ReducerContext, n int16) { + UniqueI16Table.DeleteByN(n) +} + +//stdb:reducer +func insertUniqueI32(_ server.ReducerContext, n int32, data int32) { + UniqueI32Table.Insert(UniqueI32{N: n, Data: data}) +} + +//stdb:reducer +func updateUniqueI32(_ server.ReducerContext, n int32, data int32) { + UniqueI32Table.DeleteByN(n) + UniqueI32Table.Insert(UniqueI32{N: n, Data: data}) +} + +//stdb:reducer +func deleteUniqueI32(_ server.ReducerContext, n int32) { + UniqueI32Table.DeleteByN(n) +} + +//stdb:reducer +func insertUniqueI64(_ server.ReducerContext, n int64, data int32) { + UniqueI64Table.Insert(UniqueI64{N: n, Data: data}) +} + +//stdb:reducer +func updateUniqueI64(_ server.ReducerContext, n int64, data int32) { + UniqueI64Table.DeleteByN(n) + UniqueI64Table.Insert(UniqueI64{N: n, Data: data}) +} + +//stdb:reducer +func deleteUniqueI64(_ server.ReducerContext, n int64) { + UniqueI64Table.DeleteByN(n) +} + +//stdb:reducer +func insertUniqueI128(_ server.ReducerContext, n types.Int128, data int32) { + UniqueI128Table.Insert(UniqueI128{N: n, Data: data}) +} + +//stdb:reducer +func updateUniqueI128(_ server.ReducerContext, n types.Int128, data int32) { + UniqueI128Table.DeleteByN(n) + UniqueI128Table.Insert(UniqueI128{N: n, Data: data}) +} + +//stdb:reducer +func deleteUniqueI128(_ server.ReducerContext, n types.Int128) { + UniqueI128Table.DeleteByN(n) +} + +//stdb:reducer +func insertUniqueI256(_ server.ReducerContext, n types.Int256, data int32) { + UniqueI256Table.Insert(UniqueI256{N: n, Data: data}) +} + +//stdb:reducer +func updateUniqueI256(_ server.ReducerContext, n types.Int256, data int32) { + UniqueI256Table.DeleteByN(n) + UniqueI256Table.Insert(UniqueI256{N: n, Data: data}) +} + +//stdb:reducer +func deleteUniqueI256(_ server.ReducerContext, n types.Int256) { + UniqueI256Table.DeleteByN(n) +} + +//stdb:reducer +func insertUniqueBool(_ server.ReducerContext, b bool, data int32) { + UniqueBoolTable.Insert(UniqueBool{B: b, Data: data}) +} + +//stdb:reducer +func updateUniqueBool(_ server.ReducerContext, b bool, data int32) { + UniqueBoolTable.DeleteByB(b) + UniqueBoolTable.Insert(UniqueBool{B: b, Data: data}) +} + +//stdb:reducer +func deleteUniqueBool(_ server.ReducerContext, b bool) { + UniqueBoolTable.DeleteByB(b) +} + +//stdb:reducer +func insertUniqueString(_ server.ReducerContext, s string, data int32) { + UniqueStringTable.Insert(UniqueString{S: s, Data: data}) +} + +//stdb:reducer +func updateUniqueString(_ server.ReducerContext, s string, data int32) { + UniqueStringTable.DeleteByS(s) + UniqueStringTable.Insert(UniqueString{S: s, Data: data}) +} + +//stdb:reducer +func deleteUniqueString(_ server.ReducerContext, s string) { + UniqueStringTable.DeleteByS(s) +} + +//stdb:reducer +func insertUniqueIdentity(_ server.ReducerContext, i types.Identity, data int32) { + UniqueIdentityTable.Insert(UniqueIdentity{I: i, Data: data}) +} + +//stdb:reducer +func updateUniqueIdentity(_ server.ReducerContext, i types.Identity, data int32) { + UniqueIdentityTable.DeleteByI(i) + UniqueIdentityTable.Insert(UniqueIdentity{I: i, Data: data}) +} + +//stdb:reducer +func deleteUniqueIdentity(_ server.ReducerContext, i types.Identity) { + UniqueIdentityTable.DeleteByI(i) +} + +//stdb:reducer +func insertUniqueConnectionId(_ server.ReducerContext, a types.ConnectionId, data int32) { + UniqueConnectionIdTable.Insert(UniqueConnectionId{A: a, Data: data}) +} + +//stdb:reducer +func updateUniqueConnectionId(_ server.ReducerContext, a types.ConnectionId, data int32) { + UniqueConnectionIdTable.DeleteByA(a) + UniqueConnectionIdTable.Insert(UniqueConnectionId{A: a, Data: data}) +} + +//stdb:reducer +func deleteUniqueConnectionId(_ server.ReducerContext, a types.ConnectionId) { + UniqueConnectionIdTable.DeleteByA(a) +} + +//stdb:reducer +func insertUniqueUuid(_ server.ReducerContext, u types.Uuid, data int32) { + UniqueUuidTable.Insert(UniqueUuid{U: u, Data: data}) +} + +//stdb:reducer +func updateUniqueUuid(_ server.ReducerContext, u types.Uuid, data int32) { + UniqueUuidTable.DeleteByU(u) + UniqueUuidTable.Insert(UniqueUuid{U: u, Data: data}) +} + +//stdb:reducer +func deleteUniqueUuid(_ server.ReducerContext, u types.Uuid) { + UniqueUuidTable.DeleteByU(u) +} + +// --------------------------------------------------------------------------- +// Pk* CRUD reducers -- insert, update (UpdateBy), delete (DeleteBy) by PK. +// --------------------------------------------------------------------------- + +//stdb:reducer +func insertPkU8(_ server.ReducerContext, n uint8, data int32) { + PkU8Table.Insert(PkU8{N: n, Data: data}) +} + +//stdb:reducer +func updatePkU8(_ server.ReducerContext, n uint8, data int32) { + PkU8Table.UpdateByN(PkU8{N: n, Data: data}) +} + +//stdb:reducer +func deletePkU8(_ server.ReducerContext, n uint8) { + PkU8Table.DeleteByN(n) +} + +//stdb:reducer +func insertPkU16(_ server.ReducerContext, n uint16, data int32) { + PkU16Table.Insert(PkU16{N: n, Data: data}) +} + +//stdb:reducer +func updatePkU16(_ server.ReducerContext, n uint16, data int32) { + PkU16Table.UpdateByN(PkU16{N: n, Data: data}) +} + +//stdb:reducer +func deletePkU16(_ server.ReducerContext, n uint16) { + PkU16Table.DeleteByN(n) +} + +//stdb:reducer +func insertPkU32(_ server.ReducerContext, n uint32, data int32) { + PkU32Table.Insert(PkU32{N: n, Data: data}) +} + +//stdb:reducer +func updatePkU32(_ server.ReducerContext, n uint32, data int32) { + PkU32Table.UpdateByN(PkU32{N: n, Data: data}) +} + +//stdb:reducer +func deletePkU32(_ server.ReducerContext, n uint32) { + PkU32Table.DeleteByN(n) +} + +//stdb:reducer +func insertPkU64(_ server.ReducerContext, n uint64, data int32) { + PkU64Table.Insert(PkU64{N: n, Data: data}) +} + +//stdb:reducer +func updatePkU64(_ server.ReducerContext, n uint64, data int32) { + PkU64Table.UpdateByN(PkU64{N: n, Data: data}) +} + +//stdb:reducer +func deletePkU64(_ server.ReducerContext, n uint64) { + PkU64Table.DeleteByN(n) +} + +//stdb:reducer +func insertPkU128(_ server.ReducerContext, n types.Uint128, data int32) { + PkU128Table.Insert(PkU128{N: n, Data: data}) +} + +//stdb:reducer +func updatePkU128(_ server.ReducerContext, n types.Uint128, data int32) { + PkU128Table.UpdateByN(PkU128{N: n, Data: data}) +} + +//stdb:reducer +func deletePkU128(_ server.ReducerContext, n types.Uint128) { + PkU128Table.DeleteByN(n) +} + +//stdb:reducer +func insertPkU256(_ server.ReducerContext, n types.Uint256, data int32) { + PkU256Table.Insert(PkU256{N: n, Data: data}) +} + +//stdb:reducer +func updatePkU256(_ server.ReducerContext, n types.Uint256, data int32) { + PkU256Table.UpdateByN(PkU256{N: n, Data: data}) +} + +//stdb:reducer +func deletePkU256(_ server.ReducerContext, n types.Uint256) { + PkU256Table.DeleteByN(n) +} + +//stdb:reducer +func insertPkI8(_ server.ReducerContext, n int8, data int32) { + PkI8Table.Insert(PkI8{N: n, Data: data}) +} + +//stdb:reducer +func updatePkI8(_ server.ReducerContext, n int8, data int32) { + PkI8Table.UpdateByN(PkI8{N: n, Data: data}) +} + +//stdb:reducer +func deletePkI8(_ server.ReducerContext, n int8) { + PkI8Table.DeleteByN(n) +} + +//stdb:reducer +func insertPkI16(_ server.ReducerContext, n int16, data int32) { + PkI16Table.Insert(PkI16{N: n, Data: data}) +} + +//stdb:reducer +func updatePkI16(_ server.ReducerContext, n int16, data int32) { + PkI16Table.UpdateByN(PkI16{N: n, Data: data}) +} + +//stdb:reducer +func deletePkI16(_ server.ReducerContext, n int16) { + PkI16Table.DeleteByN(n) +} + +//stdb:reducer +func insertPkI32(_ server.ReducerContext, n int32, data int32) { + PkI32Table.Insert(PkI32{N: n, Data: data}) +} + +//stdb:reducer +func updatePkI32(_ server.ReducerContext, n int32, data int32) { + PkI32Table.UpdateByN(PkI32{N: n, Data: data}) +} + +//stdb:reducer +func deletePkI32(_ server.ReducerContext, n int32) { + PkI32Table.DeleteByN(n) +} + +//stdb:reducer +func insertPkI64(_ server.ReducerContext, n int64, data int32) { + PkI64Table.Insert(PkI64{N: n, Data: data}) +} + +//stdb:reducer +func updatePkI64(_ server.ReducerContext, n int64, data int32) { + PkI64Table.UpdateByN(PkI64{N: n, Data: data}) +} + +//stdb:reducer +func deletePkI64(_ server.ReducerContext, n int64) { + PkI64Table.DeleteByN(n) +} + +//stdb:reducer +func insertPkI128(_ server.ReducerContext, n types.Int128, data int32) { + PkI128Table.Insert(PkI128{N: n, Data: data}) +} + +//stdb:reducer +func updatePkI128(_ server.ReducerContext, n types.Int128, data int32) { + PkI128Table.UpdateByN(PkI128{N: n, Data: data}) +} + +//stdb:reducer +func deletePkI128(_ server.ReducerContext, n types.Int128) { + PkI128Table.DeleteByN(n) +} + +//stdb:reducer +func insertPkI256(_ server.ReducerContext, n types.Int256, data int32) { + PkI256Table.Insert(PkI256{N: n, Data: data}) +} + +//stdb:reducer +func updatePkI256(_ server.ReducerContext, n types.Int256, data int32) { + PkI256Table.UpdateByN(PkI256{N: n, Data: data}) +} + +//stdb:reducer +func deletePkI256(_ server.ReducerContext, n types.Int256) { + PkI256Table.DeleteByN(n) +} + +//stdb:reducer +func insertPkBool(_ server.ReducerContext, b bool, data int32) { + PkBoolTable.Insert(PkBool{B: b, Data: data}) +} + +//stdb:reducer +func updatePkBool(_ server.ReducerContext, b bool, data int32) { + PkBoolTable.UpdateByB(PkBool{B: b, Data: data}) +} + +//stdb:reducer +func deletePkBool(_ server.ReducerContext, b bool) { + PkBoolTable.DeleteByB(b) +} + +//stdb:reducer +func insertPkString(_ server.ReducerContext, s string, data int32) { + PkStringTable.Insert(PkString{S: s, Data: data}) +} + +//stdb:reducer +func updatePkString(_ server.ReducerContext, s string, data int32) { + PkStringTable.UpdateByS(PkString{S: s, Data: data}) +} + +//stdb:reducer +func deletePkString(_ server.ReducerContext, s string) { + PkStringTable.DeleteByS(s) +} + +//stdb:reducer +func insertPkIdentity(_ server.ReducerContext, i types.Identity, data int32) { + PkIdentityTable.Insert(PkIdentity{I: i, Data: data}) +} + +//stdb:reducer +func updatePkIdentity(_ server.ReducerContext, i types.Identity, data int32) { + PkIdentityTable.UpdateByI(PkIdentity{I: i, Data: data}) +} + +//stdb:reducer +func deletePkIdentity(_ server.ReducerContext, i types.Identity) { + PkIdentityTable.DeleteByI(i) +} + +//stdb:reducer +func insertPkConnectionId(_ server.ReducerContext, a types.ConnectionId, data int32) { + PkConnectionIdTable.Insert(PkConnectionId{A: a, Data: data}) +} + +//stdb:reducer +func updatePkConnectionId(_ server.ReducerContext, a types.ConnectionId, data int32) { + PkConnectionIdTable.UpdateByA(PkConnectionId{A: a, Data: data}) +} + +//stdb:reducer +func deletePkConnectionId(_ server.ReducerContext, a types.ConnectionId) { + PkConnectionIdTable.DeleteByA(a) +} + +//stdb:reducer +func insertPkUuid(_ server.ReducerContext, u types.Uuid, data int32) { + PkUuidTable.Insert(PkUuid{U: u, Data: data}) +} + +//stdb:reducer +func updatePkUuid(_ server.ReducerContext, u types.Uuid, data int32) { + PkUuidTable.UpdateByU(PkUuid{U: u, Data: data}) +} + +//stdb:reducer +func deletePkUuid(_ server.ReducerContext, u types.Uuid) { + PkUuidTable.DeleteByU(u) +} + +//stdb:reducer +func insertPkSimpleEnum(_ server.ReducerContext, a SimpleEnum, data int32) { + PkSimpleEnumTable.Insert(PkSimpleEnum{A: a, Data: data}) +} + +//stdb:reducer +func insertPkU32Two(_ server.ReducerContext, n uint32, data int32) { + PkU32TwoTable.Insert(PkU32Two{N: n, Data: data}) +} + +//stdb:reducer +func updatePkU32Two(_ server.ReducerContext, n uint32, data int32) { + PkU32TwoTable.UpdateByN(PkU32Two{N: n, Data: data}) +} + +//stdb:reducer +func deletePkU32Two(_ server.ReducerContext, n uint32) { + PkU32TwoTable.DeleteByN(n) +} + +// --------------------------------------------------------------------------- +// Special reducers +// --------------------------------------------------------------------------- + +//stdb:reducer +func updatePkSimpleEnum(_ server.ReducerContext, a SimpleEnum, data int32) error { + _, found, err := PkSimpleEnumTable.FindByA(a) + if err != nil { + return err + } + if !found { + return fmt.Errorf("row not found") + } + PkSimpleEnumTable.UpdateByA(PkSimpleEnum{A: a, Data: data}) + return nil +} + +//stdb:reducer +func insertLargeTable(_ server.ReducerContext, a uint8, b uint16, c uint32, d uint64, e types.Uint128, f types.Uint256, g int8, h int16, i int32, j int64, k types.Int128, l types.Int256, m bool, n float32, o float64, p string, q SimpleEnum, r EnumWithPayload, s UnitStruct, t ByteStruct, u EveryPrimitiveStruct, v EveryVecStruct) { + LargeTableTable.Insert(LargeTable{A: a, B: b, C: c, D: d, E: e, F: f, G: g, H: h, I: i, J: j, K: k, L: l, M: m, N: n, O: o, P: p, Q: q, R: r, S: s, T: t, U: u, V: v}) +} + +//stdb:reducer +func deleteLargeTable(_ server.ReducerContext, a uint8, b uint16, c uint32, d uint64, e types.Uint128, f types.Uint256, g int8, h int16, i int32, j int64, k types.Int128, l types.Int256, m bool, n float32, o float64, p string, q SimpleEnum, r EnumWithPayload, s UnitStruct, t ByteStruct, u EveryPrimitiveStruct, v EveryVecStruct) { + LargeTableTable.Delete(LargeTable{A: a, B: b, C: c, D: d, E: e, F: f, G: g, H: h, I: i, J: j, K: k, L: l, M: m, N: n, O: o, P: p, Q: q, R: r, S: s, T: t, U: u, V: v}) +} + +//stdb:reducer +func insertTableHoldsTable(_ server.ReducerContext, a OneU8, b VecU8) { + TableHoldsTableTable.Insert(TableHoldsTable{A: a, B: b}) +} + +//stdb:reducer +func insertIntoBtreeU32(_ server.ReducerContext, rows []BTreeU32) { + for _, row := range rows { + BtreeU32Table.Insert(row) + } +} + +//stdb:reducer +func deleteFromBtreeU32(_ server.ReducerContext, rows []BTreeU32) { + for _, row := range rows { + BtreeU32Table.Delete(row) + } +} + +//stdb:reducer +func insertIntoPkBtreeU32(_ server.ReducerContext, pkU32 []PkU32, btU32 []BTreeU32) { + for _, row := range pkU32 { + PkU32Table.Insert(row) + } + for _, row := range btU32 { + BtreeU32Table.Insert(row) + } +} + +//stdb:reducer +func insertUniqueU32UpdatePkU32(_ server.ReducerContext, n uint32, dUnique int32, dPk int32) { + UniqueU32Table.Insert(UniqueU32{N: n, Data: dUnique}) + PkU32Table.UpdateByN(PkU32{N: n, Data: dPk}) +} + +//stdb:reducer +func deletePkU32InsertPkU32Two(_ server.ReducerContext, n uint32, data int32) { + PkU32TwoTable.Insert(PkU32Two{N: n, Data: data}) + PkU32Table.Delete(PkU32{N: n, Data: data}) +} + +// --------------------------------------------------------------------------- +// Caller identity/connection reducers -- use ctx.Sender() and ctx.ConnectionId(). +// --------------------------------------------------------------------------- + +//stdb:reducer +func insertCallerOneIdentity(ctx server.ReducerContext) { + OneIdentityTable.Insert(OneIdentity{I: ctx.Sender()}) +} + +//stdb:reducer +func insertCallerVecIdentity(ctx server.ReducerContext) { + VecIdentityTable.Insert(VecIdentity{I: []types.Identity{ctx.Sender()}}) +} + +//stdb:reducer +func insertCallerUniqueIdentity(ctx server.ReducerContext, data int32) { + UniqueIdentityTable.Insert(UniqueIdentity{I: ctx.Sender(), Data: data}) +} + +//stdb:reducer +func insertCallerPkIdentity(ctx server.ReducerContext, data int32) { + PkIdentityTable.Insert(PkIdentity{I: ctx.Sender(), Data: data}) +} + +//stdb:reducer +func insertCallerOneConnectionId(ctx server.ReducerContext) { + OneConnectionIdTable.Insert(OneConnectionId{A: ctx.ConnectionId()}) +} + +//stdb:reducer +func insertCallerVecConnectionId(ctx server.ReducerContext) { + VecConnectionIdTable.Insert(VecConnectionId{A: []types.ConnectionId{ctx.ConnectionId()}}) +} + +//stdb:reducer +func insertCallerUniqueConnectionId(ctx server.ReducerContext, data int32) { + UniqueConnectionIdTable.Insert(UniqueConnectionId{A: ctx.ConnectionId(), Data: data}) +} + +//stdb:reducer +func insertCallerPkConnectionId(ctx server.ReducerContext, data int32) { + PkConnectionIdTable.Insert(PkConnectionId{A: ctx.ConnectionId(), Data: data}) +} + +// --------------------------------------------------------------------------- +// Timestamp and UUID reducers +// --------------------------------------------------------------------------- + +//stdb:reducer +func insertCallTimestamp(ctx server.ReducerContext) { + OneTimestampTable.Insert(OneTimestamp{T: ctx.Timestamp()}) +} + +//stdb:reducer +func insertCallUuidV4(ctx server.ReducerContext) { + ts := ctx.Timestamp() + var b [16]byte + usec := ts.Microseconds() + b[0] = byte(usec) + b[1] = byte(usec >> 8) + b[2] = byte(usec >> 16) + b[3] = byte(usec >> 24) + b[4] = byte(usec >> 32) + b[5] = byte(usec >> 40) + b[6] = byte(usec >> 48) + b[7] = byte(usec >> 56) + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + OneUuidTable.Insert(OneUuid{U: types.NewUuid(b)}) +} + +//stdb:reducer +func insertCallUuidV7(ctx server.ReducerContext) { + ts := ctx.Timestamp() + var b [16]byte + usec := ts.Microseconds() + msec := usec / 1000 + b[0] = byte(msec >> 40) + b[1] = byte(msec >> 32) + b[2] = byte(msec >> 24) + b[3] = byte(msec >> 16) + b[4] = byte(msec >> 8) + b[5] = byte(msec) + b[6] = (b[6] & 0x0f) | 0x70 + b[8] = (b[8] & 0x3f) | 0x80 + OneUuidTable.Insert(OneUuid{U: types.NewUuid(b)}) +} + +// --------------------------------------------------------------------------- +// insertPrimitivesAsStrings converts each field of EveryPrimitiveStruct to a +// string and inserts the result into VecString. +// --------------------------------------------------------------------------- + +//stdb:reducer +func insertPrimitivesAsStrings(_ server.ReducerContext, s EveryPrimitiveStruct) { + VecStringTable.Insert(VecString{S: []string{ + fmt.Sprintf("%d", s.A), + fmt.Sprintf("%d", s.B), + fmt.Sprintf("%d", s.C), + fmt.Sprintf("%d", s.D), + s.E.String(), + s.F.String(), + fmt.Sprintf("%d", s.G), + fmt.Sprintf("%d", s.H), + fmt.Sprintf("%d", s.I), + fmt.Sprintf("%d", s.J), + s.K.String(), + s.L.String(), + fmt.Sprintf("%t", s.M), + fmt.Sprintf("%g", s.N), + fmt.Sprintf("%g", s.O), + s.P, + s.Q.String(), + s.R.String(), + s.S.String(), + s.T.String(), + s.U.String(), + }}) +} + +// --------------------------------------------------------------------------- +// Misc reducers +// --------------------------------------------------------------------------- + +//stdb:reducer +func noOpSucceeds(_ server.ReducerContext) {} + +//stdb:reducer +func sendScheduledMessage(_ server.ReducerContext, arg ScheduledTable) { + _ = arg.Text + _ = arg.ScheduledAt + _ = arg.ScheduledId +} + +//stdb:reducer +func insertUser(_ server.ReducerContext, name string, identity types.Identity) { + UsersTable.Insert(Users{Identity: identity, Name: name}) +} + +//stdb:reducer +func insertIntoIndexedSimpleEnum(_ server.ReducerContext, n SimpleEnum) { + IndexedSimpleEnumTable.Insert(IndexedSimpleEnum{N: n}) +} + +//stdb:reducer +func updateIndexedSimpleEnum(_ server.ReducerContext, a SimpleEnum, b SimpleEnum) error { + iter, err := IndexedSimpleEnumTable.Scan() + if err != nil { + return err + } + defer iter.Close() + found := false + for { + row, ok := iter.Next() + if !ok { + break + } + if row.N == a { + IndexedSimpleEnumTable.Delete(row) + found = true + break + } + } + if found { + IndexedSimpleEnumTable.Insert(IndexedSimpleEnum{N: b}) + } + return nil +} + +//stdb:reducer +func sortedUuidsInsert(ctx server.ReducerContext) error { + ts := ctx.Timestamp() + usec := ts.Microseconds() + msec := usec / 1000 + + for i := 0; i < 1000; i++ { + var b [16]byte + b[0] = byte(msec >> 40) + b[1] = byte(msec >> 32) + b[2] = byte(msec >> 24) + b[3] = byte(msec >> 16) + b[4] = byte(msec >> 8) + b[5] = byte(msec) + counter := uint16(i) + b[6] = byte(counter>>8) | 0x70 + b[7] = byte(counter) + b[8] = 0x80 + b[9] = byte(i) + + uuid := types.NewUuid(b) + PkUuidTable.Insert(PkUuid{U: uuid, Data: 0}) + } + + iter, err := PkUuidTable.Scan() + if err != nil { + return err + } + defer iter.Close() + + var lastUuid types.Uuid + first := true + for { + row, ok := iter.Next() + if !ok { + break + } + if !first { + lastBytes := lastUuid.Bytes() + curBytes := row.U.Bytes() + for idx := 0; idx < 16; idx++ { + if lastBytes[idx] < curBytes[idx] { + break + } + if lastBytes[idx] > curBytes[idx] { + return fmt.Errorf("UUIDs are not sorted correctly") + } + } + } + lastUuid = row.U + first = false + } + + return nil +} diff --git a/modules/sdk-test-go/sdk-test-go b/modules/sdk-test-go/sdk-test-go new file mode 100755 index 00000000000..c313c48ab36 Binary files /dev/null and b/modules/sdk-test-go/sdk-test-go differ diff --git a/modules/sdk-test-go/tables.go b/modules/sdk-test-go/tables.go new file mode 100644 index 00000000000..40fb806377f --- /dev/null +++ b/modules/sdk-test-go/tables.go @@ -0,0 +1,658 @@ +package main + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/types" + +// --------------------------------------------------------------------------- +// One* tables -- each holds a single value of a given type. +// --------------------------------------------------------------------------- + +//stdb:table name=one_u_8 access=public +//stdb:rls SELECT * FROM one_u_8 +type OneU8 struct { + N uint8 +} + +//stdb:table name=one_u_16 access=public +type OneU16 struct { + N uint16 +} + +//stdb:table name=one_u_32 access=public +type OneU32 struct { + N uint32 +} + +//stdb:table name=one_u_64 access=public +type OneU64 struct { + N uint64 +} + +//stdb:table name=one_u_128 access=public +type OneU128 struct { + N types.Uint128 +} + +//stdb:table name=one_u_256 access=public +type OneU256 struct { + N types.Uint256 +} + +//stdb:table name=one_i_8 access=public +type OneI8 struct { + N int8 +} + +//stdb:table name=one_i_16 access=public +type OneI16 struct { + N int16 +} + +//stdb:table name=one_i_32 access=public +type OneI32 struct { + N int32 +} + +//stdb:table name=one_i_64 access=public +type OneI64 struct { + N int64 +} + +//stdb:table name=one_i_128 access=public +type OneI128 struct { + N types.Int128 +} + +//stdb:table name=one_i_256 access=public +type OneI256 struct { + N types.Int256 +} + +//stdb:table name=one_bool access=public +type OneBool struct { + B bool +} + +//stdb:table name=one_f_32 access=public +type OneF32 struct { + F float32 +} + +//stdb:table name=one_f_64 access=public +type OneF64 struct { + F float64 +} + +//stdb:table name=one_string access=public +type OneString struct { + S string +} + +//stdb:table name=one_identity access=public +type OneIdentity struct { + I types.Identity +} + +//stdb:table name=one_connection_id access=public +type OneConnectionId struct { + A types.ConnectionId +} + +//stdb:table name=one_uuid access=public +type OneUuid struct { + U types.Uuid +} + +//stdb:table name=one_timestamp access=public +type OneTimestamp struct { + T types.Timestamp +} + +//stdb:table name=one_simple_enum access=public +type OneSimpleEnum struct { + E SimpleEnum +} + +//stdb:table name=one_enum_with_payload access=public +type OneEnumWithPayload struct { + E EnumWithPayload +} + +//stdb:table name=one_unit_struct access=public +type OneUnitStruct struct { + S UnitStruct +} + +//stdb:table name=one_byte_struct access=public +type OneByteStruct struct { + S ByteStruct +} + +//stdb:table name=one_every_primitive_struct access=public +type OneEveryPrimitiveStruct struct { + S EveryPrimitiveStruct +} + +//stdb:table name=one_every_vec_struct access=public +type OneEveryVecStruct struct { + S EveryVecStruct +} + +// --------------------------------------------------------------------------- +// Vec* tables -- each holds a slice of a given type. +// --------------------------------------------------------------------------- + +//stdb:table name=vec_u_8 access=public +type VecU8 struct { + N []uint8 +} + +//stdb:table name=vec_u_16 access=public +type VecU16 struct { + N []uint16 +} + +//stdb:table name=vec_u_32 access=public +type VecU32 struct { + N []uint32 +} + +//stdb:table name=vec_u_64 access=public +type VecU64 struct { + N []uint64 +} + +//stdb:table name=vec_u_128 access=public +type VecU128 struct { + N []types.Uint128 +} + +//stdb:table name=vec_u_256 access=public +type VecU256 struct { + N []types.Uint256 +} + +//stdb:table name=vec_i_8 access=public +type VecI8 struct { + N []int8 +} + +//stdb:table name=vec_i_16 access=public +type VecI16 struct { + N []int16 +} + +//stdb:table name=vec_i_32 access=public +type VecI32 struct { + N []int32 +} + +//stdb:table name=vec_i_64 access=public +type VecI64 struct { + N []int64 +} + +//stdb:table name=vec_i_128 access=public +type VecI128 struct { + N []types.Int128 +} + +//stdb:table name=vec_i_256 access=public +type VecI256 struct { + N []types.Int256 +} + +//stdb:table name=vec_bool access=public +type VecBool struct { + B []bool +} + +//stdb:table name=vec_f_32 access=public +type VecF32 struct { + F []float32 +} + +//stdb:table name=vec_f_64 access=public +type VecF64 struct { + F []float64 +} + +//stdb:table name=vec_string access=public +type VecString struct { + S []string +} + +//stdb:table name=vec_identity access=public +type VecIdentity struct { + I []types.Identity +} + +//stdb:table name=vec_connection_id access=public +type VecConnectionId struct { + A []types.ConnectionId +} + +//stdb:table name=vec_uuid access=public +type VecUuid struct { + U []types.Uuid +} + +//stdb:table name=vec_timestamp access=public +type VecTimestamp struct { + T []types.Timestamp +} + +//stdb:table name=vec_simple_enum access=public +type VecSimpleEnum struct { + E []SimpleEnum +} + +//stdb:table name=vec_enum_with_payload access=public +type VecEnumWithPayload struct { + E []EnumWithPayload +} + +//stdb:table name=vec_unit_struct access=public +type VecUnitStruct struct { + S []UnitStruct +} + +//stdb:table name=vec_byte_struct access=public +type VecByteStruct struct { + S []ByteStruct +} + +//stdb:table name=vec_every_primitive_struct access=public +type VecEveryPrimitiveStruct struct { + S []EveryPrimitiveStruct +} + +//stdb:table name=vec_every_vec_struct access=public +type VecEveryVecStruct struct { + S []EveryVecStruct +} + +// --------------------------------------------------------------------------- +// Option* tables -- each holds an optional value (pointer = Option). +// --------------------------------------------------------------------------- + +//stdb:table name=option_i_32 access=public +type OptionI32 struct { + N *int32 +} + +//stdb:table name=option_string access=public +type OptionString struct { + S *string +} + +//stdb:table name=option_identity access=public +type OptionIdentity struct { + I *types.Identity +} + +//stdb:table name=option_uuid access=public +type OptionUuid struct { + U *types.Uuid +} + +//stdb:table name=option_simple_enum access=public +type OptionSimpleEnum struct { + E *SimpleEnum +} + +//stdb:table name=option_every_primitive_struct access=public +type OptionEveryPrimitiveStruct struct { + S *EveryPrimitiveStruct +} + +//stdb:table name=option_vec_option_i_32 access=public +type OptionVecOptionI32 struct { + V *[]*int32 +} + +// --------------------------------------------------------------------------- +// Result* tables -- Result is a sum type with Ok/Err variants. +// --------------------------------------------------------------------------- + +//stdb:table name=result_i_32_string access=public +type ResultI32String struct { + R ResultI32StringValue +} + +//stdb:table name=result_string_i_32 access=public +type ResultStringI32 struct { + R ResultStringI32Value +} + +//stdb:table name=result_identity_string access=public +type ResultIdentityString struct { + R ResultIdentityStringValue +} + +//stdb:table name=result_simple_enum_i_32 access=public +type ResultSimpleEnumI32 struct { + R ResultSimpleEnumI32Value +} + +//stdb:table name=result_every_primitive_struct_string access=public +type ResultEveryPrimitiveStructString struct { + R ResultEveryPrimitiveStructStringValue +} + +//stdb:table name=result_vec_i_32_string access=public +type ResultVecI32String struct { + R ResultVecI32StringValue +} + +// --------------------------------------------------------------------------- +// Unique* tables -- each has a unique (non-pk) key field and an i32 Data payload. +// --------------------------------------------------------------------------- + +//stdb:table name=unique_u_8 access=public +type UniqueU8 struct { + N uint8 `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_u_16 access=public +type UniqueU16 struct { + N uint16 `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_u_32 access=public +type UniqueU32 struct { + N uint32 `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_u_64 access=public +type UniqueU64 struct { + N uint64 `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_u_128 access=public +type UniqueU128 struct { + N types.Uint128 `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_u_256 access=public +type UniqueU256 struct { + N types.Uint256 `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_i_8 access=public +type UniqueI8 struct { + N int8 `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_i_16 access=public +type UniqueI16 struct { + N int16 `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_i_32 access=public +type UniqueI32 struct { + N int32 `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_i_64 access=public +type UniqueI64 struct { + N int64 `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_i_128 access=public +type UniqueI128 struct { + N types.Int128 `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_i_256 access=public +type UniqueI256 struct { + N types.Int256 `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_bool access=public +type UniqueBool struct { + B bool `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_string access=public +type UniqueString struct { + S string `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_identity access=public +type UniqueIdentity struct { + I types.Identity `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_connection_id access=public +type UniqueConnectionId struct { + A types.ConnectionId `stdb:"unique"` + Data int32 +} + +//stdb:table name=unique_uuid access=public +type UniqueUuid struct { + U types.Uuid `stdb:"unique"` + Data int32 +} + +// --------------------------------------------------------------------------- +// Pk* tables -- each has a primary key field and an i32 Data payload. +// --------------------------------------------------------------------------- + +//stdb:table name=pk_u_8 access=public +type PkU8 struct { + N uint8 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_u_16 access=public +type PkU16 struct { + N uint16 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_u_32 access=public +type PkU32 struct { + N uint32 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_u_32_two access=public +type PkU32Two struct { + N uint32 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_u_64 access=public +type PkU64 struct { + N uint64 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_u_128 access=public +type PkU128 struct { + N types.Uint128 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_u_256 access=public +type PkU256 struct { + N types.Uint256 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_i_8 access=public +type PkI8 struct { + N int8 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_i_16 access=public +type PkI16 struct { + N int16 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_i_32 access=public +type PkI32 struct { + N int32 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_i_64 access=public +type PkI64 struct { + N int64 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_i_128 access=public +type PkI128 struct { + N types.Int128 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_i_256 access=public +type PkI256 struct { + N types.Int256 `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_bool access=public +type PkBool struct { + B bool `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_string access=public +type PkString struct { + S string `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_identity access=public +type PkIdentity struct { + I types.Identity `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_connection_id access=public +type PkConnectionId struct { + A types.ConnectionId `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_uuid access=public +type PkUuid struct { + U types.Uuid `stdb:"primarykey"` + Data int32 +} + +//stdb:table name=pk_simple_enum access=public +type PkSimpleEnum struct { + A SimpleEnum `stdb:"primarykey"` + Data int32 +} + +// --------------------------------------------------------------------------- +// Special tables +// --------------------------------------------------------------------------- + +// BTreeU32 has a btree index on the N field. +// +//stdb:table name=btree_u32 access=public +type BTreeU32 struct { + N uint32 `stdb:"index=btree"` + Data int32 +} + +// Users table with primary key on Identity. +// +//stdb:table name=users access=public +//stdb:rls SELECT * FROM users WHERE identity = :sender +type Users struct { + Identity types.Identity `stdb:"primarykey"` + Name string +} + +// IndexedTable has a single-column btree index on PlayerId (private table). +// +//stdb:table name=indexed_table access=private +type IndexedTable struct { + PlayerId uint32 `stdb:"index=btree"` +} + +// IndexedTable2 has two columns (private table). +// +//stdb:table name=indexed_table_2 access=private +type IndexedTable2 struct { + PlayerId uint32 + PlayerSnazz float32 +} + +// IndexedSimpleEnum has a btree index on N. +// +//stdb:table name=indexed_simple_enum access=public +type IndexedSimpleEnum struct { + N SimpleEnum `stdb:"index=btree"` +} + +// LargeTable has many fields of many different types. +// +//stdb:table name=large_table access=public +type LargeTable struct { + A uint8 + B uint16 + C uint32 + D uint64 + E types.Uint128 + F types.Uint256 + G int8 + H int16 + I int32 + J int64 + K types.Int128 + L types.Int256 + M bool + N float32 + O float64 + P string + Q SimpleEnum + R EnumWithPayload + S UnitStruct + T ByteStruct + U EveryPrimitiveStruct + V EveryVecStruct +} + +// TableHoldsTable holds instances of other table structs. +// +//stdb:table name=table_holds_table access=public +type TableHoldsTable struct { + A OneU8 + B VecU8 +} + +// ScheduledTable is a scheduled table with auto-incrementing primary key. +// +//stdb:table name=scheduled_table access=public +//stdb:schedule table=scheduled_table function=send_scheduled_message +type ScheduledTable struct { + ScheduledId uint64 `stdb:"primarykey,autoinc"` + ScheduledAt types.ScheduleAt + Text string +} diff --git a/modules/sdk-test-go/types.go b/modules/sdk-test-go/types.go new file mode 100644 index 00000000000..9b2d52d6244 --- /dev/null +++ b/modules/sdk-test-go/types.go @@ -0,0 +1,279 @@ +package main + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// SimpleEnum mirrors the Rust SimpleEnum enum. +// In BSATN it is a sum type with unit variants, encoded as u8 tag. +// +//stdb:enum variants=Zero,One,Two +type SimpleEnum uint8 + +const ( + SimpleEnumZero SimpleEnum = 0 + SimpleEnumOne SimpleEnum = 1 + SimpleEnumTwo SimpleEnum = 2 +) + +// UnitStruct mirrors the Rust UnitStruct (empty struct). +type UnitStruct struct{} + +// ByteStruct mirrors the Rust ByteStruct. +type ByteStruct struct { + B uint8 +} + +// EveryPrimitiveStruct mirrors the Rust EveryPrimitiveStruct. +type EveryPrimitiveStruct struct { + A uint8 + B uint16 + C uint32 + D uint64 + E types.Uint128 + F types.Uint256 + G int8 + H int16 + I int32 + J int64 + K types.Int128 + L types.Int256 + M bool + N float32 + O float64 + P string + Q types.Identity + R types.ConnectionId + S types.Timestamp + T types.TimeDuration + U types.Uuid +} + +// EveryVecStruct mirrors the Rust EveryVecStruct. +type EveryVecStruct struct { + A []uint8 + B []uint16 + C []uint32 + D []uint64 + E []types.Uint128 + F []types.Uint256 + G []int8 + H []int16 + I []int32 + J []int64 + K []types.Int128 + L []types.Int256 + M []bool + N []float32 + O []float64 + P []string + Q []types.Identity + R []types.ConnectionId + S []types.Timestamp + T []types.TimeDuration + U []types.Uuid +} + +// EnumWithPayload is a sum type with many variant types. +// +//stdb:sumtype +type EnumWithPayload interface { + enumWithPayloadTag() uint8 +} + +//stdb:variant of=EnumWithPayload name=U8 +type EnumWithPayloadU8 struct{ Value uint8 } + +//stdb:variant of=EnumWithPayload name=U16 +type EnumWithPayloadU16 struct{ Value uint16 } + +//stdb:variant of=EnumWithPayload name=U32 +type EnumWithPayloadU32 struct{ Value uint32 } + +//stdb:variant of=EnumWithPayload name=U64 +type EnumWithPayloadU64 struct{ Value uint64 } + +//stdb:variant of=EnumWithPayload name=U128 +type EnumWithPayloadU128 struct{ Value types.Uint128 } + +//stdb:variant of=EnumWithPayload name=U256 +type EnumWithPayloadU256 struct{ Value types.Uint256 } + +//stdb:variant of=EnumWithPayload name=I8 +type EnumWithPayloadI8 struct{ Value int8 } + +//stdb:variant of=EnumWithPayload name=I16 +type EnumWithPayloadI16 struct{ Value int16 } + +//stdb:variant of=EnumWithPayload name=I32 +type EnumWithPayloadI32 struct{ Value int32 } + +//stdb:variant of=EnumWithPayload name=I64 +type EnumWithPayloadI64 struct{ Value int64 } + +//stdb:variant of=EnumWithPayload name=I128 +type EnumWithPayloadI128 struct{ Value types.Int128 } + +//stdb:variant of=EnumWithPayload name=I256 +type EnumWithPayloadI256 struct{ Value types.Int256 } + +//stdb:variant of=EnumWithPayload name=Bool +type EnumWithPayloadBool struct{ Value bool } + +//stdb:variant of=EnumWithPayload name=F32 +type EnumWithPayloadF32 struct{ Value float32 } + +//stdb:variant of=EnumWithPayload name=F64 +type EnumWithPayloadF64 struct{ Value float64 } + +//stdb:variant of=EnumWithPayload name=Str +type EnumWithPayloadStr struct{ Value string } + +//stdb:variant of=EnumWithPayload name=Identity +type EnumWithPayloadIdentity struct{ Value types.Identity } + +//stdb:variant of=EnumWithPayload name=ConnectionId +type EnumWithPayloadConnectionId struct{ Value types.ConnectionId } + +//stdb:variant of=EnumWithPayload name=Timestamp +type EnumWithPayloadTimestamp struct{ Value types.Timestamp } + +//stdb:variant of=EnumWithPayload name=Uuid +type EnumWithPayloadUuid struct{ Value types.Uuid } + +//stdb:variant of=EnumWithPayload name=Bytes +type EnumWithPayloadBytes struct{ Value []uint8 } + +//stdb:variant of=EnumWithPayload name=Ints +type EnumWithPayloadInts struct{ Value []int32 } + +//stdb:variant of=EnumWithPayload name=Strings +type EnumWithPayloadStrings struct{ Value []string } + +//stdb:variant of=EnumWithPayload name=SimpleEnums +type EnumWithPayloadSimpleEnums struct{ Value []SimpleEnum } + +func (EnumWithPayloadU8) enumWithPayloadTag() uint8 { return 0 } +func (EnumWithPayloadU16) enumWithPayloadTag() uint8 { return 1 } +func (EnumWithPayloadU32) enumWithPayloadTag() uint8 { return 2 } +func (EnumWithPayloadU64) enumWithPayloadTag() uint8 { return 3 } +func (EnumWithPayloadU128) enumWithPayloadTag() uint8 { return 4 } +func (EnumWithPayloadU256) enumWithPayloadTag() uint8 { return 5 } +func (EnumWithPayloadI8) enumWithPayloadTag() uint8 { return 6 } +func (EnumWithPayloadI16) enumWithPayloadTag() uint8 { return 7 } +func (EnumWithPayloadI32) enumWithPayloadTag() uint8 { return 8 } +func (EnumWithPayloadI64) enumWithPayloadTag() uint8 { return 9 } +func (EnumWithPayloadI128) enumWithPayloadTag() uint8 { return 10 } +func (EnumWithPayloadI256) enumWithPayloadTag() uint8 { return 11 } +func (EnumWithPayloadBool) enumWithPayloadTag() uint8 { return 12 } +func (EnumWithPayloadF32) enumWithPayloadTag() uint8 { return 13 } +func (EnumWithPayloadF64) enumWithPayloadTag() uint8 { return 14 } +func (EnumWithPayloadStr) enumWithPayloadTag() uint8 { return 15 } +func (EnumWithPayloadIdentity) enumWithPayloadTag() uint8 { return 16 } +func (EnumWithPayloadConnectionId) enumWithPayloadTag() uint8 { return 17 } +func (EnumWithPayloadTimestamp) enumWithPayloadTag() uint8 { return 18 } +func (EnumWithPayloadUuid) enumWithPayloadTag() uint8 { return 19 } +func (EnumWithPayloadBytes) enumWithPayloadTag() uint8 { return 20 } +func (EnumWithPayloadInts) enumWithPayloadTag() uint8 { return 21 } +func (EnumWithPayloadStrings) enumWithPayloadTag() uint8 { return 22 } +func (EnumWithPayloadSimpleEnums) enumWithPayloadTag() uint8 { return 23 } + +// ResultI32StringValue is a Result sum type. +// +//stdb:sumtype +type ResultI32StringValue interface { + resultI32StringValueTag() uint8 +} + +//stdb:variant of=ResultI32StringValue name=ok +type ResultI32StringOk struct{ Value int32 } + +//stdb:variant of=ResultI32StringValue name=err +type ResultI32StringErr struct{ Value string } + +func (ResultI32StringOk) resultI32StringValueTag() uint8 { return 0 } +func (ResultI32StringErr) resultI32StringValueTag() uint8 { return 1 } + +// ResultStringI32Value is a Result sum type. +// +//stdb:sumtype +type ResultStringI32Value interface { + resultStringI32ValueTag() uint8 +} + +//stdb:variant of=ResultStringI32Value name=ok +type ResultStringI32Ok struct{ Value string } + +//stdb:variant of=ResultStringI32Value name=err +type ResultStringI32Err struct{ Value int32 } + +func (ResultStringI32Ok) resultStringI32ValueTag() uint8 { return 0 } +func (ResultStringI32Err) resultStringI32ValueTag() uint8 { return 1 } + +// ResultIdentityStringValue is a Result sum type. +// +//stdb:sumtype +type ResultIdentityStringValue interface { + resultIdentityStringValueTag() uint8 +} + +//stdb:variant of=ResultIdentityStringValue name=ok +type ResultIdentityStringOk struct{ Value types.Identity } + +//stdb:variant of=ResultIdentityStringValue name=err +type ResultIdentityStringErr struct{ Value string } + +func (ResultIdentityStringOk) resultIdentityStringValueTag() uint8 { return 0 } +func (ResultIdentityStringErr) resultIdentityStringValueTag() uint8 { return 1 } + +// ResultSimpleEnumI32Value is a Result sum type. +// +//stdb:sumtype +type ResultSimpleEnumI32Value interface { + resultSimpleEnumI32ValueTag() uint8 +} + +//stdb:variant of=ResultSimpleEnumI32Value name=ok +type ResultSimpleEnumI32Ok struct{ Value SimpleEnum } + +//stdb:variant of=ResultSimpleEnumI32Value name=err +type ResultSimpleEnumI32Err struct{ Value int32 } + +func (ResultSimpleEnumI32Ok) resultSimpleEnumI32ValueTag() uint8 { return 0 } +func (ResultSimpleEnumI32Err) resultSimpleEnumI32ValueTag() uint8 { return 1 } + +// ResultEveryPrimitiveStructStringValue is a Result sum type. +// +//stdb:sumtype +type ResultEveryPrimitiveStructStringValue interface { + resultEveryPrimitiveStructStringValueTag() uint8 +} + +//stdb:variant of=ResultEveryPrimitiveStructStringValue name=ok +type ResultEveryPrimitiveStructStringOk struct{ Value EveryPrimitiveStruct } + +//stdb:variant of=ResultEveryPrimitiveStructStringValue name=err +type ResultEveryPrimitiveStructStringErr struct{ Value string } + +func (ResultEveryPrimitiveStructStringOk) resultEveryPrimitiveStructStringValueTag() uint8 { + return 0 +} +func (ResultEveryPrimitiveStructStringErr) resultEveryPrimitiveStructStringValueTag() uint8 { + return 1 +} + +// ResultVecI32StringValue is a Result, String> sum type. +// +//stdb:sumtype +type ResultVecI32StringValue interface { + resultVecI32StringValueTag() uint8 +} + +//stdb:variant of=ResultVecI32StringValue name=ok +type ResultVecI32StringOk struct{ Value []int32 } + +//stdb:variant of=ResultVecI32StringValue name=err +type ResultVecI32StringErr struct{ Value string } + +func (ResultVecI32StringOk) resultVecI32StringValueTag() uint8 { return 0 } +func (ResultVecI32StringErr) resultVecI32StringValueTag() uint8 { return 1 } diff --git a/modules/sdk-test-procedure-go/go.mod b/modules/sdk-test-procedure-go/go.mod new file mode 100644 index 00000000000..24e1ab8f00c --- /dev/null +++ b/modules/sdk-test-procedure-go/go.mod @@ -0,0 +1,7 @@ +module github.com/clockworklabs/SpacetimeDB/modules/sdk-test-procedure-go + +go 1.23 + +require github.com/clockworklabs/SpacetimeDB/sdks/go v0.0.0 + +replace github.com/clockworklabs/SpacetimeDB/sdks/go => ../../sdks/go diff --git a/modules/sdk-test-procedure-go/main.go b/modules/sdk-test-procedure-go/main.go new file mode 100644 index 00000000000..e4bb606a3cd --- /dev/null +++ b/modules/sdk-test-procedure-go/main.go @@ -0,0 +1,5 @@ +package main + +//go:generate stdb-gen + +func main() {} diff --git a/modules/sdk-test-procedure-go/procedures.go b/modules/sdk-test-procedure-go/procedures.go new file mode 100644 index 00000000000..07cd08ef3e3 --- /dev/null +++ b/modules/sdk-test-procedure-go/procedures.go @@ -0,0 +1,156 @@ +package main + +import ( + "bytes" + "fmt" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/server" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +//stdb:procedure +func returnPrimitive(_ server.ProcedureContext, lhs uint32, rhs uint32) uint32 { + return lhs + rhs +} + +//stdb:procedure +func returnStruct(_ server.ProcedureContext, a uint32, b string) ReturnStruct { + return ReturnStruct{A: a, B: b} +} + +//stdb:procedure +func returnEnumA(_ server.ProcedureContext, a uint32) ReturnEnum { + return ReturnEnumA{Value: a} +} + +//stdb:procedure +func returnEnumB(_ server.ProcedureContext, b string) ReturnEnum { + return ReturnEnumB{Value: b} +} + +//stdb:procedure +func willPanic(_ server.ProcedureContext) { + panic("This procedure is expected to panic") +} + +func insertMyTable() { + MyTableTable.Insert(MyTable{ + Field: ReturnStruct{A: 42, B: "magic"}, + }) +} + +func assertRowCount(ctx server.ProcedureContext, count uint64) { + ctx.WithTx(func() { + n, err := MyTableTable.Count() + if err != nil { + panic(fmt.Sprintf("Count error: %v", err)) + } + if n != count { + panic(fmt.Sprintf("expected %d rows, got %d", count, n)) + } + }) +} + +//stdb:procedure +func insertWithTxCommit(ctx server.ProcedureContext) { + // Insert a row and commit. + ctx.WithTx(func() { + insertMyTable() + }) + + // Assert that there's a row. + assertRowCount(ctx, 1) +} + +//stdb:procedure +func insertWithTxRollback(ctx server.ProcedureContext) { + _ = ctx.TryWithTx(func() error { + insertMyTable() + return fmt.Errorf("rollback") + }) + + // Assert that there's not a row. + assertRowCount(ctx, 0) +} + +//stdb:procedure +func scheduledProc(ctx server.ProcedureContext, data ScheduledProcTable) { + procedureTs := ctx.Timestamp() + ctx.WithTx(func() { + ProcInsertsIntoTable.Insert(ProcInsertsInto{ + ReducerTs: data.ReducerTs, + ProcedureTs: procedureTs, + X: data.X, + Y: data.Y, + }) + }) +} + +//stdb:procedure +func readMySchema(ctx server.ProcedureContext) string { + moduleIdentity := ctx.Identity() + uri := fmt.Sprintf("http://localhost:3000/v1/database/%s/schema?version=9", moduleIdentity) + code, body, err := ctx.HttpGet(uri) + if err != nil { + // Encode debug info as a single field name so serde_json error shows it + return fmt.Sprintf(`{"ERR_%v___URI_%s": 0}`, err, uri) + } + if len(body) == 0 { + return fmt.Sprintf(`{"EMPTY_code_%d": 0}`, code) + } + if body[0] != '{' && body[0] != '[' { + return fmt.Sprintf(`{"NOTJSON_code_%d_len_%d_first_%x": 0}`, code, len(body), body[:min(20, len(body))]) + } + return string(body) +} + +//stdb:procedure +func invalidRequest(ctx server.ProcedureContext) string { + _, body, err := ctx.HttpGet("http://foo.invalid/") + if err != nil { + return err.Error() + } + panic(fmt.Sprintf("Got result from requesting http://foo.invalid... huh?\n%s", string(body))) +} + +// uuidToU128BE converts a Uuid's LE-stored bytes to big-endian u128 for comparison. +func uuidToU128BE(u types.Uuid) [16]byte { + le := u.Bytes() + var be [16]byte + for i := 0; i < 16; i++ { + be[i] = le[15-i] + } + return be +} + +//stdb:procedure +func sortedUuidsInsert(ctx server.ProcedureContext) { + ctx.WithTx(func() { + for i := 0; i < 1000; i++ { + uuid, err := ctx.NewUuidV7() + if err != nil { + panic(fmt.Sprintf("new uuid: %v", err)) + } + PkUuidTable.Insert(PkUuid{U: uuid, Data: 0}) + } + + // Verify UUIDs are sorted. + iter, err := PkUuidTable.Scan() + if err != nil { + panic(fmt.Sprintf("Scan error: %v", err)) + } + defer iter.Close() + + var lastUuid types.Uuid + for row, ok := iter.Next(); ok; row, ok = iter.Next() { + if lastUuid != nil { + lastBE := uuidToU128BE(lastUuid) + currBE := uuidToU128BE(row.U) + if bytes.Compare(lastBE[:], currBE[:]) >= 0 { + panic("UUIDs are not sorted correctly") + } + } + lastUuid = row.U + } + }) +} diff --git a/modules/sdk-test-procedure-go/reducers.go b/modules/sdk-test-procedure-go/reducers.go new file mode 100644 index 00000000000..b1962e8353b --- /dev/null +++ b/modules/sdk-test-procedure-go/reducers.go @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/server" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +//stdb:reducer +func scheduleProc(ctx server.ReducerContext) { + // Schedule the procedure to run in 1s. + ScheduledProcTableTable.Insert(ScheduledProcTable{ + ScheduledId: 0, + ScheduledAt: types.ScheduleAtInterval{Value: types.NewTimeDuration(1000 * 1000)}, // 1000ms in micros + ReducerTs: ctx.Timestamp(), + X: 42, + Y: 24, + }) +} diff --git a/modules/sdk-test-procedure-go/tables.go b/modules/sdk-test-procedure-go/tables.go new file mode 100644 index 00000000000..ee307a9e443 --- /dev/null +++ b/modules/sdk-test-procedure-go/tables.go @@ -0,0 +1,32 @@ +package main + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/types" + +//stdb:table name=my_table access=public +type MyTable struct { + Field ReturnStruct +} + +//stdb:table name=scheduled_proc_table access=private +//stdb:schedule table=scheduled_proc_table function=scheduled_proc +type ScheduledProcTable struct { + ScheduledId uint64 `stdb:"primarykey,autoinc"` + ScheduledAt types.ScheduleAt + ReducerTs types.Timestamp + X uint8 + Y uint8 +} + +//stdb:table name=proc_inserts_into access=public +type ProcInsertsInto struct { + ReducerTs types.Timestamp + ProcedureTs types.Timestamp + X uint8 + Y uint8 +} + +//stdb:table name=pk_uuid access=public +type PkUuid struct { + U types.Uuid + Data uint8 +} diff --git a/modules/sdk-test-procedure-go/types.go b/modules/sdk-test-procedure-go/types.go new file mode 100644 index 00000000000..952b360a02f --- /dev/null +++ b/modules/sdk-test-procedure-go/types.go @@ -0,0 +1,27 @@ +package main + +// ReturnStruct is returned by procedures. +type ReturnStruct struct { + A uint32 + B string +} + +// ReturnEnum is a sum type returned by procedures. +//stdb:sumtype +type ReturnEnum interface { + returnEnumTag() uint8 +} + +//stdb:variant of=ReturnEnum name=A +type ReturnEnumA struct { + Value uint32 +} + +func (ReturnEnumA) returnEnumTag() uint8 { return 0 } + +//stdb:variant of=ReturnEnum name=B +type ReturnEnumB struct { + Value string +} + +func (ReturnEnumB) returnEnumTag() uint8 { return 1 } diff --git a/modules/sdk-test-view-go/go.mod b/modules/sdk-test-view-go/go.mod new file mode 100644 index 00000000000..84d980262b0 --- /dev/null +++ b/modules/sdk-test-view-go/go.mod @@ -0,0 +1,7 @@ +module github.com/clockworklabs/SpacetimeDB/modules/sdk-test-view-go + +go 1.23 + +require github.com/clockworklabs/SpacetimeDB/sdks/go v0.0.0 + +replace github.com/clockworklabs/SpacetimeDB/sdks/go => ../../sdks/go diff --git a/modules/sdk-test-view-go/main.go b/modules/sdk-test-view-go/main.go new file mode 100644 index 00000000000..e4bb606a3cd --- /dev/null +++ b/modules/sdk-test-view-go/main.go @@ -0,0 +1,5 @@ +package main + +//go:generate stdb-gen + +func main() {} diff --git a/modules/sdk-test-view-go/reducers.go b/modules/sdk-test-view-go/reducers.go new file mode 100644 index 00000000000..33e6e633d07 --- /dev/null +++ b/modules/sdk-test-view-go/reducers.go @@ -0,0 +1,52 @@ +package main + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/server" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +//stdb:reducer +func insertPlayer(_ server.ReducerContext, identity types.Identity, level uint64) { + player := PlayerTable.Insert(Player{EntityId: 0, Identity: identity}) + PlayerLevelTable.Insert(PlayerLevel{EntityId: player.EntityId, Level: level}) +} + +//stdb:reducer +func deletePlayer(_ server.ReducerContext, identity types.Identity) { + player, found, err := PlayerTable.FindByIdentity(identity) + if err != nil || !found { + return + } + PlayerTable.DeleteByEntityId(player.EntityId) + PlayerLevelTable.DeleteByEntityId(player.EntityId) +} + +//stdb:reducer +func movePlayer(ctx server.ReducerContext, dx int32, dy int32) { + // Find or create my player. + myPlayer, found, _ := PlayerTable.FindByIdentity(ctx.Sender()) + if !found { + myPlayer = PlayerTable.Insert(Player{EntityId: 0, Identity: ctx.Sender()}) + } + + // Find or create my location. + loc, found, _ := PlayerLocationTable.FindByEntityId(myPlayer.EntityId) + if found { + x := loc.X + dx + y := loc.Y + dy + PlayerLocationTable.DeleteByEntityId(loc.EntityId) + PlayerLocationTable.Insert(PlayerLocation{ + EntityId: loc.EntityId, + Active: loc.Active, + X: x, + Y: y, + }) + } else { + PlayerLocationTable.Insert(PlayerLocation{ + EntityId: myPlayer.EntityId, + Active: true, + X: dx, + Y: dy, + }) + } +} diff --git a/modules/sdk-test-view-go/sdk-test-view-go b/modules/sdk-test-view-go/sdk-test-view-go new file mode 100755 index 00000000000..60ae0bb0a77 Binary files /dev/null and b/modules/sdk-test-view-go/sdk-test-view-go differ diff --git a/modules/sdk-test-view-go/tables.go b/modules/sdk-test-view-go/tables.go new file mode 100644 index 00000000000..7d733309b22 --- /dev/null +++ b/modules/sdk-test-view-go/tables.go @@ -0,0 +1,23 @@ +package main + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/types" + +//stdb:table name=player access=public +type Player struct { + EntityId uint64 `stdb:"primarykey,autoinc"` + Identity types.Identity `stdb:"unique"` +} + +//stdb:table name=player_level access=public +type PlayerLevel struct { + EntityId uint64 `stdb:"unique"` + Level uint64 `stdb:"index=btree"` +} + +//stdb:table name=player_location access=private +type PlayerLocation struct { + EntityId uint64 `stdb:"unique"` + Active bool `stdb:"index=btree"` + X int32 + Y int32 +} diff --git a/modules/sdk-test-view-go/types.go b/modules/sdk-test-view-go/types.go new file mode 100644 index 00000000000..e734ec86776 --- /dev/null +++ b/modules/sdk-test-view-go/types.go @@ -0,0 +1,9 @@ +package main + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/types" + +type PlayerAndLevel struct { + EntityId uint64 + Identity types.Identity + Level uint64 +} diff --git a/modules/sdk-test-view-go/views.go b/modules/sdk-test-view-go/views.go new file mode 100644 index 00000000000..ff13c1674af --- /dev/null +++ b/modules/sdk-test-view-go/views.go @@ -0,0 +1,90 @@ +package main + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/server" +) + +//stdb:view public=true +func myPlayer(ctx server.ViewContext) *Player { + player, found, _ := PlayerTable.FindByIdentity(ctx.Sender()) + if !found { + return nil + } + return &player +} + +//stdb:view public=true +func myPlayerAndLevel(ctx server.ViewContext) *PlayerAndLevel { + player, found, _ := PlayerTable.FindByIdentity(ctx.Sender()) + if !found { + return nil + } + level, found, _ := PlayerLevelTable.FindByEntityId(player.EntityId) + if !found { + return nil + } + return &PlayerAndLevel{ + EntityId: player.EntityId, + Identity: player.Identity, + Level: level.Level, + } +} + +//stdb:view public=true +func playersAtLevel0(_ server.AnonymousViewContext) []Player { + iter, err := PlayerLevelTable.FilterByLevel(0) + if err != nil { + return nil + } + var players []Player + for { + pl, ok := iter.Next() + if !ok { + break + } + player, found, _ := PlayerTable.FindByEntityId(pl.EntityId) + if found { + players = append(players, player) + } + } + return players +} + +//stdb:view public=true +func nearbyPlayers(ctx server.ViewContext) []PlayerLocation { + myPlayerRow, found, _ := PlayerTable.FindByIdentity(ctx.Sender()) + if !found { + return nil + } + myLoc, found, _ := PlayerLocationTable.FindByEntityId(myPlayerRow.EntityId) + if !found { + return nil + } + + iter, err := PlayerLocationTable.FilterByActive(true) + if err != nil { + return nil + } + var result []PlayerLocation + for { + loc, ok := iter.Next() + if !ok { + break + } + if loc.EntityId == myLoc.EntityId { + continue + } + dx := loc.X - myLoc.X + dy := loc.Y - myLoc.Y + if dx < 0 { + dx = -dx + } + if dy < 0 { + dy = -dy + } + if dx < 5 && dy < 5 { + result = append(result, loc) + } + } + return result +} diff --git a/sdks/go/Taskfile.yml b/sdks/go/Taskfile.yml new file mode 100644 index 00000000000..dc54ebb217b --- /dev/null +++ b/sdks/go/Taskfile.yml @@ -0,0 +1,256 @@ +version: '3' + +vars: + GO_MODULE: github.com/clockworklabs/SpacetimeDB/sdks/go + +tasks: + default: + desc: Show available tasks + cmds: + - task --list + + test: + desc: Run all Go tests + cmds: + - go test ./... + + test:bsatn: + desc: Run BSATN package tests + cmds: + - go test ./bsatn/... + + test:types: + desc: Run types package tests + cmds: + - go test ./types/... + + test:client: + desc: Run client package tests + cmds: + - go test ./client/... + + test:server: + desc: Run server package tests (native stubs) + cmds: + - go test ./server/... + + test:integration: + desc: Run integration tests (requires running SpacetimeDB) + cmds: + - go test ./tests/integration/... -v -count=1 + + test:conformance: + desc: Run conformance tests + cmds: + - go test ./tests/conformance/... -v -count=1 + + lint: + desc: Run golangci-lint + cmds: + - golangci-lint run ./... + + fmt: + desc: Format Go source files + cmds: + - gofmt -w . + + build: + desc: Build all packages + cmds: + - go build ./... + + build:wasm: + desc: Build test WASM module with Go + env: + GOOS: wasip1 + GOARCH: wasm + cmds: + - go build -buildmode=c-shared -o tests/integration/server_module/module.wasm ./tests/integration/server_module/ + + vet: + desc: Run go vet + cmds: + - go vet ./... + + tidy: + desc: Run go mod tidy + cmds: + - go mod tidy + + generate:golden: + desc: Generate golden byte files from Rust reference + cmds: + - go generate ./tests/golden/... + + build:sdk-test-module: + desc: Build sdk-test-go WASM module with Go + dir: ../../modules/sdk-test-go + cmds: + - go generate ./... + - mkdir -p target + - cmd: go build -buildmode=c-shared -ldflags="-s -w" -o target/module.wasm . + env: + GOOS: wasip1 + GOARCH: wasm + + build:benchmark-module: + desc: Build benchmarks-go WASM module with Go + dir: ../../modules/benchmarks-go + cmds: + - go generate ./... + - mkdir -p target + - cmd: go build -buildmode=c-shared -ldflags="-s -w" -o target/module.wasm . + env: + GOOS: wasip1 + GOARCH: wasm + + build:connect-disconnect-module: + desc: Build sdk-test-connect-disconnect-go WASM module + dir: ../../modules/sdk-test-connect-disconnect-go + cmds: + - go generate ./... + - mkdir -p target + - cmd: go build -buildmode=c-shared -ldflags="-s -w" -o target/module.wasm . + env: + GOOS: wasip1 + GOARCH: wasm + + build:event-table-module: + desc: Build sdk-test-event-table-go WASM module + dir: ../../modules/sdk-test-event-table-go + cmds: + - go generate ./... + - mkdir -p target + - cmd: go build -buildmode=c-shared -ldflags="-s -w" -o target/module.wasm . + env: + GOOS: wasip1 + GOARCH: wasm + + build:perf-test-module: + desc: Build perf-test-go WASM module + dir: ../../modules/perf-test-go + cmds: + - go generate ./... + - mkdir -p target + - cmd: go build -buildmode=c-shared -ldflags="-s -w" -o target/module.wasm . + env: + GOOS: wasip1 + GOARCH: wasm + + build:keynote-benchmarks-module: + desc: Build keynote-benchmarks-go WASM module + dir: ../../modules/keynote-benchmarks-go + cmds: + - go generate ./... + - mkdir -p target + - cmd: go build -buildmode=c-shared -ldflags="-s -w" -o target/module.wasm . + env: + GOOS: wasip1 + GOARCH: wasm + + build:view-module: + desc: Build sdk-test-view-go WASM module + dir: ../../modules/sdk-test-view-go + cmds: + - go generate ./... + - mkdir -p target + - cmd: go build -buildmode=c-shared -ldflags="-s -w" -o target/module.wasm . + env: + GOOS: wasip1 + GOARCH: wasm + + build:procedure-module: + desc: Build sdk-test-procedure-go WASM module + dir: ../../modules/sdk-test-procedure-go + cmds: + - go generate ./... + - mkdir -p target + - cmd: go build -buildmode=c-shared -ldflags="-s -w" -o target/module.wasm . + env: + GOOS: wasip1 + GOARCH: wasm + + build:module-test-module: + desc: Build module-test-go WASM module + dir: ../../modules/module-test-go + cmds: + - go generate ./... + - mkdir -p target + - cmd: go build -buildmode=c-shared -ldflags="-s -w" -o target/module.wasm . + env: + GOOS: wasip1 + GOARCH: wasm + + build:basic-go-server: + desc: Build basic-go server WASM module + dir: ../../templates/basic-go/spacetimedb + cmds: + - go generate ./... + - cmd: go build -buildmode=c-shared -ldflags="-s -w" -o module.wasm . + env: + GOOS: wasip1 + GOARCH: wasm + + build:basic-go-client: + desc: Build basic-go client + dir: ../../templates/basic-go + cmds: + - go build -o basic-go-client . + + build:chat-console-go-server: + desc: Build chat-console-go server WASM module + dir: ../../templates/chat-console-go/spacetimedb + cmds: + - go generate ./... + - cmd: go build -buildmode=c-shared -ldflags="-s -w" -o module.wasm . + env: + GOOS: wasip1 + GOARCH: wasm + + build:chat-console-go-client: + desc: Build chat-console-go client + dir: ../../templates/chat-console-go + cmds: + - go build -o chat-console-go-client . + + build:all-modules: + desc: Build all Go WASM modules + cmds: + - task: build:sdk-test-module + - task: build:benchmark-module + - task: build:connect-disconnect-module + - task: build:event-table-module + - task: build:perf-test-module + - task: build:keynote-benchmarks-module + - task: build:view-module + - task: build:procedure-module + - task: build:module-test-module + - task: build:basic-go-server + - task: build:chat-console-go-server + + clean: + desc: Clean build artifacts + cmds: + - rm -f tests/integration/server_module/module.wasm + - rm -f ../../modules/sdk-test-go/target/module.wasm + - rm -f ../../modules/benchmarks-go/target/module.wasm + - rm -f ../../modules/sdk-test-connect-disconnect-go/target/module.wasm + - rm -f ../../modules/sdk-test-event-table-go/target/module.wasm + - rm -f ../../modules/perf-test-go/target/module.wasm + - rm -f ../../modules/keynote-benchmarks-go/target/module.wasm + - rm -f ../../modules/sdk-test-view-go/target/module.wasm + - rm -f ../../modules/sdk-test-procedure-go/target/module.wasm + - rm -f ../../modules/module-test-go/target/module.wasm + - rm -f ../../templates/basic-go/spacetimedb/module.wasm + - rm -f ../../templates/basic-go/basic-go-client + - rm -f ../../templates/chat-console-go/spacetimedb/module.wasm + - rm -f ../../templates/chat-console-go/chat-console-go-client + - go clean -cache + + check: + desc: Run all checks (fmt, vet, lint, test) + cmds: + - task: fmt + - task: vet + - task: lint + - task: test diff --git a/sdks/go/bsatn/array.go b/sdks/go/bsatn/array.go new file mode 100644 index 00000000000..051021a7843 --- /dev/null +++ b/sdks/go/bsatn/array.go @@ -0,0 +1,41 @@ +package bsatn + +// WriteArray writes a slice as a BSATN array: u32 LE count + each element. +func WriteArray[T Serializable](w Writer, items []T) { + w.PutArrayLen(uint32(len(items))) + for i := range items { + items[i].WriteBsatn(w) + } +} + +// ReadArray reads a BSATN array using the provided element read function. +func ReadArray[T any](r Reader, readFn func(Reader) (T, error)) ([]T, error) { + count, err := r.GetArrayLen() + if err != nil { + return nil, err + } + items := make([]T, 0, count) + for i := uint32(0); i < count; i++ { + item, err := readFn(r) + if err != nil { + return nil, err + } + items = append(items, item) + } + return items, nil +} + +// WriteByteArray writes a byte slice as BSATN: u32 LE length + raw bytes. +func WriteByteArray(w Writer, data []byte) { + w.PutArrayLen(uint32(len(data))) + w.PutBytes(data) +} + +// ReadByteArray reads a BSATN byte array. +func ReadByteArray(r Reader) ([]byte, error) { + count, err := r.GetArrayLen() + if err != nil { + return nil, err + } + return r.GetBytes(int(count)) +} diff --git a/sdks/go/bsatn/bsatn_test.go b/sdks/go/bsatn/bsatn_test.go new file mode 100644 index 00000000000..e0a684bd42d --- /dev/null +++ b/sdks/go/bsatn/bsatn_test.go @@ -0,0 +1,965 @@ +package bsatn_test + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// === BOOL WRITE TESTS === + +func TestWriteBoolTrue(t *testing.T) { + w := bsatn.NewWriter(1) + w.PutBool(true) + assert.Equal(t, []byte{0x01}, w.Bytes()) +} + +func TestWriteBoolFalse(t *testing.T) { + w := bsatn.NewWriter(1) + w.PutBool(false) + assert.Equal(t, []byte{0x00}, w.Bytes()) +} + +// === BOOL READ TESTS === + +func TestReadBoolTrue(t *testing.T) { + r := bsatn.NewReader([]byte{0x01}) + v, err := r.GetBool() + require.NoError(t, err) + assert.True(t, v) +} + +func TestReadBoolFalse(t *testing.T) { + r := bsatn.NewReader([]byte{0x00}) + v, err := r.GetBool() + require.NoError(t, err) + assert.False(t, v) +} + +func TestReadBoolInvalid(t *testing.T) { + r := bsatn.NewReader([]byte{0x02}) + _, err := r.GetBool() + require.Error(t, err) + var invalidBool *bsatn.ErrInvalidBool + assert.ErrorAs(t, err, &invalidBool) + assert.Equal(t, uint8(0x02), invalidBool.Value) +} + +// === UNSIGNED INTEGER WRITE TESTS === + +func TestWriteU8(t *testing.T) { + w := bsatn.NewWriter(1) + w.PutU8(42) + assert.Equal(t, []byte{0x2a}, w.Bytes()) +} + +func TestWriteU8Max(t *testing.T) { + w := bsatn.NewWriter(1) + w.PutU8(math.MaxUint8) + assert.Equal(t, []byte{0xff}, w.Bytes()) +} + +func TestWriteU16(t *testing.T) { + w := bsatn.NewWriter(2) + w.PutU16(0x0102) + assert.Equal(t, []byte{0x02, 0x01}, w.Bytes()) +} + +func TestWriteU16Max(t *testing.T) { + w := bsatn.NewWriter(2) + w.PutU16(math.MaxUint16) + assert.Equal(t, []byte{0xff, 0xff}, w.Bytes()) +} + +func TestWriteU32(t *testing.T) { + w := bsatn.NewWriter(4) + w.PutU32(42) + assert.Equal(t, []byte{0x2a, 0x00, 0x00, 0x00}, w.Bytes()) +} + +func TestWriteU32Max(t *testing.T) { + w := bsatn.NewWriter(4) + w.PutU32(math.MaxUint32) + assert.Equal(t, []byte{0xff, 0xff, 0xff, 0xff}, w.Bytes()) +} + +func TestWriteU64(t *testing.T) { + w := bsatn.NewWriter(8) + w.PutU64(42) + assert.Equal(t, []byte{0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, w.Bytes()) +} + +func TestWriteU64Max(t *testing.T) { + w := bsatn.NewWriter(8) + w.PutU64(math.MaxUint64) + assert.Equal(t, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, w.Bytes()) +} + +// === SIGNED INTEGER WRITE TESTS === + +func TestWriteI8Positive(t *testing.T) { + w := bsatn.NewWriter(1) + w.PutI8(42) + assert.Equal(t, []byte{0x2a}, w.Bytes()) +} + +func TestWriteI8Negative(t *testing.T) { + w := bsatn.NewWriter(1) + w.PutI8(-1) + assert.Equal(t, []byte{0xff}, w.Bytes()) +} + +func TestWriteI8MinMax(t *testing.T) { + w := bsatn.NewWriter(2) + w.PutI8(math.MinInt8) + w.PutI8(math.MaxInt8) + assert.Equal(t, []byte{0x80, 0x7f}, w.Bytes()) +} + +func TestWriteI16Negative(t *testing.T) { + w := bsatn.NewWriter(2) + w.PutI16(-1) + assert.Equal(t, []byte{0xff, 0xff}, w.Bytes()) +} + +func TestWriteI16MinMax(t *testing.T) { + w := bsatn.NewWriter(4) + w.PutI16(math.MinInt16) + w.PutI16(math.MaxInt16) + // MinInt16 = -32768 = 0x8000 LE: {0x00, 0x80} + // MaxInt16 = 32767 = 0x7FFF LE: {0xff, 0x7f} + assert.Equal(t, []byte{0x00, 0x80, 0xff, 0x7f}, w.Bytes()) +} + +func TestWriteI32Negative(t *testing.T) { + w := bsatn.NewWriter(4) + w.PutI32(-1) + assert.Equal(t, []byte{0xff, 0xff, 0xff, 0xff}, w.Bytes()) +} + +func TestWriteI32Value(t *testing.T) { + w := bsatn.NewWriter(4) + w.PutI32(0x01020304) + assert.Equal(t, []byte{0x04, 0x03, 0x02, 0x01}, w.Bytes()) +} + +func TestWriteI64Negative(t *testing.T) { + w := bsatn.NewWriter(8) + w.PutI64(-1) + assert.Equal(t, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, w.Bytes()) +} + +func TestWriteI64Value(t *testing.T) { + w := bsatn.NewWriter(8) + w.PutI64(0x0102030405060708) + assert.Equal(t, []byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01}, w.Bytes()) +} + +// === FLOAT WRITE TESTS === + +func TestWriteF32One(t *testing.T) { + w := bsatn.NewWriter(4) + w.PutF32(1.0) + // IEEE 754: 1.0 = 0x3F800000 LE: {0x00, 0x00, 0x80, 0x3f} + assert.Equal(t, []byte{0x00, 0x00, 0x80, 0x3f}, w.Bytes()) +} + +func TestWriteF32NegativeZero(t *testing.T) { + w := bsatn.NewWriter(4) + w.PutF32(float32(math.Copysign(0, -1))) + // IEEE 754: -0.0 = 0x80000000 LE: {0x00, 0x00, 0x00, 0x80} + assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x80}, w.Bytes()) +} + +func TestWriteF32Pi(t *testing.T) { + w := bsatn.NewWriter(4) + w.PutF32(math.Pi) + // math.Pi as float32 = 0x40490FDB LE: {0xdb, 0x0f, 0x49, 0x40} + assert.Equal(t, []byte{0xdb, 0x0f, 0x49, 0x40}, w.Bytes()) +} + +func TestWriteF64One(t *testing.T) { + w := bsatn.NewWriter(8) + w.PutF64(1.0) + // IEEE 754: 1.0 = 0x3FF0000000000000 LE + assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f}, w.Bytes()) +} + +func TestWriteF64Pi(t *testing.T) { + w := bsatn.NewWriter(8) + w.PutF64(math.Pi) + // IEEE 754: pi = 0x400921FB54442D18 LE + assert.Equal(t, []byte{0x18, 0x2d, 0x44, 0x54, 0xfb, 0x21, 0x09, 0x40}, w.Bytes()) +} + +// === UNSIGNED INTEGER READ TESTS === + +func TestReadU8(t *testing.T) { + r := bsatn.NewReader([]byte{0x2a}) + v, err := r.GetU8() + require.NoError(t, err) + assert.Equal(t, uint8(42), v) +} + +func TestReadU16(t *testing.T) { + r := bsatn.NewReader([]byte{0x02, 0x01}) + v, err := r.GetU16() + require.NoError(t, err) + assert.Equal(t, uint16(0x0102), v) +} + +func TestReadU32(t *testing.T) { + r := bsatn.NewReader([]byte{0x2a, 0x00, 0x00, 0x00}) + v, err := r.GetU32() + require.NoError(t, err) + assert.Equal(t, uint32(42), v) +} + +func TestReadU64(t *testing.T) { + r := bsatn.NewReader([]byte{0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) + v, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(42), v) +} + +// === SIGNED INTEGER READ TESTS === + +func TestReadI8Negative(t *testing.T) { + r := bsatn.NewReader([]byte{0xff}) + v, err := r.GetI8() + require.NoError(t, err) + assert.Equal(t, int8(-1), v) +} + +func TestReadI16Negative(t *testing.T) { + r := bsatn.NewReader([]byte{0xff, 0xff}) + v, err := r.GetI16() + require.NoError(t, err) + assert.Equal(t, int16(-1), v) +} + +func TestReadI32Negative(t *testing.T) { + r := bsatn.NewReader([]byte{0xff, 0xff, 0xff, 0xff}) + v, err := r.GetI32() + require.NoError(t, err) + assert.Equal(t, int32(-1), v) +} + +func TestReadI64Negative(t *testing.T) { + r := bsatn.NewReader([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) + v, err := r.GetI64() + require.NoError(t, err) + assert.Equal(t, int64(-1), v) +} + +// === FLOAT READ TESTS === + +func TestReadF32(t *testing.T) { + r := bsatn.NewReader([]byte{0x00, 0x00, 0x80, 0x3f}) + v, err := r.GetF32() + require.NoError(t, err) + assert.Equal(t, float32(1.0), v) +} + +func TestReadF64(t *testing.T) { + r := bsatn.NewReader([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f}) + v, err := r.GetF64() + require.NoError(t, err) + assert.Equal(t, float64(1.0), v) +} + +// === STRING TESTS === + +func TestWriteString(t *testing.T) { + w := bsatn.NewWriter(16) + w.PutString("hello") + expected := []byte{0x05, 0x00, 0x00, 0x00, 'h', 'e', 'l', 'l', 'o'} + assert.Equal(t, expected, w.Bytes()) +} + +func TestWriteEmptyString(t *testing.T) { + w := bsatn.NewWriter(4) + w.PutString("") + assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, w.Bytes()) +} + +func TestWriteStringUTF8(t *testing.T) { + w := bsatn.NewWriter(16) + // U+00E9 (e with acute) is 0xC3 0xA9 in UTF-8 (2 bytes) + w.PutString("\u00e9") + // length prefix is byte count, not rune count + expected := []byte{0x02, 0x00, 0x00, 0x00, 0xc3, 0xa9} + assert.Equal(t, expected, w.Bytes()) +} + +func TestWriteStringMultiByteUTF8(t *testing.T) { + w := bsatn.NewWriter(16) + // U+1F600 (grinning face) is 0xF0 0x9F 0x98 0x80 in UTF-8 (4 bytes) + w.PutString("\U0001F600") + expected := []byte{0x04, 0x00, 0x00, 0x00, 0xf0, 0x9f, 0x98, 0x80} + assert.Equal(t, expected, w.Bytes()) +} + +func TestReadString(t *testing.T) { + data := []byte{0x05, 0x00, 0x00, 0x00, 'h', 'e', 'l', 'l', 'o'} + r := bsatn.NewReader(data) + v, err := r.GetString() + require.NoError(t, err) + assert.Equal(t, "hello", v) +} + +func TestReadEmptyString(t *testing.T) { + data := []byte{0x00, 0x00, 0x00, 0x00} + r := bsatn.NewReader(data) + v, err := r.GetString() + require.NoError(t, err) + assert.Equal(t, "", v) +} + +func TestReadStringUTF8(t *testing.T) { + data := []byte{0x02, 0x00, 0x00, 0x00, 0xc3, 0xa9} + r := bsatn.NewReader(data) + v, err := r.GetString() + require.NoError(t, err) + assert.Equal(t, "\u00e9", v) +} + +// === ROUND-TRIP TESTS === + +func TestRoundTripBool(t *testing.T) { + for _, tc := range []bool{true, false} { + encoded := bsatn.EncodeBool(tc) + decoded, err := bsatn.DecodeBool(encoded) + require.NoError(t, err) + assert.Equal(t, tc, decoded) + } +} + +func TestRoundTripU8(t *testing.T) { + for _, tc := range []uint8{0, 1, 42, math.MaxUint8} { + encoded := bsatn.EncodeU8(tc) + decoded, err := bsatn.DecodeU8(encoded) + require.NoError(t, err) + assert.Equal(t, tc, decoded) + } +} + +func TestRoundTripU16(t *testing.T) { + for _, tc := range []uint16{0, 1, 0x0102, math.MaxUint16} { + encoded := bsatn.EncodeU16(tc) + decoded, err := bsatn.DecodeU16(encoded) + require.NoError(t, err) + assert.Equal(t, tc, decoded) + } +} + +func TestRoundTripU32(t *testing.T) { + for _, tc := range []uint32{0, 1, 42, 0x01020304, math.MaxUint32} { + encoded := bsatn.EncodeU32(tc) + decoded, err := bsatn.DecodeU32(encoded) + require.NoError(t, err) + assert.Equal(t, tc, decoded) + } +} + +func TestRoundTripU64(t *testing.T) { + for _, tc := range []uint64{0, 1, 42, 0x0102030405060708, math.MaxUint64} { + encoded := bsatn.EncodeU64(tc) + decoded, err := bsatn.DecodeU64(encoded) + require.NoError(t, err) + assert.Equal(t, tc, decoded) + } +} + +func TestRoundTripI8(t *testing.T) { + for _, tc := range []int8{math.MinInt8, -1, 0, 1, math.MaxInt8} { + encoded := bsatn.EncodeI8(tc) + decoded, err := bsatn.DecodeI8(encoded) + require.NoError(t, err) + assert.Equal(t, tc, decoded) + } +} + +func TestRoundTripI16(t *testing.T) { + for _, tc := range []int16{math.MinInt16, -1, 0, 1, math.MaxInt16} { + encoded := bsatn.EncodeI16(tc) + decoded, err := bsatn.DecodeI16(encoded) + require.NoError(t, err) + assert.Equal(t, tc, decoded) + } +} + +func TestRoundTripI32(t *testing.T) { + for _, tc := range []int32{math.MinInt32, -1, 0, 1, math.MaxInt32} { + encoded := bsatn.EncodeI32(tc) + decoded, err := bsatn.DecodeI32(encoded) + require.NoError(t, err) + assert.Equal(t, tc, decoded) + } +} + +func TestRoundTripI64(t *testing.T) { + for _, tc := range []int64{math.MinInt64, -1, 0, 1, math.MaxInt64} { + encoded := bsatn.EncodeI64(tc) + decoded, err := bsatn.DecodeI64(encoded) + require.NoError(t, err) + assert.Equal(t, tc, decoded) + } +} + +func TestRoundTripF32(t *testing.T) { + for _, tc := range []float32{0, 1.0, -1.0, math.SmallestNonzeroFloat32, math.MaxFloat32, float32(math.Pi)} { + encoded := bsatn.EncodeF32(tc) + decoded, err := bsatn.DecodeF32(encoded) + require.NoError(t, err) + assert.Equal(t, tc, decoded) + } +} + +func TestRoundTripF64(t *testing.T) { + for _, tc := range []float64{0, 1.0, -1.0, math.SmallestNonzeroFloat64, math.MaxFloat64, math.Pi} { + encoded := bsatn.EncodeF64(tc) + decoded, err := bsatn.DecodeF64(encoded) + require.NoError(t, err) + assert.Equal(t, tc, decoded) + } +} + +func TestRoundTripString(t *testing.T) { + for _, tc := range []string{"", "hello", "\u00e9", "\U0001F600", "hello world"} { + encoded := bsatn.EncodeString(tc) + decoded, err := bsatn.DecodeString(encoded) + require.NoError(t, err) + assert.Equal(t, tc, decoded) + } +} + +// === CONVENIENCE FUNCTION TESTS === + +func TestEncodeBoolBytes(t *testing.T) { + assert.Equal(t, []byte{0x01}, bsatn.EncodeBool(true)) + assert.Equal(t, []byte{0x00}, bsatn.EncodeBool(false)) +} + +func TestEncodeU32Bytes(t *testing.T) { + assert.Equal(t, []byte{0x2a, 0x00, 0x00, 0x00}, bsatn.EncodeU32(42)) +} + +func TestEncodeI64Bytes(t *testing.T) { + assert.Equal(t, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, bsatn.EncodeI64(-1)) +} + +func TestEncodeStringBytes(t *testing.T) { + expected := []byte{0x05, 0x00, 0x00, 0x00, 'h', 'e', 'l', 'l', 'o'} + assert.Equal(t, expected, bsatn.EncodeString("hello")) +} + +// === ARRAY TESTS === + +func TestWriteByteArray(t *testing.T) { + w := bsatn.NewWriter(16) + bsatn.WriteByteArray(w, []byte{0xDE, 0xAD, 0xBE, 0xEF}) + // u32 LE length (4) + raw bytes + expected := []byte{0x04, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF} + assert.Equal(t, expected, w.Bytes()) +} + +func TestWriteByteArrayEmpty(t *testing.T) { + w := bsatn.NewWriter(4) + bsatn.WriteByteArray(w, []byte{}) + assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, w.Bytes()) +} + +func TestReadByteArray(t *testing.T) { + data := []byte{0x04, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF} + r := bsatn.NewReader(data) + v, err := bsatn.ReadByteArray(r) + require.NoError(t, err) + assert.Equal(t, []byte{0xDE, 0xAD, 0xBE, 0xEF}, v) +} + +func TestReadByteArrayEmpty(t *testing.T) { + data := []byte{0x00, 0x00, 0x00, 0x00} + r := bsatn.NewReader(data) + v, err := bsatn.ReadByteArray(r) + require.NoError(t, err) + assert.Empty(t, v) +} + +func TestReadArrayOfU32(t *testing.T) { + // Array of 3 u32 values: [1, 2, 3] + data := []byte{ + 0x03, 0x00, 0x00, 0x00, // count = 3 + 0x01, 0x00, 0x00, 0x00, // 1 + 0x02, 0x00, 0x00, 0x00, // 2 + 0x03, 0x00, 0x00, 0x00, // 3 + } + r := bsatn.NewReader(data) + v, err := bsatn.ReadArray(r, func(r bsatn.Reader) (uint32, error) { + return r.GetU32() + }) + require.NoError(t, err) + assert.Equal(t, []uint32{1, 2, 3}, v) +} + +func TestReadArrayEmpty(t *testing.T) { + data := []byte{0x00, 0x00, 0x00, 0x00} + r := bsatn.NewReader(data) + v, err := bsatn.ReadArray(r, func(r bsatn.Reader) (uint32, error) { + return r.GetU32() + }) + require.NoError(t, err) + assert.Empty(t, v) +} + +// === OPTION TESTS === + +func TestReadOptionSome(t *testing.T) { + // Option Some(42): tag 0 + u32 LE 42 + data := []byte{0x00, 0x2a, 0x00, 0x00, 0x00} + r := bsatn.NewReader(data) + v, err := bsatn.ReadOption(r, func(r bsatn.Reader) (uint32, error) { + return r.GetU32() + }) + require.NoError(t, err) + require.NotNil(t, v) + assert.Equal(t, uint32(42), *v) +} + +func TestReadOptionNone(t *testing.T) { + // Option None: tag 1 + data := []byte{0x01} + r := bsatn.NewReader(data) + v, err := bsatn.ReadOption(r, func(r bsatn.Reader) (uint32, error) { + return r.GetU32() + }) + require.NoError(t, err) + assert.Nil(t, v) +} + +func TestReadOptionInvalidTag(t *testing.T) { + // Option with invalid tag 5 + data := []byte{0x05} + r := bsatn.NewReader(data) + _, err := bsatn.ReadOption(r, func(r bsatn.Reader) (uint32, error) { + return r.GetU32() + }) + require.Error(t, err) + var invalidTag *bsatn.ErrInvalidTag + assert.ErrorAs(t, err, &invalidTag) + assert.Equal(t, uint8(5), invalidTag.Tag) + assert.Equal(t, "Option", invalidTag.SumName) +} + +// === SUM TAG TESTS === + +func TestWriteSumTag(t *testing.T) { + w := bsatn.NewWriter(1) + w.PutSumTag(3) + assert.Equal(t, []byte{0x03}, w.Bytes()) +} + +func TestReadSumTag(t *testing.T) { + r := bsatn.NewReader([]byte{0x03}) + v, err := r.GetSumTag() + require.NoError(t, err) + assert.Equal(t, uint8(3), v) +} + +// === MAP TESTS === + +func TestReadMap(t *testing.T) { + // Map with 2 entries: {"a": 1, "b": 2} + data := []byte{ + 0x02, 0x00, 0x00, 0x00, // count = 2 + // key "a" + 0x01, 0x00, 0x00, 0x00, 'a', + // value 1 + 0x01, 0x00, 0x00, 0x00, + // key "b" + 0x01, 0x00, 0x00, 0x00, 'b', + // value 2 + 0x02, 0x00, 0x00, 0x00, + } + r := bsatn.NewReader(data) + m, err := bsatn.ReadMap(r, + func(r bsatn.Reader) (string, error) { return r.GetString() }, + func(r bsatn.Reader) (uint32, error) { return r.GetU32() }, + ) + require.NoError(t, err) + assert.Len(t, m, 2) + assert.Equal(t, uint32(1), m["a"]) + assert.Equal(t, uint32(2), m["b"]) +} + +func TestReadMapEmpty(t *testing.T) { + data := []byte{0x00, 0x00, 0x00, 0x00} + r := bsatn.NewReader(data) + m, err := bsatn.ReadMap(r, + func(r bsatn.Reader) (string, error) { return r.GetString() }, + func(r bsatn.Reader) (uint32, error) { return r.GetU32() }, + ) + require.NoError(t, err) + assert.Empty(t, m) +} + +// === ARRAY/MAP LENGTH PREFIX TESTS === + +func TestWriteArrayLen(t *testing.T) { + w := bsatn.NewWriter(4) + w.PutArrayLen(5) + assert.Equal(t, []byte{0x05, 0x00, 0x00, 0x00}, w.Bytes()) +} + +func TestReadArrayLen(t *testing.T) { + r := bsatn.NewReader([]byte{0x05, 0x00, 0x00, 0x00}) + v, err := r.GetArrayLen() + require.NoError(t, err) + assert.Equal(t, uint32(5), v) +} + +func TestWriteMapLen(t *testing.T) { + w := bsatn.NewWriter(4) + w.PutMapLen(10) + assert.Equal(t, []byte{0x0a, 0x00, 0x00, 0x00}, w.Bytes()) +} + +func TestReadMapLen(t *testing.T) { + r := bsatn.NewReader([]byte{0x0a, 0x00, 0x00, 0x00}) + v, err := r.GetMapLen() + require.NoError(t, err) + assert.Equal(t, uint32(10), v) +} + +// === ERROR CASES === + +func TestReadU8EmptyBuffer(t *testing.T) { + r := bsatn.NewReader([]byte{}) + _, err := r.GetU8() + require.Error(t, err) + var bufErr *bsatn.ErrBufferTooShort + assert.ErrorAs(t, err, &bufErr) + assert.Equal(t, 1, bufErr.Expected) + assert.Equal(t, 0, bufErr.Given) +} + +func TestReadU16BufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{0x01}) + _, err := r.GetU16() + require.Error(t, err) + var bufErr *bsatn.ErrBufferTooShort + assert.ErrorAs(t, err, &bufErr) + assert.Equal(t, 2, bufErr.Expected) + assert.Equal(t, 1, bufErr.Given) +} + +func TestReadU32BufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{0x01, 0x02}) + _, err := r.GetU32() + require.Error(t, err) + var bufErr *bsatn.ErrBufferTooShort + assert.ErrorAs(t, err, &bufErr) + assert.Equal(t, 4, bufErr.Expected) + assert.Equal(t, 2, bufErr.Given) +} + +func TestReadU64BufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{0x01, 0x02, 0x03, 0x04}) + _, err := r.GetU64() + require.Error(t, err) + var bufErr *bsatn.ErrBufferTooShort + assert.ErrorAs(t, err, &bufErr) + assert.Equal(t, 8, bufErr.Expected) + assert.Equal(t, 4, bufErr.Given) +} + +func TestReadBoolBufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{}) + _, err := r.GetBool() + require.Error(t, err) + var bufErr *bsatn.ErrBufferTooShort + assert.ErrorAs(t, err, &bufErr) +} + +func TestReadStringBufferTooShortForLength(t *testing.T) { + // Only 2 bytes, but u32 length prefix requires 4 + r := bsatn.NewReader([]byte{0x05, 0x00}) + _, err := r.GetString() + require.Error(t, err) +} + +func TestReadStringBufferTooShortForPayload(t *testing.T) { + // Length says 5 bytes, but only 2 available + r := bsatn.NewReader([]byte{0x05, 0x00, 0x00, 0x00, 'h', 'i'}) + _, err := r.GetString() + require.Error(t, err) +} + +func TestReadByteArrayBufferTooShort(t *testing.T) { + // Length says 10 bytes, but only 3 available + r := bsatn.NewReader([]byte{0x0a, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03}) + _, err := bsatn.ReadByteArray(r) + require.Error(t, err) +} + +// === REMAINING TESTS === + +func TestReaderRemaining(t *testing.T) { + r := bsatn.NewReader([]byte{0x01, 0x02, 0x03, 0x04}) + assert.Equal(t, 4, r.Remaining()) + + _, err := r.GetU8() + require.NoError(t, err) + assert.Equal(t, 3, r.Remaining()) + + _, err = r.GetU16() + require.NoError(t, err) + assert.Equal(t, 1, r.Remaining()) +} + +// === SEQUENTIAL READ/WRITE (PRODUCT PATTERN) === + +func TestSequentialWriteMultipleValues(t *testing.T) { + // Simulate a product type with fields: bool, u32, string + w := bsatn.NewWriter(32) + w.PutBool(true) + w.PutU32(42) + w.PutString("test") + + expected := []byte{ + 0x01, // bool true + 0x2a, 0x00, 0x00, 0x00, // u32 42 + 0x04, 0x00, 0x00, 0x00, // string length 4 + 't', 'e', 's', 't', // string "test" + } + assert.Equal(t, expected, w.Bytes()) +} + +func TestSequentialReadMultipleValues(t *testing.T) { + data := []byte{ + 0x01, // bool true + 0x2a, 0x00, 0x00, 0x00, // u32 42 + 0x04, 0x00, 0x00, 0x00, // string length 4 + 't', 'e', 's', 't', // string "test" + } + r := bsatn.NewReader(data) + + b, err := r.GetBool() + require.NoError(t, err) + assert.True(t, b) + + u, err := r.GetU32() + require.NoError(t, err) + assert.Equal(t, uint32(42), u) + + s, err := r.GetString() + require.NoError(t, err) + assert.Equal(t, "test", s) + + assert.Equal(t, 0, r.Remaining()) +} + +// === ENCODE/DECODE GENERIC HELPERS === + +func TestEncodeDecodeGeneric(t *testing.T) { + data := bsatn.EncodeU32(12345) + v, err := bsatn.Decode(data, func(r bsatn.Reader) (uint32, error) { + return r.GetU32() + }) + require.NoError(t, err) + assert.Equal(t, uint32(12345), v) +} + +// === PUTBYTES RAW TEST === + +func TestPutBytesRaw(t *testing.T) { + w := bsatn.NewWriter(4) + w.PutBytes([]byte{0xCA, 0xFE, 0xBA, 0xBE}) + assert.Equal(t, []byte{0xCA, 0xFE, 0xBA, 0xBE}, w.Bytes()) +} + +// === ERROR MESSAGE FORMAT TESTS === + +func TestBufferTooShortErrorMessage(t *testing.T) { + err := &bsatn.ErrBufferTooShort{ForType: "u32", Expected: 4, Given: 2} + assert.Contains(t, err.Error(), "u32") + assert.Contains(t, err.Error(), "4") + assert.Contains(t, err.Error(), "2") +} + +func TestInvalidBoolErrorMessage(t *testing.T) { + err := &bsatn.ErrInvalidBool{Value: 0x42} + assert.Contains(t, err.Error(), "0x42") +} + +func TestInvalidTagErrorMessage(t *testing.T) { + err := &bsatn.ErrInvalidTag{Tag: 5, SumName: "MyEnum"} + assert.Contains(t, err.Error(), "5") + assert.Contains(t, err.Error(), "MyEnum") +} + +// === GETBYTES TEST === + +func TestGetBytes(t *testing.T) { + data := []byte{0x01, 0x02, 0x03, 0x04, 0x05} + r := bsatn.NewReader(data) + b, err := r.GetBytes(3) + require.NoError(t, err) + assert.Equal(t, []byte{0x01, 0x02, 0x03}, b) + assert.Equal(t, 2, r.Remaining()) +} + +func TestGetBytesTooFew(t *testing.T) { + r := bsatn.NewReader([]byte{0x01}) + _, err := r.GetBytes(5) + require.Error(t, err) +} + +// === F32/F64 SPECIAL VALUES === + +func TestRoundTripF32Inf(t *testing.T) { + posInf := float32(math.Inf(1)) + encoded := bsatn.EncodeF32(posInf) + decoded, err := bsatn.DecodeF32(encoded) + require.NoError(t, err) + assert.True(t, math.IsInf(float64(decoded), 1)) + + negInf := float32(math.Inf(-1)) + encoded = bsatn.EncodeF32(negInf) + decoded, err = bsatn.DecodeF32(encoded) + require.NoError(t, err) + assert.True(t, math.IsInf(float64(decoded), -1)) +} + +func TestRoundTripF64Inf(t *testing.T) { + posInf := math.Inf(1) + encoded := bsatn.EncodeF64(posInf) + decoded, err := bsatn.DecodeF64(encoded) + require.NoError(t, err) + assert.True(t, math.IsInf(decoded, 1)) + + negInf := math.Inf(-1) + encoded = bsatn.EncodeF64(negInf) + decoded, err = bsatn.DecodeF64(encoded) + require.NoError(t, err) + assert.True(t, math.IsInf(decoded, -1)) +} + +func TestRoundTripF32NaN(t *testing.T) { + nan := float32(math.NaN()) + encoded := bsatn.EncodeF32(nan) + decoded, err := bsatn.DecodeF32(encoded) + require.NoError(t, err) + assert.True(t, math.IsNaN(float64(decoded))) +} + +func TestRoundTripF64NaN(t *testing.T) { + nan := math.NaN() + encoded := bsatn.EncodeF64(nan) + decoded, err := bsatn.DecodeF64(encoded) + require.NoError(t, err) + assert.True(t, math.IsNaN(decoded)) +} + +// === ZERO-COPY READER TESTS === + +func TestZeroCopyReaderStringRoundTrip(t *testing.T) { + w := bsatn.NewWriter(64) + w.PutString("hello") + w.PutString("world") + data := w.Bytes() + + r := bsatn.NewZeroCopyReader(data) + s1, err := r.GetString() + require.NoError(t, err) + assert.Equal(t, "hello", s1) + + s2, err := r.GetString() + require.NoError(t, err) + assert.Equal(t, "world", s2) + + assert.Equal(t, 0, r.Remaining()) +} + +func TestZeroCopyReaderEmptyString(t *testing.T) { + w := bsatn.NewWriter(8) + w.PutString("") + data := w.Bytes() + + r := bsatn.NewZeroCopyReader(data) + s, err := r.GetString() + require.NoError(t, err) + assert.Equal(t, "", s) +} + +func TestZeroCopyReaderUTF8(t *testing.T) { + w := bsatn.NewWriter(16) + w.PutString("\u00e9") + w.PutString("\U0001F600") + data := w.Bytes() + + r := bsatn.NewZeroCopyReader(data) + s1, err := r.GetString() + require.NoError(t, err) + assert.Equal(t, "\u00e9", s1) + + s2, err := r.GetString() + require.NoError(t, err) + assert.Equal(t, "\U0001F600", s2) +} + +func TestZeroCopyReaderMatchesRegularReader(t *testing.T) { + testStrings := []string{"", "hello", "world", "\u00e9", "\U0001F600", "hello world"} + w := bsatn.NewWriter(256) + for _, s := range testStrings { + w.PutString(s) + } + data := w.Bytes() + + regular := bsatn.NewReader(append([]byte(nil), data...)) + zeroCopy := bsatn.NewZeroCopyReader(append([]byte(nil), data...)) + + for i, expected := range testStrings { + s1, err1 := regular.GetString() + s2, err2 := zeroCopy.GetString() + require.NoError(t, err1, "regular reader failed on string %d", i) + require.NoError(t, err2, "zero-copy reader failed on string %d", i) + assert.Equal(t, expected, s1) + assert.Equal(t, expected, s2) + assert.Equal(t, s1, s2, "readers disagree on string %d", i) + } +} + +func TestZeroCopyReaderMixedTypes(t *testing.T) { + w := bsatn.NewWriter(64) + w.PutU32(42) + w.PutString("test") + w.PutBool(true) + data := w.Bytes() + + r := bsatn.NewZeroCopyReader(data) + v, err := r.GetU32() + require.NoError(t, err) + assert.Equal(t, uint32(42), v) + + s, err := r.GetString() + require.NoError(t, err) + assert.Equal(t, "test", s) + + b, err := r.GetBool() + require.NoError(t, err) + assert.True(t, b) +} + +// === WRITER CAPACITY === + +func TestWriterGrowsBeyondInitialCapacity(t *testing.T) { + w := bsatn.NewWriter(1) // tiny initial capacity + w.PutU64(math.MaxUint64) + assert.Len(t, w.Bytes(), 8) + assert.Equal(t, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, w.Bytes()) +} diff --git a/sdks/go/bsatn/encode.go b/sdks/go/bsatn/encode.go new file mode 100644 index 00000000000..2c51bbb0931 --- /dev/null +++ b/sdks/go/bsatn/encode.go @@ -0,0 +1,14 @@ +package bsatn + +// Encode serializes a Serializable value to BSATN bytes. +func Encode(v Serializable) []byte { + w := NewWriter(64) + v.WriteBsatn(w) + return w.Bytes() +} + +// Decode deserializes BSATN bytes using the provided read function. +func Decode[T any](data []byte, readFn func(Reader) (T, error)) (T, error) { + r := NewReader(data) + return readFn(r) +} diff --git a/sdks/go/bsatn/errors.go b/sdks/go/bsatn/errors.go new file mode 100644 index 00000000000..aeb5012b9f0 --- /dev/null +++ b/sdks/go/bsatn/errors.go @@ -0,0 +1,33 @@ +package bsatn + +import "fmt" + +// ErrBufferTooShort is returned when there aren't enough bytes to read. +type ErrBufferTooShort struct { + ForType string + Expected int + Given int +} + +func (e *ErrBufferTooShort) Error() string { + return fmt.Sprintf("bsatn: buffer too short for %s: expected %d bytes, got %d", e.ForType, e.Expected, e.Given) +} + +// ErrInvalidBool is returned when a bool byte is not 0x00 or 0x01. +type ErrInvalidBool struct { + Value uint8 +} + +func (e *ErrInvalidBool) Error() string { + return fmt.Sprintf("bsatn: invalid bool value: 0x%02x", e.Value) +} + +// ErrInvalidTag is returned when a sum type tag is not recognized. +type ErrInvalidTag struct { + Tag uint8 + SumName string +} + +func (e *ErrInvalidTag) Error() string { + return fmt.Sprintf("bsatn: invalid tag %d for sum type %s", e.Tag, e.SumName) +} diff --git a/sdks/go/bsatn/interfaces.go b/sdks/go/bsatn/interfaces.go new file mode 100644 index 00000000000..19ea1d4569e --- /dev/null +++ b/sdks/go/bsatn/interfaces.go @@ -0,0 +1,50 @@ +package bsatn + +// Writer writes BSATN-encoded binary data. All multi-byte integers are little-endian. +type Writer interface { + PutBool(v bool) + PutU8(v uint8) + PutU16(v uint16) + PutU32(v uint32) + PutU64(v uint64) + PutI8(v int8) + PutI16(v int16) + PutI32(v int32) + PutI64(v int64) + PutF32(v float32) + PutF64(v float64) + PutString(v string) + PutBytes(v []byte) // raw bytes, no length prefix + PutArrayLen(n uint32) // write u32 LE length prefix for arrays + PutMapLen(n uint32) // write u32 LE length prefix for maps + PutSumTag(tag uint8) // write u8 variant tag for sum types + Bytes() []byte // return the accumulated buffer + Reset() // clear buffer, keep capacity + Len() int // current accumulated length +} + +// Reader reads BSATN-encoded binary data. +type Reader interface { + GetBool() (bool, error) + GetU8() (uint8, error) + GetU16() (uint16, error) + GetU32() (uint32, error) + GetU64() (uint64, error) + GetI8() (int8, error) + GetI16() (int16, error) + GetI32() (int32, error) + GetI64() (int64, error) + GetF32() (float32, error) + GetF64() (float64, error) + GetString() (string, error) + GetBytes(n int) ([]byte, error) // read exactly n raw bytes + GetArrayLen() (uint32, error) + GetMapLen() (uint32, error) + GetSumTag() (uint8, error) + Remaining() int +} + +// Serializable can write itself as BSATN. +type Serializable interface { + WriteBsatn(w Writer) +} diff --git a/sdks/go/bsatn/map.go b/sdks/go/bsatn/map.go new file mode 100644 index 00000000000..cb68875ecd4 --- /dev/null +++ b/sdks/go/bsatn/map.go @@ -0,0 +1,36 @@ +package bsatn + +// WriteMap writes a map as BSATN: u32 LE count + key-value pairs. +// Note: Go maps have non-deterministic iteration order. For deterministic output, +// callers should use sorted key iteration. +func WriteMap[K Serializable, V Serializable](w Writer, items []struct { + Key K + Value V +}) { + w.PutMapLen(uint32(len(items))) + for _, item := range items { + item.Key.WriteBsatn(w) + item.Value.WriteBsatn(w) + } +} + +// ReadMap reads a BSATN map using provided key/value read functions. +func ReadMap[K comparable, V any](r Reader, readK func(Reader) (K, error), readV func(Reader) (V, error)) (map[K]V, error) { + count, err := r.GetMapLen() + if err != nil { + return nil, err + } + m := make(map[K]V, count) + for i := uint32(0); i < count; i++ { + k, err := readK(r) + if err != nil { + return nil, err + } + v, err := readV(r) + if err != nil { + return nil, err + } + m[k] = v + } + return m, nil +} diff --git a/sdks/go/bsatn/option.go b/sdks/go/bsatn/option.go new file mode 100644 index 00000000000..d891d414473 --- /dev/null +++ b/sdks/go/bsatn/option.go @@ -0,0 +1,33 @@ +package bsatn + +// WriteOption writes an Option value as a BSATN sum type. +// Some(value) = tag 0 + encoded value +// None = tag 1 (empty product) +func WriteOption[T Serializable](w Writer, v *T) { + if v != nil { + w.PutSumTag(0) + (*v).WriteBsatn(w) + } else { + w.PutSumTag(1) + } +} + +// ReadOption reads a BSATN Option using the provided read function. +func ReadOption[T any](r Reader, readFn func(Reader) (T, error)) (*T, error) { + tag, err := r.GetSumTag() + if err != nil { + return nil, err + } + switch tag { + case 0: // Some + v, err := readFn(r) + if err != nil { + return nil, err + } + return &v, nil + case 1: // None + return nil, nil + default: + return nil, &ErrInvalidTag{Tag: tag, SumName: "Option"} + } +} diff --git a/sdks/go/bsatn/primitives.go b/sdks/go/bsatn/primitives.go new file mode 100644 index 00000000000..45bab54d08b --- /dev/null +++ b/sdks/go/bsatn/primitives.go @@ -0,0 +1,145 @@ +package bsatn + +// EncodeBool encodes a bool to BSATN bytes. +func EncodeBool(v bool) []byte { + w := NewWriter(1) + w.PutBool(v) + return w.Bytes() +} + +// DecodeBool decodes a bool from BSATN bytes. +func DecodeBool(data []byte) (bool, error) { + return NewReader(data).GetBool() +} + +// EncodeU8 encodes a uint8 to BSATN bytes. +func EncodeU8(v uint8) []byte { + w := NewWriter(1) + w.PutU8(v) + return w.Bytes() +} + +// DecodeU8 decodes a uint8 from BSATN bytes. +func DecodeU8(data []byte) (uint8, error) { + return NewReader(data).GetU8() +} + +// EncodeU16 encodes a uint16 to BSATN bytes. +func EncodeU16(v uint16) []byte { + w := NewWriter(2) + w.PutU16(v) + return w.Bytes() +} + +// DecodeU16 decodes a uint16 from BSATN bytes. +func DecodeU16(data []byte) (uint16, error) { + return NewReader(data).GetU16() +} + +// EncodeU32 encodes a uint32 to BSATN bytes. +func EncodeU32(v uint32) []byte { + w := NewWriter(4) + w.PutU32(v) + return w.Bytes() +} + +// DecodeU32 decodes a uint32 from BSATN bytes. +func DecodeU32(data []byte) (uint32, error) { + return NewReader(data).GetU32() +} + +// EncodeU64 encodes a uint64 to BSATN bytes. +func EncodeU64(v uint64) []byte { + w := NewWriter(8) + w.PutU64(v) + return w.Bytes() +} + +// DecodeU64 decodes a uint64 from BSATN bytes. +func DecodeU64(data []byte) (uint64, error) { + return NewReader(data).GetU64() +} + +// EncodeI8 encodes an int8 to BSATN bytes. +func EncodeI8(v int8) []byte { + w := NewWriter(1) + w.PutI8(v) + return w.Bytes() +} + +// DecodeI8 decodes an int8 from BSATN bytes. +func DecodeI8(data []byte) (int8, error) { + return NewReader(data).GetI8() +} + +// EncodeI16 encodes an int16 to BSATN bytes. +func EncodeI16(v int16) []byte { + w := NewWriter(2) + w.PutI16(v) + return w.Bytes() +} + +// DecodeI16 decodes an int16 from BSATN bytes. +func DecodeI16(data []byte) (int16, error) { + return NewReader(data).GetI16() +} + +// EncodeI32 encodes an int32 to BSATN bytes. +func EncodeI32(v int32) []byte { + w := NewWriter(4) + w.PutI32(v) + return w.Bytes() +} + +// DecodeI32 decodes an int32 from BSATN bytes. +func DecodeI32(data []byte) (int32, error) { + return NewReader(data).GetI32() +} + +// EncodeI64 encodes an int64 to BSATN bytes. +func EncodeI64(v int64) []byte { + w := NewWriter(8) + w.PutI64(v) + return w.Bytes() +} + +// DecodeI64 decodes an int64 from BSATN bytes. +func DecodeI64(data []byte) (int64, error) { + return NewReader(data).GetI64() +} + +// EncodeF32 encodes a float32 to BSATN bytes. +func EncodeF32(v float32) []byte { + w := NewWriter(4) + w.PutF32(v) + return w.Bytes() +} + +// DecodeF32 decodes a float32 from BSATN bytes. +func DecodeF32(data []byte) (float32, error) { + return NewReader(data).GetF32() +} + +// EncodeF64 encodes a float64 to BSATN bytes. +func EncodeF64(v float64) []byte { + w := NewWriter(8) + w.PutF64(v) + return w.Bytes() +} + +// DecodeF64 decodes a float64 from BSATN bytes. +func DecodeF64(data []byte) (float64, error) { + return NewReader(data).GetF64() +} + +// EncodeString encodes a string to BSATN bytes (u32 LE length prefix + UTF-8 bytes). +func EncodeString(v string) []byte { + w := NewWriter(4 + len(v)) + w.PutString(v) + return w.Bytes() +} + +// DecodeString decodes a string from BSATN bytes. +func DecodeString(data []byte) (string, error) { + return NewReader(data).GetString() +} diff --git a/sdks/go/bsatn/product.go b/sdks/go/bsatn/product.go new file mode 100644 index 00000000000..c7298bf9873 --- /dev/null +++ b/sdks/go/bsatn/product.go @@ -0,0 +1,14 @@ +package bsatn + +// Products are encoded as sequential fields with no length prefix. +// Each field is encoded according to its type, one after another. +// There is no special wrapper - product encoding is implicit in +// the WriteBsatn implementation of each struct type. +// +// Example: +// +// func (p *Person) WriteBsatn(w Writer) { +// w.PutU32(p.ID) +// w.PutString(p.Name) +// w.PutU8(p.Age) +// } diff --git a/sdks/go/bsatn/reader.go b/sdks/go/bsatn/reader.go new file mode 100644 index 00000000000..4358ded16f6 --- /dev/null +++ b/sdks/go/bsatn/reader.go @@ -0,0 +1,185 @@ +package bsatn + +import ( + "encoding/binary" + "math" + "unsafe" +) + +// NewReader creates a new BSATN reader from the given byte slice. +func NewReader(data []byte) Reader { + return &reader{data: data} +} + +type reader struct { + data []byte + pos int +} + +func (r *reader) readBytes(n int, forType string) ([]byte, error) { + remaining := len(r.data) - r.pos + if remaining < n { + return nil, &ErrBufferTooShort{ + ForType: forType, + Expected: n, + Given: remaining, + } + } + b := r.data[r.pos : r.pos+n] + r.pos += n + return b, nil +} + +func (r *reader) GetBool() (bool, error) { + b, err := r.readBytes(1, "bool") + if err != nil { + return false, err + } + switch b[0] { + case 0x00: + return false, nil + case 0x01: + return true, nil + default: + return false, &ErrInvalidBool{Value: b[0]} + } +} + +func (r *reader) GetU8() (uint8, error) { + b, err := r.readBytes(1, "u8") + if err != nil { + return 0, err + } + return b[0], nil +} + +func (r *reader) GetU16() (uint16, error) { + b, err := r.readBytes(2, "u16") + if err != nil { + return 0, err + } + return binary.LittleEndian.Uint16(b), nil +} + +func (r *reader) GetU32() (uint32, error) { + b, err := r.readBytes(4, "u32") + if err != nil { + return 0, err + } + return binary.LittleEndian.Uint32(b), nil +} + +func (r *reader) GetU64() (uint64, error) { + b, err := r.readBytes(8, "u64") + if err != nil { + return 0, err + } + return binary.LittleEndian.Uint64(b), nil +} + +func (r *reader) GetI8() (int8, error) { + b, err := r.readBytes(1, "i8") + if err != nil { + return 0, err + } + return int8(b[0]), nil +} + +func (r *reader) GetI16() (int16, error) { + v, err := r.GetU16() + if err != nil { + return 0, err + } + return int16(v), nil +} + +func (r *reader) GetI32() (int32, error) { + v, err := r.GetU32() + if err != nil { + return 0, err + } + return int32(v), nil +} + +func (r *reader) GetI64() (int64, error) { + v, err := r.GetU64() + if err != nil { + return 0, err + } + return int64(v), nil +} + +func (r *reader) GetF32() (float32, error) { + v, err := r.GetU32() + if err != nil { + return 0, err + } + return math.Float32frombits(v), nil +} + +func (r *reader) GetF64() (float64, error) { + v, err := r.GetU64() + if err != nil { + return 0, err + } + return math.Float64frombits(v), nil +} + +func (r *reader) GetString() (string, error) { + length, err := r.GetU32() + if err != nil { + return "", err + } + b, err := r.GetBytes(int(length)) + if err != nil { + return "", err + } + return string(b), nil +} + +func (r *reader) GetBytes(n int) ([]byte, error) { + return r.readBytes(n, "bytes") +} + +func (r *reader) GetArrayLen() (uint32, error) { + return r.GetU32() +} + +func (r *reader) GetMapLen() (uint32, error) { + return r.GetU32() +} + +func (r *reader) GetSumTag() (uint8, error) { + return r.GetU8() +} + +func (r *reader) Remaining() int { + return len(r.data) - r.pos +} + +// NewZeroCopyReader creates a Reader where GetString() returns strings +// that share the underlying buffer memory (no copy). +// SAFETY: The data buffer must outlive all strings decoded from it. +// As long as any decoded string is reachable, the GC keeps data alive. +func NewZeroCopyReader(data []byte) Reader { + return &zeroCopyReader{reader: reader{data: data}} +} + +type zeroCopyReader struct { + reader +} + +func (r *zeroCopyReader) GetString() (string, error) { + length, err := r.GetU32() + if err != nil { + return "", err + } + if length == 0 { + return "", nil + } + b, err := r.GetBytes(int(length)) + if err != nil { + return "", err + } + return unsafe.String(&b[0], len(b)), nil +} diff --git a/sdks/go/bsatn/sum.go b/sdks/go/bsatn/sum.go new file mode 100644 index 00000000000..c1a71582198 --- /dev/null +++ b/sdks/go/bsatn/sum.go @@ -0,0 +1,14 @@ +package bsatn + +// WriteSum writes a sum type value: u8 tag + payload. +func WriteSum(w Writer, tag uint8, payload Serializable) { + w.PutSumTag(tag) + if payload != nil { + payload.WriteBsatn(w) + } +} + +// WriteSumUnit writes a sum type with a unit (empty product) variant. +func WriteSumUnit(w Writer, tag uint8) { + w.PutSumTag(tag) +} diff --git a/sdks/go/bsatn/writer.go b/sdks/go/bsatn/writer.go new file mode 100644 index 00000000000..29ab173c1fb --- /dev/null +++ b/sdks/go/bsatn/writer.go @@ -0,0 +1,97 @@ +package bsatn + +import ( + "encoding/binary" + "math" +) + +// writer is the private BSATN writer implementation backed by a []byte buffer. +type writer struct { + buf []byte +} + +// NewWriter creates a new BSATN writer with an optional initial capacity. +func NewWriter(capacity int) Writer { + return &writer{buf: make([]byte, 0, capacity)} +} + +func (w *writer) PutBool(v bool) { + if v { + w.buf = append(w.buf, 0x01) + } else { + w.buf = append(w.buf, 0x00) + } +} + +func (w *writer) PutU8(v uint8) { + w.buf = append(w.buf, v) +} + +func (w *writer) PutU16(v uint16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, v) +} + +func (w *writer) PutU32(v uint32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, v) +} + +func (w *writer) PutU64(v uint64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, v) +} + +func (w *writer) PutI8(v int8) { + w.buf = append(w.buf, uint8(v)) +} + +func (w *writer) PutI16(v int16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, uint16(v)) +} + +func (w *writer) PutI32(v int32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, uint32(v)) +} + +func (w *writer) PutI64(v int64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, uint64(v)) +} + +func (w *writer) PutF32(v float32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, math.Float32bits(v)) +} + +func (w *writer) PutF64(v float64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, math.Float64bits(v)) +} + +func (w *writer) PutString(v string) { + w.PutU32(uint32(len(v))) + w.buf = append(w.buf, v...) +} + +func (w *writer) PutBytes(v []byte) { + w.buf = append(w.buf, v...) +} + +func (w *writer) PutArrayLen(n uint32) { + w.PutU32(n) +} + +func (w *writer) PutMapLen(n uint32) { + w.PutU32(n) +} + +func (w *writer) PutSumTag(tag uint8) { + w.PutU8(tag) +} + +func (w *writer) Bytes() []byte { + return w.buf +} + +func (w *writer) Reset() { + w.buf = w.buf[:0] +} + +func (w *writer) Len() int { + return len(w.buf) +} diff --git a/sdks/go/client/cache/cache.go b/sdks/go/client/cache/cache.go new file mode 100644 index 00000000000..337c77b41ec --- /dev/null +++ b/sdks/go/client/cache/cache.go @@ -0,0 +1,151 @@ +package cache + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/client/protocol" + "github.com/puzpuzpuz/xsync/v3" +) + +// ClientCache manages all table caches. +type ClientCache interface { + GetTable(name string) TableCache + RegisterTable(def TableDef) + ApplySubscribeApplied(rows *protocol.QueryRows) + ApplyTransactionUpdate(update *protocol.TransactionUpdate) +} + +func NewClientCache() ClientCache { + return &clientCache{ + tables: xsync.NewMapOf[string, *tableCache](), + } +} + +type clientCache struct { + tables *xsync.MapOf[string, *tableCache] +} + +func (cc *clientCache) GetTable(name string) TableCache { + tc, ok := cc.tables.Load(name) + if !ok { + return nil + } + return tc +} + +func (cc *clientCache) RegisterTable(def TableDef) { + cc.tables.Store(def.TableName(), newTableCache(def)) +} + +func (cc *clientCache) ApplySubscribeApplied(rows *protocol.QueryRows) { + if rows == nil { + return + } + for _, tableRows := range rows.Tables { + tc, ok := cc.tables.Load(tableRows.TableName) + if !ok { + continue + } + if tableRows.Rows == nil { + continue + } + for _, rowData := range tableRows.Rows.Rows() { + r := bsatn.NewReader(rowData) + row, err := tc.def.DecodeRow(r) + if err != nil { + continue + } + tc.applyInsert(rowData, row) + } + } +} + +func (cc *clientCache) ApplyTransactionUpdate(update *protocol.TransactionUpdate) { + if update == nil { + return + } + for _, qsUpdate := range update.QuerySets { + for _, tableUpdate := range qsUpdate.Tables { + tc, ok := cc.tables.Load(tableUpdate.TableName) + if !ok { + continue + } + for _, rows := range tableUpdate.Rows { + switch r := rows.(type) { + case *protocol.PersistentTableRows: + // Check if this table supports PK-based update detection + pkDef, hasPK := tc.def.(TableDefWithPK) + + if hasPK && r.Deletes != nil && r.Inserts != nil { + // Build map of deleted rows by PK + type deleteEntry struct { + rowBytes []byte + row any + } + deletedByPK := map[any]deleteEntry{} + for _, rowData := range r.Deletes.Rows() { + reader := bsatn.NewReader(rowData) + row, err := tc.def.DecodeRow(reader) + if err != nil { + continue + } + pk := pkDef.PrimaryKey(row) + deletedByPK[pk] = deleteEntry{rowData, row} + } + // Process inserts, detecting updates + for _, rowData := range r.Inserts.Rows() { + reader := bsatn.NewReader(rowData) + row, err := tc.def.DecodeRow(reader) + if err != nil { + continue + } + pk := pkDef.PrimaryKey(row) + if old, isUpdate := deletedByPK[pk]; isUpdate { + tc.applyUpdate(old.rowBytes, old.row, rowData, row) + delete(deletedByPK, pk) + } else { + tc.applyInsert(rowData, row) + } + } + // Remaining deletes are pure deletes + for _, old := range deletedByPK { + tc.applyDelete(old.rowBytes, old.row) + } + } else { + // No PK — fall through to existing delete-then-insert logic + if r.Deletes != nil { + for _, rowData := range r.Deletes.Rows() { + reader := bsatn.NewReader(rowData) + row, err := tc.def.DecodeRow(reader) + if err != nil { + continue + } + tc.applyDelete(rowData, row) + } + } + if r.Inserts != nil { + for _, rowData := range r.Inserts.Rows() { + reader := bsatn.NewReader(rowData) + row, err := tc.def.DecodeRow(reader) + if err != nil { + continue + } + tc.applyInsert(rowData, row) + } + } + } + case *protocol.EventTableRows: + if r.Events != nil { + for _, rowData := range r.Events.Rows() { + reader := bsatn.NewReader(rowData) + row, err := tc.def.DecodeRow(reader) + if err != nil { + continue + } + tc.applyInsert(rowData, row) + } + } + } + } + } + } +} diff --git a/sdks/go/client/cache/cache_test.go b/sdks/go/client/cache/cache_test.go new file mode 100644 index 00000000000..0c97d965cc2 --- /dev/null +++ b/sdks/go/client/cache/cache_test.go @@ -0,0 +1,699 @@ +package cache_test + +import ( + "sync/atomic" + "testing" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/client/cache" + "github.com/clockworklabs/SpacetimeDB/sdks/go/client/protocol" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// testRow is a simple row type for testing. +type testRow struct { + ID uint32 + Name string +} + +// testTableDef implements cache.TableDef for testing. +type testTableDef struct { + name string +} + +func (d *testTableDef) TableName() string { return d.name } + +func (d *testTableDef) DecodeRow(r bsatn.Reader) (any, error) { + id, err := r.GetU32() + if err != nil { + return nil, err + } + name, err := r.GetString() + if err != nil { + return nil, err + } + return &testRow{ID: id, Name: name}, nil +} + +func (d *testTableDef) EncodeRow(row any) []byte { + tr := row.(*testRow) + w := bsatn.NewWriter(32) + w.PutU32(tr.ID) + w.PutString(tr.Name) + return w.Bytes() +} + +func encodeTestRow(id uint32, name string) []byte { + w := bsatn.NewWriter(32) + w.PutU32(id) + w.PutString(name) + return w.Bytes() +} + +// --- ClientCache tests --- + +func TestClientCache_RegisterTable_GetTable(t *testing.T) { + cc := cache.NewClientCache() + + def := &testTableDef{name: "users"} + cc.RegisterTable(def) + + tc := cc.GetTable("users") + require.NotNil(t, tc, "GetTable should return a non-nil TableCache after RegisterTable") + + assert.Equal(t, 0, tc.Count(), "new table should have 0 rows") +} + +func TestClientCache_GetTable_NotFound(t *testing.T) { + cc := cache.NewClientCache() + + tc := cc.GetTable("nonexistent") + assert.Nil(t, tc, "GetTable should return nil for unregistered table") +} + +func TestClientCache_RegisterTable_MultipleTables(t *testing.T) { + cc := cache.NewClientCache() + + cc.RegisterTable(&testTableDef{name: "users"}) + cc.RegisterTable(&testTableDef{name: "items"}) + cc.RegisterTable(&testTableDef{name: "events"}) + + assert.NotNil(t, cc.GetTable("users")) + assert.NotNil(t, cc.GetTable("items")) + assert.NotNil(t, cc.GetTable("events")) + assert.Nil(t, cc.GetTable("other")) +} + +// --- ApplySubscribeApplied tests --- + +func TestClientCache_ApplySubscribeApplied(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "users"}) + + row1 := encodeTestRow(1, "alice") + row2 := encodeTestRow(2, "bobby") + allRows := append(row1, row2...) + + rows := &protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "users", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.RowOffsetsHint{Offsets: []uint64{0, uint64(len(row1))}}, + RowsData: allRows, + }, + }, + }, + } + + cc.ApplySubscribeApplied(rows) + + tc := cc.GetTable("users") + require.NotNil(t, tc) + assert.Equal(t, 2, tc.Count()) +} + +func TestClientCache_ApplySubscribeApplied_NilRows(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "users"}) + + // Should not panic + cc.ApplySubscribeApplied(nil) + + tc := cc.GetTable("users") + require.NotNil(t, tc) + assert.Equal(t, 0, tc.Count()) +} + +func TestClientCache_ApplySubscribeApplied_UnknownTable(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "users"}) + + rows := &protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "unknown_table", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: 4}, + RowsData: []byte{0x01, 0x02, 0x03, 0x04}, + }, + }, + }, + } + + // Should not panic on unknown table + cc.ApplySubscribeApplied(rows) + + tc := cc.GetTable("users") + require.NotNil(t, tc) + assert.Equal(t, 0, tc.Count(), "users table should still be empty") +} + +// --- ApplyTransactionUpdate tests --- + +func TestClientCache_ApplyTransactionUpdate_Insert(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "players"}) + + row1 := encodeTestRow(10, "charlie") + + update := &protocol.TransactionUpdate{ + QuerySets: []protocol.QuerySetUpdate{ + { + QuerySetID: 1, + Tables: []protocol.TableUpdate{ + { + TableName: "players", + Rows: []protocol.TableUpdateRows{ + &protocol.PersistentTableRows{ + Inserts: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row1))}, + RowsData: row1, + }, + Deletes: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row1))}, + RowsData: nil, + }, + }, + }, + }, + }, + }, + }, + } + + cc.ApplyTransactionUpdate(update) + + tc := cc.GetTable("players") + require.NotNil(t, tc) + assert.Equal(t, 1, tc.Count()) +} + +func TestClientCache_ApplyTransactionUpdate_Delete(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "players"}) + + row1 := encodeTestRow(10, "charlie") + + // First insert the row via subscribe + subscribeRows := &protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "players", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row1))}, + RowsData: row1, + }, + }, + }, + } + cc.ApplySubscribeApplied(subscribeRows) + + tc := cc.GetTable("players") + require.NotNil(t, tc) + require.Equal(t, 1, tc.Count()) + + // Now delete it via transaction update + update := &protocol.TransactionUpdate{ + QuerySets: []protocol.QuerySetUpdate{ + { + QuerySetID: 1, + Tables: []protocol.TableUpdate{ + { + TableName: "players", + Rows: []protocol.TableUpdateRows{ + &protocol.PersistentTableRows{ + Inserts: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row1))}, + RowsData: nil, + }, + Deletes: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row1))}, + RowsData: row1, + }, + }, + }, + }, + }, + }, + }, + } + + cc.ApplyTransactionUpdate(update) + assert.Equal(t, 0, tc.Count()) +} + +func TestClientCache_ApplyTransactionUpdate_Nil(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "players"}) + + // Should not panic + cc.ApplyTransactionUpdate(nil) +} + +// --- TableCache tests --- + +func TestTableCache_Count(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "items"}) + + row1 := encodeTestRow(1, "sword") + row2 := encodeTestRow(2, "shield") + allRows := append(row1, row2...) + + cc.ApplySubscribeApplied(&protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "items", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.RowOffsetsHint{Offsets: []uint64{0, uint64(len(row1))}}, + RowsData: allRows, + }, + }, + }, + }) + + tc := cc.GetTable("items") + require.NotNil(t, tc) + assert.Equal(t, 2, tc.Count()) +} + +func TestTableCache_Iter(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "items"}) + + row1 := encodeTestRow(1, "sword") + row2 := encodeTestRow(2, "shield") + allRows := append(row1, row2...) + + cc.ApplySubscribeApplied(&protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "items", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.RowOffsetsHint{Offsets: []uint64{0, uint64(len(row1))}}, + RowsData: allRows, + }, + }, + }, + }) + + tc := cc.GetTable("items") + require.NotNil(t, tc) + + var collected []*testRow + tc.Iter(func(row any) bool { + collected = append(collected, row.(*testRow)) + return true + }) + + assert.Len(t, collected, 2) + + // Verify we got both rows (order is not guaranteed) + ids := map[uint32]bool{} + for _, r := range collected { + ids[r.ID] = true + } + assert.True(t, ids[1], "should contain row with ID 1") + assert.True(t, ids[2], "should contain row with ID 2") +} + +func TestTableCache_Iter_EarlyStop(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "items"}) + + row1 := encodeTestRow(1, "sword") + row2 := encodeTestRow(2, "shield") + row3 := encodeTestRow(3, "potion") + allRows := append(append(row1, row2...), row3...) + + cc.ApplySubscribeApplied(&protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "items", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.RowOffsetsHint{Offsets: []uint64{ + 0, + uint64(len(row1)), + uint64(len(row1) + len(row2)), + }}, + RowsData: allRows, + }, + }, + }, + }) + + tc := cc.GetTable("items") + require.NotNil(t, tc) + + count := 0 + tc.Iter(func(row any) bool { + count++ + return false // stop after first + }) + + assert.Equal(t, 1, count, "iteration should stop after returning false") +} + +// --- Callback tests --- + +func TestTableCache_OnInsert_Callback(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "users"}) + + tc := cc.GetTable("users") + require.NotNil(t, tc) + + var insertedRow atomic.Value + tc.OnInsert(func(row any) { + insertedRow.Store(row) + }) + + row := encodeTestRow(1, "alice") + cc.ApplySubscribeApplied(&protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "users", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row))}, + RowsData: row, + }, + }, + }, + }) + + stored := insertedRow.Load() + require.NotNil(t, stored, "insert callback should have fired") + tr := stored.(*testRow) + assert.Equal(t, uint32(1), tr.ID) + assert.Equal(t, "alice", tr.Name) +} + +func TestTableCache_OnDelete_Callback(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "users"}) + + tc := cc.GetTable("users") + require.NotNil(t, tc) + + var deletedRow atomic.Value + tc.OnDelete(func(row any) { + deletedRow.Store(row) + }) + + row := encodeTestRow(1, "alice") + + // Insert via subscribe + cc.ApplySubscribeApplied(&protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "users", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row))}, + RowsData: row, + }, + }, + }, + }) + require.Equal(t, 1, tc.Count()) + + // Delete via transaction update + cc.ApplyTransactionUpdate(&protocol.TransactionUpdate{ + QuerySets: []protocol.QuerySetUpdate{ + { + QuerySetID: 1, + Tables: []protocol.TableUpdate{ + { + TableName: "users", + Rows: []protocol.TableUpdateRows{ + &protocol.PersistentTableRows{ + Inserts: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row))}, + RowsData: nil, + }, + Deletes: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row))}, + RowsData: row, + }, + }, + }, + }, + }, + }, + }, + }) + + stored := deletedRow.Load() + require.NotNil(t, stored, "delete callback should have fired") + tr := stored.(*testRow) + assert.Equal(t, uint32(1), tr.ID) + assert.Equal(t, "alice", tr.Name) + assert.Equal(t, 0, tc.Count()) +} + +func TestTableCache_RemoveCallback(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "users"}) + + tc := cc.GetTable("users") + require.NotNil(t, tc) + + var callCount atomic.Int32 + cbID := tc.OnInsert(func(row any) { + callCount.Add(1) + }) + + row := encodeTestRow(1, "alice") + + // First insert should fire callback + cc.ApplySubscribeApplied(&protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "users", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row))}, + RowsData: row, + }, + }, + }, + }) + assert.Equal(t, int32(1), callCount.Load()) + + // Remove callback + tc.RemoveCallback(cbID) + + // Second insert should NOT fire callback + row2 := encodeTestRow(2, "bob") + cc.ApplySubscribeApplied(&protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "users", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row2))}, + RowsData: row2, + }, + }, + }, + }) + assert.Equal(t, int32(1), callCount.Load(), "callback should not fire after removal") +} + +func TestTableCache_MultipleInsertCallbacks(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "users"}) + + tc := cc.GetTable("users") + require.NotNil(t, tc) + + var count1, count2 atomic.Int32 + tc.OnInsert(func(row any) { + count1.Add(1) + }) + tc.OnInsert(func(row any) { + count2.Add(1) + }) + + row := encodeTestRow(1, "alice") + cc.ApplySubscribeApplied(&protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "users", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row))}, + RowsData: row, + }, + }, + }, + }) + + assert.Equal(t, int32(1), count1.Load(), "first callback should fire") + assert.Equal(t, int32(1), count2.Load(), "second callback should fire") +} + +// --- Concurrent access tests --- + +func TestClientCache_ConcurrentInserts(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "users"}) + + tc := cc.GetTable("users") + require.NotNil(t, tc) + + var insertCount atomic.Int32 + tc.OnInsert(func(row any) { + insertCount.Add(1) + }) + + // Insert rows concurrently from multiple goroutines + const numGoroutines = 10 + const rowsPerGoroutine = 50 + done := make(chan struct{}, numGoroutines) + + for g := 0; g < numGoroutines; g++ { + go func(goroutineID int) { + defer func() { done <- struct{}{} }() + for i := 0; i < rowsPerGoroutine; i++ { + id := uint32(goroutineID*rowsPerGoroutine + i) + row := encodeTestRow(id, "user") + cc.ApplySubscribeApplied(&protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "users", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row))}, + RowsData: row, + }, + }, + }, + }) + } + }(g) + } + + // Wait for all goroutines + for i := 0; i < numGoroutines; i++ { + <-done + } + + // All rows with name "user" have the same length, but different IDs, + // so each produces unique bytes and should be stored separately. + assert.Equal(t, numGoroutines*rowsPerGoroutine, tc.Count()) + assert.Equal(t, int32(numGoroutines*rowsPerGoroutine), insertCount.Load()) +} + +func TestClientCache_ConcurrentRegisterAndGet(t *testing.T) { + cc := cache.NewClientCache() + + const numTables = 50 + done := make(chan struct{}, numTables*2) + + // Register tables concurrently + for i := 0; i < numTables; i++ { + go func(idx int) { + defer func() { done <- struct{}{} }() + name := "table_" + string(rune('A'+idx%26)) + string(rune('0'+idx/26)) + cc.RegisterTable(&testTableDef{name: name}) + }(i) + } + + // Simultaneously try to get tables + for i := 0; i < numTables; i++ { + go func(idx int) { + defer func() { done <- struct{}{} }() + name := "table_" + string(rune('A'+idx%26)) + string(rune('0'+idx/26)) + // May or may not find it depending on goroutine scheduling + _ = cc.GetTable(name) + }(i) + } + + // Wait for all + for i := 0; i < numTables*2; i++ { + <-done + } + + // After all goroutines finish, all tables should be registered + for i := 0; i < numTables; i++ { + name := "table_" + string(rune('A'+i%26)) + string(rune('0'+i/26)) + assert.NotNil(t, cc.GetTable(name), "table %s should be registered", name) + } +} + +func TestTableCache_ConcurrentCallbackRegistration(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "items"}) + + tc := cc.GetTable("items") + require.NotNil(t, tc) + + const numCallbacks = 100 + var totalInserts atomic.Int32 + done := make(chan struct{}, numCallbacks) + + // Register callbacks concurrently + for i := 0; i < numCallbacks; i++ { + go func() { + defer func() { done <- struct{}{} }() + tc.OnInsert(func(row any) { + totalInserts.Add(1) + }) + }() + } + + for i := 0; i < numCallbacks; i++ { + <-done + } + + // Insert one row -- all callbacks should fire + row := encodeTestRow(1, "sword") + cc.ApplySubscribeApplied(&protocol.QueryRows{ + Tables: []protocol.SingleTableRows{ + { + TableName: "items", + Rows: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row))}, + RowsData: row, + }, + }, + }, + }) + + assert.Equal(t, int32(numCallbacks), totalInserts.Load(), + "all %d callbacks should have fired", numCallbacks) +} + +// --- EventTableRows via TransactionUpdate --- + +func TestClientCache_ApplyTransactionUpdate_EventTableRows(t *testing.T) { + cc := cache.NewClientCache() + cc.RegisterTable(&testTableDef{name: "events"}) + + row1 := encodeTestRow(100, "event_a") + + update := &protocol.TransactionUpdate{ + QuerySets: []protocol.QuerySetUpdate{ + { + QuerySetID: 1, + Tables: []protocol.TableUpdate{ + { + TableName: "events", + Rows: []protocol.TableUpdateRows{ + &protocol.EventTableRows{ + Events: &protocol.BsatnRowList{ + SizeHint: protocol.FixedSizeHint{RowSize: uint16(len(row1))}, + RowsData: row1, + }, + }, + }, + }, + }, + }, + }, + } + + cc.ApplyTransactionUpdate(update) + + tc := cc.GetTable("events") + require.NotNil(t, tc) + assert.Equal(t, 1, tc.Count()) +} diff --git a/sdks/go/client/cache/interfaces.go b/sdks/go/client/cache/interfaces.go new file mode 100644 index 00000000000..ac0ac5be90e --- /dev/null +++ b/sdks/go/client/cache/interfaces.go @@ -0,0 +1,38 @@ +package cache + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// CallbackID identifies a registered callback. +type CallbackID uint64 + +// RowDecoder can decode a BSATN row. +type RowDecoder func(r bsatn.Reader) (any, error) + +// RowEncoder can encode a row to BSATN bytes for use as a cache key. +type RowEncoder func(row any) []byte + +// TableDef defines a table for registration with the cache. +type TableDef interface { + TableName() string + DecodeRow(r bsatn.Reader) (any, error) + EncodeRow(row any) []byte +} + +// InsertCallback is called when a row is inserted. +type InsertCallback func(row any) + +// DeleteCallback is called when a row is deleted. +type DeleteCallback func(row any) + +// UpdateCallback is called when a row is updated (old, new). +type UpdateCallback func(oldRow any, newRow any) + +// TableDefWithPK extends TableDef for tables that have a primary key. +// Tables implementing this interface support OnUpdate callbacks by +// matching deletes and inserts by primary key within a single transaction. +type TableDefWithPK interface { + TableDef + PrimaryKey(row any) any +} diff --git a/sdks/go/client/cache/table_cache.go b/sdks/go/client/cache/table_cache.go new file mode 100644 index 00000000000..58d33366dd5 --- /dev/null +++ b/sdks/go/client/cache/table_cache.go @@ -0,0 +1,101 @@ +package cache + +import ( + "sync/atomic" + + "github.com/puzpuzpuz/xsync/v3" +) + +// TableCache stores rows for a single table. +type TableCache interface { + Count() int + Iter(fn func(row any) bool) + OnInsert(cb InsertCallback) CallbackID + OnDelete(cb DeleteCallback) CallbackID + OnUpdate(cb UpdateCallback) CallbackID + RemoveCallback(id CallbackID) +} + +// newTableCache creates a new table cache. +func newTableCache(def TableDef) *tableCache { + return &tableCache{ + def: def, + rows: xsync.NewMapOf[string, any](), + insertCallbacks: xsync.NewMapOf[CallbackID, InsertCallback](), + deleteCallbacks: xsync.NewMapOf[CallbackID, DeleteCallback](), + updateCallbacks: xsync.NewMapOf[CallbackID, UpdateCallback](), + } +} + +type tableCache struct { + def TableDef + rows *xsync.MapOf[string, any] + insertCallbacks *xsync.MapOf[CallbackID, InsertCallback] + deleteCallbacks *xsync.MapOf[CallbackID, DeleteCallback] + updateCallbacks *xsync.MapOf[CallbackID, UpdateCallback] + nextCallbackID atomic.Uint64 +} + +func (tc *tableCache) Count() int { + return tc.rows.Size() +} + +func (tc *tableCache) Iter(fn func(row any) bool) { + tc.rows.Range(func(key string, value any) bool { + return fn(value) + }) +} + +func (tc *tableCache) OnInsert(cb InsertCallback) CallbackID { + id := CallbackID(tc.nextCallbackID.Add(1)) + tc.insertCallbacks.Store(id, cb) + return id +} + +func (tc *tableCache) OnDelete(cb DeleteCallback) CallbackID { + id := CallbackID(tc.nextCallbackID.Add(1)) + tc.deleteCallbacks.Store(id, cb) + return id +} + +func (tc *tableCache) OnUpdate(cb UpdateCallback) CallbackID { + id := CallbackID(tc.nextCallbackID.Add(1)) + tc.updateCallbacks.Store(id, cb) + return id +} + +func (tc *tableCache) RemoveCallback(id CallbackID) { + tc.insertCallbacks.Delete(id) + tc.deleteCallbacks.Delete(id) + tc.updateCallbacks.Delete(id) +} + +// applyInsert stores a row and fires insert callbacks. +func (tc *tableCache) applyInsert(rowBytes []byte, row any) { + key := string(rowBytes) + tc.rows.Store(key, row) + tc.insertCallbacks.Range(func(_ CallbackID, cb InsertCallback) bool { + cb(row) + return true + }) +} + +// applyDelete removes a row and fires delete callbacks. +func (tc *tableCache) applyDelete(rowBytes []byte, row any) { + key := string(rowBytes) + tc.rows.Delete(key) + tc.deleteCallbacks.Range(func(_ CallbackID, cb DeleteCallback) bool { + cb(row) + return true + }) +} + +// applyUpdate removes old row, stores new row, and fires update callbacks. +func (tc *tableCache) applyUpdate(oldRowBytes []byte, oldRow any, newRowBytes []byte, newRow any) { + tc.rows.Delete(string(oldRowBytes)) + tc.rows.Store(string(newRowBytes), newRow) + tc.updateCallbacks.Range(func(_ CallbackID, cb UpdateCallback) bool { + cb(oldRow, newRow) + return true + }) +} diff --git a/sdks/go/client/client_test.go b/sdks/go/client/client_test.go new file mode 100644 index 00000000000..731faa51050 --- /dev/null +++ b/sdks/go/client/client_test.go @@ -0,0 +1,275 @@ +package client_test + +import ( + "context" + "errors" + "testing" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/client" + "github.com/clockworklabs/SpacetimeDB/sdks/go/client/protocol" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --- Error types tests --- + +func TestConnectionError_Error(t *testing.T) { + err := &client.ConnectionError{ + Message: "failed to connect", + Err: errors.New("dial timeout"), + } + + assert.Contains(t, err.Error(), "connection error") + assert.Contains(t, err.Error(), "failed to connect") + assert.Contains(t, err.Error(), "dial timeout") +} + +func TestConnectionError_ErrorWithoutWrapped(t *testing.T) { + err := &client.ConnectionError{ + Message: "server unreachable", + } + + assert.Contains(t, err.Error(), "connection error") + assert.Contains(t, err.Error(), "server unreachable") + assert.Nil(t, err.Unwrap()) +} + +func TestConnectionError_Unwrap(t *testing.T) { + inner := errors.New("underlying error") + err := &client.ConnectionError{ + Message: "test", + Err: inner, + } + + assert.Equal(t, inner, err.Unwrap()) + assert.True(t, errors.Is(err, inner)) +} + +func TestConnectionError_ImplementsError(t *testing.T) { + var _ error = (*client.ConnectionError)(nil) +} + +func TestReducerError_Error(t *testing.T) { + err := &client.ReducerError{ + ReducerName: "add_user", + Message: "unique constraint violated", + } + + assert.Contains(t, err.Error(), "reducer") + assert.Contains(t, err.Error(), "add_user") + assert.Contains(t, err.Error(), "unique constraint violated") +} + +func TestReducerError_ImplementsError(t *testing.T) { + var _ error = (*client.ReducerError)(nil) +} + +func TestSubscriptionError_Error(t *testing.T) { + err := &client.SubscriptionError{ + QuerySetID: 42, + Message: "table not found", + } + + assert.Contains(t, err.Error(), "subscription") + assert.Contains(t, err.Error(), "42") + assert.Contains(t, err.Error(), "table not found") +} + +func TestSubscriptionError_ImplementsError(t *testing.T) { + var _ error = (*client.SubscriptionError)(nil) +} + +func TestProtocolError_Error(t *testing.T) { + err := &client.ProtocolError{ + Message: "invalid tag", + Err: errors.New("tag 99"), + } + + assert.Contains(t, err.Error(), "protocol error") + assert.Contains(t, err.Error(), "invalid tag") + assert.Contains(t, err.Error(), "tag 99") +} + +func TestProtocolError_ErrorWithoutWrapped(t *testing.T) { + err := &client.ProtocolError{ + Message: "bad data", + } + + assert.Contains(t, err.Error(), "protocol error") + assert.Contains(t, err.Error(), "bad data") + assert.Nil(t, err.Unwrap()) +} + +func TestProtocolError_Unwrap(t *testing.T) { + inner := errors.New("decode failed") + err := &client.ProtocolError{ + Message: "test", + Err: inner, + } + + assert.Equal(t, inner, err.Unwrap()) + assert.True(t, errors.Is(err, inner)) +} + +func TestProtocolError_ImplementsError(t *testing.T) { + var _ error = (*client.ProtocolError)(nil) +} + +// --- DbConnectionBuilder tests --- + +func TestNewDbConnection_ReturnsBuilder(t *testing.T) { + builder := client.NewDbConnection() + require.NotNil(t, builder) +} + +func TestDbConnectionBuilder_Chaining(t *testing.T) { + var connectCalled bool + var errorCalled bool + var disconnectCalled bool + + builder := client.NewDbConnection(). + WithUri("ws://localhost:3000"). + WithDatabaseName("test-db"). + WithToken("my-token"). + OnConnect(func(conn client.DbConnection, identity types.Identity, token string) { + connectCalled = true + }). + OnConnectError(func(err error) { + errorCalled = true + }). + OnDisconnect(func(conn client.DbConnection, err error) { + disconnectCalled = true + }) + + require.NotNil(t, builder, "builder should not be nil after chaining") + + // We cannot call Build without a real server, but we verify the builder is valid + // and chaining works. The callbacks aren't called yet. + assert.False(t, connectCalled) + assert.False(t, errorCalled) + assert.False(t, disconnectCalled) +} + +func TestDbConnectionBuilder_WithCompression(t *testing.T) { + builder := client.NewDbConnection(). + WithUri("ws://localhost:3000"). + WithDatabaseName("test-db"). + WithCompression(protocol.CompressionBrotli) + + require.NotNil(t, builder, "builder should not be nil after WithCompression") +} + +func TestDbConnectionBuilder_BuildFailsWithBadUri(t *testing.T) { + var errorCalled bool + var capturedErr error + + builder := client.NewDbConnection(). + WithUri("ws://localhost:99999-invalid"). + WithDatabaseName("nonexistent-db"). + OnConnectError(func(err error) { + errorCalled = true + capturedErr = err + }) + + ctx := context.Background() + conn, err := builder.Build(ctx) + + // Build should fail because there's no server + require.Error(t, err) + assert.Nil(t, conn) + assert.True(t, errorCalled, "OnConnectError should have been called") + assert.NotNil(t, capturedErr) + + // The returned error should be a ConnectionError + var connErr *client.ConnectionError + assert.True(t, errors.As(err, &connErr), "error should be a *ConnectionError") + assert.Contains(t, connErr.Error(), "connection error") +} + +func TestDbConnectionBuilder_BuildFailsNoOnConnectError(t *testing.T) { + // Build without OnConnectError handler -- should still return an error + builder := client.NewDbConnection(). + WithUri("ws://localhost:99999-invalid"). + WithDatabaseName("nonexistent-db") + + ctx := context.Background() + conn, err := builder.Build(ctx) + + require.Error(t, err) + assert.Nil(t, conn) +} + +// --- EventContext tests --- + +func TestEventContext_Fields(t *testing.T) { + identity := types.NewIdentity([32]byte{0x01}) + connID := types.NewConnectionId([16]byte{0x02}) + ts := types.NewTimestamp(1234567890) + + ctx := client.EventContext{ + Identity: identity, + ConnectionID: connID, + Timestamp: ts, + Conn: nil, // we don't have a real connection in unit tests + } + + assert.Equal(t, identity, ctx.Identity) + assert.Equal(t, connID, ctx.ConnectionID) + assert.Equal(t, ts, ctx.Timestamp) + assert.Nil(t, ctx.Conn) +} + +func TestReducerEventContext_Fields(t *testing.T) { + identity := types.NewIdentity([32]byte{0x01}) + connID := types.NewConnectionId([16]byte{0x02}) + ts := types.NewTimestamp(5000) + + ctx := client.ReducerEventContext{ + EventContext: client.EventContext{ + Identity: identity, + ConnectionID: connID, + Timestamp: ts, + }, + ReducerName: "add_player", + Status: "committed", + ErrMessage: "", + } + + assert.Equal(t, identity, ctx.Identity) + assert.Equal(t, "add_player", ctx.ReducerName) + assert.Equal(t, "committed", ctx.Status) + assert.Empty(t, ctx.ErrMessage) +} + +func TestReducerEventContext_WithError(t *testing.T) { + ctx := client.ReducerEventContext{ + ReducerName: "delete_user", + Status: "failed", + ErrMessage: "user not found", + } + + assert.Equal(t, "delete_user", ctx.ReducerName) + assert.Equal(t, "failed", ctx.Status) + assert.Equal(t, "user not found", ctx.ErrMessage) +} + +func TestErrorContext_Fields(t *testing.T) { + inner := errors.New("something went wrong") + ctx := client.ErrorContext{Err: inner} + + assert.Equal(t, inner, ctx.Err) +} + +// --- SubscriptionBuilder/Handle interface tests --- + +func TestSubscriptionBuilder_Interface(t *testing.T) { + // Verify the interface shapes exist and are accessible + var _ client.SubscriptionBuilder + var _ client.SubscriptionHandle +} + +func TestCallbackID_Type(t *testing.T) { + var id client.CallbackID = 42 + assert.Equal(t, client.CallbackID(42), id) +} diff --git a/sdks/go/client/db_connection.go b/sdks/go/client/db_connection.go new file mode 100644 index 00000000000..b0704612d9c --- /dev/null +++ b/sdks/go/client/db_connection.go @@ -0,0 +1,481 @@ +package client + +import ( + "context" + "sync/atomic" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/client/cache" + "github.com/clockworklabs/SpacetimeDB/sdks/go/client/protocol" + "github.com/clockworklabs/SpacetimeDB/sdks/go/client/ws" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// DbConnectionBuilder builds a DbConnection. +type DbConnectionBuilder interface { + WithUri(uri string) DbConnectionBuilder + WithDatabaseName(nameOrAddress string) DbConnectionBuilder + WithToken(token string) DbConnectionBuilder + WithCompression(c protocol.Compression) DbConnectionBuilder + OnConnect(fn func(conn DbConnection, identity types.Identity, token string)) DbConnectionBuilder + OnConnectError(fn func(err error)) DbConnectionBuilder + OnDisconnect(fn func(conn DbConnection, err error)) DbConnectionBuilder + Build(ctx context.Context) (DbConnection, error) +} + +// DbConnection is the primary interface for interacting with a SpacetimeDB database. +type DbConnection interface { + Identity() types.Identity + ConnectionId() types.ConnectionId + Token() string + IsActive() bool + Subscribe(queries ...string) SubscriptionBuilder + CallReducer(reducer string, args bsatn.Serializable) error + OneOffQuery(query string) ([][]byte, error) + Disconnect() error + Run(ctx context.Context) error + RegisterTable(def cache.TableDef) + Cache() cache.ClientCache +} + +// NewDbConnection returns a new DbConnectionBuilder. +func NewDbConnection() DbConnectionBuilder { + return &dbConnectionBuilder{ + compression: protocol.CompressionNone, + } +} + +type dbConnectionBuilder struct { + uri string + database string + token string + compression protocol.Compression + onConnect func(DbConnection, types.Identity, string) + onConnectError func(error) + onDisconnect func(DbConnection, error) +} + +func (b *dbConnectionBuilder) WithUri(uri string) DbConnectionBuilder { + b.uri = uri + return b +} + +func (b *dbConnectionBuilder) WithDatabaseName(name string) DbConnectionBuilder { + b.database = name + return b +} + +func (b *dbConnectionBuilder) WithToken(token string) DbConnectionBuilder { + b.token = token + return b +} + +func (b *dbConnectionBuilder) WithCompression(c protocol.Compression) DbConnectionBuilder { + b.compression = c + return b +} + +func (b *dbConnectionBuilder) OnConnect(fn func(DbConnection, types.Identity, string)) DbConnectionBuilder { + b.onConnect = fn + return b +} + +func (b *dbConnectionBuilder) OnConnectError(fn func(error)) DbConnectionBuilder { + b.onConnectError = fn + return b +} + +func (b *dbConnectionBuilder) OnDisconnect(fn func(DbConnection, error)) DbConnectionBuilder { + b.onDisconnect = fn + return b +} + +func (b *dbConnectionBuilder) Build(ctx context.Context) (DbConnection, error) { + wsConn, err := ws.NewConnection(). + WithUri(b.uri). + WithDatabaseName(b.database). + WithToken(b.token). + Build(ctx) + if err != nil { + if b.onConnectError != nil { + b.onConnectError(err) + } + return nil, &ConnectionError{Message: "failed to connect", Err: err} + } + + conn := &dbConnection{ + ws: wsConn, + cache: cache.NewClientCache(), + commands: make(chan *command, 256), + incoming: make(chan []byte, 64), + compression: b.compression, + onConnect: b.onConnect, + onConnectError: b.onConnectError, + onDisconnect: b.onDisconnect, + } + + return conn, nil +} + +// command types for the channel-based event loop. +type commandType int + +const ( + cmdCallReducer commandType = iota + cmdSubscribe + cmdUnsubscribe + cmdOneOffQuery + cmdDisconnect +) + +type command struct { + typ commandType + reducer string + args []byte + queries []string + querySetID uint32 + onApplied func() + result chan any +} + +// oneOffResult is the internal result passed back through the one-off query result channel. +type oneOffResult struct { + rows [][]byte + err error +} + +type dbConnection struct { + ws ws.Connection + cache cache.ClientCache + commands chan *command + incoming chan []byte + compression protocol.Compression + + identity types.Identity + connectionID types.ConnectionId + token string + + active atomic.Bool + nextRequestID atomic.Uint32 + nextQuerySetID atomic.Uint32 + + onConnect func(DbConnection, types.Identity, string) + onConnectError func(error) + onDisconnect func(DbConnection, error) + + // State maps only accessed from the Run() goroutine event loop. + subscriptionCallbacks map[uint32]func() + reducerCallbacks map[uint32]func(protocol.ServerMessage) + oneOffCallbacks map[uint32]chan any +} + +func (c *dbConnection) Identity() types.Identity { return c.identity } +func (c *dbConnection) ConnectionId() types.ConnectionId { return c.connectionID } +func (c *dbConnection) Token() string { return c.token } +func (c *dbConnection) IsActive() bool { return c.active.Load() } +func (c *dbConnection) Cache() cache.ClientCache { return c.cache } + +func (c *dbConnection) RegisterTable(def cache.TableDef) { + c.cache.RegisterTable(def) +} + +func (c *dbConnection) CallReducer(reducer string, args bsatn.Serializable) error { + var encoded []byte + if args != nil { + w := bsatn.NewWriter(64) + args.WriteBsatn(w) + encoded = w.Bytes() + } + c.commands <- &command{ + typ: cmdCallReducer, + reducer: reducer, + args: encoded, + } + return nil +} + +func (c *dbConnection) OneOffQuery(query string) ([][]byte, error) { + result := make(chan any, 1) + c.commands <- &command{ + typ: cmdOneOffQuery, + queries: []string{query}, + result: result, + } + resp := <-result + switch r := resp.(type) { + case oneOffResult: + return r.rows, r.err + default: + return nil, &ProtocolError{Message: "unexpected one-off query result type"} + } +} + +func (c *dbConnection) Subscribe(queries ...string) SubscriptionBuilder { + return &subscriptionBuilder{ + conn: c, + queries: queries, + } +} + +func (c *dbConnection) Disconnect() error { + c.commands <- &command{typ: cmdDisconnect} + return nil +} + +// Run is the single-goroutine event loop. All state access on +// subscriptionCallbacks and reducerCallbacks happens here. +func (c *dbConnection) Run(ctx context.Context) error { + c.active.Store(true) + defer c.active.Store(false) + + c.subscriptionCallbacks = make(map[uint32]func()) + c.reducerCallbacks = make(map[uint32]func(protocol.ServerMessage)) + c.oneOffCallbacks = make(map[uint32]chan any) + + // readCtx is derived from context.Background() rather than the caller's + // ctx so that context cancellation does not kill the TCP connection before + // the WebSocket close handshake can complete. Close() sends the close + // frame through the still-active reader, then internally closes the TCP + // connection, which causes Read() to return and readLoop to exit. + readCtx, readCancel := context.WithCancel(context.Background()) + defer readCancel() + + readDone := make(chan struct{}) + go func() { + defer close(readDone) + c.readLoop(readCtx) + }() + + // shutdown performs a clean WebSocket close handshake and waits for + // readLoop to exit, preventing goroutine leaks. + shutdown := func(disconnectErr error) { + c.ws.Close() // Sends close frame, waits for peer response + readCancel() // Belt-and-suspenders: cancel readCtx + <-readDone // Wait for readLoop goroutine to exit + if c.onDisconnect != nil { + c.onDisconnect(c, disconnectErr) + } + } + + for { + select { + case <-ctx.Done(): + shutdown(ctx.Err()) + return ctx.Err() + + case cmd := <-c.commands: + if err := c.handleCommand(ctx, cmd); err != nil { + return err + } + if cmd.typ == cmdDisconnect { + shutdown(nil) + return nil + } + + case msg := <-c.incoming: + c.handleIncoming(msg) + + case <-readDone: + // readLoop exited unexpectedly (server closed, network error). + if c.onDisconnect != nil { + c.onDisconnect(c, &ConnectionError{Message: "connection lost"}) + } + return &ConnectionError{Message: "connection lost"} + } + } +} + +func (c *dbConnection) readLoop(ctx context.Context) { + for { + data, err := c.ws.Recv(ctx) + if err != nil { + return + } + c.incoming <- data + } +} + +func (c *dbConnection) handleCommand(ctx context.Context, cmd *command) error { + switch cmd.typ { + case cmdCallReducer: + reqID := c.nextRequestID.Add(1) + msg := &protocol.CallReducer{ + RequestID: reqID, + Flags: 0, + Reducer: cmd.reducer, + Args: cmd.args, + } + return c.sendMessage(ctx, msg) + + case cmdSubscribe: + if cmd.onApplied != nil { + c.subscriptionCallbacks[cmd.querySetID] = cmd.onApplied + } + reqID := c.nextRequestID.Add(1) + msg := &protocol.Subscribe{ + RequestID: reqID, + QuerySetID: cmd.querySetID, + QueryStrings: cmd.queries, + } + return c.sendMessage(ctx, msg) + + case cmdUnsubscribe: + reqID := c.nextRequestID.Add(1) + msg := &protocol.Unsubscribe{ + RequestID: reqID, + QuerySetID: cmd.querySetID, + } + return c.sendMessage(ctx, msg) + + case cmdOneOffQuery: + reqID := c.nextRequestID.Add(1) + if cmd.result != nil { + c.oneOffCallbacks[reqID] = cmd.result + } + msg := &protocol.OneOffQuery{ + RequestID: reqID, + QueryString: cmd.queries[0], + } + return c.sendMessage(ctx, msg) + + case cmdDisconnect: + // Close and cleanup handled by shutdown() in Run(). + return nil + } + return nil +} + +func (c *dbConnection) sendMessage(ctx context.Context, msg protocol.ClientMessage) error { + data := bsatn.Encode(msg) + return c.ws.Send(ctx, data) +} + +func (c *dbConnection) handleIncoming(data []byte) { + decompressed, err := protocol.DecompressMessage(data) + if err != nil { + return + } + + r := bsatn.NewReader(decompressed) + msg, err := protocol.ReadServerMessage(r) + if err != nil { + return + } + + switch m := msg.(type) { + case *protocol.InitialConnection: + c.identity = m.Identity + c.connectionID = m.ConnectionID + c.token = m.Token + if c.onConnect != nil { + c.onConnect(c, m.Identity, m.Token) + } + + case *protocol.SubscribeApplied: + c.cache.ApplySubscribeApplied(&m.Rows) + if cb, ok := c.subscriptionCallbacks[m.QuerySetID]; ok { + cb() + } + + case *protocol.TransactionUpdate: + c.cache.ApplyTransactionUpdate(m) + + case *protocol.SubscriptionError: + // Log or report subscription error; for now, remove the callback. + delete(c.subscriptionCallbacks, m.QuerySetID) + + case *protocol.OneOffQueryResult: + if ch, ok := c.oneOffCallbacks[m.RequestID]; ok { + if m.ResultErr != "" { + ch <- oneOffResult{err: &ProtocolError{Message: m.ResultErr}} + } else { + var rows [][]byte + if m.ResultOk != nil { + for _, tableRows := range m.ResultOk.Tables { + if tableRows.Rows != nil { + rows = append(rows, tableRows.Rows.Rows()...) + } + } + } + ch <- oneOffResult{rows: rows} + } + delete(c.oneOffCallbacks, m.RequestID) + } + + case *protocol.ReducerResult: + if cb, ok := c.reducerCallbacks[m.RequestID]; ok { + cb(msg) + delete(c.reducerCallbacks, m.RequestID) + } + // Apply transaction update from successful reducer outcomes. + switch outcome := m.Result.(type) { + case *protocol.ReducerOk: + if outcome.TransactionUpdate != nil { + c.cache.ApplyTransactionUpdate(outcome.TransactionUpdate) + } + } + } +} + +// subscriptionBuilder implements SubscriptionBuilder. +type subscriptionBuilder struct { + conn *dbConnection + queries []string + onApplied func() +} + +func (sb *subscriptionBuilder) OnApplied(fn func()) SubscriptionBuilder { + sb.onApplied = fn + return sb +} + +func (sb *subscriptionBuilder) Build() (SubscriptionHandle, error) { + qsID := sb.conn.nextQuerySetID.Add(1) + + // Send the subscribe command with the onApplied callback so the Run() + // event loop can register it in its own goroutine (no data races). + sb.conn.commands <- &command{ + typ: cmdSubscribe, + queries: sb.queries, + querySetID: qsID, + onApplied: sb.onApplied, + } + + return &subscriptionHandle{ + conn: sb.conn, + querySetID: qsID, + active: true, + }, nil +} + +// subscriptionHandle implements SubscriptionHandle. +type subscriptionHandle struct { + conn *dbConnection + querySetID uint32 + active bool +} + +func (sh *subscriptionHandle) Unsubscribe() error { + sh.active = false + sh.conn.commands <- &command{ + typ: cmdUnsubscribe, + querySetID: sh.querySetID, + } + return nil +} + +func (sh *subscriptionHandle) IsActive() bool { + return sh.active +} + +// Ensure interfaces are satisfied at compile time. +var ( + _ DbConnectionBuilder = (*dbConnectionBuilder)(nil) + _ DbConnection = (*dbConnection)(nil) + _ SubscriptionBuilder = (*subscriptionBuilder)(nil) + _ SubscriptionHandle = (*subscriptionHandle)(nil) + _ error = (*ConnectionError)(nil) + _ error = (*ReducerError)(nil) + _ error = (*SubscriptionError)(nil) + _ error = (*ProtocolError)(nil) +) diff --git a/sdks/go/client/errors.go b/sdks/go/client/errors.go new file mode 100644 index 00000000000..69c1cadf156 --- /dev/null +++ b/sdks/go/client/errors.go @@ -0,0 +1,53 @@ +package client + +import "fmt" + +// ConnectionError represents a WebSocket connection failure. +type ConnectionError struct { + Message string + Err error +} + +func (e *ConnectionError) Error() string { + if e.Err != nil { + return fmt.Sprintf("connection error: %s: %v", e.Message, e.Err) + } + return fmt.Sprintf("connection error: %s", e.Message) +} + +func (e *ConnectionError) Unwrap() error { return e.Err } + +// ReducerError represents a reducer invocation failure. +type ReducerError struct { + ReducerName string + Message string +} + +func (e *ReducerError) Error() string { + return fmt.Sprintf("reducer %s error: %s", e.ReducerName, e.Message) +} + +// SubscriptionError represents a subscription failure. +type SubscriptionError struct { + QuerySetID uint32 + Message string +} + +func (e *SubscriptionError) Error() string { + return fmt.Sprintf("subscription %d error: %s", e.QuerySetID, e.Message) +} + +// ProtocolError represents a BSATN protocol decoding or encoding failure. +type ProtocolError struct { + Message string + Err error +} + +func (e *ProtocolError) Error() string { + if e.Err != nil { + return fmt.Sprintf("protocol error: %s: %v", e.Message, e.Err) + } + return fmt.Sprintf("protocol error: %s", e.Message) +} + +func (e *ProtocolError) Unwrap() error { return e.Err } diff --git a/sdks/go/client/events.go b/sdks/go/client/events.go new file mode 100644 index 00000000000..96334ceba1e --- /dev/null +++ b/sdks/go/client/events.go @@ -0,0 +1,24 @@ +package client + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/types" + +// EventContext is passed to table insert/delete callbacks. +type EventContext struct { + Identity types.Identity + ConnectionID types.ConnectionId + Timestamp types.Timestamp + Conn DbConnection +} + +// ReducerEventContext is passed to reducer result callbacks. +type ReducerEventContext struct { + EventContext + ReducerName string + Status string + ErrMessage string +} + +// ErrorContext is passed to error callbacks. +type ErrorContext struct { + Err error +} diff --git a/sdks/go/client/protocol/client_message.go b/sdks/go/client/protocol/client_message.go new file mode 100644 index 00000000000..551fbd4c92b --- /dev/null +++ b/sdks/go/client/protocol/client_message.go @@ -0,0 +1,117 @@ +package protocol + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + +// ClientMessage represents a message sent from client to server. +// It is a BSATN sum type with variants for each message kind. +type ClientMessage interface { + bsatn.Serializable + clientMessageTag() uint8 +} + +// Subscribe requests a new subscription to a set of queries. +// Tag 0 in the ClientMessage sum type. +type Subscribe struct { + RequestID uint32 + QuerySetID uint32 + QueryStrings []string +} + +func (*Subscribe) clientMessageTag() uint8 { return 0 } + +func (s *Subscribe) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(0) + w.PutU32(s.RequestID) + // QuerySetId is a product with a single u32 field + w.PutU32(s.QuerySetID) + // query_strings: Box<[Box]> serialized as array of strings + w.PutArrayLen(uint32(len(s.QueryStrings))) + for _, q := range s.QueryStrings { + w.PutString(q) + } +} + +// UnsubscribeFlags controls the behavior of an Unsubscribe request. +type UnsubscribeFlags uint8 + +const ( + // UnsubscribeFlagsDefault is the default unsubscribe behavior. + UnsubscribeFlagsDefault UnsubscribeFlags = 0 + // UnsubscribeFlagsSendDroppedRows requests the server send dropped rows. + UnsubscribeFlagsSendDroppedRows UnsubscribeFlags = 1 +) + +// Unsubscribe removes a previously-registered subscription. +// Tag 1 in the ClientMessage sum type. +type Unsubscribe struct { + RequestID uint32 + QuerySetID uint32 + Flags UnsubscribeFlags +} + +func (*Unsubscribe) clientMessageTag() uint8 { return 1 } + +func (u *Unsubscribe) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(1) + w.PutU32(u.RequestID) + w.PutU32(u.QuerySetID) + // UnsubscribeFlags is a sum type enum: tag byte only, empty product payload + w.PutSumTag(uint8(u.Flags)) +} + +// OneOffQuery runs a query once without subscribing to updates. +// Tag 2 in the ClientMessage sum type. +type OneOffQuery struct { + RequestID uint32 + QueryString string +} + +func (*OneOffQuery) clientMessageTag() uint8 { return 2 } + +func (o *OneOffQuery) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(2) + w.PutU32(o.RequestID) + w.PutString(o.QueryString) +} + +// CallReducer invokes a reducer (transactional database function). +// Tag 3 in the ClientMessage sum type. +type CallReducer struct { + RequestID uint32 + Flags uint8 + Reducer string + Args []byte +} + +func (*CallReducer) clientMessageTag() uint8 { return 3 } + +func (c *CallReducer) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(3) + w.PutU32(c.RequestID) + // CallReducerFlags serializes as a plain u8 + w.PutU8(c.Flags) + w.PutString(c.Reducer) + // args: Bytes serialized as byte array (u32 len + raw bytes) + bsatn.WriteByteArray(w, c.Args) +} + +// CallProcedure invokes a procedure (non-transactional database function). +// Tag 4 in the ClientMessage sum type. +type CallProcedure struct { + RequestID uint32 + Flags uint8 + Procedure string + Args []byte +} + +func (*CallProcedure) clientMessageTag() uint8 { return 4 } + +func (c *CallProcedure) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(4) + w.PutU32(c.RequestID) + // CallProcedureFlags serializes as a plain u8 + w.PutU8(c.Flags) + w.PutString(c.Procedure) + // args: Bytes serialized as byte array (u32 len + raw bytes) + bsatn.WriteByteArray(w, c.Args) +} diff --git a/sdks/go/client/protocol/compression.go b/sdks/go/client/protocol/compression.go new file mode 100644 index 00000000000..f8583b0d65a --- /dev/null +++ b/sdks/go/client/protocol/compression.go @@ -0,0 +1,50 @@ +package protocol + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + + "github.com/andybalholm/brotli" +) + +// Compression identifies the compression algorithm used for a server message. +type Compression uint8 + +const ( + // CompressionNone means no compression. + CompressionNone Compression = 0 + // CompressionBrotli means brotli compression. + CompressionBrotli Compression = 1 + // CompressionGzip means gzip compression. + CompressionGzip Compression = 2 +) + +// DecompressMessage reads the leading compression tag byte and +// decompresses the remaining payload accordingly. +func DecompressMessage(data []byte) ([]byte, error) { + if len(data) == 0 { + return nil, fmt.Errorf("protocol: empty message") + } + + tag := Compression(data[0]) + payload := data[1:] + + switch tag { + case CompressionNone: + return payload, nil + case CompressionBrotli: + r := brotli.NewReader(bytes.NewReader(payload)) + return io.ReadAll(r) + case CompressionGzip: + gr, err := gzip.NewReader(bytes.NewReader(payload)) + if err != nil { + return nil, fmt.Errorf("protocol: gzip init: %w", err) + } + defer gr.Close() + return io.ReadAll(gr) + default: + return nil, fmt.Errorf("protocol: unknown compression tag: %d", tag) + } +} diff --git a/sdks/go/client/protocol/encoding.go b/sdks/go/client/protocol/encoding.go new file mode 100644 index 00000000000..2da1b131833 --- /dev/null +++ b/sdks/go/client/protocol/encoding.go @@ -0,0 +1,387 @@ +package protocol + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// readInitialConnection reads an InitialConnection from a BSATN reader. +// Fields: identity(32B), connection_id(16B), token(string). +func readInitialConnection(r bsatn.Reader) (*InitialConnection, error) { + identity, err := types.ReadIdentity(r) + if err != nil { + return nil, err + } + + connID, err := types.ReadConnectionId(r) + if err != nil { + return nil, err + } + + token, err := r.GetString() + if err != nil { + return nil, err + } + + return &InitialConnection{ + Identity: identity, + ConnectionID: connID, + Token: token, + }, nil +} + +// readSubscribeApplied reads a SubscribeApplied from a BSATN reader. +// Fields: request_id(u32), query_set_id(QuerySetId{u32}), rows(QueryRows). +func readSubscribeApplied(r bsatn.Reader) (*SubscribeApplied, error) { + requestID, err := r.GetU32() + if err != nil { + return nil, err + } + + querySetID, err := r.GetU32() + if err != nil { + return nil, err + } + + rows, err := readQueryRows(r) + if err != nil { + return nil, err + } + + return &SubscribeApplied{ + RequestID: requestID, + QuerySetID: querySetID, + Rows: *rows, + }, nil +} + +// readUnsubscribeApplied reads an UnsubscribeApplied from a BSATN reader. +// Fields: request_id(u32), query_set_id(QuerySetId{u32}), rows(Option). +func readUnsubscribeApplied(r bsatn.Reader) (*UnsubscribeApplied, error) { + requestID, err := r.GetU32() + if err != nil { + return nil, err + } + + querySetID, err := r.GetU32() + if err != nil { + return nil, err + } + + rows, err := bsatn.ReadOption(r, func(r bsatn.Reader) (QueryRows, error) { + qr, err := readQueryRows(r) + if err != nil { + return QueryRows{}, err + } + return *qr, nil + }) + if err != nil { + return nil, err + } + + return &UnsubscribeApplied{ + RequestID: requestID, + QuerySetID: querySetID, + Rows: rows, + }, nil +} + +// readSubscriptionError reads a SubscriptionError from a BSATN reader. +// Fields: request_id(Option), query_set_id(QuerySetId{u32}), error(string). +func readSubscriptionError(r bsatn.Reader) (*SubscriptionError, error) { + requestID, err := bsatn.ReadOption(r, func(r bsatn.Reader) (uint32, error) { + return r.GetU32() + }) + if err != nil { + return nil, err + } + + querySetID, err := r.GetU32() + if err != nil { + return nil, err + } + + errMsg, err := r.GetString() + if err != nil { + return nil, err + } + + return &SubscriptionError{ + RequestID: requestID, + QuerySetID: querySetID, + Error: errMsg, + }, nil +} + +// readTransactionUpdate reads a TransactionUpdate from a BSATN reader. +// Fields: query_sets(Vec). +func readTransactionUpdate(r bsatn.Reader) (*TransactionUpdate, error) { + querySets, err := bsatn.ReadArray(r, readQuerySetUpdate) + if err != nil { + return nil, err + } + + return &TransactionUpdate{ + QuerySets: querySets, + }, nil +} + +// readOneOffQueryResult reads a OneOffQueryResult from a BSATN reader. +// Fields: request_id(u32), result(Result). +// Result is a sum type: tag 0 = Ok(QueryRows), tag 1 = Err(String). +func readOneOffQueryResult(r bsatn.Reader) (*OneOffQueryResult, error) { + requestID, err := r.GetU32() + if err != nil { + return nil, err + } + + resultTag, err := r.GetSumTag() + if err != nil { + return nil, err + } + + msg := &OneOffQueryResult{RequestID: requestID} + + switch resultTag { + case 0: // Ok(QueryRows) + rows, err := readQueryRows(r) + if err != nil { + return nil, err + } + msg.ResultOk = rows + case 1: // Err(String) + errMsg, err := r.GetString() + if err != nil { + return nil, err + } + msg.ResultErr = errMsg + default: + return nil, &bsatn.ErrInvalidTag{Tag: resultTag, SumName: "Result"} + } + + return msg, nil +} + +// readReducerResult reads a ReducerResult from a BSATN reader. +// Fields: request_id(u32), timestamp(i64), result(ReducerOutcome). +func readReducerResult(r bsatn.Reader) (*ReducerResult, error) { + requestID, err := r.GetU32() + if err != nil { + return nil, err + } + + timestamp, err := types.ReadTimestamp(r) + if err != nil { + return nil, err + } + + outcome, err := readReducerOutcome(r) + if err != nil { + return nil, err + } + + return &ReducerResult{ + RequestID: requestID, + Timestamp: timestamp, + Result: outcome, + }, nil +} + +// readReducerOutcome reads a ReducerOutcome from a BSATN reader. +// Sum type: tag 0 = Ok(ReducerOk), tag 1 = OkEmpty, tag 2 = Err(Bytes), tag 3 = InternalError(String). +func readReducerOutcome(r bsatn.Reader) (ReducerOutcome, error) { + tag, err := r.GetSumTag() + if err != nil { + return nil, err + } + + switch tag { + case 0: // Ok(ReducerOk) + retValue, err := bsatn.ReadByteArray(r) + if err != nil { + return nil, err + } + txUpdate, err := readTransactionUpdate(r) + if err != nil { + return nil, err + } + return &ReducerOk{ + RetValue: retValue, + TransactionUpdate: txUpdate, + }, nil + case 1: // OkEmpty (unit variant) + return &ReducerOkEmpty{}, nil + case 2: // Err(Bytes) + errBytes, err := bsatn.ReadByteArray(r) + if err != nil { + return nil, err + } + return &ReducerErr{ErrorBytes: errBytes}, nil + case 3: // InternalError(String) + msg, err := r.GetString() + if err != nil { + return nil, err + } + return &ReducerInternalError{Message: msg}, nil + default: + return nil, &bsatn.ErrInvalidTag{Tag: tag, SumName: "ReducerOutcome"} + } +} + +// readProcedureResult reads a ProcedureResult from a BSATN reader. +// Fields (Rust declaration order): status, timestamp, total_host_execution_duration, request_id. +func readProcedureResult(r bsatn.Reader) (*ProcedureResult, error) { + status, err := readProcedureStatus(r) + if err != nil { + return nil, err + } + + timestamp, err := types.ReadTimestamp(r) + if err != nil { + return nil, err + } + + duration, err := types.ReadTimeDuration(r) + if err != nil { + return nil, err + } + + requestID, err := r.GetU32() + if err != nil { + return nil, err + } + + return &ProcedureResult{ + Status: status, + Timestamp: timestamp, + TotalHostExecutionDuration: duration, + RequestID: requestID, + }, nil +} + +// readProcedureStatus reads a ProcedureStatus from a BSATN reader. +// Sum type: tag 0 = Returned(Bytes), tag 1 = InternalError(String). +func readProcedureStatus(r bsatn.Reader) (ProcedureStatus, error) { + tag, err := r.GetSumTag() + if err != nil { + return nil, err + } + + switch tag { + case 0: // Returned(Bytes) + value, err := bsatn.ReadByteArray(r) + if err != nil { + return nil, err + } + return &ProcedureReturned{Value: value}, nil + case 1: // InternalError(String) + msg, err := r.GetString() + if err != nil { + return nil, err + } + return &ProcedureInternalError{Message: msg}, nil + default: + return nil, &bsatn.ErrInvalidTag{Tag: tag, SumName: "ProcedureStatus"} + } +} + +// readQueryRows reads a QueryRows from a BSATN reader. +// Fields: tables(Vec). +func readQueryRows(r bsatn.Reader) (*QueryRows, error) { + tables, err := bsatn.ReadArray(r, readSingleTableRows) + if err != nil { + return nil, err + } + + return &QueryRows{Tables: tables}, nil +} + +// readSingleTableRows reads a SingleTableRows from a BSATN reader. +// Fields: table(RawIdentifier=String), rows(BsatnRowList). +func readSingleTableRows(r bsatn.Reader) (SingleTableRows, error) { + tableName, err := r.GetString() + if err != nil { + return SingleTableRows{}, err + } + + rows, err := ReadBsatnRowList(r) + if err != nil { + return SingleTableRows{}, err + } + + return SingleTableRows{ + TableName: tableName, + Rows: rows, + }, nil +} + +// readQuerySetUpdate reads a QuerySetUpdate from a BSATN reader. +// Fields: query_set_id(QuerySetId{u32}), tables(Vec). +func readQuerySetUpdate(r bsatn.Reader) (QuerySetUpdate, error) { + querySetID, err := r.GetU32() + if err != nil { + return QuerySetUpdate{}, err + } + + tables, err := bsatn.ReadArray(r, readTableUpdate) + if err != nil { + return QuerySetUpdate{}, err + } + + return QuerySetUpdate{ + QuerySetID: querySetID, + Tables: tables, + }, nil +} + +// readTableUpdate reads a TableUpdate from a BSATN reader. +// Fields: table_name(RawIdentifier=String), rows(Vec). +func readTableUpdate(r bsatn.Reader) (TableUpdate, error) { + tableName, err := r.GetString() + if err != nil { + return TableUpdate{}, err + } + + rows, err := bsatn.ReadArray(r, readTableUpdateRows) + if err != nil { + return TableUpdate{}, err + } + + return TableUpdate{ + TableName: tableName, + Rows: rows, + }, nil +} + +// readTableUpdateRows reads a TableUpdateRows from a BSATN reader. +// Sum type: tag 0 = PersistentTable(PersistentTableRows), tag 1 = EventTable(EventTableRows). +func readTableUpdateRows(r bsatn.Reader) (TableUpdateRows, error) { + tag, err := r.GetSumTag() + if err != nil { + return nil, err + } + + switch tag { + case 0: // PersistentTable + inserts, err := ReadBsatnRowList(r) + if err != nil { + return nil, err + } + deletes, err := ReadBsatnRowList(r) + if err != nil { + return nil, err + } + return &PersistentTableRows{ + Inserts: inserts, + Deletes: deletes, + }, nil + case 1: // EventTable + events, err := ReadBsatnRowList(r) + if err != nil { + return nil, err + } + return &EventTableRows{Events: events}, nil + default: + return nil, &bsatn.ErrInvalidTag{Tag: tag, SumName: "TableUpdateRows"} + } +} diff --git a/sdks/go/client/protocol/protocol_test.go b/sdks/go/client/protocol/protocol_test.go new file mode 100644 index 00000000000..771c0811a93 --- /dev/null +++ b/sdks/go/client/protocol/protocol_test.go @@ -0,0 +1,977 @@ +package protocol_test + +import ( + "bytes" + "compress/gzip" + "encoding/binary" + "testing" + + "github.com/andybalholm/brotli" + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/client/protocol" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --- ClientMessage encoding tests --- + +func TestCallReducer_WriteBsatn(t *testing.T) { + msg := &protocol.CallReducer{ + RequestID: 42, + Flags: 0, + Reducer: "add_user", + Args: []byte{0xDE, 0xAD}, + } + + w := bsatn.NewWriter(64) + msg.WriteBsatn(w) + got := w.Bytes() + + // Build expected bytes manually: + // tag=3 (u8), request_id=42 (u32 LE), flags=0 (u8), + // reducer="add_user" (u32 LE len + UTF-8), args (u32 LE len + raw bytes) + exp := bsatn.NewWriter(64) + exp.PutSumTag(3) + exp.PutU32(42) + exp.PutU8(0) + exp.PutString("add_user") + bsatn.WriteByteArray(exp, []byte{0xDE, 0xAD}) + expected := exp.Bytes() + + assert.Equal(t, expected, got) + + // Verify individual field positions + assert.Equal(t, uint8(3), got[0], "sum tag should be 3 for CallReducer") + assert.Equal(t, uint32(42), binary.LittleEndian.Uint32(got[1:5]), "request_id should be 42") + assert.Equal(t, uint8(0), got[5], "flags should be 0") +} + +func TestCallReducer_EmptyArgs(t *testing.T) { + msg := &protocol.CallReducer{ + RequestID: 1, + Flags: 0, + Reducer: "noop", + Args: nil, + } + + w := bsatn.NewWriter(64) + msg.WriteBsatn(w) + got := w.Bytes() + + exp := bsatn.NewWriter(64) + exp.PutSumTag(3) + exp.PutU32(1) + exp.PutU8(0) + exp.PutString("noop") + bsatn.WriteByteArray(exp, nil) + + assert.Equal(t, exp.Bytes(), got) +} + +func TestSubscribe_WriteBsatn(t *testing.T) { + msg := &protocol.Subscribe{ + RequestID: 10, + QuerySetID: 5, + QueryStrings: []string{"SELECT * FROM users", "SELECT * FROM items"}, + } + + w := bsatn.NewWriter(128) + msg.WriteBsatn(w) + got := w.Bytes() + + exp := bsatn.NewWriter(128) + exp.PutSumTag(0) // Subscribe tag + exp.PutU32(10) // request_id + exp.PutU32(5) // query_set_id + exp.PutArrayLen(2) + exp.PutString("SELECT * FROM users") + exp.PutString("SELECT * FROM items") + + assert.Equal(t, exp.Bytes(), got) + assert.Equal(t, uint8(0), got[0], "sum tag should be 0 for Subscribe") +} + +func TestSubscribe_EmptyQueries(t *testing.T) { + msg := &protocol.Subscribe{ + RequestID: 1, + QuerySetID: 1, + QueryStrings: nil, + } + + w := bsatn.NewWriter(64) + msg.WriteBsatn(w) + got := w.Bytes() + + exp := bsatn.NewWriter(64) + exp.PutSumTag(0) + exp.PutU32(1) + exp.PutU32(1) + exp.PutArrayLen(0) + + assert.Equal(t, exp.Bytes(), got) +} + +func TestUnsubscribe_WriteBsatn(t *testing.T) { + msg := &protocol.Unsubscribe{ + RequestID: 7, + QuerySetID: 3, + Flags: protocol.UnsubscribeFlagsDefault, + } + + w := bsatn.NewWriter(64) + msg.WriteBsatn(w) + got := w.Bytes() + + exp := bsatn.NewWriter(64) + exp.PutSumTag(1) // Unsubscribe tag + exp.PutU32(7) // request_id + exp.PutU32(3) // query_set_id + exp.PutSumTag(0) // flags = Default (tag 0) + + assert.Equal(t, exp.Bytes(), got) +} + +func TestUnsubscribe_SendDroppedRows(t *testing.T) { + msg := &protocol.Unsubscribe{ + RequestID: 9, + QuerySetID: 4, + Flags: protocol.UnsubscribeFlagsSendDroppedRows, + } + + w := bsatn.NewWriter(64) + msg.WriteBsatn(w) + got := w.Bytes() + + exp := bsatn.NewWriter(64) + exp.PutSumTag(1) // Unsubscribe tag + exp.PutU32(9) + exp.PutU32(4) + exp.PutSumTag(1) // flags = SendDroppedRows (tag 1) + + assert.Equal(t, exp.Bytes(), got) +} + +func TestOneOffQuery_WriteBsatn(t *testing.T) { + msg := &protocol.OneOffQuery{ + RequestID: 99, + QueryString: "SELECT count(*) FROM users", + } + + w := bsatn.NewWriter(64) + msg.WriteBsatn(w) + got := w.Bytes() + + exp := bsatn.NewWriter(64) + exp.PutSumTag(2) // OneOffQuery tag + exp.PutU32(99) + exp.PutString("SELECT count(*) FROM users") + + assert.Equal(t, exp.Bytes(), got) +} + +func TestCallProcedure_WriteBsatn(t *testing.T) { + msg := &protocol.CallProcedure{ + RequestID: 55, + Flags: 1, + Procedure: "my_proc", + Args: []byte{0x01, 0x02, 0x03}, + } + + w := bsatn.NewWriter(64) + msg.WriteBsatn(w) + got := w.Bytes() + + exp := bsatn.NewWriter(64) + exp.PutSumTag(4) // CallProcedure tag + exp.PutU32(55) + exp.PutU8(1) + exp.PutString("my_proc") + bsatn.WriteByteArray(exp, []byte{0x01, 0x02, 0x03}) + + assert.Equal(t, exp.Bytes(), got) +} + +// --- Compression tests --- + +func TestDecompressMessage_None(t *testing.T) { + payload := []byte{0x01, 0x02, 0x03, 0x04} + // Prepend compression tag 0 (none) + data := append([]byte{0x00}, payload...) + + result, err := protocol.DecompressMessage(data) + require.NoError(t, err) + assert.Equal(t, payload, result) +} + +func TestDecompressMessage_Gzip(t *testing.T) { + original := []byte("hello world from spacetimedb") + + // Gzip compress the payload + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + _, err := gw.Write(original) + require.NoError(t, err) + require.NoError(t, gw.Close()) + + // Prepend compression tag 2 (gzip) + data := append([]byte{0x02}, buf.Bytes()...) + + result, err := protocol.DecompressMessage(data) + require.NoError(t, err) + assert.Equal(t, original, result) +} + +func TestDecompressMessage_EmptyPayload(t *testing.T) { + _, err := protocol.DecompressMessage(nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "empty message") +} + +func TestDecompressMessage_UnknownTag(t *testing.T) { + data := []byte{0xFF, 0x01, 0x02} + _, err := protocol.DecompressMessage(data) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown compression tag") +} + +// --- BsatnRowList decoding tests --- + +func TestBsatnRowList_FixedSize(t *testing.T) { + // Build BSATN for a BsatnRowList with FixedSizeHint(rowSize=4) and 8 bytes of row data (2 rows) + w := bsatn.NewWriter(64) + w.PutSumTag(0) // FixedSize tag + w.PutU16(4) // row size = 4 + w.PutArrayLen(8) // 8 bytes of row data + w.PutBytes([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}) + + r := bsatn.NewReader(w.Bytes()) + rl, err := protocol.ReadBsatnRowList(r) + require.NoError(t, err) + + hint, ok := rl.SizeHint.(protocol.FixedSizeHint) + require.True(t, ok, "expected FixedSizeHint") + assert.Equal(t, uint16(4), hint.RowSize) + assert.Equal(t, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, rl.RowsData) + + // Test Rows() + rows := rl.Rows() + require.Len(t, rows, 2) + assert.Equal(t, []byte{0x01, 0x02, 0x03, 0x04}, rows[0]) + assert.Equal(t, []byte{0x05, 0x06, 0x07, 0x08}, rows[1]) + + // Test Len() + assert.Equal(t, 2, rl.Len()) +} + +func TestBsatnRowList_RowOffsets(t *testing.T) { + // Build BSATN for a BsatnRowList with RowOffsetsHint and variable-size rows + w := bsatn.NewWriter(64) + w.PutSumTag(1) // RowOffsets tag + w.PutArrayLen(3) // 3 offsets + w.PutU64(0) // row 0 starts at 0 + w.PutU64(2) // row 1 starts at 2 + w.PutU64(5) // row 2 starts at 5 + rowData := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11} + w.PutArrayLen(uint32(len(rowData))) + w.PutBytes(rowData) + + r := bsatn.NewReader(w.Bytes()) + rl, err := protocol.ReadBsatnRowList(r) + require.NoError(t, err) + + hint, ok := rl.SizeHint.(protocol.RowOffsetsHint) + require.True(t, ok, "expected RowOffsetsHint") + assert.Equal(t, []uint64{0, 2, 5}, hint.Offsets) + + rows := rl.Rows() + require.Len(t, rows, 3) + assert.Equal(t, []byte{0xAA, 0xBB}, rows[0]) + assert.Equal(t, []byte{0xCC, 0xDD, 0xEE}, rows[1]) + assert.Equal(t, []byte{0xFF, 0x11}, rows[2]) + + assert.Equal(t, 3, rl.Len()) +} + +func TestBsatnRowList_EmptyRowsData(t *testing.T) { + w := bsatn.NewWriter(32) + w.PutSumTag(0) // FixedSize + w.PutU16(4) // row size + w.PutArrayLen(0) // 0 bytes of data + + r := bsatn.NewReader(w.Bytes()) + rl, err := protocol.ReadBsatnRowList(r) + require.NoError(t, err) + assert.Equal(t, 0, rl.Len()) + assert.Nil(t, rl.Rows()) +} + +func TestBsatnRowList_NilRowList(t *testing.T) { + var rl *protocol.BsatnRowList + assert.Equal(t, 0, rl.Len()) + assert.Nil(t, rl.Rows()) +} + +func TestBsatnRowList_ZeroRowSize(t *testing.T) { + w := bsatn.NewWriter(32) + w.PutSumTag(0) // FixedSize + w.PutU16(0) // row size = 0 + w.PutArrayLen(4) // 4 bytes of data + w.PutBytes([]byte{0x01, 0x02, 0x03, 0x04}) + + r := bsatn.NewReader(w.Bytes()) + rl, err := protocol.ReadBsatnRowList(r) + require.NoError(t, err) + assert.Equal(t, 0, rl.Len()) + assert.Nil(t, rl.Rows()) +} + +func TestBsatnRowList_InvalidTag(t *testing.T) { + w := bsatn.NewWriter(8) + w.PutSumTag(99) // invalid tag + + r := bsatn.NewReader(w.Bytes()) + _, err := protocol.ReadBsatnRowList(r) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid tag 99") +} + +// --- ServerMessage decoding tests --- + +func TestReadServerMessage_InitialConnection(t *testing.T) { + // Build BSATN for an InitialConnection message + w := bsatn.NewWriter(128) + w.PutSumTag(0) // InitialConnection tag + + // Identity: 32 bytes + var identityBytes [32]byte + for i := range identityBytes { + identityBytes[i] = byte(i) + } + w.PutBytes(identityBytes[:]) + + // ConnectionId: 16 bytes + var connIdBytes [16]byte + for i := range connIdBytes { + connIdBytes[i] = byte(0xA0 + i) + } + w.PutBytes(connIdBytes[:]) + + // Token: string + w.PutString("my-auth-token-123") + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + ic, ok := msg.(*protocol.InitialConnection) + require.True(t, ok, "expected *InitialConnection") + assert.Equal(t, identityBytes, ic.Identity.Bytes()) + assert.Equal(t, connIdBytes, ic.ConnectionID.Bytes()) + assert.Equal(t, "my-auth-token-123", ic.Token) +} + +func TestReadServerMessage_SubscribeApplied(t *testing.T) { + w := bsatn.NewWriter(128) + w.PutSumTag(1) // SubscribeApplied tag + w.PutU32(42) // request_id + w.PutU32(7) // query_set_id + + // QueryRows: tables (Vec) - empty + w.PutArrayLen(0) + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + sa, ok := msg.(*protocol.SubscribeApplied) + require.True(t, ok, "expected *SubscribeApplied") + assert.Equal(t, uint32(42), sa.RequestID) + assert.Equal(t, uint32(7), sa.QuerySetID) + assert.Empty(t, sa.Rows.Tables) +} + +func TestReadServerMessage_SubscribeApplied_WithRows(t *testing.T) { + w := bsatn.NewWriter(256) + w.PutSumTag(1) // SubscribeApplied tag + w.PutU32(1) // request_id + w.PutU32(2) // query_set_id + + // QueryRows: tables (Vec) - 1 table + w.PutArrayLen(1) + // SingleTableRows: table_name (string) + rows (BsatnRowList) + w.PutString("users") + // BsatnRowList: FixedSize(8), 16 bytes of data (2 rows) + w.PutSumTag(0) // FixedSize + w.PutU16(8) // row size + w.PutArrayLen(16) // 16 bytes + rowData := make([]byte, 16) + for i := range rowData { + rowData[i] = byte(i + 1) + } + w.PutBytes(rowData) + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + sa, ok := msg.(*protocol.SubscribeApplied) + require.True(t, ok, "expected *SubscribeApplied") + require.Len(t, sa.Rows.Tables, 1) + assert.Equal(t, "users", sa.Rows.Tables[0].TableName) + assert.Equal(t, 2, sa.Rows.Tables[0].Rows.Len()) +} + +func TestReadServerMessage_UnsubscribeApplied_NoRows(t *testing.T) { + w := bsatn.NewWriter(64) + w.PutSumTag(2) // UnsubscribeApplied tag + w.PutU32(10) // request_id + w.PutU32(3) // query_set_id + w.PutSumTag(1) // Option::None for rows + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + ua, ok := msg.(*protocol.UnsubscribeApplied) + require.True(t, ok, "expected *UnsubscribeApplied") + assert.Equal(t, uint32(10), ua.RequestID) + assert.Equal(t, uint32(3), ua.QuerySetID) + assert.Nil(t, ua.Rows) +} + +func TestReadServerMessage_UnsubscribeApplied_WithRows(t *testing.T) { + w := bsatn.NewWriter(128) + w.PutSumTag(2) // UnsubscribeApplied tag + w.PutU32(10) // request_id + w.PutU32(3) // query_set_id + w.PutSumTag(0) // Option::Some for rows + // QueryRows: empty tables + w.PutArrayLen(0) + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + ua, ok := msg.(*protocol.UnsubscribeApplied) + require.True(t, ok, "expected *UnsubscribeApplied") + require.NotNil(t, ua.Rows) + assert.Empty(t, ua.Rows.Tables) +} + +func TestReadServerMessage_SubscriptionError(t *testing.T) { + w := bsatn.NewWriter(64) + w.PutSumTag(3) // SubscriptionError tag + w.PutSumTag(0) // Option::Some for request_id + w.PutU32(77) // request_id value + w.PutU32(5) // query_set_id + w.PutString("table not found") + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + se, ok := msg.(*protocol.SubscriptionError) + require.True(t, ok, "expected *SubscriptionError") + require.NotNil(t, se.RequestID) + assert.Equal(t, uint32(77), *se.RequestID) + assert.Equal(t, uint32(5), se.QuerySetID) + assert.Equal(t, "table not found", se.Error) +} + +func TestReadServerMessage_SubscriptionError_NoRequestID(t *testing.T) { + w := bsatn.NewWriter(64) + w.PutSumTag(3) // SubscriptionError tag + w.PutSumTag(1) // Option::None for request_id + w.PutU32(5) // query_set_id + w.PutString("unknown error") + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + se, ok := msg.(*protocol.SubscriptionError) + require.True(t, ok, "expected *SubscriptionError") + assert.Nil(t, se.RequestID) + assert.Equal(t, "unknown error", se.Error) +} + +func TestReadServerMessage_TransactionUpdate_Empty(t *testing.T) { + w := bsatn.NewWriter(32) + w.PutSumTag(4) // TransactionUpdate tag + w.PutArrayLen(0) // empty query_sets + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + tu, ok := msg.(*protocol.TransactionUpdate) + require.True(t, ok, "expected *TransactionUpdate") + assert.Empty(t, tu.QuerySets) +} + +func TestReadServerMessage_TransactionUpdate_WithPersistentRows(t *testing.T) { + w := bsatn.NewWriter(256) + w.PutSumTag(4) // TransactionUpdate tag + w.PutArrayLen(1) // 1 query set update + + // QuerySetUpdate: query_set_id + tables + w.PutU32(1) // query_set_id + w.PutArrayLen(1) // 1 table update + + // TableUpdate: table_name + rows + w.PutString("players") + w.PutArrayLen(1) // 1 TableUpdateRows + + // PersistentTableRows: tag=0, inserts + deletes + w.PutSumTag(0) // PersistentTable tag + // inserts: BsatnRowList (FixedSize, 4 bytes = 1 row of size 4) + w.PutSumTag(0) // FixedSize + w.PutU16(4) // row size + w.PutArrayLen(4) // 4 bytes + w.PutBytes([]byte{0x0A, 0x0B, 0x0C, 0x0D}) + // deletes: BsatnRowList (FixedSize, 0 bytes = 0 rows) + w.PutSumTag(0) // FixedSize + w.PutU16(4) // row size + w.PutArrayLen(0) // 0 bytes + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + tu, ok := msg.(*protocol.TransactionUpdate) + require.True(t, ok, "expected *TransactionUpdate") + require.Len(t, tu.QuerySets, 1) + assert.Equal(t, uint32(1), tu.QuerySets[0].QuerySetID) + require.Len(t, tu.QuerySets[0].Tables, 1) + assert.Equal(t, "players", tu.QuerySets[0].Tables[0].TableName) + require.Len(t, tu.QuerySets[0].Tables[0].Rows, 1) + + ptr, ok := tu.QuerySets[0].Tables[0].Rows[0].(*protocol.PersistentTableRows) + require.True(t, ok, "expected *PersistentTableRows") + assert.Equal(t, 1, ptr.Inserts.Len()) + assert.Equal(t, 0, ptr.Deletes.Len()) +} + +func TestReadServerMessage_OneOffQueryResult_Ok(t *testing.T) { + w := bsatn.NewWriter(128) + w.PutSumTag(5) // OneOffQueryResult tag + w.PutU32(33) // request_id + w.PutSumTag(0) // Result::Ok tag + + // QueryRows: 1 table, 1 row + w.PutArrayLen(1) + w.PutString("counts") + w.PutSumTag(0) // FixedSize + w.PutU16(4) // row size + w.PutArrayLen(4) // 4 bytes + w.PutBytes([]byte{0x01, 0x00, 0x00, 0x00}) // row data + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + oqr, ok := msg.(*protocol.OneOffQueryResult) + require.True(t, ok, "expected *OneOffQueryResult") + assert.Equal(t, uint32(33), oqr.RequestID) + require.NotNil(t, oqr.ResultOk) + assert.Empty(t, oqr.ResultErr) + require.Len(t, oqr.ResultOk.Tables, 1) +} + +func TestReadServerMessage_OneOffQueryResult_Err(t *testing.T) { + w := bsatn.NewWriter(64) + w.PutSumTag(5) // OneOffQueryResult tag + w.PutU32(33) // request_id + w.PutSumTag(1) // Result::Err tag + w.PutString("syntax error near SELECT") + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + oqr, ok := msg.(*protocol.OneOffQueryResult) + require.True(t, ok, "expected *OneOffQueryResult") + assert.Equal(t, uint32(33), oqr.RequestID) + assert.Nil(t, oqr.ResultOk) + assert.Equal(t, "syntax error near SELECT", oqr.ResultErr) +} + +func TestReadServerMessage_ReducerResult_OkEmpty(t *testing.T) { + w := bsatn.NewWriter(64) + w.PutSumTag(6) // ReducerResult tag + w.PutU32(100) // request_id + w.PutI64(1234567890) // timestamp (microseconds) + w.PutSumTag(1) // ReducerOutcome::OkEmpty tag + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + rr, ok := msg.(*protocol.ReducerResult) + require.True(t, ok, "expected *ReducerResult") + assert.Equal(t, uint32(100), rr.RequestID) + assert.Equal(t, int64(1234567890), rr.Timestamp.Microseconds()) + + _, ok = rr.Result.(*protocol.ReducerOkEmpty) + assert.True(t, ok, "expected *ReducerOkEmpty outcome") +} + +func TestReadServerMessage_ReducerResult_Ok(t *testing.T) { + w := bsatn.NewWriter(128) + w.PutSumTag(6) // ReducerResult tag + w.PutU32(200) // request_id + w.PutI64(9999) // timestamp + w.PutSumTag(0) // ReducerOutcome::Ok tag + + // ReducerOk: ret_value (byte array) + transaction_update + bsatn.WriteByteArray(w, []byte{0x42}) + // TransactionUpdate: empty query_sets + w.PutArrayLen(0) + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + rr, ok := msg.(*protocol.ReducerResult) + require.True(t, ok, "expected *ReducerResult") + + rok, ok := rr.Result.(*protocol.ReducerOk) + require.True(t, ok, "expected *ReducerOk outcome") + assert.Equal(t, []byte{0x42}, rok.RetValue) + require.NotNil(t, rok.TransactionUpdate) + assert.Empty(t, rok.TransactionUpdate.QuerySets) +} + +func TestReadServerMessage_ReducerResult_Err(t *testing.T) { + w := bsatn.NewWriter(64) + w.PutSumTag(6) // ReducerResult tag + w.PutU32(300) // request_id + w.PutI64(5555) // timestamp + w.PutSumTag(2) // ReducerOutcome::Err tag + bsatn.WriteByteArray(w, []byte{0xEE, 0xFF}) + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + rr, ok := msg.(*protocol.ReducerResult) + require.True(t, ok) + + re, ok := rr.Result.(*protocol.ReducerErr) + require.True(t, ok, "expected *ReducerErr outcome") + assert.Equal(t, []byte{0xEE, 0xFF}, re.ErrorBytes) +} + +func TestReadServerMessage_ReducerResult_InternalError(t *testing.T) { + w := bsatn.NewWriter(64) + w.PutSumTag(6) // ReducerResult tag + w.PutU32(400) // request_id + w.PutI64(7777) // timestamp + w.PutSumTag(3) // ReducerOutcome::InternalError tag + w.PutString("reducer panicked") + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + rr, ok := msg.(*protocol.ReducerResult) + require.True(t, ok) + + rie, ok := rr.Result.(*protocol.ReducerInternalError) + require.True(t, ok, "expected *ReducerInternalError outcome") + assert.Equal(t, "reducer panicked", rie.Message) +} + +func TestReadServerMessage_ProcedureResult_Returned(t *testing.T) { + w := bsatn.NewWriter(64) + w.PutSumTag(7) // ProcedureResult tag + + // status: Returned(Bytes) + w.PutSumTag(0) + bsatn.WriteByteArray(w, []byte{0xAA, 0xBB}) + + // timestamp + w.PutI64(111222333) + + // total_host_execution_duration + w.PutI64(5000) + + // request_id + w.PutU32(50) + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + pr, ok := msg.(*protocol.ProcedureResult) + require.True(t, ok, "expected *ProcedureResult") + assert.Equal(t, uint32(50), pr.RequestID) + assert.Equal(t, int64(111222333), pr.Timestamp.Microseconds()) + assert.Equal(t, int64(5000), pr.TotalHostExecutionDuration.Microseconds()) + + ret, ok := pr.Status.(*protocol.ProcedureReturned) + require.True(t, ok, "expected *ProcedureReturned") + assert.Equal(t, []byte{0xAA, 0xBB}, ret.Value) +} + +func TestReadServerMessage_ProcedureResult_InternalError(t *testing.T) { + w := bsatn.NewWriter(64) + w.PutSumTag(7) // ProcedureResult tag + + // status: InternalError(String) + w.PutSumTag(1) + w.PutString("host error") + + // timestamp + w.PutI64(0) + + // total_host_execution_duration + w.PutI64(0) + + // request_id + w.PutU32(51) + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + pr, ok := msg.(*protocol.ProcedureResult) + require.True(t, ok, "expected *ProcedureResult") + + pie, ok := pr.Status.(*protocol.ProcedureInternalError) + require.True(t, ok, "expected *ProcedureInternalError") + assert.Equal(t, "host error", pie.Message) +} + +func TestReadServerMessage_InvalidTag(t *testing.T) { + w := bsatn.NewWriter(8) + w.PutSumTag(99) // invalid ServerMessage tag + + r := bsatn.NewReader(w.Bytes()) + _, err := protocol.ReadServerMessage(r) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid tag 99") +} + +func TestReadServerMessage_EventTableRows(t *testing.T) { + w := bsatn.NewWriter(128) + w.PutSumTag(4) // TransactionUpdate tag + w.PutArrayLen(1) // 1 query set update + + w.PutU32(2) // query_set_id + w.PutArrayLen(1) // 1 table update + + w.PutString("events") + w.PutArrayLen(1) // 1 TableUpdateRows + + // EventTableRows: tag=1, events + w.PutSumTag(1) // EventTable tag + w.PutSumTag(0) // FixedSize + w.PutU16(2) // row size + w.PutArrayLen(4) // 4 bytes = 2 rows + w.PutBytes([]byte{0x01, 0x02, 0x03, 0x04}) + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + tu, ok := msg.(*protocol.TransactionUpdate) + require.True(t, ok) + require.Len(t, tu.QuerySets, 1) + require.Len(t, tu.QuerySets[0].Tables, 1) + require.Len(t, tu.QuerySets[0].Tables[0].Rows, 1) + + etr, ok := tu.QuerySets[0].Tables[0].Rows[0].(*protocol.EventTableRows) + require.True(t, ok, "expected *EventTableRows") + assert.Equal(t, 2, etr.Events.Len()) +} + +// --- Brotli compression test --- + +func TestDecompressMessage_Brotli(t *testing.T) { + original := []byte("brotli compressed data for spacetimedb protocol") + + // Brotli compress the payload + var buf bytes.Buffer + bw := brotli.NewWriterLevel(&buf, brotli.DefaultCompression) + _, err := bw.Write(original) + require.NoError(t, err) + require.NoError(t, bw.Close()) + + // Prepend compression tag 1 (brotli) + data := append([]byte{0x01}, buf.Bytes()...) + + result, err := protocol.DecompressMessage(data) + require.NoError(t, err) + assert.Equal(t, original, result) +} + +func TestDecompressMessage_None_SingleByte(t *testing.T) { + // Tag byte only, no payload + data := []byte{0x00} + result, err := protocol.DecompressMessage(data) + require.NoError(t, err) + assert.Empty(t, result) +} + +func TestDecompressMessage_None_EmptyAfterTag(t *testing.T) { + data := []byte{0x00} + result, err := protocol.DecompressMessage(data) + require.NoError(t, err) + assert.Equal(t, []byte{}, result) +} + +// --- Edge case: large messages --- + +func TestSubscribe_ManyQueries(t *testing.T) { + queries := make([]string, 100) + for i := range queries { + queries[i] = "SELECT * FROM table_" + string(rune('A'+i%26)) + } + + msg := &protocol.Subscribe{ + RequestID: 1, + QuerySetID: 1, + QueryStrings: queries, + } + + w := bsatn.NewWriter(4096) + msg.WriteBsatn(w) + got := w.Bytes() + + require.NotEmpty(t, got) + assert.Equal(t, uint8(0), got[0], "tag should be 0") + + // Verify array length is encoded as 100 + arrayLenOffset := 1 + 4 + 4 // tag + request_id + query_set_id + assert.Equal(t, uint32(100), binary.LittleEndian.Uint32(got[arrayLenOffset:arrayLenOffset+4])) +} + +func TestCallReducer_LargeArgs(t *testing.T) { + largeArgs := make([]byte, 65536) + for i := range largeArgs { + largeArgs[i] = byte(i % 256) + } + + msg := &protocol.CallReducer{ + RequestID: 1, + Flags: 0, + Reducer: "bulk_insert", + Args: largeArgs, + } + + w := bsatn.NewWriter(70000) + msg.WriteBsatn(w) + got := w.Bytes() + require.NotEmpty(t, got) + + // Verify the encoded size is plausible + // tag(1) + request_id(4) + flags(1) + string_len(4) + "bulk_insert"(11) + array_len(4) + 65536 + expectedMinSize := 1 + 4 + 1 + 4 + 11 + 4 + 65536 + assert.GreaterOrEqual(t, len(got), expectedMinSize) +} + +func TestBsatnRowList_ManyRowsFixedSize(t *testing.T) { + rowSize := uint16(8) + numRows := 1000 + rowData := make([]byte, int(rowSize)*numRows) + for i := range rowData { + rowData[i] = byte(i % 256) + } + + w := bsatn.NewWriter(len(rowData) + 16) + w.PutSumTag(0) + w.PutU16(rowSize) + w.PutArrayLen(uint32(len(rowData))) + w.PutBytes(rowData) + + r := bsatn.NewReader(w.Bytes()) + rl, err := protocol.ReadBsatnRowList(r) + require.NoError(t, err) + assert.Equal(t, numRows, rl.Len()) + + rows := rl.Rows() + require.Len(t, rows, numRows) + for i, row := range rows { + require.Len(t, row, int(rowSize), "row %d should have size %d", i, rowSize) + } +} + +// --- ServerMessage decoding: TransactionUpdate with multiple query sets and tables --- + +func TestReadServerMessage_TransactionUpdate_MultipleQuerySets(t *testing.T) { + w := bsatn.NewWriter(512) + w.PutSumTag(4) // TransactionUpdate tag + w.PutArrayLen(2) // 2 query set updates + + // QuerySetUpdate 1 + w.PutU32(10) // query_set_id + w.PutArrayLen(1) // 1 table + w.PutString("users") + w.PutArrayLen(1) // 1 TableUpdateRows + w.PutSumTag(0) // PersistentTable + // inserts + w.PutSumTag(0); w.PutU16(4); w.PutArrayLen(4) + w.PutBytes([]byte{0x01, 0x02, 0x03, 0x04}) + // deletes + w.PutSumTag(0); w.PutU16(4); w.PutArrayLen(0) + + // QuerySetUpdate 2 + w.PutU32(20) // query_set_id + w.PutArrayLen(1) // 1 table + w.PutString("items") + w.PutArrayLen(1) // 1 TableUpdateRows + w.PutSumTag(0) // PersistentTable + // inserts + w.PutSumTag(0); w.PutU16(2); w.PutArrayLen(4) + w.PutBytes([]byte{0xAA, 0xBB, 0xCC, 0xDD}) + // deletes + w.PutSumTag(0); w.PutU16(2); w.PutArrayLen(0) + + r := bsatn.NewReader(w.Bytes()) + msg, err := protocol.ReadServerMessage(r) + require.NoError(t, err) + + tu, ok := msg.(*protocol.TransactionUpdate) + require.True(t, ok) + require.Len(t, tu.QuerySets, 2) + assert.Equal(t, uint32(10), tu.QuerySets[0].QuerySetID) + assert.Equal(t, "users", tu.QuerySets[0].Tables[0].TableName) + assert.Equal(t, uint32(20), tu.QuerySets[1].QuerySetID) + assert.Equal(t, "items", tu.QuerySets[1].Tables[0].TableName) +} + +// --- BSATN encode helper round-trip --- + +func TestBsatnEncode_CallReducer(t *testing.T) { + msg := &protocol.CallReducer{ + RequestID: 1, + Flags: 0, + Reducer: "test", + Args: []byte{0x01}, + } + + encoded := bsatn.Encode(msg) + require.NotEmpty(t, encoded) + + // Verify the tag byte + assert.Equal(t, uint8(3), encoded[0]) +} + +func TestBsatnEncode_AllClientMessages(t *testing.T) { + messages := []protocol.ClientMessage{ + &protocol.Subscribe{RequestID: 1, QuerySetID: 1, QueryStrings: []string{"SELECT 1"}}, + &protocol.Unsubscribe{RequestID: 2, QuerySetID: 1, Flags: protocol.UnsubscribeFlagsDefault}, + &protocol.OneOffQuery{RequestID: 3, QueryString: "SELECT 1"}, + &protocol.CallReducer{RequestID: 4, Flags: 0, Reducer: "test", Args: nil}, + &protocol.CallProcedure{RequestID: 5, Flags: 0, Procedure: "test", Args: nil}, + } + + expectedTags := []uint8{0, 1, 2, 3, 4} + + for i, msg := range messages { + encoded := bsatn.Encode(msg) + require.NotEmpty(t, encoded, "message %d should encode", i) + assert.Equal(t, expectedTags[i], encoded[0], "message %d should have tag %d", i, expectedTags[i]) + } +} diff --git a/sdks/go/client/protocol/query_update.go b/sdks/go/client/protocol/query_update.go new file mode 100644 index 00000000000..972298276ce --- /dev/null +++ b/sdks/go/client/protocol/query_update.go @@ -0,0 +1,48 @@ +package protocol + +// QueryRows holds the matching rows for a set of tables, +// used in contexts like SubscribeApplied and OneOffQueryResult. +type QueryRows struct { + Tables []SingleTableRows +} + +// SingleTableRows holds the matching rows from a single table. +type SingleTableRows struct { + TableName string + Rows *BsatnRowList +} + +// QuerySetUpdate describes the changes to a single query set +// as part of a TransactionUpdate. +type QuerySetUpdate struct { + QuerySetID uint32 + Tables []TableUpdate +} + +// TableUpdate describes the row changes for a single table. +type TableUpdate struct { + TableName string + Rows []TableUpdateRows +} + +// TableUpdateRows is a sum type representing either persistent table +// insert/delete rows or event table rows. +// Tag 0 = PersistentTableRows, Tag 1 = EventTableRows. +type TableUpdateRows interface { + isTableUpdateRows() +} + +// PersistentTableRows holds inserted and deleted rows for a persistent table. +type PersistentTableRows struct { + Inserts *BsatnRowList + Deletes *BsatnRowList +} + +func (*PersistentTableRows) isTableUpdateRows() {} + +// EventTableRows holds event rows for an event table. +type EventTableRows struct { + Events *BsatnRowList +} + +func (*EventTableRows) isTableUpdateRows() {} diff --git a/sdks/go/client/protocol/reducer_result.go b/sdks/go/client/protocol/reducer_result.go new file mode 100644 index 00000000000..850041e3330 --- /dev/null +++ b/sdks/go/client/protocol/reducer_result.go @@ -0,0 +1,80 @@ +package protocol + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/types" + +// ReducerOutcome is a sum type representing the result of a reducer call. +// Tag 0 = Ok, Tag 1 = OkEmpty, Tag 2 = Err, Tag 3 = InternalError. +type ReducerOutcome interface { + isReducerOutcome() +} + +// ReducerOk indicates the reducer succeeded and its transaction committed. +// Contains the return value and transaction update. +type ReducerOk struct { + RetValue []byte + TransactionUpdate *TransactionUpdate +} + +func (*ReducerOk) isReducerOutcome() {} + +// ReducerOkEmpty indicates the reducer succeeded with zero-length return value +// and zero query set updates. This is a wire-size optimization. +type ReducerOkEmpty struct{} + +func (*ReducerOkEmpty) isReducerOutcome() {} + +// ReducerErr indicates the reducer returned a structured error +// and its transaction did not commit. The payload is BSATN-encoded. +type ReducerErr struct { + ErrorBytes []byte +} + +func (*ReducerErr) isReducerOutcome() {} + +// ReducerInternalError indicates the reducer panicked or failed +// due to a SpacetimeDB internal error. +type ReducerInternalError struct { + Message string +} + +func (*ReducerInternalError) isReducerOutcome() {} + +// ProcedureStatus is a sum type representing the result of a procedure call. +// Tag 0 = Returned, Tag 1 = InternalError. +type ProcedureStatus interface { + isProcedureStatus() +} + +// ProcedureReturned indicates the procedure ran and returned a value. +type ProcedureReturned struct { + Value []byte +} + +func (*ProcedureReturned) isProcedureStatus() {} + +// ProcedureInternalError indicates the procedure call failed in the host. +type ProcedureInternalError struct { + Message string +} + +func (*ProcedureInternalError) isProcedureStatus() {} + +// ReducerResult is the server response to a CallReducer request. +type ReducerResult struct { + RequestID uint32 + Timestamp types.Timestamp + Result ReducerOutcome +} + +func (*ReducerResult) serverMessageTag() uint8 { return 6 } + +// ProcedureResult is the server response to a CallProcedure request. +// Field order matches the Rust definition: status, timestamp, total_host_execution_duration, request_id. +type ProcedureResult struct { + Status ProcedureStatus + Timestamp types.Timestamp + TotalHostExecutionDuration types.TimeDuration + RequestID uint32 +} + +func (*ProcedureResult) serverMessageTag() uint8 { return 7 } diff --git a/sdks/go/client/protocol/rowlist.go b/sdks/go/client/protocol/rowlist.go new file mode 100644 index 00000000000..5a6961a86ae --- /dev/null +++ b/sdks/go/client/protocol/rowlist.go @@ -0,0 +1,125 @@ +package protocol + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + +// RowSizeHint describes how to determine row boundaries within packed row data. +// It is a BSATN sum type: tag 0 = FixedSize(u16), tag 1 = RowOffsets(Vec). +type RowSizeHint interface { + isRowSizeHint() +} + +// FixedSizeHint indicates all rows in the list have the same fixed byte size. +type FixedSizeHint struct { + RowSize uint16 +} + +func (FixedSizeHint) isRowSizeHint() {} + +// RowOffsetsHint provides byte offsets into RowsData for each row's start position. +// The end of each row is inferred from the start of the next row, or the end of RowsData. +type RowOffsetsHint struct { + Offsets []uint64 +} + +func (RowOffsetsHint) isRowSizeHint() {} + +// BsatnRowList holds a packed list of BSATN-encoded rows with boundary metadata. +type BsatnRowList struct { + SizeHint RowSizeHint + RowsData []byte +} + +// ReadBsatnRowList reads a BsatnRowList from a BSATN reader. +func ReadBsatnRowList(r bsatn.Reader) (*BsatnRowList, error) { + tag, err := r.GetSumTag() + if err != nil { + return nil, err + } + + var hint RowSizeHint + switch tag { + case 0: // FixedSize(u16) + size, err := r.GetU16() + if err != nil { + return nil, err + } + hint = FixedSizeHint{RowSize: size} + case 1: // RowOffsets(Vec) + count, err := r.GetArrayLen() + if err != nil { + return nil, err + } + offsets := make([]uint64, count) + for i := uint32(0); i < count; i++ { + offsets[i], err = r.GetU64() + if err != nil { + return nil, err + } + } + hint = RowOffsetsHint{Offsets: offsets} + default: + return nil, &bsatn.ErrInvalidTag{Tag: tag, SumName: "RowSizeHint"} + } + + // rows_data: Bytes = u32 len + raw bytes + data, err := bsatn.ReadByteArray(r) + if err != nil { + return nil, err + } + + return &BsatnRowList{SizeHint: hint, RowsData: data}, nil +} + +// Rows returns individual row byte slices extracted using the size hint. +func (rl *BsatnRowList) Rows() [][]byte { + if rl == nil || len(rl.RowsData) == 0 { + return nil + } + + switch h := rl.SizeHint.(type) { + case FixedSizeHint: + if h.RowSize == 0 { + return nil + } + size := int(h.RowSize) + var rows [][]byte + for i := 0; i+size <= len(rl.RowsData); i += size { + rows = append(rows, rl.RowsData[i:i+size]) + } + return rows + case RowOffsetsHint: + rows := make([][]byte, 0, len(h.Offsets)) + for i, offset := range h.Offsets { + start := int(offset) + var end int + if i+1 < len(h.Offsets) { + end = int(h.Offsets[i+1]) + } else { + end = len(rl.RowsData) + } + if start <= len(rl.RowsData) && end <= len(rl.RowsData) && start <= end { + rows = append(rows, rl.RowsData[start:end]) + } + } + return rows + } + return nil +} + +// Len returns the number of rows in the list. +func (rl *BsatnRowList) Len() int { + if rl == nil || len(rl.RowsData) == 0 { + return 0 + } + + switch h := rl.SizeHint.(type) { + case FixedSizeHint: + if h.RowSize == 0 { + return 0 + } + return len(rl.RowsData) / int(h.RowSize) + case RowOffsetsHint: + return len(h.Offsets) + } + return 0 +} diff --git a/sdks/go/client/protocol/server_message.go b/sdks/go/client/protocol/server_message.go new file mode 100644 index 00000000000..ff8d7ceea8c --- /dev/null +++ b/sdks/go/client/protocol/server_message.go @@ -0,0 +1,101 @@ +package protocol + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// ServerMessage represents a message sent from the server to the client. +// It is a BSATN sum type with variants for each response kind. +type ServerMessage interface { + serverMessageTag() uint8 +} + +// InitialConnection is sent upon a successful connection. +// Tag 0 in the ServerMessage sum type. +type InitialConnection struct { + Identity types.Identity + ConnectionID types.ConnectionId + Token string +} + +func (*InitialConnection) serverMessageTag() uint8 { return 0 } + +// SubscribeApplied is sent in response to a Subscribe, containing initial matching rows. +// Tag 1 in the ServerMessage sum type. +type SubscribeApplied struct { + RequestID uint32 + QuerySetID uint32 + Rows QueryRows +} + +func (*SubscribeApplied) serverMessageTag() uint8 { return 1 } + +// UnsubscribeApplied confirms that a subscription has been removed. +// Tag 2 in the ServerMessage sum type. +type UnsubscribeApplied struct { + RequestID uint32 + QuerySetID uint32 + Rows *QueryRows // Option: nil means None +} + +func (*UnsubscribeApplied) serverMessageTag() uint8 { return 2 } + +// SubscriptionError notifies the client of a subscription failure. +// Tag 3 in the ServerMessage sum type. +type SubscriptionError struct { + RequestID *uint32 // Option: nil means None + QuerySetID uint32 + Error string +} + +func (*SubscriptionError) serverMessageTag() uint8 { return 3 } + +// TransactionUpdate is sent after a committed transaction, +// containing query set updates for affected subscriptions. +// Tag 4 in the ServerMessage sum type. +type TransactionUpdate struct { + QuerySets []QuerySetUpdate +} + +func (*TransactionUpdate) serverMessageTag() uint8 { return 4 } + +// OneOffQueryResult is sent in response to a OneOffQuery. +// Tag 5 in the ServerMessage sum type. +// Result is modeled as: non-nil QueryRows = Ok, non-empty ErrorMsg = Err. +type OneOffQueryResult struct { + RequestID uint32 + ResultOk *QueryRows // non-nil if the query succeeded + ResultErr string // non-empty if the query failed +} + +func (*OneOffQueryResult) serverMessageTag() uint8 { return 5 } + +// ReadServerMessage reads a ServerMessage from a BSATN reader by dispatching on the sum type tag. +func ReadServerMessage(r bsatn.Reader) (ServerMessage, error) { + tag, err := r.GetSumTag() + if err != nil { + return nil, err + } + + switch tag { + case 0: + return readInitialConnection(r) + case 1: + return readSubscribeApplied(r) + case 2: + return readUnsubscribeApplied(r) + case 3: + return readSubscriptionError(r) + case 4: + return readTransactionUpdate(r) + case 5: + return readOneOffQueryResult(r) + case 6: + return readReducerResult(r) + case 7: + return readProcedureResult(r) + default: + return nil, &bsatn.ErrInvalidTag{Tag: tag, SumName: "ServerMessage"} + } +} diff --git a/sdks/go/client/subscription.go b/sdks/go/client/subscription.go new file mode 100644 index 00000000000..e872d29b68a --- /dev/null +++ b/sdks/go/client/subscription.go @@ -0,0 +1,16 @@ +package client + +// SubscriptionBuilder configures and creates a subscription. +type SubscriptionBuilder interface { + OnApplied(fn func()) SubscriptionBuilder + Build() (SubscriptionHandle, error) +} + +// SubscriptionHandle represents an active subscription. +type SubscriptionHandle interface { + Unsubscribe() error + IsActive() bool +} + +// CallbackID identifies a registered callback. +type CallbackID uint64 diff --git a/sdks/go/client/ws/connection.go b/sdks/go/client/ws/connection.go new file mode 100644 index 00000000000..e22436b4e2a --- /dev/null +++ b/sdks/go/client/ws/connection.go @@ -0,0 +1,103 @@ +package ws + +import ( + "context" + "fmt" + "net/http" + + "github.com/coder/websocket" +) + +// Connection wraps a WebSocket connection for SpacetimeDB protocol v2. +type Connection interface { + Send(ctx context.Context, data []byte) error + Recv(ctx context.Context) ([]byte, error) + Close() error +} + +// ConnectionBuilder builds a WebSocket connection. +type ConnectionBuilder interface { + WithUri(uri string) ConnectionBuilder + WithDatabaseName(name string) ConnectionBuilder + WithToken(token string) ConnectionBuilder + WithProtocol(protocol string) ConnectionBuilder + Build(ctx context.Context) (Connection, error) +} + +func NewConnection() ConnectionBuilder { + return &connectionBuilder{ + protocol: "v2.bsatn.spacetimedb", + } +} + +type connectionBuilder struct { + uri string + database string + token string + protocol string +} + +func (b *connectionBuilder) WithUri(uri string) ConnectionBuilder { + b.uri = uri + return b +} + +func (b *connectionBuilder) WithDatabaseName(name string) ConnectionBuilder { + b.database = name + return b +} + +func (b *connectionBuilder) WithToken(token string) ConnectionBuilder { + b.token = token + return b +} + +func (b *connectionBuilder) WithProtocol(protocol string) ConnectionBuilder { + b.protocol = protocol + return b +} + +func (b *connectionBuilder) Build(ctx context.Context) (Connection, error) { + // Build WebSocket URL: ws://{host}/subscribe/{database} + wsURL := fmt.Sprintf("%s/subscribe/%s", b.uri, b.database) + + // Set up headers + headers := http.Header{} + if b.token != "" { + headers.Set("Authorization", fmt.Sprintf("Bearer %s", b.token)) + } + + // Dial with protocol negotiation + conn, _, err := websocket.Dial(ctx, wsURL, &websocket.DialOptions{ + Subprotocols: []string{b.protocol}, + HTTPHeader: headers, + }) + if err != nil { + return nil, fmt.Errorf("ws: dial failed: %w", err) + } + + // Set reasonable read limit (33MB like Rust server) + conn.SetReadLimit(33 * 1024 * 1024) + + return &connection{conn: conn}, nil +} + +type connection struct { + conn *websocket.Conn +} + +func (c *connection) Send(ctx context.Context, data []byte) error { + return c.conn.Write(ctx, websocket.MessageBinary, data) +} + +func (c *connection) Recv(ctx context.Context) ([]byte, error) { + _, data, err := c.conn.Read(ctx) + if err != nil { + return nil, err + } + return data, nil +} + +func (c *connection) Close() error { + return c.conn.Close(websocket.StatusNormalClosure, "client disconnect") +} diff --git a/sdks/go/client/ws/connection_test.go b/sdks/go/client/ws/connection_test.go new file mode 100644 index 00000000000..81da0c96e72 --- /dev/null +++ b/sdks/go/client/ws/connection_test.go @@ -0,0 +1,65 @@ +package ws_test + +import ( + "testing" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/client/ws" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewConnection_ReturnsBuilder(t *testing.T) { + builder := ws.NewConnection() + require.NotNil(t, builder) +} + +func TestConnectionBuilder_Chaining(t *testing.T) { + builder := ws.NewConnection(). + WithUri("ws://localhost:3000"). + WithDatabaseName("test-db"). + WithToken("my-token"). + WithProtocol("v2.bsatn.spacetimedb") + + require.NotNil(t, builder, "builder should not be nil after chaining") +} + +func TestConnectionBuilder_WithUri(t *testing.T) { + builder := ws.NewConnection().WithUri("ws://example.com:8080") + require.NotNil(t, builder) +} + +func TestConnectionBuilder_WithDatabaseName(t *testing.T) { + builder := ws.NewConnection().WithDatabaseName("my-database") + require.NotNil(t, builder) +} + +func TestConnectionBuilder_WithToken(t *testing.T) { + builder := ws.NewConnection().WithToken("auth-token-abc") + require.NotNil(t, builder) +} + +func TestConnectionBuilder_WithProtocol(t *testing.T) { + builder := ws.NewConnection().WithProtocol("v2.bsatn.spacetimedb") + require.NotNil(t, builder) +} + +func TestConnectionBuilder_AllFieldsChained(t *testing.T) { + // Verify that all builder methods can be chained in any order + builder := ws.NewConnection(). + WithToken("tok"). + WithProtocol("v2.bsatn.spacetimedb"). + WithDatabaseName("db"). + WithUri("ws://host:1234") + + require.NotNil(t, builder) +} + +func TestConnectionBuilder_InterfaceTypes(t *testing.T) { + // Verify the interface types exist and are distinct + var _ ws.Connection + var _ ws.ConnectionBuilder + + builder := ws.NewConnection() + var cb ws.ConnectionBuilder = builder + assert.NotNil(t, cb) +} diff --git a/sdks/go/go.mod b/sdks/go/go.mod new file mode 100644 index 00000000000..c536409eeee --- /dev/null +++ b/sdks/go/go.mod @@ -0,0 +1,16 @@ +module github.com/clockworklabs/SpacetimeDB/sdks/go + +go 1.23 + +require ( + github.com/andybalholm/brotli v1.1.1 + github.com/coder/websocket v1.8.12 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/sdks/go/go.sum b/sdks/go/go.sum new file mode 100644 index 00000000000..8655de5aefa --- /dev/null +++ b/sdks/go/go.sum @@ -0,0 +1,18 @@ +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sdks/go/server/http/http.go b/sdks/go/server/http/http.go new file mode 100644 index 00000000000..ea8367b702a --- /dev/null +++ b/sdks/go/server/http/http.go @@ -0,0 +1,190 @@ +package http + +import ( + "fmt" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/sys" +) + +// Method represents an HTTP method as a BSATN sum type. +type method uint8 + +const ( + MethodGet method = 0 + MethodHead method = 1 + MethodPost method = 2 + MethodPut method = 3 + MethodDelete method = 4 + MethodConnect method = 5 + MethodOptions method = 6 + MethodTrace method = 7 + MethodPatch method = 8 + MethodExtension method = 9 // carries a string payload +) + +// version represents an HTTP version as a BSATN sum type. +type version uint8 + +const ( + versionHTTP09 version = 0 + versionHTTP10 version = 1 + versionHTTP11 version = 2 + versionHTTP2 version = 3 + versionHTTP3 version = 4 +) + +// writeMethod writes a Method sum type to BSATN. +// Simple variants (0-8) are tag-only (empty product payload). +// Extension(9) carries a String payload. +func writeMethod(w bsatn.Writer, m method) { + w.PutSumTag(uint8(m)) + // Tags 0-8 have no payload (empty product). Tag 9 has a string payload. + // We only support standard methods in this implementation. +} + +// writeVersion writes a Version sum type to BSATN (tag-only, empty product payload). +func writeVersion(w bsatn.Writer, v version) { + w.PutSumTag(uint8(v)) +} + +// writeHeaders writes a Headers product type to BSATN. +// Headers is a product with a single field: entries (array of HttpHeaderPair). +func writeHeaders(w bsatn.Writer, headers map[string]string) { + w.PutArrayLen(uint32(len(headers))) + for name, value := range headers { + // HttpHeaderPair is a product: name (String), value (byte array) + w.PutString(name) + // value is Box<[u8]> which encodes as BSATN array of u8: u32 len + raw bytes + w.PutArrayLen(uint32(len(value))) + w.PutBytes([]byte(value)) + } +} + +// writeOptionNone writes an Option::None (tag 1, empty payload). +func writeOptionNone(w bsatn.Writer) { + w.PutSumTag(1) +} + +// encodeRequest BSATN-encodes an HttpRequest product type. +// Request fields (in order): method, headers, timeout (Option), uri, version. +func encodeRequest(m method, uri string, headers map[string]string) []byte { + w := bsatn.NewWriter(256) + + // Field 1: method + writeMethod(w, m) + + // Field 2: headers + writeHeaders(w, headers) + + // Field 3: timeout (Option) - always None for now + writeOptionNone(w) + + // Field 4: uri + w.PutString(uri) + + // Field 5: version - default to HTTP/1.1 + writeVersion(w, versionHTTP11) + + return w.Bytes() +} + +// response holds the decoded HTTP response. +type response struct { + code uint16 +} + +// decodeResponse decodes a BSATN-encoded HttpResponse. +// Response fields: headers (Headers), version (Version), code (u16). +func decodeResponse(data []byte) (*response, error) { + r := bsatn.NewReader(data) + + // Field 1: headers (product with 1 field: array of HttpHeaderPair) + numHeaders, err := r.GetArrayLen() + if err != nil { + return nil, fmt.Errorf("decode response headers length: %w", err) + } + for i := uint32(0); i < numHeaders; i++ { + // HttpHeaderPair: name (String), value (byte array) + // Skip name + nameLen, err := r.GetU32() + if err != nil { + return nil, fmt.Errorf("decode header name length: %w", err) + } + if _, err := r.GetBytes(int(nameLen)); err != nil { + return nil, fmt.Errorf("decode header name: %w", err) + } + // Skip value (byte array: u32 len + bytes) + valueLen, err := r.GetU32() + if err != nil { + return nil, fmt.Errorf("decode header value length: %w", err) + } + if _, err := r.GetBytes(int(valueLen)); err != nil { + return nil, fmt.Errorf("decode header value: %w", err) + } + } + + // Field 2: version (sum type, tag only for standard versions) + if _, err := r.GetSumTag(); err != nil { + return nil, fmt.Errorf("decode response version: %w", err) + } + + // Field 3: code (u16) + code, err := r.GetU16() + if err != nil { + return nil, fmt.Errorf("decode response code: %w", err) + } + + return &response{code: code}, nil +} + +// decodeBsatnString decodes a BSATN-encoded string (u32 len + UTF-8 bytes). +func decodeBsatnString(data []byte) (string, error) { + r := bsatn.NewReader(data) + return r.GetString() +} + +// Get performs an HTTP GET request and returns the status code and response body. +func Get(uri string) (uint16, []byte, error) { + return Send(MethodGet, uri, nil, nil) +} + +// Send performs an HTTP request with the given method, URI, headers, and body. +// Returns the HTTP status code and response body. +func Send(m method, uri string, headers map[string]string, body []byte) (uint16, []byte, error) { + requestBsatn := encodeRequest(m, uri, headers) + + responseSrc, bodySrc, err := sys.ProcedureHttpRequest(requestBsatn, body) + if err != nil { + // On HTTP_ERROR, responseSrc has BSATN-encoded error string. + if responseSrc != 0 { + errData, readErr := sys.ReadBytesSource(responseSrc) + if readErr == nil && len(errData) > 0 { + errMsg, decErr := decodeBsatnString(errData) + if decErr == nil { + return 0, nil, fmt.Errorf("%s", errMsg) + } + } + } + return 0, nil, fmt.Errorf("http request failed: %w", err) + } + + // Read response BSATN. + respData, err := sys.ReadBytesSource(responseSrc) + if err != nil { + return 0, nil, fmt.Errorf("read response: %w", err) + } + + resp, err := decodeResponse(respData) + if err != nil { + return 0, nil, fmt.Errorf("decode response: %w", err) + } + + // Read response body. + respBody, err := sys.ReadBytesSource(bodySrc) + if err != nil { + return 0, nil, fmt.Errorf("read response body: %w", err) + } + + return resp.code, respBody, nil +} diff --git a/sdks/go/server/log/log.go b/sdks/go/server/log/log.go new file mode 100644 index 00000000000..3d500b23f59 --- /dev/null +++ b/sdks/go/server/log/log.go @@ -0,0 +1,41 @@ +package log + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/server/sys" + +// Logger provides logging via SpacetimeDB host console_log. +type Logger interface { + Error(msg string) + Warn(msg string) + Info(msg string) + Debug(msg string) + Trace(msg string) +} + +// NewLogger creates a Logger that writes to the host console with the given target name. +func NewLogger(target string) Logger { + return &logger{target: target} +} + +type logger struct { + target string +} + +func (l *logger) Error(msg string) { + sys.ConsoleLog(sys.LogLevelError, l.target, "", 0, msg) +} + +func (l *logger) Warn(msg string) { + sys.ConsoleLog(sys.LogLevelWarn, l.target, "", 0, msg) +} + +func (l *logger) Info(msg string) { + sys.ConsoleLog(sys.LogLevelInfo, l.target, "", 0, msg) +} + +func (l *logger) Debug(msg string) { + sys.ConsoleLog(sys.LogLevelDebug, l.target, "", 0, msg) +} + +func (l *logger) Trace(msg string) { + sys.ConsoleLog(sys.LogLevelTrace, l.target, "", 0, msg) +} diff --git a/sdks/go/server/moduledef/constraint_def.go b/sdks/go/server/moduledef/constraint_def.go new file mode 100644 index 00000000000..5cb95ad200f --- /dev/null +++ b/sdks/go/server/moduledef/constraint_def.go @@ -0,0 +1,47 @@ +package moduledef + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + +// ConstraintDef defines a constraint on a table. +type ConstraintDef interface { + bsatn.Serializable +} + +// NewUniqueConstraint creates a unique constraint definition. +// sourceName is optional (nil for auto-generated). columns are the column IDs. +func NewUniqueConstraint(sourceName *string, columns ...uint16) ConstraintDef { + return &constraintDef{ + sourceName: sourceName, + columns: columns, + } +} + +type constraintDef struct { + sourceName *string + columns []uint16 +} + +// WriteBsatn encodes the constraint definition as BSATN. +// +// Matches RawConstraintDefV10 product field order: +// +// source_name: Option +// data: RawConstraintDataV10 (sum type, Unique=0) +// +// RawUniqueConstraintDataV9 product: +// +// columns: ColList (array of u16) +func (c *constraintDef) WriteBsatn(w bsatn.Writer) { + // source_name: Option + writeOptionString(w, c.sourceName) + + // data: RawConstraintDataV10 (sum type) + // Only variant: Unique = tag 0 + w.PutSumTag(0) + + // RawUniqueConstraintDataV9 product: columns: ColList + w.PutArrayLen(uint32(len(c.columns))) + for _, col := range c.columns { + w.PutU16(col) + } +} diff --git a/sdks/go/server/moduledef/index_def.go b/sdks/go/server/moduledef/index_def.go new file mode 100644 index 00000000000..9124c35c235 --- /dev/null +++ b/sdks/go/server/moduledef/index_def.go @@ -0,0 +1,112 @@ +package moduledef + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + +// IndexAlgo defines the index algorithm. +// BSATN enum: BTree=0, Hash=1, Direct=2. +type IndexAlgo uint8 + +const ( + IndexAlgoBTree IndexAlgo = 0 + IndexAlgoHash IndexAlgo = 1 + IndexAlgoDirect IndexAlgo = 2 +) + +// IndexDef defines an index on a table. +type IndexDef interface { + bsatn.Serializable +} + +// IndexDefBuilder builds an IndexDef. +type IndexDefBuilder interface { + WithAccessorName(name string) IndexDefBuilder + Build() IndexDef +} + +// NewBTreeIndexDef creates a BTree index definition. +// sourceName is optional (nil for auto-generated). columns are the column IDs. +func NewBTreeIndexDef(sourceName *string, columns ...uint16) IndexDefBuilder { + return &indexDef{ + sourceName: sourceName, + algo: IndexAlgoBTree, + columns: columns, + } +} + +// NewHashIndexDef creates a Hash index definition. +func NewHashIndexDef(sourceName *string, columns ...uint16) IndexDefBuilder { + return &indexDef{ + sourceName: sourceName, + algo: IndexAlgoHash, + columns: columns, + } +} + +// NewDirectIndexDef creates a Direct index definition. +func NewDirectIndexDef(sourceName *string, column uint16) IndexDefBuilder { + return &indexDef{ + sourceName: sourceName, + algo: IndexAlgoDirect, + columns: []uint16{column}, + } +} + +type indexDef struct { + sourceName *string + accessorName *string + algo IndexAlgo + columns []uint16 +} + +func (d *indexDef) WithAccessorName(name string) IndexDefBuilder { + d.accessorName = &name + return d +} + +func (d *indexDef) Build() IndexDef { + return d +} + +// WriteBsatn encodes the index definition as BSATN. +// +// Matches RawIndexDefV10 product field order: +// +// source_name: Option +// accessor_name: Option +// algorithm: RawIndexAlgorithm (sum type) +func (d *indexDef) WriteBsatn(w bsatn.Writer) { + // source_name: Option + writeOptionString(w, d.sourceName) + + // accessor_name: Option + writeOptionString(w, d.accessorName) + + // algorithm: RawIndexAlgorithm (sum type) + // BTree=0 { columns: ColList }, Hash=1 { columns: ColList }, Direct=2 { column: ColId } + w.PutSumTag(uint8(d.algo)) + switch d.algo { + case IndexAlgoBTree, IndexAlgoHash: + // columns: ColList (array of u16) + w.PutArrayLen(uint32(len(d.columns))) + for _, col := range d.columns { + w.PutU16(col) + } + case IndexAlgoDirect: + // column: ColId (u16) + if len(d.columns) > 0 { + w.PutU16(d.columns[0]) + } else { + w.PutU16(0) + } + } +} + +// writeOptionString writes an Option as BSATN. +func writeOptionString(w bsatn.Writer, s *string) { + if s != nil { + w.PutSumTag(0) // Some + w.PutString(*s) + } else { + w.PutSumTag(1) // None + } +} diff --git a/sdks/go/server/moduledef/moduledef.go b/sdks/go/server/moduledef/moduledef.go new file mode 100644 index 00000000000..9f44874cd9d --- /dev/null +++ b/sdks/go/server/moduledef/moduledef.go @@ -0,0 +1,30 @@ +package moduledef + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// ModuleDefBuilder builds a RawModuleDefV10 for __describe_module__. +type ModuleDefBuilder interface { + SetTypespace(ts types.Typespace) ModuleDefBuilder + AddTypeDef(def TypeDef) ModuleDefBuilder + AddTable(def TableDef) ModuleDefBuilder + AddReducer(def ReducerDef) ModuleDefBuilder + AddProcedure(def ProcedureDef) ModuleDefBuilder + AddView(def ViewDef) ModuleDefBuilder + AddSchedule(def ScheduleDef) ModuleDefBuilder + AddLifecycleReducer(def LifecycleReducerDef) ModuleDefBuilder + AddRowLevelSecurity(sql string) ModuleDefBuilder + Build() ModuleDef +} + +// ModuleDef is a built module definition ready for BSATN encoding. +type ModuleDef interface { + bsatn.Serializable +} + +// NewModuleDefBuilder creates a new ModuleDefBuilder. +func NewModuleDefBuilder() ModuleDefBuilder { + return &moduleDefBuilder{} +} diff --git a/sdks/go/server/moduledef/moduledef_impl.go b/sdks/go/server/moduledef/moduledef_impl.go new file mode 100644 index 00000000000..df53989ee17 --- /dev/null +++ b/sdks/go/server/moduledef/moduledef_impl.go @@ -0,0 +1,227 @@ +package moduledef + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +type moduleDefBuilder struct { + typespace types.Typespace + typeDefs []TypeDef + tables []TableDef + reducers []ReducerDef + procedures []ProcedureDef + views []ViewDef + schedules []ScheduleDef + lifecycleReducers []LifecycleReducerDef + rlsFilters []string +} + +func (b *moduleDefBuilder) SetTypespace(ts types.Typespace) ModuleDefBuilder { + b.typespace = ts + return b +} + +func (b *moduleDefBuilder) AddTypeDef(def TypeDef) ModuleDefBuilder { + b.typeDefs = append(b.typeDefs, def) + return b +} + +func (b *moduleDefBuilder) AddTable(def TableDef) ModuleDefBuilder { + b.tables = append(b.tables, def) + return b +} + +func (b *moduleDefBuilder) AddReducer(def ReducerDef) ModuleDefBuilder { + b.reducers = append(b.reducers, def) + return b +} + +func (b *moduleDefBuilder) AddProcedure(def ProcedureDef) ModuleDefBuilder { + b.procedures = append(b.procedures, def) + return b +} + +func (b *moduleDefBuilder) AddView(def ViewDef) ModuleDefBuilder { + b.views = append(b.views, def) + return b +} + +func (b *moduleDefBuilder) AddSchedule(def ScheduleDef) ModuleDefBuilder { + b.schedules = append(b.schedules, def) + return b +} + +func (b *moduleDefBuilder) AddLifecycleReducer(def LifecycleReducerDef) ModuleDefBuilder { + b.lifecycleReducers = append(b.lifecycleReducers, def) + return b +} + +func (b *moduleDefBuilder) AddRowLevelSecurity(sql string) ModuleDefBuilder { + b.rlsFilters = append(b.rlsFilters, sql) + return b +} + +func (b *moduleDefBuilder) Build() ModuleDef { + return &moduleDef{ + typespace: b.typespace, + typeDefs: b.typeDefs, + tables: b.tables, + reducers: b.reducers, + procedures: b.procedures, + views: b.views, + schedules: b.schedules, + lifecycleReducers: b.lifecycleReducers, + rlsFilters: b.rlsFilters, + } +} + +type moduleDef struct { + typespace types.Typespace + typeDefs []TypeDef + tables []TableDef + reducers []ReducerDef + procedures []ProcedureDef + views []ViewDef + schedules []ScheduleDef + lifecycleReducers []LifecycleReducerDef + rlsFilters []string +} + +// WriteBsatn encodes the module definition as BSATN. +// +// Outer encoding: RawModuleDef sum type, tag 2 = V10. +// V10 content: product with one field: sections (Vec). +// Each section is a sum type with tags 0-10. +func (m *moduleDef) WriteBsatn(w bsatn.Writer) { + // Outer: RawModuleDef sum type, tag 2 = V10 + w.PutSumTag(2) + + // V10 is a struct with one field: sections: Vec
+ // Count the non-empty sections. + sectionCount := uint32(0) + if m.typespace != nil { + sectionCount++ + } + if len(m.typeDefs) > 0 { + sectionCount++ + } + if len(m.tables) > 0 { + sectionCount++ + } + if len(m.reducers) > 0 { + sectionCount++ + } + if len(m.procedures) > 0 { + sectionCount++ + } + if len(m.views) > 0 { + sectionCount++ + } + if len(m.schedules) > 0 { + sectionCount++ + } + if len(m.lifecycleReducers) > 0 { + sectionCount++ + } + if len(m.rlsFilters) > 0 { + sectionCount++ + } + + w.PutArrayLen(sectionCount) + + // Section tag 0: Typespace + if m.typespace != nil { + w.PutSumTag(sectionTagTypespace) + m.typespace.WriteBsatn(w) + } + + // Section tag 1: Types + if len(m.typeDefs) > 0 { + w.PutSumTag(sectionTagTypes) + w.PutArrayLen(uint32(len(m.typeDefs))) + for _, td := range m.typeDefs { + td.WriteBsatn(w) + } + } + + // Section tag 2: Tables + if len(m.tables) > 0 { + w.PutSumTag(sectionTagTables) + w.PutArrayLen(uint32(len(m.tables))) + for _, t := range m.tables { + t.WriteBsatn(w) + } + } + + // Section tag 3: Reducers + if len(m.reducers) > 0 { + w.PutSumTag(sectionTagReducers) + w.PutArrayLen(uint32(len(m.reducers))) + for _, r := range m.reducers { + r.WriteBsatn(w) + } + } + + // Section tag 4: Procedures + if len(m.procedures) > 0 { + w.PutSumTag(sectionTagProcedures) + w.PutArrayLen(uint32(len(m.procedures))) + for _, p := range m.procedures { + p.WriteBsatn(w) + } + } + + // Section tag 5: Views + if len(m.views) > 0 { + w.PutSumTag(sectionTagViews) + w.PutArrayLen(uint32(len(m.views))) + for _, v := range m.views { + v.WriteBsatn(w) + } + } + + // Section tag 6: Schedules + if len(m.schedules) > 0 { + w.PutSumTag(sectionTagSchedules) + w.PutArrayLen(uint32(len(m.schedules))) + for _, s := range m.schedules { + s.WriteBsatn(w) + } + } + + // Section tag 7: LifeCycleReducers + if len(m.lifecycleReducers) > 0 { + w.PutSumTag(sectionTagLifeCycleReducers) + w.PutArrayLen(uint32(len(m.lifecycleReducers))) + for _, lr := range m.lifecycleReducers { + lr.WriteBsatn(w) + } + } + + // Section tag 8: RowLevelSecurity + if len(m.rlsFilters) > 0 { + w.PutSumTag(sectionTagRowLevelSecurity) + w.PutArrayLen(uint32(len(m.rlsFilters))) + for _, sql := range m.rlsFilters { + // Each RLS filter is a product type with one field: sql (a string). + w.PutString(sql) + } + } + +} + +// Section tag constants matching RawModuleDefV10Section enum variants. +const ( + sectionTagTypespace = 0 + sectionTagTypes = 1 + sectionTagTables = 2 + sectionTagReducers = 3 + sectionTagProcedures = 4 + sectionTagViews = 5 + sectionTagSchedules = 6 + sectionTagLifeCycleReducers = 7 + sectionTagRowLevelSecurity = 8 + sectionTagCaseConversion = 9 + sectionTagExplicitNames = 10 +) diff --git a/sdks/go/server/moduledef/procedure_def.go b/sdks/go/server/moduledef/procedure_def.go new file mode 100644 index 00000000000..f6df75c64fe --- /dev/null +++ b/sdks/go/server/moduledef/procedure_def.go @@ -0,0 +1,83 @@ +package moduledef + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// ProcedureDef defines a procedure in the module. +type ProcedureDef interface { + bsatn.Serializable +} + +// ProcedureDefBuilder builds a ProcedureDef. +type ProcedureDefBuilder interface { + WithParams(params types.ProductType) ProcedureDefBuilder + WithReturnType(returnType types.AlgebraicType) ProcedureDefBuilder + WithVisibility(v FunctionVisibility) ProcedureDefBuilder + Build() ProcedureDef +} + +// NewProcedureDefBuilder creates a ProcedureDefBuilder with the given source name. +func NewProcedureDefBuilder(sourceName string) ProcedureDefBuilder { + return &procedureDef{ + sourceName: sourceName, + visibility: FunctionVisibilityClientCallable, + } +} + +type procedureDef struct { + sourceName string + params types.ProductType + returnType types.AlgebraicType + visibility FunctionVisibility +} + +func (p *procedureDef) WithParams(params types.ProductType) ProcedureDefBuilder { + p.params = params + return p +} + +func (p *procedureDef) WithReturnType(returnType types.AlgebraicType) ProcedureDefBuilder { + p.returnType = returnType + return p +} + +func (p *procedureDef) WithVisibility(v FunctionVisibility) ProcedureDefBuilder { + p.visibility = v + return p +} + +func (p *procedureDef) Build() ProcedureDef { + return p +} + +// WriteBsatn encodes the procedure definition as BSATN. +// +// Matches RawProcedureDefV10 product field order: +// +// source_name: String +// params: ProductType +// return_type: AlgebraicType +// visibility: FunctionVisibility (sum tag) +func (p *procedureDef) WriteBsatn(w bsatn.Writer) { + // source_name: String + w.PutString(p.sourceName) + + // params: ProductType + if p.params != nil { + p.params.WriteBsatn(w) + } else { + w.PutArrayLen(0) + } + + // return_type: AlgebraicType + if p.returnType != nil { + p.returnType.WriteBsatn(w) + } else { + types.AlgTypeProduct(types.NewProductType()).WriteBsatn(w) + } + + // visibility: FunctionVisibility (sum tag) + w.PutSumTag(uint8(p.visibility)) +} diff --git a/sdks/go/server/moduledef/reducer_def.go b/sdks/go/server/moduledef/reducer_def.go new file mode 100644 index 00000000000..de45a69c3d1 --- /dev/null +++ b/sdks/go/server/moduledef/reducer_def.go @@ -0,0 +1,152 @@ +package moduledef + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// FunctionVisibility defines reducer/procedure visibility. +// BSATN enum: Private=0, ClientCallable=1. +type FunctionVisibility uint8 + +const ( + FunctionVisibilityPrivate FunctionVisibility = 0 + FunctionVisibilityClientCallable FunctionVisibility = 1 +) + +// ReducerDef defines a reducer in the module. +type ReducerDef interface { + bsatn.Serializable +} + +// ReducerDefBuilder builds a ReducerDef. +type ReducerDefBuilder interface { + WithParams(params types.ProductType) ReducerDefBuilder + WithVisibility(v FunctionVisibility) ReducerDefBuilder + WithOkReturnType(t types.AlgebraicType) ReducerDefBuilder + WithErrReturnType(t types.AlgebraicType) ReducerDefBuilder + Build() ReducerDef +} + +// NewReducerDefBuilder creates a ReducerDefBuilder with the given source name. +func NewReducerDefBuilder(sourceName string) ReducerDefBuilder { + return &reducerDef{ + sourceName: sourceName, + visibility: FunctionVisibilityClientCallable, + } +} + +type reducerDef struct { + sourceName string + params types.ProductType + visibility FunctionVisibility + okReturnType types.AlgebraicType + errReturnType types.AlgebraicType +} + +func (r *reducerDef) WithParams(params types.ProductType) ReducerDefBuilder { + r.params = params + return r +} + +func (r *reducerDef) WithVisibility(v FunctionVisibility) ReducerDefBuilder { + r.visibility = v + return r +} + +func (r *reducerDef) WithOkReturnType(t types.AlgebraicType) ReducerDefBuilder { + r.okReturnType = t + return r +} + +func (r *reducerDef) WithErrReturnType(t types.AlgebraicType) ReducerDefBuilder { + r.errReturnType = t + return r +} + +func (r *reducerDef) Build() ReducerDef { + return r +} + +// WriteBsatn encodes the reducer definition as BSATN. +// +// Matches RawReducerDefV10 product field order: +// +// source_name: String +// params: ProductType +// visibility: FunctionVisibility (sum tag) +// ok_return_type: AlgebraicType +// err_return_type: AlgebraicType +func (r *reducerDef) WriteBsatn(w bsatn.Writer) { + // source_name: String + w.PutString(r.sourceName) + + // params: ProductType + if r.params != nil { + r.params.WriteBsatn(w) + } else { + // Empty product type: 0 elements + w.PutArrayLen(0) + } + + // visibility: FunctionVisibility (sum tag) + w.PutSumTag(uint8(r.visibility)) + + // ok_return_type: AlgebraicType + if r.okReturnType != nil { + r.okReturnType.WriteBsatn(w) + } else { + // Default: empty product type (unit) - tag 2 for Product, then empty elements + types.AlgTypeProduct(types.NewProductType()).WriteBsatn(w) + } + + // err_return_type: AlgebraicType + if r.errReturnType != nil { + r.errReturnType.WriteBsatn(w) + } else { + // Default: empty product type (unit) + types.AlgTypeProduct(types.NewProductType()).WriteBsatn(w) + } +} + +// Lifecycle defines lifecycle event types. +// BSATN enum: Init=0, OnConnect=1, OnDisconnect=2. +type Lifecycle uint8 + +const ( + LifecycleInit Lifecycle = 0 + LifecycleOnConnect Lifecycle = 1 + LifecycleOnDisconnect Lifecycle = 2 +) + +// LifecycleReducerDef defines a lifecycle reducer assignment. +type LifecycleReducerDef interface { + bsatn.Serializable +} + +// NewLifecycleReducerDef creates a LifecycleReducerDef. +func NewLifecycleReducerDef(lifecycle Lifecycle, functionName string) LifecycleReducerDef { + return &lifecycleReducerDef{ + lifecycle: lifecycle, + functionName: functionName, + } +} + +type lifecycleReducerDef struct { + lifecycle Lifecycle + functionName string +} + +// WriteBsatn encodes the lifecycle reducer definition as BSATN. +// +// Matches RawLifeCycleReducerDefV10 product field order: +// +// lifecycle_spec: Lifecycle (sum tag) +// function_name: String +func (lr *lifecycleReducerDef) WriteBsatn(w bsatn.Writer) { + // lifecycle_spec: Lifecycle (sum tag) + w.PutSumTag(uint8(lr.lifecycle)) + + // function_name: String + w.PutString(lr.functionName) +} diff --git a/sdks/go/server/moduledef/schedule_def.go b/sdks/go/server/moduledef/schedule_def.go new file mode 100644 index 00000000000..897638ba93e --- /dev/null +++ b/sdks/go/server/moduledef/schedule_def.go @@ -0,0 +1,51 @@ +package moduledef + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + +// ScheduleDef defines a schedule in the module. +type ScheduleDef interface { + bsatn.Serializable +} + +// NewScheduleDef creates a ScheduleDef. +// sourceName is optional (nil for auto-generated). +// tableName is the schedule table name. +// scheduleAtCol is the column index of the scheduled_at field. +// functionName is the reducer or procedure to call. +func NewScheduleDef(sourceName *string, tableName string, scheduleAtCol uint16, functionName string) ScheduleDef { + return &scheduleDef{ + sourceName: sourceName, + tableName: tableName, + scheduleAtCol: scheduleAtCol, + functionName: functionName, + } +} + +type scheduleDef struct { + sourceName *string + tableName string + scheduleAtCol uint16 + functionName string +} + +// WriteBsatn encodes the schedule definition as BSATN. +// +// Matches RawScheduleDefV10 product field order: +// +// source_name: Option +// table_name: String +// schedule_at_col: ColId (u16) +// function_name: String +func (s *scheduleDef) WriteBsatn(w bsatn.Writer) { + // source_name: Option + writeOptionString(w, s.sourceName) + + // table_name: String + w.PutString(s.tableName) + + // schedule_at_col: ColId (u16) + w.PutU16(s.scheduleAtCol) + + // function_name: String + w.PutString(s.functionName) +} diff --git a/sdks/go/server/moduledef/sequence_def.go b/sdks/go/server/moduledef/sequence_def.go new file mode 100644 index 00000000000..f39f0cc0885 --- /dev/null +++ b/sdks/go/server/moduledef/sequence_def.go @@ -0,0 +1,117 @@ +package moduledef + +import ( + "encoding/binary" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// SequenceDef defines a sequence for a table column. +type SequenceDef interface { + bsatn.Serializable +} + +// SequenceDefBuilder builds a SequenceDef. +type SequenceDefBuilder interface { + WithStart(start int64) SequenceDefBuilder + WithMinValue(min int64) SequenceDefBuilder + WithMaxValue(max int64) SequenceDefBuilder + WithIncrement(inc int64) SequenceDefBuilder + Build() SequenceDef +} + +// NewSequenceDefBuilder creates a SequenceDefBuilder. +// sourceName is optional (nil for auto-generated). column is the column ID. +func NewSequenceDefBuilder(sourceName *string, column uint16) SequenceDefBuilder { + return &sequenceDef{ + sourceName: sourceName, + column: column, + increment: 1, + } +} + +type sequenceDef struct { + sourceName *string + column uint16 + start *int64 + minValue *int64 + maxValue *int64 + increment int64 +} + +func (s *sequenceDef) WithStart(start int64) SequenceDefBuilder { + s.start = &start + return s +} + +func (s *sequenceDef) WithMinValue(min int64) SequenceDefBuilder { + s.minValue = &min + return s +} + +func (s *sequenceDef) WithMaxValue(max int64) SequenceDefBuilder { + s.maxValue = &max + return s +} + +func (s *sequenceDef) WithIncrement(inc int64) SequenceDefBuilder { + s.increment = inc + return s +} + +func (s *sequenceDef) Build() SequenceDef { + return s +} + +// WriteBsatn encodes the sequence definition as BSATN. +// +// Matches RawSequenceDefV10 product field order: +// +// source_name: Option +// column: ColId (u16) +// start: Option +// min_value: Option +// max_value: Option +// increment: i128 +func (s *sequenceDef) WriteBsatn(w bsatn.Writer) { + // source_name: Option + writeOptionString(w, s.sourceName) + + // column: ColId (u16) + w.PutU16(s.column) + + // start: Option + writeOptionI128(w, s.start) + + // min_value: Option + writeOptionI128(w, s.minValue) + + // max_value: Option + writeOptionI128(w, s.maxValue) + + // increment: i128 (16 bytes LE, sign-extended from int64) + writeI128(w, s.increment) +} + +// writeI128 writes an i128 value as 16 bytes LE, sign-extended from int64. +func writeI128(w bsatn.Writer, v int64) { + var buf [16]byte + binary.LittleEndian.PutUint64(buf[0:8], uint64(v)) + // Sign-extend: if negative, fill high 8 bytes with 0xFF + var hi uint64 + if v < 0 { + hi = ^uint64(0) + } + binary.LittleEndian.PutUint64(buf[8:16], hi) + w.PutBytes(buf[:]) +} + +// writeOptionI128 writes an Option as BSATN. +func writeOptionI128(w bsatn.Writer, v *int64) { + if v != nil { + w.PutSumTag(0) // Some + writeI128(w, *v) + } else { + w.PutSumTag(1) // None + } +} diff --git a/sdks/go/server/moduledef/table_def.go b/sdks/go/server/moduledef/table_def.go new file mode 100644 index 00000000000..1fc09fd78a5 --- /dev/null +++ b/sdks/go/server/moduledef/table_def.go @@ -0,0 +1,198 @@ +package moduledef + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// TableAccess defines table visibility. +// BSATN enum: Public=0, Private=1. +type TableAccess uint8 + +const ( + TableAccessPublic TableAccess = 0 + TableAccessPrivate TableAccess = 1 +) + +// TableType defines table category. +// BSATN enum: System=0, User=1. +type TableType uint8 + +const ( + TableTypeSystem TableType = 0 + TableTypeUser TableType = 1 +) + +// TableDef defines a table in the module. +type TableDef interface { + bsatn.Serializable +} + +// TableDefBuilder builds a TableDef. +type TableDefBuilder interface { + WithProductTypeRef(ref types.TypeRef) TableDefBuilder + WithPrimaryKey(cols ...uint16) TableDefBuilder + WithIndex(idx IndexDef) TableDefBuilder + WithConstraint(c ConstraintDef) TableDefBuilder + WithSequence(s SequenceDef) TableDefBuilder + WithTableType(tt TableType) TableDefBuilder + WithTableAccess(ta TableAccess) TableDefBuilder + WithDefaultValue(dv ColumnDefaultValue) TableDefBuilder + WithIsEvent(isEvent bool) TableDefBuilder + Build() TableDef +} + +// NewTableDefBuilder creates a TableDefBuilder with the given source name. +func NewTableDefBuilder(sourceName string) TableDefBuilder { + return &tableDef{ + sourceName: sourceName, + tableType: TableTypeUser, + tableAccess: TableAccessPublic, + } +} + +type tableDef struct { + sourceName string + productTypeRef types.TypeRef + primaryKey []uint16 + indexes []IndexDef + constraints []ConstraintDef + sequences []SequenceDef + tableType TableType + tableAccess TableAccess + defaultValues []ColumnDefaultValue + isEvent bool +} + +func (t *tableDef) WithProductTypeRef(ref types.TypeRef) TableDefBuilder { + t.productTypeRef = ref + return t +} + +func (t *tableDef) WithPrimaryKey(cols ...uint16) TableDefBuilder { + t.primaryKey = cols + return t +} + +func (t *tableDef) WithIndex(idx IndexDef) TableDefBuilder { + t.indexes = append(t.indexes, idx) + return t +} + +func (t *tableDef) WithConstraint(c ConstraintDef) TableDefBuilder { + t.constraints = append(t.constraints, c) + return t +} + +func (t *tableDef) WithSequence(s SequenceDef) TableDefBuilder { + t.sequences = append(t.sequences, s) + return t +} + +func (t *tableDef) WithTableType(tt TableType) TableDefBuilder { + t.tableType = tt + return t +} + +func (t *tableDef) WithTableAccess(ta TableAccess) TableDefBuilder { + t.tableAccess = ta + return t +} + +func (t *tableDef) WithDefaultValue(dv ColumnDefaultValue) TableDefBuilder { + t.defaultValues = append(t.defaultValues, dv) + return t +} + +func (t *tableDef) WithIsEvent(isEvent bool) TableDefBuilder { + t.isEvent = isEvent + return t +} + +func (t *tableDef) Build() TableDef { + return t +} + +// WriteBsatn encodes the table definition as BSATN. +// +// Matches RawTableDefV10 product field order: +// +// source_name: String +// product_type_ref: AlgebraicTypeRef (u32) +// primary_key: ColList (array of u16) +// indexes: Vec +// constraints: Vec +// sequences: Vec +// table_type: TableType (sum tag) +// table_access: TableAccess (sum tag) +// default_values: Vec +// is_event: bool +func (t *tableDef) WriteBsatn(w bsatn.Writer) { + // source_name: String + w.PutString(t.sourceName) + + // product_type_ref: AlgebraicTypeRef (u32) + w.PutU32(uint32(t.productTypeRef)) + + // primary_key: ColList (serialized as array of u16) + w.PutArrayLen(uint32(len(t.primaryKey))) + for _, col := range t.primaryKey { + w.PutU16(col) + } + + // indexes: Vec + w.PutArrayLen(uint32(len(t.indexes))) + for _, idx := range t.indexes { + idx.WriteBsatn(w) + } + + // constraints: Vec + w.PutArrayLen(uint32(len(t.constraints))) + for _, c := range t.constraints { + c.WriteBsatn(w) + } + + // sequences: Vec + w.PutArrayLen(uint32(len(t.sequences))) + for _, s := range t.sequences { + s.WriteBsatn(w) + } + + // table_type: TableType (sum tag) + w.PutSumTag(uint8(t.tableType)) + + // table_access: TableAccess (sum tag) + w.PutSumTag(uint8(t.tableAccess)) + + // default_values: Vec + w.PutArrayLen(uint32(len(t.defaultValues))) + for _, dv := range t.defaultValues { + dv.WriteBsatn(w) + } + + // is_event: bool + w.PutBool(t.isEvent) +} + +// ColumnDefaultValue marks a column as having a default value. +type ColumnDefaultValue interface { + bsatn.Serializable +} + +// NewColumnDefaultValue creates a ColumnDefaultValue for a specific column. +// The value is pre-encoded BSATN bytes of the default AlgebraicValue. +func NewColumnDefaultValue(colID uint16, value []byte) ColumnDefaultValue { + return &columnDefaultValue{colID: colID, value: value} +} + +type columnDefaultValue struct { + colID uint16 + value []byte +} + +// WriteBsatn encodes: col_id (u16), value (Vec). +func (d *columnDefaultValue) WriteBsatn(w bsatn.Writer) { + w.PutU16(d.colID) + w.PutArrayLen(uint32(len(d.value))) + w.PutBytes(d.value) +} diff --git a/sdks/go/server/moduledef/type_def.go b/sdks/go/server/moduledef/type_def.go new file mode 100644 index 00000000000..c33684174d5 --- /dev/null +++ b/sdks/go/server/moduledef/type_def.go @@ -0,0 +1,74 @@ +package moduledef + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// TypeDef defines a type declaration in the module. +type TypeDef interface { + bsatn.Serializable +} + +// TypeDefBuilder builds a TypeDef. +type TypeDefBuilder interface { + WithCustomOrdering(v bool) TypeDefBuilder + Build() TypeDef +} + +// NewTypeDefBuilder creates a TypeDefBuilder. +// scope is the scope segments (empty for no scope). +// sourceName is the type name. +// typeRef is the AlgebraicTypeRef pointing to this type in the typespace. +func NewTypeDefBuilder(scope []string, sourceName string, typeRef types.TypeRef) TypeDefBuilder { + return &typeDef{ + scope: scope, + sourceName: sourceName, + typeRef: typeRef, + } +} + +type typeDef struct { + scope []string + sourceName string + typeRef types.TypeRef + customOrdering bool +} + +func (t *typeDef) WithCustomOrdering(v bool) TypeDefBuilder { + t.customOrdering = v + return t +} + +func (t *typeDef) Build() TypeDef { + return t +} + +// WriteBsatn encodes the type definition as BSATN. +// +// Matches RawTypeDefV10 product field order: +// +// source_name: RawScopedTypeNameV10 +// ty: AlgebraicTypeRef (u32) +// custom_ordering: bool +// +// RawScopedTypeNameV10 product: +// +// scope: Box<[String]> (array of strings) +// source_name: String +func (t *typeDef) WriteBsatn(w bsatn.Writer) { + // source_name: RawScopedTypeNameV10 (product) + // scope: array of strings + w.PutArrayLen(uint32(len(t.scope))) + for _, s := range t.scope { + w.PutString(s) + } + // source_name: String + w.PutString(t.sourceName) + + // ty: AlgebraicTypeRef (u32) + w.PutU32(uint32(t.typeRef)) + + // custom_ordering: bool + w.PutBool(t.customOrdering) +} diff --git a/sdks/go/server/moduledef/view_def.go b/sdks/go/server/moduledef/view_def.go new file mode 100644 index 00000000000..7533b362bd4 --- /dev/null +++ b/sdks/go/server/moduledef/view_def.go @@ -0,0 +1,104 @@ +package moduledef + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// ViewDef defines a view in the module. +type ViewDef interface { + bsatn.Serializable +} + +// ViewDefBuilder builds a ViewDef. +type ViewDefBuilder interface { + WithIndex(index uint32) ViewDefBuilder + WithIsPublic(isPublic bool) ViewDefBuilder + WithIsAnonymous(isAnonymous bool) ViewDefBuilder + WithParams(params types.ProductType) ViewDefBuilder + WithReturnType(returnType types.AlgebraicType) ViewDefBuilder + Build() ViewDef +} + +// NewViewDefBuilder creates a ViewDefBuilder with the given source name. +func NewViewDefBuilder(sourceName string) ViewDefBuilder { + return &viewDef{ + sourceName: sourceName, + } +} + +type viewDef struct { + sourceName string + index uint32 + isPublic bool + isAnonymous bool + params types.ProductType + returnType types.AlgebraicType +} + +func (v *viewDef) WithIndex(index uint32) ViewDefBuilder { + v.index = index + return v +} + +func (v *viewDef) WithIsPublic(isPublic bool) ViewDefBuilder { + v.isPublic = isPublic + return v +} + +func (v *viewDef) WithIsAnonymous(isAnonymous bool) ViewDefBuilder { + v.isAnonymous = isAnonymous + return v +} + +func (v *viewDef) WithParams(params types.ProductType) ViewDefBuilder { + v.params = params + return v +} + +func (v *viewDef) WithReturnType(returnType types.AlgebraicType) ViewDefBuilder { + v.returnType = returnType + return v +} + +func (v *viewDef) Build() ViewDef { + return v +} + +// WriteBsatn encodes the view definition as BSATN. +// +// Matches RawViewDefV10 product field order: +// +// source_name: String +// index: u32 +// is_public: bool +// is_anonymous: bool +// params: ProductType +// return_type: AlgebraicType +func (v *viewDef) WriteBsatn(w bsatn.Writer) { + // source_name: String + w.PutString(v.sourceName) + + // index: u32 + w.PutU32(v.index) + + // is_public: bool + w.PutBool(v.isPublic) + + // is_anonymous: bool + w.PutBool(v.isAnonymous) + + // params: ProductType + if v.params != nil { + v.params.WriteBsatn(w) + } else { + w.PutArrayLen(0) + } + + // return_type: AlgebraicType + if v.returnType != nil { + v.returnType.WriteBsatn(w) + } else { + types.AlgTypeProduct(types.NewProductType()).WriteBsatn(w) + } +} diff --git a/sdks/go/server/reducer/lifecycle.go b/sdks/go/server/reducer/lifecycle.go new file mode 100644 index 00000000000..16bcf4cd481 --- /dev/null +++ b/sdks/go/server/reducer/lifecycle.go @@ -0,0 +1,32 @@ +package reducer + +// Lifecycle identifies lifecycle reducer types. +type Lifecycle uint8 + +const ( + LifecycleInit Lifecycle = 0 + LifecycleClientConnected Lifecycle = 1 + LifecycleClientDisconnected Lifecycle = 2 +) + +func (l Lifecycle) String() string { + switch l { + case LifecycleInit: + return "__init__" + case LifecycleClientConnected: + return "__identity_connected__" + case LifecycleClientDisconnected: + return "__identity_disconnected__" + default: + return "unknown" + } +} + +// InitReducerFunc is the signature for the __init__ lifecycle reducer. +type InitReducerFunc func(ctx ReducerContext) + +// ClientConnectedReducerFunc is the signature for the __identity_connected__ lifecycle reducer. +type ClientConnectedReducerFunc func(ctx ReducerContext) + +// ClientDisconnectedReducerFunc is the signature for the __identity_disconnected__ lifecycle reducer. +type ClientDisconnectedReducerFunc func(ctx ReducerContext) diff --git a/sdks/go/server/reducer/procedure.go b/sdks/go/server/reducer/procedure.go new file mode 100644 index 00000000000..bafa256bd1a --- /dev/null +++ b/sdks/go/server/reducer/procedure.go @@ -0,0 +1,25 @@ +package reducer + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// ProcedureContext provides context to a running procedure. +// Unlike ReducerContext, procedures do not automatically run in a transaction. +// Use WithTx or TryWithTx to access the database. +type ProcedureContext interface { + Sender() types.Identity + ConnectionId() types.ConnectionId + Timestamp() types.Timestamp + Identity() types.Identity // module identity + WithTx(fn func()) + TryWithTx(fn func() error) error + SleepUntil(target types.Timestamp) + HttpGet(uri string) (statusCode uint16, body []byte, err error) + NewUuidV7() (types.Uuid, error) +} + +// ProcedureFunc is the internal dispatch signature for procedures. +// The args are raw BSATN bytes of the procedure's parameter product type. +// Returns the BSATN-encoded result to write to the sink. +type ProcedureFunc func(ctx ProcedureContext, args []byte) ([]byte, error) diff --git a/sdks/go/server/reducer/reducer.go b/sdks/go/server/reducer/reducer.go new file mode 100644 index 00000000000..fa3335ed56a --- /dev/null +++ b/sdks/go/server/reducer/reducer.go @@ -0,0 +1,18 @@ +package reducer + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// ReducerContext provides context to a running reducer. +type ReducerContext interface { + Sender() types.Identity + ConnectionId() types.ConnectionId + Timestamp() types.Timestamp + Identity() types.Identity // Module identity (owner) + Db() any +} + +// ReducerFunc is the internal dispatch signature for reducers. +// The args are raw BSATN bytes of the reducer's parameter product type. +type ReducerFunc func(ctx ReducerContext, args []byte) error diff --git a/sdks/go/server/reducer/reducer_impl.go b/sdks/go/server/reducer/reducer_impl.go new file mode 100644 index 00000000000..0717861659d --- /dev/null +++ b/sdks/go/server/reducer/reducer_impl.go @@ -0,0 +1,29 @@ +package reducer + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// NewReducerContext creates a new ReducerContext. +func NewReducerContext(sender types.Identity, connId types.ConnectionId, ts types.Timestamp, moduleIdentity types.Identity) ReducerContext { + return &reducerContext{ + sender: sender, + connectionId: connId, + timestamp: ts, + moduleIdentity: moduleIdentity, + } +} + +type reducerContext struct { + sender types.Identity + connectionId types.ConnectionId + timestamp types.Timestamp + moduleIdentity types.Identity + db any +} + +func (c *reducerContext) Sender() types.Identity { return c.sender } +func (c *reducerContext) ConnectionId() types.ConnectionId { return c.connectionId } +func (c *reducerContext) Timestamp() types.Timestamp { return c.timestamp } +func (c *reducerContext) Identity() types.Identity { return c.moduleIdentity } +func (c *reducerContext) Db() any { return c.db } diff --git a/sdks/go/server/reducer/view.go b/sdks/go/server/reducer/view.go new file mode 100644 index 00000000000..09180963b81 --- /dev/null +++ b/sdks/go/server/reducer/view.go @@ -0,0 +1,41 @@ +package reducer + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// ViewContext provides context to an authenticated view function. +type ViewContext interface { + Sender() types.Identity +} + +// AnonymousViewContext provides context to an anonymous view function. +// Anonymous views do not have access to the caller's identity. +type AnonymousViewContext interface { + isAnonymousViewContext() +} + +// ViewFunc is the internal dispatch signature for views. +// The args are raw BSATN bytes of the view's parameter product type. +// Returns the BSATN-encoded result to write to the sink. +type ViewFunc func(ctx any, args []byte) ([]byte, error) + +// NewViewContext creates a ViewContext with the given sender identity. +func NewViewContext(sender types.Identity) ViewContext { + return &viewContext{sender: sender} +} + +// NewAnonymousViewContext creates an AnonymousViewContext. +func NewAnonymousViewContext() AnonymousViewContext { + return &anonymousViewContext{} +} + +type viewContext struct { + sender types.Identity +} + +func (c *viewContext) Sender() types.Identity { return c.sender } + +type anonymousViewContext struct{} + +func (c *anonymousViewContext) isAnonymousViewContext() {} diff --git a/sdks/go/server/runtime/exports.go b/sdks/go/server/runtime/exports.go new file mode 100644 index 00000000000..9d20385c2e2 --- /dev/null +++ b/sdks/go/server/runtime/exports.go @@ -0,0 +1,177 @@ +//go:build wasip1 + +package runtime + +import ( + "fmt" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/reducer" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/sys" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +const ( + callReducerSuccess int32 = 0 + callReducerErr int32 = 1 // HOST_CALL_FAILURE: user error or panic +) + +const ( + callViewRows int32 = 0 // ViewReturnData::Rows + callViewHeaderFirst int32 = 2 // ViewReturnData::HeaderFirst +) + +// __describe_module__ is called by the host to get the module definition. +// +//go:wasmexport __describe_module__ +func wasmDescribeModule(descriptionSink uint32) { + if describeModuleHandler == nil { + panic("runtime: no describe module handler registered") + } + data := describeModuleHandler() + _ = sys.WriteBytesToSink(descriptionSink, data) +} + +// __call_reducer__ is called by the host to execute a reducer. +// +//go:wasmexport __call_reducer__ +func wasmCallReducer(id uint32, sender0, sender1, sender2, sender3, connId0, connId1, timestamp uint64, args uint32, errSink uint32) (retCode int32) { + // Recover from panics in reducer functions (e.g. Insert/Delete/UpdateBy/DeleteBy + // panic on host errors, matching Rust SDK behavior where these operations panic). + defer func() { + if r := recover(); r != nil { + writeError(errSink, fmt.Sprintf("%v", r)) + retCode = callReducerErr + } + }() + + if callReducerHandler == nil { + writeError(errSink, "runtime: no call reducer handler registered") + return callReducerErr + } + + identity := types.NewIdentityFromU64s(sender0, sender1, sender2, sender3) + connId := types.NewConnectionIdFromU64s(connId0, connId1) + ts := types.NewTimestamp(int64(timestamp)) + moduleId := types.NewIdentity(sys.GetIdentity()) + ctx := reducer.NewReducerContext(identity, connId, ts, moduleId) + + argsData, err := sys.ReadBytesSource(args) + if err != nil { + writeError(errSink, fmt.Sprintf("failed to read args: %v", err)) + return callReducerErr + } + + if err := callReducerHandler(id, ctx, argsData); err != nil { + writeError(errSink, err.Error()) + return callReducerErr + } + + return callReducerSuccess +} + +// __preinit__10_register is called before describe_module. +// Go's init() functions have already run by this point. +// +//go:wasmexport __preinit__10_register +func wasmPreinit() { + // No-op: Go init() functions run before any wasmexport is called. +} + +// __call_view__ is called by the host to execute an authenticated view. +// +//go:wasmexport __call_view__ +func wasmCallView(id uint32, sender0, sender1, sender2, sender3 uint64, argsSrc uint32, resultSink uint32) (retCode int32) { + defer func() { + if r := recover(); r != nil { + writeError(resultSink, fmt.Sprintf("%v", r)) + retCode = callReducerErr + } + }() + + if callViewHandler == nil { + writeError(resultSink, "runtime: no call view handler registered") + return callReducerErr + } + + identity := types.NewIdentityFromU64s(sender0, sender1, sender2, sender3) + + argsData, err := sys.ReadBytesSource(argsSrc) + if err != nil { + writeError(resultSink, fmt.Sprintf("failed to read view args: %v", err)) + return callReducerErr + } + + result, err := callViewHandler(id, identity, argsData) + if err != nil { + writeError(resultSink, err.Error()) + return callReducerErr + } + + _ = sys.WriteBytesToSink(resultSink, result) + return callViewHeaderFirst +} + +// __call_view_anon__ is called by the host to execute an anonymous view. +// +//go:wasmexport __call_view_anon__ +func wasmCallViewAnon(id uint32, argsSrc uint32, resultSink uint32) (retCode int32) { + defer func() { + if r := recover(); r != nil { + writeError(resultSink, fmt.Sprintf("%v", r)) + retCode = callReducerErr + } + }() + + if callViewAnonHandler == nil { + writeError(resultSink, "runtime: no call view anon handler registered") + return callReducerErr + } + + argsData, err := sys.ReadBytesSource(argsSrc) + if err != nil { + writeError(resultSink, fmt.Sprintf("failed to read view args: %v", err)) + return callReducerErr + } + + result, err := callViewAnonHandler(id, argsData) + if err != nil { + writeError(resultSink, err.Error()) + return callReducerErr + } + + _ = sys.WriteBytesToSink(resultSink, result) + return callViewHeaderFirst +} + +// __call_procedure__ is called by the host to execute a procedure. +// Procedures always return 0. On error, they panic which becomes a WASM trap. +// +//go:wasmexport __call_procedure__ +func wasmCallProcedure(id uint32, sender0, sender1, sender2, sender3, connId0, connId1, timestamp uint64, args uint32, resultSink uint32) int32 { + if callProcedureHandler == nil { + panic("runtime: no call procedure handler registered") + } + + identity := types.NewIdentityFromU64s(sender0, sender1, sender2, sender3) + connId := types.NewConnectionIdFromU64s(connId0, connId1) + ts := types.NewTimestamp(int64(timestamp)) + ctx := NewProcedureContext(identity, connId, ts) + + argsData, err := sys.ReadBytesSource(args) + if err != nil { + panic(fmt.Sprintf("failed to read procedure args: %v", err)) + } + + result, err := callProcedureHandler(id, ctx, argsData) + if err != nil { + panic(fmt.Sprintf("procedure error: %v", err)) + } + + _ = sys.WriteBytesToSink(resultSink, result) + return callReducerSuccess +} + +// writeError writes an error message to the given sink. +func writeError(sink uint32, msg string) { + _ = sys.WriteBytesToSink(sink, []byte(msg)) +} diff --git a/sdks/go/server/runtime/exports_stub.go b/sdks/go/server/runtime/exports_stub.go new file mode 100644 index 00000000000..131dd0938ad --- /dev/null +++ b/sdks/go/server/runtime/exports_stub.go @@ -0,0 +1,10 @@ +//go:build !wasip1 + +package runtime + +// Stubs so the package compiles under standard Go for testing. +// The actual WASM exports are in exports.go. + +// NewProcedureContext stub for non-WASM builds. +// The real implementation is in procedure_ctx.go which uses sys imports. +// This stub allows the package to compile for testing. diff --git a/sdks/go/server/runtime/handlers.go b/sdks/go/server/runtime/handlers.go new file mode 100644 index 00000000000..90bc6db4233 --- /dev/null +++ b/sdks/go/server/runtime/handlers.go @@ -0,0 +1,46 @@ +package runtime + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/reducer" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// Handler function types — set by generated code in init(). + +// DescribeModuleHandler returns the BSATN-encoded module definition. +type DescribeModuleHandler func() []byte + +// CallReducerHandler dispatches a reducer call by ID. +type CallReducerHandler func(id uint32, ctx reducer.ReducerContext, args []byte) error + +// CallProcedureHandler dispatches a procedure call by ID. +type CallProcedureHandler func(id uint32, ctx reducer.ProcedureContext, args []byte) ([]byte, error) + +// CallViewHandler dispatches an authenticated view call by ID. +type CallViewHandler func(id uint32, sender types.Identity, args []byte) ([]byte, error) + +// CallViewAnonHandler dispatches an anonymous view call by ID. +type CallViewAnonHandler func(id uint32, args []byte) ([]byte, error) + +var ( + describeModuleHandler DescribeModuleHandler + callReducerHandler CallReducerHandler + callProcedureHandler CallProcedureHandler + callViewHandler CallViewHandler + callViewAnonHandler CallViewAnonHandler +) + +// SetDescribeModuleHandler registers the module description handler. +func SetDescribeModuleHandler(h DescribeModuleHandler) { describeModuleHandler = h } + +// SetCallReducerHandler registers the reducer dispatch handler. +func SetCallReducerHandler(h CallReducerHandler) { callReducerHandler = h } + +// SetCallProcedureHandler registers the procedure dispatch handler. +func SetCallProcedureHandler(h CallProcedureHandler) { callProcedureHandler = h } + +// SetCallViewHandler registers the authenticated view dispatch handler. +func SetCallViewHandler(h CallViewHandler) { callViewHandler = h } + +// SetCallViewAnonHandler registers the anonymous view dispatch handler. +func SetCallViewAnonHandler(h CallViewAnonHandler) { callViewAnonHandler = h } diff --git a/sdks/go/server/runtime/procedure_ctx.go b/sdks/go/server/runtime/procedure_ctx.go new file mode 100644 index 00000000000..770241d7462 --- /dev/null +++ b/sdks/go/server/runtime/procedure_ctx.go @@ -0,0 +1,101 @@ +package runtime + +import ( + "crypto/rand" + "fmt" + + stdbhttp "github.com/clockworklabs/SpacetimeDB/sdks/go/server/http" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/reducer" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/sys" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// procedureContext implements reducer.ProcedureContext. +type procedureContext struct { + sender types.Identity + connectionId types.ConnectionId + timestamp types.Timestamp + moduleId types.Identity + uuidCounter uint32 +} + +var _ reducer.ProcedureContext = (*procedureContext)(nil) + +// NewProcedureContext creates a ProcedureContext for procedure dispatch. +func NewProcedureContext(sender types.Identity, connId types.ConnectionId, ts types.Timestamp) reducer.ProcedureContext { + return &procedureContext{ + sender: sender, + connectionId: connId, + timestamp: ts, + moduleId: types.NewIdentity(sys.GetIdentity()), + } +} + +func (c *procedureContext) Sender() types.Identity { return c.sender } +func (c *procedureContext) ConnectionId() types.ConnectionId { return c.connectionId } +func (c *procedureContext) Timestamp() types.Timestamp { return c.timestamp } +func (c *procedureContext) Identity() types.Identity { return c.moduleId } + +func (c *procedureContext) WithTx(fn func()) { + if _, err := sys.ProcedureStartMutTx(); err != nil { + panic(fmt.Sprintf("ProcedureContext.WithTx: start tx failed: %v", err)) + } + defer func() { + if r := recover(); r != nil { + _ = sys.ProcedureAbortMutTx() + panic(r) + } + }() + fn() + if err := sys.ProcedureCommitMutTx(); err != nil { + panic(fmt.Sprintf("ProcedureContext.WithTx: commit failed: %v", err)) + } +} + +func (c *procedureContext) TryWithTx(fn func() error) error { + if _, err := sys.ProcedureStartMutTx(); err != nil { + return fmt.Errorf("ProcedureContext.TryWithTx: start tx failed: %w", err) + } + + var fnErr error + func() { + defer func() { + if r := recover(); r != nil { + _ = sys.ProcedureAbortMutTx() + panic(r) + } + }() + fnErr = fn() + }() + + if fnErr != nil { + _ = sys.ProcedureAbortMutTx() + return fnErr + } + + if err := sys.ProcedureCommitMutTx(); err != nil { + return fmt.Errorf("ProcedureContext.TryWithTx: commit failed: %w", err) + } + return nil +} + +func (c *procedureContext) SleepUntil(target types.Timestamp) { + newTs := sys.ProcedureSleepUntil(target.Microseconds()) + c.timestamp = types.NewTimestamp(newTs) +} + +func (c *procedureContext) HttpGet(uri string) (uint16, []byte, error) { + return stdbhttp.Get(uri) +} + +func (c *procedureContext) NewUuidV7() (types.Uuid, error) { + var randomBytes [4]byte + if _, err := rand.Read(randomBytes[:]); err != nil { + return nil, fmt.Errorf("ProcedureContext.NewUuidV7: failed to generate random bytes: %w", err) + } + uuid, err := types.NewUuidV7(&c.uuidCounter, c.timestamp.Microseconds(), randomBytes) + if err != nil { + return nil, fmt.Errorf("ProcedureContext.NewUuidV7: %w", err) + } + return uuid, nil +} diff --git a/sdks/go/server/runtime/table_ops.go b/sdks/go/server/runtime/table_ops.go new file mode 100644 index 00000000000..730a02f2ba0 --- /dev/null +++ b/sdks/go/server/runtime/table_ops.go @@ -0,0 +1,145 @@ +package runtime + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/sys" +) + +// GlobalWriter is a reusable BSATN writer for hot-path encoding. +// Safe because WASM is single-threaded — no concurrent access. +// Used by generated table accessor code. +var GlobalWriter = bsatn.NewWriter(256) + +// indexIdCache caches index IDs resolved from the host, keyed by index name. +var indexIdCache = map[string]uint32{} + +// GetIndexId returns the index ID for a given index name, caching it on first lookup. +// Used by generated table accessor code. +func GetIndexId(name string) (uint32, error) { + if id, ok := indexIdCache[name]; ok { + return id, nil + } + id, err := sys.IndexIdFromName(name) + if err != nil { + return 0, err + } + indexIdCache[name] = id + return id, nil +} + +// tableIdCache caches table IDs resolved from the host, keyed by table name. +var tableIdCache = map[string]uint32{} + +// GetTableId returns the table ID for a given table name, caching it on first lookup. +// Used by generated table accessor code. +func GetTableId(name string) (uint32, error) { + if id, ok := tableIdCache[name]; ok { + return id, nil + } + id, err := sys.TableIdFromName(name) + if err != nil { + return 0, err + } + tableIdCache[name] = id + return id, nil +} + +// TableIterator iterates over rows of type T. +type TableIterator[T any] interface { + Next() (T, bool) + Close() +} + +// DecodeFn decodes a single row of type T from a BSATN reader. +type DecodeFn[T any] func(r bsatn.Reader, v *T) error + +// NewTableIterator creates a TableIterator that uses the given decode function. +// Used by generated table accessor code to create iterators with generated decoders. +func NewTableIterator[T any](sysIter *sys.RowIterator, decode DecodeFn[T]) TableIterator[T] { + return &tableIterator[T]{ + sysIter: sysIter, + decodeFn: decode, + } +} + +// tableIterator uses batch reading from the host. The host's row_iter_bsatn_advance +// packs multiple BSATN-encoded rows into a single buffer. We maintain a bsatn.Reader +// over the batch and decode one row at a time, only fetching the next batch when the +// current one is consumed. +type tableIterator[T any] struct { + sysIter *sys.RowIterator + decodeFn DecodeFn[T] + buf []byte + reader bsatn.Reader +} + +func (ti *tableIterator[T]) Next() (T, bool) { + var zero T + for { + // If we have remaining bytes in the current batch, decode one row. + if ti.reader != nil && ti.reader.Remaining() > 0 { + var result T + if err := ti.decodeFn(ti.reader, &result); err != nil { + return zero, false + } + return result, true + } + + // If the host iterator is exhausted, no more rows. + if ti.sysIter.IsExhausted() { + return zero, false + } + + // Fetch the next batch from the host. + // Release old buffer so GC keeps it alive only via zero-copy string refs. + // ReadBatch allocates a fresh buffer, enabling zero-copy string decode. + ti.buf = nil + if err := ti.sysIter.ReadBatch(&ti.buf); err != nil { + return zero, false + } + if len(ti.buf) == 0 { + return zero, false + } + ti.reader = bsatn.NewZeroCopyReader(ti.buf) + } +} + +func (ti *tableIterator[T]) Close() { + ti.sysIter.Close() +} + +// EncodeKeyInto encodes a single key value into the writer using a type-switch +// for fast encoding of common primitive types. +// Used by generated table accessor code for index lookups. +func EncodeKeyInto[K any](w bsatn.Writer, key K) { + switch k := any(key).(type) { + case bool: + w.PutBool(k) + case uint8: + w.PutU8(k) + case uint16: + w.PutU16(k) + case uint32: + w.PutU32(k) + case uint64: + w.PutU64(k) + case int8: + w.PutI8(k) + case int16: + w.PutI16(k) + case int32: + w.PutI32(k) + case int64: + w.PutI64(k) + case float32: + w.PutF32(k) + case float64: + w.PutF64(k) + case string: + w.PutString(k) + case bsatn.Serializable: + k.WriteBsatn(w) + default: + panic("runtime.EncodeKeyInto: unsupported key type") + } +} diff --git a/sdks/go/server/stdb.go b/sdks/go/server/stdb.go new file mode 100644 index 00000000000..787a35aa272 --- /dev/null +++ b/sdks/go/server/stdb.go @@ -0,0 +1,40 @@ +// Package server provides top-level convenience re-exports for SpacetimeDB +// Go module authoring. Import this package to access context types, table +// access constants, and logging. +// +// With the codegen-based approach, registration is handled by generated code +// in stdb_generated.go. Users annotate types and functions with //stdb: +// directives and run `go generate` to produce the registration code. +package server + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/log" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/moduledef" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/reducer" +) + +// Table access constants. +const ( + TableAccessPublic = moduledef.TableAccessPublic + TableAccessPrivate = moduledef.TableAccessPrivate +) + +// Lifecycle constants. +const ( + LifecycleInit = reducer.LifecycleInit + LifecycleClientConnected = reducer.LifecycleClientConnected + LifecycleClientDisconnected = reducer.LifecycleClientDisconnected +) + +// Type aliases for commonly used interfaces. +type ( + ReducerContext = reducer.ReducerContext + ViewContext = reducer.ViewContext + AnonymousViewContext = reducer.AnonymousViewContext + ProcedureContext = reducer.ProcedureContext +) + +// NewLogger creates a Logger that writes to the host console. +func NewLogger(target string) log.Logger { + return log.NewLogger(target) +} diff --git a/sdks/go/server/sys/bytes_sink.go b/sdks/go/server/sys/bytes_sink.go new file mode 100644 index 00000000000..ec978d5577f --- /dev/null +++ b/sdks/go/server/sys/bytes_sink.go @@ -0,0 +1,14 @@ +package sys + +// WriteBytesToSink writes all data to a BytesSink handle. +func WriteBytesToSink(sink uint32, data []byte) error { + for len(data) > 0 { + bufLen := uint32(len(data)) + ret := rawBytesSinkWrite(sink, &data[0], &bufLen) + if ret != 0 { + return Errno(uint16(ret)) + } + data = data[bufLen:] + } + return nil +} diff --git a/sdks/go/server/sys/bytes_source.go b/sdks/go/server/sys/bytes_source.go new file mode 100644 index 00000000000..e98e02588f2 --- /dev/null +++ b/sdks/go/server/sys/bytes_source.go @@ -0,0 +1,45 @@ +package sys + +const bytesSourceInvalid = uint32(0) + +// ReadBytesSource reads all data from a BytesSource handle. +func ReadBytesSource(source uint32) ([]byte, error) { + if source == bytesSourceInvalid { + return nil, nil + } + + // Try to get remaining length for pre-allocation + var remaining uint32 + ret := rawBytesSourceRemainingLength(source, &remaining) + + var buf []byte + if ret == 0 && remaining > 0 { + buf = make([]byte, 0, remaining) + } else { + buf = make([]byte, 0, 1024) + } + + for { + spare := cap(buf) - len(buf) + if spare == 0 { + buf = append(buf, make([]byte, 1024)...) + buf = buf[:len(buf)-1024] + spare = cap(buf) - len(buf) + } + + bufLen := uint32(spare) + ptr := &buf[len(buf):cap(buf)][0] + ret := rawBytesSourceRead(source, ptr, &bufLen) + + buf = buf[:len(buf)+int(bufLen)] + + switch { + case ret == -1: + return buf, nil // exhausted + case ret == 0: + continue + default: + return nil, Errno(uint16(int16(ret))) + } + } +} diff --git a/sdks/go/server/sys/errno.go b/sdks/go/server/sys/errno.go new file mode 100644 index 00000000000..3f09e844e92 --- /dev/null +++ b/sdks/go/server/sys/errno.go @@ -0,0 +1,76 @@ +package sys + +import "fmt" + +// Errno represents a SpacetimeDB host error code. +type Errno uint16 + +const ( + ErrHostCallFailure Errno = 1 + ErrNotInTransaction Errno = 2 + ErrBsatnDecodeError Errno = 3 + ErrNoSuchTable Errno = 4 + ErrNoSuchIndex Errno = 5 + ErrNoSuchIter Errno = 6 + ErrNoSuchConsoleTimer Errno = 7 + ErrNoSuchBytes Errno = 8 + ErrNoSpace Errno = 9 + ErrWrongIndexAlgo Errno = 10 + ErrBufferTooSmall Errno = 11 + ErrUniqueAlreadyExists Errno = 12 + ErrScheduleAtDelayTooLong Errno = 13 + ErrIndexNotUnique Errno = 14 + ErrNoSuchRow Errno = 15 + ErrAutoIncOverflow Errno = 16 + ErrWouldBlockTransaction Errno = 17 + ErrTransactionNotAnonymous Errno = 18 + ErrTransactionIsReadOnly Errno = 19 + ErrTransactionIsMut Errno = 20 + ErrHTTPError Errno = 21 +) + +func (e Errno) Error() string { + switch e { + case ErrHostCallFailure: + return "spacetime: host call failure" + case ErrNotInTransaction: + return "spacetime: not in transaction" + case ErrBsatnDecodeError: + return "spacetime: BSATN decode error" + case ErrNoSuchTable: + return "spacetime: no such table" + case ErrNoSuchIndex: + return "spacetime: no such index" + case ErrNoSuchIter: + return "spacetime: no such iterator" + case ErrNoSuchConsoleTimer: + return "spacetime: no such console timer" + case ErrNoSuchBytes: + return "spacetime: no such bytes source/sink" + case ErrNoSpace: + return "spacetime: no space left in sink" + case ErrWrongIndexAlgo: + return "spacetime: wrong index algorithm" + case ErrBufferTooSmall: + return "spacetime: buffer too small" + case ErrUniqueAlreadyExists: + return "spacetime: unique constraint violation" + case ErrScheduleAtDelayTooLong: + return "spacetime: schedule_at delay too long" + case ErrIndexNotUnique: + return "spacetime: index is not unique" + case ErrNoSuchRow: + return "spacetime: no such row" + case ErrAutoIncOverflow: + return "spacetime: auto-increment overflow" + default: + return fmt.Sprintf("spacetime: unknown error %d", uint16(e)) + } +} + +func errnoFromU16(code uint16) error { + if code == 0 { + return nil + } + return Errno(code) +} diff --git a/sdks/go/server/sys/imports.go b/sdks/go/server/sys/imports.go new file mode 100644 index 00000000000..53c3ad544f7 --- /dev/null +++ b/sdks/go/server/sys/imports.go @@ -0,0 +1,90 @@ +//go:build wasip1 + +package sys + +// spacetime_10.0 host functions +// +// NOTE: //go:wasmimport only supports int32/uint32/int64/uint64/float32/float64/unsafe.Pointer. +// The WASM ABI uses i32 for all sub-32-bit types, so we use uint32/int32 here +// and cast to the appropriate narrower types in the Go wrapper functions. + +//go:wasmimport spacetime_10.0 table_id_from_name +func rawTableIdFromName(namePtr *byte, nameLen uint32, out *uint32) uint32 + +//go:wasmimport spacetime_10.0 datastore_table_row_count +func rawDatastoreTableRowCount(tableId uint32, out *uint64) uint32 + +//go:wasmimport spacetime_10.0 datastore_table_scan_bsatn +func rawDatastoreTableScanBSATN(tableId uint32, out *uint32) uint32 + +//go:wasmimport spacetime_10.0 datastore_insert_bsatn +func rawDatastoreInsertBSATN(tableId uint32, rowPtr *byte, rowLenPtr *uint32) uint32 + +//go:wasmimport spacetime_10.0 datastore_update_bsatn +func rawDatastoreUpdateBSATN(tableId uint32, indexId uint32, rowPtr *byte, rowLenPtr *uint32) uint32 + +//go:wasmimport spacetime_10.0 datastore_delete_all_by_eq_bsatn +func rawDatastoreDeleteAllByEqBSATN(tableId uint32, relPtr *byte, relLen uint32, out *uint32) uint32 + +//go:wasmimport spacetime_10.0 index_id_from_name +func rawIndexIdFromName(namePtr *byte, nameLen uint32, out *uint32) uint32 + +//go:wasmimport spacetime_10.0 datastore_index_scan_range_bsatn +func rawDatastoreIndexScanRangeBSATN(indexId uint32, prefixPtr *byte, prefixLen uint32, prefixElems uint32, rstartPtr *byte, rstartLen uint32, rendPtr *byte, rendLen uint32, out *uint32) uint32 + +//go:wasmimport spacetime_10.0 datastore_delete_by_index_scan_range_bsatn +func rawDatastoreDeleteByIndexScanRangeBSATN(indexId uint32, prefixPtr *byte, prefixLen uint32, prefixElems uint32, rstartPtr *byte, rstartLen uint32, rendPtr *byte, rendLen uint32, out *uint32) uint32 + +//go:wasmimport spacetime_10.0 row_iter_bsatn_advance +func rawRowIterBSATNAdvance(iter uint32, bufPtr *byte, bufLenPtr *uint32) int32 + +//go:wasmimport spacetime_10.0 row_iter_bsatn_close +func rawRowIterBSATNClose(iter uint32) uint32 + +//go:wasmimport spacetime_10.0 bytes_source_read +func rawBytesSourceRead(source uint32, bufPtr *byte, bufLenPtr *uint32) int32 + +//go:wasmimport spacetime_10.0 bytes_sink_write +func rawBytesSinkWrite(sink uint32, bufPtr *byte, bufLenPtr *uint32) uint32 + +//go:wasmimport spacetime_10.0 console_log +func rawConsoleLog(level uint32, targetPtr *byte, targetLen uint32, fnPtr *byte, fnLen uint32, line uint32, msgPtr *byte, msgLen uint32) + +//go:wasmimport spacetime_10.0 console_timer_start +func rawConsoleTimerStart(namePtr *byte, nameLen uint32) uint32 + +//go:wasmimport spacetime_10.0 console_timer_end +func rawConsoleTimerEnd(timerId uint32) uint32 + +//go:wasmimport spacetime_10.0 identity +func rawIdentity(outPtr *byte) + +// spacetime_10.1 + +//go:wasmimport spacetime_10.1 bytes_source_remaining_length +func rawBytesSourceRemainingLength(source uint32, out *uint32) int32 + +// spacetime_10.3 + +//go:wasmimport spacetime_10.3 procedure_start_mut_tx +func rawProcedureStartMutTx(out *int64) uint32 + +//go:wasmimport spacetime_10.3 procedure_commit_mut_tx +func rawProcedureCommitMutTx() uint32 + +//go:wasmimport spacetime_10.3 procedure_abort_mut_tx +func rawProcedureAbortMutTx() uint32 + +//go:wasmimport spacetime_10.3 procedure_sleep_until +func rawProcedureSleepUntil(wakeAtMicrosSinceUnixEpoch int64) int64 + +//go:wasmimport spacetime_10.3 procedure_http_request +func rawProcedureHttpRequest(requestPtr *byte, requestLen uint32, bodyPtr *byte, bodyLen uint32, out *uint32) uint32 + +// spacetime_10.4 + +//go:wasmimport spacetime_10.4 datastore_index_scan_point_bsatn +func rawDatastoreIndexScanPointBSATN(indexId uint32, pointPtr *byte, pointLen uint32, out *uint32) uint32 + +//go:wasmimport spacetime_10.4 datastore_delete_by_index_scan_point_bsatn +func rawDatastoreDeleteByIndexScanPointBSATN(indexId uint32, pointPtr *byte, pointLen uint32, out *uint32) uint32 diff --git a/sdks/go/server/sys/imports_stub.go b/sdks/go/server/sys/imports_stub.go new file mode 100644 index 00000000000..c1fa186793e --- /dev/null +++ b/sdks/go/server/sys/imports_stub.go @@ -0,0 +1,106 @@ +//go:build !wasip1 + +package sys + +// Stubs for native testing - these panic when called since they require WASM runtime. +// Signatures must match imports.go (using widened uint32/int32 types). + +func rawTableIdFromName(namePtr *byte, nameLen uint32, out *uint32) uint32 { + panic("rawTableIdFromName: not available outside WASM") +} + +func rawDatastoreTableRowCount(tableId uint32, out *uint64) uint32 { + panic("rawDatastoreTableRowCount: not available outside WASM") +} + +func rawDatastoreTableScanBSATN(tableId uint32, out *uint32) uint32 { + panic("rawDatastoreTableScanBSATN: not available outside WASM") +} + +func rawDatastoreInsertBSATN(tableId uint32, rowPtr *byte, rowLenPtr *uint32) uint32 { + panic("rawDatastoreInsertBSATN: not available outside WASM") +} + +func rawDatastoreUpdateBSATN(tableId uint32, indexId uint32, rowPtr *byte, rowLenPtr *uint32) uint32 { + panic("rawDatastoreUpdateBSATN: not available outside WASM") +} + +func rawDatastoreDeleteAllByEqBSATN(tableId uint32, relPtr *byte, relLen uint32, out *uint32) uint32 { + panic("rawDatastoreDeleteAllByEqBSATN: not available outside WASM") +} + +func rawIndexIdFromName(namePtr *byte, nameLen uint32, out *uint32) uint32 { + panic("rawIndexIdFromName: not available outside WASM") +} + +func rawDatastoreIndexScanRangeBSATN(indexId uint32, prefixPtr *byte, prefixLen uint32, prefixElems uint32, rstartPtr *byte, rstartLen uint32, rendPtr *byte, rendLen uint32, out *uint32) uint32 { + panic("rawDatastoreIndexScanRangeBSATN: not available outside WASM") +} + +func rawDatastoreDeleteByIndexScanRangeBSATN(indexId uint32, prefixPtr *byte, prefixLen uint32, prefixElems uint32, rstartPtr *byte, rstartLen uint32, rendPtr *byte, rendLen uint32, out *uint32) uint32 { + panic("rawDatastoreDeleteByIndexScanRangeBSATN: not available outside WASM") +} + +func rawRowIterBSATNAdvance(iter uint32, bufPtr *byte, bufLenPtr *uint32) int32 { + panic("rawRowIterBSATNAdvance: not available outside WASM") +} + +func rawRowIterBSATNClose(iter uint32) uint32 { + panic("rawRowIterBSATNClose: not available outside WASM") +} + +func rawBytesSourceRead(source uint32, bufPtr *byte, bufLenPtr *uint32) int32 { + panic("rawBytesSourceRead: not available outside WASM") +} + +func rawBytesSinkWrite(sink uint32, bufPtr *byte, bufLenPtr *uint32) uint32 { + panic("rawBytesSinkWrite: not available outside WASM") +} + +func rawConsoleLog(level uint32, targetPtr *byte, targetLen uint32, fnPtr *byte, fnLen uint32, line uint32, msgPtr *byte, msgLen uint32) { + panic("rawConsoleLog: not available outside WASM") +} + +func rawConsoleTimerStart(namePtr *byte, nameLen uint32) uint32 { + panic("rawConsoleTimerStart: not available outside WASM") +} + +func rawConsoleTimerEnd(timerId uint32) uint32 { + panic("rawConsoleTimerEnd: not available outside WASM") +} + +func rawIdentity(outPtr *byte) { + panic("rawIdentity: not available outside WASM") +} + +func rawBytesSourceRemainingLength(source uint32, out *uint32) int32 { + panic("rawBytesSourceRemainingLength: not available outside WASM") +} + +func rawProcedureStartMutTx(out *int64) uint32 { + panic("rawProcedureStartMutTx: not available outside WASM") +} + +func rawProcedureCommitMutTx() uint32 { + panic("rawProcedureCommitMutTx: not available outside WASM") +} + +func rawProcedureAbortMutTx() uint32 { + panic("rawProcedureAbortMutTx: not available outside WASM") +} + +func rawProcedureSleepUntil(wakeAtMicrosSinceUnixEpoch int64) int64 { + panic("rawProcedureSleepUntil: not available outside WASM") +} + +func rawProcedureHttpRequest(requestPtr *byte, requestLen uint32, bodyPtr *byte, bodyLen uint32, out *uint32) uint32 { + panic("rawProcedureHttpRequest: not available outside WASM") +} + +func rawDatastoreIndexScanPointBSATN(indexId uint32, pointPtr *byte, pointLen uint32, out *uint32) uint32 { + panic("rawDatastoreIndexScanPointBSATN: not available outside WASM") +} + +func rawDatastoreDeleteByIndexScanPointBSATN(indexId uint32, pointPtr *byte, pointLen uint32, out *uint32) uint32 { + panic("rawDatastoreDeleteByIndexScanPointBSATN: not available outside WASM") +} diff --git a/sdks/go/server/sys/row_iter.go b/sdks/go/server/sys/row_iter.go new file mode 100644 index 00000000000..cf653c7b0e7 --- /dev/null +++ b/sdks/go/server/sys/row_iter.go @@ -0,0 +1,111 @@ +package sys + +const rowIterInvalid = uint32(0) + +// RowIterator wraps a host row iterator handle with buffer management. +type RowIterator struct { + handle uint32 + buf []byte + done bool +} + +// NewRowIterator creates a new row iterator from a host handle. +func NewRowIterator(handle uint32) *RowIterator { + return &RowIterator{ + handle: handle, + buf: make([]byte, 0, 4096), + done: handle == rowIterInvalid, + } +} + +// IsExhausted returns true if the host iterator has been fully consumed. +func (ri *RowIterator) IsExhausted() bool { + return ri.done +} + +// ReadBatch reads the next batch of BSATN-encoded rows from the host iterator +// and appends them to buf. The host may write multiple rows packed sequentially +// into a single batch. Returns any error from the host. +func (ri *RowIterator) ReadBatch(buf *[]byte) error { + if ri.done { + return nil + } + + for { + ri.buf = ri.buf[:cap(ri.buf)] + bufLen := uint32(len(ri.buf)) + var ptr *byte + if bufLen > 0 { + ptr = &ri.buf[0] + } + + ret := rawRowIterBSATNAdvance(ri.handle, ptr, &bufLen) + + switch { + case ret == -1: + ri.done = true + if bufLen > 0 { + *buf = append(*buf, ri.buf[:bufLen]...) + } + return nil + case ret == 0: + *buf = append(*buf, ri.buf[:bufLen]...) + return nil + case ret == int32(ErrBufferTooSmall): + // bufLen now contains the needed size + ri.buf = make([]byte, bufLen) + continue + default: + return Errno(uint16(ret)) + } + } +} + +// Next reads the next batch from the iterator and returns all bytes as a single blob. +// This is suitable for single-row results (e.g., FindBy point scans). +// For multi-row iteration, use ReadBatch with a cursor-based approach instead. +func (ri *RowIterator) Next() ([]byte, bool, error) { + if ri.done { + return nil, false, nil + } + + for { + ri.buf = ri.buf[:cap(ri.buf)] + bufLen := uint32(len(ri.buf)) + var ptr *byte + if bufLen > 0 { + ptr = &ri.buf[0] + } + + ret := rawRowIterBSATNAdvance(ri.handle, ptr, &bufLen) + + switch { + case ret == -1: + ri.done = true + if bufLen > 0 { + row := make([]byte, bufLen) + copy(row, ri.buf[:bufLen]) + return row, true, nil + } + return nil, false, nil + case ret == 0: + row := make([]byte, bufLen) + copy(row, ri.buf[:bufLen]) + return row, true, nil + case ret == int32(ErrBufferTooSmall): + // bufLen now contains the needed size + ri.buf = make([]byte, bufLen) + continue + default: + return nil, false, Errno(uint16(ret)) + } + } +} + +// Close releases the iterator handle. +func (ri *RowIterator) Close() { + if !ri.done && ri.handle != rowIterInvalid { + rawRowIterBSATNClose(ri.handle) + ri.done = true + } +} diff --git a/sdks/go/server/sys/sys.go b/sdks/go/server/sys/sys.go new file mode 100644 index 00000000000..bead112428c --- /dev/null +++ b/sdks/go/server/sys/sys.go @@ -0,0 +1,297 @@ +package sys + +// sysBuf is a reusable buffer for DatastoreInsertBSATN and DatastoreUpdateBSATN. +// Safe because WASM is single-threaded. +var sysBuf []byte + +func ensureSysBuf(minCap int) { + if cap(sysBuf) < minCap { + sysBuf = make([]byte, minCap) + } + sysBuf = sysBuf[:minCap] +} + +// Log level constants +const ( + LogLevelError uint8 = 0 + LogLevelWarn uint8 = 1 + LogLevelInfo uint8 = 2 + LogLevelDebug uint8 = 3 + LogLevelTrace uint8 = 4 + LogLevelPanic uint8 = 101 +) + +// TableIdFromName looks up a table ID by name. +func TableIdFromName(name string) (uint32, error) { + nameBytes := []byte(name) + var tableId uint32 + ret := rawTableIdFromName(&nameBytes[0], uint32(len(nameBytes)), &tableId) + if err := errnoFromU16(uint16(ret)); err != nil { + return 0, err + } + return tableId, nil +} + +// DatastoreTableRowCount returns the row count for a table. +func DatastoreTableRowCount(tableId uint32) (uint64, error) { + var count uint64 + ret := rawDatastoreTableRowCount(tableId, &count) + if err := errnoFromU16(uint16(ret)); err != nil { + return 0, err + } + return count, nil +} + +// DatastoreTableScanBSATN starts a scan of all rows in a table. +func DatastoreTableScanBSATN(tableId uint32) (*RowIterator, error) { + var iterHandle uint32 + ret := rawDatastoreTableScanBSATN(tableId, &iterHandle) + if err := errnoFromU16(uint16(ret)); err != nil { + return nil, err + } + return NewRowIterator(iterHandle), nil +} + +// DatastoreInsertBSATN inserts a BSATN-encoded row and returns updated sequence values. +func DatastoreInsertBSATN(tableId uint32, row []byte) ([]byte, error) { + // Use reusable buffer. Ensure at least 1 byte so &buf[0] never panics + // (empty product types encode to 0 bytes), plus extra for potential auto-inc return. + needed := max(len(row), 1) + 64 + ensureSysBuf(needed) + copy(sysBuf, row) + bufLen := uint32(len(row)) + ret := rawDatastoreInsertBSATN(tableId, &sysBuf[0], &bufLen) + if err := errnoFromU16(uint16(ret)); err != nil { + return nil, err + } + if bufLen > 0 { + // Host wrote back auto-inc data; copy it out since sysBuf is reused. + result := make([]byte, bufLen) + copy(result, sysBuf[:bufLen]) + return result, nil + } + return nil, nil +} + +// DatastoreUpdateBSATN updates a row by its unique index, returning updated sequence values. +func DatastoreUpdateBSATN(tableId uint32, indexId uint32, row []byte) ([]byte, error) { + needed := max(len(row), 1) + 64 + ensureSysBuf(needed) + copy(sysBuf, row) + bufLen := uint32(len(row)) + ret := rawDatastoreUpdateBSATN(tableId, indexId, &sysBuf[0], &bufLen) + if err := errnoFromU16(uint16(ret)); err != nil { + return nil, err + } + if bufLen > 0 { + result := make([]byte, bufLen) + copy(result, sysBuf[:bufLen]) + return result, nil + } + return nil, nil +} + +// DatastoreDeleteAllByEqBSATN deletes all rows matching BSATN-encoded relation. +func DatastoreDeleteAllByEqBSATN(tableId uint32, rel []byte) (uint32, error) { + var deleted uint32 + // The host traps on NULL pointers (even with length 0), so we must + // always pass valid non-null pointers. Use a sentinel byte for empty slices. + var sentinel [1]byte + relPtr := &sentinel[0] + if len(rel) > 0 { + relPtr = &rel[0] + } + ret := rawDatastoreDeleteAllByEqBSATN(tableId, relPtr, uint32(len(rel)), &deleted) + if err := errnoFromU16(uint16(ret)); err != nil { + return 0, err + } + return deleted, nil +} + +// IndexIdFromName looks up an index ID by name. +func IndexIdFromName(name string) (uint32, error) { + nameBytes := []byte(name) + var indexId uint32 + ret := rawIndexIdFromName(&nameBytes[0], uint32(len(nameBytes)), &indexId) + if err := errnoFromU16(uint16(ret)); err != nil { + return 0, err + } + return indexId, nil +} + +// DatastoreIndexScanPointBSATN starts a point scan on an index. +func DatastoreIndexScanPointBSATN(indexId uint32, point []byte) (*RowIterator, error) { + var iterHandle uint32 + // The host traps on NULL pointers (even with length 0), so we must + // always pass valid non-null pointers. Use a sentinel byte for empty slices. + var sentinel [1]byte + pointPtr := &sentinel[0] + if len(point) > 0 { + pointPtr = &point[0] + } + ret := rawDatastoreIndexScanPointBSATN(indexId, pointPtr, uint32(len(point)), &iterHandle) + if err := errnoFromU16(uint16(ret)); err != nil { + return nil, err + } + return NewRowIterator(iterHandle), nil +} + +// DatastoreIndexScanRangeBSATN starts a range scan on an index. +func DatastoreIndexScanRangeBSATN(indexId uint32, prefix []byte, prefixElems uint32, rstart []byte, rend []byte) (*RowIterator, error) { + var iterHandle uint32 + // The host traps on NULL pointers (even with length 0), so we must + // always pass valid non-null pointers. Use a sentinel byte for empty slices. + var sentinel [1]byte + prefixPtr := &sentinel[0] + if len(prefix) > 0 { + prefixPtr = &prefix[0] + } + rstartPtr := &sentinel[0] + if len(rstart) > 0 { + rstartPtr = &rstart[0] + } + rendPtr := &sentinel[0] + if len(rend) > 0 { + rendPtr = &rend[0] + } + ret := rawDatastoreIndexScanRangeBSATN(indexId, prefixPtr, uint32(len(prefix)), prefixElems, rstartPtr, uint32(len(rstart)), rendPtr, uint32(len(rend)), &iterHandle) + if err := errnoFromU16(uint16(ret)); err != nil { + return nil, err + } + return NewRowIterator(iterHandle), nil +} + +// DatastoreDeleteByIndexScanPointBSATN deletes rows matching a point scan on an index. +func DatastoreDeleteByIndexScanPointBSATN(indexId uint32, point []byte) (uint32, error) { + var deleted uint32 + // The host traps on NULL pointers (even with length 0), so we must + // always pass valid non-null pointers. Use a sentinel byte for empty slices. + var sentinel [1]byte + pointPtr := &sentinel[0] + if len(point) > 0 { + pointPtr = &point[0] + } + ret := rawDatastoreDeleteByIndexScanPointBSATN(indexId, pointPtr, uint32(len(point)), &deleted) + if err := errnoFromU16(uint16(ret)); err != nil { + return 0, err + } + return deleted, nil +} + +// DatastoreDeleteByIndexScanRangeBSATN deletes rows matching a range scan on an index. +func DatastoreDeleteByIndexScanRangeBSATN(indexId uint32, prefix []byte, prefixElems uint32, rstart []byte, rend []byte) (uint32, error) { + var deleted uint32 + // The host traps on NULL pointers (even with length 0), so we must + // always pass valid non-null pointers. Use a sentinel byte for empty slices. + var sentinel [1]byte + prefixPtr := &sentinel[0] + if len(prefix) > 0 { + prefixPtr = &prefix[0] + } + rstartPtr := &sentinel[0] + if len(rstart) > 0 { + rstartPtr = &rstart[0] + } + rendPtr := &sentinel[0] + if len(rend) > 0 { + rendPtr = &rend[0] + } + ret := rawDatastoreDeleteByIndexScanRangeBSATN(indexId, prefixPtr, uint32(len(prefix)), prefixElems, rstartPtr, uint32(len(rstart)), rendPtr, uint32(len(rend)), &deleted) + if err := errnoFromU16(uint16(ret)); err != nil { + return 0, err + } + return deleted, nil +} + +// ConsoleLog logs a message to the host console. +func ConsoleLog(level uint8, target, filename string, line uint32, message string) { + targetBytes := []byte(target) + filenameBytes := []byte(filename) + msgBytes := []byte(message) + + var targetPtr, filenamePtr, msgPtr *byte + if len(targetBytes) > 0 { + targetPtr = &targetBytes[0] + } + if len(filenameBytes) > 0 { + filenamePtr = &filenameBytes[0] + } + if len(msgBytes) > 0 { + msgPtr = &msgBytes[0] + } + + rawConsoleLog(uint32(level), targetPtr, uint32(len(targetBytes)), filenamePtr, uint32(len(filenameBytes)), line, msgPtr, uint32(len(msgBytes))) +} + +// ConsoleTimerStart starts a named console timer, returning its ID. +func ConsoleTimerStart(name string) uint32 { + nameBytes := []byte(name) + return rawConsoleTimerStart(&nameBytes[0], uint32(len(nameBytes))) +} + +// ConsoleTimerEnd ends a console timer by its ID. +func ConsoleTimerEnd(timerId uint32) error { + ret := rawConsoleTimerEnd(timerId) + return errnoFromU16(uint16(ret)) +} + +// ProcedureStartMutTx starts a mutable transaction for a procedure. +// Returns the current timestamp in microseconds since the Unix epoch. +func ProcedureStartMutTx() (int64, error) { + var timestamp int64 + ret := rawProcedureStartMutTx(×tamp) + if err := errnoFromU16(uint16(ret)); err != nil { + return 0, err + } + return timestamp, nil +} + +// ProcedureCommitMutTx commits the current mutable transaction. +func ProcedureCommitMutTx() error { + ret := rawProcedureCommitMutTx() + return errnoFromU16(uint16(ret)) +} + +// ProcedureAbortMutTx aborts the current mutable transaction. +func ProcedureAbortMutTx() error { + ret := rawProcedureAbortMutTx() + return errnoFromU16(uint16(ret)) +} + +// ProcedureSleepUntil suspends execution until the specified timestamp. +// Returns the current timestamp when execution resumes. +func ProcedureSleepUntil(wakeAtMicros int64) int64 { + return rawProcedureSleepUntil(wakeAtMicros) +} + +// GetIdentity returns the module's identity as a 32-byte array. +func GetIdentity() [32]byte { + var out [32]byte + rawIdentity(&out[0]) + return out +} + +// ProcedureHttpRequest sends an HTTP request from a procedure. +// requestBsatn is the BSATN-encoded HttpRequest. +// body is the raw request body bytes. +// Returns two BytesSource handles: responseSrc (BSATN-encoded HttpResponse or error string) and bodySrc (response body). +func ProcedureHttpRequest(requestBsatn []byte, body []byte) (responseSrc uint32, bodySrc uint32, err error) { + var out [2]uint32 + // The host traps on NULL pointers (even with length 0), so we must + // always pass valid non-null pointers. Use a sentinel byte for empty slices. + var sentinel [1]byte + reqPtr := &sentinel[0] + if len(requestBsatn) > 0 { + reqPtr = &requestBsatn[0] + } + bodyPtr := &sentinel[0] + if len(body) > 0 { + bodyPtr = &body[0] + } + ret := rawProcedureHttpRequest(reqPtr, uint32(len(requestBsatn)), bodyPtr, uint32(len(body)), &out[0]) + if err := errnoFromU16(uint16(ret)); err != nil { + // On HTTP_ERROR, out[0] has the BSATN-encoded error string. + return out[0], 0, err + } + return out[0], out[1], nil +} diff --git a/sdks/go/server/table/index.go b/sdks/go/server/table/index.go new file mode 100644 index 00000000000..38c06ad409e --- /dev/null +++ b/sdks/go/server/table/index.go @@ -0,0 +1,17 @@ +package table + +// IndexId is a numeric handle for a SpacetimeDB index. +type IndexId uint32 + +// UniqueIndex provides lookup by a unique column. +type UniqueIndex[R any, K any] interface { + FindBy(key K) (R, bool, error) + DeleteBy(key K) (bool, error) + UpdateBy(key K, row R) (R, error) +} + +// BTreeIndex provides range scanning. +type BTreeIndex[R any, K any] interface { + Scan() (Iterator[R], error) + ScanRange(start, end K) (Iterator[R], error) +} diff --git a/sdks/go/server/table/table.go b/sdks/go/server/table/table.go new file mode 100644 index 00000000000..e9981b5c554 --- /dev/null +++ b/sdks/go/server/table/table.go @@ -0,0 +1,30 @@ +package table + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// TableId is a numeric handle for a SpacetimeDB table. +type TableId uint32 + +// Iterator iterates over rows of type R. +type Iterator[R any] interface { + Next() (R, bool) + Close() +} + +// Table provides operations on a SpacetimeDB table. +// WASM execution is single-threaded, so no context is needed for table operations. +type Table[R any] interface { + TableId() TableId + Insert(row R) (R, error) + Delete(row R) error + Scan() (Iterator[R], error) + Count() (uint64, error) +} + +// EncodeFn encodes a row to BSATN bytes. +type EncodeFn[R any] func(R) []byte + +// DecodeFn decodes BSATN bytes to a row. +type DecodeFn[R any] func(bsatn.Reader) (R, error) diff --git a/sdks/go/server/table/table_impl.go b/sdks/go/server/table/table_impl.go new file mode 100644 index 00000000000..014ada34b54 --- /dev/null +++ b/sdks/go/server/table/table_impl.go @@ -0,0 +1,272 @@ +package table + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/server/sys" +) + +// NewTable creates a table handle. Called during module init. +func NewTable[R any](name string, encode EncodeFn[R], decode DecodeFn[R]) Table[R] { + return &tableImpl[R]{ + name: name, + encode: encode, + decode: decode, + } +} + +type tableImpl[R any] struct { + name string + tableId TableId + encode EncodeFn[R] + decode DecodeFn[R] + resolved bool +} + +func (t *tableImpl[R]) resolve() error { + if t.resolved { + return nil + } + id, err := sys.TableIdFromName(t.name) + if err != nil { + return err + } + t.tableId = TableId(id) + t.resolved = true + return nil +} + +func (t *tableImpl[R]) TableId() TableId { return t.tableId } + +func (t *tableImpl[R]) Insert(row R) (R, error) { + if err := t.resolve(); err != nil { + var zero R + return zero, err + } + rowBytes := t.encode(row) + seqBytes, err := sys.DatastoreInsertBSATN(uint32(t.tableId), rowBytes) + if err != nil { + var zero R + return zero, err + } + if len(seqBytes) > 0 { + r := bsatn.NewReader(seqBytes) + updated, decErr := t.decode(r) + if decErr != nil { + var zero R + return zero, decErr + } + return updated, nil + } + return row, nil +} + +func (t *tableImpl[R]) Delete(row R) error { + if err := t.resolve(); err != nil { + return err + } + rowBytes := t.encode(row) + _, err := sys.DatastoreDeleteAllByEqBSATN(uint32(t.tableId), rowBytes) + return err +} + +func (t *tableImpl[R]) Scan() (Iterator[R], error) { + if err := t.resolve(); err != nil { + return nil, err + } + iter, err := sys.DatastoreTableScanBSATN(uint32(t.tableId)) + if err != nil { + return nil, err + } + return &rowIterator[R]{ + sysIter: iter, + decode: t.decode, + }, nil +} + +func (t *tableImpl[R]) Count() (uint64, error) { + if err := t.resolve(); err != nil { + return 0, err + } + return sys.DatastoreTableRowCount(uint32(t.tableId)) +} + +// rowIterator wraps sys.RowIterator with type-safe decoding. +type rowIterator[R any] struct { + sysIter *sys.RowIterator + decode DecodeFn[R] +} + +func (ri *rowIterator[R]) Next() (R, bool) { + data, ok, err := ri.sysIter.Next() + if !ok || err != nil { + var zero R + return zero, false + } + r := bsatn.NewReader(data) + val, err := ri.decode(r) + if err != nil { + var zero R + return zero, false + } + return val, true +} + +func (ri *rowIterator[R]) Close() { + ri.sysIter.Close() +} + +// NewUniqueIndex creates a unique index handle. +func NewUniqueIndex[R any, K any](indexName string, tbl Table[R], encodeRow EncodeFn[R], encodeKey func(K) []byte, decode DecodeFn[R]) UniqueIndex[R, K] { + return &uniqueIndex[R, K]{ + indexName: indexName, + tbl: tbl, + encodeRow: encodeRow, + encodeKey: encodeKey, + decode: decode, + } +} + +type uniqueIndex[R any, K any] struct { + indexName string + indexId IndexId + tbl Table[R] + encodeRow EncodeFn[R] + encodeKey func(K) []byte + decode DecodeFn[R] + resolved bool +} + +func (u *uniqueIndex[R, K]) resolve() error { + if u.resolved { + return nil + } + id, err := sys.IndexIdFromName(u.indexName) + if err != nil { + return err + } + u.indexId = IndexId(id) + u.resolved = true + return nil +} + +func (u *uniqueIndex[R, K]) FindBy(key K) (R, bool, error) { + if err := u.resolve(); err != nil { + var zero R + return zero, false, err + } + keyBytes := u.encodeKey(key) + iter, err := sys.DatastoreIndexScanPointBSATN(uint32(u.indexId), keyBytes) + if err != nil { + var zero R + return zero, false, err + } + defer iter.Close() + + data, ok, err := iter.Next() + if !ok || err != nil { + var zero R + return zero, false, err + } + r := bsatn.NewReader(data) + val, decErr := u.decode(r) + if decErr != nil { + var zero R + return zero, false, decErr + } + return val, true, nil +} + +func (u *uniqueIndex[R, K]) DeleteBy(key K) (bool, error) { + if err := u.resolve(); err != nil { + return false, err + } + keyBytes := u.encodeKey(key) + deleted, err := sys.DatastoreDeleteByIndexScanPointBSATN(uint32(u.indexId), keyBytes) + if err != nil { + return false, err + } + return deleted > 0, nil +} + +func (u *uniqueIndex[R, K]) UpdateBy(key K, row R) (R, error) { + if err := u.resolve(); err != nil { + var zero R + return zero, err + } + rowBytes := u.encodeRow(row) + seqBytes, err := sys.DatastoreUpdateBSATN(uint32(u.tbl.TableId()), uint32(u.indexId), rowBytes) + if err != nil { + var zero R + return zero, err + } + if len(seqBytes) > 0 { + r := bsatn.NewReader(seqBytes) + updated, decErr := u.decode(r) + if decErr != nil { + var zero R + return zero, decErr + } + return updated, nil + } + return row, nil +} + +// NewBTreeIndex creates a BTree index handle for range scanning. +func NewBTreeIndex[R any, K any](indexName string, encodeKey func(K) []byte, decode DecodeFn[R]) BTreeIndex[R, K] { + return &btreeIndex[R, K]{ + indexName: indexName, + encodeKey: encodeKey, + decode: decode, + } +} + +type btreeIndex[R any, K any] struct { + indexName string + indexId IndexId + encodeKey func(K) []byte + decode DecodeFn[R] + resolved bool +} + +func (b *btreeIndex[R, K]) resolve() error { + if b.resolved { + return nil + } + id, err := sys.IndexIdFromName(b.indexName) + if err != nil { + return err + } + b.indexId = IndexId(id) + b.resolved = true + return nil +} + +func (b *btreeIndex[R, K]) Scan() (Iterator[R], error) { + if err := b.resolve(); err != nil { + return nil, err + } + iter, err := sys.DatastoreIndexScanRangeBSATN(uint32(b.indexId), nil, 0, nil, nil) + if err != nil { + return nil, err + } + return &rowIterator[R]{ + sysIter: iter, + decode: b.decode, + }, nil +} + +func (b *btreeIndex[R, K]) ScanRange(start, end K) (Iterator[R], error) { + if err := b.resolve(); err != nil { + return nil, err + } + startBytes := b.encodeKey(start) + endBytes := b.encodeKey(end) + iter, err := sys.DatastoreIndexScanRangeBSATN(uint32(b.indexId), nil, 0, startBytes, endBytes) + if err != nil { + return nil, err + } + return &rowIterator[R]{ + sysIter: iter, + decode: b.decode, + }, nil +} diff --git a/sdks/go/stdb-gen b/sdks/go/stdb-gen new file mode 100755 index 00000000000..10ba4d7bb8f Binary files /dev/null and b/sdks/go/stdb-gen differ diff --git a/sdks/go/types/algebraic_type.go b/sdks/go/types/algebraic_type.go new file mode 100644 index 00000000000..4a0868d41c1 --- /dev/null +++ b/sdks/go/types/algebraic_type.go @@ -0,0 +1,156 @@ +package types + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + +// AlgebraicType describes a type in the SpacetimeDB type system. +// Each variant is serialized as a BSATN sum type with a specific tag. +type AlgebraicType interface { + bsatn.Serializable + algebraicTypeTag() uint8 +} + +// TypeRef is an index into a Typespace. +type TypeRef uint32 + +// Primitive type constructors. + +// AlgTypeBool returns an AlgebraicType for bool (tag 5). +func AlgTypeBool() AlgebraicType { return &algTypePrimitive{tag: 5} } + +// AlgTypeI8 returns an AlgebraicType for i8 (tag 6). +func AlgTypeI8() AlgebraicType { return &algTypePrimitive{tag: 6} } + +// AlgTypeU8 returns an AlgebraicType for u8 (tag 7). +func AlgTypeU8() AlgebraicType { return &algTypePrimitive{tag: 7} } + +// AlgTypeI16 returns an AlgebraicType for i16 (tag 8). +func AlgTypeI16() AlgebraicType { return &algTypePrimitive{tag: 8} } + +// AlgTypeU16 returns an AlgebraicType for u16 (tag 9). +func AlgTypeU16() AlgebraicType { return &algTypePrimitive{tag: 9} } + +// AlgTypeI32 returns an AlgebraicType for i32 (tag 10). +func AlgTypeI32() AlgebraicType { return &algTypePrimitive{tag: 10} } + +// AlgTypeU32 returns an AlgebraicType for u32 (tag 11). +func AlgTypeU32() AlgebraicType { return &algTypePrimitive{tag: 11} } + +// AlgTypeI64 returns an AlgebraicType for i64 (tag 12). +func AlgTypeI64() AlgebraicType { return &algTypePrimitive{tag: 12} } + +// AlgTypeU64 returns an AlgebraicType for u64 (tag 13). +func AlgTypeU64() AlgebraicType { return &algTypePrimitive{tag: 13} } + +// AlgTypeI128 returns an AlgebraicType for i128 (tag 14). +func AlgTypeI128() AlgebraicType { return &algTypePrimitive{tag: 14} } + +// AlgTypeU128 returns an AlgebraicType for u128 (tag 15). +func AlgTypeU128() AlgebraicType { return &algTypePrimitive{tag: 15} } + +// AlgTypeI256 returns an AlgebraicType for i256 (tag 16). +func AlgTypeI256() AlgebraicType { return &algTypePrimitive{tag: 16} } + +// AlgTypeU256 returns an AlgebraicType for u256 (tag 17). +func AlgTypeU256() AlgebraicType { return &algTypePrimitive{tag: 17} } + +// AlgTypeF32 returns an AlgebraicType for f32 (tag 18). +func AlgTypeF32() AlgebraicType { return &algTypePrimitive{tag: 18} } + +// AlgTypeF64 returns an AlgebraicType for f64 (tag 19). +func AlgTypeF64() AlgebraicType { return &algTypePrimitive{tag: 19} } + +// AlgTypeString returns an AlgebraicType for string (tag 4). +func AlgTypeString() AlgebraicType { return &algTypePrimitive{tag: 4} } + +// Compound type constructors. + +// AlgTypeRef returns an AlgebraicType referencing another type by index (tag 0). +func AlgTypeRef(ref TypeRef) AlgebraicType { return &algTypeRef{ref: ref} } + +// AlgTypeArray returns an AlgebraicType for a homogeneous array (tag 3). +func AlgTypeArray(elemType AlgebraicType) AlgebraicType { return &algTypeArray{elem: elemType} } + +// AlgTypeMap returns an AlgebraicType for a map from key to value (tag 20). +func AlgTypeMap(keyType, valueType AlgebraicType) AlgebraicType { + return &algTypeMap{key: keyType, value: valueType} +} + +// AlgTypeProduct returns an AlgebraicType wrapping a ProductType (tag 2). +func AlgTypeProduct(pt ProductType) AlgebraicType { return &algTypeProduct{pt: pt} } + +// AlgTypeSum returns an AlgebraicType wrapping a SumType (tag 1). +func AlgTypeSum(st SumType) AlgebraicType { return &algTypeSum{st: st} } + +// algTypePrimitive represents primitive/scalar types that carry no payload. +type algTypePrimitive struct { + tag uint8 +} + +func (a *algTypePrimitive) algebraicTypeTag() uint8 { return a.tag } + +func (a *algTypePrimitive) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(a.tag) + // Primitive types are unit variants (empty product payload). +} + +// algTypeRef is a reference to another type in the Typespace. +type algTypeRef struct { + ref TypeRef +} + +func (a *algTypeRef) algebraicTypeTag() uint8 { return 0 } + +func (a *algTypeRef) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(0) + w.PutU32(uint32(a.ref)) +} + +// algTypeArray describes an array of a single element type. +type algTypeArray struct { + elem AlgebraicType +} + +func (a *algTypeArray) algebraicTypeTag() uint8 { return 3 } + +func (a *algTypeArray) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(3) + a.elem.WriteBsatn(w) +} + +// algTypeMap describes a map with key and value types. +type algTypeMap struct { + key, value AlgebraicType +} + +func (a *algTypeMap) algebraicTypeTag() uint8 { return 20 } + +func (a *algTypeMap) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(20) + // MapType is a product of (key_ty, value_ty). + a.key.WriteBsatn(w) + a.value.WriteBsatn(w) +} + +// algTypeProduct wraps a ProductType as an AlgebraicType. +type algTypeProduct struct { + pt ProductType +} + +func (a *algTypeProduct) algebraicTypeTag() uint8 { return 2 } + +func (a *algTypeProduct) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(2) + a.pt.WriteBsatn(w) +} + +// algTypeSum wraps a SumType as an AlgebraicType. +type algTypeSum struct { + st SumType +} + +func (a *algTypeSum) algebraicTypeTag() uint8 { return 1 } + +func (a *algTypeSum) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(1) + a.st.WriteBsatn(w) +} diff --git a/sdks/go/types/connection_id.go b/sdks/go/types/connection_id.go new file mode 100644 index 00000000000..9701d071513 --- /dev/null +++ b/sdks/go/types/connection_id.go @@ -0,0 +1,66 @@ +package types + +import ( + "encoding/binary" + "encoding/hex" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// ConnectionId is a 16-byte value identifying a client connection. +// In BSATN it is encoded as a product-wrapped u128 (16 raw bytes). +type ConnectionId interface { + bsatn.Serializable + Bytes() [16]byte + IsZero() bool + String() string +} + +// NewConnectionId creates a ConnectionId from a 16-byte array. +func NewConnectionId(b [16]byte) ConnectionId { + return &connectionId{data: b} +} + +// NewConnectionIdFromU64s reconstructs a ConnectionId from 2 uint64 values (WASM ABI format). +// Each u64 is in little-endian byte order. +func NewConnectionIdFromU64s(c0, c1 uint64) ConnectionId { + var b [16]byte + binary.LittleEndian.PutUint64(b[0:8], c0) + binary.LittleEndian.PutUint64(b[8:16], c1) + return &connectionId{data: b} +} + +// ReadConnectionId reads a ConnectionId from a BSATN reader (16 bytes). +func ReadConnectionId(r bsatn.Reader) (ConnectionId, error) { + b, err := r.GetBytes(16) + if err != nil { + return nil, err + } + var data [16]byte + copy(data[:], b) + return &connectionId{data: data}, nil +} + +type connectionId struct { + data [16]byte +} + +func (c *connectionId) WriteBsatn(w bsatn.Writer) { + w.PutBytes(c.data[:]) +} + +func (c *connectionId) Bytes() [16]byte { return c.data } + +func (c *connectionId) IsZero() bool { + return c.data == [16]byte{} +} + +func (c *connectionId) String() string { + // Internal storage is little-endian (matching BSATN/WASM ABI). + // Display format is big-endian hex (matching Rust ConnectionId::to_hex). + var be [16]byte + for i := 0; i < 16; i++ { + be[i] = c.data[15-i] + } + return hex.EncodeToString(be[:]) +} diff --git a/sdks/go/types/energy_quanta.go b/sdks/go/types/energy_quanta.go new file mode 100644 index 00000000000..06c4a70c676 --- /dev/null +++ b/sdks/go/types/energy_quanta.go @@ -0,0 +1,40 @@ +package types + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// EnergyQuanta wraps a Uint128 representing an energy budget. +type EnergyQuanta interface { + bsatn.Serializable + Value() Uint128 + String() string +} + +// NewEnergyQuanta creates an EnergyQuanta from a Uint128. +func NewEnergyQuanta(v Uint128) EnergyQuanta { + return &energyQuanta{value: v} +} + +// ReadEnergyQuanta reads an EnergyQuanta from a BSATN reader (u128). +func ReadEnergyQuanta(r bsatn.Reader) (EnergyQuanta, error) { + v, err := ReadUint128(r) + if err != nil { + return nil, err + } + return &energyQuanta{value: v}, nil +} + +type energyQuanta struct { + value Uint128 +} + +func (e *energyQuanta) WriteBsatn(w bsatn.Writer) { + e.value.WriteBsatn(w) +} + +func (e *energyQuanta) Value() Uint128 { return e.value } + +func (e *energyQuanta) String() string { + return e.value.String() +} diff --git a/sdks/go/types/identity.go b/sdks/go/types/identity.go new file mode 100644 index 00000000000..085c09339e7 --- /dev/null +++ b/sdks/go/types/identity.go @@ -0,0 +1,68 @@ +package types + +import ( + "encoding/binary" + "encoding/hex" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// Identity is a 32-byte value representing a user identity. +// In BSATN it is encoded as a product containing a u256 (32 raw bytes). +type Identity interface { + bsatn.Serializable + Bytes() [32]byte + IsZero() bool + String() string +} + +// NewIdentity creates an Identity from a 32-byte array. +func NewIdentity(b [32]byte) Identity { + return &identity{data: b} +} + +// NewIdentityFromU64s reconstructs an Identity from 4 uint64 values (WASM ABI format). +// Each u64 is in little-endian byte order. +func NewIdentityFromU64s(s0, s1, s2, s3 uint64) Identity { + var b [32]byte + binary.LittleEndian.PutUint64(b[0:8], s0) + binary.LittleEndian.PutUint64(b[8:16], s1) + binary.LittleEndian.PutUint64(b[16:24], s2) + binary.LittleEndian.PutUint64(b[24:32], s3) + return &identity{data: b} +} + +// ReadIdentity reads an Identity from a BSATN reader (32 bytes). +func ReadIdentity(r bsatn.Reader) (Identity, error) { + b, err := r.GetBytes(32) + if err != nil { + return nil, err + } + var data [32]byte + copy(data[:], b) + return &identity{data: data}, nil +} + +type identity struct { + data [32]byte +} + +func (id *identity) WriteBsatn(w bsatn.Writer) { + w.PutBytes(id.data[:]) +} + +func (id *identity) Bytes() [32]byte { return id.data } + +func (id *identity) IsZero() bool { + return id.data == [32]byte{} +} + +func (id *identity) String() string { + // Internal storage is little-endian (matching BSATN/WASM ABI). + // Display format is big-endian hex (matching Rust Identity::to_hex). + var be [32]byte + for i := 0; i < 32; i++ { + be[i] = id.data[31-i] + } + return hex.EncodeToString(be[:]) +} diff --git a/sdks/go/types/int128.go b/sdks/go/types/int128.go new file mode 100644 index 00000000000..9f80666260e --- /dev/null +++ b/sdks/go/types/int128.go @@ -0,0 +1,83 @@ +package types + +import ( + "encoding/binary" + "math/big" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// Int128 represents a signed 128-bit integer stored as 16 bytes little-endian (two's complement). +type Int128 interface { + bsatn.Serializable + Bytes() [16]byte + Lo() uint64 + Hi() uint64 + IsZero() bool + String() string +} + +// NewInt128 creates an Int128 from low and high 64-bit components. +func NewInt128(lo, hi uint64) Int128 { + var b [16]byte + binary.LittleEndian.PutUint64(b[0:8], lo) + binary.LittleEndian.PutUint64(b[8:16], hi) + return &int128{data: b} +} + +// NewInt128FromBytes creates an Int128 from a 16-byte little-endian array (two's complement). +func NewInt128FromBytes(b [16]byte) Int128 { + return &int128{data: b} +} + +// ReadInt128 reads an Int128 from a BSATN reader (16 bytes little-endian). +func ReadInt128(r bsatn.Reader) (Int128, error) { + b, err := r.GetBytes(16) + if err != nil { + return nil, err + } + var data [16]byte + copy(data[:], b) + return &int128{data: data}, nil +} + +type int128 struct { + data [16]byte +} + +func (i *int128) WriteBsatn(w bsatn.Writer) { + w.PutBytes(i.data[:]) +} + +func (i *int128) Bytes() [16]byte { return i.data } + +func (i *int128) Lo() uint64 { + return binary.LittleEndian.Uint64(i.data[0:8]) +} + +func (i *int128) Hi() uint64 { + return binary.LittleEndian.Uint64(i.data[8:16]) +} + +func (i *int128) IsZero() bool { + return i.data == [16]byte{} +} + +func (i *int128) String() string { + // Convert LE bytes to big-endian for math/big. + var be [16]byte + for idx := 0; idx < 16; idx++ { + be[idx] = i.data[15-idx] + } + var n big.Int + if be[0]&0x80 != 0 { + // Negative two's complement: value = unsigned_value - 2^128. + n.SetBytes(be[:]) + var mod big.Int + mod.Lsh(big.NewInt(1), 128) + n.Sub(&n, &mod) + } else { + n.SetBytes(be[:]) + } + return n.String() +} diff --git a/sdks/go/types/int256.go b/sdks/go/types/int256.go new file mode 100644 index 00000000000..9412fa88cd1 --- /dev/null +++ b/sdks/go/types/int256.go @@ -0,0 +1,76 @@ +package types + +import ( + "encoding/binary" + "math/big" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// Int256 represents a signed 256-bit integer stored as 32 bytes little-endian (two's complement). +type Int256 interface { + bsatn.Serializable + Bytes() [32]byte + IsZero() bool + String() string +} + +// NewInt256 creates an Int256 from a 32-byte little-endian array (two's complement). +func NewInt256(b [32]byte) Int256 { + return &int256{data: b} +} + +// NewInt256FromU64s creates an Int256 from four uint64 values stored little-endian: +// a=bytes[0:8], b=bytes[8:16], c=bytes[16:24], d=bytes[24:32]. +func NewInt256FromU64s(a, b, c, d uint64) Int256 { + var buf [32]byte + binary.LittleEndian.PutUint64(buf[0:8], a) + binary.LittleEndian.PutUint64(buf[8:16], b) + binary.LittleEndian.PutUint64(buf[16:24], c) + binary.LittleEndian.PutUint64(buf[24:32], d) + return &int256{data: buf} +} + +// ReadInt256 reads an Int256 from a BSATN reader (32 bytes little-endian). +func ReadInt256(r bsatn.Reader) (Int256, error) { + b, err := r.GetBytes(32) + if err != nil { + return nil, err + } + var data [32]byte + copy(data[:], b) + return &int256{data: data}, nil +} + +type int256 struct { + data [32]byte +} + +func (i *int256) WriteBsatn(w bsatn.Writer) { + w.PutBytes(i.data[:]) +} + +func (i *int256) Bytes() [32]byte { return i.data } + +func (i *int256) IsZero() bool { + return i.data == [32]byte{} +} + +func (i *int256) String() string { + // Convert LE bytes to big-endian for math/big. + var be [32]byte + for idx := 0; idx < 32; idx++ { + be[idx] = i.data[31-idx] + } + var n big.Int + if be[0]&0x80 != 0 { + // Negative two's complement: value = unsigned_value - 2^256. + n.SetBytes(be[:]) + var mod big.Int + mod.Lsh(big.NewInt(1), 256) + n.Sub(&n, &mod) + } else { + n.SetBytes(be[:]) + } + return n.String() +} diff --git a/sdks/go/types/product_type.go b/sdks/go/types/product_type.go new file mode 100644 index 00000000000..d38c3f7cf0b --- /dev/null +++ b/sdks/go/types/product_type.go @@ -0,0 +1,40 @@ +package types + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + +// ProductTypeElement is a named field in a product type. +type ProductTypeElement struct { + Name string + AlgebraicType AlgebraicType +} + +// ProductType describes a product (struct) type with named fields. +type ProductType interface { + bsatn.Serializable + Elements() []ProductTypeElement +} + +// NewProductType creates a ProductType from the given elements. +func NewProductType(elements ...ProductTypeElement) ProductType { + return &productType{elements: elements} +} + +type productType struct { + elements []ProductTypeElement +} + +func (p *productType) Elements() []ProductTypeElement { + return p.elements +} + +func (p *productType) WriteBsatn(w bsatn.Writer) { + // ProductType is encoded as array of ProductTypeElement. + w.PutArrayLen(uint32(len(p.elements))) + for _, elem := range p.elements { + // Each element is a product of (name: Option, algebraic_type: AlgebraicType). + // Name is always present (Some). + w.PutSumTag(0) // Some + w.PutString(elem.Name) + elem.AlgebraicType.WriteBsatn(w) + } +} diff --git a/sdks/go/types/schedule_at.go b/sdks/go/types/schedule_at.go new file mode 100644 index 00000000000..f99094f8589 --- /dev/null +++ b/sdks/go/types/schedule_at.go @@ -0,0 +1,61 @@ +package types + +import ( + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// ScheduleAt is a sum type representing either a recurring interval or a specific time. +// - Tag 0: Interval (TimeDuration) +// - Tag 1: Time (Timestamp) +type ScheduleAt interface { + bsatn.Serializable + isScheduleAt() +} + +// ScheduleAtInterval is the Interval variant of ScheduleAt (tag 0). +type ScheduleAtInterval struct { + Value TimeDuration +} + +func (ScheduleAtInterval) isScheduleAt() {} + +func (s ScheduleAtInterval) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(0) + s.Value.WriteBsatn(w) +} + +// ScheduleAtTime is the Time variant of ScheduleAt (tag 1). +type ScheduleAtTime struct { + Value Timestamp +} + +func (ScheduleAtTime) isScheduleAt() {} + +func (s ScheduleAtTime) WriteBsatn(w bsatn.Writer) { + w.PutSumTag(1) + s.Value.WriteBsatn(w) +} + +// ReadScheduleAt reads a ScheduleAt from a BSATN reader. +func ReadScheduleAt(r bsatn.Reader) (ScheduleAt, error) { + tag, err := r.GetSumTag() + if err != nil { + return nil, err + } + switch tag { + case 0: + v, err := ReadTimeDuration(r) + if err != nil { + return nil, err + } + return ScheduleAtInterval{Value: v}, nil + case 1: + v, err := ReadTimestamp(r) + if err != nil { + return nil, err + } + return ScheduleAtTime{Value: v}, nil + default: + return nil, &bsatn.ErrInvalidTag{Tag: tag, SumName: "ScheduleAt"} + } +} diff --git a/sdks/go/types/sum_type.go b/sdks/go/types/sum_type.go new file mode 100644 index 00000000000..ab114582032 --- /dev/null +++ b/sdks/go/types/sum_type.go @@ -0,0 +1,40 @@ +package types + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + +// SumTypeVariant is a variant in a sum type. +type SumTypeVariant struct { + Name string + AlgebraicType AlgebraicType +} + +// SumType describes a sum (enum/union) type with named variants. +type SumType interface { + bsatn.Serializable + Variants() []SumTypeVariant +} + +// NewSumType creates a SumType from the given variants. +func NewSumType(variants ...SumTypeVariant) SumType { + return &sumType{variants: variants} +} + +type sumType struct { + variants []SumTypeVariant +} + +func (s *sumType) Variants() []SumTypeVariant { + return s.variants +} + +func (s *sumType) WriteBsatn(w bsatn.Writer) { + // SumType is encoded as array of SumTypeVariant. + w.PutArrayLen(uint32(len(s.variants))) + for _, v := range s.variants { + // Each variant is a product of (name: Option, algebraic_type: AlgebraicType). + // Name is always present (Some). + w.PutSumTag(0) // Some + w.PutString(v.Name) + v.AlgebraicType.WriteBsatn(w) + } +} diff --git a/sdks/go/types/time_duration.go b/sdks/go/types/time_duration.go new file mode 100644 index 00000000000..c8a95b11420 --- /dev/null +++ b/sdks/go/types/time_duration.go @@ -0,0 +1,57 @@ +package types + +import ( + "fmt" + "time" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// TimeDuration wraps an i64 representing a duration in microseconds. +type TimeDuration interface { + bsatn.Serializable + Microseconds() int64 + Duration() time.Duration + String() string +} + +// NewTimeDuration creates a TimeDuration from microseconds. +func NewTimeDuration(microseconds int64) TimeDuration { + return &timeDuration{micros: microseconds} +} + +// ReadTimeDuration reads a TimeDuration from a BSATN reader (i64). +func ReadTimeDuration(r bsatn.Reader) (TimeDuration, error) { + v, err := r.GetI64() + if err != nil { + return nil, err + } + return &timeDuration{micros: v}, nil +} + +type timeDuration struct { + micros int64 +} + +func (d *timeDuration) WriteBsatn(w bsatn.Writer) { + w.PutI64(d.micros) +} + +func (d *timeDuration) Microseconds() int64 { return d.micros } + +func (d *timeDuration) Duration() time.Duration { + // time.Duration is in nanoseconds, so multiply microseconds by 1000. + return time.Duration(d.micros) * time.Microsecond +} + +func (d *timeDuration) String() string { + micros := d.micros + sign := "" + if micros < 0 { + sign = "-" + micros = -micros + } + secs := micros / 1_000_000 + remainder := micros % 1_000_000 + return fmt.Sprintf("%s%d.%06d", sign, secs, remainder) +} diff --git a/sdks/go/types/timestamp.go b/sdks/go/types/timestamp.go new file mode 100644 index 00000000000..a9b5bbfc100 --- /dev/null +++ b/sdks/go/types/timestamp.go @@ -0,0 +1,47 @@ +package types + +import ( + "time" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// Timestamp wraps an i64 representing microseconds since the Unix epoch. +type Timestamp interface { + bsatn.Serializable + Microseconds() int64 + Time() time.Time + String() string +} + +// NewTimestamp creates a Timestamp from microseconds since Unix epoch. +func NewTimestamp(microseconds int64) Timestamp { + return ×tamp{micros: microseconds} +} + +// ReadTimestamp reads a Timestamp from a BSATN reader (i64). +func ReadTimestamp(r bsatn.Reader) (Timestamp, error) { + v, err := r.GetI64() + if err != nil { + return nil, err + } + return ×tamp{micros: v}, nil +} + +type timestamp struct { + micros int64 +} + +func (t *timestamp) WriteBsatn(w bsatn.Writer) { + w.PutI64(t.micros) +} + +func (t *timestamp) Microseconds() int64 { return t.micros } + +func (t *timestamp) Time() time.Time { + return time.UnixMicro(t.micros) +} + +func (t *timestamp) String() string { + return t.Time().UTC().Format("2006-01-02T15:04:05.000000-07:00") +} diff --git a/sdks/go/types/types_test.go b/sdks/go/types/types_test.go new file mode 100644 index 00000000000..d4b39b51a5d --- /dev/null +++ b/sdks/go/types/types_test.go @@ -0,0 +1,656 @@ +package types_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// === UINT128 TESTS === + +func TestUint128RoundTrip(t *testing.T) { + original := types.NewUint128(0x0102030405060708, 0x090A0B0C0D0E0F10) + encoded := bsatn.Encode(original) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadUint128(r) + require.NoError(t, err) + assert.Equal(t, original.Bytes(), decoded.Bytes()) + assert.Equal(t, original.Lo(), decoded.Lo()) + assert.Equal(t, original.Hi(), decoded.Hi()) +} + +func TestUint128ExactBytes(t *testing.T) { + u := types.NewUint128(1, 0) + encoded := bsatn.Encode(u) + // lo=1 in LE, hi=0 in LE -> 16 bytes + expected := []byte{ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // lo = 1 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hi = 0 + } + assert.Equal(t, expected, encoded) +} + +func TestUint128MaxValue(t *testing.T) { + u := types.NewUint128(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF) + encoded := bsatn.Encode(u) + expected := make([]byte, 16) + for i := range expected { + expected[i] = 0xFF + } + assert.Equal(t, expected, encoded) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadUint128(r) + require.NoError(t, err) + assert.Equal(t, uint64(0xFFFFFFFFFFFFFFFF), decoded.Lo()) + assert.Equal(t, uint64(0xFFFFFFFFFFFFFFFF), decoded.Hi()) +} + +func TestUint128Zero(t *testing.T) { + u := types.NewUint128(0, 0) + assert.True(t, u.IsZero()) + + nonZero := types.NewUint128(1, 0) + assert.False(t, nonZero.IsZero()) +} + +func TestUint128FromBytes(t *testing.T) { + var b [16]byte + b[0] = 0x42 + b[15] = 0xFF + u := types.NewUint128FromBytes(b) + assert.Equal(t, b, u.Bytes()) +} + +func TestUint128String(t *testing.T) { + u := types.NewUint128(1, 0) + assert.Equal(t, "1", u.String()) + + u2 := types.NewUint128(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF) + assert.Equal(t, "340282366920938463463374607431768211455", u2.String()) +} + +func TestUint128ReadBufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{0x01, 0x02, 0x03}) + _, err := types.ReadUint128(r) + require.Error(t, err) +} + +// === INT128 TESTS === + +func TestInt128RoundTrip(t *testing.T) { + original := types.NewInt128(0x0102030405060708, 0x090A0B0C0D0E0F10) + encoded := bsatn.Encode(original) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadInt128(r) + require.NoError(t, err) + assert.Equal(t, original.Bytes(), decoded.Bytes()) + assert.Equal(t, original.Lo(), decoded.Lo()) + assert.Equal(t, original.Hi(), decoded.Hi()) +} + +func TestInt128ExactBytes(t *testing.T) { + // -1 in two's complement: all 0xFF bytes + i := types.NewInt128(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF) + encoded := bsatn.Encode(i) + expected := make([]byte, 16) + for idx := range expected { + expected[idx] = 0xFF + } + assert.Equal(t, expected, encoded) +} + +func TestInt128Zero(t *testing.T) { + i := types.NewInt128(0, 0) + assert.True(t, i.IsZero()) + + nonZero := types.NewInt128(1, 0) + assert.False(t, nonZero.IsZero()) +} + +func TestInt128FromBytes(t *testing.T) { + var b [16]byte + b[0] = 0x42 + b[15] = 0x80 // sign bit set + i := types.NewInt128FromBytes(b) + assert.Equal(t, b, i.Bytes()) +} + +func TestInt128String(t *testing.T) { + i := types.NewInt128(1, 0) + assert.Equal(t, "1", i.String()) + + // -1 in two's complement + neg := types.NewInt128(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF) + assert.Equal(t, "-1", neg.String()) +} + +func TestInt128ReadBufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{0x01}) + _, err := types.ReadInt128(r) + require.Error(t, err) +} + +// === UINT256 TESTS === + +func TestUint256RoundTrip(t *testing.T) { + var b [32]byte + for i := range b { + b[i] = byte(i) + } + original := types.NewUint256(b) + encoded := bsatn.Encode(original) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadUint256(r) + require.NoError(t, err) + assert.Equal(t, original.Bytes(), decoded.Bytes()) +} + +func TestUint256ExactBytes(t *testing.T) { + u := types.NewUint256FromU64s(1, 0, 0, 0) + encoded := bsatn.Encode(u) + expected := []byte{ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // a = 1 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // b = 0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // c = 0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // d = 0 + } + assert.Equal(t, expected, encoded) +} + +func TestUint256MaxValue(t *testing.T) { + u := types.NewUint256FromU64s(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF) + encoded := bsatn.Encode(u) + expected := make([]byte, 32) + for i := range expected { + expected[i] = 0xFF + } + assert.Equal(t, expected, encoded) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadUint256(r) + require.NoError(t, err) + assert.Equal(t, u.Bytes(), decoded.Bytes()) +} + +func TestUint256Zero(t *testing.T) { + var zero [32]byte + u := types.NewUint256(zero) + assert.True(t, u.IsZero()) + + nonZero := types.NewUint256FromU64s(1, 0, 0, 0) + assert.False(t, nonZero.IsZero()) +} + +func TestUint256FromU64s(t *testing.T) { + u := types.NewUint256FromU64s(0x0102030405060708, 0x090A0B0C0D0E0F10, 0x1112131415161718, 0x191A1B1C1D1E1F20) + b := u.Bytes() + r := bsatn.NewReader(b[:]) + + v0, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(0x0102030405060708), v0) + + v1, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(0x090A0B0C0D0E0F10), v1) + + v2, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(0x1112131415161718), v2) + + v3, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(0x191A1B1C1D1E1F20), v3) +} + +func TestUint256String(t *testing.T) { + u := types.NewUint256FromU64s(1, 0, 0, 0) + assert.Equal(t, "1", u.String()) +} + +func TestUint256ReadBufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{0x01, 0x02, 0x03}) + _, err := types.ReadUint256(r) + require.Error(t, err) +} + +// === INT256 TESTS === + +func TestInt256RoundTrip(t *testing.T) { + var b [32]byte + for i := range b { + b[i] = byte(i + 0x10) + } + original := types.NewInt256(b) + encoded := bsatn.Encode(original) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadInt256(r) + require.NoError(t, err) + assert.Equal(t, original.Bytes(), decoded.Bytes()) +} + +func TestInt256ExactBytes(t *testing.T) { + // -1 in two's complement: all 0xFF bytes + i := types.NewInt256FromU64s(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF) + encoded := bsatn.Encode(i) + expected := make([]byte, 32) + for idx := range expected { + expected[idx] = 0xFF + } + assert.Equal(t, expected, encoded) +} + +func TestInt256Zero(t *testing.T) { + var zero [32]byte + i := types.NewInt256(zero) + assert.True(t, i.IsZero()) + + nonZero := types.NewInt256FromU64s(1, 0, 0, 0) + assert.False(t, nonZero.IsZero()) +} + +func TestInt256FromU64s(t *testing.T) { + i := types.NewInt256FromU64s(0xDEADBEEFCAFEBABE, 0x0102030405060708, 0x1111111111111111, 0x8000000000000000) + b := i.Bytes() + r := bsatn.NewReader(b[:]) + + v0, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(0xDEADBEEFCAFEBABE), v0) + + v1, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(0x0102030405060708), v1) + + v2, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(0x1111111111111111), v2) + + v3, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(0x8000000000000000), v3) +} + +func TestInt256String(t *testing.T) { + i := types.NewInt256FromU64s(1, 0, 0, 0) + assert.Equal(t, "1", i.String()) + + // -1 in two's complement + neg := types.NewInt256FromU64s(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF) + assert.Equal(t, "-1", neg.String()) +} + +func TestInt256ReadBufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{0x01, 0x02}) + _, err := types.ReadInt256(r) + require.Error(t, err) +} + +// === IDENTITY TESTS === + +func TestIdentityRoundTrip(t *testing.T) { + var b [32]byte + for i := range b { + b[i] = byte(i) + } + original := types.NewIdentity(b) + encoded := bsatn.Encode(original) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadIdentity(r) + require.NoError(t, err) + assert.Equal(t, original.Bytes(), decoded.Bytes()) +} + +func TestIdentityExactBytes(t *testing.T) { + var b [32]byte + for i := range b { + b[i] = byte(i) + } + id := types.NewIdentity(b) + encoded := bsatn.Encode(id) + // Identity is 32 raw bytes, no length prefix + assert.Len(t, encoded, 32) + assert.Equal(t, b[:], encoded) +} + +func TestIdentityZero(t *testing.T) { + var zero [32]byte + id := types.NewIdentity(zero) + assert.True(t, id.IsZero()) + + var nonZero [32]byte + nonZero[0] = 1 + id2 := types.NewIdentity(nonZero) + assert.False(t, id2.IsZero()) +} + +func TestIdentityString(t *testing.T) { + var b [32]byte + // Internal storage is little-endian, so b[0]-b[1] are least significant. + b[0] = 0xAB + b[1] = 0xCD + id := types.NewIdentity(b) + s := id.String() + // Display is big-endian hex, so LE bytes [0xAB, 0xCD, 0...] become "0000...00cdab" + assert.Equal(t, + "000000000000000000000000000000"+ + "000000000000000000000000000000"+"cdab", s) +} + +func TestIdentityFromU64s(t *testing.T) { + id := types.NewIdentityFromU64s(1, 2, 3, 4) + b := id.Bytes() + + r := bsatn.NewReader(b[:]) + lo0, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(1), lo0) + + lo1, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(2), lo1) + + hi0, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(3), hi0) + + hi1, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(4), hi1) +} + +func TestIdentityReadBufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{0x01, 0x02, 0x03}) + _, err := types.ReadIdentity(r) + require.Error(t, err) +} + +// === CONNECTION ID TESTS === + +func TestConnectionIdRoundTrip(t *testing.T) { + var b [16]byte + for i := range b { + b[i] = byte(i + 0x10) + } + original := types.NewConnectionId(b) + encoded := bsatn.Encode(original) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadConnectionId(r) + require.NoError(t, err) + assert.Equal(t, original.Bytes(), decoded.Bytes()) +} + +func TestConnectionIdExactBytes(t *testing.T) { + var b [16]byte + for i := range b { + b[i] = byte(i) + } + cid := types.NewConnectionId(b) + encoded := bsatn.Encode(cid) + // ConnectionId is 16 raw bytes + assert.Len(t, encoded, 16) + assert.Equal(t, b[:], encoded) +} + +func TestConnectionIdZero(t *testing.T) { + var zero [16]byte + cid := types.NewConnectionId(zero) + assert.True(t, cid.IsZero()) + + var nonZero [16]byte + nonZero[0] = 0xFF + cid2 := types.NewConnectionId(nonZero) + assert.False(t, cid2.IsZero()) +} + +func TestConnectionIdFromU64s(t *testing.T) { + cid := types.NewConnectionIdFromU64s(0xDEADBEEFCAFEBABE, 0x0102030405060708) + b := cid.Bytes() + + r := bsatn.NewReader(b[:]) + lo, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(0xDEADBEEFCAFEBABE), lo) + + hi, err := r.GetU64() + require.NoError(t, err) + assert.Equal(t, uint64(0x0102030405060708), hi) +} + +func TestConnectionIdString(t *testing.T) { + var b [16]byte + // Internal storage is little-endian, so b[0]-b[1] are least significant. + b[0] = 0xAB + b[1] = 0xCD + cid := types.NewConnectionId(b) + s := cid.String() + // Display is big-endian hex, so LE bytes [0xAB, 0xCD, 0...] become "0000...cdab" + assert.Equal(t, + "0000000000000000"+ + "000000000000"+"cdab", s) +} + +func TestConnectionIdReadBufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{0x01, 0x02}) + _, err := types.ReadConnectionId(r) + require.Error(t, err) +} + +// === TIMESTAMP TESTS === + +func TestTimestampRoundTrip(t *testing.T) { + original := types.NewTimestamp(1000000) // 1 second in microseconds + encoded := bsatn.Encode(original) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadTimestamp(r) + require.NoError(t, err) + assert.Equal(t, original.Microseconds(), decoded.Microseconds()) +} + +func TestTimestampExactBytes(t *testing.T) { + ts := types.NewTimestamp(42) + encoded := bsatn.Encode(ts) + // i64 LE encoding of 42 + expected := []byte{0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + assert.Equal(t, expected, encoded) +} + +func TestTimestampNegative(t *testing.T) { + ts := types.NewTimestamp(-1) + encoded := bsatn.Encode(ts) + expected := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + assert.Equal(t, expected, encoded) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadTimestamp(r) + require.NoError(t, err) + assert.Equal(t, int64(-1), decoded.Microseconds()) +} + +func TestTimestampTime(t *testing.T) { + micros := int64(1700000000000000) // some time in 2023 + ts := types.NewTimestamp(micros) + goTime := ts.Time() + assert.Equal(t, time.UnixMicro(micros), goTime) +} + +func TestTimestampString(t *testing.T) { + ts := types.NewTimestamp(0) // Unix epoch + s := ts.String() + // The string representation depends on local timezone, so just check it is non-empty + assert.NotEmpty(t, s) + // Also check a known timestamp to verify Time() conversion + assert.Equal(t, time.UnixMicro(0), ts.Time()) +} + +func TestTimestampReadBufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{0x01, 0x02, 0x03}) + _, err := types.ReadTimestamp(r) + require.Error(t, err) +} + +// === TIME DURATION TESTS === + +func TestTimeDurationRoundTrip(t *testing.T) { + original := types.NewTimeDuration(5000000) // 5 seconds + encoded := bsatn.Encode(original) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadTimeDuration(r) + require.NoError(t, err) + assert.Equal(t, original.Microseconds(), decoded.Microseconds()) +} + +func TestTimeDurationExactBytes(t *testing.T) { + d := types.NewTimeDuration(1000000) // 1 second in microseconds + encoded := bsatn.Encode(d) + // 1000000 = 0x000F4240 LE: {0x40, 0x42, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00} + expected := []byte{0x40, 0x42, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00} + assert.Equal(t, expected, encoded) +} + +func TestTimeDurationDuration(t *testing.T) { + d := types.NewTimeDuration(1000000) // 1 second + assert.Equal(t, time.Second, d.Duration()) +} + +func TestTimeDurationNegative(t *testing.T) { + d := types.NewTimeDuration(-1000000) // -1 second + assert.Equal(t, -time.Second, d.Duration()) +} + +func TestTimeDurationString(t *testing.T) { + d := types.NewTimeDuration(1500000) // 1.5 seconds + assert.Equal(t, "1.500000", d.String()) + + neg := types.NewTimeDuration(-1500000) // -1.5 seconds + assert.Equal(t, "-1.500000", neg.String()) +} + +func TestTimeDurationReadBufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{0x01, 0x02}) + _, err := types.ReadTimeDuration(r) + require.Error(t, err) +} + +// === ENERGY QUANTA TESTS === + +func TestEnergyQuantaRoundTrip(t *testing.T) { + u128 := types.NewUint128(12345, 0) + original := types.NewEnergyQuanta(u128) + encoded := bsatn.Encode(original) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadEnergyQuanta(r) + require.NoError(t, err) + assert.Equal(t, original.Value().Bytes(), decoded.Value().Bytes()) +} + +func TestEnergyQuantaExactBytes(t *testing.T) { + u128 := types.NewUint128(1, 0) + eq := types.NewEnergyQuanta(u128) + encoded := bsatn.Encode(eq) + // u128 is 16 raw bytes + expected := []byte{ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + assert.Equal(t, expected, encoded) +} + +func TestEnergyQuantaString(t *testing.T) { + u128 := types.NewUint128(42, 0) + eq := types.NewEnergyQuanta(u128) + assert.Equal(t, "42", eq.String()) +} + +func TestEnergyQuantaReadBufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{0x01, 0x02, 0x03}) + _, err := types.ReadEnergyQuanta(r) + require.Error(t, err) +} + +// === SCHEDULE AT TESTS === + +func TestScheduleAtIntervalRoundTrip(t *testing.T) { + dur := types.NewTimeDuration(5000000) // 5 seconds + original := types.ScheduleAtInterval{Value: dur} + encoded := bsatn.Encode(original) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadScheduleAt(r) + require.NoError(t, err) + + interval, ok := decoded.(types.ScheduleAtInterval) + require.True(t, ok) + assert.Equal(t, dur.Microseconds(), interval.Value.Microseconds()) +} + +func TestScheduleAtTimeRoundTrip(t *testing.T) { + ts := types.NewTimestamp(1700000000000000) + original := types.ScheduleAtTime{Value: ts} + encoded := bsatn.Encode(original) + + r := bsatn.NewReader(encoded) + decoded, err := types.ReadScheduleAt(r) + require.NoError(t, err) + + schedTime, ok := decoded.(types.ScheduleAtTime) + require.True(t, ok) + assert.Equal(t, ts.Microseconds(), schedTime.Value.Microseconds()) +} + +func TestScheduleAtIntervalExactBytes(t *testing.T) { + dur := types.NewTimeDuration(42) + sa := types.ScheduleAtInterval{Value: dur} + encoded := bsatn.Encode(sa) + // tag 0 + i64 LE 42 + expected := []byte{ + 0x00, // tag 0 (Interval) + 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // i64 42 + } + assert.Equal(t, expected, encoded) +} + +func TestScheduleAtTimeExactBytes(t *testing.T) { + ts := types.NewTimestamp(42) + sa := types.ScheduleAtTime{Value: ts} + encoded := bsatn.Encode(sa) + // tag 1 + i64 LE 42 + expected := []byte{ + 0x01, // tag 1 (Time) + 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // i64 42 + } + assert.Equal(t, expected, encoded) +} + +func TestScheduleAtInvalidTag(t *testing.T) { + // tag 5 is invalid + data := []byte{0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + r := bsatn.NewReader(data) + _, err := types.ReadScheduleAt(r) + require.Error(t, err) + var invalidTag *bsatn.ErrInvalidTag + assert.ErrorAs(t, err, &invalidTag) + assert.Equal(t, uint8(5), invalidTag.Tag) + assert.Equal(t, "ScheduleAt", invalidTag.SumName) +} + +func TestScheduleAtReadBufferTooShort(t *testing.T) { + r := bsatn.NewReader([]byte{}) + _, err := types.ReadScheduleAt(r) + require.Error(t, err) +} diff --git a/sdks/go/types/typespace.go b/sdks/go/types/typespace.go new file mode 100644 index 00000000000..94e0ce492ca --- /dev/null +++ b/sdks/go/types/typespace.go @@ -0,0 +1,57 @@ +package types + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + +// Typespace is a registry of types indexed by TypeRef. +type Typespace interface { + bsatn.Serializable + Add(at AlgebraicType) TypeRef + // Reserve allocates a slot and returns a TypeRef. The slot must be filled + // later via Set. This supports forward-referencing for recursive types. + Reserve() TypeRef + // Set updates the AlgebraicType at an existing TypeRef (from Add or Reserve). + Set(ref TypeRef, at AlgebraicType) + Get(ref TypeRef) AlgebraicType + Len() int +} + +// NewTypespace creates an empty Typespace. +func NewTypespace() Typespace { + return &typespace{} +} + +type typespace struct { + types []AlgebraicType +} + +func (t *typespace) Add(at AlgebraicType) TypeRef { + ref := TypeRef(len(t.types)) + t.types = append(t.types, at) + return ref +} + +func (t *typespace) Reserve() TypeRef { + ref := TypeRef(len(t.types)) + t.types = append(t.types, nil) + return ref +} + +func (t *typespace) Set(ref TypeRef, at AlgebraicType) { + t.types[ref] = at +} + +func (t *typespace) Get(ref TypeRef) AlgebraicType { + return t.types[ref] +} + +func (t *typespace) Len() int { + return len(t.types) +} + +func (t *typespace) WriteBsatn(w bsatn.Writer) { + // Typespace is encoded as array of AlgebraicType. + w.PutArrayLen(uint32(len(t.types))) + for _, at := range t.types { + at.WriteBsatn(w) + } +} diff --git a/sdks/go/types/uint128.go b/sdks/go/types/uint128.go new file mode 100644 index 00000000000..7fb859b82a9 --- /dev/null +++ b/sdks/go/types/uint128.go @@ -0,0 +1,75 @@ +package types + +import ( + "encoding/binary" + "math/big" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// Uint128 represents an unsigned 128-bit integer stored as 16 bytes little-endian. +type Uint128 interface { + bsatn.Serializable + Bytes() [16]byte + Lo() uint64 + Hi() uint64 + IsZero() bool + String() string +} + +// NewUint128 creates a Uint128 from low and high 64-bit components. +func NewUint128(lo, hi uint64) Uint128 { + var b [16]byte + binary.LittleEndian.PutUint64(b[0:8], lo) + binary.LittleEndian.PutUint64(b[8:16], hi) + return &uint128{data: b} +} + +// NewUint128FromBytes creates a Uint128 from a 16-byte little-endian array. +func NewUint128FromBytes(b [16]byte) Uint128 { + return &uint128{data: b} +} + +// ReadUint128 reads a Uint128 from a BSATN reader (16 bytes little-endian). +func ReadUint128(r bsatn.Reader) (Uint128, error) { + b, err := r.GetBytes(16) + if err != nil { + return nil, err + } + var data [16]byte + copy(data[:], b) + return &uint128{data: data}, nil +} + +type uint128 struct { + data [16]byte +} + +func (u *uint128) WriteBsatn(w bsatn.Writer) { + w.PutBytes(u.data[:]) +} + +func (u *uint128) Bytes() [16]byte { return u.data } + +func (u *uint128) Lo() uint64 { + return binary.LittleEndian.Uint64(u.data[0:8]) +} + +func (u *uint128) Hi() uint64 { + return binary.LittleEndian.Uint64(u.data[8:16]) +} + +func (u *uint128) IsZero() bool { + return u.data == [16]byte{} +} + +func (u *uint128) String() string { + // Convert LE bytes to big-endian for math/big, then format as decimal. + var be [16]byte + for i := 0; i < 16; i++ { + be[i] = u.data[15-i] + } + var n big.Int + n.SetBytes(be[:]) + return n.String() +} diff --git a/sdks/go/types/uint256.go b/sdks/go/types/uint256.go new file mode 100644 index 00000000000..42c1f6ead51 --- /dev/null +++ b/sdks/go/types/uint256.go @@ -0,0 +1,68 @@ +package types + +import ( + "encoding/binary" + "math/big" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// Uint256 represents an unsigned 256-bit integer stored as 32 bytes little-endian. +type Uint256 interface { + bsatn.Serializable + Bytes() [32]byte + IsZero() bool + String() string +} + +// NewUint256 creates a Uint256 from a 32-byte little-endian array. +func NewUint256(b [32]byte) Uint256 { + return &uint256{data: b} +} + +// NewUint256FromU64s creates a Uint256 from four uint64 values stored little-endian: +// a=bytes[0:8], b=bytes[8:16], c=bytes[16:24], d=bytes[24:32]. +func NewUint256FromU64s(a, b, c, d uint64) Uint256 { + var buf [32]byte + binary.LittleEndian.PutUint64(buf[0:8], a) + binary.LittleEndian.PutUint64(buf[8:16], b) + binary.LittleEndian.PutUint64(buf[16:24], c) + binary.LittleEndian.PutUint64(buf[24:32], d) + return &uint256{data: buf} +} + +// ReadUint256 reads a Uint256 from a BSATN reader (32 bytes little-endian). +func ReadUint256(r bsatn.Reader) (Uint256, error) { + b, err := r.GetBytes(32) + if err != nil { + return nil, err + } + var data [32]byte + copy(data[:], b) + return &uint256{data: data}, nil +} + +type uint256 struct { + data [32]byte +} + +func (u *uint256) WriteBsatn(w bsatn.Writer) { + w.PutBytes(u.data[:]) +} + +func (u *uint256) Bytes() [32]byte { return u.data } + +func (u *uint256) IsZero() bool { + return u.data == [32]byte{} +} + +func (u *uint256) String() string { + // Convert LE bytes to big-endian for math/big, then format as decimal. + var be [32]byte + for i := 0; i < 32; i++ { + be[i] = u.data[31-i] + } + var n big.Int + n.SetBytes(be[:]) + return n.String() +} diff --git a/sdks/go/types/uuid.go b/sdks/go/types/uuid.go new file mode 100644 index 00000000000..7d4bb63a636 --- /dev/null +++ b/sdks/go/types/uuid.go @@ -0,0 +1,69 @@ +package types + +import ( + "encoding/binary" + "fmt" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" +) + +// Uuid represents a universally unique identifier. +// It is stored as a product type { __uuid__: u128 } matching the Rust SATS representation. +type Uuid interface { + bsatn.Serializable + Bytes() [16]byte + IsZero() bool + String() string +} + +// NewUuid creates a Uuid from a 16-byte array (big-endian UUID format). +func NewUuid(b [16]byte) Uuid { + return &uuidImpl{data: b} +} + +// NewUuidFromU128 creates a Uuid from a Uint128 value. +func NewUuidFromU128(u Uint128) Uuid { + return &uuidImpl{data: u.Bytes()} +} + +// ReadUuid reads a Uuid from a BSATN reader. +// The wire format is a u128 (16 bytes little-endian). +func ReadUuid(r bsatn.Reader) (Uuid, error) { + b, err := r.GetBytes(16) + if err != nil { + return nil, err + } + var data [16]byte + copy(data[:], b) + return &uuidImpl{data: data}, nil +} + +type uuidImpl struct { + data [16]byte +} + +func (u *uuidImpl) WriteBsatn(w bsatn.Writer) { + w.PutBytes(u.data[:]) +} + +func (u *uuidImpl) Bytes() [16]byte { return u.data } + +func (u *uuidImpl) IsZero() bool { + return u.data == [16]byte{} +} + +func (u *uuidImpl) String() string { + // UUID is stored as U128 in LE byte order from BSATN. + // Reverse to get RFC 4122 (big-endian) byte order for display. + var be [16]byte + for i := 0; i < 16; i++ { + be[i] = u.data[15-i] + } + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", + binary.BigEndian.Uint32(be[0:4]), + binary.BigEndian.Uint16(be[4:6]), + binary.BigEndian.Uint16(be[6:8]), + binary.BigEndian.Uint16(be[8:10]), + be[10:16], + ) +} diff --git a/sdks/go/types/uuid_v7.go b/sdks/go/types/uuid_v7.go new file mode 100644 index 00000000000..9febd15714f --- /dev/null +++ b/sdks/go/types/uuid_v7.go @@ -0,0 +1,69 @@ +package types + +import "fmt" + +// NewUuidV7 generates a UUID v7 using a monotonic counter, a timestamp in microseconds +// since the Unix epoch, and 4 random bytes. +// +// The counter wraps at 0x7FFFFFFF (31-bit max). The counter is incremented after each call. +// +// The UUID v7 layout (big-endian byte order): +// +// bytes[0..5]: unix_ts_ms (48 bits) +// bytes[6]: version (0x7x) | counter bits +// bytes[7]: counter_high +// bytes[8]: variant (0x8x) | counter bits +// bytes[9..11]: counter_low +// bytes[12..15]: random +// +// The result is stored in BSATN u128 little-endian format (reversed from big-endian UUID). +func NewUuidV7(counter *uint32, timestampMicros int64, randomBytes [4]byte) (Uuid, error) { + if timestampMicros < 0 { + return nil, fmt.Errorf("timestamp before unix epoch") + } + + // Get counter value and increment (wrapping at 31-bit max). + counterVal := *counter + *counter = (counterVal + 1) & 0x7FFFFFFF + + // Convert timestamp from microseconds to milliseconds, masked to 48 bits. + tsMs := (timestampMicros / 1000) & 0xFFFFFFFFFFFF + + // Build UUID bytes in RFC 4122 big-endian order. + var bytes [16]byte + + // unix_ts_ms (48 bits, big-endian) + bytes[0] = byte(tsMs >> 40) + bytes[1] = byte(tsMs >> 32) + bytes[2] = byte(tsMs >> 24) + bytes[3] = byte(tsMs >> 16) + bytes[4] = byte(tsMs >> 8) + bytes[5] = byte(tsMs) + + // Counter bits (matching Rust layout exactly) + bytes[7] = byte((counterVal >> 23) & 0xFF) + bytes[9] = byte((counterVal >> 15) & 0xFF) + bytes[10] = byte((counterVal >> 7) & 0xFF) + bytes[11] = byte((counterVal & 0x7F) << 1) + + // Random bytes + bytes[12] |= randomBytes[0] & 0x7F + bytes[13] = randomBytes[1] + bytes[14] = randomBytes[2] + bytes[15] = randomBytes[3] + + // Apply version 7: high nibble of byte 6 = 0x70 + bytes[6] = (bytes[6] & 0x0F) | 0x70 + + // Apply RFC 4122 variant: top 2 bits of byte 8 = 10 + bytes[8] = (bytes[8] & 0x3F) | 0x80 + + // Convert big-endian UUID bytes to little-endian u128 for BSATN storage. + // Reverse the 16 bytes. + var le [16]byte + for i := 0; i < 16; i++ { + le[i] = bytes[15-i] + } + + return &uuidImpl{data: le}, nil +} diff --git a/sdks/rust/tests/connect_disconnect_client/src/module_bindings/mod.rs b/sdks/rust/tests/connect_disconnect_client/src/module_bindings/mod.rs index fcd85e7f5d0..2d79c5ff39c 100644 --- a/sdks/rust/tests/connect_disconnect_client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/connect_disconnect_client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.0.0 (commit 85095cfa85e3addc29ce58bfe670b6003271b288). +// This was generated using spacetimedb cli version 2.0.3 (commit abbcec4ab357b956f6a2d498a666c1516edcb355). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/sdks/rust/tests/event-table-client/src/module_bindings/mod.rs b/sdks/rust/tests/event-table-client/src/module_bindings/mod.rs index 76f5f7db170..f0360c12211 100644 --- a/sdks/rust/tests/event-table-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/event-table-client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.0.0 (commit 85095cfa85e3addc29ce58bfe670b6003271b288). +// This was generated using spacetimedb cli version 2.0.3 (commit abbcec4ab357b956f6a2d498a666c1516edcb355). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; @@ -26,8 +26,8 @@ pub use test_event_type::TestEvent; /// to indicate which reducer caused the event. pub enum Reducer { - EmitMultipleTestEvents, EmitTestEvent { name: String, value: u64 }, + EmitMultipleTestEvents, Noop, } @@ -38,8 +38,8 @@ impl __sdk::InModule for Reducer { impl __sdk::Reducer for Reducer { fn reducer_name(&self) -> &'static str { match self { - Reducer::EmitMultipleTestEvents => "emit_multiple_test_events", Reducer::EmitTestEvent { .. } => "emit_test_event", + Reducer::EmitMultipleTestEvents => "emit_multiple_test_events", Reducer::Noop => "noop", _ => unreachable!(), } @@ -47,15 +47,15 @@ impl __sdk::Reducer for Reducer { #[allow(clippy::clone_on_copy)] fn args_bsatn(&self) -> Result, __sats::bsatn::EncodeError> { match self { - Reducer::EmitMultipleTestEvents => { - __sats::bsatn::to_vec(&emit_multiple_test_events_reducer::EmitMultipleTestEventsArgs {}) - } Reducer::EmitTestEvent { name, value } => { __sats::bsatn::to_vec(&emit_test_event_reducer::EmitTestEventArgs { name: name.clone(), value: value.clone(), }) } + Reducer::EmitMultipleTestEvents => { + __sats::bsatn::to_vec(&emit_multiple_test_events_reducer::EmitMultipleTestEventsArgs {}) + } Reducer::Noop => __sats::bsatn::to_vec(&noop_reducer::NoopArgs {}), _ => unreachable!(), } diff --git a/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs b/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs index e4e34268510..a5f3e8d3c12 100644 --- a/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.0.0 (commit 85095cfa85e3addc29ce58bfe670b6003271b288). +// This was generated using spacetimedb cli version 2.0.3 (commit abbcec4ab357b956f6a2d498a666c1516edcb355). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/sdks/rust/tests/test-client/src/module_bindings/insert_result_every_primitive_struct_string_reducer.rs b/sdks/rust/tests/test-client/src/module_bindings/insert_result_every_primitive_struct_string_reducer.rs index 1d695df665f..ea923014d94 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/insert_result_every_primitive_struct_string_reducer.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/insert_result_every_primitive_struct_string_reducer.rs @@ -4,12 +4,12 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; -use super::every_primitive_struct_type::EveryPrimitiveStruct; +use super::result_every_primitive_struct_string_value_type::ResultEveryPrimitiveStructStringValue; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub(super) struct InsertResultEveryPrimitiveStructStringArgs { - pub r: Result, + pub r: ResultEveryPrimitiveStructStringValue, } impl From for super::Reducer { @@ -35,7 +35,7 @@ pub trait insert_result_every_primitive_struct_string { /// /// Use [`insert_result_every_primitive_struct_string:insert_result_every_primitive_struct_string_then`] to run a callback after the reducer completes. fn insert_result_every_primitive_struct_string( &self, - r: Result, + r: ResultEveryPrimitiveStructStringValue, ) -> __sdk::Result<()> { self.insert_result_every_primitive_struct_string_then(r, |_, _| {}) } @@ -48,7 +48,7 @@ pub trait insert_result_every_primitive_struct_string { /// and its status can be observed with the `callback`. fn insert_result_every_primitive_struct_string_then( &self, - r: Result, + r: ResultEveryPrimitiveStructStringValue, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send @@ -59,7 +59,7 @@ pub trait insert_result_every_primitive_struct_string { impl insert_result_every_primitive_struct_string for super::RemoteReducers { fn insert_result_every_primitive_struct_string_then( &self, - r: Result, + r: ResultEveryPrimitiveStructStringValue, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send diff --git a/sdks/rust/tests/test-client/src/module_bindings/insert_result_i_32_string_reducer.rs b/sdks/rust/tests/test-client/src/module_bindings/insert_result_i_32_string_reducer.rs index 50f44f3ed20..af1d1a2efde 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/insert_result_i_32_string_reducer.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/insert_result_i_32_string_reducer.rs @@ -4,10 +4,12 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::result_i_32_string_value_type::ResultI32StringValue; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub(super) struct InsertResultI32StringArgs { - pub r: Result, + pub r: ResultI32StringValue, } impl From for super::Reducer { @@ -31,7 +33,7 @@ pub trait insert_result_i_32_string { /// The reducer will run asynchronously in the future, /// and this method provides no way to listen for its completion status. /// /// Use [`insert_result_i_32_string:insert_result_i_32_string_then`] to run a callback after the reducer completes. - fn insert_result_i_32_string(&self, r: Result) -> __sdk::Result<()> { + fn insert_result_i_32_string(&self, r: ResultI32StringValue) -> __sdk::Result<()> { self.insert_result_i_32_string_then(r, |_, _| {}) } @@ -43,7 +45,7 @@ pub trait insert_result_i_32_string { /// and its status can be observed with the `callback`. fn insert_result_i_32_string_then( &self, - r: Result, + r: ResultI32StringValue, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send @@ -54,7 +56,7 @@ pub trait insert_result_i_32_string { impl insert_result_i_32_string for super::RemoteReducers { fn insert_result_i_32_string_then( &self, - r: Result, + r: ResultI32StringValue, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send diff --git a/sdks/rust/tests/test-client/src/module_bindings/insert_result_identity_string_reducer.rs b/sdks/rust/tests/test-client/src/module_bindings/insert_result_identity_string_reducer.rs index 372cc4295ba..c21c739d096 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/insert_result_identity_string_reducer.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/insert_result_identity_string_reducer.rs @@ -4,10 +4,12 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::result_identity_string_value_type::ResultIdentityStringValue; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub(super) struct InsertResultIdentityStringArgs { - pub r: Result<__sdk::Identity, String>, + pub r: ResultIdentityStringValue, } impl From for super::Reducer { @@ -31,7 +33,7 @@ pub trait insert_result_identity_string { /// The reducer will run asynchronously in the future, /// and this method provides no way to listen for its completion status. /// /// Use [`insert_result_identity_string:insert_result_identity_string_then`] to run a callback after the reducer completes. - fn insert_result_identity_string(&self, r: Result<__sdk::Identity, String>) -> __sdk::Result<()> { + fn insert_result_identity_string(&self, r: ResultIdentityStringValue) -> __sdk::Result<()> { self.insert_result_identity_string_then(r, |_, _| {}) } @@ -43,7 +45,7 @@ pub trait insert_result_identity_string { /// and its status can be observed with the `callback`. fn insert_result_identity_string_then( &self, - r: Result<__sdk::Identity, String>, + r: ResultIdentityStringValue, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send @@ -54,7 +56,7 @@ pub trait insert_result_identity_string { impl insert_result_identity_string for super::RemoteReducers { fn insert_result_identity_string_then( &self, - r: Result<__sdk::Identity, String>, + r: ResultIdentityStringValue, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send diff --git a/sdks/rust/tests/test-client/src/module_bindings/insert_result_simple_enum_i_32_reducer.rs b/sdks/rust/tests/test-client/src/module_bindings/insert_result_simple_enum_i_32_reducer.rs index 059db78c5b7..cf3921caead 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/insert_result_simple_enum_i_32_reducer.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/insert_result_simple_enum_i_32_reducer.rs @@ -4,12 +4,12 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; -use super::simple_enum_type::SimpleEnum; +use super::result_simple_enum_i_32_value_type::ResultSimpleEnumI32Value; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub(super) struct InsertResultSimpleEnumI32Args { - pub r: Result, + pub r: ResultSimpleEnumI32Value, } impl From for super::Reducer { @@ -33,7 +33,7 @@ pub trait insert_result_simple_enum_i_32 { /// The reducer will run asynchronously in the future, /// and this method provides no way to listen for its completion status. /// /// Use [`insert_result_simple_enum_i_32:insert_result_simple_enum_i_32_then`] to run a callback after the reducer completes. - fn insert_result_simple_enum_i_32(&self, r: Result) -> __sdk::Result<()> { + fn insert_result_simple_enum_i_32(&self, r: ResultSimpleEnumI32Value) -> __sdk::Result<()> { self.insert_result_simple_enum_i_32_then(r, |_, _| {}) } @@ -45,7 +45,7 @@ pub trait insert_result_simple_enum_i_32 { /// and its status can be observed with the `callback`. fn insert_result_simple_enum_i_32_then( &self, - r: Result, + r: ResultSimpleEnumI32Value, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send @@ -56,7 +56,7 @@ pub trait insert_result_simple_enum_i_32 { impl insert_result_simple_enum_i_32 for super::RemoteReducers { fn insert_result_simple_enum_i_32_then( &self, - r: Result, + r: ResultSimpleEnumI32Value, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send diff --git a/sdks/rust/tests/test-client/src/module_bindings/insert_result_string_i_32_reducer.rs b/sdks/rust/tests/test-client/src/module_bindings/insert_result_string_i_32_reducer.rs index c0e548b6698..6cbdc7e36d8 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/insert_result_string_i_32_reducer.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/insert_result_string_i_32_reducer.rs @@ -4,10 +4,12 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::result_string_i_32_value_type::ResultStringI32Value; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub(super) struct InsertResultStringI32Args { - pub r: Result, + pub r: ResultStringI32Value, } impl From for super::Reducer { @@ -31,7 +33,7 @@ pub trait insert_result_string_i_32 { /// The reducer will run asynchronously in the future, /// and this method provides no way to listen for its completion status. /// /// Use [`insert_result_string_i_32:insert_result_string_i_32_then`] to run a callback after the reducer completes. - fn insert_result_string_i_32(&self, r: Result) -> __sdk::Result<()> { + fn insert_result_string_i_32(&self, r: ResultStringI32Value) -> __sdk::Result<()> { self.insert_result_string_i_32_then(r, |_, _| {}) } @@ -43,7 +45,7 @@ pub trait insert_result_string_i_32 { /// and its status can be observed with the `callback`. fn insert_result_string_i_32_then( &self, - r: Result, + r: ResultStringI32Value, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send @@ -54,7 +56,7 @@ pub trait insert_result_string_i_32 { impl insert_result_string_i_32 for super::RemoteReducers { fn insert_result_string_i_32_then( &self, - r: Result, + r: ResultStringI32Value, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send diff --git a/sdks/rust/tests/test-client/src/module_bindings/insert_result_vec_i_32_string_reducer.rs b/sdks/rust/tests/test-client/src/module_bindings/insert_result_vec_i_32_string_reducer.rs index dab246f8f14..d3d6bf7a8da 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/insert_result_vec_i_32_string_reducer.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/insert_result_vec_i_32_string_reducer.rs @@ -4,10 +4,12 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::result_vec_i_32_string_value_type::ResultVecI32StringValue; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub(super) struct InsertResultVecI32StringArgs { - pub r: Result, String>, + pub r: ResultVecI32StringValue, } impl From for super::Reducer { @@ -31,7 +33,7 @@ pub trait insert_result_vec_i_32_string { /// The reducer will run asynchronously in the future, /// and this method provides no way to listen for its completion status. /// /// Use [`insert_result_vec_i_32_string:insert_result_vec_i_32_string_then`] to run a callback after the reducer completes. - fn insert_result_vec_i_32_string(&self, r: Result, String>) -> __sdk::Result<()> { + fn insert_result_vec_i_32_string(&self, r: ResultVecI32StringValue) -> __sdk::Result<()> { self.insert_result_vec_i_32_string_then(r, |_, _| {}) } @@ -43,7 +45,7 @@ pub trait insert_result_vec_i_32_string { /// and its status can be observed with the `callback`. fn insert_result_vec_i_32_string_then( &self, - r: Result, String>, + r: ResultVecI32StringValue, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send @@ -54,7 +56,7 @@ pub trait insert_result_vec_i_32_string { impl insert_result_vec_i_32_string for super::RemoteReducers { fn insert_result_vec_i_32_string_then( &self, - r: Result, String>, + r: ResultVecI32StringValue, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send diff --git a/sdks/rust/tests/test-client/src/module_bindings/mod.rs b/sdks/rust/tests/test-client/src/module_bindings/mod.rs index de9afc6155f..1517f47c75c 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.0.0 (commit e528393902d8cc982769e3b1a0f250d7d53edfa1). +// This was generated using spacetimedb cli version 2.0.3 (commit abbcec4ab357b956f6a2d498a666c1516edcb355). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; @@ -285,16 +285,22 @@ pub mod pk_uuid_table; pub mod pk_uuid_type; pub mod result_every_primitive_struct_string_table; pub mod result_every_primitive_struct_string_type; +pub mod result_every_primitive_struct_string_value_type; pub mod result_i_32_string_table; pub mod result_i_32_string_type; +pub mod result_i_32_string_value_type; pub mod result_identity_string_table; pub mod result_identity_string_type; +pub mod result_identity_string_value_type; pub mod result_simple_enum_i_32_table; pub mod result_simple_enum_i_32_type; +pub mod result_simple_enum_i_32_value_type; pub mod result_string_i_32_table; pub mod result_string_i_32_type; +pub mod result_string_i_32_value_type; pub mod result_vec_i_32_string_table; pub mod result_vec_i_32_string_type; +pub mod result_vec_i_32_string_value_type; pub mod scheduled_table_table; pub mod scheduled_table_type; pub mod send_scheduled_message_reducer; @@ -708,16 +714,22 @@ pub use pk_uuid_table::*; pub use pk_uuid_type::PkUuid; pub use result_every_primitive_struct_string_table::*; pub use result_every_primitive_struct_string_type::ResultEveryPrimitiveStructString; +pub use result_every_primitive_struct_string_value_type::ResultEveryPrimitiveStructStringValue; pub use result_i_32_string_table::*; pub use result_i_32_string_type::ResultI32String; +pub use result_i_32_string_value_type::ResultI32StringValue; pub use result_identity_string_table::*; pub use result_identity_string_type::ResultIdentityString; +pub use result_identity_string_value_type::ResultIdentityStringValue; pub use result_simple_enum_i_32_table::*; pub use result_simple_enum_i_32_type::ResultSimpleEnumI32; +pub use result_simple_enum_i_32_value_type::ResultSimpleEnumI32Value; pub use result_string_i_32_table::*; pub use result_string_i_32_type::ResultStringI32; +pub use result_string_i_32_value_type::ResultStringI32Value; pub use result_vec_i_32_string_table::*; pub use result_vec_i_32_string_type::ResultVecI32String; +pub use result_vec_i_32_string_value_type::ResultVecI32StringValue; pub use scheduled_table_table::*; pub use scheduled_table_type::ScheduledTable; pub use send_scheduled_message_reducer::send_scheduled_message; @@ -860,703 +872,703 @@ pub use vec_uuid_type::VecUuid; /// to indicate which reducer caused the event. pub enum Reducer { - DeleteFromBtreeU32 { - rows: Vec, + InsertOneU8 { + n: u8, }, - DeleteLargeTable { - a: u8, - b: u16, - c: u32, - d: u64, - e: u128, - f: __sats::u256, - g: i8, - h: i16, - i: i32, - j: i64, - k: i128, - l: __sats::i256, - m: bool, - n: f32, - o: f64, - p: String, - q: SimpleEnum, - r: EnumWithPayload, - s: UnitStruct, - t: ByteStruct, - u: EveryPrimitiveStruct, - v: EveryVecStruct, + InsertOneU16 { + n: u16, }, - DeletePkBool { - b: bool, + InsertOneU32 { + n: u32, }, - DeletePkConnectionId { - a: __sdk::ConnectionId, + InsertOneU64 { + n: u64, }, - DeletePkI128 { - n: i128, + InsertOneU128 { + n: u128, }, - DeletePkI16 { - n: i16, + InsertOneU256 { + n: __sats::u256, }, - DeletePkI256 { - n: __sats::i256, + InsertOneI8 { + n: i8, }, - DeletePkI32 { + InsertOneI16 { + n: i16, + }, + InsertOneI32 { n: i32, }, - DeletePkI64 { + InsertOneI64 { n: i64, }, - DeletePkI8 { - n: i8, + InsertOneI128 { + n: i128, }, - DeletePkIdentity { - i: __sdk::Identity, + InsertOneI256 { + n: __sats::i256, }, - DeletePkString { - s: String, + InsertOneBool { + b: bool, }, - DeletePkU128 { - n: u128, + InsertOneF32 { + f: f32, }, - DeletePkU16 { - n: u16, + InsertOneF64 { + f: f64, }, - DeletePkU256 { - n: __sats::u256, + InsertOneString { + s: String, }, - DeletePkU32 { - n: u32, + InsertOneIdentity { + i: __sdk::Identity, }, - DeletePkU32InsertPkU32Two { - n: u32, - data: i32, + InsertOneConnectionId { + a: __sdk::ConnectionId, }, - DeletePkU32Two { - n: u32, + InsertOneUuid { + u: __sdk::Uuid, }, - DeletePkU64 { - n: u64, + InsertOneTimestamp { + t: __sdk::Timestamp, }, - DeletePkU8 { - n: u8, + InsertOneSimpleEnum { + e: SimpleEnum, }, - DeletePkUuid { - u: __sdk::Uuid, + InsertOneEnumWithPayload { + e: EnumWithPayload, }, - DeleteUniqueBool { - b: bool, + InsertOneUnitStruct { + s: UnitStruct, }, - DeleteUniqueConnectionId { - a: __sdk::ConnectionId, + InsertOneByteStruct { + s: ByteStruct, }, - DeleteUniqueI128 { - n: i128, + InsertOneEveryPrimitiveStruct { + s: EveryPrimitiveStruct, }, - DeleteUniqueI16 { - n: i16, + InsertOneEveryVecStruct { + s: EveryVecStruct, }, - DeleteUniqueI256 { - n: __sats::i256, + InsertVecU8 { + n: Vec, }, - DeleteUniqueI32 { - n: i32, + InsertVecU16 { + n: Vec, }, - DeleteUniqueI64 { - n: i64, + InsertVecU32 { + n: Vec, }, - DeleteUniqueI8 { - n: i8, + InsertVecU64 { + n: Vec, }, - DeleteUniqueIdentity { - i: __sdk::Identity, + InsertVecU128 { + n: Vec, }, - DeleteUniqueString { - s: String, + InsertVecU256 { + n: Vec<__sats::u256>, }, - DeleteUniqueU128 { - n: u128, + InsertVecI8 { + n: Vec, }, - DeleteUniqueU16 { - n: u16, + InsertVecI16 { + n: Vec, }, - DeleteUniqueU256 { - n: __sats::u256, + InsertVecI32 { + n: Vec, }, - DeleteUniqueU32 { - n: u32, + InsertVecI64 { + n: Vec, }, - DeleteUniqueU64 { - n: u64, + InsertVecI128 { + n: Vec, }, - DeleteUniqueU8 { - n: u8, + InsertVecI256 { + n: Vec<__sats::i256>, }, - DeleteUniqueUuid { - u: __sdk::Uuid, + InsertVecBool { + b: Vec, }, - InsertCallTimestamp, - InsertCallUuidV4, - InsertCallUuidV7, - InsertCallerOneConnectionId, - InsertCallerOneIdentity, - InsertCallerPkConnectionId { - data: i32, + InsertVecF32 { + f: Vec, }, - InsertCallerPkIdentity { - data: i32, + InsertVecF64 { + f: Vec, }, - InsertCallerUniqueConnectionId { - data: i32, + InsertVecString { + s: Vec, }, - InsertCallerUniqueIdentity { - data: i32, + InsertVecIdentity { + i: Vec<__sdk::Identity>, }, - InsertCallerVecConnectionId, - InsertCallerVecIdentity, - InsertIntoBtreeU32 { - rows: Vec, + InsertVecConnectionId { + a: Vec<__sdk::ConnectionId>, }, - InsertIntoIndexedSimpleEnum { - n: SimpleEnum, + InsertVecUuid { + u: Vec<__sdk::Uuid>, }, - InsertIntoPkBtreeU32 { - pk_u_32: Vec, - bt_u_32: Vec, + InsertVecTimestamp { + t: Vec<__sdk::Timestamp>, }, - InsertLargeTable { - a: u8, - b: u16, - c: u32, - d: u64, - e: u128, - f: __sats::u256, - g: i8, - h: i16, - i: i32, - j: i64, - k: i128, - l: __sats::i256, - m: bool, - n: f32, - o: f64, - p: String, - q: SimpleEnum, - r: EnumWithPayload, - s: UnitStruct, - t: ByteStruct, - u: EveryPrimitiveStruct, - v: EveryVecStruct, + InsertVecSimpleEnum { + e: Vec, }, - InsertOneBool { - b: bool, + InsertVecEnumWithPayload { + e: Vec, }, - InsertOneByteStruct { - s: ByteStruct, + InsertVecUnitStruct { + s: Vec, }, - InsertOneConnectionId { - a: __sdk::ConnectionId, + InsertVecByteStruct { + s: Vec, }, - InsertOneEnumWithPayload { - e: EnumWithPayload, + InsertVecEveryPrimitiveStruct { + s: Vec, }, - InsertOneEveryPrimitiveStruct { - s: EveryPrimitiveStruct, + InsertVecEveryVecStruct { + s: Vec, }, - InsertOneEveryVecStruct { - s: EveryVecStruct, - }, - InsertOneF32 { - f: f32, - }, - InsertOneF64 { - f: f64, - }, - InsertOneI128 { - n: i128, - }, - InsertOneI16 { - n: i16, - }, - InsertOneI256 { - n: __sats::i256, + InsertOptionI32 { + n: Option, }, - InsertOneI32 { - n: i32, + InsertOptionString { + s: Option, }, - InsertOneI64 { - n: i64, + InsertOptionIdentity { + i: Option<__sdk::Identity>, }, - InsertOneI8 { - n: i8, + InsertOptionUuid { + u: Option<__sdk::Uuid>, }, - InsertOneIdentity { - i: __sdk::Identity, + InsertOptionSimpleEnum { + e: Option, }, - InsertOneSimpleEnum { - e: SimpleEnum, + InsertOptionEveryPrimitiveStruct { + s: Option, }, - InsertOneString { - s: String, + InsertOptionVecOptionI32 { + v: Option>>, }, - InsertOneTimestamp { - t: __sdk::Timestamp, + InsertResultI32String { + r: ResultI32StringValue, }, - InsertOneU128 { - n: u128, + InsertResultStringI32 { + r: ResultStringI32Value, }, - InsertOneU16 { - n: u16, + InsertResultIdentityString { + r: ResultIdentityStringValue, }, - InsertOneU256 { - n: __sats::u256, + InsertResultSimpleEnumI32 { + r: ResultSimpleEnumI32Value, }, - InsertOneU32 { - n: u32, + InsertResultEveryPrimitiveStructString { + r: ResultEveryPrimitiveStructStringValue, }, - InsertOneU64 { - n: u64, + InsertResultVecI32String { + r: ResultVecI32StringValue, }, - InsertOneU8 { + InsertUniqueU8 { n: u8, + data: i32, }, - InsertOneUnitStruct { - s: UnitStruct, - }, - InsertOneUuid { - u: __sdk::Uuid, - }, - InsertOptionEveryPrimitiveStruct { - s: Option, - }, - InsertOptionI32 { - n: Option, - }, - InsertOptionIdentity { - i: Option<__sdk::Identity>, - }, - InsertOptionSimpleEnum { - e: Option, - }, - InsertOptionString { - s: Option, - }, - InsertOptionUuid { - u: Option<__sdk::Uuid>, + UpdateUniqueU8 { + n: u8, + data: i32, }, - InsertOptionVecOptionI32 { - v: Option>>, + DeleteUniqueU8 { + n: u8, }, - InsertPkBool { - b: bool, + InsertUniqueU16 { + n: u16, data: i32, }, - InsertPkConnectionId { - a: __sdk::ConnectionId, + UpdateUniqueU16 { + n: u16, data: i32, }, - InsertPkI128 { - n: i128, - data: i32, + DeleteUniqueU16 { + n: u16, }, - InsertPkI16 { - n: i16, + InsertUniqueU32 { + n: u32, data: i32, }, - InsertPkI256 { - n: __sats::i256, + UpdateUniqueU32 { + n: u32, data: i32, }, - InsertPkI32 { - n: i32, - data: i32, + DeleteUniqueU32 { + n: u32, }, - InsertPkI64 { - n: i64, + InsertUniqueU64 { + n: u64, data: i32, }, - InsertPkI8 { - n: i8, + UpdateUniqueU64 { + n: u64, data: i32, }, - InsertPkIdentity { - i: __sdk::Identity, - data: i32, + DeleteUniqueU64 { + n: u64, }, - InsertPkSimpleEnum { - a: SimpleEnum, + InsertUniqueU128 { + n: u128, data: i32, }, - InsertPkString { - s: String, + UpdateUniqueU128 { + n: u128, data: i32, }, - InsertPkU128 { + DeleteUniqueU128 { n: u128, - data: i32, }, - InsertPkU16 { - n: u16, + InsertUniqueU256 { + n: __sats::u256, data: i32, }, - InsertPkU256 { + UpdateUniqueU256 { n: __sats::u256, data: i32, }, - InsertPkU32 { - n: u32, - data: i32, + DeleteUniqueU256 { + n: __sats::u256, }, - InsertPkU32Two { - n: u32, + InsertUniqueI8 { + n: i8, data: i32, }, - InsertPkU64 { - n: u64, + UpdateUniqueI8 { + n: i8, data: i32, }, - InsertPkU8 { - n: u8, - data: i32, + DeleteUniqueI8 { + n: i8, }, - InsertPkUuid { - u: __sdk::Uuid, + InsertUniqueI16 { + n: i16, data: i32, }, - InsertPrimitivesAsStrings { - s: EveryPrimitiveStruct, - }, - InsertResultEveryPrimitiveStructString { - r: Result, - }, - InsertResultI32String { - r: Result, - }, - InsertResultIdentityString { - r: Result<__sdk::Identity, String>, + UpdateUniqueI16 { + n: i16, + data: i32, }, - InsertResultSimpleEnumI32 { - r: Result, + DeleteUniqueI16 { + n: i16, }, - InsertResultStringI32 { - r: Result, + InsertUniqueI32 { + n: i32, + data: i32, }, - InsertResultVecI32String { - r: Result, String>, + UpdateUniqueI32 { + n: i32, + data: i32, }, - InsertTableHoldsTable { - a: OneU8, - b: VecU8, + DeleteUniqueI32 { + n: i32, }, - InsertUniqueBool { - b: bool, + InsertUniqueI64 { + n: i64, data: i32, }, - InsertUniqueConnectionId { - a: __sdk::ConnectionId, + UpdateUniqueI64 { + n: i64, data: i32, }, + DeleteUniqueI64 { + n: i64, + }, InsertUniqueI128 { n: i128, data: i32, }, - InsertUniqueI16 { - n: i16, + UpdateUniqueI128 { + n: i128, data: i32, }, + DeleteUniqueI128 { + n: i128, + }, InsertUniqueI256 { n: __sats::i256, data: i32, }, - InsertUniqueI32 { - n: i32, + UpdateUniqueI256 { + n: __sats::i256, data: i32, }, - InsertUniqueI64 { - n: i64, - data: i32, + DeleteUniqueI256 { + n: __sats::i256, }, - InsertUniqueI8 { - n: i8, + InsertUniqueBool { + b: bool, data: i32, }, - InsertUniqueIdentity { - i: __sdk::Identity, + UpdateUniqueBool { + b: bool, data: i32, }, + DeleteUniqueBool { + b: bool, + }, InsertUniqueString { s: String, data: i32, }, - InsertUniqueU128 { - n: u128, + UpdateUniqueString { + s: String, data: i32, }, - InsertUniqueU16 { - n: u16, - data: i32, + DeleteUniqueString { + s: String, }, - InsertUniqueU256 { - n: __sats::u256, + InsertUniqueIdentity { + i: __sdk::Identity, data: i32, }, - InsertUniqueU32 { - n: u32, + UpdateUniqueIdentity { + i: __sdk::Identity, data: i32, }, - InsertUniqueU32UpdatePkU32 { - n: u32, - d_unique: i32, - d_pk: i32, + DeleteUniqueIdentity { + i: __sdk::Identity, }, - InsertUniqueU64 { - n: u64, + InsertUniqueConnectionId { + a: __sdk::ConnectionId, data: i32, }, - InsertUniqueU8 { - n: u8, + UpdateUniqueConnectionId { + a: __sdk::ConnectionId, data: i32, }, + DeleteUniqueConnectionId { + a: __sdk::ConnectionId, + }, InsertUniqueUuid { u: __sdk::Uuid, data: i32, }, - InsertUser { - name: String, - identity: __sdk::Identity, + UpdateUniqueUuid { + u: __sdk::Uuid, + data: i32, }, - InsertVecBool { - b: Vec, + DeleteUniqueUuid { + u: __sdk::Uuid, }, - InsertVecByteStruct { - s: Vec, + InsertPkU8 { + n: u8, + data: i32, }, - InsertVecConnectionId { - a: Vec<__sdk::ConnectionId>, + UpdatePkU8 { + n: u8, + data: i32, }, - InsertVecEnumWithPayload { - e: Vec, + DeletePkU8 { + n: u8, }, - InsertVecEveryPrimitiveStruct { - s: Vec, + InsertPkU16 { + n: u16, + data: i32, }, - InsertVecEveryVecStruct { - s: Vec, + UpdatePkU16 { + n: u16, + data: i32, }, - InsertVecF32 { - f: Vec, + DeletePkU16 { + n: u16, }, - InsertVecF64 { - f: Vec, + InsertPkU32 { + n: u32, + data: i32, }, - InsertVecI128 { - n: Vec, + UpdatePkU32 { + n: u32, + data: i32, }, - InsertVecI16 { - n: Vec, + DeletePkU32 { + n: u32, }, - InsertVecI256 { - n: Vec<__sats::i256>, + InsertPkU64 { + n: u64, + data: i32, }, - InsertVecI32 { - n: Vec, + UpdatePkU64 { + n: u64, + data: i32, }, - InsertVecI64 { - n: Vec, + DeletePkU64 { + n: u64, }, - InsertVecI8 { - n: Vec, + InsertPkU128 { + n: u128, + data: i32, }, - InsertVecIdentity { - i: Vec<__sdk::Identity>, + UpdatePkU128 { + n: u128, + data: i32, }, - InsertVecSimpleEnum { - e: Vec, + DeletePkU128 { + n: u128, }, - InsertVecString { - s: Vec, + InsertPkU256 { + n: __sats::u256, + data: i32, }, - InsertVecTimestamp { - t: Vec<__sdk::Timestamp>, + UpdatePkU256 { + n: __sats::u256, + data: i32, }, - InsertVecU128 { - n: Vec, + DeletePkU256 { + n: __sats::u256, }, - InsertVecU16 { - n: Vec, + InsertPkI8 { + n: i8, + data: i32, }, - InsertVecU256 { - n: Vec<__sats::u256>, + UpdatePkI8 { + n: i8, + data: i32, }, - InsertVecU32 { - n: Vec, + DeletePkI8 { + n: i8, }, - InsertVecU64 { - n: Vec, + InsertPkI16 { + n: i16, + data: i32, }, - InsertVecU8 { - n: Vec, + UpdatePkI16 { + n: i16, + data: i32, }, - InsertVecUnitStruct { - s: Vec, + DeletePkI16 { + n: i16, }, - InsertVecUuid { - u: Vec<__sdk::Uuid>, + InsertPkI32 { + n: i32, + data: i32, }, - NoOpSucceeds, - SendScheduledMessage { - arg: ScheduledTable, + UpdatePkI32 { + n: i32, + data: i32, }, - SortedUuidsInsert, - UpdateIndexedSimpleEnum { - a: SimpleEnum, - b: SimpleEnum, + DeletePkI32 { + n: i32, }, - UpdatePkBool { - b: bool, + InsertPkI64 { + n: i64, data: i32, }, - UpdatePkConnectionId { - a: __sdk::ConnectionId, + UpdatePkI64 { + n: i64, + data: i32, + }, + DeletePkI64 { + n: i64, + }, + InsertPkI128 { + n: i128, data: i32, }, UpdatePkI128 { n: i128, data: i32, }, - UpdatePkI16 { - n: i16, + DeletePkI128 { + n: i128, + }, + InsertPkI256 { + n: __sats::i256, data: i32, }, UpdatePkI256 { n: __sats::i256, data: i32, }, - UpdatePkI32 { - n: i32, - data: i32, + DeletePkI256 { + n: __sats::i256, }, - UpdatePkI64 { - n: i64, + InsertPkBool { + b: bool, data: i32, }, - UpdatePkI8 { - n: i8, + UpdatePkBool { + b: bool, data: i32, }, - UpdatePkIdentity { - i: __sdk::Identity, - data: i32, + DeletePkBool { + b: bool, }, - UpdatePkSimpleEnum { - a: SimpleEnum, + InsertPkString { + s: String, data: i32, }, UpdatePkString { s: String, data: i32, }, - UpdatePkU128 { - n: u128, - data: i32, + DeletePkString { + s: String, }, - UpdatePkU16 { - n: u16, + InsertPkIdentity { + i: __sdk::Identity, data: i32, }, - UpdatePkU256 { - n: __sats::u256, + UpdatePkIdentity { + i: __sdk::Identity, data: i32, }, - UpdatePkU32 { - n: u32, - data: i32, + DeletePkIdentity { + i: __sdk::Identity, }, - UpdatePkU32Two { - n: u32, + InsertPkConnectionId { + a: __sdk::ConnectionId, data: i32, }, - UpdatePkU64 { - n: u64, + UpdatePkConnectionId { + a: __sdk::ConnectionId, data: i32, }, - UpdatePkU8 { - n: u8, + DeletePkConnectionId { + a: __sdk::ConnectionId, + }, + InsertPkUuid { + u: __sdk::Uuid, data: i32, }, UpdatePkUuid { u: __sdk::Uuid, data: i32, }, - UpdateUniqueBool { - b: bool, - data: i32, + DeletePkUuid { + u: __sdk::Uuid, }, - UpdateUniqueConnectionId { - a: __sdk::ConnectionId, + InsertPkSimpleEnum { + a: SimpleEnum, data: i32, }, - UpdateUniqueI128 { - n: i128, + InsertPkU32Two { + n: u32, data: i32, }, - UpdateUniqueI16 { - n: i16, + UpdatePkU32Two { + n: u32, data: i32, }, - UpdateUniqueI256 { - n: __sats::i256, - data: i32, + DeletePkU32Two { + n: u32, }, - UpdateUniqueI32 { - n: i32, + UpdatePkSimpleEnum { + a: SimpleEnum, data: i32, }, - UpdateUniqueI64 { - n: i64, - data: i32, + InsertLargeTable { + a: u8, + b: u16, + c: u32, + d: u64, + e: u128, + f: __sats::u256, + g: i8, + h: i16, + i: i32, + j: i64, + k: i128, + l: __sats::i256, + m: bool, + n: f32, + o: f64, + p: String, + q: SimpleEnum, + r: EnumWithPayload, + s: UnitStruct, + t: ByteStruct, + u: EveryPrimitiveStruct, + v: EveryVecStruct, }, - UpdateUniqueI8 { - n: i8, - data: i32, + DeleteLargeTable { + a: u8, + b: u16, + c: u32, + d: u64, + e: u128, + f: __sats::u256, + g: i8, + h: i16, + i: i32, + j: i64, + k: i128, + l: __sats::i256, + m: bool, + n: f32, + o: f64, + p: String, + q: SimpleEnum, + r: EnumWithPayload, + s: UnitStruct, + t: ByteStruct, + u: EveryPrimitiveStruct, + v: EveryVecStruct, }, - UpdateUniqueIdentity { - i: __sdk::Identity, - data: i32, + InsertTableHoldsTable { + a: OneU8, + b: VecU8, }, - UpdateUniqueString { - s: String, - data: i32, + InsertIntoBtreeU32 { + rows: Vec, }, - UpdateUniqueU128 { - n: u128, - data: i32, + DeleteFromBtreeU32 { + rows: Vec, }, - UpdateUniqueU16 { - n: u16, - data: i32, + InsertIntoPkBtreeU32 { + pk_u_32: Vec, + bt_u_32: Vec, }, - UpdateUniqueU256 { - n: __sats::u256, - data: i32, + InsertUniqueU32UpdatePkU32 { + n: u32, + d_unique: i32, + d_pk: i32, }, - UpdateUniqueU32 { + DeletePkU32InsertPkU32Two { n: u32, data: i32, }, - UpdateUniqueU64 { - n: u64, + InsertCallerOneIdentity, + InsertCallerVecIdentity, + InsertCallerUniqueIdentity { data: i32, }, - UpdateUniqueU8 { - n: u8, + InsertCallerPkIdentity { data: i32, }, - UpdateUniqueUuid { - u: __sdk::Uuid, + InsertCallerOneConnectionId, + InsertCallerVecConnectionId, + InsertCallerUniqueConnectionId { + data: i32, + }, + InsertCallerPkConnectionId { data: i32, }, + InsertCallTimestamp, + InsertCallUuidV4, + InsertCallUuidV7, + InsertPrimitivesAsStrings { + s: EveryPrimitiveStruct, + }, + NoOpSucceeds, + SendScheduledMessage { + arg: ScheduledTable, + }, + InsertUser { + name: String, + identity: __sdk::Identity, + }, + InsertIntoIndexedSimpleEnum { + n: SimpleEnum, + }, + UpdateIndexedSimpleEnum { + a: SimpleEnum, + b: SimpleEnum, + }, + SortedUuidsInsert, } impl __sdk::InModule for Reducer { @@ -1566,665 +1578,403 @@ impl __sdk::InModule for Reducer { impl __sdk::Reducer for Reducer { fn reducer_name(&self) -> &'static str { match self { - Reducer::DeleteFromBtreeU32 { .. } => "delete_from_btree_u_32", - Reducer::DeleteLargeTable { .. } => "delete_large_table", - Reducer::DeletePkBool { .. } => "delete_pk_bool", - Reducer::DeletePkConnectionId { .. } => "delete_pk_connection_id", - Reducer::DeletePkI128 { .. } => "delete_pk_i_128", - Reducer::DeletePkI16 { .. } => "delete_pk_i_16", - Reducer::DeletePkI256 { .. } => "delete_pk_i_256", - Reducer::DeletePkI32 { .. } => "delete_pk_i_32", - Reducer::DeletePkI64 { .. } => "delete_pk_i_64", - Reducer::DeletePkI8 { .. } => "delete_pk_i_8", - Reducer::DeletePkIdentity { .. } => "delete_pk_identity", - Reducer::DeletePkString { .. } => "delete_pk_string", - Reducer::DeletePkU128 { .. } => "delete_pk_u_128", - Reducer::DeletePkU16 { .. } => "delete_pk_u_16", - Reducer::DeletePkU256 { .. } => "delete_pk_u_256", - Reducer::DeletePkU32 { .. } => "delete_pk_u_32", - Reducer::DeletePkU32InsertPkU32Two { .. } => "delete_pk_u_32_insert_pk_u_32_two", - Reducer::DeletePkU32Two { .. } => "delete_pk_u_32_two", - Reducer::DeletePkU64 { .. } => "delete_pk_u_64", - Reducer::DeletePkU8 { .. } => "delete_pk_u_8", - Reducer::DeletePkUuid { .. } => "delete_pk_uuid", - Reducer::DeleteUniqueBool { .. } => "delete_unique_bool", - Reducer::DeleteUniqueConnectionId { .. } => "delete_unique_connection_id", - Reducer::DeleteUniqueI128 { .. } => "delete_unique_i_128", - Reducer::DeleteUniqueI16 { .. } => "delete_unique_i_16", - Reducer::DeleteUniqueI256 { .. } => "delete_unique_i_256", - Reducer::DeleteUniqueI32 { .. } => "delete_unique_i_32", - Reducer::DeleteUniqueI64 { .. } => "delete_unique_i_64", - Reducer::DeleteUniqueI8 { .. } => "delete_unique_i_8", - Reducer::DeleteUniqueIdentity { .. } => "delete_unique_identity", - Reducer::DeleteUniqueString { .. } => "delete_unique_string", - Reducer::DeleteUniqueU128 { .. } => "delete_unique_u_128", - Reducer::DeleteUniqueU16 { .. } => "delete_unique_u_16", - Reducer::DeleteUniqueU256 { .. } => "delete_unique_u_256", - Reducer::DeleteUniqueU32 { .. } => "delete_unique_u_32", - Reducer::DeleteUniqueU64 { .. } => "delete_unique_u_64", - Reducer::DeleteUniqueU8 { .. } => "delete_unique_u_8", - Reducer::DeleteUniqueUuid { .. } => "delete_unique_uuid", - Reducer::InsertCallTimestamp => "insert_call_timestamp", - Reducer::InsertCallUuidV4 => "insert_call_uuid_v_4", - Reducer::InsertCallUuidV7 => "insert_call_uuid_v_7", - Reducer::InsertCallerOneConnectionId => "insert_caller_one_connection_id", - Reducer::InsertCallerOneIdentity => "insert_caller_one_identity", - Reducer::InsertCallerPkConnectionId { .. } => "insert_caller_pk_connection_id", - Reducer::InsertCallerPkIdentity { .. } => "insert_caller_pk_identity", - Reducer::InsertCallerUniqueConnectionId { .. } => "insert_caller_unique_connection_id", - Reducer::InsertCallerUniqueIdentity { .. } => "insert_caller_unique_identity", - Reducer::InsertCallerVecConnectionId => "insert_caller_vec_connection_id", - Reducer::InsertCallerVecIdentity => "insert_caller_vec_identity", - Reducer::InsertIntoBtreeU32 { .. } => "insert_into_btree_u_32", - Reducer::InsertIntoIndexedSimpleEnum { .. } => "insert_into_indexed_simple_enum", - Reducer::InsertIntoPkBtreeU32 { .. } => "insert_into_pk_btree_u_32", - Reducer::InsertLargeTable { .. } => "insert_large_table", - Reducer::InsertOneBool { .. } => "insert_one_bool", - Reducer::InsertOneByteStruct { .. } => "insert_one_byte_struct", - Reducer::InsertOneConnectionId { .. } => "insert_one_connection_id", - Reducer::InsertOneEnumWithPayload { .. } => "insert_one_enum_with_payload", - Reducer::InsertOneEveryPrimitiveStruct { .. } => "insert_one_every_primitive_struct", - Reducer::InsertOneEveryVecStruct { .. } => "insert_one_every_vec_struct", - Reducer::InsertOneF32 { .. } => "insert_one_f_32", - Reducer::InsertOneF64 { .. } => "insert_one_f_64", - Reducer::InsertOneI128 { .. } => "insert_one_i_128", + Reducer::InsertOneU8 { .. } => "insert_one_u_8", + Reducer::InsertOneU16 { .. } => "insert_one_u_16", + Reducer::InsertOneU32 { .. } => "insert_one_u_32", + Reducer::InsertOneU64 { .. } => "insert_one_u_64", + Reducer::InsertOneU128 { .. } => "insert_one_u_128", + Reducer::InsertOneU256 { .. } => "insert_one_u_256", + Reducer::InsertOneI8 { .. } => "insert_one_i_8", Reducer::InsertOneI16 { .. } => "insert_one_i_16", - Reducer::InsertOneI256 { .. } => "insert_one_i_256", Reducer::InsertOneI32 { .. } => "insert_one_i_32", Reducer::InsertOneI64 { .. } => "insert_one_i_64", - Reducer::InsertOneI8 { .. } => "insert_one_i_8", - Reducer::InsertOneIdentity { .. } => "insert_one_identity", - Reducer::InsertOneSimpleEnum { .. } => "insert_one_simple_enum", + Reducer::InsertOneI128 { .. } => "insert_one_i_128", + Reducer::InsertOneI256 { .. } => "insert_one_i_256", + Reducer::InsertOneBool { .. } => "insert_one_bool", + Reducer::InsertOneF32 { .. } => "insert_one_f_32", + Reducer::InsertOneF64 { .. } => "insert_one_f_64", Reducer::InsertOneString { .. } => "insert_one_string", + Reducer::InsertOneIdentity { .. } => "insert_one_identity", + Reducer::InsertOneConnectionId { .. } => "insert_one_connection_id", + Reducer::InsertOneUuid { .. } => "insert_one_uuid", Reducer::InsertOneTimestamp { .. } => "insert_one_timestamp", - Reducer::InsertOneU128 { .. } => "insert_one_u_128", - Reducer::InsertOneU16 { .. } => "insert_one_u_16", - Reducer::InsertOneU256 { .. } => "insert_one_u_256", - Reducer::InsertOneU32 { .. } => "insert_one_u_32", - Reducer::InsertOneU64 { .. } => "insert_one_u_64", - Reducer::InsertOneU8 { .. } => "insert_one_u_8", + Reducer::InsertOneSimpleEnum { .. } => "insert_one_simple_enum", + Reducer::InsertOneEnumWithPayload { .. } => "insert_one_enum_with_payload", Reducer::InsertOneUnitStruct { .. } => "insert_one_unit_struct", - Reducer::InsertOneUuid { .. } => "insert_one_uuid", - Reducer::InsertOptionEveryPrimitiveStruct { .. } => "insert_option_every_primitive_struct", + Reducer::InsertOneByteStruct { .. } => "insert_one_byte_struct", + Reducer::InsertOneEveryPrimitiveStruct { .. } => "insert_one_every_primitive_struct", + Reducer::InsertOneEveryVecStruct { .. } => "insert_one_every_vec_struct", + Reducer::InsertVecU8 { .. } => "insert_vec_u_8", + Reducer::InsertVecU16 { .. } => "insert_vec_u_16", + Reducer::InsertVecU32 { .. } => "insert_vec_u_32", + Reducer::InsertVecU64 { .. } => "insert_vec_u_64", + Reducer::InsertVecU128 { .. } => "insert_vec_u_128", + Reducer::InsertVecU256 { .. } => "insert_vec_u_256", + Reducer::InsertVecI8 { .. } => "insert_vec_i_8", + Reducer::InsertVecI16 { .. } => "insert_vec_i_16", + Reducer::InsertVecI32 { .. } => "insert_vec_i_32", + Reducer::InsertVecI64 { .. } => "insert_vec_i_64", + Reducer::InsertVecI128 { .. } => "insert_vec_i_128", + Reducer::InsertVecI256 { .. } => "insert_vec_i_256", + Reducer::InsertVecBool { .. } => "insert_vec_bool", + Reducer::InsertVecF32 { .. } => "insert_vec_f_32", + Reducer::InsertVecF64 { .. } => "insert_vec_f_64", + Reducer::InsertVecString { .. } => "insert_vec_string", + Reducer::InsertVecIdentity { .. } => "insert_vec_identity", + Reducer::InsertVecConnectionId { .. } => "insert_vec_connection_id", + Reducer::InsertVecUuid { .. } => "insert_vec_uuid", + Reducer::InsertVecTimestamp { .. } => "insert_vec_timestamp", + Reducer::InsertVecSimpleEnum { .. } => "insert_vec_simple_enum", + Reducer::InsertVecEnumWithPayload { .. } => "insert_vec_enum_with_payload", + Reducer::InsertVecUnitStruct { .. } => "insert_vec_unit_struct", + Reducer::InsertVecByteStruct { .. } => "insert_vec_byte_struct", + Reducer::InsertVecEveryPrimitiveStruct { .. } => "insert_vec_every_primitive_struct", + Reducer::InsertVecEveryVecStruct { .. } => "insert_vec_every_vec_struct", Reducer::InsertOptionI32 { .. } => "insert_option_i_32", - Reducer::InsertOptionIdentity { .. } => "insert_option_identity", - Reducer::InsertOptionSimpleEnum { .. } => "insert_option_simple_enum", Reducer::InsertOptionString { .. } => "insert_option_string", + Reducer::InsertOptionIdentity { .. } => "insert_option_identity", Reducer::InsertOptionUuid { .. } => "insert_option_uuid", + Reducer::InsertOptionSimpleEnum { .. } => "insert_option_simple_enum", + Reducer::InsertOptionEveryPrimitiveStruct { .. } => "insert_option_every_primitive_struct", Reducer::InsertOptionVecOptionI32 { .. } => "insert_option_vec_option_i_32", - Reducer::InsertPkBool { .. } => "insert_pk_bool", - Reducer::InsertPkConnectionId { .. } => "insert_pk_connection_id", - Reducer::InsertPkI128 { .. } => "insert_pk_i_128", - Reducer::InsertPkI16 { .. } => "insert_pk_i_16", - Reducer::InsertPkI256 { .. } => "insert_pk_i_256", - Reducer::InsertPkI32 { .. } => "insert_pk_i_32", - Reducer::InsertPkI64 { .. } => "insert_pk_i_64", - Reducer::InsertPkI8 { .. } => "insert_pk_i_8", - Reducer::InsertPkIdentity { .. } => "insert_pk_identity", - Reducer::InsertPkSimpleEnum { .. } => "insert_pk_simple_enum", - Reducer::InsertPkString { .. } => "insert_pk_string", - Reducer::InsertPkU128 { .. } => "insert_pk_u_128", - Reducer::InsertPkU16 { .. } => "insert_pk_u_16", - Reducer::InsertPkU256 { .. } => "insert_pk_u_256", - Reducer::InsertPkU32 { .. } => "insert_pk_u_32", - Reducer::InsertPkU32Two { .. } => "insert_pk_u_32_two", - Reducer::InsertPkU64 { .. } => "insert_pk_u_64", - Reducer::InsertPkU8 { .. } => "insert_pk_u_8", - Reducer::InsertPkUuid { .. } => "insert_pk_uuid", - Reducer::InsertPrimitivesAsStrings { .. } => "insert_primitives_as_strings", - Reducer::InsertResultEveryPrimitiveStructString { .. } => "insert_result_every_primitive_struct_string", Reducer::InsertResultI32String { .. } => "insert_result_i_32_string", + Reducer::InsertResultStringI32 { .. } => "insert_result_string_i_32", Reducer::InsertResultIdentityString { .. } => "insert_result_identity_string", Reducer::InsertResultSimpleEnumI32 { .. } => "insert_result_simple_enum_i_32", - Reducer::InsertResultStringI32 { .. } => "insert_result_string_i_32", + Reducer::InsertResultEveryPrimitiveStructString { .. } => "insert_result_every_primitive_struct_string", Reducer::InsertResultVecI32String { .. } => "insert_result_vec_i_32_string", - Reducer::InsertTableHoldsTable { .. } => "insert_table_holds_table", - Reducer::InsertUniqueBool { .. } => "insert_unique_bool", - Reducer::InsertUniqueConnectionId { .. } => "insert_unique_connection_id", - Reducer::InsertUniqueI128 { .. } => "insert_unique_i_128", + Reducer::InsertUniqueU8 { .. } => "insert_unique_u_8", + Reducer::UpdateUniqueU8 { .. } => "update_unique_u_8", + Reducer::DeleteUniqueU8 { .. } => "delete_unique_u_8", + Reducer::InsertUniqueU16 { .. } => "insert_unique_u_16", + Reducer::UpdateUniqueU16 { .. } => "update_unique_u_16", + Reducer::DeleteUniqueU16 { .. } => "delete_unique_u_16", + Reducer::InsertUniqueU32 { .. } => "insert_unique_u_32", + Reducer::UpdateUniqueU32 { .. } => "update_unique_u_32", + Reducer::DeleteUniqueU32 { .. } => "delete_unique_u_32", + Reducer::InsertUniqueU64 { .. } => "insert_unique_u_64", + Reducer::UpdateUniqueU64 { .. } => "update_unique_u_64", + Reducer::DeleteUniqueU64 { .. } => "delete_unique_u_64", + Reducer::InsertUniqueU128 { .. } => "insert_unique_u_128", + Reducer::UpdateUniqueU128 { .. } => "update_unique_u_128", + Reducer::DeleteUniqueU128 { .. } => "delete_unique_u_128", + Reducer::InsertUniqueU256 { .. } => "insert_unique_u_256", + Reducer::UpdateUniqueU256 { .. } => "update_unique_u_256", + Reducer::DeleteUniqueU256 { .. } => "delete_unique_u_256", + Reducer::InsertUniqueI8 { .. } => "insert_unique_i_8", + Reducer::UpdateUniqueI8 { .. } => "update_unique_i_8", + Reducer::DeleteUniqueI8 { .. } => "delete_unique_i_8", Reducer::InsertUniqueI16 { .. } => "insert_unique_i_16", - Reducer::InsertUniqueI256 { .. } => "insert_unique_i_256", + Reducer::UpdateUniqueI16 { .. } => "update_unique_i_16", + Reducer::DeleteUniqueI16 { .. } => "delete_unique_i_16", Reducer::InsertUniqueI32 { .. } => "insert_unique_i_32", + Reducer::UpdateUniqueI32 { .. } => "update_unique_i_32", + Reducer::DeleteUniqueI32 { .. } => "delete_unique_i_32", Reducer::InsertUniqueI64 { .. } => "insert_unique_i_64", - Reducer::InsertUniqueI8 { .. } => "insert_unique_i_8", - Reducer::InsertUniqueIdentity { .. } => "insert_unique_identity", + Reducer::UpdateUniqueI64 { .. } => "update_unique_i_64", + Reducer::DeleteUniqueI64 { .. } => "delete_unique_i_64", + Reducer::InsertUniqueI128 { .. } => "insert_unique_i_128", + Reducer::UpdateUniqueI128 { .. } => "update_unique_i_128", + Reducer::DeleteUniqueI128 { .. } => "delete_unique_i_128", + Reducer::InsertUniqueI256 { .. } => "insert_unique_i_256", + Reducer::UpdateUniqueI256 { .. } => "update_unique_i_256", + Reducer::DeleteUniqueI256 { .. } => "delete_unique_i_256", + Reducer::InsertUniqueBool { .. } => "insert_unique_bool", + Reducer::UpdateUniqueBool { .. } => "update_unique_bool", + Reducer::DeleteUniqueBool { .. } => "delete_unique_bool", Reducer::InsertUniqueString { .. } => "insert_unique_string", - Reducer::InsertUniqueU128 { .. } => "insert_unique_u_128", - Reducer::InsertUniqueU16 { .. } => "insert_unique_u_16", - Reducer::InsertUniqueU256 { .. } => "insert_unique_u_256", - Reducer::InsertUniqueU32 { .. } => "insert_unique_u_32", - Reducer::InsertUniqueU32UpdatePkU32 { .. } => "insert_unique_u_32_update_pk_u_32", - Reducer::InsertUniqueU64 { .. } => "insert_unique_u_64", - Reducer::InsertUniqueU8 { .. } => "insert_unique_u_8", + Reducer::UpdateUniqueString { .. } => "update_unique_string", + Reducer::DeleteUniqueString { .. } => "delete_unique_string", + Reducer::InsertUniqueIdentity { .. } => "insert_unique_identity", + Reducer::UpdateUniqueIdentity { .. } => "update_unique_identity", + Reducer::DeleteUniqueIdentity { .. } => "delete_unique_identity", + Reducer::InsertUniqueConnectionId { .. } => "insert_unique_connection_id", + Reducer::UpdateUniqueConnectionId { .. } => "update_unique_connection_id", + Reducer::DeleteUniqueConnectionId { .. } => "delete_unique_connection_id", Reducer::InsertUniqueUuid { .. } => "insert_unique_uuid", - Reducer::InsertUser { .. } => "insert_user", - Reducer::InsertVecBool { .. } => "insert_vec_bool", - Reducer::InsertVecByteStruct { .. } => "insert_vec_byte_struct", - Reducer::InsertVecConnectionId { .. } => "insert_vec_connection_id", - Reducer::InsertVecEnumWithPayload { .. } => "insert_vec_enum_with_payload", - Reducer::InsertVecEveryPrimitiveStruct { .. } => "insert_vec_every_primitive_struct", - Reducer::InsertVecEveryVecStruct { .. } => "insert_vec_every_vec_struct", - Reducer::InsertVecF32 { .. } => "insert_vec_f_32", - Reducer::InsertVecF64 { .. } => "insert_vec_f_64", - Reducer::InsertVecI128 { .. } => "insert_vec_i_128", - Reducer::InsertVecI16 { .. } => "insert_vec_i_16", - Reducer::InsertVecI256 { .. } => "insert_vec_i_256", - Reducer::InsertVecI32 { .. } => "insert_vec_i_32", - Reducer::InsertVecI64 { .. } => "insert_vec_i_64", - Reducer::InsertVecI8 { .. } => "insert_vec_i_8", - Reducer::InsertVecIdentity { .. } => "insert_vec_identity", - Reducer::InsertVecSimpleEnum { .. } => "insert_vec_simple_enum", - Reducer::InsertVecString { .. } => "insert_vec_string", - Reducer::InsertVecTimestamp { .. } => "insert_vec_timestamp", - Reducer::InsertVecU128 { .. } => "insert_vec_u_128", - Reducer::InsertVecU16 { .. } => "insert_vec_u_16", - Reducer::InsertVecU256 { .. } => "insert_vec_u_256", - Reducer::InsertVecU32 { .. } => "insert_vec_u_32", - Reducer::InsertVecU64 { .. } => "insert_vec_u_64", - Reducer::InsertVecU8 { .. } => "insert_vec_u_8", - Reducer::InsertVecUnitStruct { .. } => "insert_vec_unit_struct", - Reducer::InsertVecUuid { .. } => "insert_vec_uuid", - Reducer::NoOpSucceeds => "no_op_succeeds", - Reducer::SendScheduledMessage { .. } => "send_scheduled_message", - Reducer::SortedUuidsInsert => "sorted_uuids_insert", - Reducer::UpdateIndexedSimpleEnum { .. } => "update_indexed_simple_enum", - Reducer::UpdatePkBool { .. } => "update_pk_bool", - Reducer::UpdatePkConnectionId { .. } => "update_pk_connection_id", - Reducer::UpdatePkI128 { .. } => "update_pk_i_128", + Reducer::UpdateUniqueUuid { .. } => "update_unique_uuid", + Reducer::DeleteUniqueUuid { .. } => "delete_unique_uuid", + Reducer::InsertPkU8 { .. } => "insert_pk_u_8", + Reducer::UpdatePkU8 { .. } => "update_pk_u_8", + Reducer::DeletePkU8 { .. } => "delete_pk_u_8", + Reducer::InsertPkU16 { .. } => "insert_pk_u_16", + Reducer::UpdatePkU16 { .. } => "update_pk_u_16", + Reducer::DeletePkU16 { .. } => "delete_pk_u_16", + Reducer::InsertPkU32 { .. } => "insert_pk_u_32", + Reducer::UpdatePkU32 { .. } => "update_pk_u_32", + Reducer::DeletePkU32 { .. } => "delete_pk_u_32", + Reducer::InsertPkU64 { .. } => "insert_pk_u_64", + Reducer::UpdatePkU64 { .. } => "update_pk_u_64", + Reducer::DeletePkU64 { .. } => "delete_pk_u_64", + Reducer::InsertPkU128 { .. } => "insert_pk_u_128", + Reducer::UpdatePkU128 { .. } => "update_pk_u_128", + Reducer::DeletePkU128 { .. } => "delete_pk_u_128", + Reducer::InsertPkU256 { .. } => "insert_pk_u_256", + Reducer::UpdatePkU256 { .. } => "update_pk_u_256", + Reducer::DeletePkU256 { .. } => "delete_pk_u_256", + Reducer::InsertPkI8 { .. } => "insert_pk_i_8", + Reducer::UpdatePkI8 { .. } => "update_pk_i_8", + Reducer::DeletePkI8 { .. } => "delete_pk_i_8", + Reducer::InsertPkI16 { .. } => "insert_pk_i_16", Reducer::UpdatePkI16 { .. } => "update_pk_i_16", - Reducer::UpdatePkI256 { .. } => "update_pk_i_256", + Reducer::DeletePkI16 { .. } => "delete_pk_i_16", + Reducer::InsertPkI32 { .. } => "insert_pk_i_32", Reducer::UpdatePkI32 { .. } => "update_pk_i_32", + Reducer::DeletePkI32 { .. } => "delete_pk_i_32", + Reducer::InsertPkI64 { .. } => "insert_pk_i_64", Reducer::UpdatePkI64 { .. } => "update_pk_i_64", - Reducer::UpdatePkI8 { .. } => "update_pk_i_8", - Reducer::UpdatePkIdentity { .. } => "update_pk_identity", - Reducer::UpdatePkSimpleEnum { .. } => "update_pk_simple_enum", + Reducer::DeletePkI64 { .. } => "delete_pk_i_64", + Reducer::InsertPkI128 { .. } => "insert_pk_i_128", + Reducer::UpdatePkI128 { .. } => "update_pk_i_128", + Reducer::DeletePkI128 { .. } => "delete_pk_i_128", + Reducer::InsertPkI256 { .. } => "insert_pk_i_256", + Reducer::UpdatePkI256 { .. } => "update_pk_i_256", + Reducer::DeletePkI256 { .. } => "delete_pk_i_256", + Reducer::InsertPkBool { .. } => "insert_pk_bool", + Reducer::UpdatePkBool { .. } => "update_pk_bool", + Reducer::DeletePkBool { .. } => "delete_pk_bool", + Reducer::InsertPkString { .. } => "insert_pk_string", Reducer::UpdatePkString { .. } => "update_pk_string", - Reducer::UpdatePkU128 { .. } => "update_pk_u_128", - Reducer::UpdatePkU16 { .. } => "update_pk_u_16", - Reducer::UpdatePkU256 { .. } => "update_pk_u_256", - Reducer::UpdatePkU32 { .. } => "update_pk_u_32", - Reducer::UpdatePkU32Two { .. } => "update_pk_u_32_two", - Reducer::UpdatePkU64 { .. } => "update_pk_u_64", - Reducer::UpdatePkU8 { .. } => "update_pk_u_8", + Reducer::DeletePkString { .. } => "delete_pk_string", + Reducer::InsertPkIdentity { .. } => "insert_pk_identity", + Reducer::UpdatePkIdentity { .. } => "update_pk_identity", + Reducer::DeletePkIdentity { .. } => "delete_pk_identity", + Reducer::InsertPkConnectionId { .. } => "insert_pk_connection_id", + Reducer::UpdatePkConnectionId { .. } => "update_pk_connection_id", + Reducer::DeletePkConnectionId { .. } => "delete_pk_connection_id", + Reducer::InsertPkUuid { .. } => "insert_pk_uuid", Reducer::UpdatePkUuid { .. } => "update_pk_uuid", - Reducer::UpdateUniqueBool { .. } => "update_unique_bool", - Reducer::UpdateUniqueConnectionId { .. } => "update_unique_connection_id", - Reducer::UpdateUniqueI128 { .. } => "update_unique_i_128", - Reducer::UpdateUniqueI16 { .. } => "update_unique_i_16", - Reducer::UpdateUniqueI256 { .. } => "update_unique_i_256", - Reducer::UpdateUniqueI32 { .. } => "update_unique_i_32", - Reducer::UpdateUniqueI64 { .. } => "update_unique_i_64", - Reducer::UpdateUniqueI8 { .. } => "update_unique_i_8", - Reducer::UpdateUniqueIdentity { .. } => "update_unique_identity", - Reducer::UpdateUniqueString { .. } => "update_unique_string", - Reducer::UpdateUniqueU128 { .. } => "update_unique_u_128", - Reducer::UpdateUniqueU16 { .. } => "update_unique_u_16", - Reducer::UpdateUniqueU256 { .. } => "update_unique_u_256", - Reducer::UpdateUniqueU32 { .. } => "update_unique_u_32", - Reducer::UpdateUniqueU64 { .. } => "update_unique_u_64", - Reducer::UpdateUniqueU8 { .. } => "update_unique_u_8", - Reducer::UpdateUniqueUuid { .. } => "update_unique_uuid", - _ => unreachable!(), - } - } - #[allow(clippy::clone_on_copy)] - fn args_bsatn(&self) -> Result, __sats::bsatn::EncodeError> { - match self { - Reducer::DeleteFromBtreeU32 { rows } => { - __sats::bsatn::to_vec(&delete_from_btree_u_32_reducer::DeleteFromBtreeU32Args { rows: rows.clone() }) - } - Reducer::DeleteLargeTable { - a, - b, - c, - d, - e, - f, - g, - h, - i, - j, - k, - l, - m, - n, - o, - p, - q, - r, - s, - t, - u, - v, - } => __sats::bsatn::to_vec(&delete_large_table_reducer::DeleteLargeTableArgs { - a: a.clone(), - b: b.clone(), - c: c.clone(), - d: d.clone(), - e: e.clone(), - f: f.clone(), - g: g.clone(), - h: h.clone(), - i: i.clone(), - j: j.clone(), - k: k.clone(), - l: l.clone(), - m: m.clone(), - n: n.clone(), - o: o.clone(), - p: p.clone(), - q: q.clone(), - r: r.clone(), - s: s.clone(), - t: t.clone(), - u: u.clone(), - v: v.clone(), - }), - Reducer::DeletePkBool { b } => { - __sats::bsatn::to_vec(&delete_pk_bool_reducer::DeletePkBoolArgs { b: b.clone() }) - } - Reducer::DeletePkConnectionId { a } => { - __sats::bsatn::to_vec(&delete_pk_connection_id_reducer::DeletePkConnectionIdArgs { a: a.clone() }) - } - Reducer::DeletePkI128 { n } => { - __sats::bsatn::to_vec(&delete_pk_i_128_reducer::DeletePkI128Args { n: n.clone() }) - } - Reducer::DeletePkI16 { n } => { - __sats::bsatn::to_vec(&delete_pk_i_16_reducer::DeletePkI16Args { n: n.clone() }) - } - Reducer::DeletePkI256 { n } => { - __sats::bsatn::to_vec(&delete_pk_i_256_reducer::DeletePkI256Args { n: n.clone() }) - } - Reducer::DeletePkI32 { n } => { - __sats::bsatn::to_vec(&delete_pk_i_32_reducer::DeletePkI32Args { n: n.clone() }) - } - Reducer::DeletePkI64 { n } => { - __sats::bsatn::to_vec(&delete_pk_i_64_reducer::DeletePkI64Args { n: n.clone() }) - } - Reducer::DeletePkI8 { n } => __sats::bsatn::to_vec(&delete_pk_i_8_reducer::DeletePkI8Args { n: n.clone() }), - Reducer::DeletePkIdentity { i } => { - __sats::bsatn::to_vec(&delete_pk_identity_reducer::DeletePkIdentityArgs { i: i.clone() }) - } - Reducer::DeletePkString { s } => { - __sats::bsatn::to_vec(&delete_pk_string_reducer::DeletePkStringArgs { s: s.clone() }) - } - Reducer::DeletePkU128 { n } => { - __sats::bsatn::to_vec(&delete_pk_u_128_reducer::DeletePkU128Args { n: n.clone() }) - } - Reducer::DeletePkU16 { n } => { - __sats::bsatn::to_vec(&delete_pk_u_16_reducer::DeletePkU16Args { n: n.clone() }) - } - Reducer::DeletePkU256 { n } => { - __sats::bsatn::to_vec(&delete_pk_u_256_reducer::DeletePkU256Args { n: n.clone() }) - } - Reducer::DeletePkU32 { n } => { - __sats::bsatn::to_vec(&delete_pk_u_32_reducer::DeletePkU32Args { n: n.clone() }) - } - Reducer::DeletePkU32InsertPkU32Two { n, data } => __sats::bsatn::to_vec( - &delete_pk_u_32_insert_pk_u_32_two_reducer::DeletePkU32InsertPkU32TwoArgs { - n: n.clone(), - data: data.clone(), - }, - ), - Reducer::DeletePkU32Two { n } => { - __sats::bsatn::to_vec(&delete_pk_u_32_two_reducer::DeletePkU32TwoArgs { n: n.clone() }) - } - Reducer::DeletePkU64 { n } => { - __sats::bsatn::to_vec(&delete_pk_u_64_reducer::DeletePkU64Args { n: n.clone() }) - } - Reducer::DeletePkU8 { n } => __sats::bsatn::to_vec(&delete_pk_u_8_reducer::DeletePkU8Args { n: n.clone() }), - Reducer::DeletePkUuid { u } => { - __sats::bsatn::to_vec(&delete_pk_uuid_reducer::DeletePkUuidArgs { u: u.clone() }) - } - Reducer::DeleteUniqueBool { b } => { - __sats::bsatn::to_vec(&delete_unique_bool_reducer::DeleteUniqueBoolArgs { b: b.clone() }) - } - Reducer::DeleteUniqueConnectionId { a } => { - __sats::bsatn::to_vec(&delete_unique_connection_id_reducer::DeleteUniqueConnectionIdArgs { - a: a.clone(), - }) - } - Reducer::DeleteUniqueI128 { n } => { - __sats::bsatn::to_vec(&delete_unique_i_128_reducer::DeleteUniqueI128Args { n: n.clone() }) - } - Reducer::DeleteUniqueI16 { n } => { - __sats::bsatn::to_vec(&delete_unique_i_16_reducer::DeleteUniqueI16Args { n: n.clone() }) - } - Reducer::DeleteUniqueI256 { n } => { - __sats::bsatn::to_vec(&delete_unique_i_256_reducer::DeleteUniqueI256Args { n: n.clone() }) - } - Reducer::DeleteUniqueI32 { n } => { - __sats::bsatn::to_vec(&delete_unique_i_32_reducer::DeleteUniqueI32Args { n: n.clone() }) - } - Reducer::DeleteUniqueI64 { n } => { - __sats::bsatn::to_vec(&delete_unique_i_64_reducer::DeleteUniqueI64Args { n: n.clone() }) - } - Reducer::DeleteUniqueI8 { n } => { - __sats::bsatn::to_vec(&delete_unique_i_8_reducer::DeleteUniqueI8Args { n: n.clone() }) - } - Reducer::DeleteUniqueIdentity { i } => { - __sats::bsatn::to_vec(&delete_unique_identity_reducer::DeleteUniqueIdentityArgs { i: i.clone() }) - } - Reducer::DeleteUniqueString { s } => { - __sats::bsatn::to_vec(&delete_unique_string_reducer::DeleteUniqueStringArgs { s: s.clone() }) - } - Reducer::DeleteUniqueU128 { n } => { - __sats::bsatn::to_vec(&delete_unique_u_128_reducer::DeleteUniqueU128Args { n: n.clone() }) - } - Reducer::DeleteUniqueU16 { n } => { - __sats::bsatn::to_vec(&delete_unique_u_16_reducer::DeleteUniqueU16Args { n: n.clone() }) - } - Reducer::DeleteUniqueU256 { n } => { - __sats::bsatn::to_vec(&delete_unique_u_256_reducer::DeleteUniqueU256Args { n: n.clone() }) - } - Reducer::DeleteUniqueU32 { n } => { - __sats::bsatn::to_vec(&delete_unique_u_32_reducer::DeleteUniqueU32Args { n: n.clone() }) - } - Reducer::DeleteUniqueU64 { n } => { - __sats::bsatn::to_vec(&delete_unique_u_64_reducer::DeleteUniqueU64Args { n: n.clone() }) - } - Reducer::DeleteUniqueU8 { n } => { - __sats::bsatn::to_vec(&delete_unique_u_8_reducer::DeleteUniqueU8Args { n: n.clone() }) - } - Reducer::DeleteUniqueUuid { u } => { - __sats::bsatn::to_vec(&delete_unique_uuid_reducer::DeleteUniqueUuidArgs { u: u.clone() }) - } - Reducer::InsertCallTimestamp => { - __sats::bsatn::to_vec(&insert_call_timestamp_reducer::InsertCallTimestampArgs {}) - } - Reducer::InsertCallUuidV4 => __sats::bsatn::to_vec(&insert_call_uuid_v_4_reducer::InsertCallUuidV4Args {}), - Reducer::InsertCallUuidV7 => __sats::bsatn::to_vec(&insert_call_uuid_v_7_reducer::InsertCallUuidV7Args {}), - Reducer::InsertCallerOneConnectionId => { - __sats::bsatn::to_vec(&insert_caller_one_connection_id_reducer::InsertCallerOneConnectionIdArgs {}) - } - Reducer::InsertCallerOneIdentity => { - __sats::bsatn::to_vec(&insert_caller_one_identity_reducer::InsertCallerOneIdentityArgs {}) - } - Reducer::InsertCallerPkConnectionId { data } => __sats::bsatn::to_vec( - &insert_caller_pk_connection_id_reducer::InsertCallerPkConnectionIdArgs { data: data.clone() }, - ), - Reducer::InsertCallerPkIdentity { data } => { - __sats::bsatn::to_vec(&insert_caller_pk_identity_reducer::InsertCallerPkIdentityArgs { - data: data.clone(), - }) - } - Reducer::InsertCallerUniqueConnectionId { data } => __sats::bsatn::to_vec( - &insert_caller_unique_connection_id_reducer::InsertCallerUniqueConnectionIdArgs { data: data.clone() }, - ), - Reducer::InsertCallerUniqueIdentity { data } => { - __sats::bsatn::to_vec(&insert_caller_unique_identity_reducer::InsertCallerUniqueIdentityArgs { - data: data.clone(), - }) - } - Reducer::InsertCallerVecConnectionId => { - __sats::bsatn::to_vec(&insert_caller_vec_connection_id_reducer::InsertCallerVecConnectionIdArgs {}) - } - Reducer::InsertCallerVecIdentity => { - __sats::bsatn::to_vec(&insert_caller_vec_identity_reducer::InsertCallerVecIdentityArgs {}) - } - Reducer::InsertIntoBtreeU32 { rows } => { - __sats::bsatn::to_vec(&insert_into_btree_u_32_reducer::InsertIntoBtreeU32Args { rows: rows.clone() }) - } - Reducer::InsertIntoIndexedSimpleEnum { n } => __sats::bsatn::to_vec( - &insert_into_indexed_simple_enum_reducer::InsertIntoIndexedSimpleEnumArgs { n: n.clone() }, - ), - Reducer::InsertIntoPkBtreeU32 { pk_u_32, bt_u_32 } => { - __sats::bsatn::to_vec(&insert_into_pk_btree_u_32_reducer::InsertIntoPkBtreeU32Args { - pk_u_32: pk_u_32.clone(), - bt_u_32: bt_u_32.clone(), - }) - } - Reducer::InsertLargeTable { - a, - b, - c, - d, - e, - f, - g, - h, - i, - j, - k, - l, - m, - n, - o, - p, - q, - r, - s, - t, - u, - v, - } => __sats::bsatn::to_vec(&insert_large_table_reducer::InsertLargeTableArgs { - a: a.clone(), - b: b.clone(), - c: c.clone(), - d: d.clone(), - e: e.clone(), - f: f.clone(), - g: g.clone(), - h: h.clone(), - i: i.clone(), - j: j.clone(), - k: k.clone(), - l: l.clone(), - m: m.clone(), - n: n.clone(), - o: o.clone(), - p: p.clone(), - q: q.clone(), - r: r.clone(), - s: s.clone(), - t: t.clone(), - u: u.clone(), - v: v.clone(), - }), - Reducer::InsertOneBool { b } => { - __sats::bsatn::to_vec(&insert_one_bool_reducer::InsertOneBoolArgs { b: b.clone() }) - } - Reducer::InsertOneByteStruct { s } => { - __sats::bsatn::to_vec(&insert_one_byte_struct_reducer::InsertOneByteStructArgs { s: s.clone() }) + Reducer::DeletePkUuid { .. } => "delete_pk_uuid", + Reducer::InsertPkSimpleEnum { .. } => "insert_pk_simple_enum", + Reducer::InsertPkU32Two { .. } => "insert_pk_u_32_two", + Reducer::UpdatePkU32Two { .. } => "update_pk_u_32_two", + Reducer::DeletePkU32Two { .. } => "delete_pk_u_32_two", + Reducer::UpdatePkSimpleEnum { .. } => "update_pk_simple_enum", + Reducer::InsertLargeTable { .. } => "insert_large_table", + Reducer::DeleteLargeTable { .. } => "delete_large_table", + Reducer::InsertTableHoldsTable { .. } => "insert_table_holds_table", + Reducer::InsertIntoBtreeU32 { .. } => "insert_into_btree_u_32", + Reducer::DeleteFromBtreeU32 { .. } => "delete_from_btree_u_32", + Reducer::InsertIntoPkBtreeU32 { .. } => "insert_into_pk_btree_u_32", + Reducer::InsertUniqueU32UpdatePkU32 { .. } => "insert_unique_u_32_update_pk_u_32", + Reducer::DeletePkU32InsertPkU32Two { .. } => "delete_pk_u_32_insert_pk_u_32_two", + Reducer::InsertCallerOneIdentity => "insert_caller_one_identity", + Reducer::InsertCallerVecIdentity => "insert_caller_vec_identity", + Reducer::InsertCallerUniqueIdentity { .. } => "insert_caller_unique_identity", + Reducer::InsertCallerPkIdentity { .. } => "insert_caller_pk_identity", + Reducer::InsertCallerOneConnectionId => "insert_caller_one_connection_id", + Reducer::InsertCallerVecConnectionId => "insert_caller_vec_connection_id", + Reducer::InsertCallerUniqueConnectionId { .. } => "insert_caller_unique_connection_id", + Reducer::InsertCallerPkConnectionId { .. } => "insert_caller_pk_connection_id", + Reducer::InsertCallTimestamp => "insert_call_timestamp", + Reducer::InsertCallUuidV4 => "insert_call_uuid_v_4", + Reducer::InsertCallUuidV7 => "insert_call_uuid_v_7", + Reducer::InsertPrimitivesAsStrings { .. } => "insert_primitives_as_strings", + Reducer::NoOpSucceeds => "no_op_succeeds", + Reducer::SendScheduledMessage { .. } => "send_scheduled_message", + Reducer::InsertUser { .. } => "insert_user", + Reducer::InsertIntoIndexedSimpleEnum { .. } => "insert_into_indexed_simple_enum", + Reducer::UpdateIndexedSimpleEnum { .. } => "update_indexed_simple_enum", + Reducer::SortedUuidsInsert => "sorted_uuids_insert", + _ => unreachable!(), + } + } + #[allow(clippy::clone_on_copy)] + fn args_bsatn(&self) -> Result, __sats::bsatn::EncodeError> { + match self { + Reducer::InsertOneU8 { n } => { + __sats::bsatn::to_vec(&insert_one_u_8_reducer::InsertOneU8Args { n: n.clone() }) } - Reducer::InsertOneConnectionId { a } => { - __sats::bsatn::to_vec(&insert_one_connection_id_reducer::InsertOneConnectionIdArgs { a: a.clone() }) + Reducer::InsertOneU16 { n } => { + __sats::bsatn::to_vec(&insert_one_u_16_reducer::InsertOneU16Args { n: n.clone() }) } - Reducer::InsertOneEnumWithPayload { e } => { - __sats::bsatn::to_vec(&insert_one_enum_with_payload_reducer::InsertOneEnumWithPayloadArgs { - e: e.clone(), - }) + Reducer::InsertOneU32 { n } => { + __sats::bsatn::to_vec(&insert_one_u_32_reducer::InsertOneU32Args { n: n.clone() }) } - Reducer::InsertOneEveryPrimitiveStruct { s } => __sats::bsatn::to_vec( - &insert_one_every_primitive_struct_reducer::InsertOneEveryPrimitiveStructArgs { s: s.clone() }, - ), - Reducer::InsertOneEveryVecStruct { s } => { - __sats::bsatn::to_vec(&insert_one_every_vec_struct_reducer::InsertOneEveryVecStructArgs { - s: s.clone(), - }) + Reducer::InsertOneU64 { n } => { + __sats::bsatn::to_vec(&insert_one_u_64_reducer::InsertOneU64Args { n: n.clone() }) } - Reducer::InsertOneF32 { f } => { - __sats::bsatn::to_vec(&insert_one_f_32_reducer::InsertOneF32Args { f: f.clone() }) + Reducer::InsertOneU128 { n } => { + __sats::bsatn::to_vec(&insert_one_u_128_reducer::InsertOneU128Args { n: n.clone() }) } - Reducer::InsertOneF64 { f } => { - __sats::bsatn::to_vec(&insert_one_f_64_reducer::InsertOneF64Args { f: f.clone() }) + Reducer::InsertOneU256 { n } => { + __sats::bsatn::to_vec(&insert_one_u_256_reducer::InsertOneU256Args { n: n.clone() }) } - Reducer::InsertOneI128 { n } => { - __sats::bsatn::to_vec(&insert_one_i_128_reducer::InsertOneI128Args { n: n.clone() }) + Reducer::InsertOneI8 { n } => { + __sats::bsatn::to_vec(&insert_one_i_8_reducer::InsertOneI8Args { n: n.clone() }) } Reducer::InsertOneI16 { n } => { __sats::bsatn::to_vec(&insert_one_i_16_reducer::InsertOneI16Args { n: n.clone() }) } - Reducer::InsertOneI256 { n } => { - __sats::bsatn::to_vec(&insert_one_i_256_reducer::InsertOneI256Args { n: n.clone() }) - } Reducer::InsertOneI32 { n } => { __sats::bsatn::to_vec(&insert_one_i_32_reducer::InsertOneI32Args { n: n.clone() }) } Reducer::InsertOneI64 { n } => { __sats::bsatn::to_vec(&insert_one_i_64_reducer::InsertOneI64Args { n: n.clone() }) } - Reducer::InsertOneI8 { n } => { - __sats::bsatn::to_vec(&insert_one_i_8_reducer::InsertOneI8Args { n: n.clone() }) + Reducer::InsertOneI128 { n } => { + __sats::bsatn::to_vec(&insert_one_i_128_reducer::InsertOneI128Args { n: n.clone() }) } - Reducer::InsertOneIdentity { i } => { - __sats::bsatn::to_vec(&insert_one_identity_reducer::InsertOneIdentityArgs { i: i.clone() }) + Reducer::InsertOneI256 { n } => { + __sats::bsatn::to_vec(&insert_one_i_256_reducer::InsertOneI256Args { n: n.clone() }) } - Reducer::InsertOneSimpleEnum { e } => { - __sats::bsatn::to_vec(&insert_one_simple_enum_reducer::InsertOneSimpleEnumArgs { e: e.clone() }) + Reducer::InsertOneBool { b } => { + __sats::bsatn::to_vec(&insert_one_bool_reducer::InsertOneBoolArgs { b: b.clone() }) + } + Reducer::InsertOneF32 { f } => { + __sats::bsatn::to_vec(&insert_one_f_32_reducer::InsertOneF32Args { f: f.clone() }) + } + Reducer::InsertOneF64 { f } => { + __sats::bsatn::to_vec(&insert_one_f_64_reducer::InsertOneF64Args { f: f.clone() }) } Reducer::InsertOneString { s } => { __sats::bsatn::to_vec(&insert_one_string_reducer::InsertOneStringArgs { s: s.clone() }) } - Reducer::InsertOneTimestamp { t } => { - __sats::bsatn::to_vec(&insert_one_timestamp_reducer::InsertOneTimestampArgs { t: t.clone() }) - } - Reducer::InsertOneU128 { n } => { - __sats::bsatn::to_vec(&insert_one_u_128_reducer::InsertOneU128Args { n: n.clone() }) + Reducer::InsertOneIdentity { i } => { + __sats::bsatn::to_vec(&insert_one_identity_reducer::InsertOneIdentityArgs { i: i.clone() }) } - Reducer::InsertOneU16 { n } => { - __sats::bsatn::to_vec(&insert_one_u_16_reducer::InsertOneU16Args { n: n.clone() }) + Reducer::InsertOneConnectionId { a } => { + __sats::bsatn::to_vec(&insert_one_connection_id_reducer::InsertOneConnectionIdArgs { a: a.clone() }) } - Reducer::InsertOneU256 { n } => { - __sats::bsatn::to_vec(&insert_one_u_256_reducer::InsertOneU256Args { n: n.clone() }) + Reducer::InsertOneUuid { u } => { + __sats::bsatn::to_vec(&insert_one_uuid_reducer::InsertOneUuidArgs { u: u.clone() }) } - Reducer::InsertOneU32 { n } => { - __sats::bsatn::to_vec(&insert_one_u_32_reducer::InsertOneU32Args { n: n.clone() }) + Reducer::InsertOneTimestamp { t } => { + __sats::bsatn::to_vec(&insert_one_timestamp_reducer::InsertOneTimestampArgs { t: t.clone() }) } - Reducer::InsertOneU64 { n } => { - __sats::bsatn::to_vec(&insert_one_u_64_reducer::InsertOneU64Args { n: n.clone() }) + Reducer::InsertOneSimpleEnum { e } => { + __sats::bsatn::to_vec(&insert_one_simple_enum_reducer::InsertOneSimpleEnumArgs { e: e.clone() }) } - Reducer::InsertOneU8 { n } => { - __sats::bsatn::to_vec(&insert_one_u_8_reducer::InsertOneU8Args { n: n.clone() }) + Reducer::InsertOneEnumWithPayload { e } => { + __sats::bsatn::to_vec(&insert_one_enum_with_payload_reducer::InsertOneEnumWithPayloadArgs { + e: e.clone(), + }) } Reducer::InsertOneUnitStruct { s } => { __sats::bsatn::to_vec(&insert_one_unit_struct_reducer::InsertOneUnitStructArgs { s: s.clone() }) } - Reducer::InsertOneUuid { u } => { - __sats::bsatn::to_vec(&insert_one_uuid_reducer::InsertOneUuidArgs { u: u.clone() }) + Reducer::InsertOneByteStruct { s } => { + __sats::bsatn::to_vec(&insert_one_byte_struct_reducer::InsertOneByteStructArgs { s: s.clone() }) } - Reducer::InsertOptionEveryPrimitiveStruct { s } => __sats::bsatn::to_vec( - &insert_option_every_primitive_struct_reducer::InsertOptionEveryPrimitiveStructArgs { s: s.clone() }, + Reducer::InsertOneEveryPrimitiveStruct { s } => __sats::bsatn::to_vec( + &insert_one_every_primitive_struct_reducer::InsertOneEveryPrimitiveStructArgs { s: s.clone() }, ), - Reducer::InsertOptionI32 { n } => { - __sats::bsatn::to_vec(&insert_option_i_32_reducer::InsertOptionI32Args { n: n.clone() }) + Reducer::InsertOneEveryVecStruct { s } => { + __sats::bsatn::to_vec(&insert_one_every_vec_struct_reducer::InsertOneEveryVecStructArgs { + s: s.clone(), + }) } - Reducer::InsertOptionIdentity { i } => { - __sats::bsatn::to_vec(&insert_option_identity_reducer::InsertOptionIdentityArgs { i: i.clone() }) + Reducer::InsertVecU8 { n } => { + __sats::bsatn::to_vec(&insert_vec_u_8_reducer::InsertVecU8Args { n: n.clone() }) } - Reducer::InsertOptionSimpleEnum { e } => { - __sats::bsatn::to_vec(&insert_option_simple_enum_reducer::InsertOptionSimpleEnumArgs { e: e.clone() }) + Reducer::InsertVecU16 { n } => { + __sats::bsatn::to_vec(&insert_vec_u_16_reducer::InsertVecU16Args { n: n.clone() }) } - Reducer::InsertOptionString { s } => { - __sats::bsatn::to_vec(&insert_option_string_reducer::InsertOptionStringArgs { s: s.clone() }) + Reducer::InsertVecU32 { n } => { + __sats::bsatn::to_vec(&insert_vec_u_32_reducer::InsertVecU32Args { n: n.clone() }) } - Reducer::InsertOptionUuid { u } => { - __sats::bsatn::to_vec(&insert_option_uuid_reducer::InsertOptionUuidArgs { u: u.clone() }) + Reducer::InsertVecU64 { n } => { + __sats::bsatn::to_vec(&insert_vec_u_64_reducer::InsertVecU64Args { n: n.clone() }) } - Reducer::InsertOptionVecOptionI32 { v } => { - __sats::bsatn::to_vec(&insert_option_vec_option_i_32_reducer::InsertOptionVecOptionI32Args { - v: v.clone(), - }) + Reducer::InsertVecU128 { n } => { + __sats::bsatn::to_vec(&insert_vec_u_128_reducer::InsertVecU128Args { n: n.clone() }) } - Reducer::InsertPkBool { b, data } => __sats::bsatn::to_vec(&insert_pk_bool_reducer::InsertPkBoolArgs { - b: b.clone(), - data: data.clone(), - }), - Reducer::InsertPkConnectionId { a, data } => { - __sats::bsatn::to_vec(&insert_pk_connection_id_reducer::InsertPkConnectionIdArgs { - a: a.clone(), - data: data.clone(), - }) + Reducer::InsertVecU256 { n } => { + __sats::bsatn::to_vec(&insert_vec_u_256_reducer::InsertVecU256Args { n: n.clone() }) } - Reducer::InsertPkI128 { n, data } => __sats::bsatn::to_vec(&insert_pk_i_128_reducer::InsertPkI128Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::InsertPkI16 { n, data } => __sats::bsatn::to_vec(&insert_pk_i_16_reducer::InsertPkI16Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::InsertPkI256 { n, data } => __sats::bsatn::to_vec(&insert_pk_i_256_reducer::InsertPkI256Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::InsertPkI32 { n, data } => __sats::bsatn::to_vec(&insert_pk_i_32_reducer::InsertPkI32Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::InsertPkI64 { n, data } => __sats::bsatn::to_vec(&insert_pk_i_64_reducer::InsertPkI64Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::InsertPkI8 { n, data } => __sats::bsatn::to_vec(&insert_pk_i_8_reducer::InsertPkI8Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::InsertPkIdentity { i, data } => { - __sats::bsatn::to_vec(&insert_pk_identity_reducer::InsertPkIdentityArgs { - i: i.clone(), - data: data.clone(), - }) + Reducer::InsertVecI8 { n } => { + __sats::bsatn::to_vec(&insert_vec_i_8_reducer::InsertVecI8Args { n: n.clone() }) } - Reducer::InsertPkSimpleEnum { a, data } => { - __sats::bsatn::to_vec(&insert_pk_simple_enum_reducer::InsertPkSimpleEnumArgs { - a: a.clone(), - data: data.clone(), - }) + Reducer::InsertVecI16 { n } => { + __sats::bsatn::to_vec(&insert_vec_i_16_reducer::InsertVecI16Args { n: n.clone() }) } - Reducer::InsertPkString { s, data } => { - __sats::bsatn::to_vec(&insert_pk_string_reducer::InsertPkStringArgs { - s: s.clone(), - data: data.clone(), - }) + Reducer::InsertVecI32 { n } => { + __sats::bsatn::to_vec(&insert_vec_i_32_reducer::InsertVecI32Args { n: n.clone() }) } - Reducer::InsertPkU128 { n, data } => __sats::bsatn::to_vec(&insert_pk_u_128_reducer::InsertPkU128Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::InsertPkU16 { n, data } => __sats::bsatn::to_vec(&insert_pk_u_16_reducer::InsertPkU16Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::InsertPkU256 { n, data } => __sats::bsatn::to_vec(&insert_pk_u_256_reducer::InsertPkU256Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::InsertPkU32 { n, data } => __sats::bsatn::to_vec(&insert_pk_u_32_reducer::InsertPkU32Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::InsertPkU32Two { n, data } => { - __sats::bsatn::to_vec(&insert_pk_u_32_two_reducer::InsertPkU32TwoArgs { - n: n.clone(), - data: data.clone(), + Reducer::InsertVecI64 { n } => { + __sats::bsatn::to_vec(&insert_vec_i_64_reducer::InsertVecI64Args { n: n.clone() }) + } + Reducer::InsertVecI128 { n } => { + __sats::bsatn::to_vec(&insert_vec_i_128_reducer::InsertVecI128Args { n: n.clone() }) + } + Reducer::InsertVecI256 { n } => { + __sats::bsatn::to_vec(&insert_vec_i_256_reducer::InsertVecI256Args { n: n.clone() }) + } + Reducer::InsertVecBool { b } => { + __sats::bsatn::to_vec(&insert_vec_bool_reducer::InsertVecBoolArgs { b: b.clone() }) + } + Reducer::InsertVecF32 { f } => { + __sats::bsatn::to_vec(&insert_vec_f_32_reducer::InsertVecF32Args { f: f.clone() }) + } + Reducer::InsertVecF64 { f } => { + __sats::bsatn::to_vec(&insert_vec_f_64_reducer::InsertVecF64Args { f: f.clone() }) + } + Reducer::InsertVecString { s } => { + __sats::bsatn::to_vec(&insert_vec_string_reducer::InsertVecStringArgs { s: s.clone() }) + } + Reducer::InsertVecIdentity { i } => { + __sats::bsatn::to_vec(&insert_vec_identity_reducer::InsertVecIdentityArgs { i: i.clone() }) + } + Reducer::InsertVecConnectionId { a } => { + __sats::bsatn::to_vec(&insert_vec_connection_id_reducer::InsertVecConnectionIdArgs { a: a.clone() }) + } + Reducer::InsertVecUuid { u } => { + __sats::bsatn::to_vec(&insert_vec_uuid_reducer::InsertVecUuidArgs { u: u.clone() }) + } + Reducer::InsertVecTimestamp { t } => { + __sats::bsatn::to_vec(&insert_vec_timestamp_reducer::InsertVecTimestampArgs { t: t.clone() }) + } + Reducer::InsertVecSimpleEnum { e } => { + __sats::bsatn::to_vec(&insert_vec_simple_enum_reducer::InsertVecSimpleEnumArgs { e: e.clone() }) + } + Reducer::InsertVecEnumWithPayload { e } => { + __sats::bsatn::to_vec(&insert_vec_enum_with_payload_reducer::InsertVecEnumWithPayloadArgs { + e: e.clone(), }) } - Reducer::InsertPkU64 { n, data } => __sats::bsatn::to_vec(&insert_pk_u_64_reducer::InsertPkU64Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::InsertPkU8 { n, data } => __sats::bsatn::to_vec(&insert_pk_u_8_reducer::InsertPkU8Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::InsertPkUuid { u, data } => __sats::bsatn::to_vec(&insert_pk_uuid_reducer::InsertPkUuidArgs { - u: u.clone(), - data: data.clone(), - }), - Reducer::InsertPrimitivesAsStrings { s } => { - __sats::bsatn::to_vec(&insert_primitives_as_strings_reducer::InsertPrimitivesAsStringsArgs { + Reducer::InsertVecUnitStruct { s } => { + __sats::bsatn::to_vec(&insert_vec_unit_struct_reducer::InsertVecUnitStructArgs { s: s.clone() }) + } + Reducer::InsertVecByteStruct { s } => { + __sats::bsatn::to_vec(&insert_vec_byte_struct_reducer::InsertVecByteStructArgs { s: s.clone() }) + } + Reducer::InsertVecEveryPrimitiveStruct { s } => __sats::bsatn::to_vec( + &insert_vec_every_primitive_struct_reducer::InsertVecEveryPrimitiveStructArgs { s: s.clone() }, + ), + Reducer::InsertVecEveryVecStruct { s } => { + __sats::bsatn::to_vec(&insert_vec_every_vec_struct_reducer::InsertVecEveryVecStructArgs { s: s.clone(), }) } - Reducer::InsertResultEveryPrimitiveStructString { r } => __sats::bsatn::to_vec( - &insert_result_every_primitive_struct_string_reducer::InsertResultEveryPrimitiveStructStringArgs { - r: r.clone(), - }, + Reducer::InsertOptionI32 { n } => { + __sats::bsatn::to_vec(&insert_option_i_32_reducer::InsertOptionI32Args { n: n.clone() }) + } + Reducer::InsertOptionString { s } => { + __sats::bsatn::to_vec(&insert_option_string_reducer::InsertOptionStringArgs { s: s.clone() }) + } + Reducer::InsertOptionIdentity { i } => { + __sats::bsatn::to_vec(&insert_option_identity_reducer::InsertOptionIdentityArgs { i: i.clone() }) + } + Reducer::InsertOptionUuid { u } => { + __sats::bsatn::to_vec(&insert_option_uuid_reducer::InsertOptionUuidArgs { u: u.clone() }) + } + Reducer::InsertOptionSimpleEnum { e } => { + __sats::bsatn::to_vec(&insert_option_simple_enum_reducer::InsertOptionSimpleEnumArgs { e: e.clone() }) + } + Reducer::InsertOptionEveryPrimitiveStruct { s } => __sats::bsatn::to_vec( + &insert_option_every_primitive_struct_reducer::InsertOptionEveryPrimitiveStructArgs { s: s.clone() }, ), + Reducer::InsertOptionVecOptionI32 { v } => { + __sats::bsatn::to_vec(&insert_option_vec_option_i_32_reducer::InsertOptionVecOptionI32Args { + v: v.clone(), + }) + } Reducer::InsertResultI32String { r } => { __sats::bsatn::to_vec(&insert_result_i_32_string_reducer::InsertResultI32StringArgs { r: r.clone() }) } + Reducer::InsertResultStringI32 { r } => { + __sats::bsatn::to_vec(&insert_result_string_i_32_reducer::InsertResultStringI32Args { r: r.clone() }) + } Reducer::InsertResultIdentityString { r } => { __sats::bsatn::to_vec(&insert_result_identity_string_reducer::InsertResultIdentityStringArgs { r: r.clone(), @@ -2235,237 +1985,347 @@ impl __sdk::Reducer for Reducer { r: r.clone(), }) } - Reducer::InsertResultStringI32 { r } => { - __sats::bsatn::to_vec(&insert_result_string_i_32_reducer::InsertResultStringI32Args { r: r.clone() }) - } + Reducer::InsertResultEveryPrimitiveStructString { r } => __sats::bsatn::to_vec( + &insert_result_every_primitive_struct_string_reducer::InsertResultEveryPrimitiveStructStringArgs { + r: r.clone(), + }, + ), Reducer::InsertResultVecI32String { r } => { __sats::bsatn::to_vec(&insert_result_vec_i_32_string_reducer::InsertResultVecI32StringArgs { r: r.clone(), }) } - Reducer::InsertTableHoldsTable { a, b } => { - __sats::bsatn::to_vec(&insert_table_holds_table_reducer::InsertTableHoldsTableArgs { - a: a.clone(), - b: b.clone(), + Reducer::InsertUniqueU8 { n, data } => { + __sats::bsatn::to_vec(&insert_unique_u_8_reducer::InsertUniqueU8Args { + n: n.clone(), + data: data.clone(), }) } - Reducer::InsertUniqueBool { b, data } => { - __sats::bsatn::to_vec(&insert_unique_bool_reducer::InsertUniqueBoolArgs { - b: b.clone(), + Reducer::UpdateUniqueU8 { n, data } => { + __sats::bsatn::to_vec(&update_unique_u_8_reducer::UpdateUniqueU8Args { + n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueConnectionId { a, data } => { - __sats::bsatn::to_vec(&insert_unique_connection_id_reducer::InsertUniqueConnectionIdArgs { - a: a.clone(), + Reducer::DeleteUniqueU8 { n } => { + __sats::bsatn::to_vec(&delete_unique_u_8_reducer::DeleteUniqueU8Args { n: n.clone() }) + } + Reducer::InsertUniqueU16 { n, data } => { + __sats::bsatn::to_vec(&insert_unique_u_16_reducer::InsertUniqueU16Args { + n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueI128 { n, data } => { - __sats::bsatn::to_vec(&insert_unique_i_128_reducer::InsertUniqueI128Args { + Reducer::UpdateUniqueU16 { n, data } => { + __sats::bsatn::to_vec(&update_unique_u_16_reducer::UpdateUniqueU16Args { n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueI16 { n, data } => { - __sats::bsatn::to_vec(&insert_unique_i_16_reducer::InsertUniqueI16Args { + Reducer::DeleteUniqueU16 { n } => { + __sats::bsatn::to_vec(&delete_unique_u_16_reducer::DeleteUniqueU16Args { n: n.clone() }) + } + Reducer::InsertUniqueU32 { n, data } => { + __sats::bsatn::to_vec(&insert_unique_u_32_reducer::InsertUniqueU32Args { n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueI256 { n, data } => { - __sats::bsatn::to_vec(&insert_unique_i_256_reducer::InsertUniqueI256Args { + Reducer::UpdateUniqueU32 { n, data } => { + __sats::bsatn::to_vec(&update_unique_u_32_reducer::UpdateUniqueU32Args { n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueI32 { n, data } => { - __sats::bsatn::to_vec(&insert_unique_i_32_reducer::InsertUniqueI32Args { + Reducer::DeleteUniqueU32 { n } => { + __sats::bsatn::to_vec(&delete_unique_u_32_reducer::DeleteUniqueU32Args { n: n.clone() }) + } + Reducer::InsertUniqueU64 { n, data } => { + __sats::bsatn::to_vec(&insert_unique_u_64_reducer::InsertUniqueU64Args { n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueI64 { n, data } => { - __sats::bsatn::to_vec(&insert_unique_i_64_reducer::InsertUniqueI64Args { + Reducer::UpdateUniqueU64 { n, data } => { + __sats::bsatn::to_vec(&update_unique_u_64_reducer::UpdateUniqueU64Args { n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueI8 { n, data } => { - __sats::bsatn::to_vec(&insert_unique_i_8_reducer::InsertUniqueI8Args { + Reducer::DeleteUniqueU64 { n } => { + __sats::bsatn::to_vec(&delete_unique_u_64_reducer::DeleteUniqueU64Args { n: n.clone() }) + } + Reducer::InsertUniqueU128 { n, data } => { + __sats::bsatn::to_vec(&insert_unique_u_128_reducer::InsertUniqueU128Args { n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueIdentity { i, data } => { - __sats::bsatn::to_vec(&insert_unique_identity_reducer::InsertUniqueIdentityArgs { - i: i.clone(), + Reducer::UpdateUniqueU128 { n, data } => { + __sats::bsatn::to_vec(&update_unique_u_128_reducer::UpdateUniqueU128Args { + n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueString { s, data } => { - __sats::bsatn::to_vec(&insert_unique_string_reducer::InsertUniqueStringArgs { - s: s.clone(), + Reducer::DeleteUniqueU128 { n } => { + __sats::bsatn::to_vec(&delete_unique_u_128_reducer::DeleteUniqueU128Args { n: n.clone() }) + } + Reducer::InsertUniqueU256 { n, data } => { + __sats::bsatn::to_vec(&insert_unique_u_256_reducer::InsertUniqueU256Args { + n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueU128 { n, data } => { - __sats::bsatn::to_vec(&insert_unique_u_128_reducer::InsertUniqueU128Args { + Reducer::UpdateUniqueU256 { n, data } => { + __sats::bsatn::to_vec(&update_unique_u_256_reducer::UpdateUniqueU256Args { n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueU16 { n, data } => { - __sats::bsatn::to_vec(&insert_unique_u_16_reducer::InsertUniqueU16Args { + Reducer::DeleteUniqueU256 { n } => { + __sats::bsatn::to_vec(&delete_unique_u_256_reducer::DeleteUniqueU256Args { n: n.clone() }) + } + Reducer::InsertUniqueI8 { n, data } => { + __sats::bsatn::to_vec(&insert_unique_i_8_reducer::InsertUniqueI8Args { n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueU256 { n, data } => { - __sats::bsatn::to_vec(&insert_unique_u_256_reducer::InsertUniqueU256Args { + Reducer::UpdateUniqueI8 { n, data } => { + __sats::bsatn::to_vec(&update_unique_i_8_reducer::UpdateUniqueI8Args { n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueU32 { n, data } => { - __sats::bsatn::to_vec(&insert_unique_u_32_reducer::InsertUniqueU32Args { + Reducer::DeleteUniqueI8 { n } => { + __sats::bsatn::to_vec(&delete_unique_i_8_reducer::DeleteUniqueI8Args { n: n.clone() }) + } + Reducer::InsertUniqueI16 { n, data } => { + __sats::bsatn::to_vec(&insert_unique_i_16_reducer::InsertUniqueI16Args { n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueU32UpdatePkU32 { n, d_unique, d_pk } => __sats::bsatn::to_vec( - &insert_unique_u_32_update_pk_u_32_reducer::InsertUniqueU32UpdatePkU32Args { - n: n.clone(), - d_unique: d_unique.clone(), - d_pk: d_pk.clone(), - }, - ), - Reducer::InsertUniqueU64 { n, data } => { - __sats::bsatn::to_vec(&insert_unique_u_64_reducer::InsertUniqueU64Args { + Reducer::UpdateUniqueI16 { n, data } => { + __sats::bsatn::to_vec(&update_unique_i_16_reducer::UpdateUniqueI16Args { n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueU8 { n, data } => { - __sats::bsatn::to_vec(&insert_unique_u_8_reducer::InsertUniqueU8Args { + Reducer::DeleteUniqueI16 { n } => { + __sats::bsatn::to_vec(&delete_unique_i_16_reducer::DeleteUniqueI16Args { n: n.clone() }) + } + Reducer::InsertUniqueI32 { n, data } => { + __sats::bsatn::to_vec(&insert_unique_i_32_reducer::InsertUniqueI32Args { n: n.clone(), data: data.clone(), }) } - Reducer::InsertUniqueUuid { u, data } => { - __sats::bsatn::to_vec(&insert_unique_uuid_reducer::InsertUniqueUuidArgs { - u: u.clone(), + Reducer::UpdateUniqueI32 { n, data } => { + __sats::bsatn::to_vec(&update_unique_i_32_reducer::UpdateUniqueI32Args { + n: n.clone(), data: data.clone(), }) } - Reducer::InsertUser { name, identity } => __sats::bsatn::to_vec(&insert_user_reducer::InsertUserArgs { - name: name.clone(), - identity: identity.clone(), - }), - Reducer::InsertVecBool { b } => { - __sats::bsatn::to_vec(&insert_vec_bool_reducer::InsertVecBoolArgs { b: b.clone() }) + Reducer::DeleteUniqueI32 { n } => { + __sats::bsatn::to_vec(&delete_unique_i_32_reducer::DeleteUniqueI32Args { n: n.clone() }) } - Reducer::InsertVecByteStruct { s } => { - __sats::bsatn::to_vec(&insert_vec_byte_struct_reducer::InsertVecByteStructArgs { s: s.clone() }) + Reducer::InsertUniqueI64 { n, data } => { + __sats::bsatn::to_vec(&insert_unique_i_64_reducer::InsertUniqueI64Args { + n: n.clone(), + data: data.clone(), + }) } - Reducer::InsertVecConnectionId { a } => { - __sats::bsatn::to_vec(&insert_vec_connection_id_reducer::InsertVecConnectionIdArgs { a: a.clone() }) + Reducer::UpdateUniqueI64 { n, data } => { + __sats::bsatn::to_vec(&update_unique_i_64_reducer::UpdateUniqueI64Args { + n: n.clone(), + data: data.clone(), + }) } - Reducer::InsertVecEnumWithPayload { e } => { - __sats::bsatn::to_vec(&insert_vec_enum_with_payload_reducer::InsertVecEnumWithPayloadArgs { - e: e.clone(), + Reducer::DeleteUniqueI64 { n } => { + __sats::bsatn::to_vec(&delete_unique_i_64_reducer::DeleteUniqueI64Args { n: n.clone() }) + } + Reducer::InsertUniqueI128 { n, data } => { + __sats::bsatn::to_vec(&insert_unique_i_128_reducer::InsertUniqueI128Args { + n: n.clone(), + data: data.clone(), }) } - Reducer::InsertVecEveryPrimitiveStruct { s } => __sats::bsatn::to_vec( - &insert_vec_every_primitive_struct_reducer::InsertVecEveryPrimitiveStructArgs { s: s.clone() }, - ), - Reducer::InsertVecEveryVecStruct { s } => { - __sats::bsatn::to_vec(&insert_vec_every_vec_struct_reducer::InsertVecEveryVecStructArgs { - s: s.clone(), + Reducer::UpdateUniqueI128 { n, data } => { + __sats::bsatn::to_vec(&update_unique_i_128_reducer::UpdateUniqueI128Args { + n: n.clone(), + data: data.clone(), }) } - Reducer::InsertVecF32 { f } => { - __sats::bsatn::to_vec(&insert_vec_f_32_reducer::InsertVecF32Args { f: f.clone() }) + Reducer::DeleteUniqueI128 { n } => { + __sats::bsatn::to_vec(&delete_unique_i_128_reducer::DeleteUniqueI128Args { n: n.clone() }) } - Reducer::InsertVecF64 { f } => { - __sats::bsatn::to_vec(&insert_vec_f_64_reducer::InsertVecF64Args { f: f.clone() }) + Reducer::InsertUniqueI256 { n, data } => { + __sats::bsatn::to_vec(&insert_unique_i_256_reducer::InsertUniqueI256Args { + n: n.clone(), + data: data.clone(), + }) } - Reducer::InsertVecI128 { n } => { - __sats::bsatn::to_vec(&insert_vec_i_128_reducer::InsertVecI128Args { n: n.clone() }) + Reducer::UpdateUniqueI256 { n, data } => { + __sats::bsatn::to_vec(&update_unique_i_256_reducer::UpdateUniqueI256Args { + n: n.clone(), + data: data.clone(), + }) } - Reducer::InsertVecI16 { n } => { - __sats::bsatn::to_vec(&insert_vec_i_16_reducer::InsertVecI16Args { n: n.clone() }) + Reducer::DeleteUniqueI256 { n } => { + __sats::bsatn::to_vec(&delete_unique_i_256_reducer::DeleteUniqueI256Args { n: n.clone() }) } - Reducer::InsertVecI256 { n } => { - __sats::bsatn::to_vec(&insert_vec_i_256_reducer::InsertVecI256Args { n: n.clone() }) + Reducer::InsertUniqueBool { b, data } => { + __sats::bsatn::to_vec(&insert_unique_bool_reducer::InsertUniqueBoolArgs { + b: b.clone(), + data: data.clone(), + }) + } + Reducer::UpdateUniqueBool { b, data } => { + __sats::bsatn::to_vec(&update_unique_bool_reducer::UpdateUniqueBoolArgs { + b: b.clone(), + data: data.clone(), + }) } - Reducer::InsertVecI32 { n } => { - __sats::bsatn::to_vec(&insert_vec_i_32_reducer::InsertVecI32Args { n: n.clone() }) + Reducer::DeleteUniqueBool { b } => { + __sats::bsatn::to_vec(&delete_unique_bool_reducer::DeleteUniqueBoolArgs { b: b.clone() }) } - Reducer::InsertVecI64 { n } => { - __sats::bsatn::to_vec(&insert_vec_i_64_reducer::InsertVecI64Args { n: n.clone() }) + Reducer::InsertUniqueString { s, data } => { + __sats::bsatn::to_vec(&insert_unique_string_reducer::InsertUniqueStringArgs { + s: s.clone(), + data: data.clone(), + }) } - Reducer::InsertVecI8 { n } => { - __sats::bsatn::to_vec(&insert_vec_i_8_reducer::InsertVecI8Args { n: n.clone() }) + Reducer::UpdateUniqueString { s, data } => { + __sats::bsatn::to_vec(&update_unique_string_reducer::UpdateUniqueStringArgs { + s: s.clone(), + data: data.clone(), + }) } - Reducer::InsertVecIdentity { i } => { - __sats::bsatn::to_vec(&insert_vec_identity_reducer::InsertVecIdentityArgs { i: i.clone() }) + Reducer::DeleteUniqueString { s } => { + __sats::bsatn::to_vec(&delete_unique_string_reducer::DeleteUniqueStringArgs { s: s.clone() }) } - Reducer::InsertVecSimpleEnum { e } => { - __sats::bsatn::to_vec(&insert_vec_simple_enum_reducer::InsertVecSimpleEnumArgs { e: e.clone() }) + Reducer::InsertUniqueIdentity { i, data } => { + __sats::bsatn::to_vec(&insert_unique_identity_reducer::InsertUniqueIdentityArgs { + i: i.clone(), + data: data.clone(), + }) } - Reducer::InsertVecString { s } => { - __sats::bsatn::to_vec(&insert_vec_string_reducer::InsertVecStringArgs { s: s.clone() }) + Reducer::UpdateUniqueIdentity { i, data } => { + __sats::bsatn::to_vec(&update_unique_identity_reducer::UpdateUniqueIdentityArgs { + i: i.clone(), + data: data.clone(), + }) } - Reducer::InsertVecTimestamp { t } => { - __sats::bsatn::to_vec(&insert_vec_timestamp_reducer::InsertVecTimestampArgs { t: t.clone() }) + Reducer::DeleteUniqueIdentity { i } => { + __sats::bsatn::to_vec(&delete_unique_identity_reducer::DeleteUniqueIdentityArgs { i: i.clone() }) } - Reducer::InsertVecU128 { n } => { - __sats::bsatn::to_vec(&insert_vec_u_128_reducer::InsertVecU128Args { n: n.clone() }) + Reducer::InsertUniqueConnectionId { a, data } => { + __sats::bsatn::to_vec(&insert_unique_connection_id_reducer::InsertUniqueConnectionIdArgs { + a: a.clone(), + data: data.clone(), + }) } - Reducer::InsertVecU16 { n } => { - __sats::bsatn::to_vec(&insert_vec_u_16_reducer::InsertVecU16Args { n: n.clone() }) + Reducer::UpdateUniqueConnectionId { a, data } => { + __sats::bsatn::to_vec(&update_unique_connection_id_reducer::UpdateUniqueConnectionIdArgs { + a: a.clone(), + data: data.clone(), + }) } - Reducer::InsertVecU256 { n } => { - __sats::bsatn::to_vec(&insert_vec_u_256_reducer::InsertVecU256Args { n: n.clone() }) + Reducer::DeleteUniqueConnectionId { a } => { + __sats::bsatn::to_vec(&delete_unique_connection_id_reducer::DeleteUniqueConnectionIdArgs { + a: a.clone(), + }) } - Reducer::InsertVecU32 { n } => { - __sats::bsatn::to_vec(&insert_vec_u_32_reducer::InsertVecU32Args { n: n.clone() }) + Reducer::InsertUniqueUuid { u, data } => { + __sats::bsatn::to_vec(&insert_unique_uuid_reducer::InsertUniqueUuidArgs { + u: u.clone(), + data: data.clone(), + }) } - Reducer::InsertVecU64 { n } => { - __sats::bsatn::to_vec(&insert_vec_u_64_reducer::InsertVecU64Args { n: n.clone() }) + Reducer::UpdateUniqueUuid { u, data } => { + __sats::bsatn::to_vec(&update_unique_uuid_reducer::UpdateUniqueUuidArgs { + u: u.clone(), + data: data.clone(), + }) } - Reducer::InsertVecU8 { n } => { - __sats::bsatn::to_vec(&insert_vec_u_8_reducer::InsertVecU8Args { n: n.clone() }) + Reducer::DeleteUniqueUuid { u } => { + __sats::bsatn::to_vec(&delete_unique_uuid_reducer::DeleteUniqueUuidArgs { u: u.clone() }) } - Reducer::InsertVecUnitStruct { s } => { - __sats::bsatn::to_vec(&insert_vec_unit_struct_reducer::InsertVecUnitStructArgs { s: s.clone() }) + Reducer::InsertPkU8 { n, data } => __sats::bsatn::to_vec(&insert_pk_u_8_reducer::InsertPkU8Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::UpdatePkU8 { n, data } => __sats::bsatn::to_vec(&update_pk_u_8_reducer::UpdatePkU8Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::DeletePkU8 { n } => __sats::bsatn::to_vec(&delete_pk_u_8_reducer::DeletePkU8Args { n: n.clone() }), + Reducer::InsertPkU16 { n, data } => __sats::bsatn::to_vec(&insert_pk_u_16_reducer::InsertPkU16Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::UpdatePkU16 { n, data } => __sats::bsatn::to_vec(&update_pk_u_16_reducer::UpdatePkU16Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::DeletePkU16 { n } => { + __sats::bsatn::to_vec(&delete_pk_u_16_reducer::DeletePkU16Args { n: n.clone() }) } - Reducer::InsertVecUuid { u } => { - __sats::bsatn::to_vec(&insert_vec_uuid_reducer::InsertVecUuidArgs { u: u.clone() }) + Reducer::InsertPkU32 { n, data } => __sats::bsatn::to_vec(&insert_pk_u_32_reducer::InsertPkU32Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::UpdatePkU32 { n, data } => __sats::bsatn::to_vec(&update_pk_u_32_reducer::UpdatePkU32Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::DeletePkU32 { n } => { + __sats::bsatn::to_vec(&delete_pk_u_32_reducer::DeletePkU32Args { n: n.clone() }) } - Reducer::NoOpSucceeds => __sats::bsatn::to_vec(&no_op_succeeds_reducer::NoOpSucceedsArgs {}), - Reducer::SendScheduledMessage { arg } => { - __sats::bsatn::to_vec(&send_scheduled_message_reducer::SendScheduledMessageArgs { arg: arg.clone() }) + Reducer::InsertPkU64 { n, data } => __sats::bsatn::to_vec(&insert_pk_u_64_reducer::InsertPkU64Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::UpdatePkU64 { n, data } => __sats::bsatn::to_vec(&update_pk_u_64_reducer::UpdatePkU64Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::DeletePkU64 { n } => { + __sats::bsatn::to_vec(&delete_pk_u_64_reducer::DeletePkU64Args { n: n.clone() }) } - Reducer::SortedUuidsInsert => __sats::bsatn::to_vec(&sorted_uuids_insert_reducer::SortedUuidsInsertArgs {}), - Reducer::UpdateIndexedSimpleEnum { a, b } => { - __sats::bsatn::to_vec(&update_indexed_simple_enum_reducer::UpdateIndexedSimpleEnumArgs { - a: a.clone(), - b: b.clone(), - }) + Reducer::InsertPkU128 { n, data } => __sats::bsatn::to_vec(&insert_pk_u_128_reducer::InsertPkU128Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::UpdatePkU128 { n, data } => __sats::bsatn::to_vec(&update_pk_u_128_reducer::UpdatePkU128Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::DeletePkU128 { n } => { + __sats::bsatn::to_vec(&delete_pk_u_128_reducer::DeletePkU128Args { n: n.clone() }) } - Reducer::UpdatePkBool { b, data } => __sats::bsatn::to_vec(&update_pk_bool_reducer::UpdatePkBoolArgs { - b: b.clone(), + Reducer::InsertPkU256 { n, data } => __sats::bsatn::to_vec(&insert_pk_u_256_reducer::InsertPkU256Args { + n: n.clone(), data: data.clone(), }), - Reducer::UpdatePkConnectionId { a, data } => { - __sats::bsatn::to_vec(&update_pk_connection_id_reducer::UpdatePkConnectionIdArgs { - a: a.clone(), - data: data.clone(), - }) + Reducer::UpdatePkU256 { n, data } => __sats::bsatn::to_vec(&update_pk_u_256_reducer::UpdatePkU256Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::DeletePkU256 { n } => { + __sats::bsatn::to_vec(&delete_pk_u_256_reducer::DeletePkU256Args { n: n.clone() }) } - Reducer::UpdatePkI128 { n, data } => __sats::bsatn::to_vec(&update_pk_i_128_reducer::UpdatePkI128Args { + Reducer::InsertPkI8 { n, data } => __sats::bsatn::to_vec(&insert_pk_i_8_reducer::InsertPkI8Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::UpdatePkI8 { n, data } => __sats::bsatn::to_vec(&update_pk_i_8_reducer::UpdatePkI8Args { + n: n.clone(), + data: data.clone(), + }), + Reducer::DeletePkI8 { n } => __sats::bsatn::to_vec(&delete_pk_i_8_reducer::DeletePkI8Args { n: n.clone() }), + Reducer::InsertPkI16 { n, data } => __sats::bsatn::to_vec(&insert_pk_i_16_reducer::InsertPkI16Args { n: n.clone(), data: data.clone(), }), @@ -2473,7 +2333,10 @@ impl __sdk::Reducer for Reducer { n: n.clone(), data: data.clone(), }), - Reducer::UpdatePkI256 { n, data } => __sats::bsatn::to_vec(&update_pk_i_256_reducer::UpdatePkI256Args { + Reducer::DeletePkI16 { n } => { + __sats::bsatn::to_vec(&delete_pk_i_16_reducer::DeletePkI16Args { n: n.clone() }) + } + Reducer::InsertPkI32 { n, data } => __sats::bsatn::to_vec(&insert_pk_i_32_reducer::InsertPkI32Args { n: n.clone(), data: data.clone(), }), @@ -2481,168 +2344,317 @@ impl __sdk::Reducer for Reducer { n: n.clone(), data: data.clone(), }), - Reducer::UpdatePkI64 { n, data } => __sats::bsatn::to_vec(&update_pk_i_64_reducer::UpdatePkI64Args { + Reducer::DeletePkI32 { n } => { + __sats::bsatn::to_vec(&delete_pk_i_32_reducer::DeletePkI32Args { n: n.clone() }) + } + Reducer::InsertPkI64 { n, data } => __sats::bsatn::to_vec(&insert_pk_i_64_reducer::InsertPkI64Args { n: n.clone(), data: data.clone(), }), - Reducer::UpdatePkI8 { n, data } => __sats::bsatn::to_vec(&update_pk_i_8_reducer::UpdatePkI8Args { + Reducer::UpdatePkI64 { n, data } => __sats::bsatn::to_vec(&update_pk_i_64_reducer::UpdatePkI64Args { n: n.clone(), data: data.clone(), }), - Reducer::UpdatePkIdentity { i, data } => { - __sats::bsatn::to_vec(&update_pk_identity_reducer::UpdatePkIdentityArgs { - i: i.clone(), - data: data.clone(), - }) - } - Reducer::UpdatePkSimpleEnum { a, data } => { - __sats::bsatn::to_vec(&update_pk_simple_enum_reducer::UpdatePkSimpleEnumArgs { - a: a.clone(), - data: data.clone(), - }) - } - Reducer::UpdatePkString { s, data } => { - __sats::bsatn::to_vec(&update_pk_string_reducer::UpdatePkStringArgs { - s: s.clone(), - data: data.clone(), - }) + Reducer::DeletePkI64 { n } => { + __sats::bsatn::to_vec(&delete_pk_i_64_reducer::DeletePkI64Args { n: n.clone() }) } - Reducer::UpdatePkU128 { n, data } => __sats::bsatn::to_vec(&update_pk_u_128_reducer::UpdatePkU128Args { + Reducer::InsertPkI128 { n, data } => __sats::bsatn::to_vec(&insert_pk_i_128_reducer::InsertPkI128Args { n: n.clone(), data: data.clone(), }), - Reducer::UpdatePkU16 { n, data } => __sats::bsatn::to_vec(&update_pk_u_16_reducer::UpdatePkU16Args { + Reducer::UpdatePkI128 { n, data } => __sats::bsatn::to_vec(&update_pk_i_128_reducer::UpdatePkI128Args { n: n.clone(), data: data.clone(), }), - Reducer::UpdatePkU256 { n, data } => __sats::bsatn::to_vec(&update_pk_u_256_reducer::UpdatePkU256Args { + Reducer::DeletePkI128 { n } => { + __sats::bsatn::to_vec(&delete_pk_i_128_reducer::DeletePkI128Args { n: n.clone() }) + } + Reducer::InsertPkI256 { n, data } => __sats::bsatn::to_vec(&insert_pk_i_256_reducer::InsertPkI256Args { n: n.clone(), data: data.clone(), }), - Reducer::UpdatePkU32 { n, data } => __sats::bsatn::to_vec(&update_pk_u_32_reducer::UpdatePkU32Args { + Reducer::UpdatePkI256 { n, data } => __sats::bsatn::to_vec(&update_pk_i_256_reducer::UpdatePkI256Args { n: n.clone(), data: data.clone(), }), - Reducer::UpdatePkU32Two { n, data } => { - __sats::bsatn::to_vec(&update_pk_u_32_two_reducer::UpdatePkU32TwoArgs { - n: n.clone(), + Reducer::DeletePkI256 { n } => { + __sats::bsatn::to_vec(&delete_pk_i_256_reducer::DeletePkI256Args { n: n.clone() }) + } + Reducer::InsertPkBool { b, data } => __sats::bsatn::to_vec(&insert_pk_bool_reducer::InsertPkBoolArgs { + b: b.clone(), + data: data.clone(), + }), + Reducer::UpdatePkBool { b, data } => __sats::bsatn::to_vec(&update_pk_bool_reducer::UpdatePkBoolArgs { + b: b.clone(), + data: data.clone(), + }), + Reducer::DeletePkBool { b } => { + __sats::bsatn::to_vec(&delete_pk_bool_reducer::DeletePkBoolArgs { b: b.clone() }) + } + Reducer::InsertPkString { s, data } => { + __sats::bsatn::to_vec(&insert_pk_string_reducer::InsertPkStringArgs { + s: s.clone(), + data: data.clone(), + }) + } + Reducer::UpdatePkString { s, data } => { + __sats::bsatn::to_vec(&update_pk_string_reducer::UpdatePkStringArgs { + s: s.clone(), + data: data.clone(), + }) + } + Reducer::DeletePkString { s } => { + __sats::bsatn::to_vec(&delete_pk_string_reducer::DeletePkStringArgs { s: s.clone() }) + } + Reducer::InsertPkIdentity { i, data } => { + __sats::bsatn::to_vec(&insert_pk_identity_reducer::InsertPkIdentityArgs { + i: i.clone(), + data: data.clone(), + }) + } + Reducer::UpdatePkIdentity { i, data } => { + __sats::bsatn::to_vec(&update_pk_identity_reducer::UpdatePkIdentityArgs { + i: i.clone(), + data: data.clone(), + }) + } + Reducer::DeletePkIdentity { i } => { + __sats::bsatn::to_vec(&delete_pk_identity_reducer::DeletePkIdentityArgs { i: i.clone() }) + } + Reducer::InsertPkConnectionId { a, data } => { + __sats::bsatn::to_vec(&insert_pk_connection_id_reducer::InsertPkConnectionIdArgs { + a: a.clone(), data: data.clone(), }) } - Reducer::UpdatePkU64 { n, data } => __sats::bsatn::to_vec(&update_pk_u_64_reducer::UpdatePkU64Args { - n: n.clone(), - data: data.clone(), - }), - Reducer::UpdatePkU8 { n, data } => __sats::bsatn::to_vec(&update_pk_u_8_reducer::UpdatePkU8Args { - n: n.clone(), + Reducer::UpdatePkConnectionId { a, data } => { + __sats::bsatn::to_vec(&update_pk_connection_id_reducer::UpdatePkConnectionIdArgs { + a: a.clone(), + data: data.clone(), + }) + } + Reducer::DeletePkConnectionId { a } => { + __sats::bsatn::to_vec(&delete_pk_connection_id_reducer::DeletePkConnectionIdArgs { a: a.clone() }) + } + Reducer::InsertPkUuid { u, data } => __sats::bsatn::to_vec(&insert_pk_uuid_reducer::InsertPkUuidArgs { + u: u.clone(), data: data.clone(), }), Reducer::UpdatePkUuid { u, data } => __sats::bsatn::to_vec(&update_pk_uuid_reducer::UpdatePkUuidArgs { u: u.clone(), data: data.clone(), }), - Reducer::UpdateUniqueBool { b, data } => { - __sats::bsatn::to_vec(&update_unique_bool_reducer::UpdateUniqueBoolArgs { - b: b.clone(), - data: data.clone(), - }) + Reducer::DeletePkUuid { u } => { + __sats::bsatn::to_vec(&delete_pk_uuid_reducer::DeletePkUuidArgs { u: u.clone() }) } - Reducer::UpdateUniqueConnectionId { a, data } => { - __sats::bsatn::to_vec(&update_unique_connection_id_reducer::UpdateUniqueConnectionIdArgs { + Reducer::InsertPkSimpleEnum { a, data } => { + __sats::bsatn::to_vec(&insert_pk_simple_enum_reducer::InsertPkSimpleEnumArgs { a: a.clone(), data: data.clone(), }) } - Reducer::UpdateUniqueI128 { n, data } => { - __sats::bsatn::to_vec(&update_unique_i_128_reducer::UpdateUniqueI128Args { + Reducer::InsertPkU32Two { n, data } => { + __sats::bsatn::to_vec(&insert_pk_u_32_two_reducer::InsertPkU32TwoArgs { n: n.clone(), data: data.clone(), }) } - Reducer::UpdateUniqueI16 { n, data } => { - __sats::bsatn::to_vec(&update_unique_i_16_reducer::UpdateUniqueI16Args { + Reducer::UpdatePkU32Two { n, data } => { + __sats::bsatn::to_vec(&update_pk_u_32_two_reducer::UpdatePkU32TwoArgs { n: n.clone(), data: data.clone(), }) } - Reducer::UpdateUniqueI256 { n, data } => { - __sats::bsatn::to_vec(&update_unique_i_256_reducer::UpdateUniqueI256Args { - n: n.clone(), - data: data.clone(), - }) + Reducer::DeletePkU32Two { n } => { + __sats::bsatn::to_vec(&delete_pk_u_32_two_reducer::DeletePkU32TwoArgs { n: n.clone() }) } - Reducer::UpdateUniqueI32 { n, data } => { - __sats::bsatn::to_vec(&update_unique_i_32_reducer::UpdateUniqueI32Args { - n: n.clone(), + Reducer::UpdatePkSimpleEnum { a, data } => { + __sats::bsatn::to_vec(&update_pk_simple_enum_reducer::UpdatePkSimpleEnumArgs { + a: a.clone(), data: data.clone(), }) } - Reducer::UpdateUniqueI64 { n, data } => { - __sats::bsatn::to_vec(&update_unique_i_64_reducer::UpdateUniqueI64Args { - n: n.clone(), - data: data.clone(), + Reducer::InsertLargeTable { + a, + b, + c, + d, + e, + f, + g, + h, + i, + j, + k, + l, + m, + n, + o, + p, + q, + r, + s, + t, + u, + v, + } => __sats::bsatn::to_vec(&insert_large_table_reducer::InsertLargeTableArgs { + a: a.clone(), + b: b.clone(), + c: c.clone(), + d: d.clone(), + e: e.clone(), + f: f.clone(), + g: g.clone(), + h: h.clone(), + i: i.clone(), + j: j.clone(), + k: k.clone(), + l: l.clone(), + m: m.clone(), + n: n.clone(), + o: o.clone(), + p: p.clone(), + q: q.clone(), + r: r.clone(), + s: s.clone(), + t: t.clone(), + u: u.clone(), + v: v.clone(), + }), + Reducer::DeleteLargeTable { + a, + b, + c, + d, + e, + f, + g, + h, + i, + j, + k, + l, + m, + n, + o, + p, + q, + r, + s, + t, + u, + v, + } => __sats::bsatn::to_vec(&delete_large_table_reducer::DeleteLargeTableArgs { + a: a.clone(), + b: b.clone(), + c: c.clone(), + d: d.clone(), + e: e.clone(), + f: f.clone(), + g: g.clone(), + h: h.clone(), + i: i.clone(), + j: j.clone(), + k: k.clone(), + l: l.clone(), + m: m.clone(), + n: n.clone(), + o: o.clone(), + p: p.clone(), + q: q.clone(), + r: r.clone(), + s: s.clone(), + t: t.clone(), + u: u.clone(), + v: v.clone(), + }), + Reducer::InsertTableHoldsTable { a, b } => { + __sats::bsatn::to_vec(&insert_table_holds_table_reducer::InsertTableHoldsTableArgs { + a: a.clone(), + b: b.clone(), }) } - Reducer::UpdateUniqueI8 { n, data } => { - __sats::bsatn::to_vec(&update_unique_i_8_reducer::UpdateUniqueI8Args { - n: n.clone(), - data: data.clone(), - }) + Reducer::InsertIntoBtreeU32 { rows } => { + __sats::bsatn::to_vec(&insert_into_btree_u_32_reducer::InsertIntoBtreeU32Args { rows: rows.clone() }) } - Reducer::UpdateUniqueIdentity { i, data } => { - __sats::bsatn::to_vec(&update_unique_identity_reducer::UpdateUniqueIdentityArgs { - i: i.clone(), - data: data.clone(), - }) + Reducer::DeleteFromBtreeU32 { rows } => { + __sats::bsatn::to_vec(&delete_from_btree_u_32_reducer::DeleteFromBtreeU32Args { rows: rows.clone() }) } - Reducer::UpdateUniqueString { s, data } => { - __sats::bsatn::to_vec(&update_unique_string_reducer::UpdateUniqueStringArgs { - s: s.clone(), - data: data.clone(), + Reducer::InsertIntoPkBtreeU32 { pk_u_32, bt_u_32 } => { + __sats::bsatn::to_vec(&insert_into_pk_btree_u_32_reducer::InsertIntoPkBtreeU32Args { + pk_u_32: pk_u_32.clone(), + bt_u_32: bt_u_32.clone(), }) } - Reducer::UpdateUniqueU128 { n, data } => { - __sats::bsatn::to_vec(&update_unique_u_128_reducer::UpdateUniqueU128Args { + Reducer::InsertUniqueU32UpdatePkU32 { n, d_unique, d_pk } => __sats::bsatn::to_vec( + &insert_unique_u_32_update_pk_u_32_reducer::InsertUniqueU32UpdatePkU32Args { n: n.clone(), - data: data.clone(), - }) - } - Reducer::UpdateUniqueU16 { n, data } => { - __sats::bsatn::to_vec(&update_unique_u_16_reducer::UpdateUniqueU16Args { + d_unique: d_unique.clone(), + d_pk: d_pk.clone(), + }, + ), + Reducer::DeletePkU32InsertPkU32Two { n, data } => __sats::bsatn::to_vec( + &delete_pk_u_32_insert_pk_u_32_two_reducer::DeletePkU32InsertPkU32TwoArgs { n: n.clone(), data: data.clone(), - }) + }, + ), + Reducer::InsertCallerOneIdentity => { + __sats::bsatn::to_vec(&insert_caller_one_identity_reducer::InsertCallerOneIdentityArgs {}) } - Reducer::UpdateUniqueU256 { n, data } => { - __sats::bsatn::to_vec(&update_unique_u_256_reducer::UpdateUniqueU256Args { - n: n.clone(), - data: data.clone(), - }) + Reducer::InsertCallerVecIdentity => { + __sats::bsatn::to_vec(&insert_caller_vec_identity_reducer::InsertCallerVecIdentityArgs {}) } - Reducer::UpdateUniqueU32 { n, data } => { - __sats::bsatn::to_vec(&update_unique_u_32_reducer::UpdateUniqueU32Args { - n: n.clone(), + Reducer::InsertCallerUniqueIdentity { data } => { + __sats::bsatn::to_vec(&insert_caller_unique_identity_reducer::InsertCallerUniqueIdentityArgs { data: data.clone(), }) } - Reducer::UpdateUniqueU64 { n, data } => { - __sats::bsatn::to_vec(&update_unique_u_64_reducer::UpdateUniqueU64Args { - n: n.clone(), + Reducer::InsertCallerPkIdentity { data } => { + __sats::bsatn::to_vec(&insert_caller_pk_identity_reducer::InsertCallerPkIdentityArgs { data: data.clone(), }) } - Reducer::UpdateUniqueU8 { n, data } => { - __sats::bsatn::to_vec(&update_unique_u_8_reducer::UpdateUniqueU8Args { - n: n.clone(), - data: data.clone(), + Reducer::InsertCallerOneConnectionId => { + __sats::bsatn::to_vec(&insert_caller_one_connection_id_reducer::InsertCallerOneConnectionIdArgs {}) + } + Reducer::InsertCallerVecConnectionId => { + __sats::bsatn::to_vec(&insert_caller_vec_connection_id_reducer::InsertCallerVecConnectionIdArgs {}) + } + Reducer::InsertCallerUniqueConnectionId { data } => __sats::bsatn::to_vec( + &insert_caller_unique_connection_id_reducer::InsertCallerUniqueConnectionIdArgs { data: data.clone() }, + ), + Reducer::InsertCallerPkConnectionId { data } => __sats::bsatn::to_vec( + &insert_caller_pk_connection_id_reducer::InsertCallerPkConnectionIdArgs { data: data.clone() }, + ), + Reducer::InsertCallTimestamp => { + __sats::bsatn::to_vec(&insert_call_timestamp_reducer::InsertCallTimestampArgs {}) + } + Reducer::InsertCallUuidV4 => __sats::bsatn::to_vec(&insert_call_uuid_v_4_reducer::InsertCallUuidV4Args {}), + Reducer::InsertCallUuidV7 => __sats::bsatn::to_vec(&insert_call_uuid_v_7_reducer::InsertCallUuidV7Args {}), + Reducer::InsertPrimitivesAsStrings { s } => { + __sats::bsatn::to_vec(&insert_primitives_as_strings_reducer::InsertPrimitivesAsStringsArgs { + s: s.clone(), }) } - Reducer::UpdateUniqueUuid { u, data } => { - __sats::bsatn::to_vec(&update_unique_uuid_reducer::UpdateUniqueUuidArgs { - u: u.clone(), - data: data.clone(), + Reducer::NoOpSucceeds => __sats::bsatn::to_vec(&no_op_succeeds_reducer::NoOpSucceedsArgs {}), + Reducer::SendScheduledMessage { arg } => { + __sats::bsatn::to_vec(&send_scheduled_message_reducer::SendScheduledMessageArgs { arg: arg.clone() }) + } + Reducer::InsertUser { name, identity } => __sats::bsatn::to_vec(&insert_user_reducer::InsertUserArgs { + name: name.clone(), + identity: identity.clone(), + }), + Reducer::InsertIntoIndexedSimpleEnum { n } => __sats::bsatn::to_vec( + &insert_into_indexed_simple_enum_reducer::InsertIntoIndexedSimpleEnumArgs { n: n.clone() }, + ), + Reducer::UpdateIndexedSimpleEnum { a, b } => { + __sats::bsatn::to_vec(&update_indexed_simple_enum_reducer::UpdateIndexedSimpleEnumArgs { + a: a.clone(), + b: b.clone(), }) } + Reducer::SortedUuidsInsert => __sats::bsatn::to_vec(&sorted_uuids_insert_reducer::SortedUuidsInsertArgs {}), _ => unreachable!(), } } diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_every_primitive_struct_string_table.rs b/sdks/rust/tests/test-client/src/module_bindings/result_every_primitive_struct_string_table.rs index 2536cbb8b0f..28a09cf809b 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/result_every_primitive_struct_string_table.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/result_every_primitive_struct_string_table.rs @@ -2,8 +2,8 @@ // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. #![allow(unused, clippy::all)] -use super::every_primitive_struct_type::EveryPrimitiveStruct; use super::result_every_primitive_struct_string_type::ResultEveryPrimitiveStructString; +use super::result_every_primitive_struct_string_value_type::ResultEveryPrimitiveStructStringValue; use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; /// Table handle for the table `result_every_primitive_struct_string`. diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_every_primitive_struct_string_type.rs b/sdks/rust/tests/test-client/src/module_bindings/result_every_primitive_struct_string_type.rs index 92228597561..fcb8e1269e4 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/result_every_primitive_struct_string_type.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/result_every_primitive_struct_string_type.rs @@ -4,12 +4,12 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; -use super::every_primitive_struct_type::EveryPrimitiveStruct; +use super::result_every_primitive_struct_string_value_type::ResultEveryPrimitiveStructStringValue; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct ResultEveryPrimitiveStructString { - pub r: Result, + pub r: ResultEveryPrimitiveStructStringValue, } impl __sdk::InModule for ResultEveryPrimitiveStructString { @@ -20,7 +20,7 @@ impl __sdk::InModule for ResultEveryPrimitiveStructString { /// /// Provides typed access to columns for query building. pub struct ResultEveryPrimitiveStructStringCols { - pub r: __sdk::__query_builder::Col>, + pub r: __sdk::__query_builder::Col, } impl __sdk::__query_builder::HasCols for ResultEveryPrimitiveStructString { diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_every_primitive_struct_string_value_type.rs b/sdks/rust/tests/test-client/src/module_bindings/result_every_primitive_struct_string_value_type.rs new file mode 100644 index 00000000000..38e7f61779f --- /dev/null +++ b/sdks/rust/tests/test-client/src/module_bindings/result_every_primitive_struct_string_value_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::every_primitive_struct_type::EveryPrimitiveStruct; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub enum ResultEveryPrimitiveStructStringValue { + Ok(EveryPrimitiveStruct), + + Err(String), +} + +impl __sdk::InModule for ResultEveryPrimitiveStructStringValue { + type Module = super::RemoteModule; +} diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_i_32_string_table.rs b/sdks/rust/tests/test-client/src/module_bindings/result_i_32_string_table.rs index 991122618d2..850854aaf6b 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/result_i_32_string_table.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/result_i_32_string_table.rs @@ -3,6 +3,7 @@ #![allow(unused, clippy::all)] use super::result_i_32_string_type::ResultI32String; +use super::result_i_32_string_value_type::ResultI32StringValue; use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; /// Table handle for the table `result_i_32_string`. diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_i_32_string_type.rs b/sdks/rust/tests/test-client/src/module_bindings/result_i_32_string_type.rs index dde3d94ca4a..9eca971c6a0 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/result_i_32_string_type.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/result_i_32_string_type.rs @@ -4,10 +4,12 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::result_i_32_string_value_type::ResultI32StringValue; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct ResultI32String { - pub r: Result, + pub r: ResultI32StringValue, } impl __sdk::InModule for ResultI32String { @@ -18,7 +20,7 @@ impl __sdk::InModule for ResultI32String { /// /// Provides typed access to columns for query building. pub struct ResultI32StringCols { - pub r: __sdk::__query_builder::Col>, + pub r: __sdk::__query_builder::Col, } impl __sdk::__query_builder::HasCols for ResultI32String { diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_i_32_string_value_type.rs b/sdks/rust/tests/test-client/src/module_bindings/result_i_32_string_value_type.rs new file mode 100644 index 00000000000..725e93108c1 --- /dev/null +++ b/sdks/rust/tests/test-client/src/module_bindings/result_i_32_string_value_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub enum ResultI32StringValue { + Ok(i32), + + Err(String), +} + +impl __sdk::InModule for ResultI32StringValue { + type Module = super::RemoteModule; +} diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_identity_string_table.rs b/sdks/rust/tests/test-client/src/module_bindings/result_identity_string_table.rs index 34d9f6e4cee..eb47e869449 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/result_identity_string_table.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/result_identity_string_table.rs @@ -3,6 +3,7 @@ #![allow(unused, clippy::all)] use super::result_identity_string_type::ResultIdentityString; +use super::result_identity_string_value_type::ResultIdentityStringValue; use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; /// Table handle for the table `result_identity_string`. diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_identity_string_type.rs b/sdks/rust/tests/test-client/src/module_bindings/result_identity_string_type.rs index 1cdb485977e..9f9087fb27f 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/result_identity_string_type.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/result_identity_string_type.rs @@ -4,10 +4,12 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::result_identity_string_value_type::ResultIdentityStringValue; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct ResultIdentityString { - pub r: Result<__sdk::Identity, String>, + pub r: ResultIdentityStringValue, } impl __sdk::InModule for ResultIdentityString { @@ -18,7 +20,7 @@ impl __sdk::InModule for ResultIdentityString { /// /// Provides typed access to columns for query building. pub struct ResultIdentityStringCols { - pub r: __sdk::__query_builder::Col>, + pub r: __sdk::__query_builder::Col, } impl __sdk::__query_builder::HasCols for ResultIdentityString { diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_identity_string_value_type.rs b/sdks/rust/tests/test-client/src/module_bindings/result_identity_string_value_type.rs new file mode 100644 index 00000000000..a203f95bbea --- /dev/null +++ b/sdks/rust/tests/test-client/src/module_bindings/result_identity_string_value_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub enum ResultIdentityStringValue { + Ok(__sdk::Identity), + + Err(String), +} + +impl __sdk::InModule for ResultIdentityStringValue { + type Module = super::RemoteModule; +} diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_simple_enum_i_32_table.rs b/sdks/rust/tests/test-client/src/module_bindings/result_simple_enum_i_32_table.rs index b35acf45ecc..c83c4cdfc00 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/result_simple_enum_i_32_table.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/result_simple_enum_i_32_table.rs @@ -3,7 +3,7 @@ #![allow(unused, clippy::all)] use super::result_simple_enum_i_32_type::ResultSimpleEnumI32; -use super::simple_enum_type::SimpleEnum; +use super::result_simple_enum_i_32_value_type::ResultSimpleEnumI32Value; use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; /// Table handle for the table `result_simple_enum_i_32`. diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_simple_enum_i_32_type.rs b/sdks/rust/tests/test-client/src/module_bindings/result_simple_enum_i_32_type.rs index e89806e4a7e..cab5496cafe 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/result_simple_enum_i_32_type.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/result_simple_enum_i_32_type.rs @@ -4,12 +4,12 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; -use super::simple_enum_type::SimpleEnum; +use super::result_simple_enum_i_32_value_type::ResultSimpleEnumI32Value; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct ResultSimpleEnumI32 { - pub r: Result, + pub r: ResultSimpleEnumI32Value, } impl __sdk::InModule for ResultSimpleEnumI32 { @@ -20,7 +20,7 @@ impl __sdk::InModule for ResultSimpleEnumI32 { /// /// Provides typed access to columns for query building. pub struct ResultSimpleEnumI32Cols { - pub r: __sdk::__query_builder::Col>, + pub r: __sdk::__query_builder::Col, } impl __sdk::__query_builder::HasCols for ResultSimpleEnumI32 { diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_simple_enum_i_32_value_type.rs b/sdks/rust/tests/test-client/src/module_bindings/result_simple_enum_i_32_value_type.rs new file mode 100644 index 00000000000..aa436953831 --- /dev/null +++ b/sdks/rust/tests/test-client/src/module_bindings/result_simple_enum_i_32_value_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::simple_enum_type::SimpleEnum; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub enum ResultSimpleEnumI32Value { + Ok(SimpleEnum), + + Err(i32), +} + +impl __sdk::InModule for ResultSimpleEnumI32Value { + type Module = super::RemoteModule; +} diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_string_i_32_table.rs b/sdks/rust/tests/test-client/src/module_bindings/result_string_i_32_table.rs index 5d611cd1068..90a64bdfdd9 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/result_string_i_32_table.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/result_string_i_32_table.rs @@ -3,6 +3,7 @@ #![allow(unused, clippy::all)] use super::result_string_i_32_type::ResultStringI32; +use super::result_string_i_32_value_type::ResultStringI32Value; use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; /// Table handle for the table `result_string_i_32`. diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_string_i_32_type.rs b/sdks/rust/tests/test-client/src/module_bindings/result_string_i_32_type.rs index ab52954cf94..722990054bd 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/result_string_i_32_type.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/result_string_i_32_type.rs @@ -4,10 +4,12 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::result_string_i_32_value_type::ResultStringI32Value; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct ResultStringI32 { - pub r: Result, + pub r: ResultStringI32Value, } impl __sdk::InModule for ResultStringI32 { @@ -18,7 +20,7 @@ impl __sdk::InModule for ResultStringI32 { /// /// Provides typed access to columns for query building. pub struct ResultStringI32Cols { - pub r: __sdk::__query_builder::Col>, + pub r: __sdk::__query_builder::Col, } impl __sdk::__query_builder::HasCols for ResultStringI32 { diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_string_i_32_value_type.rs b/sdks/rust/tests/test-client/src/module_bindings/result_string_i_32_value_type.rs new file mode 100644 index 00000000000..255a9626680 --- /dev/null +++ b/sdks/rust/tests/test-client/src/module_bindings/result_string_i_32_value_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub enum ResultStringI32Value { + Ok(String), + + Err(i32), +} + +impl __sdk::InModule for ResultStringI32Value { + type Module = super::RemoteModule; +} diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_vec_i_32_string_table.rs b/sdks/rust/tests/test-client/src/module_bindings/result_vec_i_32_string_table.rs index afd2fea744b..056a16ad7e4 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/result_vec_i_32_string_table.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/result_vec_i_32_string_table.rs @@ -3,6 +3,7 @@ #![allow(unused, clippy::all)] use super::result_vec_i_32_string_type::ResultVecI32String; +use super::result_vec_i_32_string_value_type::ResultVecI32StringValue; use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; /// Table handle for the table `result_vec_i_32_string`. diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_vec_i_32_string_type.rs b/sdks/rust/tests/test-client/src/module_bindings/result_vec_i_32_string_type.rs index 66ce7a2d84d..a7482cd437e 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/result_vec_i_32_string_type.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/result_vec_i_32_string_type.rs @@ -4,10 +4,12 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::result_vec_i_32_string_value_type::ResultVecI32StringValue; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct ResultVecI32String { - pub r: Result, String>, + pub r: ResultVecI32StringValue, } impl __sdk::InModule for ResultVecI32String { @@ -18,7 +20,7 @@ impl __sdk::InModule for ResultVecI32String { /// /// Provides typed access to columns for query building. pub struct ResultVecI32StringCols { - pub r: __sdk::__query_builder::Col, String>>, + pub r: __sdk::__query_builder::Col, } impl __sdk::__query_builder::HasCols for ResultVecI32String { diff --git a/sdks/rust/tests/test-client/src/module_bindings/result_vec_i_32_string_value_type.rs b/sdks/rust/tests/test-client/src/module_bindings/result_vec_i_32_string_value_type.rs new file mode 100644 index 00000000000..de7e61e5aaf --- /dev/null +++ b/sdks/rust/tests/test-client/src/module_bindings/result_vec_i_32_string_value_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub enum ResultVecI32StringValue { + Ok(Vec), + + Err(String), +} + +impl __sdk::InModule for ResultVecI32StringValue { + type Module = super::RemoteModule; +} diff --git a/sdks/rust/tests/test.rs b/sdks/rust/tests/test.rs index d6c5cac38ae..2340c7a17fb 100644 --- a/sdks/rust/tests/test.rs +++ b/sdks/rust/tests/test.rs @@ -315,45 +315,53 @@ declare_tests_with_suffix!(typescript, "-ts"); // TODO: migrate csharp to snake_case table names declare_tests_with_suffix!(csharp, "-cs"); declare_tests_with_suffix!(cpp, "-cpp"); +declare_tests_with_suffix!(go, "-go"); -/// Tests of event table functionality, using <./event-table-client> and <../../../modules/sdk-test>. +/// Tests of event table functionality, using <./event-table-client> and <../../../modules/sdk-test-event-table>. /// /// These are separate from the existing client because as of writing (2026-02-07), /// we do not have event table support in all of the module languages we have tested. -mod event_table_tests { - use spacetimedb_testing::sdk::Test; - - const MODULE: &str = "sdk-test-event-table"; - const CLIENT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/event-table-client"); - - fn make_test(subcommand: &str) -> Test { - Test::builder() - .with_name(subcommand) - .with_module(MODULE) - .with_client(CLIENT) - .with_language("rust") - .with_bindings_dir("src/module_bindings") - .with_compile_command("cargo build") - .with_run_command(format!("cargo run -- {}", subcommand)) - .build() - } - - #[test] - fn event_table() { - make_test("event-table").run(); - } - - #[test] - fn multiple_events() { - make_test("multiple-events").run(); - } - - #[test] - fn events_dont_persist() { - make_test("events-dont-persist").run(); - } +macro_rules! event_table_tests { + ($mod_name:ident, $suffix:literal) => { + mod $mod_name { + use spacetimedb_testing::sdk::Test; + + const MODULE: &str = concat!("sdk-test-event-table", $suffix); + const CLIENT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/event-table-client"); + + fn make_test(subcommand: &str) -> Test { + Test::builder() + .with_name(subcommand) + .with_module(MODULE) + .with_client(CLIENT) + .with_language("rust") + .with_bindings_dir("src/module_bindings") + .with_compile_command("cargo build") + .with_run_command(format!("cargo run -- {}", subcommand)) + .build() + } + + #[test] + fn event_table() { + make_test("event-table").run(); + } + + #[test] + fn multiple_events() { + make_test("multiple-events").run(); + } + + #[test] + fn events_dont_persist() { + make_test("events-dont-persist").run(); + } + } + }; } +event_table_tests!(event_table_tests, ""); +event_table_tests!(go_event_table_tests, "-go"); + macro_rules! procedure_tests { ($mod_name:ident, $suffix:literal) => { mod $mod_name { @@ -426,6 +434,7 @@ macro_rules! procedure_tests { procedure_tests!(rust_procedures, ""); procedure_tests!(typescript_procedures, "-ts"); procedure_tests!(cpp_procedures, "-cpp"); +procedure_tests!(go_procedures, "-go"); macro_rules! view_tests { ($mod_name:ident, $suffix:literal) => { @@ -488,3 +497,4 @@ macro_rules! view_tests { view_tests!(rust_view, ""); view_tests!(cpp_view, "-cpp"); +view_tests!(go_view, "-go"); diff --git a/sdks/rust/tests/view-client/src/module_bindings/mod.rs b/sdks/rust/tests/view-client/src/module_bindings/mod.rs index 525e09f98b9..6791f7b1876 100644 --- a/sdks/rust/tests/view-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/view-client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.0.0 (commit 85095cfa85e3addc29ce58bfe670b6003271b288). +// This was generated using spacetimedb cli version 2.0.3 (commit abbcec4ab357b956f6a2d498a666c1516edcb355). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; @@ -44,8 +44,8 @@ pub use players_at_level_0_table::*; /// to indicate which reducer caused the event. pub enum Reducer { - DeletePlayer { identity: __sdk::Identity }, InsertPlayer { identity: __sdk::Identity, level: u64 }, + DeletePlayer { identity: __sdk::Identity }, MovePlayer { dx: i32, dy: i32 }, } @@ -56,8 +56,8 @@ impl __sdk::InModule for Reducer { impl __sdk::Reducer for Reducer { fn reducer_name(&self) -> &'static str { match self { - Reducer::DeletePlayer { .. } => "delete_player", Reducer::InsertPlayer { .. } => "insert_player", + Reducer::DeletePlayer { .. } => "delete_player", Reducer::MovePlayer { .. } => "move_player", _ => unreachable!(), } @@ -65,15 +65,15 @@ impl __sdk::Reducer for Reducer { #[allow(clippy::clone_on_copy)] fn args_bsatn(&self) -> Result, __sats::bsatn::EncodeError> { match self { - Reducer::DeletePlayer { identity } => __sats::bsatn::to_vec(&delete_player_reducer::DeletePlayerArgs { - identity: identity.clone(), - }), Reducer::InsertPlayer { identity, level } => { __sats::bsatn::to_vec(&insert_player_reducer::InsertPlayerArgs { identity: identity.clone(), level: level.clone(), }) } + Reducer::DeletePlayer { identity } => __sats::bsatn::to_vec(&delete_player_reducer::DeletePlayerArgs { + identity: identity.clone(), + }), Reducer::MovePlayer { dx, dy } => __sats::bsatn::to_vec(&move_player_reducer::MovePlayerArgs { dx: dx.clone(), dy: dy.clone(), diff --git a/templates/basic-go/.gitignore b/templates/basic-go/.gitignore new file mode 100644 index 00000000000..2db37afbf01 --- /dev/null +++ b/templates/basic-go/.gitignore @@ -0,0 +1,2 @@ +basic-go-client +spacetimedb/module.wasm diff --git a/templates/basic-go/.template.json b/templates/basic-go/.template.json new file mode 100644 index 00000000000..fd4d46493ae --- /dev/null +++ b/templates/basic-go/.template.json @@ -0,0 +1,5 @@ +{ + "description": "A basic Go client and server template with only stubs for code", + "client_lang": "go", + "server_lang": "go" +} diff --git a/templates/basic-go/go.mod b/templates/basic-go/go.mod new file mode 100644 index 00000000000..05946a00363 --- /dev/null +++ b/templates/basic-go/go.mod @@ -0,0 +1,13 @@ +module example.com/my-spacetimedb-client + +go 1.23 + +require github.com/clockworklabs/SpacetimeDB/sdks/go v0.0.0 + +require ( + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/coder/websocket v1.8.12 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect +) + +replace github.com/clockworklabs/SpacetimeDB/sdks/go => ../../sdks/go diff --git a/templates/basic-go/go.sum b/templates/basic-go/go.sum new file mode 100644 index 00000000000..a5e37786dc6 --- /dev/null +++ b/templates/basic-go/go.sum @@ -0,0 +1,16 @@ +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/templates/basic-go/main.go b/templates/basic-go/main.go new file mode 100644 index 00000000000..9fe21bb796c --- /dev/null +++ b/templates/basic-go/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/client" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// Person matches the server-side Person table. +type Person struct { + Name string +} + +// personTableDef implements cache.TableDef for the Person table. +type personTableDef struct{} + +func (d *personTableDef) TableName() string { return "person" } + +func (d *personTableDef) DecodeRow(r bsatn.Reader) (any, error) { + name, err := r.GetString() + if err != nil { + return nil, err + } + return &Person{Name: name}, nil +} + +func (d *personTableDef) EncodeRow(row any) []byte { + p := row.(*Person) + w := bsatn.NewWriter(32) + w.PutString(p.Name) + return w.Bytes() +} + +func main() { + host := envOr("SPACETIMEDB_HOST", "http://localhost:3000") + dbName := envOr("SPACETIMEDB_DB_NAME", "my-db") + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + conn, err := client.NewDbConnection(). + WithUri(host). + WithDatabaseName(dbName). + OnConnect(func(conn client.DbConnection, identity types.Identity, token string) { + fmt.Println("Connected to SpacetimeDB") + }). + OnConnectError(func(err error) { + fmt.Fprintf(os.Stderr, "Connection error: %v\n", err) + os.Exit(1) + }). + Build(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to connect: %v\n", err) + os.Exit(1) + } + + conn.RegisterTable(&personTableDef{}) + + // Register callback for new person inserts + personCache := conn.Cache().GetTable("person") + personCache.OnInsert(func(row any) { + p := row.(*Person) + fmt.Printf("New person: %s\n", p.Name) + }) + + // Subscribe to the person table + conn.Subscribe("SELECT * FROM person"). + OnApplied(func() { + fmt.Println("Subscribed to the person table") + }). + Build() + + // Block until interrupted + if err := conn.Run(ctx); err != nil { + fmt.Fprintf(os.Stderr, "Connection error: %v\n", err) + } +} + +func envOr(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} diff --git a/templates/basic-go/spacetimedb/go.mod b/templates/basic-go/spacetimedb/go.mod new file mode 100644 index 00000000000..38ac909f178 --- /dev/null +++ b/templates/basic-go/spacetimedb/go.mod @@ -0,0 +1,7 @@ +module example.com/my-spacetimedb-module + +go 1.24 + +require github.com/clockworklabs/SpacetimeDB/sdks/go v0.0.0 + +replace github.com/clockworklabs/SpacetimeDB/sdks/go => ../../../sdks/go diff --git a/templates/basic-go/spacetimedb/go.sum b/templates/basic-go/spacetimedb/go.sum new file mode 100644 index 00000000000..67458fab477 --- /dev/null +++ b/templates/basic-go/spacetimedb/go.sum @@ -0,0 +1,8 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/templates/basic-go/spacetimedb/main.go b/templates/basic-go/spacetimedb/main.go new file mode 100644 index 00000000000..e4bb606a3cd --- /dev/null +++ b/templates/basic-go/spacetimedb/main.go @@ -0,0 +1,5 @@ +package main + +//go:generate stdb-gen + +func main() {} diff --git a/templates/basic-go/spacetimedb/reducers.go b/templates/basic-go/spacetimedb/reducers.go new file mode 100644 index 00000000000..aaa97d5e406 --- /dev/null +++ b/templates/basic-go/spacetimedb/reducers.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/server" +) + +var logger = server.NewLogger("my-module") + +//stdb:init +func initReducer(ctx server.ReducerContext) { + // Called when the module is initially published +} + +//stdb:connect +func identityConnected(ctx server.ReducerContext) { + // Called every time a new client connects +} + +//stdb:disconnect +func identityDisconnected(ctx server.ReducerContext) { + // Called every time a client disconnects +} + +//stdb:reducer +func add(ctx server.ReducerContext, name string) { + PersonTable.Insert(Person{Name: name}) +} + +//stdb:reducer name=say_hello +func sayHello(ctx server.ReducerContext) { + iter, err := PersonTable.Scan() + if err != nil { + panic(fmt.Sprintf("Scan error: %v", err)) + } + defer iter.Close() + for person, ok := iter.Next(); ok; person, ok = iter.Next() { + logger.Info(fmt.Sprintf("Hello, %s!", person.Name)) + } + logger.Info("Hello, World!") +} diff --git a/templates/basic-go/spacetimedb/types.go b/templates/basic-go/spacetimedb/types.go new file mode 100644 index 00000000000..32b65cc8634 --- /dev/null +++ b/templates/basic-go/spacetimedb/types.go @@ -0,0 +1,6 @@ +package main + +//stdb:table name=person access=public +type Person struct { + Name string +} diff --git a/templates/chat-console-go/.gitignore b/templates/chat-console-go/.gitignore new file mode 100644 index 00000000000..835598d9d8b --- /dev/null +++ b/templates/chat-console-go/.gitignore @@ -0,0 +1,2 @@ +chat-console-go-client +spacetimedb/module.wasm diff --git a/templates/chat-console-go/.template.json b/templates/chat-console-go/.template.json new file mode 100644 index 00000000000..91481ab7eb0 --- /dev/null +++ b/templates/chat-console-go/.template.json @@ -0,0 +1,5 @@ +{ + "description": "Go server/client implementing quickstart chat", + "client_lang": "go", + "server_lang": "go" +} diff --git a/templates/chat-console-go/go.mod b/templates/chat-console-go/go.mod new file mode 100644 index 00000000000..05946a00363 --- /dev/null +++ b/templates/chat-console-go/go.mod @@ -0,0 +1,13 @@ +module example.com/my-spacetimedb-client + +go 1.23 + +require github.com/clockworklabs/SpacetimeDB/sdks/go v0.0.0 + +require ( + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/coder/websocket v1.8.12 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect +) + +replace github.com/clockworklabs/SpacetimeDB/sdks/go => ../../sdks/go diff --git a/templates/chat-console-go/go.sum b/templates/chat-console-go/go.sum new file mode 100644 index 00000000000..a5e37786dc6 --- /dev/null +++ b/templates/chat-console-go/go.sum @@ -0,0 +1,16 @@ +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/templates/chat-console-go/main.go b/templates/chat-console-go/main.go new file mode 100644 index 00000000000..8079322664c --- /dev/null +++ b/templates/chat-console-go/main.go @@ -0,0 +1,278 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "os" + "os/signal" + "sort" + "strings" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/bsatn" + "github.com/clockworklabs/SpacetimeDB/sdks/go/client" + "github.com/clockworklabs/SpacetimeDB/sdks/go/client/cache" + "github.com/clockworklabs/SpacetimeDB/sdks/go/types" +) + +// --------------------------------------------------------------------------- +// Table types matching server schema +// --------------------------------------------------------------------------- + +// User matches the server-side User table. +type User struct { + Identity types.Identity + Name *string + Online bool +} + +// Message matches the server-side Message table. +type Message struct { + Sender types.Identity + Sent types.Timestamp + Text string +} + +// --------------------------------------------------------------------------- +// bsatn.Serializable helper for string reducer args +// --------------------------------------------------------------------------- + +type bsatnString string + +func (s bsatnString) WriteBsatn(w bsatn.Writer) { + w.PutString(string(s)) +} + +// --------------------------------------------------------------------------- +// TableDef implementations (for client cache) +// --------------------------------------------------------------------------- + +type userTableDef struct{} + +func (d *userTableDef) TableName() string { return "user" } + +func (d *userTableDef) DecodeRow(r bsatn.Reader) (any, error) { + identity, err := types.ReadIdentity(r) + if err != nil { + return nil, err + } + name, err := bsatn.ReadOption(r, func(r bsatn.Reader) (string, error) { + return r.GetString() + }) + if err != nil { + return nil, err + } + online, err := r.GetBool() + if err != nil { + return nil, err + } + return &User{Identity: identity, Name: name, Online: online}, nil +} + +func (d *userTableDef) EncodeRow(row any) []byte { + u := row.(*User) + w := bsatn.NewWriter(64) + u.Identity.WriteBsatn(w) + if u.Name != nil { + w.PutSumTag(0) + w.PutString(*u.Name) + } else { + w.PutSumTag(1) + } + w.PutBool(u.Online) + return w.Bytes() +} + +// PrimaryKey implements cache.TableDefWithPK, enabling OnUpdate detection. +func (d *userTableDef) PrimaryKey(row any) any { + return row.(*User).Identity.String() +} + +type messageTableDef struct{} + +func (d *messageTableDef) TableName() string { return "message" } + +func (d *messageTableDef) DecodeRow(r bsatn.Reader) (any, error) { + sender, err := types.ReadIdentity(r) + if err != nil { + return nil, err + } + sent, err := types.ReadTimestamp(r) + if err != nil { + return nil, err + } + text, err := r.GetString() + if err != nil { + return nil, err + } + return &Message{Sender: sender, Sent: sent, Text: text}, nil +} + +func (d *messageTableDef) EncodeRow(row any) []byte { + m := row.(*Message) + w := bsatn.NewWriter(128) + m.Sender.WriteBsatn(w) + m.Sent.WriteBsatn(w) + w.PutString(m.Text) + return w.Bytes() +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +func userNameOrIdentity(u *User) string { + if u.Name != nil { + return *u.Name + } + s := u.Identity.String() + if len(s) > 8 { + return s[:8] + } + return s +} + +// findUserByIdentity looks up a User in the cache by Identity. +func findUserByIdentity(tc cache.TableCache, id types.Identity) *User { + var found *User + tc.Iter(func(row any) bool { + u := row.(*User) + if u.Identity.String() == id.String() { + found = u + return false + } + return true + }) + return found +} + +func envOr(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +func main() { + host := envOr("SPACETIMEDB_HOST", "http://localhost:3000") + dbName := envOr("SPACETIMEDB_DB_NAME", "quickstart-chat") + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + conn, err := client.NewDbConnection(). + WithUri(host). + WithDatabaseName(dbName). + OnConnect(func(conn client.DbConnection, identity types.Identity, token string) { + fmt.Println("Connected to SpacetimeDB") + }). + OnConnectError(func(err error) { + fmt.Fprintf(os.Stderr, "Connection error: %v\n", err) + os.Exit(1) + }). + OnDisconnect(func(conn client.DbConnection, err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Disconnected: %v\n", err) + os.Exit(1) + } + fmt.Println("Disconnected.") + os.Exit(0) + }). + Build(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to connect: %v\n", err) + os.Exit(1) + } + + // Register table definitions + conn.RegisterTable(&userTableDef{}) + conn.RegisterTable(&messageTableDef{}) + + // Register callbacks + userCache := conn.Cache().GetTable("user") + messageCache := conn.Cache().GetTable("message") + + // When a new user joins, print a notification. + userCache.OnInsert(func(row any) { + u := row.(*User) + if u.Online { + fmt.Printf("User %s connected.\n", userNameOrIdentity(u)) + } + }) + + // When a user's status changes, print a notification. + userCache.OnUpdate(func(oldRow any, newRow any) { + old := oldRow.(*User) + new_ := newRow.(*User) + if (old.Name == nil) != (new_.Name == nil) || (old.Name != nil && new_.Name != nil && *old.Name != *new_.Name) { + fmt.Printf("User %s renamed to %s.\n", userNameOrIdentity(old), userNameOrIdentity(new_)) + } + if old.Online && !new_.Online { + fmt.Printf("User %s disconnected.\n", userNameOrIdentity(new_)) + } + if !old.Online && new_.Online { + fmt.Printf("User %s connected.\n", userNameOrIdentity(new_)) + } + }) + + // When a new message is received, print it. + messageCache.OnInsert(func(row any) { + m := row.(*Message) + senderName := "unknown" + if u := findUserByIdentity(userCache, m.Sender); u != nil { + senderName = userNameOrIdentity(u) + } + fmt.Printf("%s: %s\n", senderName, m.Text) + }) + + // Subscribe to both tables + conn.Subscribe("SELECT * FROM user", "SELECT * FROM message"). + OnApplied(func() { + // Print past messages sorted by timestamp + var messages []*Message + messageCache.Iter(func(row any) bool { + messages = append(messages, row.(*Message)) + return true + }) + sort.Slice(messages, func(i, j int) bool { + return messages[i].Sent.Microseconds() < messages[j].Sent.Microseconds() + }) + for _, m := range messages { + senderName := "unknown" + if u := findUserByIdentity(userCache, m.Sender); u != nil { + senderName = userNameOrIdentity(u) + } + fmt.Printf("%s: %s\n", senderName, m.Text) + } + fmt.Println("Fully connected and all subscriptions applied.") + fmt.Println("Use /name to set your name, or type a message!") + }). + Build() + + // Start the connection event loop in a goroutine + go func() { + if err := conn.Run(ctx); err != nil { + fmt.Fprintf(os.Stderr, "Connection error: %v\n", err) + } + cancel() + }() + + // Handle user input from stdin + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + line := scanner.Text() + if name, ok := strings.CutPrefix(line, "/name "); ok { + if err := conn.CallReducer("set_name", bsatnString(name)); err != nil { + fmt.Fprintf(os.Stderr, "Failed to set name: %v\n", err) + } + } else { + if err := conn.CallReducer("send_message", bsatnString(line)); err != nil { + fmt.Fprintf(os.Stderr, "Failed to send message: %v\n", err) + } + } + } +} diff --git a/templates/chat-console-go/spacetimedb/go.mod b/templates/chat-console-go/spacetimedb/go.mod new file mode 100644 index 00000000000..38ac909f178 --- /dev/null +++ b/templates/chat-console-go/spacetimedb/go.mod @@ -0,0 +1,7 @@ +module example.com/my-spacetimedb-module + +go 1.24 + +require github.com/clockworklabs/SpacetimeDB/sdks/go v0.0.0 + +replace github.com/clockworklabs/SpacetimeDB/sdks/go => ../../../sdks/go diff --git a/templates/chat-console-go/spacetimedb/go.sum b/templates/chat-console-go/spacetimedb/go.sum new file mode 100644 index 00000000000..67458fab477 --- /dev/null +++ b/templates/chat-console-go/spacetimedb/go.sum @@ -0,0 +1,8 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/templates/chat-console-go/spacetimedb/main.go b/templates/chat-console-go/spacetimedb/main.go new file mode 100644 index 00000000000..e4bb606a3cd --- /dev/null +++ b/templates/chat-console-go/spacetimedb/main.go @@ -0,0 +1,5 @@ +package main + +//go:generate stdb-gen + +func main() {} diff --git a/templates/chat-console-go/spacetimedb/reducers.go b/templates/chat-console-go/spacetimedb/reducers.go new file mode 100644 index 00000000000..3f228ec8821 --- /dev/null +++ b/templates/chat-console-go/spacetimedb/reducers.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + + "github.com/clockworklabs/SpacetimeDB/sdks/go/server" +) + +var logger = server.NewLogger("quickstart-chat") + +//stdb:init +func initReducer(ctx server.ReducerContext) { + // Called when the module is initially published +} + +//stdb:connect +func identityConnected(ctx server.ReducerContext) { + user, found, err := UserTable.FindByIdentity(ctx.Sender()) + if err != nil { + panic(fmt.Sprintf("FindBy error: %v", err)) + } + if found { + user.Online = true + UserTable.UpdateByIdentity(user) + } else { + UserTable.Insert(User{Identity: ctx.Sender(), Name: nil, Online: true}) + } +} + +//stdb:disconnect +func identityDisconnected(ctx server.ReducerContext) { + user, found, err := UserTable.FindByIdentity(ctx.Sender()) + if err != nil { + panic(fmt.Sprintf("FindBy error: %v", err)) + } + if found { + user.Online = false + UserTable.UpdateByIdentity(user) + } else { + logger.Warn(fmt.Sprintf("Disconnect event for unknown user with identity %v", ctx.Sender())) + } +} + +//stdb:reducer +func setName(ctx server.ReducerContext, name string) error { + if name == "" { + return fmt.Errorf("Names must not be empty") + } + user, found, err := UserTable.FindByIdentity(ctx.Sender()) + if err != nil { + return fmt.Errorf("FindBy error: %w", err) + } + if !found { + return fmt.Errorf("Cannot set name for unknown user") + } + logger.Info(fmt.Sprintf("User %v sets name to %s", ctx.Sender(), name)) + user.Name = &name + UserTable.UpdateByIdentity(user) + return nil +} + +//stdb:reducer +func sendMessage(ctx server.ReducerContext, text string) error { + if text == "" { + return fmt.Errorf("Messages must not be empty") + } + logger.Info(fmt.Sprintf("User %v: %s", ctx.Sender(), text)) + MessageTable.Insert(Message{Sender: ctx.Sender(), Sent: ctx.Timestamp(), Text: text}) + return nil +} diff --git a/templates/chat-console-go/spacetimedb/types.go b/templates/chat-console-go/spacetimedb/types.go new file mode 100644 index 00000000000..8cc34471fc2 --- /dev/null +++ b/templates/chat-console-go/spacetimedb/types.go @@ -0,0 +1,17 @@ +package main + +import "github.com/clockworklabs/SpacetimeDB/sdks/go/types" + +//stdb:table name=user access=public +type User struct { + Identity types.Identity `stdb:"primarykey"` + Name *string + Online bool +} + +//stdb:table name=message access=public +type Message struct { + Sender types.Identity + Sent types.Timestamp + Text string +}