arrowbricks 5.0.2__tar.gz → 5.0.3__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-5.0.2 → arrowbricks-5.0.3}/PKG-INFO +1 -1
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/pyproject.toml +1 -1
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/Cargo.lock +1 -1
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/Cargo.toml +1 -1
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/client/download.rs +11 -4
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/client/sea.rs +1 -8
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/client.rs +1 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/heartbeat.rs +4 -17
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/json_convert.rs +26 -38
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/pipeline/ndjson.rs +14 -26
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/pipeline/reorder.rs +9 -25
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/pipeline/sea.rs +6 -9
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/pipeline/thrift_exec.rs +2 -10
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/src/arrowbricks/cursor.py +15 -10
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/LICENSE +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/README.md +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/.gitignore +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/README.md +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/examples/duckdb_query.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/examples/fastapi_sse.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/proptest-regressions/json_convert.txt +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/rustfmt.toml +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/arrow_ffi.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/client/error.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/client/model.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/client/thrift_rpc.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/client/volume.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/lib.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/pipeline/stats.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/pipeline/test_support.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/pipeline.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/src/thrift.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests/common/mod.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests/wiremock_pipeline.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests/wiremock_thrift.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests/wiremock_volume_files.rs +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests_py/conftest.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests_py/test_ipc_stream.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests_py/test_parameters.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests_py/test_stream_ndjson_lines.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests_py/test_streaming.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests_py/test_thrift.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests_py/test_token_provider.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests_py/test_volume_files.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests_py/thrift_mock.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/src/arrowbricks/__init__.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/src/arrowbricks/_core.pyi +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/src/arrowbricks/_streaming.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/src/arrowbricks/client.py +0 -0
- {arrowbricks-5.0.2 → arrowbricks-5.0.3}/src/arrowbricks/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "arrowbricks"
|
|
3
|
-
version = "5.0.
|
|
3
|
+
version = "5.0.3"
|
|
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"
|
|
@@ -39,14 +39,21 @@ use super::model::QueryStatsAccumulator;
|
|
|
39
39
|
/// calls) -- so looping `read_to_end` on one decoder until its underlying
|
|
40
40
|
/// reader is exhausted reads every frame without reconstructing a decoder.
|
|
41
41
|
pub(crate) fn decompress_lz4_frame(compressed: &Bytes) -> Result<Bytes, ApiError> {
|
|
42
|
-
use std::io::Read;
|
|
43
42
|
// LZ4 on Arrow-IPC data often compresses several-fold. Starting at the
|
|
44
43
|
// compressed size can therefore pay for repeated buffer growth. Neither
|
|
45
44
|
// size is a bound on the other: incompressible input can expand slightly.
|
|
46
45
|
// `* 4` remains a heuristic; Vec grows normally when it underestimates.
|
|
47
46
|
// See benchmark_lz4_capacity_hypotheses for the allocation tradeoffs.
|
|
48
47
|
let mut out = Vec::with_capacity(compressed.len() * 4);
|
|
49
|
-
|
|
48
|
+
lz4_frame_decode_into(compressed, &mut out)?;
|
|
49
|
+
Ok(Bytes::from(out))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Appends every concatenated LZ4 frame in `src` to `out` -- the loop
|
|
53
|
+
/// `decompress_lz4_frame` and the Thrift inline-blob path both need.
|
|
54
|
+
pub(crate) fn lz4_frame_decode_into(src: &[u8], out: &mut Vec<u8>) -> Result<(), ApiError> {
|
|
55
|
+
use std::io::Read;
|
|
56
|
+
let mut decoder = lz4_flex::frame::FrameDecoder::new(src);
|
|
50
57
|
// Terminate on the *reader* being exhausted, not on "output stopped
|
|
51
58
|
// growing" -- found in code review that a frame which happens to decode
|
|
52
59
|
// to zero bytes (a real, valid LZ4 Frame shape: header + immediate
|
|
@@ -61,10 +68,10 @@ pub(crate) fn decompress_lz4_frame(compressed: &Bytes) -> Result<Bytes, ApiError
|
|
|
61
68
|
// between two real ones, see `decompress_lz4_frame_survives_a_zero_content_frame_in_the_middle`.
|
|
62
69
|
while !decoder.get_ref().is_empty() {
|
|
63
70
|
decoder
|
|
64
|
-
.read_to_end(
|
|
71
|
+
.read_to_end(out)
|
|
65
72
|
.map_err(|e| ApiError::permanent(format!("LZ4 frame decompress failed: {e}")))?;
|
|
66
73
|
}
|
|
67
|
-
Ok(
|
|
74
|
+
Ok(())
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
/// Reads the body into one buffer sized from `Content-Length`. Frames are
|
|
@@ -654,20 +654,13 @@ async fn join_first_error(handles: Vec<tokio::task::JoinHandle<Result<(), ApiErr
|
|
|
654
654
|
#[cfg(test)]
|
|
655
655
|
mod tests {
|
|
656
656
|
use super::*;
|
|
657
|
-
use crate::client::ApiErrorKind;
|
|
658
657
|
|
|
659
658
|
fn ok_task() -> tokio::task::JoinHandle<Result<(), ApiError>> {
|
|
660
659
|
tokio::spawn(async { Ok(()) })
|
|
661
660
|
}
|
|
662
661
|
|
|
663
662
|
fn err_task(msg: &'static str) -> tokio::task::JoinHandle<Result<(), ApiError>> {
|
|
664
|
-
tokio::spawn(async move {
|
|
665
|
-
Err(ApiError {
|
|
666
|
-
message: msg.to_string(),
|
|
667
|
-
transient: false,
|
|
668
|
-
kind: ApiErrorKind::Other,
|
|
669
|
-
})
|
|
670
|
-
})
|
|
663
|
+
tokio::spawn(async move { Err(ApiError::permanent(msg)) })
|
|
671
664
|
}
|
|
672
665
|
|
|
673
666
|
fn panicking_task() -> tokio::task::JoinHandle<Result<(), ApiError>> {
|
|
@@ -12,7 +12,7 @@ use std::time::{Duration, Instant};
|
|
|
12
12
|
|
|
13
13
|
use tokio::task::JoinHandle;
|
|
14
14
|
|
|
15
|
-
use crate::client::{ApiError,
|
|
15
|
+
use crate::client::{ApiError, join_error};
|
|
16
16
|
|
|
17
17
|
/// Matches `_streaming.py`'s `_HEARTBEAT_INTERVAL_S` -- well under typical
|
|
18
18
|
/// PaaS idle-connection ceilings for a caller forwarding these as SSE
|
|
@@ -85,11 +85,7 @@ async fn check_deadline_or_wait<T>(
|
|
|
85
85
|
let _ = handle.await;
|
|
86
86
|
*handle_slot = None;
|
|
87
87
|
let secs = total_timeout_s.unwrap_or(0.0);
|
|
88
|
-
Err(ApiError {
|
|
89
|
-
message: format!("Query exceeded {secs}s timeout"),
|
|
90
|
-
transient: false,
|
|
91
|
-
kind: ApiErrorKind::Other,
|
|
92
|
-
})
|
|
88
|
+
Err(ApiError::permanent(format!("Query exceeded {secs}s timeout")))
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
/// Shared `Drop` logic for `HeartbeatWait`/`HeartbeatStream`: best-effort, if
|
|
@@ -475,17 +471,8 @@ mod tests {
|
|
|
475
471
|
|
|
476
472
|
#[tokio::test]
|
|
477
473
|
async fn wrapped_future_error_propagates() {
|
|
478
|
-
let mut wait: HeartbeatWait<()> =
|
|
479
|
-
async {
|
|
480
|
-
Err(ApiError {
|
|
481
|
-
message: "boom".into(),
|
|
482
|
-
transient: false,
|
|
483
|
-
kind: ApiErrorKind::Other,
|
|
484
|
-
})
|
|
485
|
-
},
|
|
486
|
-
None,
|
|
487
|
-
TEST_INTERVAL,
|
|
488
|
-
);
|
|
474
|
+
let mut wait: HeartbeatWait<()> =
|
|
475
|
+
HeartbeatWait::with_interval(async { Err(ApiError::permanent("boom")) }, None, TEST_INTERVAL);
|
|
489
476
|
let err = wait.tick().await.unwrap_err();
|
|
490
477
|
assert_eq!(err.message, "boom");
|
|
491
478
|
}
|
|
@@ -28,14 +28,12 @@ use arrow_schema::{DataType, Field, Fields, Schema, TimeUnit};
|
|
|
28
28
|
use chrono::{DateTime, NaiveDate, NaiveDateTime};
|
|
29
29
|
use serde_json::Value as JsonValue;
|
|
30
30
|
|
|
31
|
-
use crate::client::{ApiError,
|
|
31
|
+
use crate::client::{ApiError, ColumnDescription};
|
|
32
32
|
|
|
33
33
|
fn conv_err(column: &str, value: &str, type_name: &str, detail: impl std::fmt::Display) -> ApiError {
|
|
34
|
-
ApiError
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
kind: ApiErrorKind::Other,
|
|
38
|
-
}
|
|
34
|
+
ApiError::permanent(format!(
|
|
35
|
+
"column `{column}` (type {type_name}): could not parse {value:?}: {detail}"
|
|
36
|
+
))
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
pub fn json_array_to_record_batch(
|
|
@@ -46,11 +44,10 @@ pub fn json_array_to_record_batch(
|
|
|
46
44
|
let mut arrays: Vec<ArrayRef> = Vec::with_capacity(columns.len());
|
|
47
45
|
|
|
48
46
|
for (col_idx, col) in columns.iter().enumerate() {
|
|
49
|
-
let type_name = col
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
})?;
|
|
47
|
+
let type_name = col
|
|
48
|
+
.type_name
|
|
49
|
+
.as_deref()
|
|
50
|
+
.ok_or_else(|| ApiError::permanent(format!("column `{}` has no type_name in the manifest", col.name)))?;
|
|
54
51
|
let values = rows.iter().map(|row| row.get(col_idx).cloned().flatten());
|
|
55
52
|
let (data_type, array) = build_column(
|
|
56
53
|
&col.name,
|
|
@@ -65,11 +62,8 @@ pub fn json_array_to_record_batch(
|
|
|
65
62
|
}
|
|
66
63
|
|
|
67
64
|
let schema = Arc::new(Schema::new(fields));
|
|
68
|
-
RecordBatch::try_new(schema, arrays)
|
|
69
|
-
|
|
70
|
-
transient: false,
|
|
71
|
-
kind: ApiErrorKind::Other,
|
|
72
|
-
})
|
|
65
|
+
RecordBatch::try_new(schema, arrays)
|
|
66
|
+
.map_err(|e| ApiError::permanent(format!("failed to assemble RecordBatch from JSON_ARRAY data: {e}")))
|
|
73
67
|
}
|
|
74
68
|
|
|
75
69
|
#[allow(clippy::too_many_lines)]
|
|
@@ -153,11 +147,9 @@ fn build_column(
|
|
|
153
147
|
}
|
|
154
148
|
"DECIMAL" => {
|
|
155
149
|
let (Some(precision), Some(scale)) = (precision, scale) else {
|
|
156
|
-
return Err(ApiError
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
kind: ApiErrorKind::Other,
|
|
160
|
-
});
|
|
150
|
+
return Err(ApiError::permanent(format!(
|
|
151
|
+
"column `{name}`: DECIMAL type missing type_precision/type_scale in manifest"
|
|
152
|
+
)));
|
|
161
153
|
};
|
|
162
154
|
let mut out = Vec::new();
|
|
163
155
|
for v in values {
|
|
@@ -168,10 +160,10 @@ fn build_column(
|
|
|
168
160
|
}
|
|
169
161
|
let array = Decimal128Array::from(out)
|
|
170
162
|
.with_precision_and_scale(precision, scale)
|
|
171
|
-
.map_err(|e|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
163
|
+
.map_err(|e| {
|
|
164
|
+
ApiError::permanent(format!(
|
|
165
|
+
"column `{name}`: invalid DECIMAL(precision={precision}, scale={scale}): {e}"
|
|
166
|
+
))
|
|
175
167
|
})?;
|
|
176
168
|
Ok((DataType::Decimal128(precision, scale), Arc::new(array)))
|
|
177
169
|
}
|
|
@@ -259,15 +251,13 @@ fn build_column(
|
|
|
259
251
|
Ok((DataType::Binary, Arc::new(BinaryArray::from(refs))))
|
|
260
252
|
}
|
|
261
253
|
"STRUCT" => {
|
|
262
|
-
let type_text = type_text.ok_or_else(||
|
|
263
|
-
|
|
264
|
-
transient: false,
|
|
265
|
-
kind: ApiErrorKind::Other,
|
|
254
|
+
let type_text = type_text.ok_or_else(|| {
|
|
255
|
+
ApiError::permanent(format!("column `{name}`: STRUCT type missing type_text in manifest"))
|
|
266
256
|
})?;
|
|
267
|
-
let field_defs = parse_struct_fields(type_text).map_err(|e|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
257
|
+
let field_defs = parse_struct_fields(type_text).map_err(|e| {
|
|
258
|
+
ApiError::permanent(format!(
|
|
259
|
+
"column `{name}`: could not parse STRUCT type_text {type_text:?}: {e}"
|
|
260
|
+
))
|
|
271
261
|
})?;
|
|
272
262
|
|
|
273
263
|
// One parsed JSON object per row (`None` = the whole struct is
|
|
@@ -338,11 +328,9 @@ fn build_column(
|
|
|
338
328
|
.map_err(|e| ApiError::permanent(format!("column `{name}`: invalid STRUCT array: {e}")))?;
|
|
339
329
|
Ok((DataType::Struct(fields), Arc::new(struct_array)))
|
|
340
330
|
}
|
|
341
|
-
other => Err(ApiError
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
kind: ApiErrorKind::Other,
|
|
345
|
-
}),
|
|
331
|
+
other => Err(ApiError::permanent(format!(
|
|
332
|
+
"column `{name}`: type {other} isn't supported by the INLINE/JSON_ARRAY fast path"
|
|
333
|
+
))),
|
|
346
334
|
}
|
|
347
335
|
}
|
|
348
336
|
|
|
@@ -16,7 +16,7 @@ use arrow_array::types::{Float32Type, Float64Type};
|
|
|
16
16
|
use arrow_schema::DataType;
|
|
17
17
|
use serde_json::Value;
|
|
18
18
|
|
|
19
|
-
use crate::client::{ApiError,
|
|
19
|
+
use crate::client::{ApiError, CancelHandle, DbClient, Protocol, QueryStatsAccumulator, join_error};
|
|
20
20
|
|
|
21
21
|
use super::reorder::{ReorderBuffer, decode_chunk_item};
|
|
22
22
|
use super::sea::submit_sea_and_report;
|
|
@@ -83,16 +83,12 @@ fn encode_ndjson_lines(batches: &[RecordBatch], non_finite_as_string: bool) -> R
|
|
|
83
83
|
let builder = arrow_json::WriterBuilder::new().with_explicit_nulls(true);
|
|
84
84
|
let mut writer = builder.build::<_, arrow_json::writer::LineDelimited>(&mut output);
|
|
85
85
|
let refs: Vec<&RecordBatch> = batches.iter().collect();
|
|
86
|
-
writer
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
message: format!("NDJSON encode error: {e}"),
|
|
93
|
-
transient: false,
|
|
94
|
-
kind: ApiErrorKind::Other,
|
|
95
|
-
})?;
|
|
86
|
+
writer
|
|
87
|
+
.write_batches(&refs)
|
|
88
|
+
.map_err(|e| ApiError::permanent(format!("NDJSON encode error: {e}")))?;
|
|
89
|
+
writer
|
|
90
|
+
.finish()
|
|
91
|
+
.map_err(|e| ApiError::permanent(format!("NDJSON encode error: {e}")))?;
|
|
96
92
|
}
|
|
97
93
|
if !output.pending.is_empty() {
|
|
98
94
|
return Err(ApiError::permanent("NDJSON encode produced an unterminated row"));
|
|
@@ -407,23 +403,15 @@ mod tests {
|
|
|
407
403
|
let builder = arrow_json::WriterBuilder::new().with_explicit_nulls(true);
|
|
408
404
|
let mut writer = builder.build::<_, arrow_json::writer::LineDelimited>(&mut buf);
|
|
409
405
|
let refs: Vec<&RecordBatch> = batches.iter().collect();
|
|
410
|
-
writer
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
message: format!("NDJSON encode error: {e}"),
|
|
417
|
-
transient: false,
|
|
418
|
-
kind: ApiErrorKind::Other,
|
|
419
|
-
})?;
|
|
406
|
+
writer
|
|
407
|
+
.write_batches(&refs)
|
|
408
|
+
.map_err(|e| ApiError::permanent(format!("NDJSON encode error: {e}")))?;
|
|
409
|
+
writer
|
|
410
|
+
.finish()
|
|
411
|
+
.map_err(|e| ApiError::permanent(format!("NDJSON encode error: {e}")))?;
|
|
420
412
|
}
|
|
421
413
|
let mut lines: Vec<String> = String::from_utf8(buf)
|
|
422
|
-
.map_err(|e| ApiError {
|
|
423
|
-
message: format!("NDJSON encode produced invalid UTF-8: {e}"),
|
|
424
|
-
transient: false,
|
|
425
|
-
kind: ApiErrorKind::Other,
|
|
426
|
-
})?
|
|
414
|
+
.map_err(|e| ApiError::permanent(format!("NDJSON encode produced invalid UTF-8: {e}")))?
|
|
427
415
|
.lines()
|
|
428
416
|
.map(|line| line.to_string())
|
|
429
417
|
.collect();
|
|
@@ -15,7 +15,7 @@ use arrow_schema::SchemaRef;
|
|
|
15
15
|
use bytes::Bytes;
|
|
16
16
|
use tokio::sync::mpsc;
|
|
17
17
|
|
|
18
|
-
use crate::client::{ApiError,
|
|
18
|
+
use crate::client::{ApiError, ChunkItem};
|
|
19
19
|
|
|
20
20
|
/// Same dict-of-lists-keyed-by-index shape as `_ResultSet._pending`: a
|
|
21
21
|
/// `chunk_index` can carry more than one blob (multiple `external_links` per
|
|
@@ -123,11 +123,9 @@ fn decode_chunk(blob: &Bytes) -> Result<Vec<RecordBatch>, ApiError> {
|
|
|
123
123
|
/// schema-only streams that contain no record batches.
|
|
124
124
|
pub(crate) fn decode_ipc_stream(blob: &Bytes) -> Result<(Vec<RecordBatch>, SchemaRef), ApiError> {
|
|
125
125
|
if blob.is_empty() {
|
|
126
|
-
return Err(ApiError
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
kind: ApiErrorKind::Other,
|
|
130
|
-
});
|
|
126
|
+
return Err(ApiError::permanent(
|
|
127
|
+
"empty Arrow IPC chunk: expected at least a schema message",
|
|
128
|
+
));
|
|
131
129
|
}
|
|
132
130
|
let mut buffer = ArrowBuffer::from(blob.clone());
|
|
133
131
|
let mut decoder = StreamDecoder::new();
|
|
@@ -137,19 +135,13 @@ pub(crate) fn decode_ipc_stream(blob: &Bytes) -> Result<(Vec<RecordBatch>, Schem
|
|
|
137
135
|
Ok(Some(batch)) => batches.push(batch),
|
|
138
136
|
Ok(None) => {}
|
|
139
137
|
Err(e) => {
|
|
140
|
-
return Err(ApiError {
|
|
141
|
-
message: format!("Arrow IPC decode error: {e}"),
|
|
142
|
-
transient: false,
|
|
143
|
-
kind: ApiErrorKind::Other,
|
|
144
|
-
});
|
|
138
|
+
return Err(ApiError::permanent(format!("Arrow IPC decode error: {e}")));
|
|
145
139
|
}
|
|
146
140
|
}
|
|
147
141
|
}
|
|
148
|
-
decoder
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
kind: ApiErrorKind::Other,
|
|
152
|
-
})?;
|
|
142
|
+
decoder
|
|
143
|
+
.finish()
|
|
144
|
+
.map_err(|e| ApiError::permanent(format!("bad Arrow IPC stream: {e}")))?;
|
|
153
145
|
let schema = decoder
|
|
154
146
|
.schema()
|
|
155
147
|
.ok_or_else(|| ApiError::permanent("Arrow IPC stream has no schema"))?;
|
|
@@ -299,15 +291,7 @@ mod tests {
|
|
|
299
291
|
|
|
300
292
|
#[tokio::test]
|
|
301
293
|
async fn error_surfaces_after_already_yielded_items() {
|
|
302
|
-
let sent = vec![
|
|
303
|
-
Ok(item(0)),
|
|
304
|
-
Ok(item(1)),
|
|
305
|
-
Err(ApiError {
|
|
306
|
-
message: "boom".into(),
|
|
307
|
-
transient: false,
|
|
308
|
-
kind: ApiErrorKind::Other,
|
|
309
|
-
}),
|
|
310
|
-
];
|
|
294
|
+
let sent = vec![Ok(item(0)), Ok(item(1)), Err(ApiError::permanent("boom"))];
|
|
311
295
|
let (tx, rx) = mpsc::channel(sent.len());
|
|
312
296
|
for r in sent {
|
|
313
297
|
tx.send(r).await.unwrap();
|
|
@@ -17,8 +17,8 @@ use serde_json::Value;
|
|
|
17
17
|
use tokio::sync::mpsc;
|
|
18
18
|
|
|
19
19
|
use crate::client::{
|
|
20
|
-
ApiError,
|
|
21
|
-
|
|
20
|
+
ApiError, CancelHandle, ColumnDescription, DbClient, InlineOrExternal, QueryStatsAccumulator, QueryStatsData,
|
|
21
|
+
StatementSubmitResult, join_error,
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
use super::reorder::{ReorderBuffer, decode_chunk_item};
|
|
@@ -100,13 +100,10 @@ impl ResultStream {
|
|
|
100
100
|
/// single capped batch isn't enough).
|
|
101
101
|
async fn fetch_at_least(&mut self, want_rows: usize) -> Result<(), ApiError> {
|
|
102
102
|
if self.poisoned {
|
|
103
|
-
return Err(ApiError
|
|
104
|
-
|
|
105
|
-
re-run the query instead of continuing to use this cursor/result"
|
|
106
|
-
|
|
107
|
-
transient: false,
|
|
108
|
-
kind: ApiErrorKind::Other,
|
|
109
|
-
});
|
|
103
|
+
return Err(ApiError::permanent(
|
|
104
|
+
"this result was left incomplete by a previous cancelled, timed-out, or failed fetch -- \
|
|
105
|
+
re-run the query instead of continuing to use this cursor/result",
|
|
106
|
+
));
|
|
110
107
|
}
|
|
111
108
|
let mut guard = PoisonOnDrop::new(&mut self.poisoned, &mut self.reporter, self.stats.as_ref());
|
|
112
109
|
while self.pending_rows < want_rows && !self.exhausted {
|
|
@@ -21,6 +21,7 @@ use tokio::sync::mpsc;
|
|
|
21
21
|
|
|
22
22
|
use crate::client::{
|
|
23
23
|
ApiError, CancelHandle, ChunkItem, ColumnDescription, DbClient, QueryStatsAccumulator, join_error,
|
|
24
|
+
lz4_frame_decode_into,
|
|
24
25
|
};
|
|
25
26
|
use crate::thrift;
|
|
26
27
|
|
|
@@ -854,16 +855,7 @@ fn build_inline_blob(
|
|
|
854
855
|
for b in batches {
|
|
855
856
|
row_count += b.row_count;
|
|
856
857
|
if lz4_compressed {
|
|
857
|
-
|
|
858
|
-
// avoiding a separate intermediate allocation and copy. The
|
|
859
|
-
// FrameDecoder's read_to_end appends to the existing buffer.
|
|
860
|
-
use std::io::Read;
|
|
861
|
-
let mut decoder = lz4_flex::frame::FrameDecoder::new(&b.batch[..]);
|
|
862
|
-
while !decoder.get_ref().is_empty() {
|
|
863
|
-
decoder
|
|
864
|
-
.read_to_end(&mut out)
|
|
865
|
-
.map_err(|e| ApiError::permanent(format!("LZ4 frame decompress failed: {e}")))?;
|
|
866
|
-
}
|
|
858
|
+
lz4_frame_decode_into(&b.batch, &mut out)?;
|
|
867
859
|
} else {
|
|
868
860
|
out.extend_from_slice(&b.batch);
|
|
869
861
|
}
|
|
@@ -48,6 +48,11 @@ Description = tuple[str, str | None, None, None, None, None, None]
|
|
|
48
48
|
_ROW_BATCH = 1000
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def _join_rows(buffered: list[Row], rows: list[Row]) -> list[Row]:
|
|
52
|
+
# `buffered + rows` copies every row; usually there's nothing buffered.
|
|
53
|
+
return buffered + rows if buffered else rows
|
|
54
|
+
|
|
55
|
+
|
|
51
56
|
def _table_to_rows(table: Any) -> list[Row]:
|
|
52
57
|
if table.num_rows == 0:
|
|
53
58
|
return []
|
|
@@ -223,8 +228,7 @@ class Cursor:
|
|
|
223
228
|
if self._row_pos >= len(self._row_buffer):
|
|
224
229
|
result = self._require_result()
|
|
225
230
|
table = await result.fetchmany_arrow(_ROW_BATCH)
|
|
226
|
-
|
|
227
|
-
self._schema = await result.schema()
|
|
231
|
+
await self._ensure_schema(result)
|
|
228
232
|
self._row_buffer = _table_to_rows(table)
|
|
229
233
|
self._row_pos = 0
|
|
230
234
|
if self._row_pos >= len(self._row_buffer):
|
|
@@ -237,26 +241,28 @@ class Cursor:
|
|
|
237
241
|
buffered = self._take_buffered(size)
|
|
238
242
|
if len(buffered) == size:
|
|
239
243
|
return buffered
|
|
240
|
-
return buffered
|
|
244
|
+
return _join_rows(buffered, _table_to_rows(await self.fetchmany_arrow(size - len(buffered))))
|
|
241
245
|
|
|
242
246
|
async def fetchall(self) -> list[Row]:
|
|
243
247
|
buffered = self._take_buffered(len(self._row_buffer))
|
|
244
|
-
return buffered
|
|
248
|
+
return _join_rows(buffered, _table_to_rows(await self.fetchall_arrow()))
|
|
249
|
+
|
|
250
|
+
async def _ensure_schema(self, result: _core.ResultSet) -> None:
|
|
251
|
+
if self._schema is None:
|
|
252
|
+
self._schema = await result.schema()
|
|
245
253
|
|
|
246
254
|
async def fetchmany_arrow(self, size: int) -> core.Table:
|
|
247
255
|
self._require_empty_row_buffer("fetchmany_arrow")
|
|
248
256
|
result = self._require_result()
|
|
249
257
|
table = await result.fetchmany_arrow(size)
|
|
250
|
-
|
|
251
|
-
self._schema = await result.schema()
|
|
258
|
+
await self._ensure_schema(result)
|
|
252
259
|
return table
|
|
253
260
|
|
|
254
261
|
async def fetchall_arrow(self) -> core.Table:
|
|
255
262
|
self._require_empty_row_buffer("fetchall_arrow")
|
|
256
263
|
result = self._require_result()
|
|
257
264
|
table = await result.fetchall_arrow()
|
|
258
|
-
|
|
259
|
-
self._schema = await result.schema()
|
|
265
|
+
await self._ensure_schema(result)
|
|
260
266
|
return table
|
|
261
267
|
|
|
262
268
|
def fetchall_streamed(self, *, total_timeout_s: float | None = None) -> AsyncIterator[Any]:
|
|
@@ -317,8 +323,7 @@ class Cursor:
|
|
|
317
323
|
if item is _core.HEARTBEAT:
|
|
318
324
|
yield HEARTBEAT
|
|
319
325
|
else:
|
|
320
|
-
|
|
321
|
-
self._schema = await result.schema()
|
|
326
|
+
await self._ensure_schema(result)
|
|
322
327
|
yield item
|
|
323
328
|
|
|
324
329
|
return _gen()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/proptest-regressions/json_convert.txt
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests/wiremock_volume_files.rs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{arrowbricks-5.0.2 → arrowbricks-5.0.3}/rust/arrowbricks_core/tests_py/test_stream_ndjson_lines.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{arrowbricks-5.0.2 → arrowbricks-5.0.3}/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
|