From ff8c8b61a5238c521987a7dd7b7a506e74db926c Mon Sep 17 00:00:00 2001 From: Shefeek Jinnah Date: Wed, 23 Sep 2026 16:55:19 +0530 Subject: [PATCH 1/2] fix(tables): send --key-determines as constant_per_key runtimedb #1300 renamed the table declaration field from key_determines to constant_per_key, and the add-table request rejects unknown fields. The CLI still sent the old name, so `tables add --key-determines` got a 400. Send constant_per_key. The flag keeps its name. Move the body into table_declaration_body so the wire name is unit-tested. --- src/commands/databases.rs | 64 +++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/src/commands/databases.rs b/src/commands/databases.rs index 0e0b617..6b10604 100644 --- a/src/commands/databases.rs +++ b/src/commands/databases.rs @@ -958,6 +958,33 @@ fn partition_keys(values: &[String]) -> Result, String> { } /// `databases tables add` — declare a table on an existing instant database. +/// Build the request body for `POST /v1/databases/{id}/schemas/{schema}/tables`, +/// leaving out every empty list so the server applies its defaults. +fn table_declaration_body( + table: &str, + key: &[String], + key_determines: &[String], + sorted_by: &[serde_json::Value], + partition_by: &[serde_json::Value], +) -> serde_json::Value { + let mut body = serde_json::json!({ "name": table }); + if !key.is_empty() { + body["key"] = serde_json::json!(key); + } + if !key_determines.is_empty() { + // The API calls this `constant_per_key`; the flag keeps its original + // name. The server rejects unknown fields, so the old wire name fails. + body["constant_per_key"] = serde_json::json!(key_determines); + } + if !sorted_by.is_empty() { + body["sorted_by"] = serde_json::json!(sorted_by); + } + if !partition_by.is_empty() { + body["partition_by"] = serde_json::json!(partition_by); + } + body +} + #[allow(clippy::too_many_arguments)] pub fn add_table( workspace_id: &str, @@ -1001,19 +1028,7 @@ pub fn add_table( let api = Api::new(Some(workspace_id)); let db = resolve_database(&api, &database); - let mut body = serde_json::json!({ "name": table }); - if !key.is_empty() { - body["key"] = serde_json::json!(key); - } - if !key_determines.is_empty() { - body["key_determines"] = serde_json::json!(key_determines); - } - if !sorted_by.is_empty() { - body["sorted_by"] = serde_json::json!(sorted_by); - } - if !partition_by.is_empty() { - body["partition_by"] = serde_json::json!(partition_by); - } + let body = table_declaration_body(table, key, key_determines, &sorted_by, &partition_by); let (status, resp) = declare_table(&api, &db.id, schema, &body); @@ -4091,6 +4106,29 @@ mod tests { mock.assert(); } + #[test] + fn table_declaration_body_sends_key_determines_as_constant_per_key() { + let cols = |v: &[&str]| v.iter().map(|s| s.to_string()).collect::>(); + assert_eq!( + table_declaration_body( + "orders", + &cols(&["order_id"]), + &cols(&["event_date"]), + &[], + &[] + ), + serde_json::json!({ + "name": "orders", + "key": ["order_id"], + "constant_per_key": ["event_date"], + }) + ); + assert_eq!( + table_declaration_body("orders", &[], &[], &[], &[]), + serde_json::json!({"name": "orders"}) + ); + } + #[test] fn fork_database_request_defaults_name_to_source_label_fork() { let to_json = |r| serde_json::to_value(&r).unwrap(); From de0e182c9b392496304c7c80ed0bcc390e8c5c40 Mon Sep 17 00:00:00 2001 From: Shefeek Jinnah Date: Wed, 23 Sep 2026 17:05:18 +0530 Subject: [PATCH 2/2] docs(tables): move the add_table doc comment back onto add_table --- src/commands/databases.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/databases.rs b/src/commands/databases.rs index 6b10604..e46a780 100644 --- a/src/commands/databases.rs +++ b/src/commands/databases.rs @@ -957,7 +957,6 @@ fn partition_keys(values: &[String]) -> Result, String> { .collect() } -/// `databases tables add` — declare a table on an existing instant database. /// Build the request body for `POST /v1/databases/{id}/schemas/{schema}/tables`, /// leaving out every empty list so the server applies its defaults. fn table_declaration_body( @@ -985,6 +984,7 @@ fn table_declaration_body( body } +/// `databases tables add` — declare a table on an existing instant database. #[allow(clippy::too_many_arguments)] pub fn add_table( workspace_id: &str,