Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ path = "src/main.rs"
# behind a shared multi-thread tokio runtime. The `arrow` feature backs result
# decode; `upload_file` runs the presigned direct-to-storage upload flow
# (see src/sdk.rs::Api::upload).
hotdata = { version = "0.9.1", features = ["arrow"] }
hotdata = { version = "0.10.0", features = ["arrow"] }
# Shared multi-thread runtime for the sync wrapper; block_on is called
# concurrently from rayon worker threads (see src/indexes.rs). The presigned
# upload (src/sdk.rs::Api::upload) drives the SDK's async `upload_file` on this
Expand Down
136 changes: 122 additions & 14 deletions src/commands/databases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ use std::path::Path;
pub enum DatabasesCommands {
/// List managed databases in the workspace
List {
/// Maximum number of databases to return
#[arg(long)]
limit: Option<u32>,

/// Pagination cursor from a previous response
#[arg(long)]
cursor: Option<String>,

/// Output format
#[arg(long = "output", short = 'o', default_value = "table", value_parser = ["table", "json", "yaml"])]
output: String,
Expand Down Expand Up @@ -411,15 +419,52 @@ pub(crate) fn get_database(api: &Api, id: &str) -> Result<Database, ApiError> {
block(api.client().databases().get(id)).map(Database::from)
}

/// List databases through the SDK's typed `databases().list` handle, mapped
/// into the CLI's summary rows.
/// Drain every page of the paginated databases list into the full set of
/// summary rows.
///
/// Routed through [`block_with_wakeup`] so a cold KEDA start surfaces a "waking
/// up worker" hint instead of an unexplained pause — `databases list` is a
/// common first command against an idle workspace.
/// `GET /v1/databases` returns one capped page plus a `next_cursor`; the CLI
/// shows and resolves against the whole workspace, so follow the cursor to the
/// end. When `spinner` is `Some`, the first page is fetched via
/// [`block_with_wakeup`] so a cold KEDA start surfaces a "waking up worker"
/// hint; the remaining pages (and the spinner-less id-only caller) use
/// [`block`].
fn list_all_databases(
api: &Api,
spinner: Option<&str>,
) -> Result<Vec<hotdata::models::DatabaseSummary>, ApiError> {
let mut all = Vec::new();
let mut cursor: Option<String> = None;
let mut first = true;
loop {
let resp = match (first, spinner) {
(true, Some(msg)) => block_with_wakeup(
api,
msg,
api.client().databases().list(None, cursor.as_deref()),
)?,
_ => block(api.client().databases().list(None, cursor.as_deref()))?,
};
first = false;
// Move the page rows out first (partial move), then read next_cursor —
// no clone needed.
all.extend(resp.databases);
let next = resp.next_cursor.flatten();
// Stop at the last page, or if the server ever returns a non-advancing
// cursor (defensive — never loop forever).
match next {
Some(c) if !c.is_empty() && Some(&c) != cursor.as_ref() => cursor = Some(c),
_ => break,
}
}
Ok(all)
}
Comment thread
anoop-narang marked this conversation as resolved.

/// List databases, mapped into the CLI's summary rows. Drains all pages so
/// `databases list` shows the whole workspace and name/catalog resolution sees
/// every database (see [`list_all_databases`]).
fn list_database_summaries(api: &Api) -> Result<Vec<DatabaseSummary>, ApiError> {
block_with_wakeup(api, "Loading databases...", api.client().databases().list())
.map(|r| r.databases.into_iter().map(DatabaseSummary::from).collect())
list_all_databases(api, Some("Loading databases..."))
.map(|dbs| dbs.into_iter().map(DatabaseSummary::from).collect())
}

/// List the ids of every managed database in the workspace.
Expand All @@ -431,11 +476,10 @@ fn list_database_summaries(api: &Api) -> Result<Vec<DatabaseSummary>, ApiError>
/// `default_connection_id` via [`get_database`]. The list summary omits the
/// connection id, hence ids only.
///
/// Deliberately spinner-less (plain [`block`], unlike
/// [`list_database_summaries`]): the caller owns its own "Loading indexes…"
/// spinner, and two indicatif bars would fight over the same line.
/// Deliberately spinner-less (the caller owns its own "Loading indexes…"
/// spinner, and two indicatif bars would fight over the same line).
pub(crate) fn list_database_ids(api: &Api) -> Result<Vec<String>, ApiError> {
block(api.client().databases().list()).map(|r| r.databases.into_iter().map(|d| d.id).collect())
list_all_databases(api, None).map(|dbs| dbs.into_iter().map(|d| d.id).collect())
}

fn fetch_database(api: &Api, id: &str) -> Database {
Expand Down Expand Up @@ -883,10 +927,30 @@ fn collect_tables(
(out, false, None)
}

pub fn list(workspace_id: &str, format: &str) {
pub fn list(workspace_id: &str, format: &str, limit: Option<u32>, cursor: Option<&str>) {
let api = Api::new(Some(workspace_id));
let mut databases = list_database_summaries(&api).unwrap_or_else(|e| e.exit());
databases.sort_by(|a, b| a.created_at.cmp(&b.created_at));
// One page, user-paginated (like `tables list` / `queries list`). Name/
// catalog resolution and the whole-workspace `indexes list` scan still see
// every database — they drain all pages internally via list_all_databases;
// only this display command pages.
let resp = block_with_wakeup(
&api,
"Loading databases...",
api.client()
.databases()
.list(limit.map(|l| l as i32), cursor),
)
.unwrap_or_else(|e| e.exit());
let has_more = resp.has_more.flatten().unwrap_or(false);
// Take next_cursor before moving the rows out below — no clone needed.
let next_cursor = resp.next_cursor.flatten();
// Server returns newest-first; keep that order so the cursor continuation is
// coherent (no client re-sort across pages).
let databases: Vec<DatabaseSummary> = resp
.databases
.into_iter()
.map(DatabaseSummary::from)
.collect();

match format {
"json" => println!("{}", serde_json::to_string_pretty(&databases).unwrap()),
Expand Down Expand Up @@ -925,6 +989,18 @@ pub fn list(workspace_id: &str, format: &str) {
}
_ => unreachable!(),
}

if has_more {
use crossterm::style::Stylize;
eprintln!(
"{}",
format!(
"More results available. Use --cursor {} to fetch the next page.",
next_cursor.as_deref().unwrap_or("")
)
.dark_grey()
);
}
}

pub fn get(workspace_id: &str, id_or_name: &str, format: &str) {
Expand Down Expand Up @@ -2330,6 +2406,38 @@ mod tests {
assert_eq!(tables[1].table, "b");
}

#[test]
fn list_all_databases_follows_cursor() {
let mut server = mockito::Server::new();
// Page 2 (reached via ?cursor=cur2): the last database, no next_cursor.
let page2 = server
.mock("GET", "/v1/databases")
.match_query(mockito::Matcher::UrlEncoded("cursor".into(), "cur2".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"databases":[{"id":"db2","name":"second","default_catalog":"c2","default_schema":"main"}],"count":1,"limit":1,"has_more":false,"next_cursor":null}"#,
)
.create();
// Page 1 (first request, no cursor): first database + a next_cursor.
let page1 = server
.mock("GET", "/v1/databases")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"databases":[{"id":"db1","name":"first","default_catalog":"c1","default_schema":"main"}],"count":1,"limit":1,"has_more":true,"next_cursor":"cur2"}"#,
)
.create();

let api = Api::test_new(&server.url(), "k", Some("ws"));
let dbs = list_all_databases(&api, None).unwrap();
page1.assert();
page2.assert();
assert_eq!(dbs.len(), 2, "drain must reassemble both pages");
assert_eq!(dbs[0].id, "db1");
assert_eq!(dbs[1].id, "db2");
}

#[test]
fn collect_tables_with_limit_makes_single_request_and_returns_cursor() {
let mut server = mockito::Server::new();
Expand Down
8 changes: 5 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,11 @@ fn main() {
databases::get(&workspace_id, &name_or_id, &output);
} else {
match command {
Some(DatabasesCommands::List { output }) => {
databases::list(&workspace_id, &output)
}
Some(DatabasesCommands::List {
output,
limit,
cursor,
}) => databases::list(&workspace_id, &output, limit, cursor.as_deref()),
Some(DatabasesCommands::Show { name_or_id, output }) => {
databases::get(&workspace_id, &name_or_id, &output)
}
Expand Down
Loading