From 2dfda34dba95cc1dc73dbce412b52e6911eb455c Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Thu, 9 Apr 2026 11:36:46 -0400 Subject: [PATCH 01/10] Show database names in spacetime list --- crates/cli/src/subcommands/list.rs | 49 ++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/subcommands/list.rs b/crates/cli/src/subcommands/list.rs index d650e98f52d..5ba9c05e6f1 100644 --- a/crates/cli/src/subcommands/list.rs +++ b/crates/cli/src/subcommands/list.rs @@ -6,7 +6,9 @@ use crate::util::UNSTABLE_WARNING; use crate::Config; use anyhow::Context; use clap::{ArgMatches, Command}; +use futures::future::join_all; use serde::Deserialize; +use spacetimedb_client_api_messages::name::DatabaseName; use spacetimedb_lib::Identity; use tabled::{ settings::{object::Columns, Alignment, Modify, Style}, @@ -33,6 +35,12 @@ struct IdentityRow { pub db_identity: Identity, } +#[derive(Tabled)] +struct DatabaseRow { + pub db_names: String, + pub db_identity: Identity, +} + pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> { eprintln!("{UNSTABLE_WARNING}\n"); @@ -58,11 +66,12 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E .context("unable to retrieve databases for identity")?; if !result.identities.is_empty() { - let mut table = Table::new(result.identities); + let databases = lookup_database_names(&config, server, result.identities).await; + let mut table = Table::new(databases); table .with(Style::psql()) .with(Modify::new(Columns::first()).with(Alignment::left())); - println!("Associated database identities for {identity}:\n"); + println!("Associated databases for {identity}:\n"); println!("{table}"); } else { println!("No databases found for {identity}."); @@ -70,3 +79,39 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E Ok(()) } + +async fn lookup_database_names( + config: &Config, + server: Option<&str>, + identities: Vec, +) -> Vec { + let lookups = identities.iter().map(|row| async { + let result = util::spacetime_reverse_dns(config, &row.db_identity.to_string(), server).await; + (row.db_identity, result) + }); + + join_all(lookups) + .await + .into_iter() + .map(|(db_identity, result)| { + let db_names = match result { + Ok(response) if !response.names.is_empty() => format_database_names(response.names), + Ok(_) => "(unnamed)".to_string(), + Err(err) => { + eprintln!("Warning: failed to look up names for {db_identity}: {err}"); + "(lookup failed)".to_string() + } + }; + + DatabaseRow { db_names, db_identity } + }) + .collect() +} + +fn format_database_names(names: Vec) -> String { + names + .into_iter() + .map(|name| name.to_string()) + .collect::>() + .join(", ") +} From e232929917ad560f61f49721927a9354b419ae14 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Thu, 9 Apr 2026 11:50:43 -0400 Subject: [PATCH 02/10] Add smoketests for spacetime list names --- .../smoketests/tests/smoketests/cli/list.rs | 78 +++++++++++++++++++ crates/smoketests/tests/smoketests/cli/mod.rs | 1 + 2 files changed, 79 insertions(+) create mode 100644 crates/smoketests/tests/smoketests/cli/list.rs diff --git a/crates/smoketests/tests/smoketests/cli/list.rs b/crates/smoketests/tests/smoketests/cli/list.rs new file mode 100644 index 00000000000..b432c7c981a --- /dev/null +++ b/crates/smoketests/tests/smoketests/cli/list.rs @@ -0,0 +1,78 @@ +//! CLI list command tests + +use spacetimedb_smoketests::{require_local_server, Smoketest}; +use std::process::Output; + +fn output_stdout(output: &Output) -> String { + String::from_utf8_lossy(&output.stdout).to_string() +} + +fn output_stderr(output: &Output) -> String { + String::from_utf8_lossy(&output.stderr).to_string() +} + +fn assert_success(output: &Output, context: &str) { + assert!( + output.status.success(), + "{context} failed:\nstdout: {}\nstderr: {}", + output_stdout(output), + output_stderr(output), + ); +} + +#[test] +fn cli_list_shows_database_names_and_identities() { + require_local_server!(); + let mut test = Smoketest::builder().autopublish(false).build(); + + let primary_name = format!("list-db-{}", std::process::id()); + let alias_name = format!("{primary_name}-alias"); + let second_alias_name = format!("{primary_name}-alt"); + let identity = test.publish_module_named(&primary_name, false).unwrap(); + + let json_body = format!(r#"["{}","{}"]"#, alias_name, second_alias_name); + let response = test + .api_call_json("PUT", &format!("/v1/database/{primary_name}/names"), &json_body) + .unwrap(); + assert_eq!( + response.status_code, + 200, + "Expected 200 status when replacing names, got {}: {}", + response.status_code, + String::from_utf8_lossy(&response.body) + ); + + let output = test.spacetime_cmd(&["list", "--server", &test.server_url]); + assert_success(&output, "spacetime list"); + + let stdout = output_stdout(&output); + assert!(stdout.contains("db_names"), "missing db_names column:\n{stdout}"); + assert!(stdout.contains("db_identity"), "missing db_identity column:\n{stdout}"); + assert!(stdout.contains(&alias_name), "missing alias name in output:\n{stdout}"); + assert!( + stdout.contains(&second_alias_name), + "missing second alias name in output:\n{stdout}" + ); + assert!( + stdout.contains(&identity), + "missing database identity in output:\n{stdout}" + ); +} + +#[test] +fn cli_list_marks_unnamed_databases() { + require_local_server!(); + let mut test = Smoketest::builder().autopublish(false).build(); + + let identity = test.publish_module().unwrap(); + + let output = test.spacetime_cmd(&["list", "--server", &test.server_url]); + assert_success(&output, "spacetime list"); + + let stdout = output_stdout(&output); + assert!(stdout.contains("(unnamed)"), "missing unnamed marker:\n{stdout}"); + assert!( + stdout.contains(&identity), + "missing database identity in output:\n{stdout}" + ); +} diff --git a/crates/smoketests/tests/smoketests/cli/mod.rs b/crates/smoketests/tests/smoketests/cli/mod.rs index d54c9749851..60ece75d377 100644 --- a/crates/smoketests/tests/smoketests/cli/mod.rs +++ b/crates/smoketests/tests/smoketests/cli/mod.rs @@ -1,5 +1,6 @@ pub mod auth; pub mod dev; pub mod generate; +pub mod list; pub mod publish; pub mod server; From b63060b956811bb9b21c6959d4c0cfb3745582b5 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Tue, 14 Apr 2026 12:08:17 -0400 Subject: [PATCH 03/10] refactor: simplify database name lookup --- crates/cli/src/subcommands/list.rs | 58 +++++++++++++----------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/crates/cli/src/subcommands/list.rs b/crates/cli/src/subcommands/list.rs index 5ba9c05e6f1..ff02685245f 100644 --- a/crates/cli/src/subcommands/list.rs +++ b/crates/cli/src/subcommands/list.rs @@ -8,7 +8,6 @@ use anyhow::Context; use clap::{ArgMatches, Command}; use futures::future::join_all; use serde::Deserialize; -use spacetimedb_client_api_messages::name::DatabaseName; use spacetimedb_lib::Identity; use tabled::{ settings::{object::Columns, Alignment, Modify, Style}, @@ -66,7 +65,16 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E .context("unable to retrieve databases for identity")?; if !result.identities.is_empty() { - let databases = lookup_database_names(&config, server, result.identities).await; + let config = &config; + let databases = join_all(result.identities.into_iter().map(|row| async move { + let db_identity = row.db_identity; + let db_names = lookup_database_names(config, server, db_identity).await; + DatabaseRow { + db_names: format_database_names(db_names), + db_identity, + } + })) + .await; let mut table = Table::new(databases); table .with(Style::psql()) @@ -80,38 +88,20 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E Ok(()) } -async fn lookup_database_names( - config: &Config, - server: Option<&str>, - identities: Vec, -) -> Vec { - let lookups = identities.iter().map(|row| async { - let result = util::spacetime_reverse_dns(config, &row.db_identity.to_string(), server).await; - (row.db_identity, result) - }); - - join_all(lookups) - .await - .into_iter() - .map(|(db_identity, result)| { - let db_names = match result { - Ok(response) if !response.names.is_empty() => format_database_names(response.names), - Ok(_) => "(unnamed)".to_string(), - Err(err) => { - eprintln!("Warning: failed to look up names for {db_identity}: {err}"); - "(lookup failed)".to_string() - } - }; - - DatabaseRow { db_names, db_identity } - }) - .collect() +async fn lookup_database_names(config: &Config, server: Option<&str>, db_identity: Identity) -> Vec { + match util::spacetime_reverse_dns(config, &db_identity.to_string(), server).await { + Ok(response) => response.names.into_iter().map(|name| name.to_string()).collect(), + Err(err) => { + eprintln!("Warning: failed to look up names for {db_identity}: {err}"); + vec!["(lookup failed)".to_string()] + } + } } -fn format_database_names(names: Vec) -> String { - names - .into_iter() - .map(|name| name.to_string()) - .collect::>() - .join(", ") +fn format_database_names(names: Vec) -> String { + if names.is_empty() { + "(unnamed)".to_string() + } else { + names.join(", ") + } } From 4476ae7bb4e2dcdef29d9ca978aa3798e636055d Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Tue, 14 Apr 2026 12:10:29 -0400 Subject: [PATCH 04/10] refactor: rename database row assembly helper --- crates/cli/src/subcommands/list.rs | 35 +++++++++++++++--------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/crates/cli/src/subcommands/list.rs b/crates/cli/src/subcommands/list.rs index ff02685245f..c60a0eab417 100644 --- a/crates/cli/src/subcommands/list.rs +++ b/crates/cli/src/subcommands/list.rs @@ -65,16 +65,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E .context("unable to retrieve databases for identity")?; if !result.identities.is_empty() { - let config = &config; - let databases = join_all(result.identities.into_iter().map(|row| async move { - let db_identity = row.db_identity; - let db_names = lookup_database_names(config, server, db_identity).await; - DatabaseRow { - db_names: format_database_names(db_names), - db_identity, - } - })) - .await; + let databases = assemble_rows(&config, server, result.identities).await; let mut table = Table::new(databases); table .with(Style::psql()) @@ -88,14 +79,24 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E Ok(()) } -async fn lookup_database_names(config: &Config, server: Option<&str>, db_identity: Identity) -> Vec { - match util::spacetime_reverse_dns(config, &db_identity.to_string(), server).await { - Ok(response) => response.names.into_iter().map(|name| name.to_string()).collect(), - Err(err) => { - eprintln!("Warning: failed to look up names for {db_identity}: {err}"); - vec!["(lookup failed)".to_string()] +async fn assemble_rows(config: &Config, server: Option<&str>, identities: Vec) -> Vec { + let lookups = identities.into_iter().map(|row| async move { + let db_identity = row.db_identity; + let db_names = match util::spacetime_reverse_dns(config, &db_identity.to_string(), server).await { + Ok(response) => response.names.into_iter().map(|name| name.to_string()).collect(), + Err(err) => { + eprintln!("Warning: failed to look up names for {db_identity}: {err}"); + vec!["(lookup failed)".to_string()] + } + }; + + DatabaseRow { + db_names: format_database_names(db_names), + db_identity, } - } + }); + + join_all(lookups).await } fn format_database_names(names: Vec) -> String { From 046920a6dbb9cfa8aa7b918bd8d303be90857fd7 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 14 Apr 2026 09:18:53 -0700 Subject: [PATCH 05/10] [bot/list-database-names]: rename columns --- crates/cli/src/subcommands/list.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/cli/src/subcommands/list.rs b/crates/cli/src/subcommands/list.rs index 5ba9c05e6f1..5e26862ed28 100644 --- a/crates/cli/src/subcommands/list.rs +++ b/crates/cli/src/subcommands/list.rs @@ -37,7 +37,9 @@ struct IdentityRow { #[derive(Tabled)] struct DatabaseRow { + #[tabled(rename = "Database Name(s)")] pub db_names: String, + #[tabled(rename = "Identity")] pub db_identity: Identity, } From 6e4cc05b24aee131c3b03c0e3e3ff7a4fd25598d Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 14 Apr 2026 09:19:55 -0700 Subject: [PATCH 06/10] [bot/list-database-names]: reformat --- crates/cli/src/subcommands/list.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/cli/src/subcommands/list.rs b/crates/cli/src/subcommands/list.rs index dabae9140bb..88749e51e82 100644 --- a/crates/cli/src/subcommands/list.rs +++ b/crates/cli/src/subcommands/list.rs @@ -72,7 +72,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E table .with(Style::psql()) .with(Modify::new(Columns::first()).with(Alignment::left())); - println!("Associated databases for {identity}:\n"); + println!("Associated databases for user {identity}:\n"); println!("{table}"); } else { println!("No databases found for {identity}."); @@ -93,18 +93,10 @@ async fn assemble_rows(config: &Config, server: Option<&str>, identities: Vec) -> String { - if names.is_empty() { - "(unnamed)".to_string() - } else { - names.join(", ") - } -} From e932edfa3b0b1ba2f6ceac1965430d2310151e99 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Tue, 14 Apr 2026 09:24:19 -0700 Subject: [PATCH 07/10] Apply suggestion from @bfops Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> --- crates/cli/src/subcommands/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cli/src/subcommands/list.rs b/crates/cli/src/subcommands/list.rs index 88749e51e82..64a1ed2d4c8 100644 --- a/crates/cli/src/subcommands/list.rs +++ b/crates/cli/src/subcommands/list.rs @@ -88,7 +88,7 @@ async fn assemble_rows(config: &Config, server: Option<&str>, identities: Vec response.names.into_iter().map(|name| name.to_string()).collect(), Err(err) => { eprintln!("Warning: failed to look up names for {db_identity}: {err}"); - vec!["(lookup failed)".to_string()] + vec![] } }; From b11958ff316d9aa515b7a5d2997bcc02f8384744 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Tue, 14 Apr 2026 12:30:35 -0400 Subject: [PATCH 08/10] test: update list smoketest expectations --- crates/smoketests/tests/smoketests/cli/list.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/smoketests/tests/smoketests/cli/list.rs b/crates/smoketests/tests/smoketests/cli/list.rs index b432c7c981a..a0ce56ad686 100644 --- a/crates/smoketests/tests/smoketests/cli/list.rs +++ b/crates/smoketests/tests/smoketests/cli/list.rs @@ -46,8 +46,11 @@ fn cli_list_shows_database_names_and_identities() { assert_success(&output, "spacetime list"); let stdout = output_stdout(&output); - assert!(stdout.contains("db_names"), "missing db_names column:\n{stdout}"); - assert!(stdout.contains("db_identity"), "missing db_identity column:\n{stdout}"); + assert!( + stdout.contains("Database Name(s)"), + "missing Database Name(s) column:\n{stdout}" + ); + assert!(stdout.contains("Identity"), "missing Identity column:\n{stdout}"); assert!(stdout.contains(&alias_name), "missing alias name in output:\n{stdout}"); assert!( stdout.contains(&second_alias_name), @@ -70,7 +73,15 @@ fn cli_list_marks_unnamed_databases() { assert_success(&output, "spacetime list"); let stdout = output_stdout(&output); - assert!(stdout.contains("(unnamed)"), "missing unnamed marker:\n{stdout}"); + assert!( + stdout.contains("Associated databases for user"), + "missing updated heading:\n{stdout}" + ); + assert!( + stdout.contains("Database Name(s)"), + "missing Database Name(s) column:\n{stdout}" + ); + assert!(stdout.contains("Identity"), "missing Identity column:\n{stdout}"); assert!( stdout.contains(&identity), "missing database identity in output:\n{stdout}" From f91ef5f2a305da5eb6b9a2d53a60721058f18ff6 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 14 Apr 2026 09:34:33 -0700 Subject: [PATCH 09/10] [bot/list-database-names]: review --- crates/cli/src/subcommands/list.rs | 35 +++++++++++------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/crates/cli/src/subcommands/list.rs b/crates/cli/src/subcommands/list.rs index 88749e51e82..6c4c483e22d 100644 --- a/crates/cli/src/subcommands/list.rs +++ b/crates/cli/src/subcommands/list.rs @@ -25,13 +25,7 @@ pub fn cli() -> Command { #[derive(Deserialize)] struct DatabasesResult { - pub identities: Vec, -} - -#[derive(Tabled, Deserialize)] -#[serde(transparent)] -struct IdentityRow { - pub db_identity: Identity, + pub identities: Vec, } #[derive(Tabled)] @@ -67,7 +61,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E .context("unable to retrieve databases for identity")?; if !result.identities.is_empty() { - let databases = assemble_rows(&config, server, result.identities).await; + let databases = assemble_rows(&config, server, result.identities).await?; let mut table = Table::new(databases); table .with(Style::psql()) @@ -81,22 +75,19 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E Ok(()) } -async fn assemble_rows(config: &Config, server: Option<&str>, identities: Vec) -> Vec { - let lookups = identities.into_iter().map(|row| async move { - let db_identity = row.db_identity; - let db_names = match util::spacetime_reverse_dns(config, &db_identity.to_string(), server).await { - Ok(response) => response.names.into_iter().map(|name| name.to_string()).collect(), - Err(err) => { - eprintln!("Warning: failed to look up names for {db_identity}: {err}"); - vec!["(lookup failed)".to_string()] - } - }; +async fn assemble_rows( + config: &Config, + server: Option<&str>, + identities: Vec, +) -> anyhow::Result> { + let lookups = identities.into_iter().map(|db_identity| async move { + let response = util::spacetime_reverse_dns(config, &db_identity.to_string(), server).await?; + let db_names: Vec<_> = response.names.into_iter().map(|name| name.to_string()).collect(); - DatabaseRow { + Ok(DatabaseRow { db_names: db_names.join(", "), db_identity, - } + }) }); - - join_all(lookups).await + join_all(lookups).await.into_iter().collect::>>() } From 1e6c5e3f58a1e34c6315e09def9b2af92072f238 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 14 Apr 2026 09:35:54 -0700 Subject: [PATCH 10/10] [bot/list-database-names]: remove test --- .../smoketests/tests/smoketests/cli/list.rs | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/crates/smoketests/tests/smoketests/cli/list.rs b/crates/smoketests/tests/smoketests/cli/list.rs index a0ce56ad686..54c03144733 100644 --- a/crates/smoketests/tests/smoketests/cli/list.rs +++ b/crates/smoketests/tests/smoketests/cli/list.rs @@ -61,29 +61,3 @@ fn cli_list_shows_database_names_and_identities() { "missing database identity in output:\n{stdout}" ); } - -#[test] -fn cli_list_marks_unnamed_databases() { - require_local_server!(); - let mut test = Smoketest::builder().autopublish(false).build(); - - let identity = test.publish_module().unwrap(); - - let output = test.spacetime_cmd(&["list", "--server", &test.server_url]); - assert_success(&output, "spacetime list"); - - let stdout = output_stdout(&output); - assert!( - stdout.contains("Associated databases for user"), - "missing updated heading:\n{stdout}" - ); - assert!( - stdout.contains("Database Name(s)"), - "missing Database Name(s) column:\n{stdout}" - ); - assert!(stdout.contains("Identity"), "missing Identity column:\n{stdout}"); - assert!( - stdout.contains(&identity), - "missing database identity in output:\n{stdout}" - ); -}