-
Notifications
You must be signed in to change notification settings - Fork 0
fix(query): resolve --database name/catalog to id #247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -439,12 +439,23 @@ fn fail_run(error_msg: &str) -> ! { | |
| std::process::exit(1); | ||
| } | ||
|
|
||
| /// Resolve an explicit `--database` name/catalog/id to its id for the | ||
| /// `X-Database-Id` scope, matching `databases delete`/`tables`. The header | ||
| /// requires an id, so a bare name/catalog would otherwise 404 as | ||
| /// "Database '<name>' not found". `None` (flag omitted) passes through with no | ||
| /// lookup so the construction-time default database is kept. | ||
| fn resolve_query_database(api: &Api, database: Option<&str>) -> Option<String> { | ||
| database.map(|d| crate::commands::databases::resolve_database(api, d).id) | ||
| } | ||
|
|
||
| pub fn execute(sql: &str, workspace_id: &str, database: Option<&str>, format: &str) { | ||
| // Scope to the explicit --database flag, else the active database resolved | ||
| // at construction (HOTDATA_DATABASE / current database). The scoped `Api` | ||
| // carries the database into submit_query's `X-Database-Id` header and into | ||
| // the database-scoped follow-up fetches (query-run poll, Arrow result). | ||
| let api = Api::new(Some(workspace_id)).scoped_to_database_opt(database); | ||
| let api = Api::new(Some(workspace_id)); | ||
| let resolved = resolve_query_database(&api, database); | ||
| let api = api.scoped_to_database_opt(resolved.as_deref()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Same raw pass-through also exists in |
||
| let database = api.database_id(); | ||
|
|
||
| let mut request = hotdata::models::QueryRequest::new(sql.to_string()); | ||
|
|
@@ -717,6 +728,46 @@ mod tests { | |
| resp | ||
| } | ||
|
|
||
| #[test] | ||
| fn query_database_flag_resolves_name_to_id() { | ||
| // Regression: `hotdata query --database <name>` must resolve a name or | ||
| // catalog alias to its id before scoping the X-Database-Id header — | ||
| // otherwise the server 404s as "Database '<name>' not found". | ||
| let mut server = mockito::Server::new(); | ||
| // name isn't a valid id → 404, then list matches by name → detail by id. | ||
| server | ||
| .mock("GET", "/v1/databases/warehouse") | ||
| .with_status(404) | ||
| .with_body(r#"{"error":"not found"}"#) | ||
| .create(); | ||
| server | ||
| .mock("GET", "/v1/databases") | ||
| .with_status(200) | ||
| .with_header("content-type", "application/json") | ||
| .with_body( | ||
| r#"{"databases":[{"id":"db_xyz","name":"warehouse","default_catalog":"wh","default_schema":"main"}]}"#, | ||
| ) | ||
| .create(); | ||
| server | ||
| .mock("GET", "/v1/databases/db_xyz") | ||
| .with_status(200) | ||
| .with_header("content-type", "application/json") | ||
| .with_body( | ||
| r#"{"id":"db_xyz","name":"warehouse","default_catalog":"wh","default_schema":"main","default_connection_id":"conn_1","attachments":[]}"#, | ||
| ) | ||
| .create(); | ||
|
|
||
| let api = Api::test_new(&server.url(), "k", Some("ws")); | ||
| assert_eq!( | ||
| resolve_query_database(&api, Some("warehouse")).as_deref(), | ||
| Some("db_xyz"), | ||
| "a --database name must resolve to its id" | ||
| ); | ||
| // Flag omitted → no lookup, passes straight through as None so the | ||
| // construction-time default database is preserved. | ||
| assert_eq!(resolve_query_database(&api, None), None); | ||
| } | ||
|
|
||
| #[test] | ||
| fn hint_for_missing_database_context() { | ||
| let tip = cross_source_hint( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blocking: this makes name→id resolution unconditional, which regresses
--database <id>for database API token credentials.The repo already documents that such a token can't reach these endpoints:
src/commands/databases.rs:1546-1554— "A database API token can't callGET /v1/databases/{id}(denied by its allow-list), so skip the check for it and save the id directly."src/commands/databases.rs:1689-1695— "A database API token can't resolve names/catalogs … Route it through the database-scoped endpoints, addressed by database id."With such a token,
hotdata query --database <db_id> "SELECT …"works today because the flag is passed verbatim intoX-Database-Idand the server accepts the scoped token. After this change the CLI first callsresolve_database→get_database, which the allow-list denies.none_if_404(src/client/sdk.rs:384-392) only swallows a 404, so a 403/401 propagates straight toe.exit()insidetry_resolve_database(src/commands/databases.rs:494) and the query never runs. If the denial happens to surface as 404, the fallbackGET /v1/databasesis denied too — same hard failure, orno database with id, catalog, or name '<id>'for a value that is a perfectly valid id.Suggested fix: skip resolution for that credential and pass the flag through, mirroring the two existing guards. Since this is now the third site inlining the same check, it's worth extracting a helper (e.g.
credentials::is_database_api_token()) rather than copying theconfig::load("default")+api_key_jwt_sourcedance a third time.A unit test covering "database API token +
--database <id>performs no lookup" would lock this in alongside the new resolution test.