Skip to content

Commit 2ff8b60

Browse files
committed
feat(databases): add --catalog flag to databases create
Maps to the default_catalog field on POST /v1/databases, separating the human-readable --name from the SQL catalog alias used in queries. Also fixes config tests that failed when HOTDATA_API_KEY was set in the environment by unsetting it inside the with_temp_config_dir test helper.
1 parent ccd0fd9 commit 2ff8b60

4 files changed

Lines changed: 44 additions & 16 deletions

File tree

src/command.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -563,13 +563,16 @@ pub enum DatabasesCommands {
563563

564564
/// Create a new managed database
565565
Create {
566-
/// SQL catalog alias — becomes the catalog name in queries:
567-
/// SELECT … FROM <name>.public.<table>.
568-
/// Must be [a-z_][a-z0-9_]*, globally unique. When provided the
569-
/// database defaults to no expiry; omit for an anonymous 24h sandbox.
566+
/// Human-readable display name for the database (e.g. "Sales reporting").
570567
#[arg(long)]
571568
name: Option<String>,
572569

570+
/// SQL catalog alias used in queries: SELECT … FROM <catalog>.schema.table.
571+
/// Must be [a-z_][a-z0-9_]*, globally unique. When provided the database
572+
/// defaults to no expiry; omit for an anonymous 24h sandbox.
573+
#[arg(long)]
574+
catalog: Option<String>,
575+
573576
/// Default schema for bare `--table` entries (default: public).
574577
/// Use dot notation in `--table` to target a different schema directly,
575578
/// e.g. `--table raw.raw_orders` always goes into the "raw" schema.
@@ -583,8 +586,8 @@ pub enum DatabasesCommands {
583586
tables: Vec<String>,
584587

585588
/// When the database expires. Accepts a relative duration (e.g. 24h, 7d, 90m)
586-
/// or an RFC 3339 timestamp. Omitting with --name means no expiry; omitting
587-
/// without --name defaults to 24h.
589+
/// or an RFC 3339 timestamp. Omitting with --catalog means no expiry; omitting
590+
/// without --catalog defaults to 24h.
588591
#[arg(long)]
589592
expires_at: Option<String>,
590593

src/config.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,10 @@ pub mod test_helpers {
354354
let guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
355355
let tmp = tempfile::tempdir().unwrap();
356356
// SAFETY: tests are serialized via ENV_LOCK mutex, so no concurrent env mutation.
357-
unsafe { std::env::set_var("HOTDATA_CONFIG_DIR", tmp.path()) };
357+
unsafe {
358+
std::env::set_var("HOTDATA_CONFIG_DIR", tmp.path());
359+
std::env::remove_var("HOTDATA_API_KEY");
360+
}
358361
(tmp, guard)
359362
}
360363
}

src/databases.rs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ struct CreateDatabaseResponse {
6666
id: String,
6767
#[serde(default)]
6868
name: Option<String>,
69+
#[serde(default)]
70+
default_catalog: Option<String>,
6971
default_connection_id: String,
7072
#[serde(default)]
7173
expires_at: Option<String>,
@@ -143,6 +145,7 @@ fn schema_name(schema: Option<&str>) -> &str {
143145
/// Build the request body for `POST /v1/databases`.
144146
pub fn create_database_request(
145147
name: Option<&str>,
148+
catalog: Option<&str>,
146149
schema: &str,
147150
tables: &[String],
148151
expires_at: Option<&str>,
@@ -156,6 +159,13 @@ pub fn create_database_request(
156159
);
157160
}
158161

162+
if let Some(c) = catalog {
163+
req.insert(
164+
"default_catalog".to_string(),
165+
serde_json::Value::String(c.to_string()),
166+
);
167+
}
168+
159169
if !tables.is_empty() {
160170
// Group tables by schema, preserving insertion order.
161171
// Dot-notation entries (e.g. "raw.raw_orders") use the named schema;
@@ -459,7 +469,7 @@ fn create_and_return_id(
459469
expires_at: Option<&str>,
460470
) -> String {
461471
use crossterm::style::Stylize;
462-
let body = create_database_request(name, schema, tables, expires_at);
472+
let body = create_database_request(name, None, schema, tables, expires_at);
463473
let (status, resp_body) = api.post_raw("/databases", &body);
464474
if !status.is_success() {
465475
eprintln!("{}", crate::util::api_error(resp_body).red());
@@ -554,14 +564,15 @@ pub fn run(
554564
pub fn create(
555565
workspace_id: &str,
556566
name: Option<&str>,
567+
catalog: Option<&str>,
557568
schema: &str,
558569
tables: &[String],
559570
expires_at: Option<&str>,
560571
format: &str,
561572
) {
562573
use crossterm::style::Stylize;
563574

564-
let body = create_database_request(name, schema, tables, expires_at);
575+
let body = create_database_request(name, catalog, schema, tables, expires_at);
565576

566577
let api = ApiClient::new(Some(workspace_id));
567578
let spinner = (format == "table").then(|| crate::util::spinner("Creating database..."));
@@ -596,12 +607,17 @@ pub fn create(
596607
if let Some(n) = &result.name {
597608
println!("name: {}", n.clone().cyan());
598609
}
610+
if let Some(c) = &result.default_catalog {
611+
println!("catalog: {}", c.clone().cyan());
612+
}
599613
println!("id: {}", result.id);
600614
if let Some(exp) = &result.expires_at {
601615
println!("expires_at: {exp}");
602616
}
603617
println!();
604-
let catalog = result.name.as_deref().unwrap_or("default");
618+
let catalog = result.default_catalog.as_deref()
619+
.or(result.name.as_deref())
620+
.unwrap_or("default");
605621
println!(
606622
"{}",
607623
format!(
@@ -823,20 +839,21 @@ mod tests {
823839

824840
#[test]
825841
fn create_database_request_empty_without_name_or_tables() {
826-
let req = create_database_request(None, "public", &[], None);
842+
let req = create_database_request(None, None, "public", &[], None);
827843
assert_eq!(req, serde_json::json!({}));
828844
}
829845

830846
#[test]
831847
fn create_database_request_includes_name() {
832-
let req = create_database_request(Some("jaffle_shop"), "public", &[], None);
848+
let req = create_database_request(Some("jaffle_shop"), None, "public", &[], None);
833849
assert_eq!(req["name"], "jaffle_shop");
834850
assert!(req.get("schemas").is_none());
835851
}
836852

837853
#[test]
838854
fn create_database_request_includes_schemas_when_tables_declared() {
839855
let req = create_database_request(
856+
None,
840857
None,
841858
"public",
842859
&["orders".to_string(), "customers".to_string()],
@@ -849,26 +866,27 @@ mod tests {
849866

850867
#[test]
851868
fn create_database_request_schemas_without_name() {
852-
let req = create_database_request(None, "analytics", &["events".to_string()], None);
869+
let req = create_database_request(None, None, "analytics", &["events".to_string()], None);
853870
assert!(req.get("name").is_none());
854871
assert_eq!(req["schemas"][0]["name"], "analytics");
855872
}
856873

857874
#[test]
858875
fn create_database_request_includes_expires_at_when_provided() {
859-
let req = create_database_request(None, "public", &[], Some("24h"));
876+
let req = create_database_request(None, None, "public", &[], Some("24h"));
860877
assert_eq!(req["expires_at"], "24h");
861878
}
862879

863880
#[test]
864881
fn create_database_request_omits_expires_at_when_none() {
865-
let req = create_database_request(None, "public", &[], None);
882+
let req = create_database_request(None, None, "public", &[], None);
866883
assert!(req.get("expires_at").is_none());
867884
}
868885

869886
#[test]
870887
fn create_database_request_dot_notation_groups_tables_by_schema() {
871888
let req = create_database_request(
889+
None,
872890
None,
873891
"public",
874892
&[
@@ -1066,6 +1084,7 @@ mod tests {
10661084
.match_body(mockito::Matcher::JsonString(
10671085
serde_json::to_string(&create_database_request(
10681086
Some("mydb"),
1087+
None,
10691088
"public",
10701089
&["gdp".to_string()],
10711090
None,
@@ -1075,7 +1094,7 @@ mod tests {
10751094
.create();
10761095

10771096
let api = ApiClient::test_new(&server.url(), "k", Some("ws-test"));
1078-
let body = create_database_request(Some("mydb"), "public", &["gdp".to_string()], None);
1097+
let body = create_database_request(Some("mydb"), None, "public", &["gdp".to_string()], None);
10791098
let (status, resp_body) = api.post_raw("/databases", &body);
10801099
assert_eq!(status.as_u16(), 201);
10811100
let parsed: CreateDatabaseResponse = serde_json::from_str(&resp_body).unwrap();
@@ -1184,6 +1203,7 @@ mod tests {
11841203
.mock("POST", "/databases")
11851204
.match_body(mockito::Matcher::Json(create_database_request(
11861205
Some("scratch"),
1206+
None,
11871207
"public",
11881208
&[],
11891209
None,

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,13 +432,15 @@ fn main() {
432432
}
433433
Some(DatabasesCommands::Create {
434434
name,
435+
catalog,
435436
schema,
436437
tables,
437438
expires_at,
438439
output,
439440
}) => databases::create(
440441
&workspace_id,
441442
name.as_deref(),
443+
catalog.as_deref(),
442444
&schema,
443445
&tables,
444446
expires_at.as_deref(),

0 commit comments

Comments
 (0)