From 8ce50289e3ca9ece6756140bded3151002e6e0ba Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Fri, 22 May 2026 19:49:47 -0700 Subject: [PATCH 1/3] feat(context): scope context commands to active database Context commands now call /v1/databases/{database_id}/context instead of /v1/context. A --database / -d flag is added to the context command; when omitted it falls back to the active database set via 'hotdata databases set'. Exits with a clear error if no database is active. Paired with hotdata-dev/runtimedb#474. --- src/command.rs | 10 +++++++--- src/context.rs | 37 +++++++++++++++++++------------------ src/main.rs | 19 +++++++++++++++---- 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/command.rs b/src/command.rs index e1d8a56..0c1a281 100644 --- a/src/command.rs +++ b/src/command.rs @@ -228,12 +228,16 @@ pub enum Commands { command: Option, }, - /// Sync workspace text context with local Markdown (`./.md` in the current directory) + /// Sync database context with local Markdown (`./.md` in the current directory) Context { /// Workspace ID (defaults to first workspace from login) #[arg(long, short = 'w', global = true)] workspace_id: Option, + /// Database ID (defaults to active database set via 'hotdata databases set') + #[arg(long, short = 'd', global = true)] + database_id: Option, + #[command(subcommand)] command: ContextCommands, }, @@ -852,7 +856,7 @@ pub enum ContextCommands { name: String, }, - /// Download context from the workspace to ./.md + /// Download context from the database to ./.md Pull { /// Context name (trailing `.md` ignored, e.g. `USER.md` → `USER`) name: String, @@ -866,7 +870,7 @@ pub enum ContextCommands { dry_run: bool, }, - /// Upload ./.md to the workspace as named context + /// Upload ./.md to the database as named context Push { /// Context name (trailing `.md` ignored, e.g. `USER.md` → `USER`; reads `./USER.md`) name: String, diff --git a/src/context.rs b/src/context.rs index 2a38650..8b7a44f 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,4 +1,4 @@ -//! Workspace context: `/v1/context` sync with `./{NAME}.md` in the current directory. +//! Database context: `/v1/databases/{id}/context` sync with `./{NAME}.md` in the current directory. use crate::api::ApiClient; use crossterm::style::Stylize; @@ -28,7 +28,7 @@ static RESERVED_WORDS: LazyLock> = LazyLock::new(|| { }); #[derive(Debug, Deserialize, Serialize)] -struct WorkspaceContextEntry { +struct DatabaseContextEntry { name: String, content: String, updated_at: String, @@ -36,17 +36,17 @@ struct WorkspaceContextEntry { #[derive(Deserialize)] struct ListResponse { - contexts: Vec, + contexts: Vec, } #[derive(Deserialize)] struct GetResponse { - context: WorkspaceContextEntry, + context: DatabaseContextEntry, } #[derive(Deserialize)] struct UpsertResponse { - context: WorkspaceContextEntry, + context: DatabaseContextEntry, } /// Normalizes a context name from the CLI: trims, takes the final path segment, and strips a @@ -124,9 +124,10 @@ fn local_md_path(name: &str) -> PathBuf { fn fetch_context( api: &ApiClient, + database_id: &str, name: &str, -) -> Result { - let path = format!("/context/{name}"); +) -> Result { + let path = format!("/databases/{database_id}/context/{name}"); let (status, body) = api.get_raw(&path); if status == reqwest::StatusCode::NOT_FOUND { return Err(status); @@ -143,11 +144,11 @@ fn fetch_context( Ok(parsed.context) } -pub fn list(workspace_id: &str, prefix: Option<&str>, format: &str) { +pub fn list(workspace_id: &str, database_id: &str, prefix: Option<&str>, format: &str) { let api = ApiClient::new(Some(workspace_id)); - let body: ListResponse = api.get("/context"); + let body: ListResponse = api.get(&format!("/databases/{database_id}/context")); - let mut rows: Vec = body.contexts; + let mut rows: Vec = body.contexts; if let Some(p) = prefix { rows.retain(|c| c.name.starts_with(p)); } @@ -176,7 +177,7 @@ pub fn list(workspace_id: &str, prefix: Option<&str>, format: &str) { } } -pub fn show(workspace_id: &str, name: &str) { +pub fn show(workspace_id: &str, database_id: &str, name: &str) { let name = normalize_context_cli_name(name); if let Err(e) = validate_context_stem(&name) { eprintln!("error: {e}"); @@ -184,7 +185,7 @@ pub fn show(workspace_id: &str, name: &str) { } let api = ApiClient::new(Some(workspace_id)); - match fetch_context(&api, &name) { + match fetch_context(&api, database_id, &name) { Ok(ctx) => { print!("{}", ctx.content); if !ctx.content.ends_with('\n') { @@ -194,7 +195,7 @@ pub fn show(workspace_id: &str, name: &str) { Err(reqwest::StatusCode::NOT_FOUND) => { eprintln!( "{}", - format!("error: no context named '{name}' in this workspace.").red() + format!("error: no context named '{name}' in this database.").red() ); eprintln!( "{}", @@ -207,7 +208,7 @@ pub fn show(workspace_id: &str, name: &str) { } } -pub fn pull(workspace_id: &str, name: &str, force: bool, dry_run: bool) { +pub fn pull(workspace_id: &str, database_id: &str, name: &str, force: bool, dry_run: bool) { let name = normalize_context_cli_name(name); if let Err(e) = validate_context_stem(&name) { eprintln!("error: {e}"); @@ -229,12 +230,12 @@ pub fn pull(workspace_id: &str, name: &str, force: bool, dry_run: bool) { } let api = ApiClient::new(Some(workspace_id)); - let ctx = match fetch_context(&api, &name) { + let ctx = match fetch_context(&api, database_id, &name) { Ok(c) => c, Err(reqwest::StatusCode::NOT_FOUND) => { eprintln!( "{}", - format!("error: no context named '{name}' in this workspace.").red() + format!("error: no context named '{name}' in this database.").red() ); std::process::exit(1); } @@ -270,7 +271,7 @@ pub fn pull(workspace_id: &str, name: &str, force: bool, dry_run: bool) { ); } -pub fn push(workspace_id: &str, name: &str, dry_run: bool) { +pub fn push(workspace_id: &str, database_id: &str, name: &str, dry_run: bool) { let name = normalize_context_cli_name(name); if let Err(e) = validate_context_stem(&name) { eprintln!("error: {e}"); @@ -310,7 +311,7 @@ pub fn push(workspace_id: &str, name: &str, dry_run: bool) { let api = ApiClient::new(Some(workspace_id)); let body = json!({ "name": &name, "content": content }); - let resp: UpsertResponse = api.post("/context", &body); + let resp: UpsertResponse = api.post(&format!("/databases/{database_id}/context"), &body); println!( "{}", diff --git a/src/main.rs b/src/main.rs index 8286e4f..6fc173e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -939,21 +939,32 @@ fn main() { } Commands::Context { workspace_id, + database_id, command, } => { let workspace_id = resolve_workspace(workspace_id); + let database_id = database_id + .or_else(|| config::load_current_database("default", &workspace_id)) + .unwrap_or_else(|| { + eprintln!( + "error: no active database. Use 'hotdata databases set ' to set one, or pass --database." + ); + std::process::exit(1); + }); match command { ContextCommands::List { output, prefix } => { - context::list(&workspace_id, prefix.as_deref(), &output) + context::list(&workspace_id, &database_id, prefix.as_deref(), &output) + } + ContextCommands::Show { name } => { + context::show(&workspace_id, &database_id, &name) } - ContextCommands::Show { name } => context::show(&workspace_id, &name), ContextCommands::Pull { name, force, dry_run, - } => context::pull(&workspace_id, &name, force, dry_run), + } => context::pull(&workspace_id, &database_id, &name, force, dry_run), ContextCommands::Push { name, dry_run } => { - context::push(&workspace_id, &name, dry_run) + context::push(&workspace_id, &database_id, &name, dry_run) } } } From 3efa78392a917221ca5dfc43ec6a77a52ab145ac Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Fri, 22 May 2026 19:53:40 -0700 Subject: [PATCH 2/3] fix(context): correct --database-id flag name in error message --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 6fc173e..66e84ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -947,7 +947,7 @@ fn main() { .or_else(|| config::load_current_database("default", &workspace_id)) .unwrap_or_else(|| { eprintln!( - "error: no active database. Use 'hotdata databases set ' to set one, or pass --database." + "error: no active database. Use 'hotdata databases set ' to set one, or pass --database-id." ); std::process::exit(1); }); From e4033f8fd079605d6e57336a83662792d38c8df4 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Fri, 22 May 2026 20:38:44 -0700 Subject: [PATCH 3/3] fix(datasets): add missing type discriminator to dataset source payloads The DatasetSource schema is a tagged union requiring a 'type' field. Without it the API rejects both create-from-query and create-from-saved-query with a deserialization error. --- src/datasets.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/datasets.rs b/src/datasets.rs index 65219ee..1cb6548 100644 --- a/src/datasets.rs +++ b/src/datasets.rs @@ -107,7 +107,7 @@ fn create_dataset( pub fn create_from_query(workspace_id: &str, sql: &str, description: Option<&str>, name: &str) { let api = ApiClient::new(Some(workspace_id)); - create_dataset(&api, description, name, json!({ "sql": sql })); + create_dataset(&api, description, name, json!({ "type": "sql_query", "sql": sql })); } pub fn create_from_saved_query( @@ -117,7 +117,7 @@ pub fn create_from_saved_query( name: &str, ) { let api = ApiClient::new(Some(workspace_id)); - create_dataset(&api, description, name, json!({ "saved_query_id": query_id })); + create_dataset(&api, description, name, json!({ "type": "saved_query", "saved_query_id": query_id })); } pub fn list(workspace_id: &str, limit: Option, offset: Option, format: &str) {