duckdb 1.4.2.0 → 1.4.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +169 -0
  3. data/.github/workflows/linter.yml +30 -0
  4. data/.github/workflows/test_on_macos.yml +7 -2
  5. data/.github/workflows/test_on_ubuntu.yml +7 -2
  6. data/.github/workflows/test_on_windows.yml +8 -3
  7. data/.rubocop.yml +34 -0
  8. data/CHANGELOG.md +74 -0
  9. data/Dockerfile +2 -2
  10. data/Gemfile +12 -0
  11. data/Gemfile.lock +70 -7
  12. data/Rakefile +8 -6
  13. data/bin/console +4 -3
  14. data/duckdb.gemspec +2 -6
  15. data/ext/duckdb/bind_info.c +243 -0
  16. data/ext/duckdb/bind_info.h +13 -0
  17. data/ext/duckdb/connection.c +60 -2
  18. data/ext/duckdb/connection.h +1 -0
  19. data/ext/duckdb/data_chunk.c +137 -0
  20. data/ext/duckdb/data_chunk.h +13 -0
  21. data/ext/duckdb/duckdb.c +7 -0
  22. data/ext/duckdb/function_info.c +65 -0
  23. data/ext/duckdb/function_info.h +13 -0
  24. data/ext/duckdb/init_info.c +65 -0
  25. data/ext/duckdb/init_info.h +13 -0
  26. data/ext/duckdb/logical_type.c +42 -0
  27. data/ext/duckdb/memory_helper.c +339 -0
  28. data/ext/duckdb/memory_helper.h +8 -0
  29. data/ext/duckdb/result.c +8 -9
  30. data/ext/duckdb/result.h +1 -0
  31. data/ext/duckdb/ruby-duckdb.h +7 -0
  32. data/ext/duckdb/scalar_function.c +336 -1
  33. data/ext/duckdb/scalar_function.h +2 -0
  34. data/ext/duckdb/table_function.c +403 -0
  35. data/ext/duckdb/table_function.h +16 -0
  36. data/ext/duckdb/vector.c +201 -0
  37. data/ext/duckdb/vector.h +13 -0
  38. data/lib/duckdb/appender.rb +21 -11
  39. data/lib/duckdb/bind_info.rb +32 -0
  40. data/lib/duckdb/casting.rb +35 -0
  41. data/lib/duckdb/connection.rb +123 -2
  42. data/lib/duckdb/converter.rb +32 -47
  43. data/lib/duckdb/data_chunk.rb +114 -0
  44. data/lib/duckdb/extracted_statements.rb +1 -1
  45. data/lib/duckdb/function_info.rb +21 -0
  46. data/lib/duckdb/init_info.rb +22 -0
  47. data/lib/duckdb/instance_cache.rb +0 -4
  48. data/lib/duckdb/interval.rb +1 -1
  49. data/lib/duckdb/logical_type.rb +46 -5
  50. data/lib/duckdb/prepared_statement.rb +22 -8
  51. data/lib/duckdb/result.rb +1 -0
  52. data/lib/duckdb/scalar_function.rb +119 -0
  53. data/lib/duckdb/table_function.rb +191 -0
  54. data/lib/duckdb/vector.rb +20 -0
  55. data/lib/duckdb/version.rb +1 -1
  56. data/lib/duckdb.rb +8 -0
  57. data/sample/async_query.rb +2 -0
  58. data/sample/async_query_stream.rb +2 -0
  59. data/sample/issue922.rb +54 -0
  60. data/sample/issue922_benchmark.rb +169 -0
  61. data/sample/issue930.rb +49 -0
  62. data/sample/issue930_benchmark.rb +70 -0
  63. metadata +30 -57
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuckDB
4
+ #
5
+ # The DuckDB::BindInfo encapsulates information for the bind phase of table functions.
6
+ #
7
+ # During the bind phase, you can:
8
+ # - Access parameters passed to the function
9
+ # - Define the output schema (columns)
10
+ # - Set performance hints (cardinality)
11
+ # - Report errors
12
+ #
13
+ # Example:
14
+ #
15
+ # table_function.bind do |bind_info|
16
+ # # Get parameters
17
+ # limit = bind_info.get_parameter(0).to_i
18
+ #
19
+ # # Define output schema
20
+ # bind_info.add_result_column('id', DuckDB::LogicalType::BIGINT)
21
+ # bind_info.add_result_column('name', DuckDB::LogicalType::VARCHAR)
22
+ #
23
+ # # Set cardinality hint
24
+ # bind_info.set_cardinality(limit, true)
25
+ # end
26
+ #
27
+ class BindInfo
28
+ def add_result_column(name, type)
29
+ _add_result_column(name.to_s, DuckDB::LogicalType.resolve(type))
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuckDB
4
+ module Casting
5
+ def cast(value, type) # rubocop:disable Metrics/MethodLength
6
+ type = type.type if type.is_a?(DuckDB::LogicalType)
7
+ case type
8
+ when :integer, :bigint, :hugeint
9
+ Integer(value)
10
+ when :float, :double
11
+ Float(value)
12
+ when :varchar
13
+ value.to_s
14
+ when :timestamp
15
+ cast_as_timestamp(value)
16
+ when :date
17
+ cast_as_date(value)
18
+ else
19
+ raise ArgumentError, "Unsupported type: #{type} for value: #{value}"
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def cast_as_timestamp(value)
26
+ DuckDB::Converter._parse_time(value)
27
+ end
28
+
29
+ def cast_as_date(value)
30
+ DuckDB::Converter._parse_date(value)
31
+ end
32
+ end
33
+
34
+ extend Casting
35
+ end
@@ -80,10 +80,10 @@ module DuckDB
80
80
  # pending_result.execute_task while pending_result.state == :not_ready
81
81
  # result = pending_result.execute_pending
82
82
  # result.each.first
83
- def async_query_stream(sql, *args, **kwargs)
83
+ def async_query_stream(sql, *, **)
84
84
  warn("`#{self.class}#{__method__}` will be deprecated. Use `#{self.class}#async_query` instead.")
85
85
 
86
- async_query(sql, *args, **kwargs)
86
+ async_query(sql, *, **)
87
87
  end
88
88
 
89
89
  # connects DuckDB database
@@ -156,8 +156,129 @@ module DuckDB
156
156
  end
157
157
  end
158
158
 
159
+ # Registers a scalar function with the connection.
160
+ #
161
+ # Scalar functions with Ruby callbacks require single-threaded execution.
162
+ #
163
+ # @overload register_scalar_function(scalar_function)
164
+ # Register a pre-created ScalarFunction object.
165
+ # @param scalar_function [DuckDB::ScalarFunction] the scalar function to register
166
+ #
167
+ # @overload register_scalar_function(name:, return_type:, **kwargs, &block)
168
+ # Create and register a scalar function inline.
169
+ # @param name [String, Symbol] the function name
170
+ # @param return_type [DuckDB::LogicalType] the return type
171
+ # @param parameter_type [DuckDB::LogicalType, nil] single parameter type
172
+ # @param parameter_types [Array<DuckDB::LogicalType>, nil] multiple parameter types
173
+ # @yield [*args] the function implementation
174
+ #
175
+ # @raise [DuckDB::Error] if threads setting is not 1
176
+ # @raise [ArgumentError] if both object and keywords/block are provided
177
+ # @return [void]
178
+ #
179
+ # @example Register pre-created function
180
+ # con.execute('SET threads=1')
181
+ # sf = DuckDB::ScalarFunction.create(
182
+ # name: :triple,
183
+ # return_type: DuckDB::LogicalType::INTEGER,
184
+ # parameter_type: DuckDB::LogicalType::INTEGER
185
+ # ) { |v| v * 3 }
186
+ # con.register_scalar_function(sf)
187
+ #
188
+ # @example Register inline (single parameter)
189
+ # con.execute('SET threads=1')
190
+ # con.register_scalar_function(
191
+ # name: :triple,
192
+ # return_type: DuckDB::LogicalType::INTEGER,
193
+ # parameter_type: DuckDB::LogicalType::INTEGER
194
+ # ) { |v| v * 3 }
195
+ #
196
+ # @example Register inline (multiple parameters)
197
+ # con.execute('SET threads=1')
198
+ # con.register_scalar_function(
199
+ # name: :add,
200
+ # return_type: DuckDB::LogicalType::INTEGER,
201
+ # parameter_types: [DuckDB::LogicalType::INTEGER, DuckDB::LogicalType::INTEGER]
202
+ # ) { |a, b| a + b }
203
+ def register_scalar_function(scalar_function = nil, **kwargs, &)
204
+ # Validate: can't pass both object and inline arguments
205
+ if scalar_function.is_a?(ScalarFunction)
206
+ raise ArgumentError, 'Cannot pass both ScalarFunction object and keyword arguments' if kwargs.any?
207
+
208
+ raise ArgumentError, 'Cannot pass both ScalarFunction object and block' if block_given?
209
+ end
210
+
211
+ check_threads
212
+ sf = scalar_function || ScalarFunction.create(**kwargs, &)
213
+ _register_scalar_function(sf)
214
+ end
215
+
216
+ #
217
+ # Registers a table function with the database connection.
218
+ #
219
+ # table_function = DuckDB::TableFunction.new
220
+ # table_function.name = 'my_function'
221
+ # table_function.bind { |bind_info| ... }
222
+ # table_function.execute { |func_info, output| ... }
223
+ # connection.register_table_function(table_function)
224
+ #
225
+ def register_table_function(table_function)
226
+ raise ArgumentError, 'table_function must be a TableFunction' unless table_function.is_a?(TableFunction)
227
+
228
+ check_threads
229
+ _register_table_function(table_function)
230
+ end
231
+
232
+ # Exposes a Ruby object as a queryable DuckDB table function via a registered adapter.
233
+ #
234
+ # Looks up a table adapter registered for the object's class via
235
+ # +DuckDB::TableFunction.add_table_adapter+, then uses it to create and register
236
+ # a table function under the given name.
237
+ #
238
+ # @param object [Object] the Ruby object to expose as a table (e.g. a CSV instance)
239
+ # @param name [String] the SQL name of the table function
240
+ # @param columns [Hash{String => DuckDB::LogicalType}, nil] optional column schema override;
241
+ # if omitted, the adapter determines the columns (e.g. from headers or inference)
242
+ # @raise [ArgumentError] if no adapter is registered for the object's class
243
+ # @raise [DuckDB::Error] if threads setting is not 1
244
+ # @return [void]
245
+ #
246
+ # @example Expose a CSV as a table
247
+ # require 'csv'
248
+ # con.execute('SET threads=1')
249
+ # DuckDB::TableFunction.add_table_adapter(CSV, CSVTableAdapter.new)
250
+ # csv = CSV.new(File.read('data.csv'), headers: true)
251
+ # con.expose_as_table(csv, 'csv_table')
252
+ # con.query('SELECT * FROM csv_table()').to_a
253
+ #
254
+ # @example With explicit column types
255
+ # con.expose_as_table(csv, 'csv_table', columns: {
256
+ # 'id' => DuckDB::LogicalType::BIGINT,
257
+ # 'name' => DuckDB::LogicalType::VARCHAR
258
+ # })
259
+ #
260
+ def expose_as_table(object, name, columns: nil)
261
+ adapter = TableFunction.table_adapter_for(object.class)
262
+ raise ArgumentError, "No table adapter registered for #{object.class}" if adapter.nil?
263
+
264
+ tf = adapter.call(object, name, columns:)
265
+ register_table_function(tf)
266
+ end
267
+
159
268
  private
160
269
 
270
+ def check_threads
271
+ result = execute("SELECT current_setting('threads')")
272
+ thread_count = result.first.first.to_i
273
+
274
+ return unless thread_count > 1
275
+
276
+ raise DuckDB::Error,
277
+ 'Functions with Ruby callbacks require single-threaded execution. ' \
278
+ "Current threads setting: #{thread_count}. " \
279
+ "Execute 'SET threads=1' before registering functions."
280
+ end
281
+
161
282
  def run_appender_block(appender, &)
162
283
  return appender unless block_given?
163
284
 
@@ -28,9 +28,11 @@ module DuckDB
28
28
  Date.new(year, month, day)
29
29
  end
30
30
 
31
+ # rubocop:disable Metrics/ParameterLists
31
32
  def _to_time(year, month, day, hour, minute, second, microsecond)
32
33
  Time.local(year, month, day, hour, minute, second, microsecond)
33
34
  end
35
+ # rubocop:enable Metrics/ParameterLists
34
36
 
35
37
  def _to_time_from_duckdb_time(hour, minute, second, microsecond)
36
38
  Time.parse(
@@ -59,46 +61,17 @@ module DuckDB
59
61
  end
60
62
 
61
63
  def _to_time_from_duckdb_time_tz(hour, min, sec, micro, timezone)
62
- sign = '+'
63
- if timezone.negative?
64
- timezone = -timezone
65
- sign = '-'
66
- end
67
-
68
- tzhour = timezone / 3600
69
- tzmin = (timezone % 3600) / 60
70
-
71
- Time.parse(
72
- format(
73
- '%<hour>02d:%<min>02d:%<sec>02d.%<micro>06d%<sign>s%<tzhour>02d:%<tzmin>02d',
74
- hour: hour,
75
- min: min,
76
- sec: sec,
77
- micro: micro,
78
- sign: sign,
79
- tzhour: tzhour,
80
- tzmin: tzmin
81
- )
82
- )
64
+ tz_offset = format_timezone_offset(timezone)
65
+ time_str = format('%<hour>02d:%<min>02d:%<sec>02d.%<micro>06d%<tz>s',
66
+ hour: hour, min: min, sec: sec, micro: micro, tz: tz_offset)
67
+ Time.parse(time_str)
83
68
  end
84
69
 
85
70
  def _to_time_from_duckdb_timestamp_tz(bits)
86
71
  micro = bits % 1_000_000
87
- sec = (bits / 1_000_000)
88
- time = EPOCH_UTC + sec
89
-
90
- Time.parse(
91
- format(
92
- '%<year>04d-%<mon>02d-%<day>02d %<hour>02d:%<min>02d:%<sec>02d.%<micro>06d +0000',
93
- year: time.year,
94
- mon: time.month,
95
- day: time.day,
96
- hour: time.hour,
97
- min: time.min,
98
- sec: time.sec,
99
- micro: micro
100
- )
101
- )
72
+ time = EPOCH_UTC + (bits / 1_000_000)
73
+ timestamp_str = format_timestamp_with_micro(time, micro)
74
+ Time.parse(timestamp_str)
102
75
  end
103
76
 
104
77
  def _to_hugeint_from_vector(lower, upper)
@@ -134,25 +107,23 @@ module DuckDB
134
107
  when Date, Time
135
108
  value
136
109
  else
137
- begin
138
- Date.parse(value)
139
- rescue StandardError => e
140
- raise(ArgumentError, "Cannot parse `#{value.inspect}` to Date object. #{e.message}")
141
- end
110
+ Date.parse(value)
142
111
  end
112
+ rescue StandardError => e
113
+ raise(ArgumentError, "Cannot parse `#{value.inspect}` to Date object. #{e.message}")
143
114
  end
144
115
 
145
116
  def _parse_time(value)
146
117
  case value
147
118
  when Time
148
119
  value
120
+ when DateTime
121
+ value.to_time
149
122
  else
150
- begin
151
- Time.parse(value)
152
- rescue StandardError => e
153
- raise(ArgumentError, "Cannot parse `#{value.inspect}` to Time object. #{e.message}")
154
- end
123
+ Time.parse(value)
155
124
  end
125
+ rescue StandardError => e
126
+ raise(ArgumentError, "Cannot parse `#{value.inspect}` to Time object. #{e.message}")
156
127
  end
157
128
 
158
129
  def _parse_deciaml(value)
@@ -172,6 +143,20 @@ module DuckDB
172
143
  DuckDB::QueryProgress.new(percentage, rows_processed, total_rows_to_process).freeze
173
144
  end
174
145
 
146
+ def format_timezone_offset(timezone)
147
+ sign = timezone.negative? ? '-' : '+'
148
+ offset = timezone.abs
149
+ tzhour = offset / 3600
150
+ tzmin = (offset % 3600) / 60
151
+ format('%<sign>s%<hour>02d:%<min>02d', sign: sign, hour: tzhour, min: tzmin)
152
+ end
153
+
154
+ def format_timestamp_with_micro(time, micro)
155
+ format('%<year>04d-%<mon>02d-%<day>02d %<hour>02d:%<min>02d:%<sec>02d.%<micro>06d +0000',
156
+ year: time.year, mon: time.month, day: time.day,
157
+ hour: time.hour, min: time.min, sec: time.sec, micro: micro)
158
+ end
159
+
175
160
  private
176
161
 
177
162
  def integer_to_hugeint(value)
@@ -186,7 +171,7 @@ module DuckDB
186
171
  end
187
172
 
188
173
  def decimal_to_hugeint(value)
189
- integer_value = (value * (10 ** value.scale)).to_i
174
+ integer_value = (value * (10**value.scale)).to_i
190
175
  integer_to_hugeint(integer_value)
191
176
  rescue FloatDomainError => e
192
177
  raise(ArgumentError, "The argument `#{value.inspect}` must be converted to Integer. #{e.message}")
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuckDB
4
+ #
5
+ # The DuckDB::DataChunk represents a chunk of data for table function output.
6
+ #
7
+ # During table function execution, data chunks are used to return rows.
8
+ #
9
+ # Example:
10
+ #
11
+ # done = false
12
+ # table_function.init { |_init_info| done = false }
13
+ #
14
+ # table_function.execute do |func_info, output|
15
+ # if done
16
+ # output.size = 0 # Signal completion
17
+ # else
18
+ # # High-level API
19
+ # output.set_value(0, 0, 42) # column 0, row 0, value 42
20
+ # output.set_value(1, 0, 'Alice') # column 1, row 0, value 'Alice'
21
+ # output.size = 1
22
+ # done = true
23
+ # end
24
+ # end
25
+ #
26
+ class DataChunk
27
+ # Most methods are defined in C extension (ext/duckdb/data_chunk.c)
28
+
29
+ #
30
+ # Sets a value at the specified column and row index.
31
+ # Type conversion is automatic based on the column's logical type.
32
+ #
33
+ # @param col_idx [Integer] Column index (0-based)
34
+ # @param row_idx [Integer] Row index (0-based)
35
+ # @param value [Object] Value to set (Integer, Float, String, Time, Date, nil)
36
+ # @return [Object] The value that was set
37
+ #
38
+ # @example Set integer value
39
+ # output.set_value(0, 0, 42)
40
+ #
41
+ # @example Set string value
42
+ # output.set_value(1, 0, 'hello')
43
+ #
44
+ # @example Set NULL value
45
+ # output.set_value(0, 1, nil)
46
+ #
47
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
48
+ def set_value(col_idx, row_idx, value)
49
+ vector = get_vector(col_idx)
50
+ logical_type = vector.logical_type
51
+ type_id = logical_type.type
52
+
53
+ # Handle NULL
54
+ if value.nil?
55
+ vector.set_validity(row_idx, false)
56
+ return value
57
+ end
58
+
59
+ case type_id
60
+ when :boolean
61
+ data = vector.get_data
62
+ MemoryHelper.write_boolean(data, row_idx, value)
63
+ when :tinyint
64
+ data = vector.get_data
65
+ MemoryHelper.write_tinyint(data, row_idx, value)
66
+ when :smallint
67
+ data = vector.get_data
68
+ MemoryHelper.write_smallint(data, row_idx, value)
69
+ when :integer
70
+ data = vector.get_data
71
+ MemoryHelper.write_integer(data, row_idx, value)
72
+ when :bigint
73
+ data = vector.get_data
74
+ MemoryHelper.write_bigint(data, row_idx, value)
75
+ when :utinyint
76
+ data = vector.get_data
77
+ MemoryHelper.write_utinyint(data, row_idx, value)
78
+ when :usmallint
79
+ data = vector.get_data
80
+ MemoryHelper.write_usmallint(data, row_idx, value)
81
+ when :uinteger
82
+ data = vector.get_data
83
+ MemoryHelper.write_uinteger(data, row_idx, value)
84
+ when :ubigint
85
+ data = vector.get_data
86
+ MemoryHelper.write_ubigint(data, row_idx, value)
87
+ when :float
88
+ data = vector.get_data
89
+ MemoryHelper.write_float(data, row_idx, value)
90
+ when :double
91
+ data = vector.get_data
92
+ MemoryHelper.write_double(data, row_idx, value)
93
+ when :varchar
94
+ vector.assign_string_element(row_idx, value.to_s)
95
+ when :blob
96
+ vector.assign_string_element_len(row_idx, value.to_s)
97
+ when :timestamp
98
+ data = vector.get_data
99
+ MemoryHelper.write_timestamp(data, row_idx, value)
100
+ when :timestamp_tz
101
+ data = vector.get_data
102
+ MemoryHelper.write_timestamp_tz(data, row_idx, value)
103
+ when :date
104
+ data = vector.get_data
105
+ MemoryHelper.write_date(data, row_idx, value)
106
+ else
107
+ raise ArgumentError, "Unsupported type for DataChunk#set_value: #{type_id} for value `#{value.inspect}`"
108
+ end
109
+
110
+ value
111
+ end
112
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
113
+ end
114
+ end
@@ -6,7 +6,7 @@ module DuckDB
6
6
 
7
7
  def initialize(con, sql)
8
8
  @con = con
9
- super(con, sql)
9
+ super
10
10
  end
11
11
 
12
12
  def each
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuckDB
4
+ #
5
+ # The DuckDB::FunctionInfo provides context during table function execution.
6
+ #
7
+ # It is passed to the execute callback along with the output DataChunk.
8
+ #
9
+ # Example:
10
+ #
11
+ # table_function.execute do |func_info, output|
12
+ # # Report errors during execution
13
+ # func_info.set_error('Something went wrong')
14
+ # end
15
+ #
16
+ # rubocop:disable Lint/EmptyClass
17
+ class FunctionInfo
18
+ # All methods are defined in C extension (ext/duckdb/function_info.c)
19
+ end
20
+ # rubocop:enable Lint/EmptyClass
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuckDB
4
+ #
5
+ # The DuckDB::InitInfo provides context during table function initialization.
6
+ #
7
+ # It is passed to the init callback to set up execution state.
8
+ #
9
+ # Example:
10
+ #
11
+ # table_function.init do |init_info|
12
+ # # Initialize execution state
13
+ # # Can report errors if initialization fails
14
+ # init_info.set_error('Initialization failed')
15
+ # end
16
+ #
17
+ # rubocop:disable Lint/EmptyClass
18
+ class InitInfo
19
+ # All methods are defined in C extension (ext/duckdb/init_info.c)
20
+ end
21
+ # rubocop:enable Lint/EmptyClass
22
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if defined?(DuckDB::InstanceCache)
4
-
5
3
  module DuckDB
6
4
  # The DuckDB::InstanceCache is necessary if a client/program (re)opens
7
5
  # multiple databases to the same file within the same statement.
@@ -22,5 +20,3 @@ module DuckDB
22
20
  end
23
21
  end
24
22
  end
25
-
26
- end
@@ -62,7 +62,7 @@ module DuckDB
62
62
  #
63
63
  # DuckDB::Interval.mk_interval(year: 1, month: 2, day: 3, hour: 4, min: 5, sec: 6, usec: 123456)
64
64
  # => #<DuckDB::Interval:0x00007f9b9c0b3b60 @interval_months=14, @interval_days=3, @interval_micros=14706123456>
65
- def mk_interval(year: 0, month: 0, day: 0, hour: 0, min: 0, sec: 0, usec: 0)
65
+ def mk_interval(year: 0, month: 0, day: 0, hour: 0, min: 0, sec: 0, usec: 0) # rubocop:disable Metrics/ParameterLists
66
66
  Interval.new(
67
67
  interval_months: (year * 12) + month,
68
68
  interval_days: day,
@@ -54,6 +54,47 @@ module DuckDB
54
54
  const_set(method_name.upcase, send(method_name))
55
55
  end
56
56
 
57
+ class << self
58
+ def resolve(symbol)
59
+ return symbol if symbol.is_a?(DuckDB::LogicalType)
60
+
61
+ DuckDB::LogicalType.const_get(symbol.upcase)
62
+ rescue NameError
63
+ raise ArgumentError, "Unknown logical type symbol: #{symbol}"
64
+ end
65
+
66
+ # Creates an array logical type with the given child type and size.
67
+ #
68
+ # The +type+ argument can be a symbol or a DuckDB::LogicalType instance.
69
+ # The +size+ argument specifies the fixed size of the array.
70
+ #
71
+ # require 'duckdb'
72
+ #
73
+ # array_type = DuckDB::LogicalType.create_array(:integer, 3)
74
+ # array_type.type #=> :array
75
+ # array_type.child_type.type #=> :integer
76
+ # array_type.size #=> 3
77
+ def create_array(type, size)
78
+ _create_array_type(LogicalType.resolve(type), size)
79
+ end
80
+
81
+ # Creates a list logical type with the given child type.
82
+ #
83
+ # The +type+ argument can be a symbol or a DuckDB::LogicalType instance.
84
+ #
85
+ # require 'duckdb'
86
+ #
87
+ # list_type = DuckDB::LogicalType.create_list(:integer)
88
+ # list_type.type #=> :list
89
+ # list_type.child_type.type #=> :integer
90
+ #
91
+ # nested_list = DuckDB::LogicalType.create_list(list_type)
92
+ # nested_list.child_type.type #=> :list
93
+ def create_list(type)
94
+ _create_list_type(LogicalType.resolve(type))
95
+ end
96
+ end
97
+
57
98
  # returns logical type's type symbol
58
99
  # `:unknown` means that the logical type's type is unknown/unsupported by ruby-duckdb.
59
100
  # `:invalid` means that the logical type's type is invalid in duckdb.
@@ -104,7 +145,7 @@ module DuckDB
104
145
  # names = union_logical_type.each_member_name.to_a
105
146
  # # => ["member1", "member2"]
106
147
  def each_member_name
107
- return to_enum(__method__) {member_count} unless block_given?
148
+ return to_enum(__method__) { member_count } unless block_given?
108
149
 
109
150
  member_count.times do |i|
110
151
  yield member_name_at(i)
@@ -126,7 +167,7 @@ module DuckDB
126
167
  # names = union_logical_type.each_member_type.map(&:type)
127
168
  # # => [:varchar, :integer]
128
169
  def each_member_type
129
- return to_enum(__method__) {member_count} unless block_given?
170
+ return to_enum(__method__) { member_count } unless block_given?
130
171
 
131
172
  member_count.times do |i|
132
173
  yield member_type_at(i)
@@ -148,7 +189,7 @@ module DuckDB
148
189
  # names = struct_logical_type.each_child_name.to_a
149
190
  # # => ["child1", "child2"]
150
191
  def each_child_name
151
- return to_enum(__method__) {child_count} unless block_given?
192
+ return to_enum(__method__) { child_count } unless block_given?
152
193
 
153
194
  child_count.times do |i|
154
195
  yield child_name_at(i)
@@ -170,7 +211,7 @@ module DuckDB
170
211
  # types = struct_logical_type.each_child_type.map(&:type)
171
212
  # # => [:integer, :varchar]
172
213
  def each_child_type
173
- return to_enum(__method__) {child_count} unless block_given?
214
+ return to_enum(__method__) { child_count } unless block_given?
174
215
 
175
216
  child_count.times do |i|
176
217
  yield child_type_at(i)
@@ -192,7 +233,7 @@ module DuckDB
192
233
  # values = enum_logical_type.each_value.to_a
193
234
  # # => ["happy", "sad"]
194
235
  def each_dictionary_value
195
- return to_enum(__method__) {dictionary_size} unless block_given?
236
+ return to_enum(__method__) { dictionary_size } unless block_given?
196
237
 
197
238
  dictionary_size.times do |i|
198
239
  yield dictionary_value_at(i)
@@ -143,7 +143,8 @@ module DuckDB
143
143
  def bind_uint64(index, val)
144
144
  return _bind_uint64(index, val) if val.between?(0, 18_446_744_073_709_551_615)
145
145
 
146
- raise DuckDB::Error, "can't bind uint64(bind_uint64) to `#{val}`. The `#{val}` is out of range 0..18446744073709551615."
146
+ raise DuckDB::Error,
147
+ "can't bind uint64(bind_uint64) to `#{val}`. The `#{val}` is out of range 0..18446744073709551615."
147
148
  end
148
149
 
149
150
  # binds i-th parameter with SQL prepared statement.
@@ -319,6 +320,7 @@ module DuckDB
319
320
 
320
321
  private
321
322
 
323
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
322
324
  def bind_with_index(index, value)
323
325
  case value
324
326
  when NilClass
@@ -326,14 +328,9 @@ module DuckDB
326
328
  when Float
327
329
  bind_double(index, value)
328
330
  when Integer
329
- case value
330
- when RANGE_INT64
331
- bind_int64(index, value)
332
- else
333
- bind_varchar(index, value.to_s)
334
- end
331
+ bind_integer_value(index, value)
335
332
  when String
336
- blob?(value) ? bind_blob(index, value) : bind_varchar(index, value)
333
+ bind_string_value(index, value)
337
334
  when TrueClass, FalseClass
338
335
  bind_bool(index, value)
339
336
  when Time
@@ -346,6 +343,23 @@ module DuckDB
346
343
  raise(DuckDB::Error, "not supported type `#{value}` (#{value.class})")
347
344
  end
348
345
  end
346
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
347
+
348
+ def bind_integer_value(index, value)
349
+ if RANGE_INT64.cover?(value)
350
+ bind_int64(index, value)
351
+ else
352
+ bind_varchar(index, value.to_s)
353
+ end
354
+ end
355
+
356
+ def bind_string_value(index, value)
357
+ if blob?(value)
358
+ bind_blob(index, value)
359
+ else
360
+ bind_varchar(index, value)
361
+ end
362
+ end
349
363
 
350
364
  def bind_with_name(name, value)
351
365
  raise DuckDB::Error, 'not supported binding with name' unless respond_to?(:bind_parameter_index)
data/lib/duckdb/result.rb CHANGED
@@ -24,6 +24,7 @@ module DuckDB
24
24
  # end
25
25
  class Result
26
26
  include Enumerable
27
+
27
28
  RETURN_TYPES = %i[invalid changed_rows nothing query_result].freeze
28
29
 
29
30
  alias column_size column_count