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,851 @@
1
+ use arrow_array::cast::AsArray;
2
+ use arrow_array::temporal_conversions::MICROSECONDS_IN_DAY;
3
+ use arrow_array::types::Int32Type;
4
+ use arrow_array::{
5
+ Array, ArrayRef, BinaryArray, BooleanArray, Date32Array, Decimal128Array, Decimal256Array,
6
+ Float32Array, Float64Array, Int8Array, Int16Array, Int32Array, Int64Array,
7
+ IntervalMonthDayNanoArray, NullArray, StringArray, Time64MicrosecondArray,
8
+ Time64NanosecondArray, TimestampMicrosecondArray, TimestampMillisecondArray,
9
+ TimestampNanosecondArray, TimestampSecondArray, UInt8Array, UInt16Array, UInt32Array,
10
+ UInt64Array,
11
+ };
12
+ use arrow_buffer::{IntervalMonthDayNano, i256};
13
+ use arrow_schema::{Field, Fields};
14
+ use indexmap::IndexMap;
15
+
16
+ use super::columns::build_array;
17
+ use super::schema::arrow_type;
18
+ use super::*;
19
+ use crate::builders::{column, data_chunk};
20
+ use crate::logical_types::ExtraTypeInfo;
21
+ use crate::logical_types::LogicalType;
22
+ use crate::logical_types::{ChildType, LogicalTypeId, LogicalTypes, logical_type_with_info};
23
+ use crate::values::{date_value, decimal_value, interval_value, time_value, timestamp_value};
24
+ use crate::vector::{TimeUnit, TimestampUnit};
25
+
26
+ fn definitions(chunk: &DataChunk) -> Vec<ColumnDefinition> {
27
+ chunk
28
+ .column_names
29
+ .clone()
30
+ .unwrap_or_default()
31
+ .into_iter()
32
+ .zip(chunk.types.iter())
33
+ .map(|(name, logical_type)| ColumnDefinition {
34
+ name,
35
+ logical_type: logical_type.clone(),
36
+ })
37
+ .collect()
38
+ }
39
+
40
+ fn record_batch(chunk: &DataChunk) -> RecordBatch {
41
+ let schema = schema(&definitions(chunk)).unwrap();
42
+ to_record_batch(chunk, &schema).unwrap()
43
+ }
44
+
45
+ fn struct_value(entries: Vec<(&str, Value)>) -> Value {
46
+ let mut row = IndexMap::new();
47
+ for (name, value) in entries {
48
+ row.insert(name.to_string(), value);
49
+ }
50
+ Value::Struct(row)
51
+ }
52
+
53
+ fn named(name: &str) -> Option<String> {
54
+ Some(name.to_string())
55
+ }
56
+
57
+ fn enum_type() -> LogicalType {
58
+ LogicalTypes::r#enum(vec![
59
+ "sad".to_string(),
60
+ "ok".to_string(),
61
+ "happy".to_string(),
62
+ ])
63
+ }
64
+
65
+ /// The physical shape DuckDB shreds a `VARIANT` into on the wire, as observed
66
+ /// from a live `quack_serve`.
67
+ fn variant_type() -> LogicalType {
68
+ logical_type_with_info(
69
+ LogicalTypeId::Variant,
70
+ ExtraTypeInfo::Struct {
71
+ alias: None,
72
+ child_types: vec![
73
+ ChildType {
74
+ name: "keys".to_string(),
75
+ logical_type: LogicalTypes::list(LogicalTypes::varchar()),
76
+ },
77
+ ChildType {
78
+ name: "children".to_string(),
79
+ logical_type: LogicalTypes::list(LogicalTypes::r#struct(vec![
80
+ ChildType {
81
+ name: "keys_index".to_string(),
82
+ logical_type: LogicalTypes::uinteger(),
83
+ },
84
+ ChildType {
85
+ name: "values_index".to_string(),
86
+ logical_type: LogicalTypes::uinteger(),
87
+ },
88
+ ])),
89
+ },
90
+ ChildType {
91
+ name: "values".to_string(),
92
+ logical_type: LogicalTypes::list(LogicalTypes::r#struct(vec![
93
+ ChildType {
94
+ name: "type_id".to_string(),
95
+ logical_type: LogicalTypes::utinyint(),
96
+ },
97
+ ChildType {
98
+ name: "byte_offset".to_string(),
99
+ logical_type: LogicalTypes::uinteger(),
100
+ },
101
+ ])),
102
+ },
103
+ ChildType {
104
+ name: "data".to_string(),
105
+ logical_type: LogicalTypes::blob(),
106
+ },
107
+ ],
108
+ },
109
+ )
110
+ }
111
+
112
+ fn point_type() -> LogicalType {
113
+ LogicalTypes::r#struct(vec![
114
+ ChildType {
115
+ name: "x".to_string(),
116
+ logical_type: LogicalTypes::integer(),
117
+ },
118
+ ChildType {
119
+ name: "y".to_string(),
120
+ logical_type: LogicalTypes::varchar(),
121
+ },
122
+ ])
123
+ }
124
+
125
+ /// Every scalar type, as (column name, logical type, one value, the Arrow
126
+ /// array expected for that value followed by a null).
127
+ fn scalar_cases() -> Vec<(&'static str, LogicalType, Value, ArrayRef)> {
128
+ vec![
129
+ (
130
+ "null_v",
131
+ LogicalTypes::null(),
132
+ Value::Null,
133
+ Arc::new(NullArray::new(2)),
134
+ ),
135
+ (
136
+ "bool_v",
137
+ LogicalTypes::boolean(),
138
+ Value::Bool(true),
139
+ Arc::new(BooleanArray::from(vec![Some(true), None])),
140
+ ),
141
+ (
142
+ "tiny_v",
143
+ LogicalTypes::tinyint(),
144
+ Value::Int(127),
145
+ Arc::new(Int8Array::from(vec![Some(127), None])),
146
+ ),
147
+ (
148
+ "small_v",
149
+ LogicalTypes::smallint(),
150
+ Value::Int(32767),
151
+ Arc::new(Int16Array::from(vec![Some(32767), None])),
152
+ ),
153
+ (
154
+ "int_v",
155
+ LogicalTypes::integer(),
156
+ Value::Int(2147483647),
157
+ Arc::new(Int32Array::from(vec![Some(2147483647), None])),
158
+ ),
159
+ (
160
+ "big_v",
161
+ LogicalTypes::bigint(),
162
+ Value::Int(9007199254740993),
163
+ Arc::new(Int64Array::from(vec![Some(9007199254740993), None])),
164
+ ),
165
+ (
166
+ "utiny_v",
167
+ LogicalTypes::utinyint(),
168
+ Value::UInt(255),
169
+ Arc::new(UInt8Array::from(vec![Some(255), None])),
170
+ ),
171
+ (
172
+ "usmall_v",
173
+ LogicalTypes::usmallint(),
174
+ Value::UInt(65535),
175
+ Arc::new(UInt16Array::from(vec![Some(65535), None])),
176
+ ),
177
+ (
178
+ "uint_v",
179
+ LogicalTypes::uinteger(),
180
+ Value::UInt(4294967295),
181
+ Arc::new(UInt32Array::from(vec![Some(4294967295), None])),
182
+ ),
183
+ (
184
+ "ubig_v",
185
+ LogicalTypes::ubigint(),
186
+ Value::UInt(u64::MAX),
187
+ Arc::new(UInt64Array::from(vec![Some(u64::MAX), None])),
188
+ ),
189
+ (
190
+ "huge_v",
191
+ LogicalTypes::hugeint(),
192
+ Value::HugeInt(-123456789012345678901234567890),
193
+ Arc::new(
194
+ Decimal256Array::from(vec![
195
+ Some(i256::from_i128(-123456789012345678901234567890)),
196
+ None,
197
+ ])
198
+ .with_precision_and_scale(HUGE_INT_PRECISION, 0)
199
+ .unwrap(),
200
+ ),
201
+ ),
202
+ (
203
+ "uhuge_v",
204
+ LogicalTypes::uhugeint(),
205
+ Value::UHugeInt(u128::MAX),
206
+ Arc::new(
207
+ Decimal256Array::from(vec![Some(i256::from_parts(u128::MAX, 0)), None])
208
+ .with_precision_and_scale(HUGE_INT_PRECISION, 0)
209
+ .unwrap(),
210
+ ),
211
+ ),
212
+ (
213
+ "float_v",
214
+ LogicalTypes::float(),
215
+ Value::Float(1.5),
216
+ Arc::new(Float32Array::from(vec![Some(1.5), None])),
217
+ ),
218
+ (
219
+ "double_v",
220
+ LogicalTypes::double(),
221
+ Value::Double(2.25),
222
+ Arc::new(Float64Array::from(vec![Some(2.25), None])),
223
+ ),
224
+ (
225
+ "dec_v",
226
+ LogicalTypes::decimal(9, 2),
227
+ decimal_value("1234567.89", 9, 2).unwrap(),
228
+ Arc::new(
229
+ Decimal128Array::from(vec![Some(123456789), None])
230
+ .with_precision_and_scale(9, 2)
231
+ .unwrap(),
232
+ ),
233
+ ),
234
+ (
235
+ "string_v",
236
+ LogicalTypes::varchar(),
237
+ Value::String("hello".to_string()),
238
+ Arc::new(StringArray::from(vec![Some("hello"), None])),
239
+ ),
240
+ (
241
+ "blob_v",
242
+ LogicalTypes::blob(),
243
+ Value::Bytes(b"hi".to_vec()),
244
+ Arc::new(BinaryArray::from(vec![Some(b"hi".as_slice()), None])),
245
+ ),
246
+ (
247
+ "uuid_v",
248
+ LogicalTypes::uuid(),
249
+ Value::String("00112233-4455-6677-8899-aabbccddeeff".to_string()),
250
+ Arc::new(StringArray::from(vec![
251
+ Some("00112233-4455-6677-8899-aabbccddeeff"),
252
+ None,
253
+ ])),
254
+ ),
255
+ (
256
+ "enum_v",
257
+ enum_type(),
258
+ Value::String("ok".to_string()),
259
+ Arc::new(StringArray::from(vec![Some("ok"), None])),
260
+ ),
261
+ (
262
+ "date_v",
263
+ LogicalTypes::date(),
264
+ date_value(18263),
265
+ Arc::new(Date32Array::from(vec![Some(18263), None])),
266
+ ),
267
+ (
268
+ "time_v",
269
+ LogicalTypes::time(),
270
+ time_value(1234567, TimeUnit::Micros),
271
+ Arc::new(Time64MicrosecondArray::from(vec![Some(1234567), None])),
272
+ ),
273
+ (
274
+ "time_ns_v",
275
+ LogicalTypes::time_ns(),
276
+ time_value(1234567890, TimeUnit::Nanos),
277
+ Arc::new(Time64NanosecondArray::from(vec![Some(1234567890), None])),
278
+ ),
279
+ (
280
+ "ts_s_v",
281
+ LogicalTypes::timestamp_seconds(),
282
+ timestamp_value(1, TimestampUnit::Seconds, false),
283
+ Arc::new(TimestampSecondArray::from(vec![Some(1), None])),
284
+ ),
285
+ (
286
+ "ts_ms_v",
287
+ LogicalTypes::timestamp_millis(),
288
+ timestamp_value(1234, TimestampUnit::Millis, false),
289
+ Arc::new(TimestampMillisecondArray::from(vec![Some(1234), None])),
290
+ ),
291
+ (
292
+ "ts_v",
293
+ LogicalTypes::timestamp(),
294
+ timestamp_value(1234567, TimestampUnit::Micros, false),
295
+ Arc::new(TimestampMicrosecondArray::from(vec![Some(1234567), None])),
296
+ ),
297
+ (
298
+ "ts_ns_v",
299
+ LogicalTypes::timestamp_nanos(),
300
+ timestamp_value(1234567890, TimestampUnit::Nanos, false),
301
+ Arc::new(TimestampNanosecondArray::from(vec![Some(1234567890), None])),
302
+ ),
303
+ (
304
+ "ts_tz_v",
305
+ LogicalTypes::timestamp_tz(),
306
+ timestamp_value(1234567, TimestampUnit::Micros, true),
307
+ Arc::new(
308
+ TimestampMicrosecondArray::from(vec![Some(1234567), None])
309
+ .with_timezone(UTC_TIMEZONE),
310
+ ),
311
+ ),
312
+ (
313
+ "interval_v",
314
+ LogicalTypes::interval(),
315
+ interval_value(1, 2, 3),
316
+ Arc::new(IntervalMonthDayNanoArray::from(vec![
317
+ Some(IntervalMonthDayNano::new(1, 2, 3000)),
318
+ None,
319
+ ])),
320
+ ),
321
+ ]
322
+ }
323
+
324
+ #[test]
325
+ fn scalar_columns_map_to_their_arrow_types() {
326
+ let cases = scalar_cases();
327
+ let chunk = data_chunk(
328
+ cases
329
+ .iter()
330
+ .map(|(name, logical_type, value, _)| {
331
+ column(
332
+ logical_type.clone(),
333
+ [value.clone(), Value::Null],
334
+ named(name),
335
+ )
336
+ })
337
+ .collect(),
338
+ )
339
+ .unwrap();
340
+
341
+ let batch = record_batch(&chunk);
342
+
343
+ assert_eq!(batch.num_rows(), 2);
344
+ for (name, _, _, expected) in &cases {
345
+ let index = batch.schema().index_of(name).unwrap();
346
+ assert_eq!(batch.column(index), expected, "column {name}");
347
+ }
348
+ }
349
+
350
+ #[test]
351
+ fn build_array_and_arrow_type_agree_for_every_mapped_type() {
352
+ let mut types: Vec<LogicalType> = scalar_cases()
353
+ .into_iter()
354
+ .map(|(_, logical_type, _, _)| logical_type)
355
+ .collect();
356
+ types.extend([
357
+ LogicalTypes::bit(),
358
+ LogicalTypes::geometry(None),
359
+ LogicalTypes::list(LogicalTypes::integer()),
360
+ LogicalTypes::list(point_type()),
361
+ LogicalTypes::array(LogicalTypes::integer(), 2),
362
+ LogicalTypes::map(LogicalTypes::varchar(), LogicalTypes::integer()),
363
+ point_type(),
364
+ variant_type(),
365
+ ]);
366
+
367
+ for logical_type in types {
368
+ let declared = arrow_type(&logical_type).unwrap();
369
+ let built = build_array(&logical_type, &declared, &[]).unwrap();
370
+ assert_eq!(
371
+ built.data_type(),
372
+ &declared,
373
+ "{:?} builds an array that disagrees with arrow_type()",
374
+ logical_type.id
375
+ );
376
+ }
377
+ }
378
+
379
+ #[test]
380
+ fn nested_columns_preserve_null_containers_and_entries() {
381
+ let chunk = data_chunk(vec![
382
+ column(
383
+ LogicalTypes::list(LogicalTypes::integer()),
384
+ [
385
+ Value::List(vec![Value::Int(1), Value::Null, Value::Int(3)]),
386
+ Value::Null,
387
+ Value::List(vec![]),
388
+ ],
389
+ named("list_v"),
390
+ ),
391
+ column(
392
+ LogicalTypes::array(LogicalTypes::integer(), 2),
393
+ [
394
+ Value::List(vec![Value::Int(7), Value::Int(8)]),
395
+ Value::Null,
396
+ Value::List(vec![Value::Null, Value::Int(9)]),
397
+ ],
398
+ named("array_v"),
399
+ ),
400
+ column(
401
+ point_type(),
402
+ [
403
+ struct_value(vec![
404
+ ("x", Value::Int(1)),
405
+ ("y", Value::String("a".to_string())),
406
+ ]),
407
+ Value::Null,
408
+ struct_value(vec![("x", Value::Int(3)), ("y", Value::Null)]),
409
+ ],
410
+ named("struct_v"),
411
+ ),
412
+ column(
413
+ LogicalTypes::map(LogicalTypes::varchar(), LogicalTypes::integer()),
414
+ [
415
+ Value::List(vec![struct_value(vec![
416
+ ("key", Value::String("a".to_string())),
417
+ ("value", Value::Int(1)),
418
+ ])]),
419
+ Value::Null,
420
+ Value::List(vec![]),
421
+ ],
422
+ named("map_v"),
423
+ ),
424
+ column(
425
+ LogicalTypes::list(point_type()),
426
+ [
427
+ Value::List(vec![struct_value(vec![
428
+ ("x", Value::Int(5)),
429
+ ("y", Value::String("b".to_string())),
430
+ ])]),
431
+ Value::Null,
432
+ Value::List(vec![Value::Null]),
433
+ ],
434
+ named("list_of_struct_v"),
435
+ ),
436
+ ])
437
+ .unwrap();
438
+
439
+ let batch = record_batch(&chunk);
440
+ let point_fields = Fields::from(vec![
441
+ Field::new("x", DataType::Int32, true),
442
+ Field::new("y", DataType::Utf8, true),
443
+ ]);
444
+
445
+ assert_eq!(
446
+ batch
447
+ .schema()
448
+ .field_with_name("list_v")
449
+ .unwrap()
450
+ .data_type(),
451
+ &DataType::List(Arc::new(Field::new_list_field(DataType::Int32, true)))
452
+ );
453
+ assert_eq!(
454
+ batch
455
+ .schema()
456
+ .field_with_name("array_v")
457
+ .unwrap()
458
+ .data_type(),
459
+ &DataType::FixedSizeList(Arc::new(Field::new_list_field(DataType::Int32, true)), 2)
460
+ );
461
+ assert_eq!(
462
+ batch.schema().field_with_name("map_v").unwrap().data_type(),
463
+ &DataType::List(Arc::new(Field::new_list_field(
464
+ DataType::Struct(Fields::from(vec![
465
+ Field::new("key", DataType::Utf8, true),
466
+ Field::new("value", DataType::Int32, true),
467
+ ])),
468
+ true
469
+ )))
470
+ );
471
+
472
+ let lists = batch.column_by_name("list_v").unwrap().as_list::<i32>();
473
+ assert!(lists.is_null(1));
474
+ assert_eq!(lists.value_length(0), 3);
475
+ assert_eq!(lists.value_length(2), 0);
476
+ assert_eq!(
477
+ lists.value(0).as_primitive::<Int32Type>(),
478
+ &Int32Array::from(vec![Some(1), None, Some(3)])
479
+ );
480
+
481
+ let arrays = batch
482
+ .column_by_name("array_v")
483
+ .unwrap()
484
+ .as_fixed_size_list();
485
+ assert!(arrays.is_null(1));
486
+ assert_eq!(
487
+ arrays.value(2).as_primitive::<Int32Type>(),
488
+ &Int32Array::from(vec![None, Some(9)])
489
+ );
490
+
491
+ let structs = batch.column_by_name("struct_v").unwrap().as_struct();
492
+ assert!(structs.is_null(1));
493
+ assert_eq!(
494
+ structs.column(0).as_primitive::<Int32Type>(),
495
+ &Int32Array::from(vec![Some(1), None, Some(3)])
496
+ );
497
+ assert_eq!(
498
+ structs.column(1).as_string::<i32>(),
499
+ &StringArray::from(vec![Some("a"), None, None])
500
+ );
501
+
502
+ let list_of_struct = batch
503
+ .column_by_name("list_of_struct_v")
504
+ .unwrap()
505
+ .as_list::<i32>();
506
+ let entries = list_of_struct.value(0);
507
+ let entries = entries.as_struct();
508
+ assert_eq!(entries.fields(), &point_fields);
509
+ assert_eq!(
510
+ entries.column(0).as_primitive::<Int32Type>(),
511
+ &Int32Array::from(vec![Some(5)])
512
+ );
513
+ let null_entry = list_of_struct.value(2);
514
+ assert!(null_entry.is_null(0));
515
+ }
516
+
517
+ #[test]
518
+ fn schema_is_built_without_any_chunk() {
519
+ let columns = vec![
520
+ ColumnDefinition {
521
+ name: "id".to_string(),
522
+ logical_type: LogicalTypes::integer(),
523
+ },
524
+ ColumnDefinition {
525
+ name: "label".to_string(),
526
+ logical_type: LogicalTypes::varchar(),
527
+ },
528
+ ];
529
+
530
+ let schema = schema(&columns).unwrap();
531
+
532
+ assert_eq!(
533
+ schema
534
+ .fields()
535
+ .iter()
536
+ .map(|field| field.name().as_str())
537
+ .collect::<Vec<_>>(),
538
+ vec!["id", "label"]
539
+ );
540
+ assert_eq!(schema.field(0).data_type(), &DataType::Int32);
541
+ assert!(schema.field(0).is_nullable());
542
+ assert_eq!(schema.field(1).data_type(), &DataType::Utf8);
543
+ }
544
+
545
+ #[test]
546
+ fn empty_chunks_keep_the_schema_and_row_count() {
547
+ let chunk = data_chunk(vec![column(LogicalTypes::integer(), [], named("id"))]).unwrap();
548
+ let schema = schema(&[ColumnDefinition {
549
+ name: "id".to_string(),
550
+ logical_type: LogicalTypes::integer(),
551
+ }])
552
+ .unwrap();
553
+
554
+ let batch = to_record_batch(&chunk, &schema).unwrap();
555
+
556
+ assert_eq!(batch.num_rows(), 0);
557
+ assert_eq!(batch.schema(), schema);
558
+ }
559
+
560
+ #[test]
561
+ fn unsupported_types_are_reported_by_name() {
562
+ let error = arrow_type(&LogicalTypes::time_tz()).unwrap_err();
563
+
564
+ assert!(
565
+ matches!(&error, QuackError::UnsupportedType(message) if message.contains("TimeTz")),
566
+ "unexpected error: {error}"
567
+ );
568
+ }
569
+
570
+ #[test]
571
+ fn times_outside_the_arrow_day_bound_are_rejected() {
572
+ let chunk = data_chunk(vec![column(
573
+ LogicalTypes::time(),
574
+ [time_value(MICROSECONDS_IN_DAY, TimeUnit::Micros)],
575
+ named("time_v"),
576
+ )])
577
+ .unwrap();
578
+ let schema = schema(&definitions(&chunk)).unwrap();
579
+
580
+ let error = to_record_batch(&chunk, &schema).unwrap_err();
581
+
582
+ assert!(
583
+ matches!(&error, QuackError::Protocol(message) if message.contains("out of range")),
584
+ "unexpected error: {error}"
585
+ );
586
+ }
587
+
588
+ #[test]
589
+ fn out_of_range_narrowing_is_a_protocol_error() {
590
+ let chunk = data_chunk(vec![column(
591
+ LogicalTypes::tinyint(),
592
+ [Value::Int(1000)],
593
+ named("tiny_v"),
594
+ )])
595
+ .unwrap();
596
+ let schema = schema(&definitions(&chunk)).unwrap();
597
+
598
+ let error = to_record_batch(&chunk, &schema).unwrap_err();
599
+
600
+ assert!(
601
+ matches!(&error, QuackError::Protocol(message) if message.contains("out of range")),
602
+ "unexpected error: {error}"
603
+ );
604
+ }
605
+
606
+ #[test]
607
+ fn value_variants_that_contradict_the_logical_type_are_rejected() {
608
+ let chunk = data_chunk(vec![column(
609
+ LogicalTypes::integer(),
610
+ [Value::String("nope".to_string())],
611
+ named("int_v"),
612
+ )])
613
+ .unwrap();
614
+ let schema = schema(&definitions(&chunk)).unwrap();
615
+
616
+ let error = to_record_batch(&chunk, &schema).unwrap_err();
617
+
618
+ assert!(
619
+ matches!(&error, QuackError::Protocol(message) if message.contains("does not match")),
620
+ "unexpected error: {error}"
621
+ );
622
+ }
623
+
624
+ #[test]
625
+ fn variant_columns_pass_through_the_struct_duckdb_shreds_them_into() {
626
+ // `SELECT {'x': 1, 'y': 'two'}::VARIANT`, as a live quack_serve decodes it.
627
+ let variant = struct_value(vec![
628
+ (
629
+ "keys",
630
+ Value::List(vec![
631
+ Value::String("x".to_string()),
632
+ Value::String("y".to_string()),
633
+ ]),
634
+ ),
635
+ (
636
+ "children",
637
+ Value::List(vec![
638
+ struct_value(vec![
639
+ ("keys_index", Value::UInt(0)),
640
+ ("values_index", Value::UInt(1)),
641
+ ]),
642
+ struct_value(vec![
643
+ ("keys_index", Value::UInt(1)),
644
+ ("values_index", Value::UInt(2)),
645
+ ]),
646
+ ]),
647
+ ),
648
+ (
649
+ "values",
650
+ Value::List(vec![
651
+ struct_value(vec![
652
+ ("type_id", Value::UInt(29)),
653
+ ("byte_offset", Value::UInt(0)),
654
+ ]),
655
+ struct_value(vec![
656
+ ("type_id", Value::UInt(5)),
657
+ ("byte_offset", Value::UInt(2)),
658
+ ]),
659
+ struct_value(vec![
660
+ ("type_id", Value::UInt(16)),
661
+ ("byte_offset", Value::UInt(6)),
662
+ ]),
663
+ ]),
664
+ ),
665
+ (
666
+ "data",
667
+ Value::Bytes(vec![2, 0, 1, 0, 0, 0, 3, 116, 119, 111]),
668
+ ),
669
+ ]);
670
+ let chunk = data_chunk(vec![column(
671
+ variant_type(),
672
+ [variant, Value::Null],
673
+ named("variant_v"),
674
+ )])
675
+ .unwrap();
676
+
677
+ let batch = record_batch(&chunk);
678
+
679
+ let column = batch.column(0).as_struct();
680
+ assert_eq!(column.data_type(), &arrow_type(&variant_type()).unwrap());
681
+ let keys = column.column_by_name("keys").unwrap().as_list::<i32>();
682
+ assert_eq!(
683
+ keys.value(0).as_string::<i32>(),
684
+ &StringArray::from(vec!["x", "y"])
685
+ );
686
+ // A NULL variant is a null container, not a struct of empty children.
687
+ assert!(column.is_null(1));
688
+ assert_eq!(
689
+ column
690
+ .column_by_name("data")
691
+ .unwrap()
692
+ .as_binary::<i32>()
693
+ .value(0),
694
+ &[2, 0, 1, 0, 0, 0, 3, 116, 119, 111]
695
+ );
696
+ }
697
+
698
+ #[test]
699
+ fn values_in_a_sqlnull_column_are_rejected() {
700
+ let chunk = data_chunk(vec![column(
701
+ LogicalTypes::null(),
702
+ [Value::Int(7)],
703
+ named("null_v"),
704
+ )])
705
+ .unwrap();
706
+ let schema = schema(&definitions(&chunk)).unwrap();
707
+
708
+ let error = to_record_batch(&chunk, &schema).unwrap_err();
709
+
710
+ assert!(
711
+ matches!(&error, QuackError::Protocol(message) if message.contains("does not match")),
712
+ "unexpected error: {error}"
713
+ );
714
+ }
715
+
716
+ #[test]
717
+ fn structs_missing_a_declared_field_are_rejected() {
718
+ let chunk = data_chunk(vec![column(
719
+ point_type(),
720
+ [struct_value(vec![
721
+ ("x", Value::Int(1)),
722
+ ("z", Value::String("wrong name".to_string())),
723
+ ])],
724
+ named("point_v"),
725
+ )])
726
+ .unwrap();
727
+ let schema = schema(&definitions(&chunk)).unwrap();
728
+
729
+ let error = to_record_batch(&chunk, &schema).unwrap_err();
730
+
731
+ assert!(
732
+ matches!(&error, QuackError::Protocol(message) if message.contains("no field named \"y\"")),
733
+ "unexpected error: {error}"
734
+ );
735
+ }
736
+
737
+ #[test]
738
+ fn structs_carrying_an_undeclared_field_are_rejected() {
739
+ let chunk = data_chunk(vec![column(
740
+ point_type(),
741
+ [struct_value(vec![
742
+ ("x", Value::Int(1)),
743
+ ("y", Value::String("here".to_string())),
744
+ ("z", Value::Int(2)),
745
+ ])],
746
+ named("point_v"),
747
+ )])
748
+ .unwrap();
749
+ let schema = schema(&definitions(&chunk)).unwrap();
750
+
751
+ let error = to_record_batch(&chunk, &schema).unwrap_err();
752
+
753
+ assert!(
754
+ matches!(&error, QuackError::Protocol(message) if message.contains("has 3 fields but the type declares 2")),
755
+ "unexpected error: {error}"
756
+ );
757
+ }
758
+
759
+ #[test]
760
+ fn errors_name_the_column_type_rather_than_the_builder() {
761
+ // VARIANT shares its builder with STRUCT, so the error has to name VARIANT.
762
+ let chunk = data_chunk(vec![column(
763
+ variant_type(),
764
+ [struct_value(vec![
765
+ ("keys", Value::List(vec![])),
766
+ ("data", Value::Bytes(vec![])),
767
+ ])],
768
+ named("variant_v"),
769
+ )])
770
+ .unwrap();
771
+ let schema = schema(&definitions(&chunk)).unwrap();
772
+
773
+ let error = to_record_batch(&chunk, &schema).unwrap_err();
774
+
775
+ assert!(
776
+ matches!(&error, QuackError::Protocol(message) if message
777
+ .contains("Variant value has 2 fields but the type declares 4")),
778
+ "unexpected error: {error}"
779
+ );
780
+ }
781
+
782
+ #[test]
783
+ fn nested_columns_honour_the_schema_field_names() {
784
+ let chunk = data_chunk(vec![column(
785
+ LogicalTypes::list(LogicalTypes::integer()),
786
+ [Value::List(vec![Value::Int(1), Value::Int(2)])],
787
+ named("list_v"),
788
+ )])
789
+ .unwrap();
790
+ // Parquet and Spark name the list child "element" rather than "item";
791
+ // building against the schema's own field keeps either name working.
792
+ let item = Arc::new(Field::new("element", DataType::Int32, true));
793
+ let schema = Arc::new(Schema::new(vec![Field::new(
794
+ "list_v",
795
+ DataType::List(item.clone()),
796
+ true,
797
+ )]));
798
+
799
+ let batch = to_record_batch(&chunk, &schema).unwrap();
800
+
801
+ assert_eq!(batch.schema(), schema);
802
+ assert_eq!(
803
+ batch.column(0).data_type(),
804
+ &DataType::List(item),
805
+ "the list child field must come from the schema"
806
+ );
807
+ }
808
+
809
+ #[test]
810
+ fn nested_columns_whose_schema_shape_disagrees_are_rejected() {
811
+ let chunk = data_chunk(vec![column(
812
+ LogicalTypes::list(LogicalTypes::integer()),
813
+ [Value::List(vec![Value::Int(1)])],
814
+ named("list_v"),
815
+ )])
816
+ .unwrap();
817
+ let schema = schema(&[ColumnDefinition {
818
+ name: "list_v".to_string(),
819
+ logical_type: LogicalTypes::varchar(),
820
+ }])
821
+ .unwrap();
822
+
823
+ let error = to_record_batch(&chunk, &schema).unwrap_err();
824
+
825
+ assert!(
826
+ matches!(&error, QuackError::Protocol(message) if message.contains("where List is required")),
827
+ "unexpected error: {error}"
828
+ );
829
+ }
830
+
831
+ #[test]
832
+ fn chunks_that_disagree_with_the_schema_are_rejected() {
833
+ let chunk = data_chunk(vec![column(
834
+ LogicalTypes::varchar(),
835
+ [Value::String("hello".to_string())],
836
+ named("id"),
837
+ )])
838
+ .unwrap();
839
+ let schema = schema(&[ColumnDefinition {
840
+ name: "id".to_string(),
841
+ logical_type: LogicalTypes::integer(),
842
+ }])
843
+ .unwrap();
844
+
845
+ let error = to_record_batch(&chunk, &schema).unwrap_err();
846
+
847
+ assert!(
848
+ matches!(&error, QuackError::Protocol(message) if message.contains("column types must match schema types")),
849
+ "unexpected error: {error}"
850
+ );
851
+ }