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
@@ -0,0 +1,399 @@
1
+ //! Column builders: one Arrow array per `DataChunk` column.
2
+
3
+ use std::iter::zip;
4
+ use std::sync::Arc;
5
+
6
+ use arrow_array::builder::{BinaryBuilder, BooleanBuilder, PrimitiveBuilder, StringBuilder};
7
+ use arrow_array::types::{
8
+ Date32Type, Decimal128Type, Decimal256Type, Float32Type, Float64Type, Int8Type, Int16Type,
9
+ Int32Type, Int64Type, IntervalMonthDayNanoType, Time64MicrosecondType, Time64NanosecondType,
10
+ TimestampMicrosecondType, TimestampMillisecondType, TimestampNanosecondType,
11
+ TimestampSecondType, UInt8Type, UInt16Type, UInt32Type, UInt64Type,
12
+ };
13
+ use arrow_array::{
14
+ ArrayRef, ArrowPrimitiveType, BooleanArray, FixedSizeListArray, ListArray, NullArray,
15
+ PrimitiveArray, StructArray,
16
+ };
17
+ use arrow_buffer::{NullBuffer, OffsetBuffer};
18
+ use arrow_schema::{DataType, FieldRef, Fields};
19
+
20
+ use crate::errors::{QuackError, Result};
21
+ use crate::logical_types::{LogicalType, LogicalTypeId, get_child_type, get_struct_children};
22
+ use crate::vector::{TimeUnit, TimestampUnit, Value};
23
+
24
+ use super::schema::decimal_precision_and_scale;
25
+ use super::values::{
26
+ as_bool, as_bytes, as_date, as_decimal, as_double, as_float, as_huge_int, as_int, as_interval,
27
+ as_str, as_time, as_timestamp, as_uint, as_unsigned_huge_int, narrow_int, narrow_uint,
28
+ };
29
+ use super::{HUGE_INT_PRECISION, UTC_TIMEZONE, arrow_error, mismatch, schema_shape, unsupported};
30
+
31
+ const NULL_VALUE: Value = Value::Null;
32
+
33
+ /// Builds one column. `data_type` is the schema's type for the column, so
34
+ /// nested builders reuse the schema's own field objects instead of rebuilding
35
+ /// an identical field tree for every chunk.
36
+ pub(super) fn build_array(
37
+ logical_type: &LogicalType,
38
+ data_type: &DataType,
39
+ values: &[&Value],
40
+ ) -> Result<ArrayRef> {
41
+ let id = logical_type.id;
42
+ Ok(match id {
43
+ LogicalTypeId::SqlNull => {
44
+ // A SQLNULL column carries nothing but nulls; a decoded value in
45
+ // one would otherwise be dropped on the floor by `NullArray`.
46
+ if let Some(value) = values.iter().find(|value| !matches!(value, Value::Null)) {
47
+ return Err(mismatch(id, value));
48
+ }
49
+ Arc::new(NullArray::new(values.len()))
50
+ }
51
+ LogicalTypeId::Boolean => Arc::new(boolean(values, |value| as_bool(id, value))?),
52
+ LogicalTypeId::TinyInt => Arc::new(primitive::<Int8Type>(values, |value| {
53
+ narrow_int(id, value)
54
+ })?),
55
+ LogicalTypeId::SmallInt => Arc::new(primitive::<Int16Type>(values, |value| {
56
+ narrow_int(id, value)
57
+ })?),
58
+ LogicalTypeId::Integer => Arc::new(primitive::<Int32Type>(values, |value| {
59
+ narrow_int(id, value)
60
+ })?),
61
+ LogicalTypeId::BigInt => {
62
+ Arc::new(primitive::<Int64Type>(values, |value| as_int(id, value))?)
63
+ }
64
+ LogicalTypeId::UTinyInt => Arc::new(primitive::<UInt8Type>(values, |value| {
65
+ narrow_uint(id, value)
66
+ })?),
67
+ LogicalTypeId::USmallInt => Arc::new(primitive::<UInt16Type>(values, |value| {
68
+ narrow_uint(id, value)
69
+ })?),
70
+ LogicalTypeId::UInteger => Arc::new(primitive::<UInt32Type>(values, |value| {
71
+ narrow_uint(id, value)
72
+ })?),
73
+ LogicalTypeId::UBigInt => {
74
+ Arc::new(primitive::<UInt64Type>(values, |value| as_uint(id, value))?)
75
+ }
76
+ LogicalTypeId::HugeInt => Arc::new(
77
+ primitive::<Decimal256Type>(values, |value| as_huge_int(id, value))?
78
+ .with_precision_and_scale(HUGE_INT_PRECISION, 0)
79
+ .map_err(arrow_error)?,
80
+ ),
81
+ LogicalTypeId::UHugeInt => Arc::new(
82
+ primitive::<Decimal256Type>(values, |value| as_unsigned_huge_int(id, value))?
83
+ .with_precision_and_scale(HUGE_INT_PRECISION, 0)
84
+ .map_err(arrow_error)?,
85
+ ),
86
+ LogicalTypeId::Float => Arc::new(primitive::<Float32Type>(values, |value| {
87
+ as_float(id, value)
88
+ })?),
89
+ LogicalTypeId::Double => Arc::new(primitive::<Float64Type>(values, |value| {
90
+ as_double(id, value)
91
+ })?),
92
+ LogicalTypeId::Decimal => {
93
+ let (precision, scale) = decimal_precision_and_scale(logical_type)?;
94
+ Arc::new(
95
+ primitive::<Decimal128Type>(values, |value| {
96
+ as_decimal(id, value).map(|decimal| decimal.value)
97
+ })?
98
+ .with_precision_and_scale(precision, scale)
99
+ .map_err(arrow_error)?,
100
+ )
101
+ }
102
+ LogicalTypeId::Varchar
103
+ | LogicalTypeId::Char
104
+ | LogicalTypeId::Enum
105
+ | LogicalTypeId::Uuid => {
106
+ let strings = collect(values, |value| as_str(id, value))?;
107
+ let mut builder =
108
+ StringBuilder::with_capacity(strings.len(), byte_capacity(id, &strings)?);
109
+ for value in &strings {
110
+ builder.append_option(*value);
111
+ }
112
+ Arc::new(builder.finish())
113
+ }
114
+ LogicalTypeId::Blob | LogicalTypeId::Geometry | LogicalTypeId::Bit => {
115
+ let blobs = collect(values, |value| as_bytes(id, value))?;
116
+ let mut builder = BinaryBuilder::with_capacity(blobs.len(), byte_capacity(id, &blobs)?);
117
+ for value in &blobs {
118
+ builder.append_option(*value);
119
+ }
120
+ Arc::new(builder.finish())
121
+ }
122
+ LogicalTypeId::Date => {
123
+ Arc::new(primitive::<Date32Type>(values, |value| as_date(id, value))?)
124
+ }
125
+ LogicalTypeId::Time => Arc::new(primitive::<Time64MicrosecondType>(values, |value| {
126
+ as_time(id, value, TimeUnit::Micros)
127
+ })?),
128
+ LogicalTypeId::TimeNs => Arc::new(primitive::<Time64NanosecondType>(values, |value| {
129
+ as_time(id, value, TimeUnit::Nanos)
130
+ })?),
131
+ LogicalTypeId::TimestampSec => {
132
+ Arc::new(primitive::<TimestampSecondType>(values, |value| {
133
+ as_timestamp(id, value, TimestampUnit::Seconds, false)
134
+ })?)
135
+ }
136
+ LogicalTypeId::TimestampMs => {
137
+ Arc::new(primitive::<TimestampMillisecondType>(values, |value| {
138
+ as_timestamp(id, value, TimestampUnit::Millis, false)
139
+ })?)
140
+ }
141
+ LogicalTypeId::Timestamp => {
142
+ Arc::new(primitive::<TimestampMicrosecondType>(values, |value| {
143
+ as_timestamp(id, value, TimestampUnit::Micros, false)
144
+ })?)
145
+ }
146
+ LogicalTypeId::TimestampNs => {
147
+ Arc::new(primitive::<TimestampNanosecondType>(values, |value| {
148
+ as_timestamp(id, value, TimestampUnit::Nanos, false)
149
+ })?)
150
+ }
151
+ LogicalTypeId::TimestampTz => Arc::new(
152
+ primitive::<TimestampMicrosecondType>(values, |value| {
153
+ as_timestamp(id, value, TimestampUnit::Micros, true)
154
+ })?
155
+ .with_timezone(UTC_TIMEZONE),
156
+ ),
157
+ LogicalTypeId::Interval => {
158
+ Arc::new(primitive::<IntervalMonthDayNanoType>(values, |value| {
159
+ as_interval(id, value)
160
+ })?)
161
+ }
162
+ LogicalTypeId::List | LogicalTypeId::Map => {
163
+ build_list(logical_type, list_item(data_type)?, values)?
164
+ }
165
+ LogicalTypeId::Array => {
166
+ let (item, size) = fixed_size_list_item(data_type)?;
167
+ build_fixed_size_list(logical_type, item, size, values)?
168
+ }
169
+ LogicalTypeId::Struct | LogicalTypeId::Variant => {
170
+ build_struct(logical_type, struct_children(data_type)?, values)?
171
+ }
172
+ other => return Err(unsupported(other)),
173
+ })
174
+ }
175
+
176
+ fn build_list(logical_type: &LogicalType, item: &FieldRef, values: &[&Value]) -> Result<ArrayRef> {
177
+ // LIST and MAP share this builder, so the errors name the column's own type.
178
+ let id = logical_type.id;
179
+ let mut offsets = Vec::with_capacity(values.len() + 1);
180
+ let mut validity = Vec::with_capacity(values.len());
181
+ let mut items = Vec::with_capacity(values.len());
182
+ offsets.push(0i32);
183
+ for value in values {
184
+ match *value {
185
+ Value::Null => validity.push(false),
186
+ Value::List(entries) => {
187
+ items.extend(entries.iter());
188
+ validity.push(true);
189
+ }
190
+ other => return Err(mismatch(id, other)),
191
+ }
192
+ offsets.push(i32::try_from(items.len()).map_err(|_| {
193
+ QuackError::protocol(format!(
194
+ "{id:?} column exceeds the Arrow 32-bit offset limit"
195
+ ))
196
+ })?);
197
+ }
198
+ let child = build_array(get_child_type(logical_type)?, item.data_type(), &items)?;
199
+ Ok(Arc::new(
200
+ ListArray::try_new(
201
+ item.clone(),
202
+ OffsetBuffer::new(offsets.into()),
203
+ child,
204
+ Some(NullBuffer::from(validity)),
205
+ )
206
+ .map_err(arrow_error)?,
207
+ ))
208
+ }
209
+
210
+ fn build_fixed_size_list(
211
+ logical_type: &LogicalType,
212
+ item: &FieldRef,
213
+ size: i32,
214
+ values: &[&Value],
215
+ ) -> Result<ArrayRef> {
216
+ let width = usize::try_from(size)
217
+ .map_err(|_| QuackError::protocol(format!("Arrow schema declares ARRAY size {size}")))?;
218
+ let mut validity = Vec::with_capacity(values.len());
219
+ let mut items = Vec::with_capacity(values.len() * width);
220
+ for value in values {
221
+ match *value {
222
+ Value::Null => {
223
+ items.extend(std::iter::repeat_n(&NULL_VALUE, width));
224
+ validity.push(false);
225
+ }
226
+ Value::List(entries) => {
227
+ if entries.len() != width {
228
+ return Err(QuackError::protocol(format!(
229
+ "ARRAY value has {} entries, expected {width}",
230
+ entries.len()
231
+ )));
232
+ }
233
+ items.extend(entries.iter());
234
+ validity.push(true);
235
+ }
236
+ other => return Err(mismatch(logical_type.id, other)),
237
+ }
238
+ }
239
+ let child = build_array(get_child_type(logical_type)?, item.data_type(), &items)?;
240
+ Ok(Arc::new(
241
+ FixedSizeListArray::try_new(item.clone(), size, child, Some(NullBuffer::from(validity)))
242
+ .map_err(arrow_error)?,
243
+ ))
244
+ }
245
+
246
+ fn build_struct(
247
+ logical_type: &LogicalType,
248
+ fields: &Fields,
249
+ values: &[&Value],
250
+ ) -> Result<ArrayRef> {
251
+ // STRUCT and VARIANT share this builder, so the errors name the column's
252
+ // own type.
253
+ let id = logical_type.id;
254
+ let children = get_struct_children(logical_type)?;
255
+ if children.len() != fields.len() {
256
+ return Err(QuackError::protocol(format!(
257
+ "{id:?} has {} children but the Arrow schema declares {}",
258
+ children.len(),
259
+ fields.len()
260
+ )));
261
+ }
262
+ let mut validity = Vec::with_capacity(values.len());
263
+ for value in values {
264
+ match *value {
265
+ Value::Null => validity.push(false),
266
+ Value::Struct(row) => {
267
+ // Field names are unique, so matching the arity here and each
268
+ // declared name below together reject a struct carrying an
269
+ // undeclared field.
270
+ if row.len() != children.len() {
271
+ return Err(QuackError::protocol(format!(
272
+ "{id:?} value has {} fields but the type declares {}",
273
+ row.len(),
274
+ children.len()
275
+ )));
276
+ }
277
+ validity.push(true);
278
+ }
279
+ other => return Err(mismatch(id, other)),
280
+ }
281
+ }
282
+ let mut arrays = Vec::with_capacity(children.len());
283
+ for (child_index, (child, field)) in zip(children, fields).enumerate() {
284
+ // The decoder inserts struct fields in child order, so the positional
285
+ // lookup hits and the lookup by name is only a fallback. A declared
286
+ // child absent from a present struct is a decoder-invariant violation,
287
+ // not an implicit null.
288
+ let child_values = values
289
+ .iter()
290
+ .map(|value| match *value {
291
+ Value::Struct(row) => match row.get_index(child_index) {
292
+ Some((name, value)) if name == &child.name => Ok(value),
293
+ _ => row.get(&child.name).ok_or_else(|| {
294
+ QuackError::protocol(format!(
295
+ "{id:?} value has no field named {:?}",
296
+ child.name
297
+ ))
298
+ }),
299
+ },
300
+ // Only a null parent reaches here; the pass above rejected
301
+ // every other variant. Its child slots are null and masked by
302
+ // the parent's validity bit.
303
+ _ => Ok(&NULL_VALUE),
304
+ })
305
+ .collect::<Result<Vec<_>>>()?;
306
+ arrays.push(build_array(
307
+ &child.logical_type,
308
+ field.data_type(),
309
+ &child_values,
310
+ )?);
311
+ }
312
+ Ok(Arc::new(
313
+ StructArray::try_new_with_length(
314
+ fields.clone(),
315
+ arrays,
316
+ Some(NullBuffer::from(validity)),
317
+ values.len(),
318
+ )
319
+ .map_err(arrow_error)?,
320
+ ))
321
+ }
322
+
323
+ fn list_item(data_type: &DataType) -> Result<&FieldRef> {
324
+ match data_type {
325
+ DataType::List(item) => Ok(item),
326
+ other => Err(schema_shape("List", other)),
327
+ }
328
+ }
329
+
330
+ fn fixed_size_list_item(data_type: &DataType) -> Result<(&FieldRef, i32)> {
331
+ match data_type {
332
+ DataType::FixedSizeList(item, size) => Ok((item, *size)),
333
+ other => Err(schema_shape("FixedSizeList", other)),
334
+ }
335
+ }
336
+
337
+ fn struct_children(data_type: &DataType) -> Result<&Fields> {
338
+ match data_type {
339
+ DataType::Struct(fields) => Ok(fields),
340
+ other => Err(schema_shape("Struct", other)),
341
+ }
342
+ }
343
+
344
+ fn primitive<'a, T: ArrowPrimitiveType>(
345
+ values: &[&'a Value],
346
+ extract: impl Fn(&'a Value) -> Result<T::Native>,
347
+ ) -> Result<PrimitiveArray<T>> {
348
+ let mut builder = PrimitiveBuilder::<T>::with_capacity(values.len());
349
+ for value in values {
350
+ match *value {
351
+ Value::Null => builder.append_null(),
352
+ value => builder.append_value(extract(value)?),
353
+ }
354
+ }
355
+ Ok(builder.finish())
356
+ }
357
+
358
+ fn boolean<'a>(
359
+ values: &[&'a Value],
360
+ extract: impl Fn(&'a Value) -> Result<bool>,
361
+ ) -> Result<BooleanArray> {
362
+ let mut builder = BooleanBuilder::with_capacity(values.len());
363
+ for value in values {
364
+ match *value {
365
+ Value::Null => builder.append_null(),
366
+ value => builder.append_value(extract(value)?),
367
+ }
368
+ }
369
+ Ok(builder.finish())
370
+ }
371
+
372
+ fn collect<'a, T>(
373
+ values: &[&'a Value],
374
+ extract: impl Fn(&'a Value) -> Result<T>,
375
+ ) -> Result<Vec<Option<T>>> {
376
+ values
377
+ .iter()
378
+ .map(|value| match value {
379
+ Value::Null => Ok(None),
380
+ value => extract(value).map(Some),
381
+ })
382
+ .collect()
383
+ }
384
+
385
+ /// Total byte length of a string or blob column, rejecting columns that would
386
+ /// overflow Arrow's 32-bit offsets (the builders panic rather than error).
387
+ fn byte_capacity<T: AsRef<[u8]>>(id: LogicalTypeId, values: &[Option<T>]) -> Result<usize> {
388
+ let total: usize = values
389
+ .iter()
390
+ .flatten()
391
+ .map(|value| value.as_ref().len())
392
+ .sum();
393
+ if total > i32::MAX as usize {
394
+ return Err(QuackError::protocol(format!(
395
+ "{id:?} column of {total} bytes exceeds the Arrow 32-bit offset limit"
396
+ )));
397
+ }
398
+ Ok(total)
399
+ }
@@ -0,0 +1,134 @@
1
+ //! Arrow output support for query results.
2
+ //!
3
+ //! The bridge is read-only: it maps an already decoded [`DataChunk`] onto
4
+ //! [`arrow_array::RecordBatch`] using the column
5
+ //! [`LogicalType`](crate::LogicalType)s, so the wire codecs are untouched. It
6
+ //! is gated behind the `arrow` feature.
7
+ //!
8
+ //! [`QuackResultStream::into_record_batches`] is the entry point; the module
9
+ //! otherwise only re-exports the `arrow-rs` crates the bridge builds against,
10
+ //! so a caller can match their versions.
11
+
12
+ mod columns;
13
+ mod schema;
14
+ #[cfg(test)]
15
+ mod tests;
16
+ mod values;
17
+
18
+ use std::iter::zip;
19
+ use std::sync::Arc;
20
+
21
+ use arrow_array::{RecordBatch, RecordBatchOptions};
22
+ use arrow_schema::{ArrowError, DataType, Schema, SchemaRef};
23
+ use futures_util::StreamExt;
24
+ use futures_util::stream::BoxStream;
25
+
26
+ use crate::builders::ColumnDefinition;
27
+ use crate::client::QuackResultStream;
28
+ use crate::errors::{QuackError, Result};
29
+ use crate::logical_types::LogicalTypeId;
30
+ use crate::vector::{DataChunk, Value};
31
+
32
+ use self::columns::build_array;
33
+ use self::schema::field;
34
+
35
+ pub use arrow_array;
36
+ pub use arrow_buffer;
37
+ pub use arrow_schema;
38
+
39
+ const HUGE_INT_PRECISION: u8 = 39;
40
+ const UTC_TIMEZONE: &str = "UTC";
41
+
42
+ /// Builds the Arrow schema of a query result from its column definitions.
43
+ ///
44
+ /// The definitions come from prepare time, so the schema is available even for
45
+ /// results that carry no chunks at all.
46
+ fn schema(columns: &[ColumnDefinition]) -> Result<SchemaRef> {
47
+ let fields = columns
48
+ .iter()
49
+ .map(|column| field(&column.name, &column.logical_type))
50
+ .collect::<Result<Vec<_>>>()?;
51
+ Ok(Arc::new(Schema::new(fields)))
52
+ }
53
+
54
+ /// Converts a decoded chunk into a [`RecordBatch`] with the given schema.
55
+ ///
56
+ /// Returns an error if the chunk's column types do not match the schema.
57
+ fn to_record_batch(chunk: &DataChunk, schema: &SchemaRef) -> Result<RecordBatch> {
58
+ if chunk.types.len() != schema.fields().len() {
59
+ return Err(QuackError::protocol(format!(
60
+ "DataChunk has {} columns but the Arrow schema declares {}",
61
+ chunk.types.len(),
62
+ schema.fields().len()
63
+ )));
64
+ }
65
+ let mut arrays = Vec::with_capacity(chunk.types.len());
66
+ for (index, (logical_type, field)) in zip(&chunk.types, schema.fields()).enumerate() {
67
+ let values = chunk.column_values(index).ok_or_else(|| {
68
+ QuackError::protocol(format!("DataChunk is missing column vector {index}"))
69
+ })?;
70
+ if values.len() != chunk.row_count {
71
+ return Err(QuackError::protocol(format!(
72
+ "column {index} has {} values, expected {}",
73
+ values.len(),
74
+ chunk.row_count
75
+ )));
76
+ }
77
+ arrays.push(build_array(
78
+ logical_type,
79
+ field.data_type(),
80
+ &values.iter().collect::<Vec<_>>(),
81
+ )?);
82
+ }
83
+ RecordBatch::try_new_with_options(
84
+ schema.clone(),
85
+ arrays,
86
+ &RecordBatchOptions::new().with_row_count(Some(chunk.row_count)),
87
+ )
88
+ .map_err(arrow_error)
89
+ }
90
+
91
+ impl QuackResultStream {
92
+ /// Consumes the stream as Arrow record batches sharing a single schema.
93
+ ///
94
+ /// Like `into_chunks()`, the returned stream holds the client's connection
95
+ /// until it is drained or dropped, so another query on the same client
96
+ /// waits for it to finish.
97
+ pub fn into_record_batches(
98
+ self,
99
+ ) -> Result<(SchemaRef, BoxStream<'static, Result<RecordBatch>>)> {
100
+ let (columns, chunks) = self.into_chunks();
101
+ let schema = schema(&columns)?;
102
+ let batch_schema = schema.clone();
103
+ let batches = chunks
104
+ .map(move |chunk| chunk.and_then(|chunk| to_record_batch(&chunk, &batch_schema)))
105
+ .boxed();
106
+ Ok((schema, batches))
107
+ }
108
+ }
109
+
110
+ fn unsupported(id: LogicalTypeId) -> QuackError {
111
+ QuackError::unsupported(format!("logical type {id:?} has no Arrow mapping"))
112
+ }
113
+
114
+ fn mismatch(id: LogicalTypeId, value: &Value) -> QuackError {
115
+ QuackError::protocol(format!(
116
+ "decoded value {value:?} does not match logical type {id:?}"
117
+ ))
118
+ }
119
+
120
+ fn out_of_range(id: LogicalTypeId, value: impl std::fmt::Display) -> QuackError {
121
+ QuackError::protocol(format!(
122
+ "decoded value {value} is out of range for logical type {id:?}"
123
+ ))
124
+ }
125
+
126
+ fn schema_shape(expected: &str, actual: &DataType) -> QuackError {
127
+ QuackError::protocol(format!(
128
+ "Arrow schema declares {actual} where {expected} is required"
129
+ ))
130
+ }
131
+
132
+ fn arrow_error(error: ArrowError) -> QuackError {
133
+ QuackError::protocol(format!("arrow conversion failed: {error}"))
134
+ }
@@ -0,0 +1,103 @@
1
+ //! The `LogicalType` to Arrow `DataType` mapping.
2
+
3
+ use std::sync::Arc;
4
+
5
+ use arrow_schema::{DataType, Field, FieldRef, Fields, IntervalUnit, TimeUnit as ArrowTimeUnit};
6
+
7
+ use crate::errors::{QuackError, Result};
8
+ use crate::logical_types::{
9
+ LogicalType, LogicalTypeId, get_array_size, get_child_type, get_decimal_width_and_scale,
10
+ get_struct_children,
11
+ };
12
+
13
+ use super::{HUGE_INT_PRECISION, UTC_TIMEZONE, unsupported};
14
+
15
+ /// Maps a Quack logical type onto the Arrow type the bridge produces for it.
16
+ ///
17
+ /// Three mappings are provisional and may change in a future release: `MAP` is
18
+ /// encoded as `List<Struct<key, value>>` rather than Arrow's native `Map`,
19
+ /// `ENUM` as plain `Utf8` rather than a `Dictionary`, and `VARIANT` as the
20
+ /// struct DuckDB shreds it into rather than Arrow's canonical `arrow.variant`
21
+ /// extension, whose binary encoding is not the one DuckDB sends.
22
+ pub(super) fn arrow_type(logical_type: &LogicalType) -> Result<DataType> {
23
+ Ok(match logical_type.id {
24
+ LogicalTypeId::SqlNull => DataType::Null,
25
+ LogicalTypeId::Boolean => DataType::Boolean,
26
+ LogicalTypeId::TinyInt => DataType::Int8,
27
+ LogicalTypeId::SmallInt => DataType::Int16,
28
+ LogicalTypeId::Integer => DataType::Int32,
29
+ LogicalTypeId::BigInt => DataType::Int64,
30
+ LogicalTypeId::UTinyInt => DataType::UInt8,
31
+ LogicalTypeId::USmallInt => DataType::UInt16,
32
+ LogicalTypeId::UInteger => DataType::UInt32,
33
+ LogicalTypeId::UBigInt => DataType::UInt64,
34
+ LogicalTypeId::HugeInt | LogicalTypeId::UHugeInt => {
35
+ DataType::Decimal256(HUGE_INT_PRECISION, 0)
36
+ }
37
+ LogicalTypeId::Float => DataType::Float32,
38
+ LogicalTypeId::Double => DataType::Float64,
39
+ LogicalTypeId::Decimal => {
40
+ let (precision, scale) = decimal_precision_and_scale(logical_type)?;
41
+ DataType::Decimal128(precision, scale)
42
+ }
43
+ LogicalTypeId::Varchar
44
+ | LogicalTypeId::Char
45
+ | LogicalTypeId::Enum
46
+ | LogicalTypeId::Uuid => DataType::Utf8,
47
+ LogicalTypeId::Blob | LogicalTypeId::Geometry | LogicalTypeId::Bit => DataType::Binary,
48
+ LogicalTypeId::Date => DataType::Date32,
49
+ LogicalTypeId::Time => DataType::Time64(ArrowTimeUnit::Microsecond),
50
+ LogicalTypeId::TimeNs => DataType::Time64(ArrowTimeUnit::Nanosecond),
51
+ LogicalTypeId::TimestampSec => DataType::Timestamp(ArrowTimeUnit::Second, None),
52
+ LogicalTypeId::TimestampMs => DataType::Timestamp(ArrowTimeUnit::Millisecond, None),
53
+ LogicalTypeId::Timestamp => DataType::Timestamp(ArrowTimeUnit::Microsecond, None),
54
+ LogicalTypeId::TimestampNs => DataType::Timestamp(ArrowTimeUnit::Nanosecond, None),
55
+ LogicalTypeId::TimestampTz => {
56
+ DataType::Timestamp(ArrowTimeUnit::Microsecond, Some(UTC_TIMEZONE.into()))
57
+ }
58
+ LogicalTypeId::Interval => DataType::Interval(IntervalUnit::MonthDayNano),
59
+ LogicalTypeId::List | LogicalTypeId::Map => DataType::List(list_item_field(logical_type)?),
60
+ LogicalTypeId::Array => {
61
+ DataType::FixedSizeList(list_item_field(logical_type)?, array_size(logical_type)?)
62
+ }
63
+ LogicalTypeId::Struct | LogicalTypeId::Variant => {
64
+ DataType::Struct(struct_fields(logical_type)?)
65
+ }
66
+ other => return Err(unsupported(other)),
67
+ })
68
+ }
69
+
70
+ pub(super) fn field(name: &str, logical_type: &LogicalType) -> Result<FieldRef> {
71
+ Ok(Arc::new(Field::new(name, arrow_type(logical_type)?, true)))
72
+ }
73
+
74
+ fn list_item_field(logical_type: &LogicalType) -> Result<FieldRef> {
75
+ Ok(Arc::new(Field::new_list_field(
76
+ arrow_type(get_child_type(logical_type)?)?,
77
+ true,
78
+ )))
79
+ }
80
+
81
+ fn struct_fields(logical_type: &LogicalType) -> Result<Fields> {
82
+ Ok(get_struct_children(logical_type)?
83
+ .iter()
84
+ .map(|child| field(&child.name, &child.logical_type))
85
+ .collect::<Result<Vec<_>>>()?
86
+ .into())
87
+ }
88
+
89
+ fn array_size(logical_type: &LogicalType) -> Result<i32> {
90
+ let size = get_array_size(logical_type)?;
91
+ i32::try_from(size)
92
+ .map_err(|_| QuackError::protocol(format!("ARRAY size {size} exceeds the Arrow limit")))
93
+ }
94
+
95
+ pub(super) fn decimal_precision_and_scale(logical_type: &LogicalType) -> Result<(u8, i8)> {
96
+ let (width, scale) = get_decimal_width_and_scale(logical_type)?;
97
+ match (u8::try_from(width), i8::try_from(scale)) {
98
+ (Ok(width), Ok(scale)) => Ok((width, scale)),
99
+ _ => Err(QuackError::protocol(format!(
100
+ "DECIMAL({width}, {scale}) exceeds the Arrow decimal limits"
101
+ ))),
102
+ }
103
+ }