arrowbricks 3.0.0__tar.gz → 3.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/PKG-INFO +1 -1
  2. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/pyproject.toml +1 -1
  3. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/Cargo.lock +1 -1
  4. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/Cargo.toml +1 -1
  5. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/src/client.rs +23 -2
  6. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/src/pipeline.rs +47 -32
  7. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests/wiremock_pipeline.rs +125 -0
  8. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests/wiremock_thrift.rs +15 -0
  9. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/thrift_mock.py +14 -0
  10. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/LICENSE +0 -0
  11. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/README.md +0 -0
  12. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/.gitignore +0 -0
  13. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/README.md +0 -0
  14. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/examples/duckdb_query.py +0 -0
  15. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/examples/fastapi_sse.py +0 -0
  16. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/rustfmt.toml +0 -0
  17. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/src/heartbeat.rs +0 -0
  18. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/src/json_convert.rs +0 -0
  19. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/src/lib.rs +0 -0
  20. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/src/thrift.rs +0 -0
  21. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests/wiremock_volume_files.rs +0 -0
  22. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/conftest.py +0 -0
  23. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_ipc_stream.py +0 -0
  24. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_parameters.py +0 -0
  25. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_stream_ndjson_lines.py +0 -0
  26. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_streaming.py +0 -0
  27. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_thrift.py +0 -0
  28. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_token_provider.py +0 -0
  29. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_volume_files.py +0 -0
  30. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/src/arrowbricks/__init__.py +0 -0
  31. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/src/arrowbricks/_core.pyi +0 -0
  32. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/src/arrowbricks/_streaming.py +0 -0
  33. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/src/arrowbricks/client.py +0 -0
  34. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/src/arrowbricks/cursor.py +0 -0
  35. {arrowbricks-3.0.0 → arrowbricks-3.0.1}/src/arrowbricks/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrowbricks
3
- Version: 3.0.0
3
+ Version: 3.0.1
4
4
  Requires-Dist: arro3-core>=0.8 ; extra == 'arro3'
5
5
  Provides-Extra: arro3
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arrowbricks"
3
- version = "3.0.0"
3
+ version = "3.0.1"
4
4
  description = "Runs SQL against a Databricks SQL warehouse via the Statement Execution API and hands you the result as Arrow -- a DB-API-ish Cursor (fetchone/fetchmany/fetchall/fetchall_arrow) or NDJSON streaming. Rust/PyO3 core throughout -- zero required runtime dependencies."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -240,7 +240,7 @@ dependencies = [
240
240
 
241
241
  [[package]]
242
242
  name = "arrowbricks_core"
243
- version = "3.0.0"
243
+ version = "3.0.1"
244
244
  dependencies = [
245
245
  "arrow",
246
246
  "arrow-json",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "arrowbricks_core"
3
- version = "3.0.0"
3
+ version = "3.0.1"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -980,7 +980,14 @@ impl DbClient {
980
980
  .map_err(join_error)?
981
981
  }
982
982
 
983
- async fn ensure_warehouse_running(&self) -> Result<(), ApiError> {
983
+ /// Shared by both protocols -- plain REST against `/api/2.0/sql/warehouses/{id}`,
984
+ /// nothing SEA- or Thrift-specific about it. SEA's own `submit_and_poll`
985
+ /// has always called this; the Thrift path (`pipeline::execute_lazy_thrift`)
986
+ /// didn't, which meant a stopped warehouse got no proactive wake on
987
+ /// `protocol="thrift"` (now the default) -- statement submission would
988
+ /// eventually surface an error instead, with no `warehouse_start_timeout`
989
+ /// wait for it to come up first. Fixed by calling this from both.
990
+ pub(crate) async fn ensure_warehouse_running(&self) -> Result<(), ApiError> {
984
991
  {
985
992
  let confirmed = *self.warehouse_confirmed_running_at.lock().unwrap();
986
993
  if let Some(at) = confirmed {
@@ -1661,12 +1668,26 @@ impl DbClient {
1661
1668
  };
1662
1669
  match fetched {
1663
1670
  Ok(blobs) => {
1671
+ // `meta.row_count` is the manifest's declared count for
1672
+ // the whole `chunk_index`, not per-blob -- safe to use as
1673
+ // `decode_chunk_item`'s truncation bound (same "server can
1674
+ // over-deliver past its declared count" protection Thrift's
1675
+ // resultLinks and arrowBatches paths both already have, see
1676
+ // AGENTS.md) only when there's exactly one blob, where
1677
+ // "the whole chunk's count" and "this blob's count" are the
1678
+ // same number. A chunk_index resolving to more than one
1679
+ // blob is a real but rare/defensive-coding case (see
1680
+ // `ChunkMeta::pre_resolved_links`'s own doc comment) whose
1681
+ // true per-blob row split isn't known here -- truncating
1682
+ // the first blob to the *whole* chunk's count would be
1683
+ // wrong, so those are left untruncated rather than guessed.
1684
+ let truncate_to = if blobs.len() == 1 { meta.row_count } else { None };
1664
1685
  for blob in blobs {
1665
1686
  let item = ChunkItem {
1666
1687
  blob,
1667
1688
  row_count: meta.row_count,
1668
1689
  chunk_index: meta.chunk_index,
1669
- truncate_to: None,
1690
+ truncate_to,
1670
1691
  };
1671
1692
  if worker_tx.send(Ok(item)).await.is_err() {
1672
1693
  return Ok(());
@@ -377,32 +377,6 @@ fn hex_encode(bytes: &[u8]) -> String {
377
377
  s
378
378
  }
379
379
 
380
- /// Submit -> (maybe poll) -> start background chunk fetching for the
381
- /// `protocol="thrift"` backend -- see `client::Protocol::Thrift`'s own doc
382
- /// comment for the wire-format background and why this is faster than SEA
383
- /// for small queries. Produces the exact same `ResultStream` shape as
384
- /// `execute_lazy`/`execute_lazy_prefer_inline` (same `fetchmany_arrow`/
385
- /// `fetchall_arrow`/`schema` contract, same `ReorderBuffer`/`decode_chunk`
386
- /// underneath), so `PyResultSet` and everything above it needs zero
387
- /// Thrift-specific handling.
388
- ///
389
- /// Session handling mirrors the SEA session pool (`client::SessionPool`)
390
- /// exactly, with one necessary difference: Thrift's `TExecuteStatementReq`
391
- /// *requires* a `sessionHandle` (unlike SEA's optional `session_id`), so
392
- /// pool exhaustion/creation failure can't fall back to a session-less
393
- /// submission the way SEA does -- instead, a fresh, unpooled session is
394
- /// opened just for this one call and closed again immediately afterward.
395
- /// This is a genuinely necessary fallback, not a shortcut: without it, a
396
- /// caller who exhausts `MAX_SESSIONS_PER_KEY` concurrent statements would
397
- /// get a hard failure instead of the same "a bit more session-creation
398
- /// overhead, but still works" degradation SEA gets.
399
- ///
400
- /// A session is only needed for the initial `ExecuteStatement` call --
401
- /// every subsequent RPC (`GetOperationStatus`/`FetchResults`/
402
- /// `CloseOperation`) addresses the operation directly by its own handle, so
403
- /// the session is checked back in (or closed, if it was a throwaway) right
404
- /// after `ExecuteStatement` returns, not held for the statement's whole
405
- /// lifetime the way one might expect from a "session" name.
406
380
  /// What's known once a Thrift statement has reached a terminal state and is
407
381
  /// ready to hand off to `drive_thrift_fetch_loop` -- a small bundle so
408
382
  /// `execute_lazy_thrift`'s own error paths (see its doc comment) have one
@@ -492,8 +466,31 @@ async fn submit_and_await_thrift_statement(
492
466
  }
493
467
 
494
468
  /// Submit -> (maybe poll) -> start background chunk fetching for the
495
- /// `protocol="thrift"` backend. See `client::Protocol::Thrift`'s own doc
496
- /// comment for the wire-format background.
469
+ /// `protocol="thrift"` backend -- see `client::Protocol::Thrift`'s own doc
470
+ /// comment for the wire-format background and why this is faster than SEA
471
+ /// for small queries. Produces the exact same `ResultStream` shape as
472
+ /// `execute_lazy`/`execute_lazy_prefer_inline` (same `fetchmany_arrow`/
473
+ /// `fetchall_arrow`/`schema` contract, same `ReorderBuffer`/`decode_chunk`
474
+ /// underneath), so `PyResultSet` and everything above it needs zero
475
+ /// Thrift-specific handling.
476
+ ///
477
+ /// Session handling mirrors the SEA session pool (`client::SessionPool`)
478
+ /// exactly, with one necessary difference: Thrift's `TExecuteStatementReq`
479
+ /// *requires* a `sessionHandle` (unlike SEA's optional `session_id`), so
480
+ /// pool exhaustion/creation failure can't fall back to a session-less
481
+ /// submission the way SEA does -- instead, a fresh, unpooled session is
482
+ /// opened just for this one call and closed again immediately afterward.
483
+ /// This is a genuinely necessary fallback, not a shortcut: without it, a
484
+ /// caller who exhausts `MAX_SESSIONS_PER_KEY` concurrent statements would
485
+ /// get a hard failure instead of the same "a bit more session-creation
486
+ /// overhead, but still works" degradation SEA gets.
487
+ ///
488
+ /// A session is only needed for the initial `ExecuteStatement` call --
489
+ /// every subsequent RPC (`GetOperationStatus`/`FetchResults`/
490
+ /// `CloseOperation`) addresses the operation directly by its own handle, so
491
+ /// the session is checked back in (or closed, if it was a throwaway) right
492
+ /// after `ExecuteStatement` returns, not held for the statement's whole
493
+ /// lifetime the way one might expect from a "session" name.
497
494
  ///
498
495
  /// **A session checked out for this call (whether pooled or a throwaway,
499
496
  /// see `client::ThriftSessionPool`'s doc comment) must stay open until the
@@ -526,6 +523,7 @@ pub async fn execute_lazy_thrift(
526
523
  schema: Option<&str>,
527
524
  parameters: Option<Value>,
528
525
  ) -> Result<ResultStream, ApiError> {
526
+ client.ensure_warehouse_running().await?;
529
527
  let pooled = client.thrift_checkout_session(catalog, schema).await;
530
528
  let (session, from_pool) = match pooled {
531
529
  Some(s) => (s, true),
@@ -1390,11 +1388,25 @@ pub struct JsonResult {
1390
1388
  /// Casting by the manifest's column type_name, if wanted, is left to the
1391
1389
  /// caller -- same pass-through the Python original leaves to its own
1392
1390
  /// caller.
1393
- fn decode_json_chunk(blob: &Bytes) -> Result<Vec<Vec<Option<String>>>, ApiError> {
1394
- serde_json::from_slice(&blob[..]).map_err(|e| ApiError {
1391
+ ///
1392
+ /// `truncate_to`, if `Some(n)` and the chunk decodes to more than `n` rows,
1393
+ /// slices to exactly the first `n` -- the JSON_ARRAY counterpart to
1394
+ /// `decode_chunk_item`'s Arrow-IPC truncation (see its own doc comment and
1395
+ /// `fetch_chunks_with_backpressure`'s `truncate_to` wiring): a JSON_ARRAY
1396
+ /// chunk is delivered through the exact same cloud-fetch blob-storage
1397
+ /// mechanism as an Arrow one, just decoded differently, so the same
1398
+ /// "server can over-deliver past its declared row count" risk applies here
1399
+ /// too. No batch-boundary complexity to worry about here (unlike the Arrow
1400
+ /// case) -- rows are already a flat `Vec`, so this is a plain `truncate`.
1401
+ fn decode_json_chunk(blob: &Bytes, truncate_to: Option<i64>) -> Result<Vec<Vec<Option<String>>>, ApiError> {
1402
+ let mut rows: Vec<Vec<Option<String>>> = serde_json::from_slice(&blob[..]).map_err(|e| ApiError {
1395
1403
  message: format!("bad JSON_ARRAY chunk: {e}"),
1396
1404
  transient: false,
1397
- })
1405
+ })?;
1406
+ if let Some(n) = truncate_to.filter(|n| *n > 0) {
1407
+ rows.truncate(n as usize);
1408
+ }
1409
+ Ok(rows)
1398
1410
  }
1399
1411
 
1400
1412
  /// JSON_ARRAY counterpart to `run_pipeline` -- same submit/poll/fetch/
@@ -1421,7 +1433,10 @@ pub async fn run_json_pipeline(
1421
1433
 
1422
1434
  let mut decode_handles = Vec::with_capacity(num_chunks);
1423
1435
  while let Some(item) = reorder.next().await? {
1424
- decode_handles.push(tokio::task::spawn_blocking(move || decode_json_chunk(&item.blob)));
1436
+ let truncate_to = item.truncate_to;
1437
+ decode_handles.push(tokio::task::spawn_blocking(move || {
1438
+ decode_json_chunk(&item.blob, truncate_to)
1439
+ }));
1425
1440
  }
1426
1441
 
1427
1442
  let mut rows = Vec::new();
@@ -336,6 +336,67 @@ async fn lazy_pipeline_skips_the_extra_resolution_get_for_a_pre_resolved_chunk()
336
336
  assert_ids_in_order(&batches, 10);
337
337
  }
338
338
 
339
+ /// Regression test for the same "server can over-deliver past its declared
340
+ /// row count" behavior Thrift's resultLinks/arrowBatches paths already
341
+ /// guard against (see AGENTS.md) -- found missing on this SEA path during a
342
+ /// later review pass, not yet observed to actually trigger against a real
343
+ /// warehouse (unlike the Thrift case, which was), but structurally
344
+ /// identical and cheap to close defensively: the manifest declares 3 rows
345
+ /// for chunk_index 0, the actual file backing it encodes 5. A single link
346
+ /// per chunk_index is the common case where `fetch_chunks_with_backpressure`
347
+ /// now passes the declared count through as `ChunkItem::truncate_to`.
348
+ #[tokio::test]
349
+ async fn lazy_pipeline_truncates_a_sea_chunk_that_overshoots_its_declared_row_count() {
350
+ let server = MockServer::start().await;
351
+
352
+ Mock::given(method("GET"))
353
+ .and(path(format!("/api/2.0/sql/warehouses/{WAREHOUSE_ID}")))
354
+ .respond_with(ResponseTemplate::new(200).set_body_json(json!({"state": "RUNNING"})))
355
+ .mount(&server)
356
+ .await;
357
+
358
+ let uri = server.uri();
359
+ Mock::given(method("POST"))
360
+ .and(path("/api/2.0/sql/statements"))
361
+ .respond_with(ResponseTemplate::new(200).set_body_json(json!({
362
+ "statement_id": STATEMENT_ID,
363
+ "status": {"state": "SUCCEEDED"},
364
+ "manifest": {
365
+ "chunks": [{"chunk_index": 0, "row_count": 3}],
366
+ "schema": {"columns": [
367
+ {"name": "id", "type_name": "LONG"},
368
+ {"name": "label", "type_name": "STRING"},
369
+ ]},
370
+ },
371
+ "result": {
372
+ "external_links": [{"chunk_index": 0, "external_link": format!("{uri}/_data/chunk-0")}]
373
+ },
374
+ })))
375
+ .mount(&server)
376
+ .await;
377
+
378
+ Mock::given(method("GET"))
379
+ .and(path_regex(r"^/_data/chunk-0$"))
380
+ .respond_with(
381
+ // Declares 3 rows above, but the file itself has 5 -- must be
382
+ // sliced down to 3, not returned in full.
383
+ ResponseTemplate::new(200).set_body_raw(build_chunk_bytes(0, 5), "application/vnd.apache.arrow.stream"),
384
+ )
385
+ .mount(&server)
386
+ .await;
387
+
388
+ let client = Arc::new(DbClient::new(&server.uri(), WAREHOUSE_ID, "fake-token").with_protocol(Protocol::Sea));
389
+ let mut stream = execute_lazy(client, "SELECT * FROM t", None, None, None).await.unwrap();
390
+ let (batches, _schema) = stream.fetchall_arrow().await.unwrap();
391
+
392
+ let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
393
+ assert_eq!(
394
+ total_rows, 3,
395
+ "must keep exactly the manifest's declared row count, not however many rows the file actually encoded"
396
+ );
397
+ assert_ids_in_order(&batches, 3);
398
+ }
399
+
339
400
  /// Regression test for a bug caught in code review before it shipped:
340
401
  /// `pre_resolved` was originally a plain `HashMap<i64, String>`, which keeps
341
402
  /// only the *last* entry when `result.external_links` has more than one for
@@ -739,6 +800,70 @@ async fn json_pipeline_happy_path() {
739
800
  assert_eq!(ids, (0..15).collect::<Vec<_>>());
740
801
  }
741
802
 
803
+ /// Regression test for the same "server can over-deliver past its declared
804
+ /// row count" behavior as the Arrow-format entries in this file and in
805
+ /// AGENTS.md -- a JSON_ARRAY chunk is delivered through the exact same
806
+ /// cloud-fetch blob-storage mechanism as an Arrow one, just decoded
807
+ /// differently, so the same risk applies here. The manifest declares 3 rows
808
+ /// for chunk_index 0, the actual file backing it has 5.
809
+ #[tokio::test]
810
+ async fn json_pipeline_truncates_a_chunk_that_overshoots_its_declared_row_count() {
811
+ let server = MockServer::start().await;
812
+
813
+ Mock::given(method("GET"))
814
+ .and(path(format!("/api/2.0/sql/warehouses/{WAREHOUSE_ID}")))
815
+ .respond_with(ResponseTemplate::new(200).set_body_json(json!({"state": "RUNNING"})))
816
+ .mount(&server)
817
+ .await;
818
+
819
+ let uri = server.uri();
820
+ Mock::given(method("POST"))
821
+ .and(path("/api/2.0/sql/statements"))
822
+ .respond_with(ResponseTemplate::new(200).set_body_json(json!({
823
+ "statement_id": STATEMENT_ID,
824
+ "status": {"state": "SUCCEEDED"},
825
+ "manifest": {
826
+ "chunks": [{"chunk_index": 0, "row_count": 3}],
827
+ "schema": {"columns": [
828
+ {"name": "id", "type_name": "LONG"},
829
+ {"name": "label", "type_name": "STRING"},
830
+ ]},
831
+ },
832
+ "result": {
833
+ "external_links": [{"chunk_index": 0, "external_link": format!("{uri}/_data/chunk-0")}]
834
+ },
835
+ })))
836
+ .mount(&server)
837
+ .await;
838
+
839
+ // Declares 3 rows above, but the file itself has 5.
840
+ let rows: Vec<_> = (0..5)
841
+ .map(|id: i64| json!([id.to_string(), format!("row_{id}")]))
842
+ .collect();
843
+ Mock::given(method("GET"))
844
+ .and(path_regex(r"^/_data/chunk-0$"))
845
+ .respond_with(ResponseTemplate::new(200).set_body_json(rows))
846
+ .mount(&server)
847
+ .await;
848
+
849
+ let client = Arc::new(DbClient::new(&server.uri(), WAREHOUSE_ID, "fake-token").with_protocol(Protocol::Sea));
850
+ let result = run_json_pipeline(client, "SELECT * FROM t", None, None, None)
851
+ .await
852
+ .unwrap();
853
+
854
+ assert_eq!(
855
+ result.rows.len(),
856
+ 3,
857
+ "must keep exactly the manifest's declared row count, not however many rows the file actually encoded"
858
+ );
859
+ let ids: Vec<i64> = result
860
+ .rows
861
+ .iter()
862
+ .map(|r| r[0].as_ref().unwrap().parse().unwrap())
863
+ .collect();
864
+ assert_eq!(ids, vec![0, 1, 2]);
865
+ }
866
+
742
867
  #[tokio::test]
743
868
  async fn json_pipeline_survives_reverse_arrival() {
744
869
  let server = MockServer::start().await;
@@ -361,7 +361,22 @@ fn thrift_client(server: &MockServer) -> Arc<DbClient> {
361
361
  Arc::new(DbClient::new(&server.uri(), WAREHOUSE_ID, "fake-token").with_protocol(Protocol::Thrift))
362
362
  }
363
363
 
364
+ /// Every `execute_lazy_thrift` call opens with `ensure_warehouse_running`
365
+ /// (a plain REST GET, shared with SEA -- see `DbClient::ensure_warehouse_running`'s
366
+ /// own doc comment for why the Thrift path didn't have this until now)
367
+ /// before it ever touches a session, so every test that submits a Thrift
368
+ /// statement needs this mocked -- bundled into the near-universal
369
+ /// "set up a session" helper below rather than repeated at every call site.
370
+ async fn mount_warehouse_running(server: &MockServer) {
371
+ Mock::given(method("GET"))
372
+ .and(path(format!("/api/2.0/sql/warehouses/{WAREHOUSE_ID}")))
373
+ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"state": "RUNNING"})))
374
+ .mount(server)
375
+ .await;
376
+ }
377
+
364
378
  async fn mount_open_session_always(server: &MockServer, guid: &'static [u8]) -> Arc<AtomicUsize> {
379
+ mount_warehouse_running(server).await;
365
380
  let calls = Arc::new(AtomicUsize::new(0));
366
381
  let calls_for_mock = calls.clone();
367
382
  Mock::given(method("POST"))
@@ -35,6 +35,7 @@ from __future__ import annotations
35
35
 
36
36
  import http.server
37
37
  import io
38
+ import json
38
39
  import re
39
40
  import threading
40
41
  from collections.abc import Callable
@@ -355,6 +356,19 @@ class ThriftMockServer:
355
356
  self.wfile.write(resp_bytes)
356
357
 
357
358
  def do_GET(self) -> None: # noqa: N802
359
+ # Every execute_lazy_thrift call opens with ensure_warehouse_running
360
+ # (a plain REST GET, shared with SEA -- see DbClient::ensure_warehouse_running's
361
+ # own doc comment) before it ever touches a session -- built in here,
362
+ # not test-registered, since every test that submits a Thrift
363
+ # statement needs it, same as tests/conftest.py's SEA mock_warehouse.
364
+ if self.path == f"/api/2.0/sql/warehouses/{outer.warehouse_id}":
365
+ body = json.dumps({"state": "RUNNING"}).encode()
366
+ self.send_response(200)
367
+ self.send_header("Content-Type", "application/json")
368
+ self.send_header("Content-Length", str(len(body)))
369
+ self.end_headers()
370
+ self.wfile.write(body)
371
+ return
358
372
  for pattern, responder in outer._get_routes:
359
373
  m = pattern.match(self.path)
360
374
  if m:
File without changes
File without changes