Skip to content

Commit 4da7330

Browse files
committed
fix(cli): resolve 6 bugs found during E2E testing
- datasets create: add missing -o/--output flag; move banner to stderr - sandbox new: move "Sandbox created" banner to stderr so -o json is parseable - sandbox read: use println! instead of print! to ensure trailing newline - sandbox: add delete subcommand (DELETE /sandboxes/{id}) - workspaces set: check HOTDATA_SANDBOX instead of HOTDATA_WORKSPACE; update error message - context push: surface friendly hint when server blocks push inside an active sandbox
1 parent 77ef3bb commit 4da7330

6 files changed

Lines changed: 75 additions & 16 deletions

File tree

src/command.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,10 @@ pub enum DatasetsCommands {
478478
/// Saved query ID to create the dataset from
479479
#[arg(long, conflicts_with = "sql", required_unless_present = "sql")]
480480
query_id: Option<String>,
481+
482+
/// Output format
483+
#[arg(long = "output", short = 'o', default_value = "table", value_parser = ["table", "json", "yaml"])]
484+
output: String,
481485
},
482486

483487
/// Update a dataset's description and/or name
@@ -854,6 +858,12 @@ pub enum SandboxCommands {
854858
#[arg(trailing_var_arg = true, required = true)]
855859
cmd: Vec<String>,
856860
},
861+
862+
/// Delete a sandbox permanently
863+
Delete {
864+
/// Sandbox ID to delete
865+
id: String,
866+
},
857867
}
858868

859869
#[derive(Subcommand)]

src/context.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,28 @@ pub fn push(workspace_id: &str, database_id: &str, name: &str, dry_run: bool) {
311311

312312
let api = ApiClient::new(Some(workspace_id));
313313
let body = json!({ "name": &name, "content": content });
314-
let resp: UpsertResponse = api.post(&format!("/databases/{database_id}/context"), &body);
314+
let (status, resp_body) = api.post_raw(&format!("/databases/{database_id}/context"), &body);
315+
316+
if !status.is_success() {
317+
let msg = crate::util::api_error(resp_body.clone());
318+
if msg.to_lowercase().contains("not allowed within a session") {
319+
eprintln!("{}", msg.red());
320+
eprintln!(
321+
"{}",
322+
"hint: context push is blocked inside an active sandbox. \
323+
Run 'hotdata sandbox set' (no args) to clear the active sandbox first."
324+
.dark_grey()
325+
);
326+
} else {
327+
eprintln!("{}", msg.red());
328+
}
329+
std::process::exit(1);
330+
}
331+
332+
let resp: UpsertResponse = serde_json::from_str(&resp_body).unwrap_or_else(|e| {
333+
eprintln!("error parsing response: {e}");
334+
std::process::exit(1);
335+
});
315336

316337
println!(
317338
"{}",

src/datasets.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ fn default_schema() -> String {
1717
"main".to_string()
1818
}
1919

20-
#[derive(Deserialize)]
20+
#[derive(Deserialize, Serialize)]
2121
struct CreateResponse {
2222
id: String,
2323
label: String,
@@ -75,6 +75,7 @@ fn create_dataset(
7575
description: Option<&str>,
7676
name: &str,
7777
source: serde_json::Value,
78+
format: &str,
7879
) {
7980
let label = description.unwrap_or(name);
8081
let body = json!({ "table_name": name, "label": label, "source": source });
@@ -96,28 +97,36 @@ fn create_dataset(
9697
};
9798

9899
use crossterm::style::Stylize;
99-
println!("{}", "Dataset created".green());
100-
println!("id: {}", dataset.id);
101-
println!("label: {}", dataset.label);
102-
println!(
103-
"full_name: datasets.{}.{}",
104-
dataset.schema_name, dataset.table_name
105-
);
100+
match format {
101+
"json" => println!("{}", serde_json::to_string_pretty(&dataset).unwrap()),
102+
"yaml" => print!("{}", serde_yaml::to_string(&dataset).unwrap()),
103+
"table" => {
104+
eprintln!("{}", "Dataset created".green());
105+
println!("id: {}", dataset.id);
106+
println!("label: {}", dataset.label);
107+
println!(
108+
"full_name: datasets.{}.{}",
109+
dataset.schema_name, dataset.table_name
110+
);
111+
}
112+
_ => unreachable!(),
113+
}
106114
}
107115

108-
pub fn create_from_query(workspace_id: &str, sql: &str, description: Option<&str>, name: &str) {
116+
pub fn create_from_query(workspace_id: &str, sql: &str, description: Option<&str>, name: &str, format: &str) {
109117
let api = ApiClient::new(Some(workspace_id));
110-
create_dataset(&api, description, name, json!({ "type": "sql_query", "sql": sql }));
118+
create_dataset(&api, description, name, json!({ "type": "sql_query", "sql": sql }), format);
111119
}
112120

113121
pub fn create_from_saved_query(
114122
workspace_id: &str,
115123
query_id: &str,
116124
description: Option<&str>,
117125
name: &str,
126+
format: &str,
118127
) {
119128
let api = ApiClient::new(Some(workspace_id));
120-
create_dataset(&api, description, name, json!({ "type": "saved_query", "saved_query_id": query_id }));
129+
create_dataset(&api, description, name, json!({ "type": "saved_query", "saved_query_id": query_id }), format);
121130
}
122131

123132
pub fn list(workspace_id: &str, limit: Option<u32>, offset: Option<u32>, format: &str) {

src/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,20 +216,23 @@ fn main() {
216216
description,
217217
sql,
218218
query_id,
219+
output,
219220
}) => {
220221
if let Some(sql) = sql {
221222
datasets::create_from_query(
222223
&workspace_id,
223224
&sql,
224225
description.as_deref(),
225226
&name,
227+
&output,
226228
)
227229
} else {
228230
datasets::create_from_saved_query(
229231
&workspace_id,
230232
query_id.as_deref().unwrap_or_else(|| unreachable!("clap enforces --sql or --query-id")),
231233
description.as_deref(),
232234
&name,
235+
&output,
233236
)
234237
}
235238
}
@@ -957,6 +960,9 @@ fn main() {
957960
Some(SandboxCommands::Set { id: set_id }) => {
958961
sandbox::set(set_id.as_deref(), &workspace_id)
959962
}
963+
Some(SandboxCommands::Delete { id: delete_id }) => {
964+
sandbox::delete(&delete_id, &workspace_id)
965+
}
960966
None => match id {
961967
Some(id) => sandbox::get(&id, &workspace_id, &output),
962968
None => {

src/sandbox.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ pub fn read(sandbox_id: &str, workspace_id: &str) {
135135
if body.sandbox.markdown.is_empty() {
136136
eprintln!("{}", "Sandbox markdown is empty.".dark_grey());
137137
} else {
138-
print!("{}", body.sandbox.markdown);
138+
println!("{}", body.sandbox.markdown);
139139
}
140140
}
141141

@@ -195,7 +195,7 @@ pub fn new(workspace_id: &str, name: Option<&str>, format: &str) {
195195
eprintln!("warning: could not save sandbox to config: {e}");
196196
}
197197

198-
println!("{}", "Sandbox created".green());
198+
eprintln!("{}", "Sandbox created".green());
199199
match format {
200200
"json" => println!("{}", serde_json::json!({"public_id": sandbox_id})),
201201
"yaml" => print!(
@@ -340,6 +340,19 @@ pub fn set(sandbox_id: Option<&str>, workspace_id: &str) {
340340
}
341341
}
342342

343+
pub fn delete(sandbox_id: &str, workspace_id: &str) {
344+
let api = ApiClient::new(Some(workspace_id));
345+
let path = format!("/sandboxes/{sandbox_id}");
346+
let (status, resp_body) = api.delete_raw(&path);
347+
348+
if !status.is_success() {
349+
eprintln!("{}", crate::util::api_error(resp_body).red());
350+
std::process::exit(1);
351+
}
352+
353+
eprintln!("{}", "Sandbox deleted".green());
354+
}
355+
343356
#[cfg(test)]
344357
mod tests {
345358
use super::*;

src/workspace.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ struct ListResponse {
1717
}
1818

1919
pub fn set(workspace_id: Option<&str>) {
20-
if std::env::var("HOTDATA_WORKSPACE").is_ok()
20+
if std::env::var("HOTDATA_SANDBOX").is_ok()
2121
|| crate::sandbox::find_sandbox_run_ancestor().is_some()
2222
{
23-
eprintln!("error: workspace is locked");
23+
eprintln!("error: workspace cannot be changed inside a sandbox");
2424
std::process::exit(1);
2525
}
2626
let api = ApiClient::new(None);

0 commit comments

Comments
 (0)