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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/LICENSE-DuckDB +7 -0
  4. data/LICENSE-QuackProtocol +21 -0
  5. data/LICENSE-Rust-dependencies.json +2057 -0
  6. data/README.md +229 -0
  7. data/ext/quack/Cargo.lock +1803 -0
  8. data/ext/quack/Cargo.toml +17 -0
  9. data/ext/quack/extconf.rb +4 -0
  10. data/ext/quack/src/lib.rs +647 -0
  11. data/ext/quack/vendor/quack_protocol/Cargo.lock +1907 -0
  12. data/ext/quack/vendor/quack_protocol/Cargo.toml +41 -0
  13. data/ext/quack/vendor/quack_protocol/LICENSE +21 -0
  14. data/ext/quack/vendor/quack_protocol/PATCHES.md +16 -0
  15. data/ext/quack/vendor/quack_protocol/README.md +272 -0
  16. data/ext/quack/vendor/quack_protocol/src/arrow/columns.rs +399 -0
  17. data/ext/quack/vendor/quack_protocol/src/arrow/mod.rs +134 -0
  18. data/ext/quack/vendor/quack_protocol/src/arrow/schema.rs +103 -0
  19. data/ext/quack/vendor/quack_protocol/src/arrow/tests.rs +851 -0
  20. data/ext/quack/vendor/quack_protocol/src/arrow/values.rs +150 -0
  21. data/ext/quack/vendor/quack_protocol/src/binary.rs +504 -0
  22. data/ext/quack/vendor/quack_protocol/src/builders.rs +217 -0
  23. data/ext/quack/vendor/quack_protocol/src/client.rs +1486 -0
  24. data/ext/quack/vendor/quack_protocol/src/constants.rs +11 -0
  25. data/ext/quack/vendor/quack_protocol/src/errors.rs +103 -0
  26. data/ext/quack/vendor/quack_protocol/src/inspection.rs +250 -0
  27. data/ext/quack/vendor/quack_protocol/src/json.rs +225 -0
  28. data/ext/quack/vendor/quack_protocol/src/lib.rs +54 -0
  29. data/ext/quack/vendor/quack_protocol/src/logical_types.rs +977 -0
  30. data/ext/quack/vendor/quack_protocol/src/messages.rs +843 -0
  31. data/ext/quack/vendor/quack_protocol/src/pool.rs +741 -0
  32. data/ext/quack/vendor/quack_protocol/src/protocol_tests.rs +759 -0
  33. data/ext/quack/vendor/quack_protocol/src/proxy_error.rs +77 -0
  34. data/ext/quack/vendor/quack_protocol/src/sql.rs +359 -0
  35. data/ext/quack/vendor/quack_protocol/src/tls.rs +354 -0
  36. data/ext/quack/vendor/quack_protocol/src/values.rs +77 -0
  37. data/ext/quack/vendor/quack_protocol/src/vector.rs +1364 -0
  38. data/ext/quack/vendor/quack_protocol/testdata/tls/key.der +0 -0
  39. data/ext/quack/vendor/quack_protocol/testdata/tls/spki.der +0 -0
  40. data/ext/quack/vendor/quack_protocol/testdata/tls/v1-cert.der +0 -0
  41. data/ext/quack/vendor/quack_protocol/testdata/tls/v3-cert.der +0 -0
  42. data/lib/quack/codec.rb +263 -0
  43. data/lib/quack/connection.rb +131 -0
  44. data/lib/quack/logical_types.rb +72 -0
  45. data/lib/quack/result.rb +64 -0
  46. data/lib/quack/sql.rb +183 -0
  47. data/lib/quack/types.rb +40 -0
  48. data/lib/quack/variant.rb +298 -0
  49. data/lib/quack.rb +34 -0
  50. metadata +136 -0
data/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # quack-ruby
2
+
3
+ A direct Ruby client for DuckDB's Quack remote protocol. It uses Magnus and a
4
+ packaged, patched [quack_protocol 0.4.1](https://github.com/bnjjj/quack_protocol_rs)
5
+ over HTTP(S). The client does not embed DuckDB or use ADBC.
6
+
7
+ Supports DuckDB 1.5.5 / protocol v1 and DuckDB 2.0.0 / protocol v3. For Rails,
8
+ use [activerecord-quack-adapter](https://github.com/sidequery/activerecord-quack-adapter).
9
+
10
+ ## Install
11
+
12
+ Add to your Gemfile:
13
+
14
+ ```ruby
15
+ gem "quack-ruby", "~> 0.1.0"
16
+ ```
17
+
18
+ Precompiled gems cover Ruby 3.2, 3.3 and 3.4 on macOS (Apple Silicon and Intel)
19
+ and Linux glibc (ARM64 and x86-64). Bundler selects the matching platform gem;
20
+ the Quack extension installs without running Cargo or a compiler. The package
21
+ is named `quack-ruby`; its Ruby entry point remains `require "quack"`.
22
+
23
+ Linux binaries target glibc, not Alpine/musl. A source gem is also available
24
+ for other platforms and Ruby versions. Building that extension requires Rust,
25
+ a C toolchain, Ruby development headers and libclang. Installing directly from
26
+ Git also builds from source. To select the source package explicitly, use
27
+ `gem install quack-ruby --platform ruby`.
28
+
29
+ For a checkout:
30
+
31
+ ```sh
32
+ bundle install
33
+ CARGO="$PWD/script/cached-cargo" bundle exec rake compile
34
+ ```
35
+
36
+ A Quack server must already be running. Provision databases on that server.
37
+ The client initializes its session timezone to UTC, also after reconnect.
38
+
39
+ ## Usage
40
+
41
+ ```ruby
42
+ require "quack"
43
+
44
+ Quack.connect(ENV.fetch("QUACK_URI"), auth_token: ENV.fetch("QUACK_TOKEN"), timeout: 30) do |connection|
45
+ result = connection.query("select 42 as answer")
46
+ end
47
+ ```
48
+
49
+ For a parameterized query, choose positional or named parameters:
50
+
51
+ ```ruby
52
+ connection = Quack.connect(ENV.fetch("QUACK_URI"), auth_token: ENV.fetch("QUACK_TOKEN"))
53
+ begin
54
+ result = connection.query("select ?::bigint as answer", [42])
55
+ result.columns # => ["answer"]
56
+ result.rows # => [[42]]
57
+ result.to_a # => [{"answer" => 42}]
58
+
59
+ connection.first("select :answer::bigint as answer", answer: 42)
60
+ connection.one("select 42 as answer") # Requires exactly one row.
61
+ connection.values("select 42") # => [42]
62
+ connection.execute("create temp table scratch (id integer)")
63
+
64
+ connection.stream("select i from range(1000000) t(i)") do |stream|
65
+ stream.each { |row| puts row.fetch("i") }
66
+ # each_row yields arrays; each_batch yields arrays of row arrays.
67
+ end
68
+
69
+ statement = connection.prepare("select ?::bigint as answer")
70
+ statement.query([42])
71
+ connection.transaction { |c| c.execute("insert into scratch values (?)", [1]) }
72
+ connection.info # Negotiated protocol version and server metadata.
73
+ ensure
74
+ connection.close
75
+ end
76
+ ```
77
+
78
+ Parameters/templates use client-side SQL substitution, not server-side bind
79
+ messages or a server statement cache. Quoting supports Unicode, binary strings,
80
+ NUL text, exact decimals/integers, temporal values, lists and structs. The lexer
81
+ preserves SQL strings, quoted identifiers, escaped strings, dollar-quoted
82
+ strings and nested comments. Identifiers must be quoted separately with
83
+ `Quack::SQL.identifier`.
84
+
85
+ `query` materializes all rows. `stream` fetches chunks on demand; block iteration
86
+ closes results even on early exit or exceptions. When keeping an unconsumed
87
+ stream, close it explicitly. A session permits one active result; an overlapping
88
+ operation raises `Quack::BusyError` rather than waiting indefinitely. Use a
89
+ connection per concurrent worker or an ordinary Ruby connection pool.
90
+ Duplicate result column names are preserved in `rows`; hash enumeration keeps
91
+ the last value for a repeated name.
92
+
93
+ Result conversions:
94
+
95
+ | DuckDB value | Ruby value |
96
+ | --- | --- |
97
+ | Signed/unsigned integers, including 128-bit and BIGNUM | `Integer`, without precision loss |
98
+ | Decimal | `BigDecimal` |
99
+ | Boolean, floating point, text, NULL | `true`/`false`, `Float`, `String`, `nil` |
100
+ | BLOB / other byte payloads | Binary-encoded `String` |
101
+ | Date | Proleptic Gregorian `Date` |
102
+ | Timestamp | `Quack::Timestamp < Time`, retaining SQL units, timezone kind and nanoseconds |
103
+ | Time / time with offset | `Quack::TimeOfDay` (supports `24:00:00`) |
104
+ | Interval | `Quack::Interval(months, days, microseconds)` |
105
+ | Infinite date/timestamp | `Quack::InfiniteTemporal` |
106
+ | List/array, struct | `Array`, string-keyed `Hash` |
107
+ | JSON, UUID, ENUM | `String` (Rails casts JSON to Ruby objects) |
108
+ | MAP | `Quack::Map`, with `entries`, `[]`, `each`, `to_h` |
109
+ | BIT | `Quack::BitString(bits)`, a string of logical 0/1 bits |
110
+ | GEOMETRY | `Quack::Geometry(wkb, crs)`, standard WKB and optional CRS |
111
+ | UNION | `Quack::Union(tag, value, members)`; tag distinguishes a NULL member from SQL NULL |
112
+ | VARIANT | `Quack::Variant`, with a typed `node` tree and logical Ruby `value` |
113
+ | TUPLE / unnamed STRUCT | `Quack::Tuple(values)`, preserving every ordinal field |
114
+
115
+ These logical values support SQL parameters, model assignment and native v1
116
+ append, including values nested in lists, arrays and structs. BIGNUM decoding,
117
+ DuckDB 2.0 type IDs and unnamed field handling are fixed in the packaged crate;
118
+ no separate fork download is needed to build the gem.
119
+
120
+ Read timestamps retain their SQL kind when rebound. Construct one explicitly
121
+ with `Quack::Timestamp.from(time, "TIMESTAMP_NS")` or, on DuckDB 2.0,
122
+ `Quack::Timestamp.from(time, "TIMESTAMPTZ_NS")`. Plain `Time`/`DateTime` values
123
+ use TIMESTAMPTZ when microseconds suffice and TIMESTAMP_NS when nanoseconds are
124
+ needed. Connections initialize `TimeZone = 'UTC'`, including after reconnect.
125
+ Subsequent explicit timezone changes follow DuckDB's normal cast semantics.
126
+ Values beyond a destination's precision are rejected by native append rather
127
+ than silently truncated. Rails writes preserve nine fractional digits.
128
+
129
+ `Quack::Variant.new({"items" => [1, nil, "é"]})` builds a heterogeneous value.
130
+ Use `Quack::Variant.new(value, type: "UBIGINT")` to specify a scalar type.
131
+ `variant.value` returns a Ruby tree; `variant.node` preserves each nested scalar
132
+ type. Assign a new Variant to edit it. DuckDB normalizes a root VARIANT null to
133
+ SQL NULL (`nil`); nulls inside containers remain nested nodes. Geometry uses
134
+ standard WKB rather than a client geometry engine, preserving Z/M, empty values,
135
+ collections and CRS metadata.
136
+
137
+ All storable public type families are exercised on the supported server versions.
138
+ DuckDB 1.5 cannot persist unnamed STRUCT/ROW tuples (even regular SQL INSERT fails);
139
+ their query/parameter roundtrips work, and tuple persistence is tested on 2.0.
140
+ TIMESTAMPTZ_NS is a DuckDB 2.0 type and is unavailable on 1.5. Internal/binder-only
141
+ types and types supplied by optional server extensions are not universal targets.
142
+
143
+ For protocol v1 only, the native append path is exposed:
144
+
145
+ ```ruby
146
+ connection.append_rows("events", [{id: 1, name: "signup"}],
147
+ columns: {id: "BIGINT", name: "VARCHAR"}, batch_size: 1024)
148
+ ```
149
+
150
+ Append resolves destination metadata from the server, including nested types,
151
+ all-null columns, empty lists, ENUM dictionaries, case-sensitive field names and
152
+ array sizes. Optional `columns` accepts full DuckDB SQL type declarations and
153
+ must name every destination column; hash order does not change column placement.
154
+ Rows must have exactly those names. Values are checked against destination
155
+ integer ranges, decimal scale/precision and temporal units before sending any
156
+ batch. Native SEND_DATA carries the values; append does not fall back to INSERT.
157
+ Schema-qualified table names and a `schema:` option are supported in the current
158
+ catalog. Select another catalog with `USE` before appending.
159
+ Append batches are separate operations; wrap them in a transaction if atomicity
160
+ is required.
161
+
162
+ ## Lifecycle and protocol limits
163
+
164
+ - v3 is preferred, with v1 fallback on a rejected handshake. Require v3 by
165
+ setting both protocol-version bounds to 3.
166
+ - Native v3 SEND_DATA is unavailable in the underlying transport. `append_rows`
167
+ raises `Quack::UnsupportedTypeError` on v3; SQL INSERT works on both versions.
168
+ - Sessions retain temp tables, settings and transactions. Statements are never
169
+ replayed after a failure. Reconnect explicitly; session state is lost.
170
+ - Native network waits release the GVL but block the calling thread. They do
171
+ not integrate with a Fiber scheduler. HTTP timeouts bound waits; there is no
172
+ server-side cancellation API. Thread interrupts arrive when a call returns.
173
+ - Connections cannot cross forks. Create them after forking, or explicitly
174
+ reconnect in the child. Close explicitly for deterministic cleanup; GC
175
+ performs a bounded best-effort disconnect.
176
+ - One session permits one active result. Close or consume it before another
177
+ operation. Use a connection per concurrent worker.
178
+ - Nested transactions/savepoints are unavailable. Append batches are separate
179
+ operations; wrap them in a transaction when atomicity is needed.
180
+ - Errors inherit from `Quack::Error`: ConnectionError, TimeoutError, ServerError,
181
+ ProtocolError, UnsupportedTypeError, ProxyError, BusyError and ForkError.
182
+
183
+ ## Development
184
+
185
+ ```sh
186
+ CARGO="$PWD/script/cached-cargo" bundle exec rake compile
187
+ script/test
188
+ cargo fmt --manifest-path ext/quack/Cargo.toml --check
189
+ cargo fmt --manifest-path ext/quack/vendor/quack_protocol/Cargo.toml --check
190
+ mbx clippy --manifest-path ext/quack/Cargo.toml -- -D warnings
191
+ bundle exec rake package
192
+ ```
193
+
194
+ `script/test` uses uv to start disposable DuckDB 1.5.5 and 2.0.0 servers, run
195
+ the integration suite and stop them. Use `DUCKDB_TEST_VERSIONS=2.0.0 script/test`
196
+ for one server version. `bundle exec rake test` without a server runs local
197
+ checks and explicitly skips integration cases.
198
+
199
+ Tests cover all storable public type families, nested logical values, exact
200
+ integers/decimals, nonfinite floats and negative zero, timestamp units/TZ,
201
+ geometry dimensions/CRS, typed NULLs, empty results, parameter rebinding,
202
+ streaming, auth/TLS, timeouts, GVL release, fork/reconnect and native v1 append.
203
+ The append matrix includes batches, all-null/empty nested values, column
204
+ ordering and rejection of invalid values before any batch is written. VARIANT
205
+ leaf families also run through SQL and native v1 append. Tests distinguish
206
+ server-version restrictions from client support.
207
+
208
+ Development CI covers Linux/macOS and Ruby 3.2, 3.3 and 3.4 using Boxington.
209
+ The `Native gems` workflow builds fresh release binaries with rb-sys-dock,
210
+ packages all three Ruby ABIs into each platform gem, and runs the live suite
211
+ on all twelve OS/architecture/Ruby combinations with build tools blocked.
212
+ Release builds do not restore or publish compiler caches.
213
+
214
+ To release, dispatch `Release` from main and choose a patch, minor or major
215
+ bump. It updates the Ruby/Rust versions and locks, commits and tags the release,
216
+ builds all five packages, and runs the twelve installed-binary test combinations.
217
+ Only after every check passes does it publish those exact artifacts through
218
+ RubyGems Trusted Publishing and create a GitHub Release with the gem assets.
219
+ An optional `release_tag` publishes an existing tag without another bump; its
220
+ source version must match the tag. Source and binary gems use the same version.
221
+
222
+ ## License
223
+
224
+ MIT. The vendored Rust crate retains its own license and provenance in
225
+ `ext/quack/vendor/quack_protocol/`. The type codecs follow DuckDB's public
226
+ [BIGNUM](https://github.com/duckdb/duckdb/blob/v2.0.0/src/common/types/bignum.cpp),
227
+ [BIT](https://github.com/duckdb/duckdb/blob/v2.0.0/src/common/types/bit.cpp) and
228
+ [VARIANT](https://github.com/duckdb/duckdb/blob/v2.0.0/src/include/duckdb/common/types/variant_visitor.hpp)
229
+ formats; DuckDB's MIT notice is included in LICENSE-DuckDB.