diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9064fa6b0..468e109b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -835,6 +835,10 @@ jobs: const publicRef = (context.eventName === 'pull_request') ? context.payload.pull_request.head.ref : context.sha; const publicPrNumber = context.payload.pull_request?.number ?? context.payload.inputs?.pr_number; const preDispatch = new Date().toISOString(); + const inputs = { public_ref: publicRef }; + if (publicPrNumber) { + inputs.public_pr_number = String(publicPrNumber); + } // Dispatch the workflow in the target repository await github.rest.actions.createWorkflowDispatch({ @@ -842,7 +846,7 @@ jobs: repo: targetRepo, workflow_id: workflowId, ref: targetRef, - inputs: { public_ref: publicRef, public_pr_number: String(publicPrNumber) } + inputs, }); const sleep = (ms) => new Promise(r => setTimeout(r, ms)); diff --git a/crates/bindings-cpp/include/spacetimedb/procedure_context.h b/crates/bindings-cpp/include/spacetimedb/procedure_context.h index 1ee1b88f7..f9107d702 100644 --- a/crates/bindings-cpp/include/spacetimedb/procedure_context.h +++ b/crates/bindings-cpp/include/spacetimedb/procedure_context.h @@ -97,17 +97,22 @@ public: * * Example: * @code - * auto module_id = ctx.identity(); + * auto module_id = ctx.database_identity(); * std::string url = "http://localhost:3000/v1/database/" + * module_id.to_hex() + "/schema?version=9"; * @endcode */ - Identity identity() const { + Identity database_identity() const { std::array id_bytes; ::identity(id_bytes.data()); return Identity(id_bytes); } + [[deprecated("Use database_identity() instead.")]] + Identity identity() const { + return database_identity(); + } + /** * @brief Get the random number generator for this procedure call * diff --git a/crates/bindings-cpp/include/spacetimedb/reducer_context.h b/crates/bindings-cpp/include/spacetimedb/reducer_context.h index 307c107e5..8c8fba26e 100644 --- a/crates/bindings-cpp/include/spacetimedb/reducer_context.h +++ b/crates/bindings-cpp/include/spacetimedb/reducer_context.h @@ -58,11 +58,16 @@ public: return *rng_instance; } - Identity identity() const { + Identity database_identity() const { std::array buffer; ::identity(buffer.data()); return Identity(buffer); } + + [[deprecated("Use database_identity() instead.")]] + Identity identity() const { + return database_identity(); + } /** * Generate a new random UUID v4. diff --git a/crates/bindings-cpp/include/spacetimedb/tx_context.h b/crates/bindings-cpp/include/spacetimedb/tx_context.h index 68d1d3181..1a04ef027 100644 --- a/crates/bindings-cpp/include/spacetimedb/tx_context.h +++ b/crates/bindings-cpp/include/spacetimedb/tx_context.h @@ -69,7 +69,9 @@ public: // Access to ReducerContext methods Identity sender() const { return ctx_.sender(); } const AuthCtx& sender_auth() const { return ctx_.sender_auth(); } - Identity identity() const { return ctx_.identity(); } + Identity database_identity() const { return ctx_.database_identity(); } + [[deprecated("Use database_identity() instead.")]] + Identity identity() const { return database_identity(); } StdbRng& rng() const { return ctx_.rng(); } /** diff --git a/crates/bindings-csharp/BSATN.Runtime/Builtins.cs b/crates/bindings-csharp/BSATN.Runtime/Builtins.cs index ec2331aa6..e204985b5 100644 --- a/crates/bindings-csharp/BSATN.Runtime/Builtins.cs +++ b/crates/bindings-csharp/BSATN.Runtime/Builtins.cs @@ -1,8 +1,10 @@ namespace SpacetimeDB; -using System.Diagnostics; using System.Runtime.InteropServices; using SpacetimeDB.BSATN; +#if !NET5_0_OR_GREATER +using System.Diagnostics; +#endif internal static class Util { diff --git a/crates/bindings-csharp/BSATN.Runtime/QueryBuilder.cs b/crates/bindings-csharp/BSATN.Runtime/QueryBuilder.cs index 45f481847..db16bd56c 100644 --- a/crates/bindings-csharp/BSATN.Runtime/QueryBuilder.cs +++ b/crates/bindings-csharp/BSATN.Runtime/QueryBuilder.cs @@ -877,10 +877,14 @@ internal static class SqlFormat public static string FormatHexLiteral(string hex) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(hex); +#else if (hex is null) { throw new ArgumentNullException(nameof(hex)); } +#endif var s = hex; if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs index 3ce055b4b..bb33a8b65 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs @@ -655,9 +655,13 @@ namespace SpacetimeDB // **Note:** must be 0..=u32::MAX internal int CounterUuid; + public Identity DatabaseIdentity => Internal.IReducerContext.GetDatabaseIdentity(); - // We need this property to be non-static for parity with client SDK. - public Identity Identity => Internal.IReducerContext.GetIdentity(); + // We keep this property for compatibility with existing module code. + [global::System.Obsolete( + "ReducerContext.Identity is deprecated. Use DatabaseIdentity instead." + )] + public Identity Identity => DatabaseIdentity; internal ReducerContext( Identity identity, diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#FFI.verified.cs index a9774bfc6..c96357625 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#FFI.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#FFI.verified.cs @@ -57,9 +57,13 @@ namespace SpacetimeDB // **Note:** must be 0..=u32::MAX internal int CounterUuid; + public Identity DatabaseIdentity => Internal.IReducerContext.GetDatabaseIdentity(); - // We need this property to be non-static for parity with client SDK. - public Identity Identity => Internal.IReducerContext.GetIdentity(); + // We keep this property for compatibility with existing module code. + [global::System.Obsolete( + "ReducerContext.Identity is deprecated. Use DatabaseIdentity instead." + )] + public Identity Identity => DatabaseIdentity; internal ReducerContext( Identity identity, diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs index 4632875e0..4c6be2c99 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs @@ -499,9 +499,13 @@ namespace SpacetimeDB // **Note:** must be 0..=u32::MAX internal int CounterUuid; + public Identity DatabaseIdentity => Internal.IReducerContext.GetDatabaseIdentity(); - // We need this property to be non-static for parity with client SDK. - public Identity Identity => Internal.IReducerContext.GetIdentity(); + // We keep this property for compatibility with existing module code. + [global::System.Obsolete( + "ReducerContext.Identity is deprecated. Use DatabaseIdentity instead." + )] + public Identity Identity => DatabaseIdentity; internal ReducerContext( Identity identity, diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index e778b6d69..f6ffa50b9 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -2189,8 +2189,10 @@ public class Module : IIncrementalGenerator public readonly AuthCtx SenderAuth; // **Note:** must be 0..=u32::MAX internal int CounterUuid; - // We need this property to be non-static for parity with client SDK. - public Identity Identity => Internal.IReducerContext.GetIdentity(); + public Identity DatabaseIdentity => Internal.IReducerContext.GetDatabaseIdentity(); + // We keep this property for compatibility with existing module code. + [global::System.Obsolete("ReducerContext.Identity is deprecated. Use DatabaseIdentity instead.")] + public Identity Identity => DatabaseIdentity; internal ReducerContext(Identity identity, ConnectionId? connectionId, Random random, Timestamp time, AuthCtx? senderAuth = null) diff --git a/crates/bindings-csharp/Runtime/Internal/IReducer.cs b/crates/bindings-csharp/Runtime/Internal/IReducer.cs index ec2e698a1..878c98a2a 100644 --- a/crates/bindings-csharp/Runtime/Internal/IReducer.cs +++ b/crates/bindings-csharp/Runtime/Internal/IReducer.cs @@ -1,15 +1,19 @@ namespace SpacetimeDB.Internal; +using System; using System.Text; using SpacetimeDB.BSATN; public interface IReducerContext { - public static Identity GetIdentity() + public static Identity GetDatabaseIdentity() { FFI.identity(out var identity); return identity; } + + [Obsolete("IReducerContext.GetIdentity() is deprecated. Use GetDatabaseIdentity() instead.")] + public static Identity GetIdentity() => GetDatabaseIdentity(); } public interface IReducer diff --git a/crates/bindings-typescript/src/lib/reducers.ts b/crates/bindings-typescript/src/lib/reducers.ts index a4252ba68..0eae2adc2 100644 --- a/crates/bindings-typescript/src/lib/reducers.ts +++ b/crates/bindings-typescript/src/lib/reducers.ts @@ -103,6 +103,8 @@ export interface JwtClaims { */ export type ReducerCtx = Readonly<{ sender: Identity; + databaseIdentity: Identity; + /** @deprecated Use `databaseIdentity` instead. */ identity: Identity; timestamp: Timestamp; connectionId: ConnectionId | null; diff --git a/crates/bindings-typescript/src/server/procedures.ts b/crates/bindings-typescript/src/server/procedures.ts index 5e0791c15..39e5f5854 100644 --- a/crates/bindings-typescript/src/server/procedures.ts +++ b/crates/bindings-typescript/src/server/procedures.ts @@ -75,6 +75,8 @@ export interface ProcedureOpts { export interface ProcedureCtx { readonly sender: Identity; + readonly databaseIdentity: Identity; + /** @deprecated Use `databaseIdentity` instead. */ readonly identity: Identity; readonly timestamp: Timestamp; readonly connectionId: ConnectionId | null; @@ -195,10 +197,14 @@ const ProcedureCtxImpl = class ProcedureCtx this.#dbView = dbView; } - get identity() { + get databaseIdentity() { return (this.#identity ??= new Identity(sys.identity())); } + get identity() { + return this.databaseIdentity; + } + get random() { return (this.#random ??= makeRandom(this.timestamp)); } diff --git a/crates/bindings-typescript/src/server/runtime.ts b/crates/bindings-typescript/src/server/runtime.ts index e05d4c7f3..5031b1d85 100644 --- a/crates/bindings-typescript/src/server/runtime.ts +++ b/crates/bindings-typescript/src/server/runtime.ts @@ -221,10 +221,14 @@ export const ReducerCtxImpl = class ReducerCtx< me.#senderAuth = undefined; } - get identity() { + get databaseIdentity() { return (this.#identity ??= new Identity(sys.identity())); } + get identity() { + return this.databaseIdentity; + } + get senderAuth() { return (this.#senderAuth ??= AuthCtxImpl.fromSystemTables( this.connectionId, diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 27775242c..121957389 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -687,7 +687,7 @@ pub use spacetimedb_bindings_macro::table; /// /// #[reducer] /// fn scheduled(ctx: &ReducerContext, args: ScheduledArgs) -> Result<(), String> { -/// if ctx.sender() != ctx.identity() { +/// if ctx.sender() != ctx.database_identity() { /// return Err("Reducer `scheduled` may not be invoked by clients, only via scheduling.".into()); /// } /// // Reducer body... @@ -1081,7 +1081,7 @@ impl ReducerContext { } /// Read the current module's [`Identity`]. - pub fn identity(&self) -> Identity { + pub fn database_identity(&self) -> Identity { // Hypothetically, we *could* read the module identity out of the system tables. // However, this would be: // - Onerous, because we have no tooling to inspect the system tables from module code. @@ -1093,6 +1093,12 @@ impl ReducerContext { Identity::from_byte_array(spacetimedb_bindings_sys::identity()) } + /// Read the current module's [`Identity`]. + #[deprecated(note = "Use `ReducerContext::database_identity` instead.")] + pub fn identity(&self) -> Identity { + self.database_identity() + } + /// Create an anonymous (no sender) read-only view context pub fn as_anonymous_read_only(&self) -> AnonymousViewContext { AnonymousViewContext::default() diff --git a/crates/update/src/cli/uninstall.rs b/crates/update/src/cli/uninstall.rs index 0ec1f69ca..3c9e14dbe 100644 --- a/crates/update/src/cli/uninstall.rs +++ b/crates/update/src/cli/uninstall.rs @@ -28,10 +28,97 @@ impl Uninstall { Ok(None) => {} Err(e) => tracing::warn!("{e:#}"), } + let dir = paths.cli_bin_dir.version_dir(&version); + if !dir.0.exists() { + anyhow::bail!("v{version} is not installed"); + } if yes.confirm(format!("Uninstall v{version}?"))? { - let dir = paths.cli_bin_dir.version_dir(&version); - std::fs::remove_dir_all(dir)?; + std::fs::remove_dir_all(&dir)?; } Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use spacetimedb_paths::FromPathUnchecked; + use spacetimedb_paths::RootDir; + + fn make_temp_paths() -> (tempfile::TempDir, SpacetimePaths) { + let tmp = tempfile::tempdir().unwrap(); + let base = tmp.path().join("spacetime"); + std::fs::create_dir_all(&base).unwrap(); + let root = RootDir::from_path_unchecked(base); + let paths = SpacetimePaths::from_root_dir(&root); + (tmp, paths) + } + + #[test] + fn test_uninstall_nonexistent_version_errors_before_prompt() { + let (_tmp, paths) = make_temp_paths(); + let uninstall = Uninstall { + version: "9.9.9".to_owned(), + yes: ForceYes { yes: true }, + }; + let result = uninstall.exec(&paths); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err.to_string().contains("9.9.9"), + "error should mention the version number" + ); + assert!( + err.to_string().contains("not installed"), + "error should say 'not installed'" + ); + } + + #[test] + fn test_uninstall_current_version_errors() { + let (_tmp, paths) = make_temp_paths(); + // Create the "current" symlink target so it exists on disk + let current_dir = paths.cli_bin_dir.version_dir("2.0.0"); + std::fs::create_dir_all(¤t_dir.0).unwrap(); + paths.cli_bin_dir.set_current_version("2.0.0").unwrap(); + + let uninstall = Uninstall { + version: "2.0.0".to_owned(), + yes: ForceYes { yes: true }, + }; + let result = uninstall.exec(&paths); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("currently used version"),); + } + + #[test] + fn test_uninstall_current_keyword_errors() { + let (_tmp, paths) = make_temp_paths(); + let uninstall = Uninstall { + version: "current".to_owned(), + yes: ForceYes { yes: true }, + }; + let result = uninstall.exec(&paths); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("cannot remove `current`"),); + } + + #[test] + fn test_uninstall_existing_version_with_yes() { + let (_tmp, paths) = make_temp_paths(); + let version_dir = paths.cli_bin_dir.version_dir("1.0.0"); + std::fs::create_dir_all(&version_dir.0).unwrap(); + // Create a dummy file so we can verify the directory existed + std::fs::write(version_dir.0.join("spacetime"), "dummy").unwrap(); + + assert!(version_dir.0.exists(), "version dir should exist before"); + + let uninstall = Uninstall { + version: "1.0.0".to_owned(), + yes: ForceYes { yes: true }, + }; + uninstall.exec(&paths).unwrap(); + + assert!(!version_dir.0.exists(), "version dir should be removed after uninstall"); + } +} diff --git a/modules/module-test-cpp/src/lib.cpp b/modules/module-test-cpp/src/lib.cpp index a50f8e74f..bc5305b1f 100644 --- a/modules/module-test-cpp/src/lib.cpp +++ b/modules/module-test-cpp/src/lib.cpp @@ -280,7 +280,7 @@ SPACETIMEDB_REDUCER(list_over_age, ReducerContext ctx, uint8_t age) { // Log module identity SPACETIMEDB_REDUCER(log_module_identity, ReducerContext ctx) { - LOG_INFO("Module identity: " + ctx.identity().to_string()); + LOG_INFO("Module identity: " + ctx.database_identity().to_string()); return Ok(); } @@ -550,8 +550,8 @@ SPACETIMEDB_REDUCER(test_btree_index_args, ReducerContext ctx) { // Test reducer for assertions SPACETIMEDB_REDUCER(assert_caller_identity_is_module_identity, ReducerContext ctx) { - LOG_INFO("Sender: " + ctx.sender().to_string() + " Identity: " + ctx.identity().to_string()); - if (ctx.sender() != ctx.identity()) { + LOG_INFO("Sender: " + ctx.sender().to_string() + " Identity: " + ctx.database_identity().to_string()); + if (ctx.sender() != ctx.database_identity()) { LOG_ERROR("Assertion failed: caller identity does not match module identity"); } else { LOG_INFO("Assertion passed: caller identity matches module identity"); @@ -693,7 +693,7 @@ SPACETIMEDB_PROCEDURE(Unit, with_tx, ProcedureContext ctx) { // Hit SpacetimeDB's schema HTTP route and return its result as a string SPACETIMEDB_PROCEDURE(std::string, get_my_schema_via_http, ProcedureContext ctx) { - Identity module_identity = ctx.identity(); + Identity module_identity = ctx.database_identity(); std::string url = "http://localhost:3000/v1/database/" + module_identity.to_string() + "/schema?version=9"; auto result = ctx.http.get(url); diff --git a/modules/module-test-cs/Lib.cs b/modules/module-test-cs/Lib.cs index 57aefc07a..7ac37fc3d 100644 --- a/modules/module-test-cs/Lib.cs +++ b/modules/module-test-cs/Lib.cs @@ -300,7 +300,7 @@ static partial class Module public static void log_module_identity(ReducerContext ctx) { // Note: converting to lowercase to match the Rust formatting. - Log.Info($"Module identity: {ctx.Identity.ToString().ToLower()}"); + Log.Info($"Module identity: {ctx.DatabaseIdentity.ToString().ToLower()}"); } [Reducer] @@ -492,7 +492,7 @@ static partial class Module public static void assert_caller_identity_is_module_identity(ReducerContext ctx) { var caller = ctx.Sender; - var owner = ctx.Identity; + var owner = ctx.DatabaseIdentity; if (!caller.Equals(owner)) { throw new Exception($"Caller {caller} is not the owner {owner}"); diff --git a/modules/module-test-ts/src/index.ts b/modules/module-test-ts/src/index.ts index 7861153c7..b8f0c01d5 100644 --- a/modules/module-test-ts/src/index.ts +++ b/modules/module-test-ts/src/index.ts @@ -331,7 +331,7 @@ export const listOverAge = spacetimedb.reducer( // log_module_identity() export const log_module_identity = spacetimedb.reducer(ctx => { - console.info(`Module identity: ${ctx.identity}`); + console.info(`Module identity: ${ctx.databaseIdentity}`); }); // test(arg: TestAlias(TestA), arg2: TestB, arg3: TestC, arg4: TestF) @@ -494,7 +494,7 @@ export const test_btree_index_args = spacetimedb.reducer(ctx => { export const assert_caller_identity_is_module_identity = spacetimedb.reducer( ctx => { const caller = ctx.sender; - const owner = ctx.identity; + const owner = ctx.databaseIdentity; if (String(caller) !== String(owner)) { throw new Error(`Caller ${caller} is not the owner ${owner}`); } else { @@ -507,7 +507,7 @@ export const assert_caller_identity_is_module_identity = spacetimedb.reducer( // // This is a silly thing to do, but an effective test of the procedure HTTP API. export const getMySchemaViaHttp = spacetimedb.procedure(t.string(), ctx => { - const module_identity = ctx.identity; + const module_identity = ctx.databaseIdentity; try { const response = ctx.http.fetch( `http://localhost:3000/v1/database/${module_identity}/schema?version=9` diff --git a/modules/module-test/src/lib.rs b/modules/module-test/src/lib.rs index fe1d49169..56e6b288e 100644 --- a/modules/module-test/src/lib.rs +++ b/modules/module-test/src/lib.rs @@ -295,7 +295,7 @@ pub fn list_over_age(ctx: &ReducerContext, age: u8) { #[spacetimedb::reducer] fn log_module_identity(ctx: &ReducerContext) { - log::info!("Module identity: {}", ctx.identity()); + log::info!("Module identity: {}", ctx.database_identity()); } #[spacetimedb::reducer] @@ -508,7 +508,7 @@ fn test_btree_index_args(ctx: &ReducerContext) { #[spacetimedb::reducer] fn assert_caller_identity_is_module_identity(ctx: &ReducerContext) { let caller = ctx.sender(); - let owner = ctx.identity(); + let owner = ctx.database_identity(); if caller != owner { panic!("Caller {caller} is not the owner {owner}"); } else { diff --git a/modules/sdk-test-procedure-cpp/src/lib.cpp b/modules/sdk-test-procedure-cpp/src/lib.cpp index df9f54ae1..31e366970 100644 --- a/modules/sdk-test-procedure-cpp/src/lib.cpp +++ b/modules/sdk-test-procedure-cpp/src/lib.cpp @@ -142,7 +142,7 @@ SPACETIMEDB_PROCEDURE(Unit, insert_with_tx_rollback, ProcedureContext ctx) { // Test HTTP GET request to the module's own schema endpoint SPACETIMEDB_PROCEDURE(std::string, read_my_schema, ProcedureContext ctx) { // Get the module identity (database address) - Identity module_identity = ctx.identity(); + Identity module_identity = ctx.database_identity(); std::string identity_hex = module_identity.to_hex_string(); LOG_INFO("read_my_schema using identity: " + identity_hex); diff --git a/modules/sdk-test-procedure-ts/src/index.ts b/modules/sdk-test-procedure-ts/src/index.ts index c9cd308d7..f89aa7666 100644 --- a/modules/sdk-test-procedure-ts/src/index.ts +++ b/modules/sdk-test-procedure-ts/src/index.ts @@ -91,7 +91,7 @@ export const will_panic = spacetimedb.procedure(t.unit(), _ctx => { }); export const read_my_schema = spacetimedb.procedure(t.string(), ctx => { - const module_identity = ctx.identity; + const module_identity = ctx.databaseIdentity; const response = ctx.http.fetch( `http://localhost:3000/v1/database/${module_identity}/schema?version=9` );