Skip to content

Commit 8628b1b

Browse files
authored
feat(datasets): add --sql and --query-id to datasets create
1 parent 6c95bc3 commit 8628b1b

File tree

4 files changed

+133
-49
lines changed

4 files changed

+133
-49
lines changed

skills/hotdata-cli/SKILL.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,15 @@ hotdata datasets <dataset_id> [--workspace-id <workspace_id>] [--format table|js
131131
#### Create a dataset
132132
```
133133
hotdata datasets create --label "My Dataset" --file data.csv [--table-name my_dataset] [--workspace-id <workspace_id>]
134+
hotdata datasets create --label "My Dataset" --sql "SELECT * FROM ..." [--table-name my_dataset] [--workspace-id <workspace_id>]
135+
hotdata datasets create --label "My Dataset" --query-id <saved_query_id> [--table-name my_dataset] [--workspace-id <workspace_id>]
134136
```
135137
- `--file` uploads a local file. Omit to pipe data via stdin: `cat data.csv | hotdata datasets create --label "My Dataset"`
138+
- `--sql` creates a dataset from a SQL query result.
139+
- `--query-id` creates a dataset from a previously saved query.
140+
- `--file`, `--sql`, and `--query-id` are mutually exclusive.
136141
- Format is auto-detected from file extension (`.csv`, `.json`, `.parquet`) or file content.
137-
- `--label` is optional when `--file` is provided — defaults to the filename without extension.
142+
- `--label` is optional when `--file` is provided — defaults to the filename without extension. Required for `--sql` and `--query-id`.
138143
- `--table-name` is optional — derived from the label if omitted.
139144

140145
#### Querying datasets

src/command.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ pub enum DatasetsCommands {
183183
format: String,
184184
},
185185

186-
/// Create a new dataset from a file, piped stdin, or a pre-existing upload ID
186+
/// Create a new dataset from a file, piped stdin, upload ID, or SQL query
187187
Create {
188188
/// Dataset label (derived from filename if omitted)
189189
#[arg(long)]
@@ -194,18 +194,25 @@ pub enum DatasetsCommands {
194194
table_name: Option<String>,
195195

196196
/// Path to a file to upload (omit to read from stdin)
197-
#[arg(long, conflicts_with = "upload_id")]
197+
#[arg(long, conflicts_with_all = ["upload_id", "sql"])]
198198
file: Option<String>,
199199

200200
/// Skip upload and use a pre-existing upload ID directly
201-
#[arg(long, conflicts_with = "file")]
201+
#[arg(long, conflicts_with_all = ["file", "sql"])]
202202
upload_id: Option<String>,
203203

204204
/// Source format when using --upload-id (csv, json, parquet)
205205
#[arg(long, default_value = "csv", value_parser = ["csv", "json", "parquet"], requires = "upload_id")]
206206
format: String,
207-
},
208207

208+
/// SQL query to create the dataset from
209+
#[arg(long, conflicts_with_all = ["file", "upload_id", "query_id"])]
210+
sql: Option<String>,
211+
212+
/// Saved query ID to create the dataset from
213+
#[arg(long, conflicts_with_all = ["file", "upload_id", "sql"])]
214+
query_id: Option<String>,
215+
},
209216
}
210217

211218

src/datasets.rs

Lines changed: 108 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,76 @@ fn upload_from_stdin(
220220
(id, ft.format)
221221
}
222222

223-
pub fn create(
223+
fn create_dataset(
224+
workspace_id: &str,
225+
label: &str,
226+
table_name: Option<&str>,
227+
source: serde_json::Value,
228+
on_failure: Option<Box<dyn FnOnce()>>,
229+
) {
230+
let profile_config = match config::load("default") {
231+
Ok(c) => c,
232+
Err(e) => {
233+
eprintln!("{e}");
234+
std::process::exit(1);
235+
}
236+
};
237+
238+
let api_key = match &profile_config.api_key {
239+
Some(key) if key != "PLACEHOLDER" => key.clone(),
240+
_ => {
241+
eprintln!("error: not authenticated. Run 'hotdata auth login' to log in.");
242+
std::process::exit(1);
243+
}
244+
};
245+
246+
let mut body = json!({ "label": label, "source": source });
247+
if let Some(tn) = table_name {
248+
body["table_name"] = json!(tn);
249+
}
250+
251+
let url = format!("{}/datasets", profile_config.api_url);
252+
let client = reqwest::blocking::Client::new();
253+
254+
let resp = match client
255+
.post(&url)
256+
.header("Authorization", format!("Bearer {api_key}"))
257+
.header("X-Workspace-Id", workspace_id)
258+
.json(&body)
259+
.send()
260+
{
261+
Ok(r) => r,
262+
Err(e) => {
263+
eprintln!("error connecting to API: {e}");
264+
std::process::exit(1);
265+
}
266+
};
267+
268+
if !resp.status().is_success() {
269+
use crossterm::style::Stylize;
270+
eprintln!("{}", crate::util::api_error(resp.text().unwrap_or_default()).red());
271+
if let Some(f) = on_failure {
272+
f();
273+
}
274+
std::process::exit(1);
275+
}
276+
277+
let dataset: CreateResponse = match resp.json() {
278+
Ok(v) => v,
279+
Err(e) => {
280+
eprintln!("error parsing response: {e}");
281+
std::process::exit(1);
282+
}
283+
};
284+
285+
use crossterm::style::Stylize;
286+
println!("{}", "Dataset created".green());
287+
println!("id: {}", dataset.id);
288+
println!("label: {}", dataset.label);
289+
println!("full_name: datasets.main.{}", dataset.table_name);
290+
}
291+
292+
pub fn create_from_upload(
224293
workspace_id: &str,
225294
label: Option<&str>,
226295
table_name: Option<&str>,
@@ -295,56 +364,53 @@ pub fn create(
295364
};
296365

297366
let source = json!({ "upload_id": upload_id, "format": format });
298-
let mut body = json!({ "label": label, "source": source });
299-
if let Some(tn) = table_name {
300-
body["table_name"] = json!(tn);
301-
}
302-
303-
let url = format!("{}/datasets", profile_config.api_url);
304-
305-
let resp = match client
306-
.post(&url)
307-
.header("Authorization", format!("Bearer {api_key}"))
308-
.header("X-Workspace-Id", workspace_id)
309-
.json(&body)
310-
.send()
311-
{
312-
Ok(r) => r,
313-
Err(e) => {
314-
eprintln!("error connecting to API: {e}");
315-
std::process::exit(1);
316-
}
317-
};
318367

319-
if !resp.status().is_success() {
320-
use crossterm::style::Stylize;
321-
eprintln!("{}", crate::util::api_error(resp.text().unwrap_or_default()).red());
322-
// Only show the resume hint when the upload_id came from a fresh upload
323-
if upload_id_was_uploaded {
368+
let on_failure: Option<Box<dyn FnOnce()>> = if upload_id_was_uploaded {
369+
let uid = upload_id.clone();
370+
Some(Box::new(move || {
371+
use crossterm::style::Stylize;
324372
eprintln!(
325373
"{}",
326-
format!(
327-
"Resume dataset creation without re-uploading by passing --upload-id {upload_id}"
328-
)
329-
.yellow()
374+
format!("Resume dataset creation without re-uploading by passing --upload-id {uid}").yellow()
330375
);
331-
}
332-
std::process::exit(1);
333-
}
376+
}))
377+
} else {
378+
None
379+
};
334380

335-
let dataset: CreateResponse = match resp.json() {
336-
Ok(v) => v,
337-
Err(e) => {
338-
eprintln!("error parsing response: {e}");
381+
create_dataset(workspace_id, label, table_name, source, on_failure);
382+
}
383+
384+
pub fn create_from_query(
385+
workspace_id: &str,
386+
sql: &str,
387+
label: Option<&str>,
388+
table_name: Option<&str>,
389+
) {
390+
let label = match label {
391+
Some(l) => l,
392+
None => {
393+
eprintln!("error: --label is required when using --sql");
339394
std::process::exit(1);
340395
}
341396
};
397+
create_dataset(workspace_id, label, table_name, json!({ "sql": sql }), None);
398+
}
342399

343-
use crossterm::style::Stylize;
344-
println!("{}", "Dataset created".green());
345-
println!("id: {}", dataset.id);
346-
println!("label: {}", dataset.label);
347-
println!("full_name: datasets.main.{}", dataset.table_name);
400+
pub fn create_from_saved_query(
401+
workspace_id: &str,
402+
query_id: &str,
403+
label: Option<&str>,
404+
table_name: Option<&str>,
405+
) {
406+
let label = match label {
407+
Some(l) => l,
408+
None => {
409+
eprintln!("error: --label is required when using --query-id");
410+
std::process::exit(1);
411+
}
412+
};
413+
create_dataset(workspace_id, label, table_name, json!({ "saved_query_id": query_id }), None);
348414
}
349415

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

src/main.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,14 @@ fn main() {
7171
Some(DatasetsCommands::List { limit, offset, format }) => {
7272
datasets::list(&workspace_id, limit, offset, &format)
7373
}
74-
Some(DatasetsCommands::Create { label, table_name, file, upload_id, format }) => {
75-
datasets::create(&workspace_id, label.as_deref(), table_name.as_deref(), file.as_deref(), upload_id.as_deref(), &format)
74+
Some(DatasetsCommands::Create { label, table_name, file, upload_id, format, sql, query_id }) => {
75+
if let Some(sql) = sql {
76+
datasets::create_from_query(&workspace_id, &sql, label.as_deref(), table_name.as_deref())
77+
} else if let Some(query_id) = query_id {
78+
datasets::create_from_saved_query(&workspace_id, &query_id, label.as_deref(), table_name.as_deref())
79+
} else {
80+
datasets::create_from_upload(&workspace_id, label.as_deref(), table_name.as_deref(), file.as_deref(), upload_id.as_deref(), &format)
81+
}
7682
}
7783
None => {
7884
use clap::CommandFactory;

0 commit comments

Comments
 (0)