duckdb 1.5.2.1 → 1.5.4.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +52 -0
  4. data/ext/duckdb/aggregate_function.c +1 -1
  5. data/ext/duckdb/aggregate_function_set.c +86 -0
  6. data/ext/duckdb/aggregate_function_set.h +14 -0
  7. data/ext/duckdb/appender.c +62 -4
  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 -3
  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 +14 -11
  22. data/ext/duckdb/error.c +1 -1
  23. data/ext/duckdb/error.h +1 -3
  24. data/ext/duckdb/extconf.rb +28 -13
  25. data/ext/duckdb/function_executor.c +308 -2
  26. data/ext/duckdb/function_executor.h +44 -0
  27. data/ext/duckdb/prepared_statement.c +38 -0
  28. data/ext/duckdb/result.c +49 -53
  29. data/ext/duckdb/result.h +11 -0
  30. data/ext/duckdb/ruby-duckdb.h +4 -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/lib/duckdb/aggregate_function.rb +7 -1
  48. data/lib/duckdb/aggregate_function_set.rb +29 -0
  49. data/lib/duckdb/appender.rb +97 -0
  50. data/lib/duckdb/arrow_array_stream.rb +33 -0
  51. data/lib/duckdb/connection.rb +139 -9
  52. data/lib/duckdb/prepared_statement.rb +35 -0
  53. data/lib/duckdb/result.rb +39 -2
  54. data/lib/duckdb/scalar_function.rb +9 -4
  55. data/lib/duckdb/scalar_function_set.rb +0 -1
  56. data/lib/duckdb/table_description.rb +7 -0
  57. data/lib/duckdb/table_name_parser.rb +58 -0
  58. data/lib/duckdb/version.rb +1 -1
  59. data/lib/duckdb.rb +3 -0
  60. metadata +11 -2
@@ -7,6 +7,11 @@ require_relative 'converter'
7
7
  module DuckDB
8
8
  # The DuckDB::Appender encapsulates DuckDB Appender.
9
9
  #
10
+ # The +table+ argument (2nd positional argument) supports dot-notation and quoting:
11
+ #
12
+ # - <tt>'schema.table'</tt> — interpreted as schema-qualified (deprecated; use +schema:+ instead)
13
+ # - <tt>'"schema.table"'</tt> or <tt>"'schema.table'"</tt> — treated as a literal table name containing a dot
14
+ #
10
15
  # require 'duckdb'
11
16
  # db = DuckDB::Database.open
12
17
  # con = db.connect
@@ -15,11 +20,21 @@ module DuckDB
15
20
  # appender.append_row(1, 'Alice')
16
21
  class Appender
17
22
  include DuckDB::Converter
23
+ include DuckDB::TableNameParser
18
24
 
19
25
  class << self
20
26
  alias from_query create_query
21
27
  end
22
28
 
29
+ def initialize(con, table_or_schema, table = nil, schema: nil, catalog: nil)
30
+ if table
31
+ warn_deprecated_3arg
32
+ _initialize(con, table_or_schema, table)
33
+ else
34
+ initialize_with_parsed_table(con, table_or_schema, schema: schema, catalog: catalog)
35
+ end
36
+ end
37
+
23
38
  # :call-seq:
24
39
  # appender.begin_row -> self
25
40
  # A nop method, provided for backwards compatibility reasons.
@@ -543,6 +558,29 @@ module DuckDB
543
558
  raise_appender_error('failed to append_uhugeint')
544
559
  end
545
560
 
561
+ # :call-seq:
562
+ # appender.append_uuid(val) -> self
563
+ #
564
+ # Appends a UUID value to the current row in the appender.
565
+ # +val+ must be a String in canonical UUID format
566
+ # (<tt>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</tt>).
567
+ # Raises ArgumentError if +val+ is not a valid UUID string.
568
+ #
569
+ # require 'duckdb'
570
+ # db = DuckDB::Database.open
571
+ # con = db.connect
572
+ # con.query('CREATE TABLE uuids (id UUID)')
573
+ # appender = con.appender('uuids')
574
+ # appender
575
+ # .append_uuid('550e8400-e29b-41d4-a716-446655440000')
576
+ # .end_row
577
+ # .flush
578
+ def append_uuid(value)
579
+ return self if _append_uuid(value)
580
+
581
+ raise_appender_error('failed to append_uuid')
582
+ end
583
+
546
584
  # call-seq:
547
585
  # appender.append_date(val) -> self
548
586
  #
@@ -661,6 +699,20 @@ module DuckDB
661
699
 
662
700
  # call-seq:
663
701
  # appender.append_data_chunk(chunk) -> self
702
+ #
703
+ # Appends a pre-filled DuckDB::DataChunk to the appender.
704
+ #
705
+ # require 'duckdb'
706
+ # db = DuckDB::Database.open
707
+ # con = db.connect
708
+ # con.query('CREATE TABLE users (id INTEGER, name VARCHAR)')
709
+ # appender = con.appender('users')
710
+ # chunk = DuckDB::DataChunk.new([DuckDB::LogicalType::INTEGER, DuckDB::LogicalType::VARCHAR])
711
+ # chunk.set_value(0, 0, 1)
712
+ # chunk.set_value(1, 0, 'Alice')
713
+ # chunk.size = 1
714
+ # appender.append_data_chunk(chunk)
715
+ # appender.flush
664
716
  def append_data_chunk(chunk)
665
717
  raise ArgumentError, "expected DuckDB::DataChunk, got #{chunk.class}" unless chunk.is_a?(DuckDB::DataChunk)
666
718
 
@@ -669,6 +721,34 @@ module DuckDB
669
721
  raise_appender_error('failed to append_data_chunk')
670
722
  end
671
723
 
724
+ # call-seq:
725
+ # appender.append_default_to_chunk(chunk, col, row) -> self
726
+ #
727
+ # Appends the DEFAULT value for the column at +col+ and +row+ in +chunk+.
728
+ # If no DEFAULT is defined for the column, NULL is used.
729
+ # Call this before appending the chunk via #append_data_chunk.
730
+ #
731
+ # require 'duckdb'
732
+ # db = DuckDB::Database.open
733
+ # con = db.connect
734
+ # con.query('CREATE TABLE users (name VARCHAR, enabled BOOLEAN DEFAULT TRUE)')
735
+ # appender = con.appender('users')
736
+ # chunk = DuckDB::DataChunk.new([DuckDB::LogicalType::VARCHAR, DuckDB::LogicalType::BOOLEAN])
737
+ # appender.append_default_to_chunk(chunk, 1, 0) # enabled DEFAULT for row 0
738
+ # appender.append_default_to_chunk(chunk, 1, 1) # enabled DEFAULT for row 1
739
+ # chunk.set_value(0, 0, 'Alice')
740
+ # chunk.set_value(0, 1, 'Bob')
741
+ # chunk.size = 2
742
+ # appender.append_data_chunk(chunk)
743
+ # appender.flush
744
+ def append_default_to_chunk(chunk, col, row)
745
+ raise ArgumentError, "expected DuckDB::DataChunk, got #{chunk.class}" unless chunk.is_a?(DuckDB::DataChunk)
746
+
747
+ return self if _append_default_to_chunk(chunk, col, row)
748
+
749
+ raise_appender_error('failed to append_default_to_chunk')
750
+ end
751
+
672
752
  # appends value.
673
753
  #
674
754
  # require 'duckdb'
@@ -722,6 +802,23 @@ module DuckDB
722
802
 
723
803
  private
724
804
 
805
+ def warn_deprecated_3arg # :nodoc:
806
+ warn(
807
+ 'DuckDB::Appender.new(con, schema, table) is deprecated. ' \
808
+ 'Use DuckDB::Appender.new(con, table, schema: schema) instead.',
809
+ category: :deprecated
810
+ )
811
+ end
812
+
813
+ def initialize_with_parsed_table(con, table_name, schema:, catalog:) # :nodoc:
814
+ table_name, schema, catalog = parse_table_name(table_name, schema, catalog)
815
+ if catalog
816
+ _initialize_ext(con, catalog, schema, table_name)
817
+ else
818
+ _initialize(con, schema, table_name)
819
+ end
820
+ end
821
+
725
822
  def raise_appender_error(default_message) # :nodoc:
726
823
  message = error_message
727
824
  raise DuckDB::Error, message || default_message
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuckDB
4
+ # The ArrowArrayStream class represents an exported Arrow C stream of a
5
+ # query result (Arrow C Data Interface). It is created by
6
+ # DuckDB::Result#arrow_c_stream and cannot be instantiated directly.
7
+ #
8
+ # The object satisfies the Ruby Arrow C stream protocol: #arrow_c_stream
9
+ # returns self and #to_i returns the address of the underlying
10
+ # <tt>struct ArrowArrayStream</tt>, so it can be consumed by ruby-polars,
11
+ # red-arrow and other Arrow consumers:
12
+ #
13
+ # result = con.query('SELECT * FROM users')
14
+ #
15
+ # # ruby-polars
16
+ # df = Polars::DataFrame.new(result)
17
+ #
18
+ # # red-arrow
19
+ # reader = Arrow::RecordBatchReader.import(result.arrow_c_stream.to_i)
20
+ #
21
+ # The consumer takes ownership of the stream's contents; a result can be
22
+ # exported only once.
23
+ #
24
+ # [EXPERIMENTAL] This API is built on DuckDB's unstable Arrow C API and
25
+ # may change in any minor release.
26
+ class ArrowArrayStream
27
+ class << self
28
+ def new
29
+ raise DuckDB::Error, 'DuckDB::ArrowArrayStream cannot be instantiated directly. Use DuckDB::Result#arrow_c_stream.'
30
+ end
31
+ end
32
+ end
33
+ end
@@ -8,6 +8,8 @@ module DuckDB
8
8
  # con = db.connect
9
9
  # con.query(sql)
10
10
  class Connection
11
+ include DuckDB::TableNameParser
12
+
11
13
  # executes sql with args.
12
14
  # The first argument sql must be SQL string.
13
15
  # The rest arguments are parameters of SQL string.
@@ -127,11 +129,43 @@ module DuckDB
127
129
  PreparedStatement.prepare(self, str, &)
128
130
  end
129
131
 
130
- # returns Appender object.
131
- # The first argument is table name
132
- def appender(table, &)
133
- appender = create_appender(table)
134
- run_appender_block(appender, &)
132
+ # :call-seq:
133
+ # connection.appender(table, schema: nil, catalog: nil) -> DuckDB::Appender
134
+ # connection.appender(table, schema: nil, catalog: nil) { |appender| ... } -> self
135
+ #
136
+ # Returns a DuckDB::Appender for bulk-inserting rows into +table+.
137
+ # If a block is given, the appender is flushed and closed automatically after the block.
138
+ #
139
+ # +schema:+ and +catalog:+ optionally qualify the table.
140
+ #
141
+ # Raises DuckDB::Error if the table (or schema/catalog) does not exist.
142
+ #
143
+ # Table name parsing (quoting, dot-notation) is handled by DuckDB::Appender.new.
144
+ # See DuckDB::Appender.new for details on quoting and dot-notation.
145
+ #
146
+ # require 'duckdb'
147
+ # db = DuckDB::Database.open
148
+ # con = db.connect
149
+ # con.query('CREATE TABLE users (id INTEGER, name VARCHAR)')
150
+ #
151
+ # # block form (recommended) — flushes and closes automatically
152
+ # con.appender('users') do |a|
153
+ # a.append_row(1, 'Alice')
154
+ # a.append_row(2, 'Bob')
155
+ # end
156
+ #
157
+ # # with schema
158
+ # con.appender('users', schema: 'main') do |a|
159
+ # a.append_row(3, 'Carol')
160
+ # end
161
+ #
162
+ # # manual form
163
+ # appender = con.appender('users')
164
+ # appender.append_row(4, 'Dave')
165
+ # appender.close
166
+ def appender(table, schema: nil, catalog: nil, &)
167
+ table, schema, catalog = parse_connection_appender_table(table, schema, catalog)
168
+ run_appender_block(Appender.new(self, table, schema: schema, catalog: catalog), &)
135
169
  end
136
170
 
137
171
  if Appender.respond_to?(:create_query)
@@ -234,7 +268,7 @@ module DuckDB
234
268
  # allowing DuckDB to dispatch to the correct implementation based on argument types.
235
269
  #
236
270
  # @param scalar_function_set [DuckDB::ScalarFunctionSet] the function set to register
237
- # @return [void]
271
+ # @return [self]
238
272
  # @raise [TypeError] if argument is not a DuckDB::ScalarFunctionSet
239
273
  #
240
274
  # @example Register multiple overloads under one name
@@ -251,6 +285,42 @@ module DuckDB
251
285
  _register_scalar_function_set(scalar_function_set)
252
286
  end
253
287
 
288
+ # Registers an aggregate function set with the connection.
289
+ # An aggregate function set groups multiple overloads of an aggregate function under one name,
290
+ # allowing DuckDB to dispatch to the correct implementation based on argument types.
291
+ #
292
+ # @param aggregate_function_set [DuckDB::AggregateFunctionSet] the function set to register
293
+ # @return [self]
294
+ # @raise [TypeError] if argument is not a DuckDB::AggregateFunctionSet
295
+ #
296
+ # @example Register multiple overloads under one name
297
+ # af_bigint = DuckDB::AggregateFunction.new
298
+ # af_bigint.name = 'my_sum'
299
+ # af_bigint.return_type = DuckDB::LogicalType::BIGINT
300
+ # af_bigint.add_parameter(DuckDB::LogicalType::BIGINT)
301
+ # af_bigint.set_init { 0 }
302
+ # af_bigint.set_update { |state, val| state + val }
303
+ # af_bigint.set_combine { |s1, s2| s1 + s2 }
304
+ #
305
+ # af_double = DuckDB::AggregateFunction.new
306
+ # af_double.name = 'my_sum'
307
+ # af_double.return_type = DuckDB::LogicalType::DOUBLE
308
+ # af_double.add_parameter(DuckDB::LogicalType::DOUBLE)
309
+ # af_double.set_init { 0.0 }
310
+ # af_double.set_update { |state, val| state + val }
311
+ # af_double.set_combine { |s1, s2| s1 + s2 }
312
+ #
313
+ # set = DuckDB::AggregateFunctionSet.new(:my_sum)
314
+ # set.add(af_bigint).add(af_double)
315
+ # con.register_aggregate_function_set(set)
316
+ def register_aggregate_function_set(aggregate_function_set)
317
+ unless aggregate_function_set.is_a?(AggregateFunctionSet)
318
+ raise TypeError, "#{aggregate_function_set.class} is not a DuckDB::AggregateFunctionSet"
319
+ end
320
+
321
+ _register_aggregate_function_set(aggregate_function_set)
322
+ end
323
+
254
324
  # Registers an aggregate function with the connection.
255
325
  #
256
326
  # @param aggregate_function [DuckDB::AggregateFunction] the aggregate function to register
@@ -313,8 +383,62 @@ module DuckDB
313
383
  register_table_function(tf)
314
384
  end
315
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
+
316
426
  private
317
427
 
428
+ # Drives the Arrow stream at +address+ chunk by chunk into +table+,
429
+ # returning the number of rows appended.
430
+ def append_arrow_chunks(table, address)
431
+ converted_schema = _arrow_converted_schema(address)
432
+ rows = 0
433
+ appender(table) do |app|
434
+ while (chunk = _arrow_next_chunk(address, converted_schema))
435
+ rows += chunk.size
436
+ app.append_data_chunk(chunk)
437
+ end
438
+ end
439
+ rows
440
+ end
441
+
318
442
  def run_appender_block(appender, &)
319
443
  return appender unless block_given?
320
444
 
@@ -323,9 +447,15 @@ module DuckDB
323
447
  appender.close
324
448
  end
325
449
 
326
- def create_appender(table)
327
- t1, t2 = table.split('.')
328
- t2 ? Appender.new(self, t1, t2) : Appender.new(self, t2, t1)
450
+ # Silently pre-parses dot-notation so Appender.new receives clean values
451
+ # and does not emit a misleading "DuckDB::Appender.new" warning.
452
+ # con.appender('a.b') has always split on dot — no warning needed.
453
+ # Quoted table names pass through unchanged for Appender.new to handle.
454
+ def parse_connection_appender_table(table, schema, catalog)
455
+ return [table, schema, catalog] if quoted_table_name?(table)
456
+ return [table, schema, catalog] unless table.include?('.')
457
+
458
+ dot_notation_split(table, schema, catalog)
329
459
  end
330
460
 
331
461
  alias execute query
@@ -193,6 +193,23 @@ module DuckDB
193
193
  _bind_uhugeint(index, lower, upper)
194
194
  end
195
195
 
196
+ # binds i-th parameter with a UUID value.
197
+ # The first argument is the index of the parameter (1-based).
198
+ # The second argument must be a String in canonical UUID format
199
+ # (<tt>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</tt>).
200
+ # Raises ArgumentError if the value is not a valid UUID string.
201
+ #
202
+ # require 'duckdb'
203
+ # db = DuckDB::Database.open
204
+ # con = db.connect
205
+ # con.query('CREATE TABLE uuids (id UUID)')
206
+ # stmt = DuckDB::PreparedStatement.new(con, 'INSERT INTO uuids(id) VALUES ($1)')
207
+ # stmt.bind_uuid(1, '550e8400-e29b-41d4-a716-446655440000')
208
+ # stmt.execute
209
+ def bind_uuid(index, value)
210
+ _bind_uuid(index, value)
211
+ end
212
+
196
213
  # binds i-th parameter with SQL prepared statement.
197
214
  # The first argument is index of parameter.
198
215
  # The index of first parameter is 1 not 0.
@@ -250,6 +267,24 @@ module DuckDB
250
267
  _bind_timestamp(index, time.year, time.month, time.day, time.hour, time.min, time.sec, time.usec)
251
268
  end
252
269
 
270
+ # binds i-th parameter of TIMESTAMP WITH TIME ZONE (TIMESTAMPTZ) type with SQL prepared statement.
271
+ # The first argument is index of parameter.
272
+ # The index of first parameter is 1 not 0.
273
+ # The second argument value is to expected time value.
274
+ #
275
+ # require 'duckdb'
276
+ # db = DuckDB::Database.open('duckdb_database')
277
+ # con = db.connect
278
+ # sql ='SELECT name FROM users WHERE created_at = ?'
279
+ # stmt = PreparedStatement.new(con, sql)
280
+ # stmt.bind_timestamp_tz(1, Time.now)
281
+ # # or you can specify timestamp string.
282
+ # # stmt.bind_timestamp_tz(1, '2022-02-23 07:39:45+00')
283
+ def bind_timestamp_tz(index, value)
284
+ time = _parse_time(value).utc
285
+ _bind_timestamp_tz(index, time.year, time.month, time.day, time.hour, time.min, time.sec, time.usec)
286
+ end
287
+
253
288
  # binds i-th parameter with SQL prepared statement.
254
289
  # The first argument is index of parameter.
255
290
  # The index of first parameter is 1 not 0.
data/lib/duckdb/result.rb CHANGED
@@ -81,11 +81,48 @@ module DuckDB
81
81
  # result = con.query('SELECT * FROM enums')
82
82
  # result.enum_dictionary_values(1) # => ['sad', 'ok', 'happy', '𝘾𝝾օɭ 😎']
83
83
  def enum_dictionary_values(col_index)
84
+ column = columns[col_index]
85
+
86
+ raise ArgumentError, "Invalid index: #{col_index}" if column.nil? || col_index.negative?
87
+
88
+ lt = column.logical_type
89
+
90
+ raise DuckDB::Error, "Column[#{col_index}] type is not enum" if lt.type != :enum
91
+
84
92
  values = []
85
- _enum_dictionary_size(col_index).times do |i|
86
- values << _enum_dictionary_value(col_index, i)
93
+ lt.dictionary_size.times do |i|
94
+ values << lt.dictionary_value_at(i)
87
95
  end
88
96
  values
89
97
  end
98
+
99
+ private
100
+
101
+ def _enum_dictionary_size(idx)
102
+ warn(":_enum_dictionary_size is deprecated. use columns[#{idx}].logical_type.dictionary_size instead.")
103
+
104
+ raise ArgumentError, "Invalid index: #{idx}" if idx.negative?
105
+
106
+ columns[idx]&.logical_type&.dictionary_size
107
+ end
108
+
109
+ def _enum_dictionary_value(col_index, idx)
110
+ warn(":_enum_dictionary_value is deprecated.\
111
+ use columns[#{col_index}].logical_type.dictionary_value_at(#{idx}) instead.")
112
+
113
+ raise ArgumentError, "Invalid index: #{col_index}" if col_index.negative?
114
+
115
+ lt = columns[col_index]&.logical_type
116
+
117
+ raise DuckDB::Error, "Column[#{col_index}] type is not enum" if lt&.type != :enum
118
+
119
+ lt.dictionary_value_at(idx)
120
+ end
121
+
122
+ def _column_type(idx)
123
+ warn(":_column_type is deprecated. use columns[#{idx}].send(:_type) instead.")
124
+
125
+ columns[idx].send(:_type)
126
+ end
90
127
  end
91
128
  end
@@ -7,8 +7,7 @@ module DuckDB
7
7
  class ScalarFunction
8
8
  # Create and configure a scalar function in one call
9
9
  #
10
- # @param name [String, Symbol, nil] the function name; use +nil+ when creating overloads
11
- # intended for use in a +DuckDB::ScalarFunctionSet+ (the set provides the name)
10
+ # @param name [String, Symbol] the function name (required)
12
11
  # @param return_type [DuckDB::LogicalType|:logical_type_symbol] the return type
13
12
  # @param parameter_type [DuckDB::LogicalType|:logical_type_symbol, nil] single fixed parameter type
14
13
  # @param parameter_types [Array<DuckDB::LogicalType|:logical_type_symbol>, nil] multiple fixed parameter types
@@ -64,7 +63,7 @@ module DuckDB
64
63
  # null_handling: true
65
64
  # ) { |v| v.nil? ? 0 : v }
66
65
  def self.create( # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/ParameterLists
67
- return_type:, name: nil, parameter_type: nil, parameter_types: nil, varargs_type: nil, null_handling: false, &
66
+ name:, return_type:, parameter_type: nil, parameter_types: nil, varargs_type: nil, null_handling: false, &
68
67
  )
69
68
  raise ArgumentError, 'Block required' unless block_given?
70
69
  raise ArgumentError, 'Cannot specify both parameter_type and parameter_types' if parameter_type && parameter_types
@@ -78,7 +77,7 @@ module DuckDB
78
77
  end
79
78
 
80
79
  sf = new
81
- sf.name = name.to_s if name
80
+ sf.name = name
82
81
  sf.return_type = return_type
83
82
  params.each { |type| sf.add_parameter(type) }
84
83
  sf.varargs_type = varargs_type if varargs_type
@@ -87,6 +86,12 @@ module DuckDB
87
86
  sf
88
87
  end
89
88
 
89
+ def name=(value)
90
+ set_name(value.to_s)
91
+ end
92
+
93
+ private :set_name
94
+
90
95
  include FunctionTypeValidation
91
96
 
92
97
  # Adds a parameter to the scalar function.
@@ -24,7 +24,6 @@ module DuckDB
24
24
  raise TypeError, "#{scalar_function.class} is not a DuckDB::ScalarFunction"
25
25
  end
26
26
 
27
- scalar_function.name = @name
28
27
  _add(scalar_function)
29
28
  end
30
29
  end
@@ -21,11 +21,17 @@ module DuckDB
21
21
  # # id: integer, default=false
22
22
  # # name: varchar, default=true
23
23
  class TableDescription
24
+ include DuckDB::TableNameParser
25
+
24
26
  # Creates a new TableDescription for the given table.
25
27
  #
26
28
  # +con+ must be a DuckDB::Connection. +table+ is the table name (String).
27
29
  # Optionally pass +schema:+ and/or +catalog:+ to qualify the table.
28
30
  #
31
+ # The +table+ argument supports dot-notation and quoting:
32
+ # - <tt>'schema.table'</tt> — interpreted as schema-qualified (deprecated; use +schema:+ instead)
33
+ # - <tt>'"a.b"'</tt> or <tt>"'a.b'"</tt> — treated as a literal table name containing a dot
34
+ #
29
35
  # Raises DuckDB::Error if the connection is invalid, the table name is nil,
30
36
  # or the table (or schema/catalog) does not exist.
31
37
  #
@@ -40,6 +46,7 @@ module DuckDB
40
46
  raise DuckDB::Error, '1st argument must be DuckDB::Connection object.' unless con.is_a?(DuckDB::Connection)
41
47
  raise DuckDB::Error, '2nd argument must be table name.' if table.nil?
42
48
 
49
+ table, schema, catalog = parse_table_name(table, schema, catalog)
43
50
  raise DuckDB::Error, error_message unless _initialize(con, catalog, schema, table)
44
51
  end
45
52
 
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuckDB
4
+ # DuckDB::TableNameParser provides shared table name parsing for classes that
5
+ # accept a table name argument, such as DuckDB::Appender and DuckDB::TableDescription.
6
+ #
7
+ # It handles:
8
+ # - Dot-notation: <tt>'schema.table'</tt> is split into schema and table (deprecated).
9
+ # - Quoting: <tt>'"a.b"'</tt> or <tt>"'a.b'"</tt> strips the quotes and treats the name literally.
10
+ module TableNameParser
11
+ private
12
+
13
+ # Parses +table+, +schema+, and +catalog+, handling quoting and dot-notation.
14
+ # Returns <tt>[table, schema, catalog]</tt>.
15
+ def parse_table_name(table, schema, catalog)
16
+ if quoted_table_name?(table)
17
+ [unquote_table_name(table), schema, catalog]
18
+ elsif table.include?('.')
19
+ warn_dot_notation_deprecated(table)
20
+ dot_notation_split(table, schema, catalog)
21
+ else
22
+ [table, schema, catalog]
23
+ end
24
+ end
25
+
26
+ def quoted_table_name?(name) # :nodoc:
27
+ name.match?(/\A(["']).*\1\z/)
28
+ end
29
+
30
+ def unquote_table_name(name) # :nodoc:
31
+ name[1..-2]
32
+ end
33
+
34
+ # Splits a dot-notation string into [table, schema, catalog].
35
+ # Explicit keyword args take precedence over dot-notation parts.
36
+ def dot_notation_split(table, schema, catalog) # :nodoc:
37
+ parts = table.split('.')
38
+ raise ArgumentError, "Too many dot-separated segments in '#{table}'" if parts.length > 3
39
+
40
+ case parts.length
41
+ when 2 then [parts[1], schema || parts[0], catalog]
42
+ when 3 then [parts[2], schema || parts[1], catalog || parts[0]]
43
+ else raise ArgumentError, "Unexpected segment count in '#{table}'"
44
+ end
45
+ end
46
+
47
+ def warn_dot_notation_deprecated(table) # :nodoc:
48
+ class_name = self.class.name
49
+ warn(
50
+ "Passing dot-notation '#{table}' to #{class_name}.new is deprecated. " \
51
+ "If '#{table}' is a schema-qualified table, use #{class_name}.new(con, table, schema: schema) instead. " \
52
+ "If '#{table}' is a literal table name containing a dot, " \
53
+ "use #{class_name}.new(con, '\"#{table}\"') instead.",
54
+ category: :deprecated
55
+ )
56
+ end
57
+ end
58
+ end
@@ -3,5 +3,5 @@
3
3
  module DuckDB
4
4
  # The version string of ruby-duckdb.
5
5
  # Currently, ruby-duckdb is NOT semantic versioning.
6
- VERSION = '1.5.2.1'
6
+ VERSION = '1.5.4.0'
7
7
  end
data/lib/duckdb.rb CHANGED
@@ -4,10 +4,12 @@ require 'duckdb/duckdb_native'
4
4
  require 'duckdb/library_version'
5
5
  require 'duckdb/version'
6
6
  require 'duckdb/converter'
7
+ require 'duckdb/table_name_parser'
7
8
  require 'duckdb/database'
8
9
  require 'duckdb/connection'
9
10
  require 'duckdb/extracted_statements'
10
11
  require 'duckdb/result'
12
+ require 'duckdb/arrow_array_stream'
11
13
  require 'duckdb/prepared_statement'
12
14
  require 'duckdb/pending_result'
13
15
  require 'duckdb/appender'
@@ -18,6 +20,7 @@ require 'duckdb/function_type_validation'
18
20
  require 'duckdb/scalar_function'
19
21
  require 'duckdb/scalar_function_set'
20
22
  require 'duckdb/aggregate_function'
23
+ require 'duckdb/aggregate_function_set'
21
24
  require 'duckdb/expression'
22
25
  require 'duckdb/client_context'
23
26
  require 'duckdb/scalar_function/bind_info'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duckdb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.2.1
4
+ version: 1.5.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masaki Suketa
@@ -43,8 +43,14 @@ files:
43
43
  - duckdb.gemspec
44
44
  - ext/duckdb/aggregate_function.c
45
45
  - ext/duckdb/aggregate_function.h
46
+ - ext/duckdb/aggregate_function_set.c
47
+ - ext/duckdb/aggregate_function_set.h
46
48
  - ext/duckdb/appender.c
47
49
  - ext/duckdb/appender.h
50
+ - ext/duckdb/arrow_array_stream.c
51
+ - ext/duckdb/arrow_array_stream.h
52
+ - ext/duckdb/arrow_import.c
53
+ - ext/duckdb/arrow_import.h
48
54
  - ext/duckdb/blob.c
49
55
  - ext/duckdb/blob.h
50
56
  - ext/duckdb/client_context.c
@@ -110,7 +116,9 @@ files:
110
116
  - ext/duckdb/vector.h
111
117
  - lib/duckdb.rb
112
118
  - lib/duckdb/aggregate_function.rb
119
+ - lib/duckdb/aggregate_function_set.rb
113
120
  - lib/duckdb/appender.rb
121
+ - lib/duckdb/arrow_array_stream.rb
114
122
  - lib/duckdb/casting.rb
115
123
  - lib/duckdb/client_context.rb
116
124
  - lib/duckdb/column.rb
@@ -140,6 +148,7 @@ files:
140
148
  - lib/duckdb/table_function/bind_info.rb
141
149
  - lib/duckdb/table_function/function_info.rb
142
150
  - lib/duckdb/table_function/init_info.rb
151
+ - lib/duckdb/table_name_parser.rb
143
152
  - lib/duckdb/value.rb
144
153
  - lib/duckdb/vector.rb
145
154
  - lib/duckdb/version.rb
@@ -165,7 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
165
174
  - !ruby/object:Gem::Version
166
175
  version: '0'
167
176
  requirements: []
168
- rubygems_version: 4.0.6
177
+ rubygems_version: 4.0.10
169
178
  specification_version: 4
170
179
  summary: Ruby bindings for the DuckDB database engine.
171
180
  test_files: []