duckdb 1.5.2.0 → 1.5.3.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/duckdb.gemspec +37 -0
  4. data/ext/duckdb/aggregate_function.c +62 -100
  5. data/ext/duckdb/aggregate_function.h +2 -2
  6. data/ext/duckdb/aggregate_function_set.c +86 -0
  7. data/ext/duckdb/aggregate_function_set.h +14 -0
  8. data/ext/duckdb/appender.c +121 -39
  9. data/ext/duckdb/appender.h +1 -1
  10. data/ext/duckdb/client_context.c +5 -5
  11. data/ext/duckdb/client_context.h +2 -2
  12. data/ext/duckdb/column.c +13 -13
  13. data/ext/duckdb/column.h +1 -1
  14. data/ext/duckdb/connection.c +63 -41
  15. data/ext/duckdb/connection.h +2 -2
  16. data/ext/duckdb/converter.h +1 -7
  17. data/ext/duckdb/conveter.c +6 -6
  18. data/ext/duckdb/data_chunk.c +22 -22
  19. data/ext/duckdb/data_chunk.h +2 -2
  20. data/ext/duckdb/database.c +10 -10
  21. data/ext/duckdb/database.h +1 -1
  22. data/ext/duckdb/duckdb.c +18 -17
  23. data/ext/duckdb/expression.c +8 -8
  24. data/ext/duckdb/expression.h +1 -1
  25. data/ext/duckdb/extconf.rb +32 -16
  26. data/ext/duckdb/extracted_statements.c +15 -15
  27. data/ext/duckdb/extracted_statements.h +1 -1
  28. data/ext/duckdb/instance_cache.c +10 -10
  29. data/ext/duckdb/instance_cache.h +1 -1
  30. data/ext/duckdb/logical_type.c +94 -133
  31. data/ext/duckdb/logical_type.h +2 -2
  32. data/ext/duckdb/memory_helper.c +28 -28
  33. data/ext/duckdb/pending_result.c +27 -27
  34. data/ext/duckdb/pending_result.h +2 -2
  35. data/ext/duckdb/prepared_statement.c +120 -103
  36. data/ext/duckdb/prepared_statement.h +2 -2
  37. data/ext/duckdb/result.c +24 -74
  38. data/ext/duckdb/result.h +2 -3
  39. data/ext/duckdb/ruby-duckdb.h +5 -0
  40. data/ext/duckdb/scalar_function.c +3 -3
  41. data/ext/duckdb/table_description.c +1 -1
  42. data/ext/duckdb/table_function.c +3 -3
  43. data/ext/duckdb/table_function_bind_info.c +1 -1
  44. data/ext/duckdb/value.c +62 -50
  45. data/ext/duckdb/value.h +2 -2
  46. data/ext/duckdb/vector.c +20 -20
  47. data/ext/duckdb/vector.h +2 -2
  48. data/lib/duckdb/aggregate_function.rb +208 -3
  49. data/lib/duckdb/aggregate_function_set.rb +29 -0
  50. data/lib/duckdb/appender.rb +148 -0
  51. data/lib/duckdb/connection.rb +86 -25
  52. data/lib/duckdb/converter.rb +5 -0
  53. data/lib/duckdb/logical_type.rb +1 -3
  54. data/lib/duckdb/prepared_statement.rb +19 -1
  55. data/lib/duckdb/result.rb +39 -2
  56. data/lib/duckdb/scalar_function.rb +9 -4
  57. data/lib/duckdb/scalar_function_set.rb +0 -1
  58. data/lib/duckdb/table_description.rb +7 -0
  59. data/lib/duckdb/table_function.rb +0 -1
  60. data/lib/duckdb/table_name_parser.rb +58 -0
  61. data/lib/duckdb/value.rb +19 -0
  62. data/lib/duckdb/version.rb +1 -1
  63. data/lib/duckdb.rb +2 -0
  64. metadata +7 -3
  65. data/lib/duckdb/duckdb_native.so +0 -0
@@ -1,13 +1,159 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DuckDB
4
- # DuckDB::AggregateFunction encapsulates DuckDB's aggregate function.
4
+ # DuckDB::AggregateFunction lets you register a custom aggregate function
5
+ # written in Ruby and call it from SQL.
5
6
  #
6
- # @note DuckDB::AggregateFunction is experimental. Phase 1.0 only supports
7
- # +set_init+ and +set_finalize+; +update+ and +combine+ are internal no-ops.
7
+ # An aggregate function folds many rows into a single value. You define its
8
+ # behaviour with four callbacks:
9
+ #
10
+ # * +set_init+ — called once per group; returns the initial state.
11
+ # * +set_update+ — called once per row; receives the current state and the
12
+ # input value(s), returns the new state.
13
+ # * +set_combine+ — merges two partial states (required for parallel
14
+ # execution); receives source and target states, returns the
15
+ # merged state.
16
+ # * +set_finalize+ — converts the final state into the SQL result value.
17
+ #
18
+ # Only +set_init+ is required. The other three have sensible defaults:
19
+ # * +set_update+ defaults to +{ |state, *| state }+ (ignore inputs)
20
+ # * +set_combine+ defaults to +{ |s1, _s2| s1 }+ (keep source state)
21
+ # * +set_finalize+ defaults to +{ |x| x }+ (return state as-is)
22
+ #
23
+ # @note The default +set_combine+ keeps the source state and discards the
24
+ # target, which is only correct for single-threaded (single-partition)
25
+ # execution. If DuckDB runs the aggregate in parallel it will produce
26
+ # wrong results. Always supply an explicit +set_combine+ when the
27
+ # aggregate must be parallel-safe.
28
+ #
29
+ # == Basic example: custom SUM
30
+ #
31
+ # af = DuckDB::AggregateFunction.new
32
+ # af.name = 'my_sum'
33
+ # af.return_type = DuckDB::LogicalType::BIGINT
34
+ # af.add_parameter(DuckDB::LogicalType::BIGINT)
35
+ #
36
+ # af.set_init { 0 }
37
+ # af.set_update { |state, value| state + value }
38
+ # af.set_combine { |s1, s2| s1 + s2 }
39
+ #
40
+ # con.register_aggregate_function(af)
41
+ # con.query('SELECT my_sum(i) FROM range(100) t(i)').first.first # => 4950
42
+ #
43
+ # == Example: weighted average with Hash state
44
+ #
45
+ # af = DuckDB::AggregateFunction.new
46
+ # af.name = 'weighted_avg'
47
+ # af.return_type = DuckDB::LogicalType::DOUBLE
48
+ # af.add_parameter(DuckDB::LogicalType::DOUBLE) # value
49
+ # af.add_parameter(DuckDB::LogicalType::DOUBLE) # weight
50
+ #
51
+ # af.set_init { { sum: 0.0, weight: 0.0 } }
52
+ # af.set_update { |state, value, weight| { sum: state[:sum] + value * weight, weight: state[:weight] + weight } }
53
+ # af.set_combine { |s1, s2| { sum: s1[:sum] + s2[:sum], weight: s1[:weight] + s2[:weight] } }
54
+ # af.set_finalize { |state| state[:weight].zero? ? nil : state[:sum] / state[:weight] }
55
+ #
56
+ # con.register_aggregate_function(af)
8
57
  class AggregateFunction
9
58
  include FunctionTypeValidation
10
59
 
60
+ def name=(value)
61
+ set_name(value.to_s)
62
+ end
63
+
64
+ private :set_name
65
+
66
+ class << self
67
+ # Creates a new AggregateFunction in a single call.
68
+ #
69
+ # This is a convenience factory that builds and configures an
70
+ # AggregateFunction without requiring you to set each attribute
71
+ # separately.
72
+ #
73
+ # @param name [String, Symbol] the SQL function name
74
+ # @param return_type [DuckDB::LogicalType | Symbol] the SQL return type
75
+ # @param params [Array<DuckDB::LogicalType | Symbol>] input parameter types
76
+ # (empty array for a zero-argument aggregate)
77
+ # @param init [#call] callable that returns the initial per-group state
78
+ # @param update [#call] callable that folds one row into the state;
79
+ # receives +state, *inputs+ and must return the updated state.
80
+ # Default: +->( state, *) { state }+ (ignore inputs)
81
+ # @param combine [#call] callable that merges two partial states;
82
+ # receives +source_state, target_state+ and must return the merged
83
+ # state. Default: +->(state, _other) { state }+ (keep source only —
84
+ # only correct for single-threaded execution)
85
+ # @param finalize [#call] callable that converts the final state into the
86
+ # SQL result value; receives +state+ and must return a value compatible
87
+ # with +return_type+.
88
+ # Default: +->(state) { state }+ (return state as-is)
89
+ # @param null_handling [Boolean] when +true+, enables special NULL
90
+ # handling so that rows with NULL inputs are passed to +update+ as
91
+ # +nil+ instead of being skipped (default: +false+)
92
+ # @return [DuckDB::AggregateFunction] the configured aggregate function,
93
+ # ready to be passed to +Connection#register_aggregate_function+
94
+ # @raise [ArgumentError] if any of +init+, +update+, +combine+, or
95
+ # +finalize+ does not respond to +call+
96
+ #
97
+ # == Example: custom SUM
98
+ #
99
+ # af = DuckDB::AggregateFunction.create(
100
+ # name: 'my_sum',
101
+ # return_type: :bigint,
102
+ # params: [:bigint],
103
+ # init: -> { 0 },
104
+ # update: ->(state, value) { state + value },
105
+ # combine: ->(state, other) { state + other }
106
+ # )
107
+ # con.register_aggregate_function(af)
108
+ # con.query('SELECT my_sum(i) FROM range(100) t(i)').first.first # => 4950
109
+ #
110
+ # == Example: count including NULL values
111
+ #
112
+ # af = DuckDB::AggregateFunction.create(
113
+ # name: 'count_with_nulls',
114
+ # return_type: :bigint,
115
+ # params: [:bigint],
116
+ # init: -> { 0 },
117
+ # update: ->(state, _value) { state + 1 },
118
+ # combine: ->(state, other) { state + other },
119
+ # null_handling: true
120
+ # )
121
+ def create( # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists, Metrics/AbcSize
122
+ name:,
123
+ return_type:,
124
+ params: [], # rubocop:disable Style/KeywordParametersOrder
125
+ init:,
126
+ update: ->(state, *_inputs) { state },
127
+ combine: ->(state, _other_state) { state },
128
+ finalize: ->(state) { state },
129
+ null_handling: false
130
+ )
131
+ callable!(:init, init)
132
+ callable!(:update, update)
133
+ callable!(:combine, combine)
134
+ callable!(:finalize, finalize)
135
+
136
+ af = AggregateFunction.new
137
+ af.name = name
138
+ af.return_type = return_type
139
+ params.each do |param|
140
+ af.add_parameter(param)
141
+ end
142
+ af.set_init { init.call }
143
+ af.set_update { |state, *inputs| update.call(state, *inputs) }
144
+ af.set_combine { |state, other_state| combine.call(state, other_state) }
145
+ af.set_finalize { |state| finalize.call(state) }
146
+ af.set_special_handling if null_handling
147
+ af
148
+ end
149
+
150
+ private
151
+
152
+ def callable!(name, arg)
153
+ raise ArgumentError, "#{name} must respond to `call`" unless arg.respond_to?(:call)
154
+ end
155
+ end
156
+
11
157
  # Sets the return type for the aggregate function.
12
158
  #
13
159
  # @param logical_type [DuckDB::LogicalType | :logical_type_symbol] the return type
@@ -30,6 +176,65 @@ module DuckDB
30
176
  _add_parameter(logical_type)
31
177
  end
32
178
 
179
+ # Sets the block that initialises the per-group state.
180
+ # The block takes no arguments and returns the initial state value.
181
+ # This is the only required callback; defaults for +set_update+,
182
+ # +set_combine+, and +set_finalize+ are injected automatically on the
183
+ # first call if those methods have not been called explicitly.
184
+ #
185
+ # @note The injected default for +set_combine+ is +{ |s1, _s2| s1 }+, which
186
+ # is only correct for single-threaded execution. Always call +set_combine+
187
+ # explicitly when the aggregate must be parallel-safe.
188
+ #
189
+ # @return [DuckDB::AggregateFunction] self
190
+ def set_init(&)
191
+ unless @init_set
192
+ _set_update { |state, *| state } unless @update_set
193
+ _set_combine { |s1, _s2| s1 } unless @combine_set
194
+ _set_finalize { |x| x } unless @finalize_set
195
+ end
196
+ _set_init(&)
197
+ @init_set = true
198
+ end
199
+
200
+ # Sets the block that accumulates one row into the state.
201
+ # The block receives the current state followed by the input column
202
+ # value(s) for that row, and must return the updated state.
203
+ # Default: +{ |state, *| state }+ (ignore inputs, keep state unchanged).
204
+ # May be called after +set_init+ to override the injected default.
205
+ #
206
+ # @return [DuckDB::AggregateFunction] self
207
+ def set_update(&)
208
+ @update_set = true
209
+ _set_update(&)
210
+ end
211
+
212
+ # Sets the block that merges two partial states during parallel execution.
213
+ # The block receives the source and target states and must return the
214
+ # merged state.
215
+ # May be called after +set_init+ to override the injected default.
216
+ #
217
+ # @note The default +{ |s1, _s2| s1 }+ is only correct for single-threaded
218
+ # execution. Supply an explicit combine block for parallel-safe aggregates.
219
+ #
220
+ # @return [DuckDB::AggregateFunction] self
221
+ def set_combine(&)
222
+ @combine_set = true
223
+ _set_combine(&)
224
+ end
225
+
226
+ # Sets the block that converts the final state into the SQL result value.
227
+ # The block receives the accumulated state and must return a value
228
+ # compatible with the declared +return_type+.
229
+ # Default: +{ |x| x }+ (return the state as-is).
230
+ # May be called after +set_init+ to override the injected default.
231
+ #
232
+ # @return [DuckDB::AggregateFunction] self
233
+ def set_finalize(&)
234
+ @finalize_set = true
235
+ _set_finalize(&)
236
+ end
237
+
33
238
  # Sets special NULL handling for the aggregate function.
34
239
  # By default DuckDB skips rows with NULL input values. Calling this
35
240
  # method disables that behaviour so the update callback is invoked even
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuckDB
4
+ # DuckDB::AggregateFunctionSet encapsulates DuckDB's aggregate function set,
5
+ # which allows registering multiple overloads of an aggregate function under one name.
6
+ #
7
+ # @note DuckDB::AggregateFunctionSet is experimental.
8
+ class AggregateFunctionSet
9
+ # @param name [String, Symbol] the function set name shared by all overloads
10
+ # @raise [TypeError] if name is not a String or Symbol
11
+ def initialize(name)
12
+ raise TypeError, "#{name.class} is not a String or Symbol" unless name.is_a?(String) || name.is_a?(Symbol)
13
+
14
+ _initialize(name.to_s)
15
+ end
16
+
17
+ # @param aggregate_function [DuckDB::AggregateFunction] the overload to add
18
+ # @return [self]
19
+ # @raise [TypeError] if aggregate_function is not a DuckDB::AggregateFunction
20
+ # @raise [DuckDB::Error] if the overload already exists in the set
21
+ def add(aggregate_function)
22
+ unless aggregate_function.is_a?(DuckDB::AggregateFunction)
23
+ raise TypeError, "#{aggregate_function.class} is not a DuckDB::AggregateFunction"
24
+ end
25
+
26
+ _add(aggregate_function)
27
+ end
28
+ end
29
+ end
@@ -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.
@@ -93,6 +108,80 @@ module DuckDB
93
108
  raise_appender_error('failed to close')
94
109
  end
95
110
 
111
+ # :call-seq:
112
+ # appender.add_column(column_name) -> self
113
+ #
114
+ # Specifies a column to append to, allowing selective column insertion.
115
+ # Columns not added will use their default values or be computed from
116
+ # generated column expressions.
117
+ # Raises DuckDB::Error if the column does not exist in the table.
118
+ #
119
+ # require 'duckdb'
120
+ # db = DuckDB::Database.open
121
+ # con = db.connect
122
+ # con.query('CREATE TABLE t (id UUID PRIMARY KEY DEFAULT uuidv4(), name VARCHAR)')
123
+ # appender = con.appender('t')
124
+ # appender.add_column('name')
125
+ # appender
126
+ # .append_varchar('Alice')
127
+ # .end_row
128
+ # .flush
129
+ def add_column(column)
130
+ return self if _add_column(column)
131
+
132
+ raise_appender_error('failed to add_column')
133
+ end
134
+
135
+ # :call-seq:
136
+ # appender.clear_columns -> self
137
+ #
138
+ # Clears the list of columns previously set by #add_column, so that all
139
+ # columns of the table become active again. Any previously appended rows
140
+ # are flushed before the column list is reset; if the flush fails (e.g.
141
+ # a constraint violation), this method raises DuckDB::Error.
142
+ #
143
+ # require 'duckdb'
144
+ # db = DuckDB::Database.open
145
+ # con = db.connect
146
+ # con.query('CREATE TABLE t (id UUID PRIMARY KEY DEFAULT uuidv4(), name VARCHAR)')
147
+ # appender = con.appender('t')
148
+ # appender.add_column('name')
149
+ # appender
150
+ # .append_varchar('Alice')
151
+ # .end_row
152
+ # .flush
153
+ # appender.clear_columns
154
+ # # all table columns are active again
155
+ def clear_columns
156
+ return self if _clear_columns
157
+
158
+ raise_appender_error('failed to clear_columns')
159
+ end
160
+
161
+ if DuckDB::Appender.private_method_defined?(:_clear)
162
+ # :call-seq:
163
+ # appender.clear -> self
164
+ #
165
+ # Clears all unflushed data from the appender, discarding any appended rows
166
+ # that have not yet been flushed to the table.
167
+ #
168
+ # require 'duckdb'
169
+ # db = DuckDB::Database.open
170
+ # con = db.connect
171
+ # con.query('CREATE TABLE users (id INTEGER, name VARCHAR)')
172
+ # appender = con.appender('users')
173
+ # appender
174
+ # .append_int32(1)
175
+ # .append_varchar('Alice')
176
+ # .end_row
177
+ # .clear # discards the row above without flushing to the table
178
+ def clear
179
+ return self if _clear
180
+
181
+ raise_appender_error('failed to clear')
182
+ end
183
+ end
184
+
96
185
  # call-seq:
97
186
  # appender.append_bool(val) -> self
98
187
  #
@@ -587,6 +676,20 @@ module DuckDB
587
676
 
588
677
  # call-seq:
589
678
  # appender.append_data_chunk(chunk) -> self
679
+ #
680
+ # Appends a pre-filled DuckDB::DataChunk to the appender.
681
+ #
682
+ # require 'duckdb'
683
+ # db = DuckDB::Database.open
684
+ # con = db.connect
685
+ # con.query('CREATE TABLE users (id INTEGER, name VARCHAR)')
686
+ # appender = con.appender('users')
687
+ # chunk = DuckDB::DataChunk.new([DuckDB::LogicalType::INTEGER, DuckDB::LogicalType::VARCHAR])
688
+ # chunk.set_value(0, 0, 1)
689
+ # chunk.set_value(1, 0, 'Alice')
690
+ # chunk.size = 1
691
+ # appender.append_data_chunk(chunk)
692
+ # appender.flush
590
693
  def append_data_chunk(chunk)
591
694
  raise ArgumentError, "expected DuckDB::DataChunk, got #{chunk.class}" unless chunk.is_a?(DuckDB::DataChunk)
592
695
 
@@ -595,6 +698,34 @@ module DuckDB
595
698
  raise_appender_error('failed to append_data_chunk')
596
699
  end
597
700
 
701
+ # call-seq:
702
+ # appender.append_default_to_chunk(chunk, col, row) -> self
703
+ #
704
+ # Appends the DEFAULT value for the column at +col+ and +row+ in +chunk+.
705
+ # If no DEFAULT is defined for the column, NULL is used.
706
+ # Call this before appending the chunk via #append_data_chunk.
707
+ #
708
+ # require 'duckdb'
709
+ # db = DuckDB::Database.open
710
+ # con = db.connect
711
+ # con.query('CREATE TABLE users (name VARCHAR, enabled BOOLEAN DEFAULT TRUE)')
712
+ # appender = con.appender('users')
713
+ # chunk = DuckDB::DataChunk.new([DuckDB::LogicalType::VARCHAR, DuckDB::LogicalType::BOOLEAN])
714
+ # appender.append_default_to_chunk(chunk, 1, 0) # enabled DEFAULT for row 0
715
+ # appender.append_default_to_chunk(chunk, 1, 1) # enabled DEFAULT for row 1
716
+ # chunk.set_value(0, 0, 'Alice')
717
+ # chunk.set_value(0, 1, 'Bob')
718
+ # chunk.size = 2
719
+ # appender.append_data_chunk(chunk)
720
+ # appender.flush
721
+ def append_default_to_chunk(chunk, col, row)
722
+ raise ArgumentError, "expected DuckDB::DataChunk, got #{chunk.class}" unless chunk.is_a?(DuckDB::DataChunk)
723
+
724
+ return self if _append_default_to_chunk(chunk, col, row)
725
+
726
+ raise_appender_error('failed to append_default_to_chunk')
727
+ end
728
+
598
729
  # appends value.
599
730
  #
600
731
  # require 'duckdb'
@@ -648,6 +779,23 @@ module DuckDB
648
779
 
649
780
  private
650
781
 
782
+ def warn_deprecated_3arg # :nodoc:
783
+ warn(
784
+ 'DuckDB::Appender.new(con, schema, table) is deprecated. ' \
785
+ 'Use DuckDB::Appender.new(con, table, schema: schema) instead.',
786
+ category: :deprecated
787
+ )
788
+ end
789
+
790
+ def initialize_with_parsed_table(con, table_name, schema:, catalog:) # :nodoc:
791
+ table_name, schema, catalog = parse_table_name(table_name, schema, catalog)
792
+ if catalog
793
+ _initialize_ext(con, catalog, schema, table_name)
794
+ else
795
+ _initialize(con, schema, table_name)
796
+ end
797
+ end
798
+
651
799
  def raise_appender_error(default_message) # :nodoc:
652
800
  message = error_message
653
801
  raise DuckDB::Error, message || default_message
@@ -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.
@@ -34,7 +36,7 @@ module DuckDB
34
36
 
35
37
  def query_multi_sql(sql)
36
38
  stmts = ExtractedStatements.new(self, sql)
37
- return query_sql(sql) if stmts.size == 1
39
+ return _query_sql(sql) if stmts.size == 1
38
40
 
39
41
  result = nil
40
42
  stmts.each do |stmt|
@@ -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
@@ -276,7 +346,6 @@ module DuckDB
276
346
  def register_table_function(table_function)
277
347
  raise ArgumentError, 'table_function must be a TableFunction' unless table_function.is_a?(TableFunction)
278
348
 
279
- check_threads
280
349
  _register_table_function(table_function)
281
350
  end
282
351
 
@@ -291,12 +360,10 @@ module DuckDB
291
360
  # @param columns [Hash{String => DuckDB::LogicalType}, nil] optional column schema override;
292
361
  # if omitted, the adapter determines the columns (e.g. from headers or inference)
293
362
  # @raise [ArgumentError] if no adapter is registered for the object's class
294
- # @raise [DuckDB::Error] if threads setting is not 1
295
363
  # @return [void]
296
364
  #
297
365
  # @example Expose a CSV as a table
298
366
  # require 'csv'
299
- # con.execute('SET threads=1')
300
367
  # DuckDB::TableFunction.add_table_adapter(CSV, CSVTableAdapter.new)
301
368
  # csv = CSV.new(File.read('data.csv'), headers: true)
302
369
  # con.expose_as_table(csv, 'csv_table')
@@ -318,18 +385,6 @@ module DuckDB
318
385
 
319
386
  private
320
387
 
321
- def check_threads
322
- result = execute("SELECT current_setting('threads')")
323
- thread_count = result.first.first.to_i
324
-
325
- return unless thread_count > 1
326
-
327
- raise DuckDB::Error,
328
- 'Functions with Ruby callbacks require single-threaded execution. ' \
329
- "Current threads setting: #{thread_count}. " \
330
- "Execute 'SET threads=1' before registering functions."
331
- end
332
-
333
388
  def run_appender_block(appender, &)
334
389
  return appender unless block_given?
335
390
 
@@ -338,9 +393,15 @@ module DuckDB
338
393
  appender.close
339
394
  end
340
395
 
341
- def create_appender(table)
342
- t1, t2 = table.split('.')
343
- t2 ? Appender.new(self, t1, t2) : Appender.new(self, t2, t1)
396
+ # Silently pre-parses dot-notation so Appender.new receives clean values
397
+ # and does not emit a misleading "DuckDB::Appender.new" warning.
398
+ # con.appender('a.b') has always split on dot — no warning needed.
399
+ # Quoted table names pass through unchanged for Appender.new to handle.
400
+ def parse_connection_appender_table(table, schema, catalog)
401
+ return [table, schema, catalog] if quoted_table_name?(table)
402
+ return [table, schema, catalog] unless table.include?('.')
403
+
404
+ dot_notation_split(table, schema, catalog)
344
405
  end
345
406
 
346
407
  alias execute query
@@ -18,6 +18,7 @@ module DuckDB
18
18
  RANGE_UINT64 = 0..18_446_744_073_709_551_615
19
19
  RANGE_HUGEINT = (-(1 << 127)..((1 << 127) - 1))
20
20
  RANGE_UHUGEINT = (0..((1 << 128) - 1))
21
+ RANGE_DECIMAL_WIDTH = 1..38
21
22
 
22
23
  HALF_HUGEINT_BIT = 64
23
24
  HALF_HUGEINT = 1 << HALF_HUGEINT_BIT
@@ -122,6 +123,10 @@ module DuckDB
122
123
  value >> HALF_HUGEINT_BIT
123
124
  end
124
125
 
126
+ def _decimal_width(value)
127
+ value.to_s('F').gsub(/[^0-9]/, '').length
128
+ end
129
+
125
130
  def _to_decimal_from_hugeint(width, scale, upper, lower = nil)
126
131
  v = lower.nil? ? upper : _to_hugeint_from_vector(lower, upper)
127
132
  _to_decimal_from_value(width, scale, v)
@@ -2,8 +2,6 @@
2
2
 
3
3
  module DuckDB
4
4
  class LogicalType # rubocop:disable Metrics/ClassLength
5
- RANGE_DECIMAL_WIDTH = 1..38
6
-
7
5
  alias :alias get_alias
8
6
  alias :alias= set_alias
9
7
 
@@ -171,7 +169,7 @@ module DuckDB
171
169
  # decimal_type.width #=> 18
172
170
  # decimal_type.scale #=> 3
173
171
  def create_decimal(width, scale)
174
- raise DuckDB::Error, 'width must be between 1 and 38' unless RANGE_DECIMAL_WIDTH.cover?(width)
172
+ raise DuckDB::Error, 'width must be between 1 and 38' unless Converter::RANGE_DECIMAL_WIDTH.cover?(width)
175
173
  raise DuckDB::Error, "scale must be between 0 and width(#{width})" unless (0..width).cover?(scale)
176
174
 
177
175
  _create_decimal_type(width, scale)