Skip to content

Commit 46f3e07

Browse files
cloutiertylerbfopsgefjonjdetter
authored
Fixes issues with --delete-data=on-conflict (#3730)
# Description of Changes Fixes #3729 I genuinely don't know what came over me. # API and ABI breaking changes None # Expected complexity level and risk 1.5 very straightforward but not strictly trivial # Testing Adds automated integration tests (written in Rust and run with `cargo test`, although note this comment from @matklad about integration tests for the future https://internals.rust-lang.org/t/running-test-crates-in-parallel/15639/2): - [x] Can publish an updated module if no migration is required - [x] Can publish an updated module if auto-migration is required (with the yes-break flag true/false) - [x] Cannot publish if a manual migration is required - [x] Can publish if a manual migration is required but the user specified `--delete-data` - [x] Can publish if a manual migration is required by the user specified `--delete-data=on-conflict` - [x] No data deletion occurs if no migration is required and `--delete-data=on-conflict` is specified --------- Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> Co-authored-by: Zeke Foppa <196249+bfops@users.noreply.github.com> Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> Co-authored-by: Phoebe Goldman <phoebe@clockworklabs.io> Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com>
1 parent a959996 commit 46f3e07

File tree

18 files changed

+785
-46
lines changed

18 files changed

+785
-46
lines changed

Cargo.lock

Lines changed: 81 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cli/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ notify.workspace = true
8686

8787
[dev-dependencies]
8888
pretty_assertions.workspace = true
89+
fs_extra.workspace = true
90+
assert_cmd = "2"
91+
predicates = "3"
92+
portpicker = "0.1"
93+
reqwest = { workspace = true, features = ["blocking", "json"] }
8994

9095
[target.'cfg(not(target_env = "msvc"))'.dependencies]
9196
tikv-jemallocator = { workspace = true }

crates/cli/src/subcommands/build.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ pub fn cli() -> clap::Command {
2222
.default_value("src")
2323
.help("The directory to lint for nonfunctional print statements. If set to the empty string, skips linting.")
2424
)
25+
.arg(
26+
// TODO: Make this into --extra-build-args (or something similar) that will get passed along to the language's compiler.
27+
Arg::new("features")
28+
.long("features")
29+
.value_parser(clap::value_parser!(OsString))
30+
.required(false)
31+
.help("Additional features to pass to the build process (e.g. `--features feature1,feature2` for Rust modules).")
32+
// We're hiding this because we think it deserves a refactor first (see the TODO above)
33+
.hide(true)
34+
)
2535
.arg(
2636
Arg::new("debug")
2737
.long("debug")
@@ -33,6 +43,7 @@ pub fn cli() -> clap::Command {
3343

3444
pub async fn exec(_config: Config, args: &ArgMatches) -> Result<(PathBuf, &'static str), anyhow::Error> {
3545
let project_path = args.get_one::<PathBuf>("project_path").unwrap();
46+
let features = args.get_one::<OsString>("features");
3647
let lint_dir = args.get_one::<OsString>("lint_dir").unwrap();
3748
let lint_dir = if lint_dir.is_empty() {
3849
None
@@ -56,7 +67,7 @@ pub async fn exec(_config: Config, args: &ArgMatches) -> Result<(PathBuf, &'stat
5667
));
5768
}
5869

59-
let result = crate::tasks::build(project_path, lint_dir.as_deref(), build_debug)?;
70+
let result = crate::tasks::build(project_path, lint_dir.as_deref(), build_debug, features)?;
6071
println!("Build finished successfully.");
6172

6273
Ok(result)

crates/cli/src/subcommands/dev.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
9494
let clear_database = args
9595
.get_one::<ClearMode>("clear-database")
9696
.copied()
97-
.unwrap_or(ClearMode::Never);
97+
.unwrap_or(ClearMode::OnConflict);
9898
let force = args.get_flag("force");
9999

100100
// If you don't specify a server, we default to your default server
@@ -388,7 +388,7 @@ async fn generate_build_and_publish(
388388

389389
println!("{}", "Building...".cyan());
390390
let (_path_to_program, _host_type) =
391-
tasks::build(spacetimedb_dir, Some(Path::new("src")), false).context("Failed to build project")?;
391+
tasks::build(spacetimedb_dir, Some(Path::new("src")), false, None).context("Failed to build project")?;
392392
println!("{}", "Build complete!".green());
393393

394394
println!("{}", "Generating module bindings...".cyan());
@@ -413,15 +413,14 @@ async fn generate_build_and_publish(
413413
ClearMode::OnConflict => "on-conflict",
414414
};
415415
let mut publish_args = vec![
416-
"publish",
417-
database_name,
418-
"--project-path",
419-
project_path_str,
420-
"--yes",
421-
"--delete-data",
422-
clear_flag,
416+
"publish".to_string(),
417+
database_name.to_string(),
418+
"--project-path".to_string(),
419+
project_path_str.to_string(),
420+
"--yes".to_string(),
421+
format!("--delete-data={}", clear_flag),
423422
];
424-
publish_args.extend_from_slice(&["--server", server]);
423+
publish_args.extend_from_slice(&["--server".to_string(), server.to_string()]);
425424

426425
let publish_cmd = publish::cli();
427426
let publish_matches = publish_cmd

crates/cli/src/subcommands/publish.rs

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,26 @@ i.e. only lowercase ASCII letters and numbers, separated by dashes."),
102102
.after_help("Run `spacetime help publish` for more detailed information.")
103103
}
104104

105+
fn confirm_and_clear(
106+
name_or_identity: &str,
107+
force: bool,
108+
mut builder: reqwest::RequestBuilder,
109+
) -> Result<reqwest::RequestBuilder, anyhow::Error> {
110+
println!(
111+
"This will DESTROY the current {} module, and ALL corresponding data.",
112+
name_or_identity
113+
);
114+
if !y_or_n(
115+
force,
116+
format!("Are you sure you want to proceed? [deleting {}]", name_or_identity).as_str(),
117+
)? {
118+
anyhow::bail!("Aborted.");
119+
}
120+
121+
builder = builder.query(&[("clear", true)]);
122+
Ok(builder)
123+
}
124+
105125
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
106126
let server = args.get_one::<String>("server").map(|s| s.as_str());
107127
let name_or_identity = args.get_one::<String>("name|identity");
@@ -175,11 +195,15 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
175195
let domain = percent_encoding::percent_encode(name_or_identity.as_bytes(), encode_set);
176196
let mut builder = client.put(format!("{database_host}/v1/database/{domain}"));
177197

178-
if clear_database != ClearMode::Always {
198+
// note that this only happens in the case where we've passed a `name_or_identity`, but that's required if we pass `--clear-database`.
199+
if clear_database == ClearMode::Always {
200+
builder = confirm_and_clear(name_or_identity, force, builder)?;
201+
} else {
179202
builder = apply_pre_publish_if_needed(
180203
builder,
181204
&client,
182205
&database_host,
206+
name_or_identity,
183207
&domain.to_string(),
184208
host_type,
185209
&program_bytes,
@@ -196,25 +220,6 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
196220
client.post(format!("{database_host}/v1/database"))
197221
};
198222

199-
if clear_database == ClearMode::Always || clear_database == ClearMode::OnConflict {
200-
// Note: `name_or_identity` should be set, because it is `required` in the CLI arg config.
201-
println!(
202-
"This will DESTROY the current {} module, and ALL corresponding data.",
203-
name_or_identity.unwrap()
204-
);
205-
if !y_or_n(
206-
force,
207-
format!(
208-
"Are you sure you want to proceed? [deleting {}]",
209-
name_or_identity.unwrap()
210-
)
211-
.as_str(),
212-
)? {
213-
println!("Aborting");
214-
return Ok(());
215-
}
216-
builder = builder.query(&[("clear", true)]);
217-
}
218223
if let Some(n) = num_replicas {
219224
eprintln!("WARNING: Use of unstable option `--num-replicas`.\n");
220225
builder = builder.query(&[("num_replicas", *n)]);
@@ -334,6 +339,7 @@ async fn apply_pre_publish_if_needed(
334339
mut builder: reqwest::RequestBuilder,
335340
client: &reqwest::Client,
336341
base_url: &str,
342+
name_or_identity: &str,
337343
domain: &String,
338344
host_type: &str,
339345
program_bytes: &[u8],
@@ -342,6 +348,9 @@ async fn apply_pre_publish_if_needed(
342348
force_break_clients: bool,
343349
force: bool,
344350
) -> Result<reqwest::RequestBuilder, anyhow::Error> {
351+
// The caller enforces this
352+
assert!(clear_database != ClearMode::Always);
353+
345354
if let Some(pre) = call_pre_publish(
346355
client,
347356
base_url,
@@ -354,19 +363,15 @@ async fn apply_pre_publish_if_needed(
354363
{
355364
match pre {
356365
PrePublishResult::ManualMigrate(manual) => {
357-
if clear_database == ClearMode::OnConflict {
358-
println!("{}", manual.reason);
359-
println!("Proceeding with database clear due to --delete-data=on-conflict.");
360-
}
361366
if clear_database == ClearMode::Never {
362367
println!("{}", manual.reason);
363368
println!("Aborting publish due to required manual migration.");
364369
anyhow::bail!("Aborting because publishing would require manual migration or deletion of data and --delete-data was not specified.");
365370
}
366-
if clear_database == ClearMode::Always {
367-
println!("{}", manual.reason);
368-
println!("Proceeding with database clear due to --delete-data=always.");
369-
}
371+
println!("{}", manual.reason);
372+
println!("Proceeding with database clear due to --delete-data=on-conflict.");
373+
374+
builder = confirm_and_clear(name_or_identity, force, builder)?;
370375
}
371376
PrePublishResult::AutoMigrate(auto) => {
372377
println!("{}", auto.migrate_plan);

crates/cli/src/tasks/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@ pub fn build(
1313
project_path: &Path,
1414
lint_dir: Option<&Path>,
1515
build_debug: bool,
16+
features: Option<&std::ffi::OsString>,
1617
) -> anyhow::Result<(PathBuf, &'static str)> {
1718
let lang = util::detect_module_language(project_path)?;
19+
if features.is_some() && lang != ModuleLanguage::Rust {
20+
anyhow::bail!("The --features option is only supported for Rust modules.");
21+
}
1822
let output_path = match lang {
19-
ModuleLanguage::Rust => build_rust(project_path, lint_dir, build_debug),
23+
ModuleLanguage::Rust => build_rust(project_path, features, lint_dir, build_debug),
2024
ModuleLanguage::Csharp => build_csharp(project_path, build_debug),
2125
ModuleLanguage::Javascript => build_javascript(project_path, build_debug),
2226
}?;

0 commit comments

Comments
 (0)