Skip to content

Commit 5fc3b07

Browse files
authored
feat(cli): loading indicators for indexes list and usage (#215)
* feat(cli): loading indicators for indexes list and usage Both commands sat silent while fetching. `indexes list` gets one spinner over the whole fetch - the unscoped path is a whole-workspace scan (connections enumeration + managed-DB discovery + per-table index reads) that can take seconds. The database discovery inside the scan (databases::list_database_ids) drops its nested "Loading databases..." wakeup spinner so two indicatif bars never fight over the same line; `databases list` itself keeps the wakeup-hint spinner. `usage` gets a plain spinner around its single request. * feat(databases): show created_at in list and show commands (#217) * feat(databases): show created_at in list and show commands Adds a CREATED column to `databases list` and a created_at row to `databases show`. Uses the updated SDK models from sdk-rust feat/database-timestamps (PR #909 in runtimedb). Cargo.toml temporarily points to local sdk-rust path; update to published version once sdk-rust feat/database-timestamps is released. * chore: update hotdata SDK to v0.8.1 (created_at on databases) --------- Co-authored-by: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> * feat(ingest): rename the catalog verb connectors -> datasources (#216) 'hotdata ingest datasources' now browses the catalog of available datasource types; 'connectors' stays as a hidden alias. Help, error hints, README, and the skill follow. Co-authored-by: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> * fix(indexes): clear spinner before exiting on API error The fetch helpers called e.exit() (process::exit) internally, leaving the indicatif spinner live on the terminal when any API call failed. Convert list_one_table, list_one_table_scan, collect_tables, connection_lookup, managed_db_connection_ids, workspace_scan_targets, and collect_connection_wide to return Result<_, ApiError>, then finish_and_clear() the spinner in list() before unwrap_or_else(|e| e.exit()) -- matching the usage.rs pattern. * style: cargo fmt --------- Co-authored-by: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com>
1 parent 1bdadc6 commit 5fc3b07

3 files changed

Lines changed: 90 additions & 65 deletions

File tree

‎src/commands/databases.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,12 @@ fn list_database_summaries(api: &Api) -> Result<Vec<DatabaseSummary>, ApiError>
403403
/// rediscovers managed databases here and resolves each one's
404404
/// `default_connection_id` via [`get_database`]. The list summary omits the
405405
/// connection id, hence ids only.
406+
///
407+
/// Deliberately spinner-less (plain [`block`], unlike
408+
/// [`list_database_summaries`]): the caller owns its own "Loading indexes…"
409+
/// spinner, and two indicatif bars would fight over the same line.
406410
pub(crate) fn list_database_ids(api: &Api) -> Result<Vec<String>, ApiError> {
407-
list_database_summaries(api).map(|dbs| dbs.into_iter().map(|d| d.id).collect())
411+
block(api.client().databases().list()).map(|r| r.databases.into_iter().map(|d| d.id).collect())
408412
}
409413

410414
fn fetch_database(api: &Api, id: &str) -> Database {

‎src/commands/indexes.rs‎

Lines changed: 81 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::client::sdk::{Api, block, block_with_wakeup, none_if_404};
1+
use crate::client::sdk::{Api, ApiError, block, block_with_wakeup, none_if_404};
22
use crate::commands::databases;
33
use rayon::prelude::*;
44
use serde::{Deserialize, Serialize};
@@ -182,8 +182,8 @@ fn connection_label_to_id_map(connections: &[ConnectionRef]) -> HashMap<String,
182182
m
183183
}
184184

185-
fn connection_lookup(api: &Api) -> HashMap<String, String> {
186-
let resp = block(api.client().connections().list()).unwrap_or_else(|e| e.exit());
185+
fn connection_lookup(api: &Api) -> Result<HashMap<String, String>, ApiError> {
186+
let resp = block(api.client().connections().list())?;
187187
let refs: Vec<ConnectionRef> = resp
188188
.connections
189189
.into_iter()
@@ -192,7 +192,7 @@ fn connection_lookup(api: &Api) -> HashMap<String, String> {
192192
name: c.name,
193193
})
194194
.collect();
195-
connection_label_to_id_map(&refs)
195+
Ok(connection_label_to_id_map(&refs))
196196
}
197197

198198
/// Pick the connection id to address a per-table index call with during a
@@ -233,15 +233,15 @@ struct ScanTarget {
233233
/// connection id, so each database needs a `databases get`; a database deleted
234234
/// between the list and the get (404) is skipped, any other error surfaces
235235
/// loudly to match the rest of this path.
236-
fn managed_db_connection_ids(api: &Api) -> Vec<String> {
237-
let ids = databases::list_database_ids(api).unwrap_or_else(|e| e.exit());
238-
ids.par_iter()
239-
.filter_map(|id| {
240-
none_if_404(databases::get_database(api, id))
241-
.unwrap_or_else(|e| e.exit())
242-
.map(|db| db.default_connection_id)
236+
fn managed_db_connection_ids(api: &Api) -> Result<Vec<String>, ApiError> {
237+
let ids = databases::list_database_ids(api)?;
238+
let conn_ids: Result<Vec<Option<String>>, ApiError> = ids
239+
.par_iter()
240+
.map(|id| {
241+
Ok(none_if_404(databases::get_database(api, id))?.map(|db| db.default_connection_id))
243242
})
244-
.collect()
243+
.collect();
244+
Ok(conn_ids?.into_iter().flatten().collect())
245245
}
246246

247247
/// Build the per-table scan list for a whole-workspace (unscoped) `indexes
@@ -254,11 +254,15 @@ fn managed_db_connection_ids(api: &Api) -> Vec<String> {
254254
/// with a connection-scoped `information_schema` call, exactly like the
255255
/// `--connection-id` path. The two table sets are disjoint: a managed database's
256256
/// connection is never returned by `connections list`.
257-
fn workspace_scan_targets(api: &Api, schema: Option<&str>, table: Option<&str>) -> Vec<ScanTarget> {
257+
fn workspace_scan_targets(
258+
api: &Api,
259+
schema: Option<&str>,
260+
table: Option<&str>,
261+
) -> Result<Vec<ScanTarget>, ApiError> {
258262
// Regular connections: one workspace-wide enumeration, label (= connection
259263
// name) mapped back to its id, falling back to the label itself (#161).
260-
let name_to_id = connection_lookup(api);
261-
let mut targets: Vec<ScanTarget> = collect_tables(api, None, schema, table)
264+
let name_to_id = connection_lookup(api)?;
265+
let mut targets: Vec<ScanTarget> = collect_tables(api, None, schema, table)?
262266
.into_iter()
263267
.map(|t| {
264268
let conn_id = scan_connection_id(None, &t.connection, &name_to_id).to_string();
@@ -268,21 +272,23 @@ fn workspace_scan_targets(api: &Api, schema: Option<&str>, table: Option<&str>)
268272

269273
// Managed databases: discover their hidden connections, then scan each
270274
// scoped (the per-connection enumeration is what surfaces `__db_*` tables).
271-
let db_conns = managed_db_connection_ids(api);
272-
let managed: Vec<ScanTarget> = db_conns
275+
let db_conns = managed_db_connection_ids(api)?;
276+
let managed: Result<Vec<Vec<ScanTarget>>, ApiError> = db_conns
273277
.par_iter()
274-
.flat_map(|conn| {
275-
collect_tables(api, Some(conn), schema, table)
276-
.into_iter()
277-
.map(|t| ScanTarget {
278-
conn_id: conn.clone(),
279-
table: t,
280-
})
281-
.collect::<Vec<_>>()
278+
.map(|conn| {
279+
collect_tables(api, Some(conn), schema, table).map(|tables| {
280+
tables
281+
.into_iter()
282+
.map(|t| ScanTarget {
283+
conn_id: conn.clone(),
284+
table: t,
285+
})
286+
.collect::<Vec<_>>()
287+
})
282288
})
283289
.collect();
284-
targets.extend(managed);
285-
targets
290+
targets.extend(managed?.into_iter().flatten());
291+
Ok(targets)
286292
}
287293

288294
/// Gather index rows across a connection's (or the workspace's) tables — the
@@ -298,36 +304,36 @@ fn collect_connection_wide(
298304
connection_id: Option<&str>,
299305
schema: Option<&str>,
300306
table: Option<&str>,
301-
) -> Vec<IndexRow> {
307+
) -> Result<Vec<IndexRow>, ApiError> {
302308
let targets = match connection_id {
303-
Some(cid) => collect_tables(api, Some(cid), schema, table)
309+
Some(cid) => collect_tables(api, Some(cid), schema, table)?
304310
.into_iter()
305311
.map(|t| ScanTarget {
306312
conn_id: cid.to_string(),
307313
table: t,
308314
})
309315
.collect(),
310-
None => workspace_scan_targets(api, schema, table),
316+
None => workspace_scan_targets(api, schema, table)?,
311317
};
312-
let per_table: Vec<(String, Vec<Index>)> = targets
318+
let per_table: Result<Vec<(String, Vec<Index>)>, ApiError> = targets
313319
.par_iter()
314320
.map(|tg| {
315321
let t = &tg.table;
316322
let full = format!("{}.{}.{}", t.connection, t.schema, t.table);
317-
let indexes = list_one_table_scan(api, &tg.conn_id, &t.schema, &t.table);
318-
(full, indexes)
323+
let indexes = list_one_table_scan(api, &tg.conn_id, &t.schema, &t.table)?;
324+
Ok((full, indexes))
319325
})
320326
.collect();
321327
let mut rows: Vec<IndexRow> = Vec::new();
322-
for (full, indexes) in per_table {
328+
for (full, indexes) in per_table? {
323329
for i in indexes {
324330
rows.push(IndexRow {
325331
inner: i,
326332
table: Some(full.clone()),
327333
});
328334
}
329335
}
330-
rows
336+
Ok(rows)
331337
}
332338

333339
/// How to continue after merging one `/information_schema` page.
@@ -358,7 +364,7 @@ fn collect_tables(
358364
connection_id: Option<&str>,
359365
schema: Option<&str>,
360366
table: Option<&str>,
361-
) -> Vec<InfoTable> {
367+
) -> Result<Vec<InfoTable>, ApiError> {
362368
let mut out = Vec::new();
363369
let mut cursor: Option<String> = None;
364370
loop {
@@ -369,8 +375,7 @@ fn collect_tables(
369375
None,
370376
None,
371377
cursor.as_deref(),
372-
))
373-
.unwrap_or_else(|e| e.exit());
378+
))?;
374379
out.extend(resp.tables.into_iter().map(|t| InfoTable {
375380
connection: t.connection,
376381
schema: t.schema,
@@ -383,23 +388,33 @@ fn collect_tables(
383388
}
384389
}
385390
sort_info_tables(&mut out);
386-
out
391+
Ok(out)
387392
}
388393

389-
fn list_one_table(api: &Api, connection_id: &str, schema: &str, table: &str) -> Vec<Index> {
394+
fn list_one_table(
395+
api: &Api,
396+
connection_id: &str,
397+
schema: &str,
398+
table: &str,
399+
) -> Result<Vec<Index>, ApiError> {
390400
// The SDK's typed `IndexInfoResponse.status` is a closed `ready`/`pending`
391401
// enum; the CLI accepts any status string for display. Keep the CLI's own
392402
// tolerant deserialization via the seam's untyped GET escape hatch.
393403
let path = format!("/connections/{connection_id}/tables/{schema}/{table}/indexes");
394-
let body: ListResponse = api.get_json(&path, &[]).unwrap_or_else(|e| e.exit());
395-
body.indexes
404+
let body: ListResponse = api.get_json(&path, &[])?;
405+
Ok(body.indexes)
396406
}
397407

398-
fn list_one_table_scan(api: &Api, connection_id: &str, schema: &str, table: &str) -> Vec<Index> {
408+
fn list_one_table_scan(
409+
api: &Api,
410+
connection_id: &str,
411+
schema: &str,
412+
table: &str,
413+
) -> Result<Vec<Index>, ApiError> {
399414
let path = format!("/connections/{connection_id}/tables/{schema}/{table}/indexes");
400-
match none_if_404(api.get_json::<ListResponse>(&path, &[])).unwrap_or_else(|e| e.exit()) {
401-
Some(body) => body.indexes,
402-
None => Vec::new(),
415+
match none_if_404(api.get_json::<ListResponse>(&path, &[]))? {
416+
Some(body) => Ok(body.indexes),
417+
None => Ok(Vec::new()),
403418
}
404419
}
405420

@@ -479,7 +494,7 @@ pub fn infer_for_search(
479494
let connection_id = crate::commands::connections::resolve_connection_id(&api, connection_name);
480495

481496
// Fetch indexes for this table
482-
let indexes = list_one_table(&api, &connection_id, schema, table);
497+
let indexes = list_one_table(&api, &connection_id, schema, table).unwrap_or_else(|e| e.exit());
483498

484499
let location = format!("{}.{}.{}", connection_name, schema, table);
485500
match resolve_search_params(&indexes, hint_type, hint_column, &location) {
@@ -500,9 +515,13 @@ pub fn list(
500515
) {
501516
let api = Api::new(Some(workspace_id));
502517

503-
let (rows, multi_table) = match (connection_id, schema, table) {
504-
(Some(cid), Some(sch), Some(tbl)) => {
505-
let indexes = list_one_table(&api, cid, sch, tbl);
518+
// One spinner over the whole fetch — the unscoped path is a
519+
// whole-workspace scan (many requests) that otherwise sits silent.
520+
// The database discovery inside is deliberately spinner-less
521+
// (databases::list_database_ids) so nothing fights for the line.
522+
let spinner = crate::util::spinner("Loading indexes…");
523+
let result = match (connection_id, schema, table) {
524+
(Some(cid), Some(sch), Some(tbl)) => list_one_table(&api, cid, sch, tbl).map(|indexes| {
506525
let rows: Vec<IndexRow> = indexes
507526
.into_iter()
508527
.map(|i| IndexRow {
@@ -511,12 +530,11 @@ pub fn list(
511530
})
512531
.collect();
513532
(rows, false)
514-
}
515-
_ => (
516-
collect_connection_wide(&api, connection_id, schema, table),
517-
true,
518-
),
533+
}),
534+
_ => collect_connection_wide(&api, connection_id, schema, table).map(|rows| (rows, true)),
519535
};
536+
spinner.finish_and_clear();
537+
let (rows, multi_table) = result.unwrap_or_else(|e| e.exit());
520538

521539
match format {
522540
"json" => println!("{}", serde_json::to_string_pretty(&rows).unwrap()),
@@ -872,7 +890,7 @@ mod tests {
872890
.create();
873891

874892
let api = Api::test_new(&server.url(), "k", Some("ws"));
875-
let rows = collect_connection_wide(&api, Some("conn-real"), None, None);
893+
let rows = collect_connection_wide(&api, Some("conn-real"), None, None).unwrap();
876894
info.assert();
877895
idx.assert();
878896
assert_eq!(rows.len(), 1);
@@ -954,7 +972,7 @@ mod tests {
954972
.create();
955973

956974
let api = Api::test_new(&server.url(), "k", Some("ws"));
957-
let rows = collect_connection_wide(&api, None, None, None);
975+
let rows = collect_connection_wide(&api, None, None, None).unwrap();
958976
conns.assert();
959977
info_ws.assert();
960978
dbs.assert();
@@ -1053,7 +1071,7 @@ mod tests {
10531071
.create();
10541072

10551073
let api = Api::test_new(&server.url(), "k", Some("ws"));
1056-
let mut rows = collect_connection_wide(&api, None, None, None);
1074+
let mut rows = collect_connection_wide(&api, None, None, None).unwrap();
10571075
conns.assert();
10581076
info_ws.assert();
10591077
reg_idx.assert();
@@ -1087,7 +1105,7 @@ mod tests {
10871105
.create();
10881106

10891107
let api = Api::test_new(&server.url(), "k", Some("ws1"));
1090-
let tables = collect_tables(&api, None, None, None);
1108+
let tables = collect_tables(&api, None, None, None).unwrap();
10911109
mock.assert();
10921110
assert_eq!(tables.len(), 2);
10931111
assert_eq!(tables[0].table, "a");
@@ -1107,7 +1125,7 @@ mod tests {
11071125
.create();
11081126

11091127
let api = Api::test_new(&server.url(), "k", Some("ws"));
1110-
let rows = list_one_table_scan(&api, "cid", "sch", "tbl");
1128+
let rows = list_one_table_scan(&api, "cid", "sch", "tbl").unwrap();
11111129
mock.assert();
11121130
assert!(rows.is_empty());
11131131
}
@@ -1134,7 +1152,7 @@ mod tests {
11341152
.create();
11351153

11361154
let api = Api::test_new(&server.url(), "k", None);
1137-
let rows = list_one_table(&api, "cid", "sch", "tbl");
1155+
let rows = list_one_table(&api, "cid", "sch", "tbl").unwrap();
11381156
mock.assert();
11391157
assert_eq!(rows.len(), 1);
11401158
assert_eq!(rows[0].index_name, "ix1");
@@ -1164,7 +1182,7 @@ mod tests {
11641182
.create();
11651183

11661184
let api = Api::test_new(&server.url(), "k", None);
1167-
let rows = list_one_table(&api, "cid", "sch", "tbl");
1185+
let rows = list_one_table(&api, "cid", "sch", "tbl").unwrap();
11681186
mock.assert();
11691187
assert_eq!(rows.len(), 1);
11701188
assert_eq!(rows[0].status, "building");
@@ -1181,7 +1199,7 @@ mod tests {
11811199
.create();
11821200

11831201
let api = Api::test_new(&server.url(), "k", None);
1184-
let rows = list_one_table_scan(&api, "x", "s", "t");
1202+
let rows = list_one_table_scan(&api, "x", "s", "t").unwrap();
11851203
mock.assert();
11861204
assert!(rows.is_empty());
11871205
}

‎src/commands/usage.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ pub fn usage(workspace_id: &str, since: Option<&str>, format: &str) {
4141
let query: Vec<(&str, String)> = since
4242
.map(|s| vec![("since", s.to_string())])
4343
.unwrap_or_default();
44-
let u: Usage = api.get_json("/usage", &query).unwrap_or_else(|e| e.exit());
44+
let spinner = crate::util::spinner("Loading usage…");
45+
let result = api.get_json("/usage", &query);
46+
spinner.finish_and_clear();
47+
let u: Usage = result.unwrap_or_else(|e| e.exit());
4548

4649
match format {
4750
"json" => println!("{}", serde_json::to_string_pretty(&u).unwrap()),

0 commit comments

Comments
 (0)