Skip to content

Commit ee62435

Browse files
committed
Merge main into feat/datasets-rework
2 parents 0ec8e77 + 17059bd commit ee62435

8 files changed

Lines changed: 162 additions & 19 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ sysinfo = { version = "0.38.4", default-features = false, features = ["system"]
4040
self_update = { version = "0.42", default-features = false, features = ["rustls"] }
4141
lzma-rs = "0.3"
4242
tempfile = "3"
43+
urlencoding = "2.1.3"
4344

4445
[dev-dependencies]
4546
mockito = "1"

src/api.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub struct ApiClient {
3232
pub api_url: String,
3333
workspace_id: Option<String>,
3434
sandbox_id: Option<String>,
35+
database_id: Option<String>,
3536
}
3637

3738
impl ApiClient {
@@ -117,6 +118,7 @@ impl ApiClient {
117118
}
118119
profile_config.sandbox
119120
}),
121+
database_id: workspace_id.and_then(|ws| crate::config::load_current_database("default", ws)),
120122
}
121123
}
122124

@@ -129,6 +131,7 @@ impl ApiClient {
129131
api_url: api_url.to_string(),
130132
workspace_id: workspace_id.map(String::from),
131133
sandbox_id: None,
134+
database_id: None,
132135
}
133136
}
134137

@@ -167,6 +170,9 @@ impl ApiClient {
167170
req = req.header("X-Session-Id", sid);
168171
req = req.header("X-Sandbox-Id", sid);
169172
}
173+
if let Some(ref db_id) = self.database_id {
174+
req = req.header("X-Database-Id", db_id);
175+
}
170176
req
171177
}
172178

src/command.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,12 @@ pub enum DatabasesCommands {
558558
output: String,
559559
},
560560

561+
/// Set the current database (used by default when no database is specified)
562+
Set {
563+
/// Database id or description
564+
id_or_description: String,
565+
},
566+
561567
/// Delete a managed database and its tables
562568
Delete {
563569
/// Database name or connection ID
@@ -594,8 +600,9 @@ pub enum DatabasesCommands {
594600
pub enum DatabaseTablesCommands {
595601
/// List tables in a managed database
596602
List {
597-
/// Database name or connection ID
598-
database: String,
603+
/// Database id or description (defaults to current database)
604+
#[arg(long)]
605+
database: Option<String>,
599606

600607
/// Filter by schema name
601608
#[arg(long)]
@@ -608,8 +615,9 @@ pub enum DatabaseTablesCommands {
608615

609616
/// Load a parquet file into a table (creates or replaces the table)
610617
Load {
611-
/// Database name or connection ID
612-
database: String,
618+
/// Database id or description (defaults to current database)
619+
#[arg(long)]
620+
database: Option<String>,
613621

614622
/// Table name
615623
table: String,
@@ -633,8 +641,9 @@ pub enum DatabaseTablesCommands {
633641

634642
/// Delete a table from a managed database
635643
Delete {
636-
/// Database name or connection ID
637-
database: String,
644+
/// Database id or description (defaults to current database)
645+
#[arg(long)]
646+
database: Option<String>,
638647

639648
/// Table name
640649
table: String,

src/config.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ pub struct ProfileConfig {
101101
pub workspaces: Vec<WorkspaceEntry>,
102102
#[serde(default, skip_serializing_if = "Option::is_none", alias = "session")]
103103
pub sandbox: Option<String>,
104+
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
105+
pub current_databases: HashMap<String, String>,
104106
}
105107

106108
#[derive(Debug, Deserialize, Serialize)]
@@ -227,6 +229,60 @@ pub fn clear_sandbox(profile: &str) -> Result<(), String> {
227229
write_config(&config_path, &content)
228230
}
229231

232+
pub fn save_current_database(profile: &str, workspace_id: &str, database_id: &str) -> Result<(), String> {
233+
let config_path = config_path()?;
234+
235+
let mut config_file: ConfigFile = if config_path.exists() {
236+
let content = fs::read_to_string(&config_path)
237+
.map_err(|e| format!("error reading config file: {e}"))?;
238+
serde_yaml::from_str(&content).map_err(|e| format!("error parsing config file: {e}"))?
239+
} else {
240+
ConfigFile { profiles: HashMap::new() }
241+
};
242+
243+
config_file
244+
.profiles
245+
.entry(profile.to_string())
246+
.or_default()
247+
.current_databases
248+
.insert(workspace_id.to_string(), database_id.to_string());
249+
250+
let content = serde_yaml::to_string(&config_file)
251+
.map_err(|e| format!("error serializing config: {e}"))?;
252+
write_config(&config_path, &content)
253+
}
254+
255+
pub fn load_current_database(profile: &str, workspace_id: &str) -> Option<String> {
256+
let config_path = config_path().ok()?;
257+
if !config_path.exists() {
258+
return None;
259+
}
260+
let content = fs::read_to_string(&config_path).ok()?;
261+
let config_file: ConfigFile = serde_yaml::from_str(&content).ok()?;
262+
config_file.profiles.get(profile)?.current_databases.get(workspace_id).cloned()
263+
}
264+
265+
pub fn clear_current_database(profile: &str, workspace_id: &str) -> Result<(), String> {
266+
let config_path = config_path()?;
267+
268+
if !config_path.exists() {
269+
return Ok(());
270+
}
271+
272+
let content = fs::read_to_string(&config_path)
273+
.map_err(|e| format!("error reading config file: {e}"))?;
274+
let mut config_file: ConfigFile =
275+
serde_yaml::from_str(&content).map_err(|e| format!("error parsing config file: {e}"))?;
276+
277+
if let Some(entry) = config_file.profiles.get_mut(profile) {
278+
entry.current_databases.remove(workspace_id);
279+
}
280+
281+
let content = serde_yaml::to_string(&config_file)
282+
.map_err(|e| format!("error serializing config: {e}"))?;
283+
write_config(&config_path, &content)
284+
}
285+
230286
pub fn resolve_workspace_id(provided: Option<String>, profile_config: &ProfileConfig) -> Result<String, String> {
231287
if let Some(id) = provided {
232288
return Ok(id);

src/databases.rs

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub struct Database {
2323
pub id: String,
2424
pub description: Option<String>,
2525
pub default_connection_id: String,
26+
#[serde(default)]
2627
attachments: Vec<DatabaseAttachment>,
2728
}
2829

@@ -82,7 +83,10 @@ fn fetch_database(api: &ApiClient, id: &str) -> Database {
8283

8384
pub fn try_resolve_database(api: &ApiClient, id_or_description: &str) -> Result<Database, String> {
8485
// Try a direct id lookup first — avoids the list round-trip for the common case.
85-
if let Some(db) = api.get_none_if_not_found(&format!("/databases/{id_or_description}")) {
86+
// Percent-encode the segment so descriptions containing spaces or other URL-unsafe
87+
// characters don't cause a URL parse error before the list fallback can run.
88+
let encoded = urlencoding::encode(id_or_description);
89+
if let Some(db) = api.get_none_if_not_found(&format!("/databases/{encoded}")) {
8690
return Ok(db);
8791
}
8892

@@ -436,6 +440,11 @@ pub fn create(
436440
}
437441
};
438442

443+
if let Err(e) = crate::config::save_current_database("default", workspace_id, &result.id) {
444+
use crossterm::style::Stylize;
445+
eprintln!("{}", format!("warning: database created but could not set as current: {e}").yellow());
446+
}
447+
439448
match format {
440449
"json" => println!("{}", serde_json::to_string_pretty(&result).unwrap()),
441450
"yaml" => print!("{}", serde_yaml::to_string(&result).unwrap()),
@@ -450,6 +459,34 @@ pub fn create(
450459
}
451460
}
452461

462+
pub fn set(workspace_id: &str, id_or_description: &str) {
463+
use crossterm::style::Stylize;
464+
let api = ApiClient::new(Some(workspace_id));
465+
let db = resolve_database(&api, id_or_description);
466+
if let Err(e) = crate::config::save_current_database("default", workspace_id, &db.id) {
467+
eprintln!("{}", format!("error saving current database: {e}").red());
468+
std::process::exit(1);
469+
}
470+
println!("{}", format!("Current database set to {}", db.id).green());
471+
}
472+
473+
fn resolve_current_database(provided: Option<&str>, workspace_id: &str) -> String {
474+
if let Some(id) = provided {
475+
return id.to_string();
476+
}
477+
match crate::config::load_current_database("default", workspace_id) {
478+
Some(id) => id,
479+
None => {
480+
use crossterm::style::Stylize;
481+
eprintln!(
482+
"{}",
483+
"error: no current database set. Use 'hotdata databases set <id>' or pass a database id.".red()
484+
);
485+
std::process::exit(1);
486+
}
487+
}
488+
}
489+
453490
pub fn delete(workspace_id: &str, id_or_description: &str) {
454491
use crossterm::style::Stylize;
455492

@@ -462,12 +499,19 @@ pub fn delete(workspace_id: &str, id_or_description: &str) {
462499
std::process::exit(1);
463500
}
464501

502+
// If the deleted database was the current one, clear it so subsequent
503+
// commands don't silently send a stale X-Database-Id header.
504+
if crate::config::load_current_database("default", workspace_id).as_deref() == Some(&db.id) {
505+
let _ = crate::config::clear_current_database("default", workspace_id);
506+
}
507+
465508
println!("{}", "Database deleted.".green());
466509
}
467510

468-
pub fn tables_list(workspace_id: &str, database: &str, schema: Option<&str>, format: &str) {
511+
pub fn tables_list(workspace_id: &str, database: Option<&str>, schema: Option<&str>, format: &str) {
512+
let database = resolve_current_database(database, workspace_id);
469513
let api = ApiClient::new(Some(workspace_id));
470-
let db = resolve_database(&api, database);
514+
let db = resolve_database(&api, &database);
471515
let tables = collect_tables(&api, &db.default_connection_id, schema);
472516

473517
let rows = table_rows(tables);
@@ -502,7 +546,7 @@ pub fn tables_list(workspace_id: &str, database: &str, schema: Option<&str>, for
502546

503547
pub fn tables_load(
504548
workspace_id: &str,
505-
database: &str,
549+
database: Option<&str>,
506550
table: &str,
507551
schema: Option<&str>,
508552
file: Option<&str>,
@@ -511,8 +555,9 @@ pub fn tables_load(
511555
) {
512556
use crossterm::style::Stylize;
513557

558+
let database = resolve_current_database(database, workspace_id);
514559
let api = ApiClient::new(Some(workspace_id));
515-
let db = resolve_database(&api, database);
560+
let db = resolve_database(&api, &database);
516561
let schema = schema_name(schema);
517562

518563
// clap enforces mutual exclusion; only one of these is ever Some.
@@ -564,11 +609,12 @@ pub fn tables_load(
564609
println!("rows: {}", result.row_count);
565610
}
566611

567-
pub fn tables_delete(workspace_id: &str, database: &str, table: &str, schema: Option<&str>) {
612+
pub fn tables_delete(workspace_id: &str, database: Option<&str>, table: &str, schema: Option<&str>) {
568613
use crossterm::style::Stylize;
569614

615+
let database = resolve_current_database(database, workspace_id);
570616
let api = ApiClient::new(Some(workspace_id));
571-
let db = resolve_database(&api, database);
617+
let db = resolve_database(&api, &database);
572618
let schema = schema_name(schema);
573619

574620
let path = managed_table_delete_path(&db.default_connection_id, schema, table);

src/main.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ struct Cli {
5656
command: Option<Commands>,
5757
}
5858

59+
/// Set once after workspace resolution so the database footer can reference it
60+
/// without re-doing config I/O.
61+
static ACTIVE_WORKSPACE_ID: std::sync::OnceLock<String> = std::sync::OnceLock::new();
62+
5963
fn resolve_workspace(provided: Option<String>) -> String {
6064
// HOTDATA_WORKSPACE env var takes priority and blocks --workspace-id flag
6165
if let Ok(ws) = std::env::var("HOTDATA_WORKSPACE") {
@@ -67,6 +71,7 @@ fn resolve_workspace(provided: Option<String>) -> String {
6771
);
6872
std::process::exit(1);
6973
}
74+
let _ = ACTIVE_WORKSPACE_ID.set(ws.clone());
7075
return ws;
7176
}
7277
if sandbox::find_sandbox_run_ancestor().is_some() {
@@ -75,7 +80,10 @@ fn resolve_workspace(provided: Option<String>) -> String {
7580
}
7681
match config::load("default") {
7782
Ok(profile) => match config::resolve_workspace_id(provided, &profile) {
78-
Ok(id) => id,
83+
Ok(id) => {
84+
let _ = ACTIVE_WORKSPACE_ID.set(id.clone());
85+
id
86+
}
7987
Err(e) => {
8088
eprintln!("error: {e}");
8189
std::process::exit(1);
@@ -125,11 +133,25 @@ extern "C" fn print_sandbox_footer() {
125133
);
126134
}
127135

136+
extern "C" fn print_database_footer() {
137+
use crossterm::style::Stylize;
138+
if let Some(ws_id) = ACTIVE_WORKSPACE_ID.get() {
139+
if let Some(id) = config::load_current_database("default", ws_id) {
140+
eprintln!(
141+
"{}",
142+
format!("current database: {id} use 'hotdata databases set' to change")
143+
.dark_grey(),
144+
);
145+
}
146+
}
147+
}
148+
128149
fn main() {
129150
// Register before `Cli::parse`, since `--help` / `--version` exit
130151
// from inside the parser. Safety: `atexit` is async-signal-safe;
131152
// the callback only reads env vars / files and writes to stderr.
132153
unsafe { atexit(print_sandbox_footer) };
154+
unsafe { atexit(print_database_footer) };
133155

134156
dotenvy::dotenv().ok();
135157
let cli = Cli::parse();
@@ -373,6 +395,9 @@ fn main() {
373395
&tables,
374396
&output,
375397
),
398+
Some(DatabasesCommands::Set { id_or_description }) => {
399+
databases::set(&workspace_id, &id_or_description)
400+
}
376401
Some(DatabasesCommands::Delete { name_or_id }) => {
377402
databases::delete(&workspace_id, &name_or_id)
378403
}
@@ -385,7 +410,7 @@ fn main() {
385410
let (database, schema, table) = parse_db_target(&target);
386411
databases::tables_load(
387412
&workspace_id,
388-
&database,
413+
Some(database.as_str()),
389414
&table,
390415
Some(schema.as_str()),
391416
file.as_deref(),
@@ -400,7 +425,7 @@ fn main() {
400425
output,
401426
} => databases::tables_list(
402427
&workspace_id,
403-
&database,
428+
database.as_deref(),
404429
schema.as_deref(),
405430
&output,
406431
),
@@ -413,7 +438,7 @@ fn main() {
413438
upload_id,
414439
} => databases::tables_load(
415440
&workspace_id,
416-
&database,
441+
database.as_deref(),
417442
&table,
418443
Some(schema.as_str()),
419444
file.as_deref(),
@@ -426,7 +451,7 @@ fn main() {
426451
schema,
427452
} => databases::tables_delete(
428453
&workspace_id,
429-
&database,
454+
database.as_deref(),
430455
&table,
431456
Some(schema.as_str()),
432457
),

0 commit comments

Comments
 (0)