duckdb 1.5.3.0 → 1.5.5.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/CLAUDE.md +4 -0
  4. data/README.md +52 -0
  5. data/duckdb.gemspec +1 -1
  6. data/ext/duckdb/aggregate_function.c +0 -1
  7. data/ext/duckdb/appender.c +17 -0
  8. data/ext/duckdb/arrow_array_stream.c +226 -0
  9. data/ext/duckdb/arrow_array_stream.h +61 -0
  10. data/ext/duckdb/arrow_import.c +165 -0
  11. data/ext/duckdb/arrow_import.h +6 -0
  12. data/ext/duckdb/blob.c +1 -1
  13. data/ext/duckdb/blob.h +1 -2
  14. data/ext/duckdb/config.c +1 -1
  15. data/ext/duckdb/config.h +1 -1
  16. data/ext/duckdb/connection.c +26 -4
  17. data/ext/duckdb/converter.h +1 -0
  18. data/ext/duckdb/conveter.c +39 -9
  19. data/ext/duckdb/data_chunk.c +10 -0
  20. data/ext/duckdb/data_chunk.h +1 -0
  21. data/ext/duckdb/duckdb.c +13 -11
  22. data/ext/duckdb/error.c +13 -1
  23. data/ext/duckdb/error.h +2 -3
  24. data/ext/duckdb/function_executor.c +308 -2
  25. data/ext/duckdb/function_executor.h +44 -0
  26. data/ext/duckdb/function_vector.c +14 -0
  27. data/ext/duckdb/prepared_statement.c +141 -5
  28. data/ext/duckdb/result.c +49 -3
  29. data/ext/duckdb/result.h +11 -0
  30. data/ext/duckdb/ruby-duckdb.h +3 -0
  31. data/ext/duckdb/scalar_function.c +97 -29
  32. data/ext/duckdb/scalar_function.h +2 -4
  33. data/ext/duckdb/scalar_function_bind_info.c +13 -13
  34. data/ext/duckdb/scalar_function_bind_info.h +1 -1
  35. data/ext/duckdb/scalar_function_set.c +9 -9
  36. data/ext/duckdb/scalar_function_set.h +2 -2
  37. data/ext/duckdb/table_description.c +19 -19
  38. data/ext/duckdb/table_description.h +1 -1
  39. data/ext/duckdb/table_function.c +94 -28
  40. data/ext/duckdb/table_function.h +2 -2
  41. data/ext/duckdb/table_function_bind_info.c +20 -20
  42. data/ext/duckdb/table_function_bind_info.h +2 -2
  43. data/ext/duckdb/table_function_function_info.c +5 -5
  44. data/ext/duckdb/table_function_function_info.h +2 -2
  45. data/ext/duckdb/table_function_init_info.c +70 -5
  46. data/ext/duckdb/table_function_init_info.h +2 -2
  47. data/ext/duckdb/util.c +8 -0
  48. data/ext/duckdb/util.h +1 -0
  49. data/ext/duckdb/value.c +526 -1
  50. data/lib/duckdb/appender.rb +23 -0
  51. data/lib/duckdb/arrow_array_stream.rb +33 -0
  52. data/lib/duckdb/connection.rb +73 -0
  53. data/lib/duckdb/converter/int_to_sym.rb +54 -1
  54. data/lib/duckdb/converter.rb +7 -7
  55. data/lib/duckdb/error.rb +26 -0
  56. data/lib/duckdb/function_type_validation.rb +1 -0
  57. data/lib/duckdb/logical_type.rb +6 -3
  58. data/lib/duckdb/prepared_statement.rb +32 -0
  59. data/lib/duckdb/scalar_function.rb +6 -6
  60. data/lib/duckdb/value.rb +400 -0
  61. data/lib/duckdb/version.rb +1 -1
  62. data/lib/duckdb.rb +2 -0
  63. metadata +10 -3
@@ -383,8 +383,81 @@ module DuckDB
383
383
  register_table_function(tf)
384
384
  end
385
385
 
386
+ # [EXPERIMENTAL] Appends an Arrow producer into an existing table.
387
+ #
388
+ # Reads +producer+ (any object responding to +#arrow_c_stream+, such as a
389
+ # ruby-polars +DataFrame+ or a +DuckDB::Result+) as an Arrow C stream and
390
+ # appends its chunks into the existing table +table+. The producer's Arrow
391
+ # columns must line up with the table's columns positionally and by count.
392
+ # DuckDB casts compatible column types (e.g. INTEGER into a BIGINT column);
393
+ # a type that cannot be cast (e.g. a non-numeric VARCHAR into an INTEGER
394
+ # column) or a column-count mismatch raises +DuckDB::Error+.
395
+ #
396
+ # This is not transactional: a schema mismatch fails before any rows are
397
+ # written, but a rarer mid-stream failure can leave earlier chunks
398
+ # appended. Wrap the call in your own transaction for all-or-nothing.
399
+ #
400
+ # This API is built on DuckDB's unstable Arrow C API and may change in any
401
+ # minor release.
402
+ #
403
+ # @param table [String] the name of the existing target table
404
+ # @param producer [#arrow_c_stream] the Arrow producer
405
+ # @raise [TypeError] if +producer+ does not respond to +#arrow_c_stream+
406
+ # @return [Integer] the number of rows appended
407
+ #
408
+ # @example Load a Polars DataFrame into a table
409
+ # con.query('CREATE TABLE t (id INTEGER, name VARCHAR)')
410
+ # con.append_arrow('t', polars_df)
411
+ #
412
+ def append_arrow(table, producer)
413
+ unless producer.respond_to?(:arrow_c_stream)
414
+ raise TypeError, "Arrow producer must respond to #arrow_c_stream, got #{producer.class}"
415
+ end
416
+
417
+ stream = producer.arrow_c_stream # keep the producer's stream alive for the duration
418
+ address = stream.to_i
419
+ begin
420
+ append_arrow_chunks(table, address)
421
+ ensure
422
+ _arrow_release(address)
423
+ end
424
+ end
425
+
426
+ # Returns the names of the tables referenced by the given SQL query,
427
+ # without executing it.
428
+ #
429
+ # @param query [String] the SQL query to inspect
430
+ # @param qualified [Boolean] if true, returns each table reference as
431
+ # written in the query, keeping any catalog/schema qualification;
432
+ # if false (default), bare table names only
433
+ # @return [Array<String>] the referenced table names, in unspecified order
434
+ # @raise [DuckDB::Error] if the query cannot be parsed
435
+ #
436
+ # @example
437
+ # con.table_names('SELECT * FROM users u JOIN orders o ON u.id = o.user_id').sort
438
+ # #=> ["orders", "users"]
439
+ # con.table_names('SELECT * FROM memory.main.users', qualified: true)
440
+ # #=> ["memory.main.users"]
441
+ def table_names(query, qualified: false)
442
+ _get_table_names(query, qualified).to_ruby
443
+ end
444
+
386
445
  private
387
446
 
447
+ # Drives the Arrow stream at +address+ chunk by chunk into +table+,
448
+ # returning the number of rows appended.
449
+ def append_arrow_chunks(table, address)
450
+ converted_schema = _arrow_converted_schema(address)
451
+ rows = 0
452
+ appender(table) do |app|
453
+ while (chunk = _arrow_next_chunk(address, converted_schema))
454
+ rows += chunk.size
455
+ app.append_data_chunk(chunk)
456
+ end
457
+ end
458
+ rows
459
+ end
460
+
388
461
  def run_appender_block(appender, &)
389
462
  return appender unless block_given?
390
463
 
@@ -73,11 +73,64 @@ module DuckDB
73
73
  35 => :bignum,
74
74
  36 => :sqlnull,
75
75
  37 => :string_literal,
76
- 38 => :integer_literal
76
+ 38 => :integer_literal,
77
+ 39 => :time_ns
77
78
  }.freeze
78
79
 
80
+ ERROR_TYPES = %i[
81
+ invalid
82
+ out_of_range
83
+ conversion
84
+ unknown_type
85
+ decimal
86
+ mismatch_type
87
+ divide_by_zero
88
+ object_size
89
+ invalid_type
90
+ serialization
91
+ transaction
92
+ not_implemented
93
+ expression
94
+ catalog
95
+ parser
96
+ planner
97
+ scheduler
98
+ executor
99
+ constraint
100
+ index
101
+ stat
102
+ connection
103
+ syntax
104
+ settings
105
+ binder
106
+ network
107
+ optimizer
108
+ null_pointer
109
+ io
110
+ interrupt
111
+ fatal
112
+ internal
113
+ invalid_input
114
+ out_of_memory
115
+ permission
116
+ parameter_not_resolved
117
+ parameter_not_allowed
118
+ dependency
119
+ http
120
+ missing_extension
121
+ autoload
122
+ sequence
123
+ invalid_configuration
124
+ ].freeze
125
+
79
126
  module_function
80
127
 
128
+ def error_type_to_sym(val) # :nodoc:
129
+ raise DuckDB::Error, "Unknown error type: #{val}" if val >= ERROR_TYPES.size
130
+
131
+ ERROR_TYPES[val]
132
+ end
133
+
81
134
  def statement_type_to_sym(val) # :nodoc:
82
135
  raise DuckDB::Error, "Unknown statement type: #{val}" if val >= STATEMENT_TYPES.size
83
136
 
@@ -83,18 +83,18 @@ module DuckDB
83
83
 
84
84
  def _to_time_from_duckdb_timestamp_ns(time)
85
85
  _to_time_from_duckdb_timestamp_s(time / 1_000_000_000).then do |tm|
86
- _to_time(tm.year, tm.month, tm.day, tm.hour, tm.min, tm.sec, time % 1_000_000_000 / 1000)
86
+ _to_time(tm.year, tm.month, tm.day, tm.hour, tm.min, tm.sec, Rational(time % 1_000_000_000, 1000))
87
87
  end
88
88
  end
89
89
 
90
90
  def _to_time_from_duckdb_time_ns(nanos)
91
91
  hour = nanos / 3_600_000_000_000
92
- nanos %= 3_600_000_000_000
93
- min = nanos / 60_000_000_000
94
- nanos %= 60_000_000_000
95
- sec = nanos / 1_000_000_000
96
- microsecond = (nanos % 1_000_000_000) / 1_000
97
- _to_time_from_duckdb_time(hour, min, sec, microsecond)
92
+ min = nanos % 3_600_000_000_000 / 60_000_000_000
93
+ sec = nanos % 60_000_000_000 / 1_000_000_000
94
+ nsec = nanos % 1_000_000_000
95
+ return Time.utc(1970, 1, 1, hour, min, sec, Rational(nsec, 1_000)) if default_timezone_utc?
96
+
97
+ Time.parse(format('%<hour>02d:%<min>02d:%<sec>02d.%<nsec>09d', hour: hour, min: min, sec: sec, nsec: nsec))
98
98
  end
99
99
 
100
100
  def _to_time_from_duckdb_time_tz(hour, min, sec, micro, timezone)
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuckDB
4
+ # The exception raised by ruby-duckdb.
5
+ class Error < StandardError
6
+ # +error_type_id+ is the raw DuckDB error type id, set by the C extension when
7
+ # a query result fails; it defaults to +nil+ for all other errors.
8
+ def initialize(message = nil, error_type_id = nil)
9
+ super(message)
10
+ @error_type_id = error_type_id
11
+ end
12
+
13
+ # Returns the DuckDB error category as a Symbol (e.g. +:constraint+,
14
+ # +:catalog+, +:parser+), or +nil+ when the error did not originate from a
15
+ # DuckDB query result (e.g. internal binding failures).
16
+ #
17
+ # begin
18
+ # con.query('INSERT INTO t VALUES (1)') # duplicate primary key
19
+ # rescue DuckDB::Error => e
20
+ # e.error_type # => :constraint
21
+ # end
22
+ def error_type
23
+ @error_type_id && Converter::IntToSym.error_type_to_sym(@error_type_id)
24
+ end
25
+ end
26
+ end
@@ -23,6 +23,7 @@ module DuckDB
23
23
  timestamp_s
24
24
  timestamp_ms
25
25
  timestamp_ns
26
+ time_ns
26
27
  time_tz
27
28
  timestamp_tz
28
29
  tinyint
@@ -7,7 +7,7 @@ module DuckDB
7
7
 
8
8
  @logical_types = {}
9
9
 
10
- {
10
+ types = {
11
11
  boolean: 1,
12
12
  tinyint: 2,
13
13
  smallint: 3,
@@ -47,8 +47,11 @@ module DuckDB
47
47
  sqlnull: 36,
48
48
  string_literal: 37,
49
49
  integer_literal: 38
50
- # time_ns: 39
51
- }.each do |method_name, type_id|
50
+ }
51
+ # duckdb_create_logical_type rejects TIME_NS on DuckDB < 1.5.0.
52
+ types[:time_ns] = 39 if Gem::Version.new(DuckDB::LIBRARY_VERSION) >= Gem::Version.new('1.5.0')
53
+
54
+ types.each do |method_name, type_id|
52
55
  define_singleton_method(method_name) do
53
56
  @logical_types[type_id] ||= DuckDB::LogicalType.new(type_id)
54
57
  end
@@ -76,6 +76,21 @@ module DuckDB
76
76
  Converter::IntToSym.type_to_sym(i)
77
77
  end
78
78
 
79
+ # returns the column type of the result set of the prepared statement
80
+ # without executing it. The argument is the column index (0-based).
81
+ # Returns :invalid if the column index is out of range.
82
+ #
83
+ # require 'duckdb'
84
+ # db = DuckDB::Database.open
85
+ # con = db.connect
86
+ # con.execute('CREATE TABLE users (id INTEGER, name VARCHAR(255))')
87
+ # stmt = con.prepared_statement('SELECT * FROM users')
88
+ # stmt.column_type(0) # => :integer
89
+ def column_type(index)
90
+ i = _column_type(index)
91
+ Converter::IntToSym.type_to_sym(i)
92
+ end
93
+
79
94
  # binds all parameters with SQL prepared statement.
80
95
  #
81
96
  # require 'duckdb'
@@ -193,6 +208,23 @@ module DuckDB
193
208
  _bind_uhugeint(index, lower, upper)
194
209
  end
195
210
 
211
+ # binds i-th parameter with a UUID value.
212
+ # The first argument is the index of the parameter (1-based).
213
+ # The second argument must be a String in canonical UUID format
214
+ # (<tt>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</tt>).
215
+ # Raises ArgumentError if the value is not a valid UUID string.
216
+ #
217
+ # require 'duckdb'
218
+ # db = DuckDB::Database.open
219
+ # con = db.connect
220
+ # con.query('CREATE TABLE uuids (id UUID)')
221
+ # stmt = DuckDB::PreparedStatement.new(con, 'INSERT INTO uuids(id) VALUES ($1)')
222
+ # stmt.bind_uuid(1, '550e8400-e29b-41d4-a716-446655440000')
223
+ # stmt.execute
224
+ def bind_uuid(index, value)
225
+ _bind_uuid(index, value)
226
+ end
227
+
196
228
  # binds i-th parameter with SQL prepared statement.
197
229
  # The first argument is index of parameter.
198
230
  # The index of first parameter is 1 not 0.
@@ -96,8 +96,8 @@ module DuckDB
96
96
 
97
97
  # Adds a parameter to the scalar function.
98
98
  # Currently supports BIGINT, BLOB, BOOLEAN, DATE, DECIMAL, DOUBLE, FLOAT, HUGEINT, INTEGER, INTERVAL, SMALLINT,
99
- # TIME, TIMESTAMP, TIMESTAMP_S, TIMESTAMP_MS, TIMESTAMP_NS, TIME_TZ, TIMESTAMP_TZ, TINYINT, UBIGINT, UHUGEINT,
100
- # UINTEGER, USMALLINT, UTINYINT, UUID, and VARCHAR types.
99
+ # TIME, TIMESTAMP, TIMESTAMP_S, TIMESTAMP_MS, TIMESTAMP_NS, TIME_NS, TIME_TZ, TIMESTAMP_TZ, TINYINT, UBIGINT,
100
+ # UHUGEINT, UINTEGER, USMALLINT, UTINYINT, UUID, and VARCHAR types.
101
101
  # For DECIMAL, pass a DuckDB::LogicalType instance created with DuckDB::LogicalType.create_decimal(width, scale).
102
102
  #
103
103
  # @param logical_type [DuckDB::LogicalType | :logical_type_symbol] the parameter type
@@ -111,8 +111,8 @@ module DuckDB
111
111
 
112
112
  # Sets the return type for the scalar function.
113
113
  # Currently supports BIGINT, BLOB, BOOLEAN, DATE, DECIMAL, DOUBLE, FLOAT, HUGEINT, INTEGER, INTERVAL, SMALLINT,
114
- # TIME, TIMESTAMP, TIMESTAMP_S, TIMESTAMP_MS, TIMESTAMP_NS, TIME_TZ, TIMESTAMP_TZ, TINYINT, UBIGINT, UHUGEINT,
115
- # UINTEGER, USMALLINT, UTINYINT, UUID, and VARCHAR types.
114
+ # TIME, TIMESTAMP, TIMESTAMP_S, TIMESTAMP_MS, TIMESTAMP_NS, TIME_NS, TIME_TZ, TIMESTAMP_TZ, TINYINT, UBIGINT,
115
+ # UHUGEINT, UINTEGER, USMALLINT, UTINYINT, UUID, and VARCHAR types.
116
116
  # For DECIMAL, pass a DuckDB::LogicalType instance created with DuckDB::LogicalType.create_decimal(width, scale).
117
117
  #
118
118
  # @param logical_type [DuckDB::LogicalType | :logical_type_symbol] the return type
@@ -144,8 +144,8 @@ module DuckDB
144
144
  # (e.g. a separator followed by a variable list of values).
145
145
  # The block receives fixed parameters positionally, then varargs as a splat (|fixed, *rest|).
146
146
  # Currently supports BIGINT, BLOB, BOOLEAN, DATE, DECIMAL, DOUBLE, FLOAT, HUGEINT, INTEGER, INTERVAL, SMALLINT,
147
- # TIME, TIMESTAMP, TIMESTAMP_S, TIMESTAMP_MS, TIMESTAMP_NS, TIME_TZ, TIMESTAMP_TZ, TINYINT, UBIGINT, UHUGEINT,
148
- # UINTEGER, USMALLINT, UTINYINT, UUID, and VARCHAR types.
147
+ # TIME, TIMESTAMP, TIMESTAMP_S, TIMESTAMP_MS, TIMESTAMP_NS, TIME_NS, TIME_TZ, TIMESTAMP_TZ, TINYINT, UBIGINT,
148
+ # UHUGEINT, UINTEGER, USMALLINT, UTINYINT, UUID, and VARCHAR types.
149
149
  # For DECIMAL, pass a DuckDB::LogicalType instance created with DuckDB::LogicalType.create_decimal(width, scale).
150
150
  #
151
151
  # @param logical_type [DuckDB::LogicalType | :logical_type_symbol] the varargs element type