quack-ruby 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/LICENSE-DuckDB +7 -0
- data/LICENSE-QuackProtocol +21 -0
- data/LICENSE-Rust-dependencies.json +2057 -0
- data/README.md +229 -0
- data/ext/quack/Cargo.lock +1803 -0
- data/ext/quack/Cargo.toml +17 -0
- data/ext/quack/extconf.rb +4 -0
- data/ext/quack/src/lib.rs +647 -0
- data/ext/quack/vendor/quack_protocol/Cargo.lock +1907 -0
- data/ext/quack/vendor/quack_protocol/Cargo.toml +41 -0
- data/ext/quack/vendor/quack_protocol/LICENSE +21 -0
- data/ext/quack/vendor/quack_protocol/PATCHES.md +16 -0
- data/ext/quack/vendor/quack_protocol/README.md +272 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/columns.rs +399 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/mod.rs +134 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/schema.rs +103 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/tests.rs +851 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/values.rs +150 -0
- data/ext/quack/vendor/quack_protocol/src/binary.rs +504 -0
- data/ext/quack/vendor/quack_protocol/src/builders.rs +217 -0
- data/ext/quack/vendor/quack_protocol/src/client.rs +1486 -0
- data/ext/quack/vendor/quack_protocol/src/constants.rs +11 -0
- data/ext/quack/vendor/quack_protocol/src/errors.rs +103 -0
- data/ext/quack/vendor/quack_protocol/src/inspection.rs +250 -0
- data/ext/quack/vendor/quack_protocol/src/json.rs +225 -0
- data/ext/quack/vendor/quack_protocol/src/lib.rs +54 -0
- data/ext/quack/vendor/quack_protocol/src/logical_types.rs +977 -0
- data/ext/quack/vendor/quack_protocol/src/messages.rs +843 -0
- data/ext/quack/vendor/quack_protocol/src/pool.rs +741 -0
- data/ext/quack/vendor/quack_protocol/src/protocol_tests.rs +759 -0
- data/ext/quack/vendor/quack_protocol/src/proxy_error.rs +77 -0
- data/ext/quack/vendor/quack_protocol/src/sql.rs +359 -0
- data/ext/quack/vendor/quack_protocol/src/tls.rs +354 -0
- data/ext/quack/vendor/quack_protocol/src/values.rs +77 -0
- data/ext/quack/vendor/quack_protocol/src/vector.rs +1364 -0
- data/ext/quack/vendor/quack_protocol/testdata/tls/key.der +0 -0
- data/ext/quack/vendor/quack_protocol/testdata/tls/spki.der +0 -0
- data/ext/quack/vendor/quack_protocol/testdata/tls/v1-cert.der +0 -0
- data/ext/quack/vendor/quack_protocol/testdata/tls/v3-cert.der +0 -0
- data/lib/quack/codec.rb +263 -0
- data/lib/quack/connection.rb +131 -0
- data/lib/quack/logical_types.rb +72 -0
- data/lib/quack/result.rb +64 -0
- data/lib/quack/sql.rb +183 -0
- data/lib/quack/types.rb +40 -0
- data/lib/quack/variant.rb +298 -0
- data/lib/quack.rb +34 -0
- metadata +136 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "quack_protocol"
|
|
3
|
+
version = "0.4.1"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
rust-version = "1.85"
|
|
6
|
+
description = "Rust client SDK for DuckDB's experimental Quack remote protocol"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
documentation = "https://docs.rs/quack_protocol"
|
|
10
|
+
keywords = ["duckdb", "quack", "database", "client"]
|
|
11
|
+
categories = ["api-bindings", "database"]
|
|
12
|
+
|
|
13
|
+
[features]
|
|
14
|
+
arrow = ["dep:arrow-array", "dep:arrow-schema", "dep:arrow-buffer"]
|
|
15
|
+
|
|
16
|
+
[dependencies]
|
|
17
|
+
arrow-array = { version = "59", optional = true, default-features = false }
|
|
18
|
+
arrow-buffer = { version = "59", optional = true, default-features = false }
|
|
19
|
+
arrow-schema = { version = "59", optional = true, default-features = false }
|
|
20
|
+
async-stream = "0.3"
|
|
21
|
+
base64 = "0.22"
|
|
22
|
+
futures-core = "0.3"
|
|
23
|
+
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
|
|
24
|
+
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
|
|
25
|
+
hex = "0.4"
|
|
26
|
+
indexmap = "2"
|
|
27
|
+
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "http2"] }
|
|
28
|
+
ring = "0.17"
|
|
29
|
+
# Must stay on the rustls line reqwest uses: `use_preconfigured_tls` only
|
|
30
|
+
# accepts a `ClientConfig` from that version.
|
|
31
|
+
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
|
32
|
+
serde_json = "1"
|
|
33
|
+
thiserror = "2"
|
|
34
|
+
tokio = { version = "1", default-features = false, features = ["rt", "sync", "time"] }
|
|
35
|
+
tracing = "0.1"
|
|
36
|
+
url = "2"
|
|
37
|
+
x509-parser = { version = "0.18", default-features = false }
|
|
38
|
+
|
|
39
|
+
[dev-dependencies]
|
|
40
|
+
tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread", "time"] }
|
|
41
|
+
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) the quack_protocol contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Packaged protocol fixes
|
|
2
|
+
|
|
3
|
+
Source: bnjjj/quack_protocol_rs 0.4.1, commit
|
|
4
|
+
ee5cf21e3450b0651dd0fe9ed94e455c3b3b66b3.
|
|
5
|
+
|
|
6
|
+
The Ruby source gem includes this MIT-licensed crate so codec fixes are present
|
|
7
|
+
when users build it without a separate unpublished dependency.
|
|
8
|
+
|
|
9
|
+
- Decode BIGNUM's binary payload as bytes, rather than UTF-8. The Ruby bridge
|
|
10
|
+
converts its sign/magnitude encoding to an arbitrary-precision Integer.
|
|
11
|
+
- Recognize DuckDB 2.0 TIMESTAMPTZ_NS (logical ID 33) and TUPLE (ID 110).
|
|
12
|
+
- Keep unnamed STRUCT/TUPLE fields by ordinal, rather than overwriting every
|
|
13
|
+
empty-name field in an IndexMap. Encoding and append normalization use the
|
|
14
|
+
same ordinal keys. UNION's empty-name tag field remains unchanged.
|
|
15
|
+
|
|
16
|
+
Real protocol integration coverage lives in the Ruby test suite.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# quack_protocol
|
|
2
|
+
|
|
3
|
+
Rust client-side SDK for DuckDB's experimental Quack remote protocol.
|
|
4
|
+
|
|
5
|
+
The `0.3.0` line supports both Quack protocol v1 and protocol v3 from
|
|
6
|
+
DuckDB 2.0 alpha:
|
|
7
|
+
|
|
8
|
+
- DuckDB `BinarySerializer`-compatible primitive, object, logical type, vector, and `DataChunk` codecs.
|
|
9
|
+
- Quack connection leases and heartbeats, prepare/query, acknowledged fetch streaming,
|
|
10
|
+
disconnect, success, and error messages.
|
|
11
|
+
- A session pool for running queries concurrently against one server.
|
|
12
|
+
- Async HTTP `POST /quack` transport using `application/duckdb`.
|
|
13
|
+
- URI parsing for `localhost:9494`, `quack:host:port`, bracketed IPv6, and direct HTTP(S) URLs.
|
|
14
|
+
- SQL literal formatting for positional and named parameters.
|
|
15
|
+
|
|
16
|
+
```rust
|
|
17
|
+
use futures_util::TryStreamExt;
|
|
18
|
+
use quack_protocol::{QuackClient, QuackClientOptions, Result};
|
|
19
|
+
|
|
20
|
+
#[tokio::main]
|
|
21
|
+
async fn main() -> Result<()> {
|
|
22
|
+
let client = QuackClient::connect(
|
|
23
|
+
"localhost:9494",
|
|
24
|
+
QuackClientOptions {
|
|
25
|
+
auth_token: Some("super_secret".to_string()),
|
|
26
|
+
..Default::default()
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
.await?;
|
|
30
|
+
|
|
31
|
+
let (_columns, rows) = client
|
|
32
|
+
.query("SELECT 42::INTEGER AS answer", None)
|
|
33
|
+
.await?
|
|
34
|
+
.into_rows();
|
|
35
|
+
let rows: Vec<_> = rows.try_collect().await?;
|
|
36
|
+
println!("{:?}", rows);
|
|
37
|
+
|
|
38
|
+
client.disconnect().await?;
|
|
39
|
+
Ok(())
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Connection model
|
|
44
|
+
|
|
45
|
+
A `QuackClient` is **one session** on the server: one connection id, for the
|
|
46
|
+
client's whole life and for every clone of it. Two things follow.
|
|
47
|
+
|
|
48
|
+
**Queries on one client are serialized.** The server keeps a single resumable
|
|
49
|
+
result cursor per connection id, and a PREPARE resets it - so a second query
|
|
50
|
+
would invalidate a FETCH still in flight. Queries therefore take a lock, and a
|
|
51
|
+
result stream holds it until the stream is drained or dropped. Cloning the
|
|
52
|
+
client does not change that; the clones share the session.
|
|
53
|
+
|
|
54
|
+
**Session state persists.** Temporary tables, `SET`, transactions, and attached
|
|
55
|
+
databases live on the connection, so consecutive calls on one client see each
|
|
56
|
+
other's effects.
|
|
57
|
+
|
|
58
|
+
For concurrency, use a `QuackPool`. It keeps several sessions and hands a free
|
|
59
|
+
one to each caller, so N queries run on the server at once - what a query engine
|
|
60
|
+
wants when it opens one scan per partition:
|
|
61
|
+
|
|
62
|
+
```rust
|
|
63
|
+
use quack_protocol::{QuackClientOptions, QuackPool, QuackPoolOptions, Result};
|
|
64
|
+
|
|
65
|
+
#[tokio::main]
|
|
66
|
+
async fn main() -> Result<()> {
|
|
67
|
+
let pool = QuackPool::connect(
|
|
68
|
+
"localhost:9494",
|
|
69
|
+
QuackClientOptions::default(),
|
|
70
|
+
// Size this to the parallelism you want - DataFusion's
|
|
71
|
+
// `target_partitions`, say. Every connection is a DuckDB connection on
|
|
72
|
+
// the server.
|
|
73
|
+
QuackPoolOptions { max_connections: 8 },
|
|
74
|
+
)
|
|
75
|
+
.await?;
|
|
76
|
+
|
|
77
|
+
// Each concurrent query runs on its own session.
|
|
78
|
+
let (_columns, chunks) = pool.query("SELECT 42::INTEGER AS answer", None).await?.into_chunks();
|
|
79
|
+
let _ = chunks;
|
|
80
|
+
Ok(())
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`QuackPool` is cheap to clone and shares its connections, so one pool can back
|
|
85
|
+
every table in a federated catalog. Connections open on demand up to
|
|
86
|
+
`max_connections`; callers past the limit wait for one to come free.
|
|
87
|
+
|
|
88
|
+
The pool gives **no session affinity** between calls: two `pool.query()` calls
|
|
89
|
+
may land on different sessions, so a temp table created by one is invisible to
|
|
90
|
+
the other. Work that depends on session state takes a connection out of the pool
|
|
91
|
+
and holds it:
|
|
92
|
+
|
|
93
|
+
```rust,ignore
|
|
94
|
+
let connection = pool.acquire().await?;
|
|
95
|
+
connection.execute("CREATE TEMP TABLE staging AS SELECT 1 AS id", None).await?;
|
|
96
|
+
let ids = connection.values("SELECT id FROM staging").await?;
|
|
97
|
+
// `connection` goes back to the pool when it is dropped.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`QuackClientOptions` redacts `auth_token` in its `Debug` output. Extra HTTP
|
|
101
|
+
headers go in `QuackClientOptions::headers` as name/value pairs; an invalid
|
|
102
|
+
name or value is reported when the client connects.
|
|
103
|
+
|
|
104
|
+
A stream returned by `pool.query()` holds its connection until it is drained or
|
|
105
|
+
dropped, so drop it when the results are no longer wanted rather than leaving it
|
|
106
|
+
parked - the connection is not free until then.
|
|
107
|
+
|
|
108
|
+
### TLS with a self-signed server certificate
|
|
109
|
+
|
|
110
|
+
`quack_serve` uses HTTPS unless it is bound to localhost, and by default serves a
|
|
111
|
+
self-signed certificate. A self-signed certificate does not verify against any
|
|
112
|
+
CA, so, as DuckDB's own client does, authenticate the server by pinning the
|
|
113
|
+
certificate's SHA-256 fingerprint - the one `quack_generate_keys()` returns, or
|
|
114
|
+
`openssl x509 -in server.pem -noout -fingerprint -sha256` prints:
|
|
115
|
+
|
|
116
|
+
```rust,ignore
|
|
117
|
+
let client = QuackClient::connect(
|
|
118
|
+
"quack:myhost:9494",
|
|
119
|
+
QuackClientOptions {
|
|
120
|
+
ssl_fingerprint: Some("2B:31:F0:...:EA".to_string()),
|
|
121
|
+
..Default::default()
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
.await?;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A pinned client trusts exactly that certificate and skips the CA and hostname
|
|
128
|
+
checks; the TLS handshake is still verified against the certificate's key. A pin
|
|
129
|
+
implies HTTPS, and pinned connections use TLS 1.3. Without a pin, HTTPS
|
|
130
|
+
connections verify the server against the standard web PKI roots.
|
|
131
|
+
|
|
132
|
+
### Closing sessions
|
|
133
|
+
|
|
134
|
+
A session the client no longer holds is closed, so the server does not keep a
|
|
135
|
+
cursor for a connection id nobody can use again:
|
|
136
|
+
|
|
137
|
+
- `disconnect()` (on a client, `close()` on a pool) closes deterministically and
|
|
138
|
+
reports failure.
|
|
139
|
+
- Dropping the last handle to a client - or the pool that holds it - sends the
|
|
140
|
+
DISCONNECT from a background task. That is best-effort: it only goes out while
|
|
141
|
+
the Tokio runtime is alive, so call `disconnect()`/`close()` when the program
|
|
142
|
+
is about to exit or the outcome matters.
|
|
143
|
+
|
|
144
|
+
### When the server forgets a connection
|
|
145
|
+
|
|
146
|
+
After a server restart, a session opened against the old server is gone and the
|
|
147
|
+
server answers `Invalid connection id`. Current Quack protocol versions carry
|
|
148
|
+
only the error text, however, and SQL can raise exactly the same message after
|
|
149
|
+
an earlier statement committed. The client therefore never retries a statement
|
|
150
|
+
automatically.
|
|
151
|
+
|
|
152
|
+
The pool retires a session when the response exactly matches a known stale
|
|
153
|
+
connection message, but returns the error to the caller. A later pool call opens
|
|
154
|
+
a fresh session within `max_connections`. A plain `QuackClient` does not
|
|
155
|
+
reconnect; it is one session, and quietly opening another would drop the session
|
|
156
|
+
state that came with it.
|
|
157
|
+
|
|
158
|
+
## Arrow output
|
|
159
|
+
|
|
160
|
+
The optional `arrow` feature adds a read-only bridge that turns query results
|
|
161
|
+
into `arrow_array::RecordBatch` streams. The Arrow schema is built from the
|
|
162
|
+
column definitions returned at prepare time, so it is correct even for results
|
|
163
|
+
that carry no rows.
|
|
164
|
+
|
|
165
|
+
```toml
|
|
166
|
+
[dependencies]
|
|
167
|
+
quack_protocol = { version = "0.2", features = ["arrow"] }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```rust
|
|
171
|
+
use futures_util::TryStreamExt;
|
|
172
|
+
use quack_protocol::arrow::arrow_array::RecordBatch;
|
|
173
|
+
use quack_protocol::{QuackClient, QuackClientOptions, Result};
|
|
174
|
+
|
|
175
|
+
#[tokio::main]
|
|
176
|
+
async fn main() -> Result<()> {
|
|
177
|
+
let client = QuackClient::connect("localhost:9494", QuackClientOptions::default()).await?;
|
|
178
|
+
|
|
179
|
+
let (schema, batches) = client
|
|
180
|
+
.query("SELECT 42::INTEGER AS answer", None)
|
|
181
|
+
.await?
|
|
182
|
+
.into_record_batches()?;
|
|
183
|
+
let batches: Vec<RecordBatch> = batches.try_collect().await?;
|
|
184
|
+
println!("{schema}: {} rows", batches.iter().map(RecordBatch::num_rows).sum::<usize>());
|
|
185
|
+
|
|
186
|
+
client.disconnect().await?;
|
|
187
|
+
Ok(())
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`into_record_batches()` is the whole entry point. `quack_protocol::arrow`
|
|
192
|
+
otherwise only re-exports the `arrow-array`, `arrow-buffer`, and `arrow-schema`
|
|
193
|
+
crates, so downstream code can match versions.
|
|
194
|
+
|
|
195
|
+
The batch stream holds the client's connection until it is drained or dropped,
|
|
196
|
+
as `into_chunks()` and `into_rows()` do: a `QuackClient` runs one query at a
|
|
197
|
+
time, and a second query on the same client waits for the first stream to
|
|
198
|
+
finish. `QuackPool::query` returns the same `QuackResultStream`, so
|
|
199
|
+
`into_record_batches()` works there too - and concurrent scans then run on
|
|
200
|
+
separate sessions. See [Connection model](#connection-model).
|
|
201
|
+
|
|
202
|
+
### Type mapping
|
|
203
|
+
|
|
204
|
+
| DuckDB | Arrow |
|
|
205
|
+
|---|---|
|
|
206
|
+
| `NULL` | `Null` |
|
|
207
|
+
| `BOOLEAN` | `Boolean` |
|
|
208
|
+
| `TINYINT` … `BIGINT` | `Int8` … `Int64` |
|
|
209
|
+
| `UTINYINT` … `UBIGINT` | `UInt8` … `UInt64` |
|
|
210
|
+
| `HUGEINT`, `UHUGEINT` | `Decimal256(39, 0)` |
|
|
211
|
+
| `FLOAT`, `DOUBLE` | `Float32`, `Float64` |
|
|
212
|
+
| `DECIMAL(w, s)` | `Decimal128(w, s)` |
|
|
213
|
+
| `VARCHAR`, `CHAR`, `ENUM`, `UUID` | `Utf8` |
|
|
214
|
+
| `BLOB`, `GEOMETRY`, `BIT` | `Binary` |
|
|
215
|
+
| `DATE` | `Date32` |
|
|
216
|
+
| `TIME`, `TIME_NS` | `Time64(Microsecond)`, `Time64(Nanosecond)` |
|
|
217
|
+
| `TIMESTAMP_S`, `TIMESTAMP_MS`, `TIMESTAMP`, `TIMESTAMP_NS` | `Timestamp(<unit>, None)` |
|
|
218
|
+
| `TIMESTAMPTZ` | `Timestamp(Microsecond, Some("UTC"))` |
|
|
219
|
+
| `INTERVAL` | `Interval(MonthDayNano)` |
|
|
220
|
+
| `LIST`, `MAP` | `List(child)` |
|
|
221
|
+
| `ARRAY(n)` | `FixedSizeList(child, n)` |
|
|
222
|
+
| `STRUCT` | `Struct(children)` |
|
|
223
|
+
| `VARIANT` | `Struct(keys, children, values, data)` |
|
|
224
|
+
|
|
225
|
+
Every field is nullable; the wire carries no non-null guarantee. A `NULL` list,
|
|
226
|
+
array, or struct is a null container, not an empty one. `TIME` values must lie
|
|
227
|
+
within one day, as Arrow requires, so DuckDB's `TIME '24:00:00'` is rejected.
|
|
228
|
+
`TIMETZ` and `UNION` have no Arrow mapping yet and produce
|
|
229
|
+
`QuackError::UnsupportedType`.
|
|
230
|
+
|
|
231
|
+
Three mappings are provisional and may change in a future release: `MAP` is
|
|
232
|
+
encoded as `List<Struct<key, value>>` rather than Arrow's native `Map`, `ENUM`
|
|
233
|
+
as plain `Utf8` rather than a `Dictionary`, and `VARIANT` as the struct DuckDB
|
|
234
|
+
shreds it into rather than Arrow's canonical `arrow.variant` extension, whose
|
|
235
|
+
binary encoding is not the one DuckDB sends. A `VARIANT` column therefore
|
|
236
|
+
arrives as its physical layout — `keys`, `children`, `values`, and a `data`
|
|
237
|
+
blob — the same shape the `Value` path already exposes, and reading a value
|
|
238
|
+
back out of it means interpreting that encoding yourself.
|
|
239
|
+
|
|
240
|
+
Quack is still experimental upstream and not yet covered by a stable official wire spec. This implementation follows DuckDB's `duckdb-quack` extension.
|
|
241
|
+
|
|
242
|
+
## Compatibility
|
|
243
|
+
|
|
244
|
+
The client prefers protocol v3 and retries with the separate v1 codec when an
|
|
245
|
+
older server rejects the v3 handshake. `QuackConnectionInfo::quack_version`
|
|
246
|
+
reports the selected version. Set `min_supported_quack_version` and
|
|
247
|
+
`max_supported_quack_version` to the same value to require one version.
|
|
248
|
+
|
|
249
|
+
Protocol v1 retains its legacy prepare, fetch, and append payloads. Protocol v3
|
|
250
|
+
uses query UUIDs, acknowledged fetch batches, raw chunk payloads, and connection
|
|
251
|
+
heartbeats. The v3 send-data write path is not implemented yet, so `append` and
|
|
252
|
+
`append_rows` return an explicit unsupported-protocol error on a v3 connection;
|
|
253
|
+
they continue to work on v1 connections.
|
|
254
|
+
|
|
255
|
+
## Admission-aware proxies
|
|
256
|
+
|
|
257
|
+
`inspection::inspect_message_header` reads only a bounded Quack header and
|
|
258
|
+
distinguishes incomplete, invalid, and oversized input. It returns an
|
|
259
|
+
`inspection::MessageHeader` containing the operation, connection ID, query ID, and
|
|
260
|
+
encoded header length without decoding SQL, authentication, or result data.
|
|
261
|
+
Callers must cap collection at `MAX_MESSAGE_HEADER_BYTES` and replay the inspected
|
|
262
|
+
bytes.
|
|
263
|
+
`inspect_control_response` accepts only bounded complete connect, success, and
|
|
264
|
+
error responses, including DuckDB's structured invalidation flag. Fatality is a
|
|
265
|
+
conservative retirement signal and never authorizes replay.
|
|
266
|
+
|
|
267
|
+
The client decodes recognized structured proxy HTTP failures as
|
|
268
|
+
`QuackError::Proxy(ProxyError)`. This is a new error variant: exhaustive error
|
|
269
|
+
matches must handle it. Unknown or oversized failures remain generic protocol
|
|
270
|
+
errors; raw proxy messages are discarded. Supply assignment and lifecycle headers
|
|
271
|
+
through `QuackClientOptions::headers`; the transport sends them on every request,
|
|
272
|
+
including heartbeat and disconnect. Debug output redacts these headers.
|