arrowbricks 3.0.4__tar.gz → 3.1.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 (36) hide show
  1. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/PKG-INFO +62 -5
  2. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/README.md +61 -4
  3. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/pyproject.toml +1 -1
  4. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/Cargo.lock +165 -1
  5. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/Cargo.toml +21 -1
  6. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/README.md +1 -1
  7. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/src/client.rs +586 -106
  8. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/src/heartbeat.rs +154 -83
  9. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/src/json_convert.rs +131 -32
  10. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/src/lib.rs +373 -27
  11. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/src/pipeline.rs +707 -85
  12. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/src/thrift.rs +221 -4
  13. arrowbricks-3.1.1/rust/arrowbricks_core/tests/common/mod.rs +29 -0
  14. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/tests/wiremock_pipeline.rs +282 -21
  15. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/tests/wiremock_thrift.rs +202 -1
  16. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/tests_py/test_token_provider.py +13 -5
  17. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/src/arrowbricks/__init__.py +6 -0
  18. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/src/arrowbricks/_core.pyi +37 -1
  19. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/src/arrowbricks/_streaming.py +16 -2
  20. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/src/arrowbricks/client.py +33 -0
  21. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/src/arrowbricks/cursor.py +70 -5
  22. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/LICENSE +0 -0
  23. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/.gitignore +0 -0
  24. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/examples/duckdb_query.py +0 -0
  25. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/examples/fastapi_sse.py +0 -0
  26. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/rustfmt.toml +0 -0
  27. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/tests/wiremock_volume_files.rs +0 -0
  28. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/tests_py/conftest.py +0 -0
  29. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/tests_py/test_ipc_stream.py +0 -0
  30. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/tests_py/test_parameters.py +0 -0
  31. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/tests_py/test_stream_ndjson_lines.py +0 -0
  32. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/tests_py/test_streaming.py +0 -0
  33. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/tests_py/test_thrift.py +0 -0
  34. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/tests_py/test_volume_files.py +0 -0
  35. {arrowbricks-3.0.4 → arrowbricks-3.1.1}/rust/arrowbricks_core/tests_py/thrift_mock.py +0 -0
  36. {arrowbricks-3.0.4 → arrowbricks-3.1.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.4
3
+ Version: 3.1.1
4
4
  Requires-Dist: arro3-core>=0.8 ; extra == 'arro3'
5
5
  Provides-Extra: arro3
6
6
  License-File: LICENSE
@@ -77,9 +77,14 @@ async for item in client.stream_query_json("SELECT * FROM my_catalog.my_schema.b
77
77
 
78
78
  See [`examples/basic.py`](examples/basic.py) for a runnable version,
79
79
  [`examples/cursor_paging.py`](examples/cursor_paging.py) for paging a large
80
- result with `fetchmany`/`fetchmany_arrow` without buffering it all upfront, or
80
+ result with `fetchmany`/`fetchmany_arrow` without buffering it all upfront,
81
81
  [`examples/azure_auth.py`](examples/azure_auth.py) for a caching
82
- `token_provider` built on Azure AD (`DefaultAzureCredential`).
82
+ `token_provider` built on Azure AD (`DefaultAzureCredential`, needs
83
+ `azure-identity`), or [`examples/oauth_m2m_auth.py`](examples/oauth_m2m_auth.py)
84
+ for the same idea using Databricks' own OAuth machine-to-machine
85
+ client-credentials flow instead -- like `databricks-sql-connector`'s
86
+ `auth_type="databricks-oauth"`, but stdlib-only (`urllib.request`), no
87
+ extra dependency.
83
88
 
84
89
  ## FastAPI SSE example
85
90
 
@@ -135,19 +140,71 @@ arrowbricks has no opinion on *how* you get a token and no cloud-SDK dependency
135
140
  conn = connect(host=..., warehouse_id=..., token_provider=my_token_provider)
136
141
  ```
137
142
 
143
+ ## Observability
144
+
145
+ Pass `on_event` to `connect`/`DatabricksClient` to get a `QueryStats` snapshot once per query, at completion (success, cancelled, timed out, or errored):
146
+
147
+ ```python
148
+ def log_query(stats):
149
+ print(
150
+ f"{stats.statement_id}: {stats.outcome} in {stats.fetch_s:.2f}s, {stats.bytes_downloaded} bytes, {stats.retry_count} retries"
151
+ )
152
+
153
+
154
+ conn = connect(host=..., warehouse_id=..., token=..., on_event=log_query)
155
+ ```
156
+
157
+ `on_event` is sync or async, same as `token_provider`, and applies to every query run through that client (not passed per-call). It's strictly fire-and-forget: any exception it raises is caught and swallowed on the Rust side, never surfaced to your query, and dispatching it never blocks or measurably slows the actual fetch -- a slow or broken logging/metrics callback can't make your queries slower or fail them. `QueryStats` fields: `statement_id: str`, `protocol: "thrift" | "sea"`, `warehouse_wait_s: float` (0.0 if the client already knew the warehouse was running), `submit_to_ready_s: float` (statement submit -> ready to fetch), `fetch_s: float` (download + decode), `num_chunks: int`, `bytes_downloaded: int`, `retry_count: int`, `concurrency_used: int`, and `outcome: "success" | "cancelled" | "timeout" | "error"`.
158
+
159
+ `on_event` fires once the fetch of a given query actually completes (or is abandoned) -- a `Cursor` whose result is never fully drained (e.g. only a partial `fetchmany()`, then abandoned) never fires one, same category as this package's existing documented gap around GC-based cleanup.
160
+
161
+ ## Cancellation
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.
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.
166
+
167
+ ## Errors
168
+
169
+ Every exception arrowbricks raises itself is an `ArrowbricksError` -- a `RuntimeError` subclass, so an `except RuntimeError` written before this hierarchy existed keeps catching everything unchanged. Three subclasses split by what actually changes what your code should do next:
170
+
171
+ - `TransientError` -- a network blip, connection reset, or 5xx that survived every internal retry (`retry_attempts`, exponential backoff up to `retry_max_wait_s` -- see below) before ever reaching you. Back off further and try again later; retrying immediately just repeats what already failed.
172
+ - `AuthError` -- HTTP 401/403, even after every internal retry re-fetched a token from your `token_provider`, *or* your `token_provider` itself raised (e.g. its own OAuth refresh call came back unauthorized). Treat the credential itself as bad/expired, not a transient blip.
173
+ - `StatementError` -- the SQL statement itself failed or was canceled server-side (bad SQL, a permissions error, a warehouse-side query failure). Retrying the identical statement will fail identically; this isn't a transport problem.
174
+
175
+ Anything else (a generic non-auth/non-statement HTTP error, an internal parse/decode failure) raises the plain `ArrowbricksError` base.
176
+
177
+ ```python
178
+ from arrowbricks import ArrowbricksError, AuthError, StatementError, TransientError
179
+
180
+ try:
181
+ await cursor.execute(sql)
182
+ except AuthError:
183
+ refresh_credentials()
184
+ except StatementError:
185
+ raise # bad SQL -- don't retry
186
+ except TransientError:
187
+ await asyncio.sleep(5)
188
+ await cursor.execute(sql) # safe to retry
189
+ except ArrowbricksError:
190
+ raise
191
+ ```
192
+
193
+ `QueryTimeout` (`total_timeout_s` exceeded -- see Cancellation above) also subclasses `ArrowbricksError`, so `except ArrowbricksError` is a genuine catch-all for every exception this package raises itself.
194
+
138
195
  ## API
139
196
 
140
197
  - `connect(host, warehouse_id, *, token=None, token_provider=None, ...) -> Connection`
141
198
  - `Connection.cursor() -> Cursor`
142
199
  - `Connection.client -> DatabricksClient` -- the same client `cursor()` uses, for lower-level access (e.g. `stream_query_json`, `upload_volume_file`).
143
- - `Cursor.execute(sql, parameters=None, *, row_limit=None, offset=None, catalog=None, schema=None, total_timeout_s=None, prefer_inline=False) -> Cursor` -- submits and waits for the statement, like a real DB-API cursor. `parameters`, if given, is Databricks' own named-parameter format -- `[{"name": ..., "value": ..., "type": ...}]` bound against `:name` markers in `sql`. `prefer_inline=True` tries fetching a small result (well under Databricks' 25 MiB inline cap) in the same round trip as the submission itself, skipping the chunk-fetch entirely -- if the result turns out too big, or has a column type this can't convert (nested ARRAY/MAP/STRUCT, VARIANT), it transparently re-runs the query the normal way, so a caller who sets this without actually expecting a small result pays for the query twice. Leave it off unless you know the result is small.
200
+ - `Cursor.execute(sql, parameters=None, *, row_limit=None, offset=None, catalog=None, schema=None, total_timeout_s=None, prefer_inline=False) -> Cursor` -- submits and waits for the statement, like a real DB-API cursor. `parameters`, if given, is Databricks' own named-parameter format -- `[{"name": ..., "value": ..., "type": ...}]` bound against `:name` markers in `sql`. `prefer_inline=True` tries fetching a small result (well under Databricks' 25 MiB inline cap) in the same round trip as the submission itself, skipping the chunk-fetch entirely. If the result turns out too big, the statement fails server-side before ever really executing, and arrowbricks transparently re-runs it the normal way -- safe to double-submit, since nothing committed the first time; a caller who sets `prefer_inline` without actually expecting a small result just pays for the query twice in this case. If instead the result comes back but has a column type this can't convert (nested ARRAY/MAP/STRUCT, VARIANT), the statement already *succeeded* -- arrowbricks does **not** silently re-run it (that could duplicate a write for non-idempotent SQL); it raises `ArrowbricksError` naming the statement instead, so you know it ran and can decide yourself whether re-running is safe. Leave `prefer_inline` off unless you know the result is small, and unless you know the query is a `SELECT` (or otherwise idempotent) if you want the byte-limit fallback's automatic retry to be safe too.
144
201
  - `Cursor.execute_streamed(...)` -- same args, but an async generator yielding `HEARTBEAT` while waiting on a slow cold start, then the ready `Cursor` -- for bridging e.g. an SSE connection. Its timeout/heartbeats stop the moment the statement is ready, *before* any chunk has been downloaded -- see `fetchall_streamed` below for the download phase itself.
145
202
  - `Cursor.fetchone() -> tuple | None`, `Cursor.fetchmany(size) -> list[tuple]`, `Cursor.fetchall() -> list[tuple]`, and iterating a `Cursor` directly -- row tuples; needs the `arro3` extra.
146
203
  - `Cursor.fetchmany_arrow(size) -> Table`, `Cursor.fetchall_arrow() -> Table` -- an Arrow table (implements `__arrow_c_stream__`, so arro3/pyarrow/DuckDB can all consume it directly, zero-copy).
147
204
  - `Cursor.fetchall_streamed(*, total_timeout_s=None)` / `Cursor.fetchall_arrow_streamed(*, total_timeout_s=None)` -- like `fetchall()`/`fetchall_arrow()`, but yield `HEARTBEAT` while pulling chunks instead of blocking silently, then the final rows/Table -- for a caller downloading a large result over SSE who needs heartbeats (and a timeout) through the *download*, not just the initial wait. Compose with `execute_streamed` and a shared deadline if you want one combined budget across both phases (see `examples/fastapi_sse_pivot.py`).
148
205
  - `Cursor.description` -- DB-API-style `[(name, type_name, None, None, None, None, None), ...]` after `execute()`.
149
206
  - `client.stream_query_json(sql, **kwargs)` (or the equivalent free function `stream_query_json(client, sql, **kwargs)`) -- yields `HEARTBEAT`, then each row as a JSON string, as soon as its chunk arrives. Timestamps come out as full ISO-8601, every column key is always present (`"col":null` for a null value, never an omitted key). JSON has no literal for NaN/Infinity/-Infinity, so those come back as `"col":null` by default -- pass `non_finite_floats="string"` to get `"col":"NaN"`/`"col":"Infinity"`/`"col":"-Infinity"` instead if you need to tell them apart from a real NULL.
150
- - `DatabricksClient(host, warehouse_id, *, token=None, token_provider=None, protocol="thrift", ...)` -- the lower-level client `Connection` wraps. `client.upload_volume_file(volume_path, data)`/`client.delete_volume_file(volume_path)` for the Files API. Pass `protocol="sea"` to opt into the REST Statement Execution API backend instead of the default Thrift one (see above) -- `prefer_inline` (SEA-only) has no effect under `protocol="thrift"` (silent no-op, not an error), since Thrift's own inline-result mechanism already covers that case.
207
+ - `DatabricksClient(host, warehouse_id, *, token=None, token_provider=None, protocol="thrift", on_event=None, retry_attempts=6, retry_max_wait_s=20.0, ...)` -- the lower-level client `Connection` wraps. `client.upload_volume_file(volume_path, data)`/`client.delete_volume_file(volume_path)` for the Files API. Pass `protocol="sea"` to opt into the REST Statement Execution API backend instead of the default Thrift one (see above) -- `prefer_inline` (SEA-only) has no effect under `protocol="thrift"` (silent no-op, not an error), since Thrift's own inline-result mechanism already covers that case. `on_event` -- see Observability below. `retry_attempts`/`retry_max_wait_s` tune the retry policy behind `TransientError`/`AuthError` (see Errors above) -- total attempts (the first try plus `retry_attempts - 1` retries) and the exponential-backoff ceiling in seconds, for every retryable request this client makes (statement submit/poll, chunk-index resolution, chunk download, volume file ops). `retry_attempts` must be at least 1 (raises `ValueError` otherwise).
151
208
  - `write_ipc_stream(table, buf)` -- writes any Arrow-C-Data-Interface-compatible object as an uncompressed Arrow-IPC stream (see below).
152
209
  - `ReplayableArrowChunk(data: bytes, chunk_index, declared_row_count=None)` -- wraps raw Arrow-IPC stream bytes (e.g. previously downloaded and stored) so they can be read more than once via `__arrow_c_stream__` (a schema peek, then the actual scan -- DuckDB's registration path does this), and `.to_table()` for a one-shot parse. No extra dependency needed.
153
210
 
@@ -65,9 +65,14 @@ async for item in client.stream_query_json("SELECT * FROM my_catalog.my_schema.b
65
65
 
66
66
  See [`examples/basic.py`](examples/basic.py) for a runnable version,
67
67
  [`examples/cursor_paging.py`](examples/cursor_paging.py) for paging a large
68
- result with `fetchmany`/`fetchmany_arrow` without buffering it all upfront, or
68
+ result with `fetchmany`/`fetchmany_arrow` without buffering it all upfront,
69
69
  [`examples/azure_auth.py`](examples/azure_auth.py) for a caching
70
- `token_provider` built on Azure AD (`DefaultAzureCredential`).
70
+ `token_provider` built on Azure AD (`DefaultAzureCredential`, needs
71
+ `azure-identity`), or [`examples/oauth_m2m_auth.py`](examples/oauth_m2m_auth.py)
72
+ for the same idea using Databricks' own OAuth machine-to-machine
73
+ client-credentials flow instead -- like `databricks-sql-connector`'s
74
+ `auth_type="databricks-oauth"`, but stdlib-only (`urllib.request`), no
75
+ extra dependency.
71
76
 
72
77
  ## FastAPI SSE example
73
78
 
@@ -123,19 +128,71 @@ arrowbricks has no opinion on *how* you get a token and no cloud-SDK dependency
123
128
  conn = connect(host=..., warehouse_id=..., token_provider=my_token_provider)
124
129
  ```
125
130
 
131
+ ## Observability
132
+
133
+ Pass `on_event` to `connect`/`DatabricksClient` to get a `QueryStats` snapshot once per query, at completion (success, cancelled, timed out, or errored):
134
+
135
+ ```python
136
+ def log_query(stats):
137
+ print(
138
+ f"{stats.statement_id}: {stats.outcome} in {stats.fetch_s:.2f}s, {stats.bytes_downloaded} bytes, {stats.retry_count} retries"
139
+ )
140
+
141
+
142
+ conn = connect(host=..., warehouse_id=..., token=..., on_event=log_query)
143
+ ```
144
+
145
+ `on_event` is sync or async, same as `token_provider`, and applies to every query run through that client (not passed per-call). It's strictly fire-and-forget: any exception it raises is caught and swallowed on the Rust side, never surfaced to your query, and dispatching it never blocks or measurably slows the actual fetch -- a slow or broken logging/metrics callback can't make your queries slower or fail them. `QueryStats` fields: `statement_id: str`, `protocol: "thrift" | "sea"`, `warehouse_wait_s: float` (0.0 if the client already knew the warehouse was running), `submit_to_ready_s: float` (statement submit -> ready to fetch), `fetch_s: float` (download + decode), `num_chunks: int`, `bytes_downloaded: int`, `retry_count: int`, `concurrency_used: int`, and `outcome: "success" | "cancelled" | "timeout" | "error"`.
146
+
147
+ `on_event` fires once the fetch of a given query actually completes (or is abandoned) -- a `Cursor` whose result is never fully drained (e.g. only a partial `fetchmany()`, then abandoned) never fires one, same category as this package's existing documented gap around GC-based cleanup.
148
+
149
+ ## Cancellation
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.
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.
154
+
155
+ ## Errors
156
+
157
+ Every exception arrowbricks raises itself is an `ArrowbricksError` -- a `RuntimeError` subclass, so an `except RuntimeError` written before this hierarchy existed keeps catching everything unchanged. Three subclasses split by what actually changes what your code should do next:
158
+
159
+ - `TransientError` -- a network blip, connection reset, or 5xx that survived every internal retry (`retry_attempts`, exponential backoff up to `retry_max_wait_s` -- see below) before ever reaching you. Back off further and try again later; retrying immediately just repeats what already failed.
160
+ - `AuthError` -- HTTP 401/403, even after every internal retry re-fetched a token from your `token_provider`, *or* your `token_provider` itself raised (e.g. its own OAuth refresh call came back unauthorized). Treat the credential itself as bad/expired, not a transient blip.
161
+ - `StatementError` -- the SQL statement itself failed or was canceled server-side (bad SQL, a permissions error, a warehouse-side query failure). Retrying the identical statement will fail identically; this isn't a transport problem.
162
+
163
+ Anything else (a generic non-auth/non-statement HTTP error, an internal parse/decode failure) raises the plain `ArrowbricksError` base.
164
+
165
+ ```python
166
+ from arrowbricks import ArrowbricksError, AuthError, StatementError, TransientError
167
+
168
+ try:
169
+ await cursor.execute(sql)
170
+ except AuthError:
171
+ refresh_credentials()
172
+ except StatementError:
173
+ raise # bad SQL -- don't retry
174
+ except TransientError:
175
+ await asyncio.sleep(5)
176
+ await cursor.execute(sql) # safe to retry
177
+ except ArrowbricksError:
178
+ raise
179
+ ```
180
+
181
+ `QueryTimeout` (`total_timeout_s` exceeded -- see Cancellation above) also subclasses `ArrowbricksError`, so `except ArrowbricksError` is a genuine catch-all for every exception this package raises itself.
182
+
126
183
  ## API
127
184
 
128
185
  - `connect(host, warehouse_id, *, token=None, token_provider=None, ...) -> Connection`
129
186
  - `Connection.cursor() -> Cursor`
130
187
  - `Connection.client -> DatabricksClient` -- the same client `cursor()` uses, for lower-level access (e.g. `stream_query_json`, `upload_volume_file`).
131
- - `Cursor.execute(sql, parameters=None, *, row_limit=None, offset=None, catalog=None, schema=None, total_timeout_s=None, prefer_inline=False) -> Cursor` -- submits and waits for the statement, like a real DB-API cursor. `parameters`, if given, is Databricks' own named-parameter format -- `[{"name": ..., "value": ..., "type": ...}]` bound against `:name` markers in `sql`. `prefer_inline=True` tries fetching a small result (well under Databricks' 25 MiB inline cap) in the same round trip as the submission itself, skipping the chunk-fetch entirely -- if the result turns out too big, or has a column type this can't convert (nested ARRAY/MAP/STRUCT, VARIANT), it transparently re-runs the query the normal way, so a caller who sets this without actually expecting a small result pays for the query twice. Leave it off unless you know the result is small.
188
+ - `Cursor.execute(sql, parameters=None, *, row_limit=None, offset=None, catalog=None, schema=None, total_timeout_s=None, prefer_inline=False) -> Cursor` -- submits and waits for the statement, like a real DB-API cursor. `parameters`, if given, is Databricks' own named-parameter format -- `[{"name": ..., "value": ..., "type": ...}]` bound against `:name` markers in `sql`. `prefer_inline=True` tries fetching a small result (well under Databricks' 25 MiB inline cap) in the same round trip as the submission itself, skipping the chunk-fetch entirely. If the result turns out too big, the statement fails server-side before ever really executing, and arrowbricks transparently re-runs it the normal way -- safe to double-submit, since nothing committed the first time; a caller who sets `prefer_inline` without actually expecting a small result just pays for the query twice in this case. If instead the result comes back but has a column type this can't convert (nested ARRAY/MAP/STRUCT, VARIANT), the statement already *succeeded* -- arrowbricks does **not** silently re-run it (that could duplicate a write for non-idempotent SQL); it raises `ArrowbricksError` naming the statement instead, so you know it ran and can decide yourself whether re-running is safe. Leave `prefer_inline` off unless you know the result is small, and unless you know the query is a `SELECT` (or otherwise idempotent) if you want the byte-limit fallback's automatic retry to be safe too.
132
189
  - `Cursor.execute_streamed(...)` -- same args, but an async generator yielding `HEARTBEAT` while waiting on a slow cold start, then the ready `Cursor` -- for bridging e.g. an SSE connection. Its timeout/heartbeats stop the moment the statement is ready, *before* any chunk has been downloaded -- see `fetchall_streamed` below for the download phase itself.
133
190
  - `Cursor.fetchone() -> tuple | None`, `Cursor.fetchmany(size) -> list[tuple]`, `Cursor.fetchall() -> list[tuple]`, and iterating a `Cursor` directly -- row tuples; needs the `arro3` extra.
134
191
  - `Cursor.fetchmany_arrow(size) -> Table`, `Cursor.fetchall_arrow() -> Table` -- an Arrow table (implements `__arrow_c_stream__`, so arro3/pyarrow/DuckDB can all consume it directly, zero-copy).
135
192
  - `Cursor.fetchall_streamed(*, total_timeout_s=None)` / `Cursor.fetchall_arrow_streamed(*, total_timeout_s=None)` -- like `fetchall()`/`fetchall_arrow()`, but yield `HEARTBEAT` while pulling chunks instead of blocking silently, then the final rows/Table -- for a caller downloading a large result over SSE who needs heartbeats (and a timeout) through the *download*, not just the initial wait. Compose with `execute_streamed` and a shared deadline if you want one combined budget across both phases (see `examples/fastapi_sse_pivot.py`).
136
193
  - `Cursor.description` -- DB-API-style `[(name, type_name, None, None, None, None, None), ...]` after `execute()`.
137
194
  - `client.stream_query_json(sql, **kwargs)` (or the equivalent free function `stream_query_json(client, sql, **kwargs)`) -- yields `HEARTBEAT`, then each row as a JSON string, as soon as its chunk arrives. Timestamps come out as full ISO-8601, every column key is always present (`"col":null` for a null value, never an omitted key). JSON has no literal for NaN/Infinity/-Infinity, so those come back as `"col":null` by default -- pass `non_finite_floats="string"` to get `"col":"NaN"`/`"col":"Infinity"`/`"col":"-Infinity"` instead if you need to tell them apart from a real NULL.
138
- - `DatabricksClient(host, warehouse_id, *, token=None, token_provider=None, protocol="thrift", ...)` -- the lower-level client `Connection` wraps. `client.upload_volume_file(volume_path, data)`/`client.delete_volume_file(volume_path)` for the Files API. Pass `protocol="sea"` to opt into the REST Statement Execution API backend instead of the default Thrift one (see above) -- `prefer_inline` (SEA-only) has no effect under `protocol="thrift"` (silent no-op, not an error), since Thrift's own inline-result mechanism already covers that case.
195
+ - `DatabricksClient(host, warehouse_id, *, token=None, token_provider=None, protocol="thrift", on_event=None, retry_attempts=6, retry_max_wait_s=20.0, ...)` -- the lower-level client `Connection` wraps. `client.upload_volume_file(volume_path, data)`/`client.delete_volume_file(volume_path)` for the Files API. Pass `protocol="sea"` to opt into the REST Statement Execution API backend instead of the default Thrift one (see above) -- `prefer_inline` (SEA-only) has no effect under `protocol="thrift"` (silent no-op, not an error), since Thrift's own inline-result mechanism already covers that case. `on_event` -- see Observability below. `retry_attempts`/`retry_max_wait_s` tune the retry policy behind `TransientError`/`AuthError` (see Errors above) -- total attempts (the first try plus `retry_attempts - 1` retries) and the exponential-backoff ceiling in seconds, for every retryable request this client makes (statement submit/poll, chunk-index resolution, chunk download, volume file ops). `retry_attempts` must be at least 1 (raises `ValueError` otherwise).
139
196
  - `write_ipc_stream(table, buf)` -- writes any Arrow-C-Data-Interface-compatible object as an uncompressed Arrow-IPC stream (see below).
140
197
  - `ReplayableArrowChunk(data: bytes, chunk_index, declared_row_count=None)` -- wraps raw Arrow-IPC stream bytes (e.g. previously downloaded and stored) so they can be read more than once via `__arrow_c_stream__` (a schema peek, then the actual scan -- DuckDB's registration path does this), and `.to_table()` for a one-shot parse. No extra dependency needed.
141
198
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arrowbricks"
3
- version = "3.0.4"
3
+ version = "3.1.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"
@@ -241,14 +241,16 @@ dependencies = [
241
241
 
242
242
  [[package]]
243
243
  name = "arrowbricks_core"
244
- version = "3.0.4"
244
+ version = "3.1.1"
245
245
  dependencies = [
246
246
  "arrow",
247
247
  "arrow-json",
248
+ "base64 0.23.1",
248
249
  "bytes",
249
250
  "chrono",
250
251
  "hyper-rustls",
251
252
  "lz4_flex",
253
+ "proptest",
252
254
  "pyo3",
253
255
  "pyo3-arrow",
254
256
  "pyo3-async-runtimes",
@@ -304,6 +306,21 @@ version = "0.23.1"
304
306
  source = "registry+https://github.com/rust-lang/crates.io-index"
305
307
  checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
306
308
 
309
+ [[package]]
310
+ name = "bit-set"
311
+ version = "0.8.0"
312
+ source = "registry+https://github.com/rust-lang/crates.io-index"
313
+ checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
314
+ dependencies = [
315
+ "bit-vec",
316
+ ]
317
+
318
+ [[package]]
319
+ name = "bit-vec"
320
+ version = "0.8.0"
321
+ source = "registry+https://github.com/rust-lang/crates.io-index"
322
+ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
323
+
307
324
  [[package]]
308
325
  name = "bitflags"
309
326
  version = "2.13.1"
@@ -458,6 +475,22 @@ version = "1.0.2"
458
475
  source = "registry+https://github.com/rust-lang/crates.io-index"
459
476
  checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
460
477
 
478
+ [[package]]
479
+ name = "errno"
480
+ version = "0.3.14"
481
+ source = "registry+https://github.com/rust-lang/crates.io-index"
482
+ checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
483
+ dependencies = [
484
+ "libc",
485
+ "windows-sys 0.61.2",
486
+ ]
487
+
488
+ [[package]]
489
+ name = "fastrand"
490
+ version = "2.5.0"
491
+ source = "registry+https://github.com/rust-lang/crates.io-index"
492
+ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
493
+
461
494
  [[package]]
462
495
  name = "find-msvc-tools"
463
496
  version = "0.1.10"
@@ -1038,6 +1071,12 @@ version = "0.2.16"
1038
1071
  source = "registry+https://github.com/rust-lang/crates.io-index"
1039
1072
  checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
1040
1073
 
1074
+ [[package]]
1075
+ name = "linux-raw-sys"
1076
+ version = "0.12.1"
1077
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1078
+ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
1079
+
1041
1080
  [[package]]
1042
1081
  name = "litemap"
1043
1082
  version = "0.8.2"
@@ -1232,6 +1271,15 @@ dependencies = [
1232
1271
  "zerovec",
1233
1272
  ]
1234
1273
 
1274
+ [[package]]
1275
+ name = "ppv-lite86"
1276
+ version = "0.2.21"
1277
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1278
+ checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
1279
+ dependencies = [
1280
+ "zerocopy",
1281
+ ]
1282
+
1235
1283
  [[package]]
1236
1284
  name = "proc-macro2"
1237
1285
  version = "1.0.107"
@@ -1241,6 +1289,25 @@ dependencies = [
1241
1289
  "unicode-ident",
1242
1290
  ]
1243
1291
 
1292
+ [[package]]
1293
+ name = "proptest"
1294
+ version = "1.11.0"
1295
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1296
+ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
1297
+ dependencies = [
1298
+ "bit-set",
1299
+ "bit-vec",
1300
+ "bitflags",
1301
+ "num-traits",
1302
+ "rand",
1303
+ "rand_chacha",
1304
+ "rand_xorshift",
1305
+ "regex-syntax",
1306
+ "rusty-fork",
1307
+ "tempfile",
1308
+ "unarray",
1309
+ ]
1310
+
1244
1311
  [[package]]
1245
1312
  name = "pyo3"
1246
1313
  version = "0.29.2"
@@ -1346,6 +1413,12 @@ dependencies = [
1346
1413
  "serde",
1347
1414
  ]
1348
1415
 
1416
+ [[package]]
1417
+ name = "quick-error"
1418
+ version = "1.2.3"
1419
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1420
+ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
1421
+
1349
1422
  [[package]]
1350
1423
  name = "quote"
1351
1424
  version = "1.0.47"
@@ -1361,6 +1434,44 @@ version = "5.3.0"
1361
1434
  source = "registry+https://github.com/rust-lang/crates.io-index"
1362
1435
  checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
1363
1436
 
1437
+ [[package]]
1438
+ name = "rand"
1439
+ version = "0.9.5"
1440
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1441
+ checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
1442
+ dependencies = [
1443
+ "rand_chacha",
1444
+ "rand_core",
1445
+ ]
1446
+
1447
+ [[package]]
1448
+ name = "rand_chacha"
1449
+ version = "0.9.0"
1450
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1451
+ checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
1452
+ dependencies = [
1453
+ "ppv-lite86",
1454
+ "rand_core",
1455
+ ]
1456
+
1457
+ [[package]]
1458
+ name = "rand_core"
1459
+ version = "0.9.5"
1460
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1461
+ checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
1462
+ dependencies = [
1463
+ "getrandom 0.3.4",
1464
+ ]
1465
+
1466
+ [[package]]
1467
+ name = "rand_xorshift"
1468
+ version = "0.4.0"
1469
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1470
+ checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
1471
+ dependencies = [
1472
+ "rand_core",
1473
+ ]
1474
+
1364
1475
  [[package]]
1365
1476
  name = "rawpointer"
1366
1477
  version = "0.2.1"
@@ -1462,6 +1573,19 @@ dependencies = [
1462
1573
  "semver",
1463
1574
  ]
1464
1575
 
1576
+ [[package]]
1577
+ name = "rustix"
1578
+ version = "1.1.4"
1579
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1580
+ checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
1581
+ dependencies = [
1582
+ "bitflags",
1583
+ "errno",
1584
+ "libc",
1585
+ "linux-raw-sys",
1586
+ "windows-sys 0.61.2",
1587
+ ]
1588
+
1465
1589
  [[package]]
1466
1590
  name = "rustls"
1467
1591
  version = "0.23.43"
@@ -1542,6 +1666,18 @@ version = "1.0.23"
1542
1666
  source = "registry+https://github.com/rust-lang/crates.io-index"
1543
1667
  checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
1544
1668
 
1669
+ [[package]]
1670
+ name = "rusty-fork"
1671
+ version = "0.3.1"
1672
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1673
+ checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
1674
+ dependencies = [
1675
+ "fnv",
1676
+ "quick-error",
1677
+ "tempfile",
1678
+ "wait-timeout",
1679
+ ]
1680
+
1545
1681
  [[package]]
1546
1682
  name = "ryu"
1547
1683
  version = "1.0.23"
@@ -1760,6 +1896,19 @@ version = "0.13.5"
1760
1896
  source = "registry+https://github.com/rust-lang/crates.io-index"
1761
1897
  checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
1762
1898
 
1899
+ [[package]]
1900
+ name = "tempfile"
1901
+ version = "3.27.0"
1902
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1903
+ checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
1904
+ dependencies = [
1905
+ "fastrand",
1906
+ "getrandom 0.3.4",
1907
+ "once_cell",
1908
+ "rustix",
1909
+ "windows-sys 0.61.2",
1910
+ ]
1911
+
1763
1912
  [[package]]
1764
1913
  name = "thiserror"
1765
1914
  version = "1.0.69"
@@ -1945,6 +2094,12 @@ version = "2.1.3"
1945
2094
  source = "registry+https://github.com/rust-lang/crates.io-index"
1946
2095
  checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9"
1947
2096
 
2097
+ [[package]]
2098
+ name = "unarray"
2099
+ version = "0.1.4"
2100
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2101
+ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
2102
+
1948
2103
  [[package]]
1949
2104
  name = "unicode-ident"
1950
2105
  version = "1.0.24"
@@ -1993,6 +2148,15 @@ version = "0.9.5"
1993
2148
  source = "registry+https://github.com/rust-lang/crates.io-index"
1994
2149
  checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1995
2150
 
2151
+ [[package]]
2152
+ name = "wait-timeout"
2153
+ version = "0.2.1"
2154
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2155
+ checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
2156
+ dependencies = [
2157
+ "libc",
2158
+ ]
2159
+
1996
2160
  [[package]]
1997
2161
  name = "walkdir"
1998
2162
  version = "2.5.0"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "arrowbricks_core"
3
- version = "3.0.4"
3
+ version = "3.1.1"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -15,6 +15,12 @@ crate-type = ["cdylib", "rlib"]
15
15
  arrow = { version = "59.1.0", default-features = false, features = ["ipc"] }
16
16
  arrow-json = "59.1.0"
17
17
  bytes = "1.12.1"
18
+ # Already an unconditional transitive dependency of arrow-cast (pulled in by
19
+ # pyo3-arrow) at this exact version -- declared directly here too so
20
+ # json_convert.rs can decode BINARY columns without hand-rolling base64. No
21
+ # new code ships that wasn't already linked in (feature unification, same
22
+ # reasoning as chrono/rustls below).
23
+ base64 = "0.23"
18
24
  # Already an unconditional transitive dependency of pyo3-arrow (see its own
19
25
  # comment below) -- declared directly here too so json_convert.rs can parse
20
26
  # DATE/TIMESTAMP/TIMESTAMP_NTZ strings without hand-rolling calendar
@@ -75,6 +81,20 @@ extension-module = ["pyo3/extension-module"]
75
81
  [dev-dependencies]
76
82
  tokio = { version = "1.53.1", features = ["rt-multi-thread", "macros", "test-util", "net", "io-util"] }
77
83
  wiremock = "0.6.5"
84
+ # Property-based fuzzing for the hand-rolled wire-protocol parsers
85
+ # (thrift.rs's Reader, json_convert.rs's STRUCT type_text tokenizer,
86
+ # client.rs's decompress_lz4_frame) -- dev-only, runs inside plain
87
+ # `cargo test` via the `proptest!` macro (`#[test]` under the hood, no
88
+ # separate fuzz-target/corpus infra the way cargo-fuzz/afl need, and no
89
+ # nightly toolchain requirement either). Never touches the shipped wheel's
90
+ # dependency graph -- `[dev-dependencies]` are only ever linked into `cargo
91
+ # test`/`cargo bench` binaries, never into the `cdylib` maturin actually
92
+ # builds (see this crate's own `extension-module` feature/profile split
93
+ # above for how that build works). Default config (256 cases/property) is
94
+ # left alone; per-property `ProptestConfig` below trims a couple of the
95
+ # slower ones (deep nesting, wide byte vectors) so `cargo test` doesn't
96
+ # become noticeably slower overall.
97
+ proptest = "1"
78
98
 
79
99
  # Currently clean on all three by luck, not enforcement -- pinning them here
80
100
  # so a future PR that introduces one fails CI instead of silently landing.
@@ -66,7 +66,7 @@ unlike the table object itself.
66
66
 
67
67
  ## API
68
68
 
69
- - `Client(host, warehouse_id, *, token=None, token_provider=None, chunk_fetch_concurrency=64, http_timeout=60.0, wait_timeout="30s", warehouse_start_timeout=300.0, warehouse_confirmed_running_ttl_s=30.0, compress_results=True, protocol="thrift")` -- exactly one of `token`/`token_provider`. `token_provider` is a callable (sync or async) returning a token string, called fresh on every request, no caching. `compress_results` requests LZ4-compressed cloud-fetch chunks (see above); set `False` to opt out. `protocol="thrift"` (the default, as of this crate's own real-workspace benchmarking -- see `AGENTS.md`'s design-invariant entry) speaks the same HiveServer2-compatible Thrift-over-HTTPS protocol `databricks-sql-connector` uses by default (`thrift.rs`, a hand-rolled `TBinaryProtocol` reader/writer -- no new Cargo dependency) -- measurably faster for small queries (its `ExecuteStatement` RPC can return a small result inline via `getDirectResults`, in the same call that submits the statement) at the cost of `prefer_inline` becoming a silent no-op (Thrift has no INLINE-disposition equivalent, and doesn't need one); never slower than SEA on any query shape tested. `protocol="sea"` instead talks to the REST Statement Execution API -- still fully supported, opt in explicitly if you have a reason to prefer it. On par with SEA for a large, multi-chunk result too -- `run_thrift_fetch_loop` (`pipeline.rs`) fans its chunk downloads out across a `chunk_fetch_concurrency`-sized worker pool spanning the *whole* result (pipelined with the sequential `FetchResults` discovery calls, not serialized behind them), the same concurrency shape as SEA's own `fetch_chunks_with_backpressure` (see `AGENTS.md`'s own entry on this).
69
+ - `Client(host, warehouse_id, *, token=None, token_provider=None, chunk_fetch_concurrency=64, http_timeout=60.0, wait_timeout="30s", warehouse_start_timeout=300.0, warehouse_confirmed_running_ttl_s=30.0, compress_results=True, protocol="thrift", retry_attempts=6, retry_max_wait_s=20.0)` -- exactly one of `token`/`token_provider`. `retry_attempts`/`retry_max_wait_s` tune the retry policy (total attempts, exponential-backoff ceiling in seconds) behind every retryable request this client makes; `retry_attempts` must be at least 1. `token_provider` is a callable (sync or async) returning a token string, called fresh on every request, no caching. `compress_results` requests LZ4-compressed cloud-fetch chunks (see above); set `False` to opt out. `protocol="thrift"` (the default, as of this crate's own real-workspace benchmarking -- see `AGENTS.md`'s design-invariant entry) speaks the same HiveServer2-compatible Thrift-over-HTTPS protocol `databricks-sql-connector` uses by default (`thrift.rs`, a hand-rolled `TBinaryProtocol` reader/writer -- no new Cargo dependency) -- measurably faster for small queries (its `ExecuteStatement` RPC can return a small result inline via `getDirectResults`, in the same call that submits the statement) at the cost of `prefer_inline` becoming a silent no-op (Thrift has no INLINE-disposition equivalent, and doesn't need one); never slower than SEA on any query shape tested. `protocol="sea"` instead talks to the REST Statement Execution API -- still fully supported, opt in explicitly if you have a reason to prefer it. On par with SEA for a large, multi-chunk result too -- `run_thrift_fetch_loop` (`pipeline.rs`) fans its chunk downloads out across a `chunk_fetch_concurrency`-sized worker pool spanning the *whole* result (pipelined with the sequential `FetchResults` discovery calls, not serialized behind them), the same concurrency shape as SEA's own `fetch_chunks_with_backpressure` (see `AGENTS.md`'s own entry on this).
70
70
  - `Client.execute(statement, *, catalog=None, schema=None, parameters=None, prefer_inline=False) -> ResultSet` -- submits and starts background chunk fetching without pulling anything yet. `parameters` is Databricks' own named-parameter format (`[{"name":..., "value":..., "type":...}]`), passed straight through. `prefer_inline=True` submits with `disposition=INLINE, format=JSON_ARRAY` instead, for a caller who expects a small (well under Databricks' 25 MiB inline cap) result and wants to skip the chunk-fetch round trip -- on a result too big for INLINE, or containing a column type `json_convert.rs` doesn't map (STRUCT/ARRAY-of-STRUCT/MAP/VARIANT), it transparently falls back to a second, normal `execute()` and the query runs twice. See `AGENTS.md`'s "Design invariants" section (in the root package) for the full reasoning and real-workspace verification behind this.
71
71
  - `ResultSet.fetchmany_arrow(n) -> Table` -- pulls/decodes only as many chunks as needed for `n` rows, buffering the rest; may return fewer than `n` once exhausted.
72
72
  - `ResultSet.fetchall_arrow() -> Table` -- drains everything remaining.