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,759 @@
1
+ use indexmap::IndexMap;
2
+
3
+ use crate::binary;
4
+ use crate::client::{ParsedQuackUri, is_version_negotiation_error, parse_quack_uri};
5
+ use crate::constants::{QUACK_V1, QUACK_V3};
6
+ use crate::messages::{
7
+ MessageHeader, MessageType, QuackMessage, decode_message, decode_message_for_version,
8
+ encode_message, encode_message_for_version,
9
+ };
10
+ use crate::sql::{format_sql, sql_literal};
11
+ use crate::vector::{
12
+ decode_data_chunk, encode_data_chunk, rows_from_chunk, rows_from_chunk_with_names,
13
+ };
14
+ use crate::*;
15
+
16
+ fn some_name(name: &str) -> Option<String> {
17
+ Some(name.to_string())
18
+ }
19
+
20
+ fn row(entries: Vec<(&str, Value)>) -> Row {
21
+ let mut row = Row::new();
22
+ for (key, value) in entries {
23
+ row.insert(key.to_string(), value);
24
+ }
25
+ row
26
+ }
27
+
28
+ #[test]
29
+ fn binary_round_trips_objects_and_primitives() {
30
+ let mut writer = binary::BinaryWriter::new();
31
+ writer
32
+ .write_object(|object| {
33
+ object.write_field(1, |object| object.write_string("hello"))?;
34
+ object.write_field(2, |object| object.write_sleb(-12345i64))?;
35
+ object.write_field(3, |object| object.write_uleb(987654321u64))?;
36
+ object.write_field(4, |object| {
37
+ object.write_huge_int_parts(binary::split_signed_huge_int(-42))
38
+ })?;
39
+ Ok(())
40
+ })
41
+ .unwrap();
42
+
43
+ let bytes = writer.into_bytes();
44
+ let mut reader = binary::BinaryReader::new(&bytes);
45
+ let decoded = reader
46
+ .read_object(|object| {
47
+ Ok((
48
+ object.read_required_field(1, |object| object.read_string())?,
49
+ object.read_required_field(2, |object| object.read_sleb_i64())?,
50
+ object.read_required_field(3, |object| object.read_uleb_u64())?,
51
+ object.read_required_field(4, |object| object.read_huge_int_parts())?,
52
+ ))
53
+ })
54
+ .unwrap();
55
+ reader.assert_eof().unwrap();
56
+
57
+ assert_eq!(decoded.0, "hello");
58
+ assert_eq!(decoded.1, -12345);
59
+ assert_eq!(decoded.2, 987654321);
60
+ assert_eq!(binary::combine_signed_huge_int(decoded.3), -42);
61
+ }
62
+
63
+ #[test]
64
+ fn data_chunk_round_trips_flat_nested_and_decimal_values() {
65
+ let struct_type = LogicalTypes::r#struct(vec![
66
+ ChildType {
67
+ name: "x".to_string(),
68
+ logical_type: LogicalTypes::integer(),
69
+ },
70
+ ChildType {
71
+ name: "y".to_string(),
72
+ logical_type: LogicalTypes::varchar(),
73
+ },
74
+ ]);
75
+ let mut point0 = IndexMap::new();
76
+ point0.insert("x".to_string(), Value::Int(1));
77
+ point0.insert("y".to_string(), Value::String("a".to_string()));
78
+ let mut point2 = IndexMap::new();
79
+ point2.insert("x".to_string(), Value::Int(3));
80
+ point2.insert("y".to_string(), Value::Null);
81
+
82
+ let chunk = data_chunk(vec![
83
+ column(
84
+ LogicalTypes::boolean(),
85
+ vec![Value::Bool(true), Value::Bool(false), Value::Null],
86
+ some_name("flag"),
87
+ ),
88
+ column(
89
+ LogicalTypes::bigint(),
90
+ vec![Value::Int(1), Value::Int(2), Value::Null],
91
+ some_name("amount"),
92
+ ),
93
+ column(
94
+ LogicalTypes::decimal(10, 2),
95
+ vec![
96
+ Value::String("12.34".to_string()),
97
+ Value::Decimal(DecimalValue {
98
+ value: -500,
99
+ width: 10,
100
+ scale: 2,
101
+ }),
102
+ Value::Null,
103
+ ],
104
+ some_name("price"),
105
+ ),
106
+ column(
107
+ LogicalTypes::list(LogicalTypes::integer()),
108
+ vec![
109
+ Value::List(vec![Value::Int(1), Value::Int(2)]),
110
+ Value::Null,
111
+ Value::List(vec![]),
112
+ ],
113
+ some_name("items"),
114
+ ),
115
+ column(
116
+ LogicalTypes::array(LogicalTypes::varchar(), 2),
117
+ vec![
118
+ Value::List(vec![Value::String("a".into()), Value::String("b".into())]),
119
+ Value::Null,
120
+ Value::List(vec![Value::String("c".into()), Value::String("d".into())]),
121
+ ],
122
+ some_name("pair"),
123
+ ),
124
+ column(
125
+ struct_type,
126
+ vec![Value::Struct(point0), Value::Null, Value::Struct(point2)],
127
+ some_name("point"),
128
+ ),
129
+ ])
130
+ .unwrap();
131
+
132
+ let mut writer = binary::BinaryWriter::new();
133
+ encode_data_chunk(&mut writer, &chunk).unwrap();
134
+ let bytes = writer.into_bytes();
135
+ let mut reader = binary::BinaryReader::new(&bytes);
136
+ let decoded = decode_data_chunk(&mut reader).unwrap();
137
+ let rows = rows_from_chunk_with_names(&decoded, chunk.column_names.as_ref().unwrap()).unwrap();
138
+
139
+ assert_eq!(rows[0]["flag"], Value::Bool(true));
140
+ assert_eq!(rows[1]["flag"], Value::Bool(false));
141
+ assert_eq!(rows[2]["flag"], Value::Null);
142
+ assert_eq!(rows[0]["amount"], Value::Int(1));
143
+ match &rows[0]["price"] {
144
+ Value::Decimal(value) => assert_eq!(decimal_to_string(value), "12.34"),
145
+ other => panic!("unexpected decimal value {other:?}"),
146
+ }
147
+ match &rows[1]["price"] {
148
+ Value::Decimal(value) => assert_eq!(decimal_to_string(value), "-5.00"),
149
+ other => panic!("unexpected decimal value {other:?}"),
150
+ }
151
+ assert_eq!(
152
+ rows[0]["items"],
153
+ Value::List(vec![Value::Int(1), Value::Int(2)])
154
+ );
155
+ assert_eq!(rows[1]["items"], Value::Null);
156
+ assert_eq!(
157
+ rows[2]["pair"],
158
+ Value::List(vec![Value::String("c".into()), Value::String("d".into())])
159
+ );
160
+ }
161
+
162
+ #[test]
163
+ fn messages_round_trip_prepare_response_with_results() {
164
+ let chunk = data_chunk(vec![
165
+ column(
166
+ LogicalTypes::integer(),
167
+ vec![Value::Int(1), Value::Null, Value::Int(3)],
168
+ some_name("id"),
169
+ ),
170
+ column(
171
+ LogicalTypes::varchar(),
172
+ vec![
173
+ Value::String("one".into()),
174
+ Value::Null,
175
+ Value::String("three".into()),
176
+ ],
177
+ some_name("label"),
178
+ ),
179
+ ])
180
+ .unwrap();
181
+ let message = QuackMessage::PrepareResponse {
182
+ header: MessageHeader::new(MessageType::PrepareResponse)
183
+ .with_connection("conn-1")
184
+ .with_client_query_id(7),
185
+ result_types: chunk.types.clone(),
186
+ result_names: vec!["id".to_string(), "label".to_string()],
187
+ needs_more_fetch: false,
188
+ results: vec![chunk],
189
+ result_uuid: binary::HugeIntParts {
190
+ upper: 123,
191
+ lower: 456,
192
+ },
193
+ };
194
+
195
+ let decoded = decode_message(&encode_message(&message).unwrap()).unwrap();
196
+
197
+ match decoded {
198
+ QuackMessage::PrepareResponse {
199
+ header,
200
+ result_names,
201
+ results,
202
+ result_uuid,
203
+ ..
204
+ } => {
205
+ assert_eq!(header.connection_id.as_deref(), Some("conn-1"));
206
+ assert_eq!(header.client_query_id, Some(7));
207
+ assert_eq!(result_names, vec!["id", "label"]);
208
+ assert_eq!(
209
+ result_uuid,
210
+ binary::HugeIntParts {
211
+ upper: 123,
212
+ lower: 456
213
+ }
214
+ );
215
+ let rows = rows_from_chunk_with_names(&results[0], &result_names).unwrap();
216
+ assert_eq!(
217
+ rows,
218
+ vec![
219
+ row(vec![
220
+ ("id", Value::Int(1)),
221
+ ("label", Value::String("one".into()))
222
+ ]),
223
+ row(vec![("id", Value::Null), ("label", Value::Null)]),
224
+ row(vec![
225
+ ("id", Value::Int(3)),
226
+ ("label", Value::String("three".into()))
227
+ ]),
228
+ ]
229
+ );
230
+ }
231
+ other => panic!("unexpected message {other:?}"),
232
+ }
233
+ }
234
+
235
+ #[test]
236
+ fn protocol_v1_connection_request_omits_v3_lease_fields() {
237
+ let message = QuackMessage::ConnectionRequest {
238
+ header: MessageHeader::new(MessageType::ConnectionRequest),
239
+ auth_string: Some("token".to_string()),
240
+ client_duckdb_version: Some("v1.5.3".to_string()),
241
+ client_platform: Some("linux_amd64".to_string()),
242
+ min_supported_quack_version: QUACK_V1,
243
+ max_supported_quack_version: QUACK_V1,
244
+ client_id: Some("must-not-be-serialized".to_string()),
245
+ heartbeat_timeout_seconds: 60,
246
+ };
247
+
248
+ let decoded = decode_message_for_version(
249
+ &encode_message_for_version(&message, QUACK_V1).unwrap(),
250
+ QUACK_V1,
251
+ )
252
+ .unwrap();
253
+ match decoded {
254
+ QuackMessage::ConnectionRequest {
255
+ client_id,
256
+ heartbeat_timeout_seconds,
257
+ min_supported_quack_version,
258
+ max_supported_quack_version,
259
+ ..
260
+ } => {
261
+ assert_eq!(client_id, None);
262
+ assert_eq!(heartbeat_timeout_seconds, 0);
263
+ assert_eq!(min_supported_quack_version, QUACK_V1);
264
+ assert_eq!(max_supported_quack_version, QUACK_V1);
265
+ }
266
+ other => panic!("unexpected message {other:?}"),
267
+ }
268
+ }
269
+
270
+ #[test]
271
+ fn negotiation_retries_v1_for_legacy_handshake_failures_but_not_authentication() {
272
+ assert!(is_version_negotiation_error(&QuackError::Protocol(
273
+ "Quack HTTP request failed with 500 Internal Server Error".to_string(),
274
+ )));
275
+ assert!(is_version_negotiation_error(&QuackError::Server(
276
+ "Unsupported Quack version - server only supports version 1 of quack".to_string(),
277
+ )));
278
+ assert!(!is_version_negotiation_error(&QuackError::Server(
279
+ "Authentication failed".to_string(),
280
+ )));
281
+ }
282
+
283
+ #[test]
284
+ fn protocol_v1_prepare_and_fetch_requests_use_legacy_bodies() {
285
+ let uuid = binary::HugeIntParts {
286
+ upper: 123,
287
+ lower: 456,
288
+ };
289
+ let prepare = QuackMessage::PrepareRequest {
290
+ header: MessageHeader::new(MessageType::PrepareRequest).with_connection("conn-v1"),
291
+ sql: "SELECT 42".to_string(),
292
+ query_uuid: Some(uuid),
293
+ inline_rows: Some(1_000),
294
+ };
295
+ let fetch = QuackMessage::FetchRequest {
296
+ header: MessageHeader::new(MessageType::FetchRequest).with_connection("conn-v1"),
297
+ result_uuid: uuid,
298
+ batch_index: Some(7),
299
+ ack_index: Some(6),
300
+ };
301
+
302
+ match decode_message_for_version(
303
+ &encode_message_for_version(&prepare, QUACK_V1).unwrap(),
304
+ QUACK_V1,
305
+ )
306
+ .unwrap()
307
+ {
308
+ QuackMessage::PrepareRequest {
309
+ sql,
310
+ query_uuid,
311
+ inline_rows,
312
+ ..
313
+ } => {
314
+ assert_eq!(sql, "SELECT 42");
315
+ assert_eq!(query_uuid, None);
316
+ assert_eq!(inline_rows, None);
317
+ }
318
+ other => panic!("unexpected message {other:?}"),
319
+ }
320
+ match decode_message_for_version(
321
+ &encode_message_for_version(&fetch, QUACK_V1).unwrap(),
322
+ QUACK_V1,
323
+ )
324
+ .unwrap()
325
+ {
326
+ QuackMessage::FetchRequest {
327
+ result_uuid,
328
+ batch_index,
329
+ ack_index,
330
+ ..
331
+ } => {
332
+ assert_eq!(result_uuid, uuid);
333
+ assert_eq!(batch_index, None);
334
+ assert_eq!(ack_index, None);
335
+ }
336
+ other => panic!("unexpected message {other:?}"),
337
+ }
338
+ }
339
+
340
+ #[test]
341
+ fn protocol_v1_fetch_and_append_round_trip_legacy_chunk_wrappers() {
342
+ let chunk = data_chunk(vec![column(
343
+ LogicalTypes::integer(),
344
+ vec![Value::Int(41), Value::Int(42)],
345
+ some_name("answer"),
346
+ )])
347
+ .unwrap();
348
+ let fetch = QuackMessage::FetchResponse {
349
+ header: MessageHeader::new(MessageType::FetchResponse).with_connection("conn-v1"),
350
+ results: vec![chunk.clone()],
351
+ total_batches: Some(99),
352
+ batch_index: None,
353
+ };
354
+ let append = QuackMessage::AppendRequest {
355
+ header: MessageHeader::new(MessageType::SendDataRequest).with_connection("conn-v1"),
356
+ schema_name: Some("main".to_string()),
357
+ table_name: "answers".to_string(),
358
+ append_chunk: chunk,
359
+ };
360
+
361
+ match decode_message_for_version(
362
+ &encode_message_for_version(&fetch, QUACK_V1).unwrap(),
363
+ QUACK_V1,
364
+ )
365
+ .unwrap()
366
+ {
367
+ QuackMessage::FetchResponse {
368
+ results,
369
+ total_batches,
370
+ batch_index,
371
+ ..
372
+ } => {
373
+ assert_eq!(results.len(), 1);
374
+ assert_eq!(total_batches, None);
375
+ assert_eq!(batch_index, None);
376
+ }
377
+ other => panic!("unexpected message {other:?}"),
378
+ }
379
+ match decode_message_for_version(
380
+ &encode_message_for_version(&append, QUACK_V1).unwrap(),
381
+ QUACK_V1,
382
+ )
383
+ .unwrap()
384
+ {
385
+ QuackMessage::AppendRequest {
386
+ schema_name,
387
+ table_name,
388
+ append_chunk,
389
+ ..
390
+ } => {
391
+ assert_eq!(schema_name.as_deref(), Some("main"));
392
+ assert_eq!(table_name, "answers");
393
+ assert_eq!(append_chunk.row_count, 2);
394
+ }
395
+ other => panic!("unexpected message {other:?}"),
396
+ }
397
+ }
398
+
399
+ #[test]
400
+ fn protocol_v3_connection_request_round_trips_lease_fields() {
401
+ let message = QuackMessage::ConnectionRequest {
402
+ header: MessageHeader::new(MessageType::ConnectionRequest),
403
+ auth_string: Some("token".to_string()),
404
+ client_duckdb_version: Some("v2.0.0-alpha39998".to_string()),
405
+ client_platform: Some("linux_arm64".to_string()),
406
+ min_supported_quack_version: QUACK_V3,
407
+ max_supported_quack_version: QUACK_V3,
408
+ client_id: Some("quaxy-worker".to_string()),
409
+ heartbeat_timeout_seconds: 60,
410
+ };
411
+
412
+ assert_eq!(
413
+ decode_message(&encode_message(&message).unwrap()).unwrap(),
414
+ message
415
+ );
416
+ }
417
+
418
+ #[test]
419
+ fn protocol_v3_prepare_and_fetch_requests_round_trip_indices() {
420
+ let query_uuid = binary::HugeIntParts {
421
+ upper: 123,
422
+ lower: 456,
423
+ };
424
+ let prepare = QuackMessage::PrepareRequest {
425
+ header: MessageHeader::new(MessageType::PrepareRequest).with_connection("conn-1"),
426
+ sql: "SELECT 42".to_string(),
427
+ query_uuid: Some(query_uuid),
428
+ inline_rows: Some(1_000),
429
+ };
430
+ let fetch = QuackMessage::FetchRequest {
431
+ header: MessageHeader::new(MessageType::FetchRequest).with_connection("conn-1"),
432
+ result_uuid: query_uuid,
433
+ batch_index: Some(7),
434
+ ack_index: Some(6),
435
+ };
436
+
437
+ assert_eq!(
438
+ decode_message(&encode_message(&prepare).unwrap()).unwrap(),
439
+ prepare
440
+ );
441
+ assert_eq!(
442
+ decode_message(&encode_message(&fetch).unwrap()).unwrap(),
443
+ fetch
444
+ );
445
+ }
446
+
447
+ #[test]
448
+ fn protocol_v3_fetch_response_round_trips_raw_chunk_payloads() {
449
+ let chunk = data_chunk(vec![column(
450
+ LogicalTypes::integer(),
451
+ vec![Value::Int(41), Value::Int(42)],
452
+ some_name("answer"),
453
+ )])
454
+ .unwrap();
455
+ let message = QuackMessage::FetchResponse {
456
+ header: MessageHeader::new(MessageType::FetchResponse).with_connection("conn-1"),
457
+ results: vec![chunk],
458
+ total_batches: None,
459
+ batch_index: Some(1),
460
+ };
461
+
462
+ let decoded = decode_message(&encode_message(&message).unwrap()).unwrap();
463
+ match decoded {
464
+ QuackMessage::FetchResponse {
465
+ results,
466
+ total_batches,
467
+ batch_index,
468
+ ..
469
+ } => {
470
+ assert_eq!(total_batches, None);
471
+ assert_eq!(batch_index, Some(1));
472
+ assert_eq!(
473
+ rows_from_chunk_with_names(&results[0], &["answer".to_string()]).unwrap(),
474
+ vec![
475
+ row(vec![("answer", Value::Int(41))]),
476
+ row(vec![("answer", Value::Int(42))]),
477
+ ]
478
+ );
479
+ }
480
+ other => panic!("unexpected message {other:?}"),
481
+ }
482
+ }
483
+
484
+ #[test]
485
+ fn protocol_v3_heartbeat_round_trips() {
486
+ let message = QuackMessage::HeartbeatRequest {
487
+ header: MessageHeader::new(MessageType::HeartbeatRequest).with_connection("conn-1"),
488
+ };
489
+
490
+ assert_eq!(
491
+ decode_message(&encode_message(&message).unwrap()).unwrap(),
492
+ message
493
+ );
494
+ }
495
+
496
+ #[test]
497
+ fn parses_documented_quack_uri_forms() {
498
+ assert_eq!(
499
+ parse_quack_uri("localhost:9494", None).unwrap(),
500
+ ParsedQuackUri {
501
+ base_url: "http://localhost:9494".to_string(),
502
+ host: "localhost".to_string(),
503
+ port: 9494,
504
+ ssl: false,
505
+ }
506
+ );
507
+ assert_eq!(
508
+ parse_quack_uri("localhost", None).unwrap().base_url,
509
+ "http://localhost:9494"
510
+ );
511
+ assert_eq!(
512
+ parse_quack_uri("quack:localhost", None).unwrap().base_url,
513
+ "http://localhost:9494"
514
+ );
515
+ assert_eq!(
516
+ parse_quack_uri("quack://localhost:9000", None)
517
+ .unwrap()
518
+ .base_url,
519
+ "http://localhost:9000"
520
+ );
521
+ assert_eq!(
522
+ parse_quack_uri("quack:[::1]:9494", None).unwrap().base_url,
523
+ "http://[::1]:9494"
524
+ );
525
+ }
526
+
527
+ #[test]
528
+ fn formats_sql_literals_and_placeholders() {
529
+ assert_eq!(
530
+ sql_literal(&SqlParameter::from("can't")).unwrap(),
531
+ "'can''t'"
532
+ );
533
+ assert_eq!(sql_literal(&SqlParameter::from(true)).unwrap(), "TRUE");
534
+ assert_eq!(
535
+ sql_literal(&SqlParameter::from(Value::HugeInt(42))).unwrap(),
536
+ "42"
537
+ );
538
+ assert_eq!(
539
+ sql_literal(&SqlParameter::List(vec![
540
+ SqlParameter::from(1),
541
+ SqlParameter::from("two"),
542
+ SqlParameter::from(Value::Null),
543
+ ]))
544
+ .unwrap(),
545
+ "[1, 'two', NULL]"
546
+ );
547
+ assert_eq!(
548
+ sql_literal(&SqlParameter::from(
549
+ date_from_iso_date("2020-01-02").unwrap()
550
+ ))
551
+ .unwrap(),
552
+ "DATE '2020-01-02'"
553
+ );
554
+ assert_eq!(
555
+ sql_literal(&SqlParameter::from(timestamp_value(
556
+ 1,
557
+ TimestampUnit::Seconds,
558
+ false,
559
+ )))
560
+ .unwrap(),
561
+ "TIMESTAMP '1970-01-01 00:00:01.000'"
562
+ );
563
+ assert_eq!(
564
+ sql_literal(&SqlParameter::from(interval_value(1, 2, 3))).unwrap(),
565
+ "INTERVAL '1 months 2 days 3 microseconds'"
566
+ );
567
+
568
+ assert_eq!(
569
+ format_sql(
570
+ "SELECT '?' AS s, ? AS v -- ?\n",
571
+ Some(&SqlParameters::Positional(vec![SqlParameter::from(42)])),
572
+ )
573
+ .unwrap(),
574
+ "SELECT '?' AS s, 42 AS v -- ?\n"
575
+ );
576
+
577
+ let mut named = IndexMap::new();
578
+ named.insert("id".to_string(), SqlParameter::from(7));
579
+ named.insert("label".to_string(), SqlParameter::from("seven"));
580
+ assert_eq!(
581
+ format_sql(
582
+ "SELECT :id::INTEGER AS id, :label AS label",
583
+ Some(&SqlParameters::Named(named)),
584
+ )
585
+ .unwrap(),
586
+ "SELECT 7::INTEGER AS id, 'seven' AS label"
587
+ );
588
+
589
+ assert!(
590
+ format_sql(
591
+ "SELECT ?, ?",
592
+ Some(&SqlParameters::Positional(vec![SqlParameter::from(1)])),
593
+ )
594
+ .is_err()
595
+ );
596
+ }
597
+
598
+ #[test]
599
+ fn builds_chunks_from_rows() {
600
+ let rows = vec![
601
+ row(vec![
602
+ ("id", Value::Int(1)),
603
+ ("label", Value::String("one".into())),
604
+ ("amount", Value::String("12.34".into())),
605
+ ]),
606
+ row(vec![
607
+ ("id", Value::Int(2)),
608
+ ("label", Value::String("two".into())),
609
+ ("amount", Value::Null),
610
+ ]),
611
+ ];
612
+ let chunk = data_chunk_from_rows(
613
+ &rows,
614
+ Some(vec![
615
+ ColumnDefinition {
616
+ name: "id".to_string(),
617
+ logical_type: LogicalTypes::integer(),
618
+ },
619
+ ColumnDefinition {
620
+ name: "label".to_string(),
621
+ logical_type: LogicalTypes::varchar(),
622
+ },
623
+ ColumnDefinition {
624
+ name: "amount".to_string(),
625
+ logical_type: LogicalTypes::decimal(10, 2),
626
+ },
627
+ ]),
628
+ )
629
+ .unwrap();
630
+
631
+ assert_eq!(
632
+ chunk.types.iter().map(|typ| typ.id).collect::<Vec<_>>(),
633
+ vec![
634
+ LogicalTypeId::Integer,
635
+ LogicalTypeId::Varchar,
636
+ LogicalTypeId::Decimal,
637
+ ]
638
+ );
639
+ assert_eq!(rows_from_chunk(&chunk).unwrap(), rows);
640
+ }
641
+
642
+ #[test]
643
+ fn converts_quack_values_to_json() {
644
+ assert_eq!(
645
+ to_json_value(&Value::HugeInt(9007199254740993), JsonOptions::default()).unwrap(),
646
+ serde_json::json!("9007199254740993")
647
+ );
648
+ assert_eq!(
649
+ to_json_value(&Value::Bytes(vec![104, 105]), JsonOptions::default()).unwrap(),
650
+ serde_json::json!("aGk=")
651
+ );
652
+ assert_eq!(
653
+ to_json_value(
654
+ &decimal_value("12.34", 10, 2).unwrap(),
655
+ JsonOptions::default()
656
+ )
657
+ .unwrap(),
658
+ serde_json::json!("12.34")
659
+ );
660
+ assert_eq!(
661
+ to_json_value(
662
+ &date_from_iso_date("2020-01-02").unwrap(),
663
+ JsonOptions::default()
664
+ )
665
+ .unwrap(),
666
+ serde_json::json!("2020-01-02")
667
+ );
668
+ assert_eq!(
669
+ to_json_value(
670
+ &time_value(1_234_567, TimeUnit::Micros),
671
+ JsonOptions::default()
672
+ )
673
+ .unwrap(),
674
+ serde_json::json!("00:00:01.234567")
675
+ );
676
+ assert_eq!(
677
+ to_json_value(
678
+ &timestamp_value(1_234_567, TimestampUnit::Micros, false),
679
+ JsonOptions::default(),
680
+ )
681
+ .unwrap(),
682
+ serde_json::json!("1970-01-01T00:00:01.234567Z")
683
+ );
684
+ }
685
+
686
+ #[test]
687
+ fn decodes_error_response_with_exception_type_and_extra_info() {
688
+ // An ERROR_RESPONSE as DuckDB 2.0 servers send it: the message followed by
689
+ // exception_type (2), extra_info (3) and must_invalidate (4).
690
+ let mut writer = binary::BinaryWriter::new();
691
+ crate::messages::encode_header(
692
+ &mut writer,
693
+ &MessageHeader {
694
+ message_type: MessageType::ErrorResponse,
695
+ connection_id: some_name("conn"),
696
+ client_query_id: None,
697
+ },
698
+ )
699
+ .unwrap();
700
+ writer
701
+ .write_object(|object| {
702
+ object.write_field(1, |object| {
703
+ object.write_string("Catalog Error: Table with name t does not exist!")
704
+ })?;
705
+ object.write_field(2, |object| object.write_string("Catalog"))?;
706
+ object.write_field(3, |object| {
707
+ object.write_list(
708
+ &[("name", "t"), ("type", "Table")],
709
+ |entry, (key, value), _| {
710
+ entry.write_object(|entry| {
711
+ entry.write_field(0, |entry| entry.write_string(key))?;
712
+ entry.write_field(1, |entry| entry.write_string(value))
713
+ })
714
+ },
715
+ )
716
+ })?;
717
+ object.write_field(4, |object| object.write_bool(true))
718
+ })
719
+ .unwrap();
720
+
721
+ match decode_message_for_version(&writer.into_bytes(), QUACK_V3).unwrap() {
722
+ QuackMessage::ErrorResponse { message, .. } => {
723
+ assert_eq!(message, "Catalog Error: Table with name t does not exist!")
724
+ }
725
+ other => panic!("expected ERROR_RESPONSE, got {other:?}"),
726
+ }
727
+ }
728
+
729
+ #[test]
730
+ fn decodes_error_response_with_only_a_message() {
731
+ // An ERROR_RESPONSE as DuckDB 1.x servers send it: the message and nothing
732
+ // else. It must decode under both protocol versions.
733
+ for version in [QUACK_V1, QUACK_V3] {
734
+ let mut writer = binary::BinaryWriter::new();
735
+ crate::messages::encode_header(
736
+ &mut writer,
737
+ &MessageHeader {
738
+ message_type: MessageType::ErrorResponse,
739
+ connection_id: some_name("conn"),
740
+ client_query_id: None,
741
+ },
742
+ )
743
+ .unwrap();
744
+ writer
745
+ .write_object(|object| {
746
+ object.write_field(1, |object| {
747
+ object.write_string("Catalog Error: Table with name t does not exist!")
748
+ })
749
+ })
750
+ .unwrap();
751
+
752
+ match decode_message_for_version(&writer.into_bytes(), version).unwrap() {
753
+ QuackMessage::ErrorResponse { message, .. } => {
754
+ assert_eq!(message, "Catalog Error: Table with name t does not exist!")
755
+ }
756
+ other => panic!("expected ERROR_RESPONSE, got {other:?}"),
757
+ }
758
+ }
759
+ }