Skip to content

Commit 8b028b2

Browse files
committed
fix(databases): scope result load to the target database
`databases load --result` (and `tables load --result`) hit the connection-scoped load endpoint, where the server scopes a result-sourced load by the X-Database-Id header. That header carried the ambient active database, which may be unset or differ from the --catalog/--database target, so the load could be rejected (missing scope) or resolve against the wrong database. Re-scope the load client to the resolved target database via the new Api::scoped_to_database, so the load request always carries X-Database-Id for the addressed database.
1 parent 3a3d1a0 commit 8b028b2

2 files changed

Lines changed: 49 additions & 1 deletion

File tree

src/client/sdk.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,19 @@ impl Api {
609609
self.database_id.as_deref()
610610
}
611611

612+
/// Return a clone of this handle scoped to `database_id`, so its requests
613+
/// carry the `X-Database-Id` header regardless of the ambient active
614+
/// database. Used by database-addressed flows (e.g. `databases load`) that
615+
/// resolve the target database from a `--catalog` / `--database` argument:
616+
/// the server scopes a result-sourced load by this header, so it must name
617+
/// the load target, not whatever database happens to be active.
618+
pub fn scoped_to_database(&self, database_id: &str) -> Api {
619+
Api {
620+
database_id: Some(database_id.to_string()),
621+
..self.clone()
622+
}
623+
}
624+
612625
/// Best-effort probe of the scoped workspace's runtimedb scale state, via
613626
/// the control-plane `GET /v1/workspaces/{id}/runtime/status` endpoint.
614627
///
@@ -1442,6 +1455,30 @@ mod tests {
14421455
m.assert();
14431456
}
14441457

1458+
#[test]
1459+
fn scoped_to_database_sends_x_database_id_without_ambient_scope() {
1460+
// `databases load --catalog X` builds an unscoped Api (no active
1461+
// database) and re-scopes it to the resolved target, so the load POST
1462+
// carries X-Database-Id even when nothing is set via `databases set`.
1463+
let mut server = mockito::Server::new();
1464+
let m = server
1465+
.mock("POST", "/v1/query")
1466+
.match_header("X-Database-Id", "db-target")
1467+
.with_status(200)
1468+
.with_header("content-type", "application/json")
1469+
.with_body(r#"{"ok":true}"#)
1470+
.create();
1471+
1472+
let api = Api::test_new(&server.url(), "test-jwt", Some("ws-1"));
1473+
assert_eq!(api.database_id(), None, "test_new starts unscoped");
1474+
let api = api.scoped_to_database("db-target");
1475+
let (status, _body) = api
1476+
.post_raw("/query", &serde_json::json!({"sql": "select 1"}))
1477+
.expect("post_raw should succeed");
1478+
assert_eq!(status, reqwest::StatusCode::OK);
1479+
m.assert();
1480+
}
1481+
14451482
// --- presigned direct-to-storage upload ---------------------------------
14461483

14471484
/// A deterministic ASCII payload of `len` bytes written to a temp parquet

src/commands/databases.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1404,6 +1404,11 @@ pub fn tables_load(
14041404
None => database.clone(),
14051405
};
14061406
let db = resolve_database(&api, &lookup_key);
1407+
// Scope requests to the resolved target database. The load hits the
1408+
// connection-scoped endpoint, where the server scopes a result-sourced load
1409+
// by the X-Database-Id header; that must name the --catalog/--database
1410+
// target, not the ambient active database (which may be unset or different).
1411+
let api = api.scoped_to_database(&db.id);
14071412
let schema = schema_name(schema);
14081413

14091414
// clap enforces mutual exclusion; only one source is ever Some. A file/URL
@@ -2154,12 +2159,15 @@ mod tests {
21542159
.with_body(full_detail("db_1", "sales", "conn_default"))
21552160
.create();
21562161
// A result load hits the same loads endpoint but with a result_id body
2157-
// and no upload step (no POST /v1/uploads mock is registered).
2162+
// and no upload step (no POST /v1/uploads mock is registered). The
2163+
// server scopes a result-sourced load by X-Database-Id, so the request
2164+
// must carry the resolved target database id.
21582165
let load = server
21592166
.mock(
21602167
"POST",
21612168
"/v1/connections/conn_default/schemas/public/tables/orders/loads",
21622169
)
2170+
.match_header("X-Database-Id", "db_1")
21632171
.match_body(mockito::Matcher::JsonString(
21642172
serde_json::to_string(&load_table_request_from_result("rslt_123")).unwrap(),
21652173
))
@@ -2177,6 +2185,9 @@ mod tests {
21772185

21782186
let api = Api::test_new(&server.url(), "k", Some("ws1"));
21792187
let db = resolve_database(&api, "db_1");
2188+
// Mirror tables_load: scope to the resolved target database so the load
2189+
// carries X-Database-Id.
2190+
let api = api.scoped_to_database(&db.id);
21802191
let path = managed_table_load_path(&db.default_connection_id, "public", "orders");
21812192
let body = load_table_request_from_result("rslt_123");
21822193
let (status, resp_body) = api.post_raw(&path, &body).unwrap();

0 commit comments

Comments
 (0)