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.
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/PKG-INFO +1 -1
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/pyproject.toml +1 -1
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/Cargo.lock +1 -1
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/Cargo.toml +1 -1
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/src/client.rs +23 -2
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/src/pipeline.rs +47 -32
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests/wiremock_pipeline.rs +125 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests/wiremock_thrift.rs +15 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/thrift_mock.py +14 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/LICENSE +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/README.md +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/.gitignore +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/README.md +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/examples/duckdb_query.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/examples/fastapi_sse.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/rustfmt.toml +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/src/heartbeat.rs +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/src/json_convert.rs +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/src/lib.rs +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/src/thrift.rs +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests/wiremock_volume_files.rs +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/conftest.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_ipc_stream.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_parameters.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_stream_ndjson_lines.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_streaming.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_thrift.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_token_provider.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_volume_files.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/src/arrowbricks/__init__.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/src/arrowbricks/_core.pyi +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/src/arrowbricks/_streaming.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/src/arrowbricks/client.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/src/arrowbricks/cursor.py +0 -0
- {arrowbricks-3.0.0 → arrowbricks-3.0.1}/src/arrowbricks/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "arrowbricks"
|
|
3
|
-
version = "3.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"
|
|
@@ -980,7 +980,14 @@ impl DbClient {
|
|
|
980
980
|
.map_err(join_error)?
|
|
981
981
|
}
|
|
982
982
|
|
|
983
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1394
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests/wiremock_volume_files.rs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_stream_ndjson_lines.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{arrowbricks-3.0.0 → arrowbricks-3.0.1}/rust/arrowbricks_core/tests_py/test_token_provider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|