Skip to content

Commit e1769b5

Browse files
Add warning prompt for 1.0 -> 2.0 module upgrade path (#4247)
# Description of Changes Adds a warning prompt for 1.0 -> 2.0 module upgrade path. # API and ABI breaking changes None # Expected complexity level and risk 1 # Testing This patch checks a wasm module binary compiled with pre-2.0 bindings into source control. A smoketest was added that first publishes the the pre-compiled module and then publishes a new module using the 2.0 bindings in its place.
1 parent 9de9d06 commit e1769b5

13 files changed

Lines changed: 255 additions & 10 deletions

File tree

crates/cli/src/subcommands/publish.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,27 @@ fn confirm_and_clear(
134134
Ok(builder)
135135
}
136136

137+
fn confirm_major_version_upgrade() -> Result<(), anyhow::Error> {
138+
println!(
139+
"It looks like you're trying to do a major version upgrade from 1.0 to 2.0. We recommend first looking at the upgrade notes before committing to this upgrade: https://spacetimedb.com/docs/upgrade"
140+
);
141+
println!();
142+
println!("WARNING: Once you publish you cannot revert back to version 1.0.");
143+
println!();
144+
145+
let mut input = String::new();
146+
print!("Please type 'upgrade' to accept this change: ");
147+
let mut stdout = std::io::stdout();
148+
std::io::Write::flush(&mut stdout)?;
149+
std::io::stdin().read_line(&mut input)?;
150+
151+
if input.trim() == "upgrade" {
152+
return Ok(());
153+
}
154+
155+
anyhow::bail!("Aborting because major version upgrade was not accepted.");
156+
}
157+
137158
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
138159
let server = args.get_one::<String>("server").map(|s| s.as_str());
139160
let name_or_identity = args.get_one::<String>("name|identity");
@@ -377,6 +398,14 @@ async fn apply_pre_publish_if_needed(
377398
)
378399
.await?
379400
{
401+
let major_version_upgrade = match &pre {
402+
PrePublishResult::AutoMigrate(auto) => auto.major_version_upgrade,
403+
PrePublishResult::ManualMigrate(manual) => manual.major_version_upgrade,
404+
};
405+
if major_version_upgrade {
406+
confirm_major_version_upgrade()?;
407+
}
408+
380409
match pre {
381410
PrePublishResult::ManualMigrate(manual) => {
382411
if clear_database == ClearMode::Never {

crates/client-api-messages/src/name.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,13 @@ pub struct PrePublishAutoMigrateResult {
131131
pub migrate_plan: Box<str>,
132132
pub break_clients: bool,
133133
pub token: spacetimedb_lib::Hash,
134+
pub major_version_upgrade: bool,
134135
}
135136

136137
#[derive(serde::Serialize, serde::Deserialize, Debug)]
137138
pub struct PrePublishManualMigrateResult {
138139
pub reason: String,
140+
pub major_version_upgrade: bool,
139141
}
140142

141143
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]

crates/client-api/src/routes/database.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,7 @@ pub async fn pre_publish<S: NodeDelegate + ControlStateDelegate + Authorization>
942942
new_module_hash,
943943
breaks_client,
944944
plan,
945+
major_version_upgrade,
945946
} => {
946947
info!(
947948
"planned auto-migration of database {} from {} to {}",
@@ -958,12 +959,17 @@ pub async fn pre_publish<S: NodeDelegate + ControlStateDelegate + Authorization>
958959
token,
959960
migrate_plan: plan,
960961
break_clients: breaks_client,
962+
major_version_upgrade,
961963
}))
962964
}
963-
MigratePlanResult::AutoMigrationError(e) => {
965+
MigratePlanResult::AutoMigrationError {
966+
error: e,
967+
major_version_upgrade,
968+
} => {
964969
info!("database {database_identity} needs manual migration");
965970
Ok(PrePublishResult::ManualMigrate(PrePublishManualMigrateResult {
966971
reason: e.to_string(),
972+
major_version_upgrade,
967973
}))
968974
}
969975
}

crates/core/src/host/host_controller.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use spacetimedb_lib::{hash_bytes, AlgebraicValue, Identity, Timestamp};
3636
use spacetimedb_paths::server::{ModuleLogsDir, ServerDataDir};
3737
use spacetimedb_sats::hash::Hash;
3838
use spacetimedb_schema::auto_migrate::{ponder_migrate, AutoMigrateError, MigrationPolicy, PrettyPrintStyle};
39-
use spacetimedb_schema::def::ModuleDef;
39+
use spacetimedb_schema::def::{ModuleDef, RawModuleDefVersion};
4040
use spacetimedb_table::page_pool::PagePool;
4141
use std::future::Future;
4242
use std::ops::Deref;
@@ -1161,15 +1161,26 @@ impl Host {
11611161

11621162
let module_def =
11631163
extract_schema_with_pools(page_pool, bsatn_rlb_pool, host_runtimes, program.bytes, host_type).await?;
1164+
let major_version_upgrade = matches!(
1165+
(
1166+
old_module.module_def.raw_module_def_version(),
1167+
module_def.raw_module_def_version()
1168+
),
1169+
(RawModuleDefVersion::V9OrEarlier, RawModuleDefVersion::V10)
1170+
);
11641171

11651172
let res = match ponder_migrate(&old_module.module_def, &module_def) {
11661173
Ok(plan) => MigratePlanResult::Success {
11671174
old_module_hash: old_module.module_hash,
11681175
new_module_hash: program.hash,
11691176
breaks_client: plan.breaks_client(),
11701177
plan: plan.pretty_print(style)?.into(),
1178+
major_version_upgrade,
1179+
},
1180+
Err(e) => MigratePlanResult::AutoMigrationError {
1181+
error: e,
1182+
major_version_upgrade,
11711183
},
1172-
Err(e) => MigratePlanResult::AutoMigrationError(e),
11731184
};
11741185

11751186
Ok(res)
@@ -1194,8 +1205,12 @@ pub enum MigratePlanResult {
11941205
new_module_hash: Hash,
11951206
plan: Box<str>,
11961207
breaks_client: bool,
1208+
major_version_upgrade: bool,
1209+
},
1210+
AutoMigrationError {
1211+
error: ErrorStream<AutoMigrateError>,
1212+
major_version_upgrade: bool,
11971213
},
1198-
AutoMigrationError(ErrorStream<AutoMigrateError>),
11991214
}
12001215

12011216
const STORAGE_METERING_INTERVAL: Duration = Duration::from_secs(15);

crates/schema/src/def.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ pub struct ModuleDef {
148148
raw_module_def_version: RawModuleDefVersion,
149149
}
150150

151-
#[derive(Debug, Clone)]
151+
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
152152
pub enum RawModuleDefVersion {
153153
/// Represents [`RawModuleDefV9`] and earlier.
154154
V9OrEarlier,
@@ -157,6 +157,11 @@ pub enum RawModuleDefVersion {
157157
}
158158

159159
impl ModuleDef {
160+
/// The raw module definition version this module was authored under.
161+
pub fn raw_module_def_version(&self) -> RawModuleDefVersion {
162+
self.raw_module_def_version
163+
}
164+
160165
/// The tables of the module definition.
161166
pub fn tables(&self) -> impl Iterator<Item = &TableDef> {
162167
self.tables.values()
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Smoketest Fixtures
2+
3+
`upgrade_old_module_v1.wasm` is an old-format module fixture used by
4+
`crates/smoketests/tests/publish_upgrade_prompt.rs` to test the `1.0 -> 2.0`
5+
upgrade confirmation flow.
6+
7+
It was produced from a pre-`RawModuleDefV10` bindings snapshot and exports
8+
`__describe_module__` (not `__describe_module_v10__`).
9+
10+
## Regenerate
11+
12+
```bash
13+
# from repo root
14+
TMP="$(mktemp -d)"
15+
git archive --format=tar d3f59480e -o "$TMP/old-repo.tar"
16+
mkdir -p "$TMP/old-repo"
17+
tar -xf "$TMP/old-repo.tar" -C "$TMP/old-repo"
18+
19+
mkdir -p "$TMP/old-module/src"
20+
cat > "$TMP/old-module/Cargo.toml" <<EOF
21+
[package]
22+
name = "upgrade_old_module"
23+
version = "0.1.0"
24+
edition = "2021"
25+
26+
[lib]
27+
crate-type = ["cdylib"]
28+
29+
[dependencies]
30+
spacetimedb = { path = "$TMP/old-repo/crates/bindings", features = ["unstable"] }
31+
EOF
32+
33+
cat > "$TMP/old-module/src/lib.rs" <<'EOF'
34+
use spacetimedb::{reducer, ReducerContext};
35+
36+
#[reducer]
37+
pub fn noop(_ctx: &ReducerContext) {}
38+
EOF
39+
40+
CARGO_NET_OFFLINE=true CARGO_TARGET_DIR="$TMP/target-old" \
41+
cargo build --release --target wasm32-unknown-unknown \
42+
--manifest-path "$TMP/old-module/Cargo.toml"
43+
44+
cp "$TMP/target-old/wasm32-unknown-unknown/release/upgrade_old_module.wasm" \
45+
crates/smoketests/fixtures/upgrade_old_module_v1.wasm
46+
```
96.6 KB
Binary file not shown.

crates/smoketests/src/lib.rs

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ use regex::Regex;
5757
use spacetimedb_guard::{ensure_binaries_built, SpacetimeDbGuard};
5858
use std::env;
5959
use std::fs;
60-
use std::path::PathBuf;
60+
use std::path::{Path, PathBuf};
6161
use std::process::{Command, Output, Stdio};
6262
use std::sync::OnceLock;
6363
use std::time::Instant;
@@ -607,6 +607,40 @@ impl Smoketest {
607607
output
608608
}
609609

610+
/// Runs a spacetime CLI command with stdin input.
611+
///
612+
/// Returns the command output. The command is run but not yet asserted.
613+
/// Uses --config-path to isolate test config from user config.
614+
/// Callers should pass `--server` explicitly when the command needs it.
615+
pub fn spacetime_cmd_with_stdin(&self, args: &[&str], stdin_input: &str) -> Output {
616+
let start = Instant::now();
617+
let cli_path = ensure_binaries_built();
618+
let mut child = Command::new(&cli_path)
619+
.arg("--config-path")
620+
.arg(&self.config_path)
621+
.args(args)
622+
.current_dir(self.project_dir.path())
623+
.stdin(Stdio::piped())
624+
.stdout(Stdio::piped())
625+
.stderr(Stdio::piped())
626+
.spawn()
627+
.expect("Failed to spawn spacetime command");
628+
629+
{
630+
use std::io::Write;
631+
let stdin = child.stdin.as_mut().expect("missing child stdin");
632+
stdin
633+
.write_all(stdin_input.as_bytes())
634+
.expect("Failed to write spacetime stdin");
635+
}
636+
637+
let output = child.wait_with_output().expect("Failed to wait for spacetime command");
638+
639+
let cmd_name = args.first().unwrap_or(&"unknown");
640+
eprintln!("[TIMING] spacetime {} (stdin): {:?}", cmd_name, start.elapsed());
641+
output
642+
}
643+
610644
/// Runs a spacetime CLI command and returns stdout as a string.
611645
///
612646
/// Panics if the command fails.
@@ -624,6 +658,23 @@ impl Smoketest {
624658
Ok(String::from_utf8_lossy(&output.stdout).to_string())
625659
}
626660

661+
/// Runs a spacetime CLI command with stdin and returns stdout as a string.
662+
///
663+
/// Panics if the command fails.
664+
/// Callers should pass `--server` explicitly when the command needs it.
665+
pub fn spacetime_with_stdin(&self, args: &[&str], stdin_input: &str) -> Result<String> {
666+
let output = self.spacetime_cmd_with_stdin(args, stdin_input);
667+
if !output.status.success() {
668+
bail!(
669+
"spacetime {:?} failed:\nstdout: {}\nstderr: {}",
670+
args,
671+
String::from_utf8_lossy(&output.stdout),
672+
String::from_utf8_lossy(&output.stderr)
673+
);
674+
}
675+
Ok(String::from_utf8_lossy(&output.stdout).to_string())
676+
}
677+
627678
/// Writes new module code to the project.
628679
///
629680
/// This switches from precompiled mode to runtime compilation mode.
@@ -692,6 +743,20 @@ log = "0.4"
692743
self.precompiled_wasm_path = Some(path);
693744
}
694745

746+
/// Switches to using an explicit precompiled WASM path.
747+
///
748+
/// After calling this, subsequent `publish_module*` calls will use this
749+
/// WASM file instead of building from source.
750+
pub fn use_precompiled_wasm_path(&mut self, path: impl AsRef<Path>) -> Result<()> {
751+
let path = path.as_ref();
752+
if !path.exists() {
753+
bail!("Pre-compiled wasm not found at {}", path.display());
754+
}
755+
eprintln!("[PRECOMPILED] Switching to explicit wasm path: {}", path.display());
756+
self.precompiled_wasm_path = Some(path.to_path_buf());
757+
Ok(())
758+
}
759+
695760
/// Runs `spacetime build` and returns the raw output.
696761
///
697762
/// Use this when you need to check for build failures (e.g., wasm_bindgen detection).
@@ -738,16 +803,29 @@ log = "0.4"
738803

739804
/// Publishes the module with name, clear, and break_clients options.
740805
pub fn publish_module_with_options(&mut self, name: &str, clear: bool, break_clients: bool) -> Result<String> {
741-
self.publish_module_internal(Some(name), clear, break_clients)
806+
self.publish_module_internal(Some(name), clear, break_clients, None)
807+
}
808+
809+
/// Publishes the module and allows supplying stdin input to the CLI.
810+
///
811+
/// Useful for interactive publish prompts which require typed acknowledgements.
812+
pub fn publish_module_with_stdin(&mut self, name: &str, stdin_input: &str) -> Result<String> {
813+
self.publish_module_internal(Some(name), false, false, Some(stdin_input))
742814
}
743815

744816
/// Internal helper for publishing with options.
745817
fn publish_module_opts(&mut self, name: Option<&str>, clear: bool) -> Result<String> {
746-
self.publish_module_internal(name, clear, false)
818+
self.publish_module_internal(name, clear, false, None)
747819
}
748820

749821
/// Internal helper for publishing with all options.
750-
fn publish_module_internal(&mut self, name: Option<&str>, clear: bool, break_clients: bool) -> Result<String> {
822+
fn publish_module_internal(
823+
&mut self,
824+
name: Option<&str>,
825+
clear: bool,
826+
break_clients: bool,
827+
stdin_input: Option<&str>,
828+
) -> Result<String> {
751829
let start = Instant::now();
752830

753831
// Determine the WASM path - either precompiled or build it
@@ -811,7 +889,10 @@ log = "0.4"
811889
args.push(&name_owned);
812890
}
813891

814-
let output = self.spacetime(&args)?;
892+
let output = match stdin_input {
893+
Some(stdin_input) => self.spacetime_with_stdin(&args, stdin_input)?,
894+
None => self.spacetime(&args)?,
895+
};
815896
eprintln!(
816897
"[TIMING] spacetime publish (after build): {:?}",
817898
publish_start.elapsed()
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use std::path::PathBuf;
2+
3+
use spacetimedb_smoketests::{random_string, workspace_root, Smoketest};
4+
5+
const MODULE_CODE: &str = r#"
6+
use spacetimedb::{reducer, ReducerContext};
7+
8+
#[reducer]
9+
pub fn noop(_ctx: &ReducerContext) {}
10+
"#;
11+
12+
fn old_fixture_wasm() -> PathBuf {
13+
workspace_root()
14+
.join("crates")
15+
.join("smoketests")
16+
.join("fixtures")
17+
.join("upgrade_old_module_v1.wasm")
18+
}
19+
20+
#[test]
21+
fn upgrade_prompt_on_publish() {
22+
let mut test = Smoketest::builder().autopublish(false).build();
23+
24+
let old_wasm = old_fixture_wasm();
25+
assert!(old_wasm.exists(), "expected old fixture wasm at {}", old_wasm.display());
26+
27+
let db_name = format!("upgrade-smoke-{}", random_string());
28+
29+
test.use_precompiled_wasm_path(&old_wasm).unwrap();
30+
let initial_identity = test.publish_module_named(&db_name, false).unwrap();
31+
assert_eq!(test.database_identity.as_deref(), Some(initial_identity.as_str()));
32+
33+
// Switch back to source-built module, which uses current bindings.
34+
test.write_module_code(MODULE_CODE).unwrap();
35+
36+
let deny_err = test.publish_module_named(&db_name, false).unwrap_err().to_string();
37+
assert!(deny_err.contains("major version upgrade from 1.0 to 2.0"));
38+
assert!(deny_err.contains("Please type 'upgrade' to accept this change:"));
39+
40+
let accepted_identity = test.publish_module_with_stdin(&db_name, "upgrade\n").unwrap();
41+
assert_eq!(accepted_identity, initial_identity);
42+
}

docs/docs/00200-core-concepts/00100-databases.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ See [`spacetime publish`](/databases/building-publishing) for details on the pub
9595

9696
When you republish to an existing database, SpacetimeDB attempts to automatically migrate the schema. For details on what changes are supported and migration strategies:
9797

98+
- [1.x to 2.0 Upgrade Notes](/upgrade) - Required reading before major-version upgrades.
9899
- [Automatic Migrations](/databases/automatic-migrations) - Learn which schema changes are safe, breaking, or forbidden.
99100
- [Incremental Migrations](/databases/incremental-migrations) - Advanced pattern for complex schema changes.
100101

0 commit comments

Comments
 (0)