arrowbricks 3.1.4__tar.gz → 4.0.0__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 (50) hide show
  1. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/PKG-INFO +12 -3
  2. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/README.md +11 -2
  3. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/pyproject.toml +3 -7
  4. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/Cargo.lock +3 -136
  5. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/Cargo.toml +9 -19
  6. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/README.md +5 -6
  7. arrowbricks-4.0.0/rust/arrowbricks_core/src/arrow_ffi.rs +106 -0
  8. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/client/model.rs +25 -1
  9. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/client/sea.rs +40 -3
  10. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/client.rs +23 -9
  11. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/heartbeat.rs +12 -0
  12. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/lib.rs +68 -34
  13. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/pipeline/ndjson.rs +23 -5
  14. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/pipeline/sea.rs +9 -2
  15. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/pipeline/stats.rs +24 -0
  16. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/pipeline/thrift_exec.rs +47 -3
  17. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests/wiremock_pipeline.rs +140 -0
  18. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests/wiremock_thrift.rs +242 -0
  19. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests_py/test_ipc_stream.py +38 -0
  20. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests_py/test_thrift.py +5 -4
  21. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/src/arrowbricks/_core.pyi +16 -4
  22. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/src/arrowbricks/_streaming.py +36 -14
  23. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/src/arrowbricks/cursor.py +5 -17
  24. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/LICENSE +0 -0
  25. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/.gitignore +0 -0
  26. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/examples/duckdb_query.py +0 -0
  27. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/examples/fastapi_sse.py +0 -0
  28. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/proptest-regressions/json_convert.txt +0 -0
  29. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/rustfmt.toml +0 -0
  30. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/client/download.rs +0 -0
  31. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/client/error.rs +0 -0
  32. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/client/thrift_rpc.rs +0 -0
  33. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/client/volume.rs +0 -0
  34. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/json_convert.rs +0 -0
  35. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/pipeline/reorder.rs +0 -0
  36. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/pipeline/test_support.rs +0 -0
  37. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/pipeline.rs +0 -0
  38. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/src/thrift.rs +0 -0
  39. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests/common/mod.rs +0 -0
  40. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests/wiremock_volume_files.rs +0 -0
  41. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests_py/conftest.py +0 -0
  42. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests_py/test_parameters.py +0 -0
  43. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests_py/test_stream_ndjson_lines.py +0 -0
  44. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests_py/test_streaming.py +0 -0
  45. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests_py/test_token_provider.py +0 -0
  46. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests_py/test_volume_files.py +0 -0
  47. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/rust/arrowbricks_core/tests_py/thrift_mock.py +0 -0
  48. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/src/arrowbricks/__init__.py +0 -0
  49. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/src/arrowbricks/client.py +0 -0
  50. {arrowbricks-3.1.4 → arrowbricks-4.0.0}/src/arrowbricks/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrowbricks
3
- Version: 3.1.4
3
+ Version: 4.0.0
4
4
  Requires-Dist: arro3-core>=0.8 ; extra == 'arro3'
5
5
  Provides-Extra: arro3
6
6
  License-File: LICENSE
@@ -160,9 +160,9 @@ conn = connect(host=..., warehouse_id=..., token=..., on_event=log_query)
160
160
 
161
161
  ## Cancellation
162
162
 
163
- When a chunk download times out (`total_timeout_s`) or your code cancels the surrounding coroutine (`task.cancel()`/`asyncio.wait_for`) while it's in flight, arrowbricks fires a best-effort server-side cancel in the background (Thrift's `CancelOperation`, or SEA's `POST .../cancel`) so Databricks stops running the query instead of finishing it for nobody. This is fire-and-forget: the `QueryTimeout`/cancellation still reaches you immediately, and the cancel call's own result (success or failure) is never surfaced or awaited.
163
+ When `total_timeout_s` elapses or your code cancels the surrounding coroutine (`task.cancel()`/`asyncio.wait_for`) while a query is in flight, arrowbricks fires a best-effort server-side cancel in the background (Thrift's `CancelOperation`, or SEA's `POST .../cancel`) so Databricks stops running the query instead of finishing it for nobody. This is fire-and-forget: the `QueryTimeout`/cancellation still reaches you immediately, and the cancel call's own result (success or failure) is never surfaced or awaited.
164
164
 
165
- This covers `Cursor.fetchall_streamed()`/`fetchall_arrow_streamed()`, `client.stream_query_json(...)`, and the lower-level `._core.Client`'s own streamed APIs -- everything that wraps the chunk-download phase in the Rust-level heartbeat. Neither `total_timeout_s` nor a bare `task.cancel()` on `Cursor.execute()`/`execute_streamed()` itself (the initial submit/poll wait) triggers a cancel -- by the time a query's chunks are being fetched at all, the statement has typically already finished running server-side, so there's usually nothing left to cancel there in practice.
165
+ This covers both phases of a query: the submit/poll wait while the statement is still running (`Cursor.execute()`/`execute_streamed()`, and the start of `client.stream_query_json(...)`), and the chunk download afterwards (`Cursor.fetchall_streamed()`/`fetchall_arrow_streamed()`, the rest of `stream_query_json`). `stream_query_json`'s `total_timeout_s` is one budget across both phases. On `protocol="sea"` there is one short blind spot: the submit request itself can wait server-side for up to `wait_timeout` (default 30s) before Databricks returns a statement id, and a query abandoned inside that window can't be cancelled.
166
166
 
167
167
  ## Errors
168
168
 
@@ -212,6 +212,15 @@ except ArrowbricksError:
212
212
 
213
213
  ## Arrow vs. row-tuple fetches
214
214
 
215
+ From 4.0, Arrow fetches and `ReplayableArrowChunk.to_table()` return
216
+ `arrowbricks._core.Table`. It exposes `num_rows`, `num_columns`,
217
+ `column_names`, `len(table)` and the Arrow capsule interfaces. For methods
218
+ such as `.column()`, `.schema` or `.to_batches()`, first convert with
219
+ `arro3.core.Table.from_arrow(table)`, `pyarrow.table(table)` or
220
+ `polars.from_arrow(table)`. DuckDB can consume it directly.
221
+ `write_ipc_stream` requires `__arrow_c_stream__`; wrap an array-only producer
222
+ in a table before passing it in.
223
+
215
224
  Everything above works with zero dependencies installed *except* row-tuple fetches. `fetchall_arrow`/`fetchmany_arrow` return an Arrow table straight from the Rust core -- the faster path if your code can consume Arrow directly (DuckDB, pyarrow, polars, a Parquet writer, ...):
216
225
 
217
226
  ```python
@@ -148,9 +148,9 @@ conn = connect(host=..., warehouse_id=..., token=..., on_event=log_query)
148
148
 
149
149
  ## Cancellation
150
150
 
151
- When a chunk download times out (`total_timeout_s`) or your code cancels the surrounding coroutine (`task.cancel()`/`asyncio.wait_for`) while it's in flight, arrowbricks fires a best-effort server-side cancel in the background (Thrift's `CancelOperation`, or SEA's `POST .../cancel`) so Databricks stops running the query instead of finishing it for nobody. This is fire-and-forget: the `QueryTimeout`/cancellation still reaches you immediately, and the cancel call's own result (success or failure) is never surfaced or awaited.
151
+ When `total_timeout_s` elapses or your code cancels the surrounding coroutine (`task.cancel()`/`asyncio.wait_for`) while a query is in flight, arrowbricks fires a best-effort server-side cancel in the background (Thrift's `CancelOperation`, or SEA's `POST .../cancel`) so Databricks stops running the query instead of finishing it for nobody. This is fire-and-forget: the `QueryTimeout`/cancellation still reaches you immediately, and the cancel call's own result (success or failure) is never surfaced or awaited.
152
152
 
153
- This covers `Cursor.fetchall_streamed()`/`fetchall_arrow_streamed()`, `client.stream_query_json(...)`, and the lower-level `._core.Client`'s own streamed APIs -- everything that wraps the chunk-download phase in the Rust-level heartbeat. Neither `total_timeout_s` nor a bare `task.cancel()` on `Cursor.execute()`/`execute_streamed()` itself (the initial submit/poll wait) triggers a cancel -- by the time a query's chunks are being fetched at all, the statement has typically already finished running server-side, so there's usually nothing left to cancel there in practice.
153
+ This covers both phases of a query: the submit/poll wait while the statement is still running (`Cursor.execute()`/`execute_streamed()`, and the start of `client.stream_query_json(...)`), and the chunk download afterwards (`Cursor.fetchall_streamed()`/`fetchall_arrow_streamed()`, the rest of `stream_query_json`). `stream_query_json`'s `total_timeout_s` is one budget across both phases. On `protocol="sea"` there is one short blind spot: the submit request itself can wait server-side for up to `wait_timeout` (default 30s) before Databricks returns a statement id, and a query abandoned inside that window can't be cancelled.
154
154
 
155
155
  ## Errors
156
156
 
@@ -200,6 +200,15 @@ except ArrowbricksError:
200
200
 
201
201
  ## Arrow vs. row-tuple fetches
202
202
 
203
+ From 4.0, Arrow fetches and `ReplayableArrowChunk.to_table()` return
204
+ `arrowbricks._core.Table`. It exposes `num_rows`, `num_columns`,
205
+ `column_names`, `len(table)` and the Arrow capsule interfaces. For methods
206
+ such as `.column()`, `.schema` or `.to_batches()`, first convert with
207
+ `arro3.core.Table.from_arrow(table)`, `pyarrow.table(table)` or
208
+ `polars.from_arrow(table)`. DuckDB can consume it directly.
209
+ `write_ipc_stream` requires `__arrow_c_stream__`; wrap an array-only producer
210
+ in a table before passing it in.
211
+
203
212
  Everything above works with zero dependencies installed *except* row-tuple fetches. `fetchall_arrow`/`fetchmany_arrow` return an Arrow table straight from the Rust core -- the faster path if your code can consume Arrow directly (DuckDB, pyarrow, polars, a Parquet writer, ...):
204
213
 
205
214
  ```python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arrowbricks"
3
- version = "3.1.4"
3
+ version = "4.0.0"
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"
@@ -15,12 +15,8 @@ requires-python = ">=3.11"
15
15
  dependencies = []
16
16
 
17
17
  [project.optional-dependencies]
18
- # pyo3-arrow's own Table.column()/schema methods are designed to hand back
19
- # the caller's *real* arro3.core objects for further arro3-ecosystem
20
- # interop (not a value pyo3-arrow can materialize itself), so
21
- # fetchone/fetchmany/fetchall need this installed. `pip install
22
- # arrowbricks[arro3]` -- or handle rows as Arrow via fetchall_arrow/
23
- # fetchmany_arrow instead, which don't need it at all.
18
+ # Row materialization converts our capsule table through arro3-core.
19
+ # Arrow fetches and streaming do not need this optional dependency.
24
20
  arro3 = ["arro3-core>=0.8"]
25
21
 
26
22
  [project.urls]
@@ -81,7 +81,6 @@ dependencies = [
81
81
  "atoi",
82
82
  "base64 0.23.1",
83
83
  "chrono",
84
- "comfy-table",
85
84
  "half",
86
85
  "lexical-core",
87
86
  "num-traits",
@@ -178,7 +177,7 @@ dependencies = [
178
177
 
179
178
  [[package]]
180
179
  name = "arrowbricks_core"
181
- version = "3.1.4"
180
+ version = "4.0.0"
182
181
  dependencies = [
183
182
  "arrow-array",
184
183
  "arrow-buffer",
@@ -193,7 +192,6 @@ dependencies = [
193
192
  "memchr",
194
193
  "proptest",
195
194
  "pyo3",
196
- "pyo3-arrow",
197
195
  "pyo3-async-runtimes",
198
196
  "pythonize",
199
197
  "reqwest",
@@ -303,9 +301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
303
301
  checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
304
302
  dependencies = [
305
303
  "iana-time-zone",
306
- "js-sys",
307
304
  "num-traits",
308
- "wasm-bindgen",
309
305
  "windows-link",
310
306
  ]
311
307
 
@@ -329,16 +325,6 @@ dependencies = [
329
325
  "memchr",
330
326
  ]
331
327
 
332
- [[package]]
333
- name = "comfy-table"
334
- version = "7.2.2"
335
- source = "registry+https://github.com/rust-lang/crates.io-index"
336
- checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47"
337
- dependencies = [
338
- "unicode-segmentation",
339
- "unicode-width",
340
- ]
341
-
342
328
  [[package]]
343
329
  name = "const-random"
344
330
  version = "0.1.18"
@@ -889,7 +875,7 @@ dependencies = [
889
875
  "jni-sys",
890
876
  "log",
891
877
  "simd_cesu8",
892
- "thiserror 2.0.20",
878
+ "thiserror",
893
879
  "walkdir",
894
880
  "windows-link",
895
881
  ]
@@ -1039,16 +1025,6 @@ dependencies = [
1039
1025
  "twox-hash",
1040
1026
  ]
1041
1027
 
1042
- [[package]]
1043
- name = "matrixmultiply"
1044
- version = "0.3.11"
1045
- source = "registry+https://github.com/rust-lang/crates.io-index"
1046
- checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
1047
- dependencies = [
1048
- "autocfg",
1049
- "rawpointer",
1050
- ]
1051
-
1052
1028
  [[package]]
1053
1029
  name = "memchr"
1054
1030
  version = "2.8.3"
@@ -1066,21 +1042,6 @@ dependencies = [
1066
1042
  "windows-sys 0.61.2",
1067
1043
  ]
1068
1044
 
1069
- [[package]]
1070
- name = "ndarray"
1071
- version = "0.17.2"
1072
- source = "registry+https://github.com/rust-lang/crates.io-index"
1073
- checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
1074
- dependencies = [
1075
- "matrixmultiply",
1076
- "num-complex",
1077
- "num-integer",
1078
- "num-traits",
1079
- "portable-atomic",
1080
- "portable-atomic-util",
1081
- "rawpointer",
1082
- ]
1083
-
1084
1045
  [[package]]
1085
1046
  name = "num-bigint"
1086
1047
  version = "0.5.1"
@@ -1129,23 +1090,6 @@ dependencies = [
1129
1090
  "libc",
1130
1091
  ]
1131
1092
 
1132
- [[package]]
1133
- name = "numpy"
1134
- version = "0.29.0"
1135
- source = "registry+https://github.com/rust-lang/crates.io-index"
1136
- checksum = "6a5b15d63a5ff39e378daed0e1340d3a5964703ea9712eb09a0dc66fade996f4"
1137
- dependencies = [
1138
- "half",
1139
- "libc",
1140
- "ndarray",
1141
- "num-complex",
1142
- "num-integer",
1143
- "num-traits",
1144
- "pyo3",
1145
- "pyo3-build-config",
1146
- "rustc-hash",
1147
- ]
1148
-
1149
1093
  [[package]]
1150
1094
  name = "once_cell"
1151
1095
  version = "1.21.4"
@@ -1194,15 +1138,6 @@ version = "1.14.0"
1194
1138
  source = "registry+https://github.com/rust-lang/crates.io-index"
1195
1139
  checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
1196
1140
 
1197
- [[package]]
1198
- name = "portable-atomic-util"
1199
- version = "0.2.7"
1200
- source = "registry+https://github.com/rust-lang/crates.io-index"
1201
- checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
1202
- dependencies = [
1203
- "portable-atomic",
1204
- ]
1205
-
1206
1141
  [[package]]
1207
1142
  name = "potential_utf"
1208
1143
  version = "0.1.5"
@@ -1255,9 +1190,6 @@ version = "0.29.2"
1255
1190
  source = "registry+https://github.com/rust-lang/crates.io-index"
1256
1191
  checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
1257
1192
  dependencies = [
1258
- "chrono",
1259
- "chrono-tz",
1260
- "indexmap",
1261
1193
  "libc",
1262
1194
  "once_cell",
1263
1195
  "portable-atomic",
@@ -1266,27 +1198,6 @@ dependencies = [
1266
1198
  "pyo3-macros",
1267
1199
  ]
1268
1200
 
1269
- [[package]]
1270
- name = "pyo3-arrow"
1271
- version = "0.19.0"
1272
- source = "registry+https://github.com/rust-lang/crates.io-index"
1273
- checksum = "3d5ddf226a2dbf7607570d0657c2bf6fbe299208368b2914f3dd7e7ba0b57688"
1274
- dependencies = [
1275
- "arrow-array",
1276
- "arrow-buffer",
1277
- "arrow-cast",
1278
- "arrow-data",
1279
- "arrow-schema",
1280
- "arrow-select",
1281
- "chrono",
1282
- "chrono-tz",
1283
- "half",
1284
- "indexmap",
1285
- "numpy",
1286
- "pyo3",
1287
- "thiserror 1.0.69",
1288
- ]
1289
-
1290
1201
  [[package]]
1291
1202
  name = "pyo3-async-runtimes"
1292
1203
  version = "0.29.0"
@@ -1413,12 +1324,6 @@ dependencies = [
1413
1324
  "rand_core",
1414
1325
  ]
1415
1326
 
1416
- [[package]]
1417
- name = "rawpointer"
1418
- version = "0.2.1"
1419
- source = "registry+https://github.com/rust-lang/crates.io-index"
1420
- checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
1421
-
1422
1327
  [[package]]
1423
1328
  name = "regex"
1424
1329
  version = "1.13.1"
@@ -1499,12 +1404,6 @@ dependencies = [
1499
1404
  "windows-sys 0.52.0",
1500
1405
  ]
1501
1406
 
1502
- [[package]]
1503
- name = "rustc-hash"
1504
- version = "2.1.3"
1505
- source = "registry+https://github.com/rust-lang/crates.io-index"
1506
- checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
1507
-
1508
1407
  [[package]]
1509
1408
  name = "rustc_version"
1510
1409
  version = "0.4.1"
@@ -1850,33 +1749,13 @@ dependencies = [
1850
1749
  "windows-sys 0.61.2",
1851
1750
  ]
1852
1751
 
1853
- [[package]]
1854
- name = "thiserror"
1855
- version = "1.0.69"
1856
- source = "registry+https://github.com/rust-lang/crates.io-index"
1857
- checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
1858
- dependencies = [
1859
- "thiserror-impl 1.0.69",
1860
- ]
1861
-
1862
1752
  [[package]]
1863
1753
  name = "thiserror"
1864
1754
  version = "2.0.20"
1865
1755
  source = "registry+https://github.com/rust-lang/crates.io-index"
1866
1756
  checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
1867
1757
  dependencies = [
1868
- "thiserror-impl 2.0.20",
1869
- ]
1870
-
1871
- [[package]]
1872
- name = "thiserror-impl"
1873
- version = "1.0.69"
1874
- source = "registry+https://github.com/rust-lang/crates.io-index"
1875
- checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
1876
- dependencies = [
1877
- "proc-macro2",
1878
- "quote",
1879
- "syn 2.0.119",
1758
+ "thiserror-impl",
1880
1759
  ]
1881
1760
 
1882
1761
  [[package]]
@@ -2047,18 +1926,6 @@ version = "1.0.24"
2047
1926
  source = "registry+https://github.com/rust-lang/crates.io-index"
2048
1927
  checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
2049
1928
 
2050
- [[package]]
2051
- name = "unicode-segmentation"
2052
- version = "1.13.3"
2053
- source = "registry+https://github.com/rust-lang/crates.io-index"
2054
- checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
2055
-
2056
- [[package]]
2057
- name = "unicode-width"
2058
- version = "0.2.2"
2059
- source = "registry+https://github.com/rust-lang/crates.io-index"
2060
- checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
2061
-
2062
1929
  [[package]]
2063
1930
  name = "untrusted"
2064
1931
  version = "0.9.0"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "arrowbricks_core"
3
- version = "3.1.4"
3
+ version = "4.0.0"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -11,7 +11,10 @@ crate-type = ["cdylib", "rlib"]
11
11
  [dependencies]
12
12
  # Depend on the components we use. The umbrella `arrow` crate also builds
13
13
  # arithmetic, string, and row kernels even with default-features disabled.
14
- arrow-array = "59.1.0"
14
+ # `ffi` backs src/arrow_ffi.rs, this crate's own Arrow PyCapsule bridge.
15
+ # `chrono-tz` lets arrow-json format Databricks' named-zone timestamps
16
+ # ("Etc/UTC"); pyo3-arrow used to enable it implicitly.
17
+ arrow-array = { version = "59.1.0", features = ["ffi", "chrono-tz"] }
15
18
  arrow-buffer = "59.1.0"
16
19
  arrow-ipc = "59.1.0"
17
20
  arrow-schema = "59.1.0"
@@ -20,15 +23,14 @@ bytes = "1.12.1"
20
23
  # Already in the dependency graph; vectorized newline search in NDJSON output.
21
24
  memchr = "2"
22
25
  # Already an unconditional transitive dependency of arrow-cast (pulled in by
23
- # pyo3-arrow) at this exact version -- declared directly here too so
26
+ # arrow-json) at this exact version -- declared directly here too so
24
27
  # json_convert.rs can decode BINARY columns without hand-rolling base64. No
25
28
  # new code ships that wasn't already linked in (feature unification, same
26
29
  # reasoning as chrono/rustls below).
27
30
  base64 = "0.23"
28
- # Already an unconditional transitive dependency of pyo3-arrow (see its own
29
- # comment below) -- declared directly here too so json_convert.rs can parse
30
- # DATE/TIMESTAMP/TIMESTAMP_NTZ strings without hand-rolling calendar
31
- # arithmetic. No new code ships that wasn't already linked in.
31
+ # Already an unconditional transitive dependency of arrow-array -- declared
32
+ # directly here too so json_convert.rs can parse DATE/TIMESTAMP/TIMESTAMP_NTZ
33
+ # strings without hand-rolling calendar arithmetic. No new code ships that wasn't already linked in.
32
34
  chrono = { version = "0.4", default-features = false, features = ["std"] }
33
35
  # Decompresses cloud-fetch chunk bytes when the server honors our
34
36
  # `result_compression: LZ4_FRAME` request (see client.rs's execute_statement)
@@ -36,12 +38,6 @@ chrono = { version = "0.4", default-features = false, features = ["std"] }
36
38
  # Pure Rust, no C toolchain dependency -- unlike reqwest's TLS stack below.
37
39
  lz4_flex = { version = "0.14", default-features = false, features = ["frame", "std"] }
38
40
  pyo3 = { version = "0.29.1", features = ["abi3-py311"] }
39
- # pyo3-arrow's only optional feature is buffer_protocol (on by default),
40
- # which we don't use -- numpy/chrono/chrono-tz/arrow-cast(prettyprint) below
41
- # it are unconditional dependencies of the crate itself, not features, so
42
- # they ship regardless; not worth dropping pyo3-arrow over that (reimplementing
43
- # the Arrow C Data Interface ourselves is a much bigger risk than the MBs saved).
44
- pyo3-arrow = { version = "0.19.0", default-features = false }
45
41
  pyo3-async-runtimes = { version = "0.29.0", features = ["tokio-runtime"] }
46
42
  pythonize = "0.29.0"
47
43
  # reqwest's own `rustls` feature is hardcoded to `__rustls-aws-lc-rs`, no way
@@ -135,9 +131,3 @@ opt-level = "s"
135
131
  strip = true
136
132
  lto = true
137
133
  codegen-units = 1
138
-
139
- [profile.release.package.pyo3-arrow]
140
- # This bridge is dominated by interoperability helpers. Optimizing it for
141
- # size saves ~32 KiB on macOS arm64; the IPC decoder keeps opt-level="s".
142
- # Replay and live-query benchmarks are recorded in benchmarks/2026-09-06.md.
143
- opt-level = "z"
@@ -63,12 +63,11 @@ asyncio.run(main())
63
63
  ```
64
64
 
65
65
  `table` implements the [Arrow C Data Interface](https://arrow.apache.org/docs/format/CDataInterface.html)
66
- (`__arrow_c_stream__`/`__arrow_c_array__`) -- any consumer that speaks that
67
- protocol (DuckDB, pyarrow, arro3) can import it with zero copies, no extra
68
- dependency needed. Note: `table.column(i)`/`table.schema` (materializing
69
- native Python values) are `pyo3-arrow` methods designed to hand back the
70
- caller's own *real* `arro3.core` objects -- they need `arro3-core` installed,
71
- unlike the table object itself.
66
+ (`__arrow_c_stream__`/`__arrow_c_schema__`) -- any consumer that speaks that
67
+ protocol (DuckDB, pyarrow, polars, arro3) can import it with zero copies, no
68
+ extra dependency needed. The table itself only exposes `num_rows`,
69
+ `num_columns`, `column_names` and `len()`; to read values, import it into an
70
+ Arrow library first (e.g. `arro3.core.Table.from_arrow(table)`).
72
71
 
73
72
  ## API
74
73
 
@@ -0,0 +1,106 @@
1
+ //! Arrow PyCapsule interface, both directions, on arrow-rs's own
2
+ //! `ffi_stream` -- replaces `pyo3-arrow`, whose `Table.__arrow_c_stream__`
3
+ //! honors `requested_schema` via `arrow_cast::cast` and so kept the whole cast
4
+ //! kernel (~1.8 MiB, a third of `.text`) linked in for a feature no caller of
5
+ //! this crate used.
6
+
7
+ use std::ffi::CStr;
8
+
9
+ use arrow_array::ffi::FFI_ArrowSchema;
10
+ use arrow_array::ffi_stream::{ArrowArrayStreamReader, FFI_ArrowArrayStream};
11
+ use arrow_array::{RecordBatch, RecordBatchIterator};
12
+ use arrow_schema::SchemaRef;
13
+ use pyo3::exceptions::PyRuntimeError;
14
+ use pyo3::prelude::*;
15
+ use pyo3::types::{PyCapsule, PyCapsuleMethods};
16
+
17
+ const STREAM_CAPSULE: &CStr = c"arrow_array_stream";
18
+ const SCHEMA_CAPSULE: &CStr = c"arrow_schema";
19
+
20
+ /// An in-memory Arrow table. Consume it through `__arrow_c_stream__`
21
+ /// (arro3, pyarrow, polars, DuckDB, ...); the attributes are just enough to
22
+ /// inspect a result without importing another Arrow library.
23
+ #[pyclass(name = "Table", module = "arrowbricks._core", frozen)]
24
+ pub struct PyTable {
25
+ batches: Vec<RecordBatch>,
26
+ schema: SchemaRef,
27
+ }
28
+
29
+ impl PyTable {
30
+ pub fn try_new(batches: Vec<RecordBatch>, schema: SchemaRef) -> PyResult<Self> {
31
+ // A stream advertises one schema for every batch. Exporting different
32
+ // buffer layouts under it can make consumers misinterpret memory.
33
+ // Like the previous bridge, ignore metadata and nullability differences.
34
+ if batches.iter().any(|batch| {
35
+ let fields = batch.schema_ref().fields();
36
+ fields.len() != schema.fields().len()
37
+ || fields.iter().zip(schema.fields()).any(|(actual, expected)| {
38
+ actual.name() != expected.name() || !actual.data_type().equals_datatype(expected.data_type())
39
+ })
40
+ }) {
41
+ return Err(PyRuntimeError::new_err("All batches must have same schema"));
42
+ }
43
+ Ok(Self { batches, schema })
44
+ }
45
+ }
46
+
47
+ #[pymethods]
48
+ impl PyTable {
49
+ #[getter]
50
+ fn num_rows(&self) -> usize {
51
+ self.batches.iter().map(RecordBatch::num_rows).sum()
52
+ }
53
+
54
+ #[getter]
55
+ fn num_columns(&self) -> usize {
56
+ self.schema.fields().len()
57
+ }
58
+
59
+ #[getter]
60
+ fn column_names(&self) -> Vec<String> {
61
+ self.schema.fields().iter().map(|f| f.name().clone()).collect()
62
+ }
63
+
64
+ fn __len__(&self) -> usize {
65
+ self.num_rows()
66
+ }
67
+
68
+ fn __repr__(&self) -> String {
69
+ format!(
70
+ "arrowbricks.Table(num_rows={}, num_columns={})",
71
+ self.num_rows(),
72
+ self.num_columns()
73
+ )
74
+ }
75
+
76
+ /// `requested_schema` is ignored, as the PyCapsule interface allows: the
77
+ /// consumer checks the schema it actually gets.
78
+ #[pyo3(signature = (requested_schema=None))]
79
+ fn __arrow_c_stream__<'py>(
80
+ &self,
81
+ py: Python<'py>,
82
+ requested_schema: Option<Bound<'py, PyAny>>,
83
+ ) -> PyResult<Bound<'py, PyCapsule>> {
84
+ let _ = requested_schema;
85
+ let reader = RecordBatchIterator::new(self.batches.clone().into_iter().map(Ok), self.schema.clone());
86
+ PyCapsule::new_with_value(py, FFI_ArrowArrayStream::new(Box::new(reader)), STREAM_CAPSULE)
87
+ }
88
+
89
+ fn __arrow_c_schema__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyCapsule>> {
90
+ let schema =
91
+ FFI_ArrowSchema::try_from(self.schema.as_ref()).map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
92
+ PyCapsule::new_with_value(py, schema, SCHEMA_CAPSULE)
93
+ }
94
+ }
95
+
96
+ /// Imports any object implementing `__arrow_c_stream__`.
97
+ pub fn import_stream(obj: &Bound<'_, PyAny>) -> PyResult<ArrowArrayStreamReader> {
98
+ let capsule = obj.call_method0("__arrow_c_stream__")?.cast_into::<PyCapsule>()?;
99
+ let ptr = capsule
100
+ .pointer_checked(Some(STREAM_CAPSULE))?
101
+ .cast::<FFI_ArrowArrayStream>();
102
+ // SAFETY: the capsule name check guarantees an `ArrowArrayStream`;
103
+ // `from_raw` moves it out and marks the capsule's copy released, so the
104
+ // capsule destructor does not release it a second time.
105
+ unsafe { ArrowArrayStreamReader::from_raw(ptr.as_ptr()) }.map_err(|e| PyRuntimeError::new_err(e.to_string()))
106
+ }
@@ -6,6 +6,7 @@
6
6
  //! statement gets you before any chunk is fetched (`ChunkMeta`,
7
7
  //! `StatementSubmitResult`, `InlineOrExternal`, `ChunkItem`).
8
8
 
9
+ use std::sync::Mutex;
9
10
  use std::sync::atomic::{AtomicU8, AtomicU32, AtomicU64, AtomicUsize, Ordering};
10
11
 
11
12
  use bytes::Bytes;
@@ -55,7 +56,7 @@ pub struct ColumnDescription {
55
56
  /// `Drop for HeartbeatWait`/`Drop for HeartbeatStream`) can fire
56
57
  /// `DbClient::cancel_statement` without either of those generic structs
57
58
  /// needing to know anything protocol-specific themselves.
58
- #[derive(Clone)]
59
+ #[derive(Clone, Debug)]
59
60
  pub enum CancelHandle {
60
61
  Sea { statement_id: String },
61
62
  Thrift { operation: thrift::OperationHandle },
@@ -113,6 +114,17 @@ pub struct QueryStatsAccumulator {
113
114
  /// anything itself.
114
115
  warehouse_wait_bits: AtomicU64,
115
116
  outcome: AtomicU8,
117
+ /// The statement/operation handle while it's submitted but not yet
118
+ /// terminal -- set by the submit/poll loops (`client/sea.rs`'s
119
+ /// `submit_and_poll_inner`, `pipeline/thrift_exec.rs`'s
120
+ /// `submit_and_await_thrift_statement`) as soon as Databricks hands one
121
+ /// back, cleared once the statement reaches a terminal state. Whatever
122
+ /// is still here when `pipeline/stats.rs`'s `CancelInFlightOnDrop`
123
+ /// drops (the submit future was abandoned -- a `total_timeout_s`, a
124
+ /// Python-side `task.cancel()`/`asyncio.wait_for` -- or a poll failed
125
+ /// mid-wait) gets a best-effort server-side cancel, so a query nobody
126
+ /// is waiting for anymore stops running on the warehouse.
127
+ in_flight: Mutex<Option<CancelHandle>>,
116
128
  }
117
129
 
118
130
  const OUTCOME_UNSET: u8 = 0;
@@ -170,6 +182,18 @@ impl QueryStatsAccumulator {
170
182
  pub fn warehouse_wait_s(&self) -> f64 {
171
183
  f64::from_bits(self.warehouse_wait_bits.load(Ordering::Relaxed))
172
184
  }
185
+
186
+ pub fn set_in_flight(&self, handle: CancelHandle) {
187
+ *self.in_flight.lock().unwrap() = Some(handle);
188
+ }
189
+
190
+ pub fn clear_in_flight(&self) {
191
+ *self.in_flight.lock().unwrap() = None;
192
+ }
193
+
194
+ pub fn take_in_flight(&self) -> Option<CancelHandle> {
195
+ self.in_flight.lock().unwrap().take()
196
+ }
173
197
  }
174
198
 
175
199
  /// One query's worth of timing/counters, handed to `EventSink::on_event`
@@ -21,7 +21,8 @@ use super::DbClient;
21
21
  use super::POLL_INTERVAL;
22
22
  use super::error::{ApiError, join_error};
23
23
  use super::model::{
24
- ChunkItem, ChunkMeta, ColumnDescription, InlineOrExternal, QueryStatsAccumulator, StatementSubmitResult,
24
+ CancelHandle, ChunkItem, ChunkMeta, ColumnDescription, InlineOrExternal, QueryStatsAccumulator,
25
+ StatementSubmitResult,
25
26
  };
26
27
 
27
28
  /// Typed response shapes -- replaces navigating a dynamic `serde_json::Value`
@@ -373,10 +374,17 @@ impl DbClient {
373
374
  }
374
375
  }
375
376
 
377
+ let mut checkin = SessionCheckin {
378
+ client: self,
379
+ catalog,
380
+ schema,
381
+ session_id,
382
+ };
376
383
  let result = self.submit_and_poll_inner(body, stats).await;
377
- if let Some(id) = session_id {
378
- self.checkin_session(catalog, schema, id, result.is_ok());
384
+ if result.is_ok() {
385
+ self.note_warehouse_running();
379
386
  }
387
+ checkin.finish(result.is_ok());
380
388
  result
381
389
  }
382
390
 
@@ -390,6 +398,9 @@ impl DbClient {
390
398
  .authed_json(reqwest::Method::POST, &url, Some(&body), Some(stats))
391
399
  .await?;
392
400
 
401
+ stats.set_in_flight(CancelHandle::Sea {
402
+ statement_id: data.statement_id.clone(),
403
+ });
393
404
  while !matches!(
394
405
  data.status.state.as_str(),
395
406
  "SUCCEEDED" | "FAILED" | "CANCELED" | "CLOSED"
@@ -400,6 +411,7 @@ impl DbClient {
400
411
  .authed_json(reqwest::Method::GET, &poll_url, None, Some(stats))
401
412
  .await?;
402
413
  }
414
+ stats.clear_in_flight();
403
415
 
404
416
  match data.status.state.as_str() {
405
417
  "FAILED" => {
@@ -599,6 +611,31 @@ impl DbClient {
599
611
  /// panic case would let that worker's unfetched work vanish with no error at
600
612
  /// all: the channel closing normally looks to the consumer exactly like a
601
613
  /// complete, successful result instead of a truncated one.
614
+ /// Returns `submit_and_poll`'s session to the pool even when its future is
615
+ /// dropped mid-poll (a timeout or cancellation) -- without it the pool's
616
+ /// reservation for that key leaks, and after `MAX_SESSIONS_PER_KEY` such
617
+ /// drops every later query for the key runs session-less.
618
+ struct SessionCheckin<'a> {
619
+ client: &'a DbClient,
620
+ catalog: Option<&'a str>,
621
+ schema: Option<&'a str>,
622
+ session_id: Option<String>,
623
+ }
624
+
625
+ impl SessionCheckin<'_> {
626
+ fn finish(&mut self, keep: bool) {
627
+ if let Some(id) = self.session_id.take() {
628
+ self.client.checkin_session(self.catalog, self.schema, id, keep);
629
+ }
630
+ }
631
+ }
632
+
633
+ impl Drop for SessionCheckin<'_> {
634
+ fn drop(&mut self) {
635
+ self.finish(false);
636
+ }
637
+ }
638
+
602
639
  async fn join_first_error(handles: Vec<tokio::task::JoinHandle<Result<(), ApiError>>>) -> Option<ApiError> {
603
640
  let mut first_err = None;
604
641
  for h in handles {