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.
- checksums.yaml +4 -4
- data/.github/copilot-instructions.md +169 -0
- data/.github/workflows/linter.yml +30 -0
- data/.github/workflows/test_on_macos.yml +7 -2
- data/.github/workflows/test_on_ubuntu.yml +7 -2
- data/.github/workflows/test_on_windows.yml +8 -3
- data/.rubocop.yml +34 -0
- data/CHANGELOG.md +74 -0
- data/Dockerfile +2 -2
- data/Gemfile +12 -0
- data/Gemfile.lock +70 -7
- data/Rakefile +8 -6
- data/bin/console +4 -3
- data/duckdb.gemspec +2 -6
- data/ext/duckdb/bind_info.c +243 -0
- data/ext/duckdb/bind_info.h +13 -0
- data/ext/duckdb/connection.c +60 -2
- data/ext/duckdb/connection.h +1 -0
- data/ext/duckdb/data_chunk.c +137 -0
- data/ext/duckdb/data_chunk.h +13 -0
- data/ext/duckdb/duckdb.c +7 -0
- data/ext/duckdb/function_info.c +65 -0
- data/ext/duckdb/function_info.h +13 -0
- data/ext/duckdb/init_info.c +65 -0
- data/ext/duckdb/init_info.h +13 -0
- data/ext/duckdb/logical_type.c +42 -0
- data/ext/duckdb/memory_helper.c +339 -0
- data/ext/duckdb/memory_helper.h +8 -0
- data/ext/duckdb/result.c +8 -9
- data/ext/duckdb/result.h +1 -0
- data/ext/duckdb/ruby-duckdb.h +7 -0
- data/ext/duckdb/scalar_function.c +336 -1
- data/ext/duckdb/scalar_function.h +2 -0
- data/ext/duckdb/table_function.c +403 -0
- data/ext/duckdb/table_function.h +16 -0
- data/ext/duckdb/vector.c +201 -0
- data/ext/duckdb/vector.h +13 -0
- data/lib/duckdb/appender.rb +21 -11
- data/lib/duckdb/bind_info.rb +32 -0
- data/lib/duckdb/casting.rb +35 -0
- data/lib/duckdb/connection.rb +123 -2
- data/lib/duckdb/converter.rb +32 -47
- data/lib/duckdb/data_chunk.rb +114 -0
- data/lib/duckdb/extracted_statements.rb +1 -1
- data/lib/duckdb/function_info.rb +21 -0
- data/lib/duckdb/init_info.rb +22 -0
- data/lib/duckdb/instance_cache.rb +0 -4
- data/lib/duckdb/interval.rb +1 -1
- data/lib/duckdb/logical_type.rb +46 -5
- data/lib/duckdb/prepared_statement.rb +22 -8
- data/lib/duckdb/result.rb +1 -0
- data/lib/duckdb/scalar_function.rb +119 -0
- data/lib/duckdb/table_function.rb +191 -0
- data/lib/duckdb/vector.rb +20 -0
- data/lib/duckdb/version.rb +1 -1
- data/lib/duckdb.rb +8 -0
- data/sample/async_query.rb +2 -0
- data/sample/async_query_stream.rb +2 -0
- data/sample/issue922.rb +54 -0
- data/sample/issue922_benchmark.rb +169 -0
- data/sample/issue930.rb +49 -0
- data/sample/issue930_benchmark.rb +70 -0
- metadata +30 -57
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DuckDB
|
|
4
|
+
# DuckDB::ScalarFunction encapsulates DuckDB's scalar function
|
|
5
|
+
#
|
|
6
|
+
# @note DuckDB::ScalarFunction is experimental.
|
|
7
|
+
# @note DuckDB::ScalarFunction must be used with threads=1 in DuckDB.
|
|
8
|
+
# Set this with: connection.execute('SET threads=1')
|
|
9
|
+
class ScalarFunction
|
|
10
|
+
# Create and configure a scalar function in one call
|
|
11
|
+
#
|
|
12
|
+
# @param name [String, Symbol] the function name
|
|
13
|
+
# @param return_type [DuckDB::LogicalType] the return type
|
|
14
|
+
# @param parameter_type [DuckDB::LogicalType, nil] single parameter type (use this OR parameter_types)
|
|
15
|
+
# @param parameter_types [Array<DuckDB::LogicalType>, nil] multiple parameter types
|
|
16
|
+
# @yield [*args] the function implementation
|
|
17
|
+
# @return [DuckDB::ScalarFunction] configured scalar function ready to register
|
|
18
|
+
# @raise [ArgumentError] if block is not provided or both parameter_type and parameter_types are specified
|
|
19
|
+
#
|
|
20
|
+
# @example Single parameter function
|
|
21
|
+
# sf = DuckDB::ScalarFunction.create(
|
|
22
|
+
# name: :triple,
|
|
23
|
+
# return_type: DuckDB::LogicalType::INTEGER,
|
|
24
|
+
# parameter_type: DuckDB::LogicalType::INTEGER
|
|
25
|
+
# ) { |v| v * 3 }
|
|
26
|
+
#
|
|
27
|
+
# @example Multiple parameters
|
|
28
|
+
# sf = DuckDB::ScalarFunction.create(
|
|
29
|
+
# name: :add,
|
|
30
|
+
# return_type: DuckDB::LogicalType::INTEGER,
|
|
31
|
+
# parameter_types: [DuckDB::LogicalType::INTEGER, DuckDB::LogicalType::INTEGER]
|
|
32
|
+
# ) { |a, b| a + b }
|
|
33
|
+
#
|
|
34
|
+
# @example No parameters (constant function)
|
|
35
|
+
# sf = DuckDB::ScalarFunction.create(
|
|
36
|
+
# name: :random_num,
|
|
37
|
+
# return_type: DuckDB::LogicalType::INTEGER
|
|
38
|
+
# ) { rand(100) }
|
|
39
|
+
def self.create(name:, return_type:, parameter_type: nil, parameter_types: nil, &) # rubocop:disable Metrics/MethodLength
|
|
40
|
+
raise ArgumentError, 'Block required' unless block_given?
|
|
41
|
+
raise ArgumentError, 'Cannot specify both parameter_type and parameter_types' if parameter_type && parameter_types
|
|
42
|
+
|
|
43
|
+
params = if parameter_type
|
|
44
|
+
[parameter_type]
|
|
45
|
+
elsif parameter_types
|
|
46
|
+
parameter_types
|
|
47
|
+
else
|
|
48
|
+
[]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sf = new
|
|
52
|
+
sf.name = name.to_s
|
|
53
|
+
sf.return_type = return_type
|
|
54
|
+
params.each { |type| sf.add_parameter(type) }
|
|
55
|
+
sf.set_function(&)
|
|
56
|
+
sf
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Supported types for scalar function parameters and return values
|
|
60
|
+
SUPPORTED_TYPES = %i[
|
|
61
|
+
bigint
|
|
62
|
+
blob
|
|
63
|
+
boolean
|
|
64
|
+
date
|
|
65
|
+
double
|
|
66
|
+
float
|
|
67
|
+
integer
|
|
68
|
+
smallint
|
|
69
|
+
time
|
|
70
|
+
timestamp
|
|
71
|
+
tinyint
|
|
72
|
+
ubigint
|
|
73
|
+
uinteger
|
|
74
|
+
usmallint
|
|
75
|
+
utinyint
|
|
76
|
+
varchar
|
|
77
|
+
].freeze
|
|
78
|
+
|
|
79
|
+
private_constant :SUPPORTED_TYPES
|
|
80
|
+
|
|
81
|
+
# Adds a parameter to the scalar function.
|
|
82
|
+
# Currently supports BIGINT, BLOB, BOOLEAN, DATE, DOUBLE, FLOAT, INTEGER, SMALLINT, TIME, TIMESTAMP, TINYINT,
|
|
83
|
+
# UBIGINT, UINTEGER, USMALLINT, UTINYINT, and VARCHAR types.
|
|
84
|
+
#
|
|
85
|
+
# @param logical_type [DuckDB::LogicalType] the parameter type
|
|
86
|
+
# @return [DuckDB::ScalarFunction] self
|
|
87
|
+
# @raise [DuckDB::Error] if the type is not supported
|
|
88
|
+
def add_parameter(logical_type)
|
|
89
|
+
raise DuckDB::Error, 'logical_type must be a DuckDB::LogicalType' unless logical_type.is_a?(DuckDB::LogicalType)
|
|
90
|
+
|
|
91
|
+
unless SUPPORTED_TYPES.include?(logical_type.type)
|
|
92
|
+
type_list = SUPPORTED_TYPES.map(&:upcase).join(', ')
|
|
93
|
+
raise DuckDB::Error,
|
|
94
|
+
"Only #{type_list} parameter types are currently supported"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
_add_parameter(logical_type)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Sets the return type for the scalar function.
|
|
101
|
+
# Currently supports BIGINT, BLOB, BOOLEAN, DATE, DOUBLE, FLOAT, INTEGER, SMALLINT, TIME, TIMESTAMP, TINYINT,
|
|
102
|
+
# UBIGINT, UINTEGER, USMALLINT, UTINYINT, and VARCHAR types.
|
|
103
|
+
#
|
|
104
|
+
# @param logical_type [DuckDB::LogicalType] the return type
|
|
105
|
+
# @return [DuckDB::ScalarFunction] self
|
|
106
|
+
# @raise [DuckDB::Error] if the type is not supported
|
|
107
|
+
def return_type=(logical_type)
|
|
108
|
+
raise DuckDB::Error, 'logical_type must be a DuckDB::LogicalType' unless logical_type.is_a?(DuckDB::LogicalType)
|
|
109
|
+
|
|
110
|
+
unless SUPPORTED_TYPES.include?(logical_type.type)
|
|
111
|
+
type_list = SUPPORTED_TYPES.map(&:upcase).join(', ')
|
|
112
|
+
raise DuckDB::Error,
|
|
113
|
+
"Only #{type_list} return types are currently supported"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
_set_return_type(logical_type)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DuckDB
|
|
4
|
+
#
|
|
5
|
+
# The DuckDB::TableFunction encapsulates a DuckDB table function.
|
|
6
|
+
#
|
|
7
|
+
# NOTE: DuckDB::TableFunction is experimental now.
|
|
8
|
+
#
|
|
9
|
+
# require 'duckdb'
|
|
10
|
+
#
|
|
11
|
+
# db = DuckDB::Database.new
|
|
12
|
+
# conn = db.connect
|
|
13
|
+
#
|
|
14
|
+
# # Low-level API:
|
|
15
|
+
# tf = DuckDB::TableFunction.new
|
|
16
|
+
# tf.name = 'my_function'
|
|
17
|
+
# tf.add_parameter(DuckDB::LogicalType::BIGINT)
|
|
18
|
+
#
|
|
19
|
+
# tf.bind do |bind_info|
|
|
20
|
+
# bind_info.add_result_column('value', DuckDB::LogicalType::BIGINT)
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# tf.execute do |func_info, output|
|
|
24
|
+
# # Fill output data...
|
|
25
|
+
# 0 # Return 0 to signal done
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# conn.register_table_function(tf)
|
|
29
|
+
#
|
|
30
|
+
# # High-level API (recommended):
|
|
31
|
+
# tf = DuckDB::TableFunction.create(
|
|
32
|
+
# name: 'my_function',
|
|
33
|
+
# parameters: [DuckDB::LogicalType::BIGINT],
|
|
34
|
+
# columns: { 'value' => DuckDB::LogicalType::BIGINT }
|
|
35
|
+
# ) do |func_info, output|
|
|
36
|
+
# # Fill output data...
|
|
37
|
+
# 0 # Return row count (0 when done)
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
class TableFunction
|
|
41
|
+
# TableFunction#initialize is defined in C extension
|
|
42
|
+
|
|
43
|
+
@table_adapters = {}
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
#
|
|
47
|
+
# Creates a new table function with a declarative API.
|
|
48
|
+
#
|
|
49
|
+
# @param name [String] The name of the table function
|
|
50
|
+
# @param parameters [Array<LogicalType>, Hash<String, LogicalType>] Function parameters (optional)
|
|
51
|
+
# @param columns [Hash<String, LogicalType>] Output columns (required)
|
|
52
|
+
# @yield [func_info, output] The execute block that generates data
|
|
53
|
+
# @yieldparam func_info [FunctionInfo] Function execution context
|
|
54
|
+
# @yieldparam output [DataChunk] Output data chunk to fill
|
|
55
|
+
# @yieldreturn [Integer] Number of rows generated (0 when done)
|
|
56
|
+
# @return [TableFunction] The configured table function
|
|
57
|
+
#
|
|
58
|
+
# @example Simple range function
|
|
59
|
+
# tf = TableFunction.create(
|
|
60
|
+
# name: 'my_range',
|
|
61
|
+
# parameters: [LogicalType::BIGINT],
|
|
62
|
+
# columns: { 'value' => LogicalType::BIGINT }
|
|
63
|
+
# ) do |func_info, output|
|
|
64
|
+
# # Generate data...
|
|
65
|
+
# 0 # Signal done
|
|
66
|
+
# end
|
|
67
|
+
#
|
|
68
|
+
# @example Function that returns data
|
|
69
|
+
# tf = TableFunction.create(
|
|
70
|
+
# name: 'my_function',
|
|
71
|
+
# columns: { 'value' => LogicalType::BIGINT }
|
|
72
|
+
# ) do |func_info, output|
|
|
73
|
+
# vec = output.get_vector(0)
|
|
74
|
+
# # Fill vector...
|
|
75
|
+
# 3 # Return row count
|
|
76
|
+
# end
|
|
77
|
+
#
|
|
78
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
79
|
+
def create(name:, columns:, parameters: nil, &)
|
|
80
|
+
raise ArgumentError, 'name is required' unless name
|
|
81
|
+
raise ArgumentError, 'columns are required' unless columns
|
|
82
|
+
raise ArgumentError, 'block is required' unless block_given?
|
|
83
|
+
|
|
84
|
+
tf = new
|
|
85
|
+
tf.name = name
|
|
86
|
+
|
|
87
|
+
# Add parameters (positional or named)
|
|
88
|
+
if parameters
|
|
89
|
+
case parameters
|
|
90
|
+
when Array
|
|
91
|
+
parameters.each { |type| tf.add_parameter(type) }
|
|
92
|
+
when Hash
|
|
93
|
+
parameters.each { |param_name, type| tf.add_named_parameter(param_name, type) }
|
|
94
|
+
else
|
|
95
|
+
raise ArgumentError, 'parameters must be Array or Hash'
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Set bind callback to add result columns
|
|
100
|
+
tf.bind do |bind_info|
|
|
101
|
+
columns.each do |col_name, col_type|
|
|
102
|
+
bind_info.add_result_column(col_name, col_type)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Set init callback (required by DuckDB)
|
|
107
|
+
tf.init do |_init_info|
|
|
108
|
+
# No-op
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Set execute callback - user's block returns row count
|
|
112
|
+
tf.execute do |func_info, output|
|
|
113
|
+
size = yield(func_info, output)
|
|
114
|
+
output.size = Integer(size)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
tf
|
|
118
|
+
end
|
|
119
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
120
|
+
|
|
121
|
+
# Registers a table adapter for a Ruby class.
|
|
122
|
+
#
|
|
123
|
+
# The adapter is used by +DuckDB::Connection#expose_as_table+ to convert
|
|
124
|
+
# instances of +klass+ into a DuckDB table function. The adapter must respond
|
|
125
|
+
# to +call(object, name, columns: nil)+ and return a +DuckDB::TableFunction+.
|
|
126
|
+
#
|
|
127
|
+
# == Implementing a Table Adapter
|
|
128
|
+
#
|
|
129
|
+
# An adapter is any object that responds to +call(object, name, columns: nil)+.
|
|
130
|
+
# The +columns:+ keyword argument allows callers to override the column schema;
|
|
131
|
+
# the adapter should fall back to its own schema detection when it is +nil+.
|
|
132
|
+
#
|
|
133
|
+
# The execute block passed to +DuckDB::TableFunction.create+ must:
|
|
134
|
+
# - Write one batch of rows into +output+ per call
|
|
135
|
+
# - Return the number of rows written as an +Integer+
|
|
136
|
+
# - Return +0+ to signal that all data has been exhausted
|
|
137
|
+
#
|
|
138
|
+
# @example Minimal adapter for CSV objects
|
|
139
|
+
# class CSVTableAdapter
|
|
140
|
+
# def call(csv, name, columns: nil)
|
|
141
|
+
# columns ||= infer_columns(csv)
|
|
142
|
+
#
|
|
143
|
+
# DuckDB::TableFunction.create(name:, columns:) do |_func_info, output|
|
|
144
|
+
# row = csv.readline
|
|
145
|
+
# if row
|
|
146
|
+
# row.each_with_index { |cell, i| output.set_value(i, 0, cell[1]) }
|
|
147
|
+
# 1 # wrote one row
|
|
148
|
+
# else
|
|
149
|
+
# csv.rewind
|
|
150
|
+
# 0 # signal end of data
|
|
151
|
+
# end
|
|
152
|
+
# end
|
|
153
|
+
# end
|
|
154
|
+
#
|
|
155
|
+
# private
|
|
156
|
+
#
|
|
157
|
+
# def infer_columns(csv)
|
|
158
|
+
# headers = csv.first.headers
|
|
159
|
+
# csv.rewind
|
|
160
|
+
# headers.each_with_object({}) { |h, hsh| hsh[h] = DuckDB::LogicalType::VARCHAR }
|
|
161
|
+
# end
|
|
162
|
+
# end
|
|
163
|
+
#
|
|
164
|
+
# # Register and use:
|
|
165
|
+
# DuckDB::TableFunction.add_table_adapter(CSV, CSVTableAdapter.new)
|
|
166
|
+
# con.execute('SET threads=1')
|
|
167
|
+
# con.expose_as_table(csv, 'csv_table')
|
|
168
|
+
# con.query('SELECT * FROM csv_table()').to_a
|
|
169
|
+
#
|
|
170
|
+
# @param klass [Class] the Ruby class to register an adapter for (e.g. +CSV+)
|
|
171
|
+
# @param adapter [#call] the adapter object
|
|
172
|
+
# @return [void]
|
|
173
|
+
#
|
|
174
|
+
def add_table_adapter(klass, adapter)
|
|
175
|
+
@table_adapters[klass] = adapter
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Returns the table adapter registered for the given class, or +nil+ if none.
|
|
179
|
+
#
|
|
180
|
+
# @param klass [Class] the Ruby class to look up
|
|
181
|
+
# @return [#call, nil] the registered adapter, or +nil+ if not found
|
|
182
|
+
#
|
|
183
|
+
# @example
|
|
184
|
+
# adapter = DuckDB::TableFunction.table_adapter_for(CSV)
|
|
185
|
+
#
|
|
186
|
+
def table_adapter_for(klass)
|
|
187
|
+
@table_adapters[klass]
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DuckDB
|
|
4
|
+
#
|
|
5
|
+
# The DuckDB::Vector represents a column vector in a data chunk.
|
|
6
|
+
#
|
|
7
|
+
# Vectors store the actual data for table function output.
|
|
8
|
+
#
|
|
9
|
+
# Example:
|
|
10
|
+
#
|
|
11
|
+
# vector = output.get_vector(0)
|
|
12
|
+
# vector.assign_string_element(0, 'hello')
|
|
13
|
+
# vector.assign_string_element(1, 'world')
|
|
14
|
+
#
|
|
15
|
+
# rubocop:disable Lint/EmptyClass
|
|
16
|
+
class Vector
|
|
17
|
+
# All methods are defined in C extension (ext/duckdb/vector.c)
|
|
18
|
+
end
|
|
19
|
+
# rubocop:enable Lint/EmptyClass
|
|
20
|
+
end
|
data/lib/duckdb/version.rb
CHANGED
data/lib/duckdb.rb
CHANGED
|
@@ -14,8 +14,16 @@ require 'duckdb/appender'
|
|
|
14
14
|
require 'duckdb/config'
|
|
15
15
|
require 'duckdb/column'
|
|
16
16
|
require 'duckdb/logical_type'
|
|
17
|
+
require 'duckdb/scalar_function'
|
|
18
|
+
require 'duckdb/bind_info'
|
|
19
|
+
require 'duckdb/init_info'
|
|
20
|
+
require 'duckdb/function_info'
|
|
21
|
+
require 'duckdb/vector'
|
|
22
|
+
require 'duckdb/data_chunk'
|
|
23
|
+
require 'duckdb/table_function'
|
|
17
24
|
require 'duckdb/infinity'
|
|
18
25
|
require 'duckdb/instance_cache'
|
|
26
|
+
require 'duckdb/casting'
|
|
19
27
|
|
|
20
28
|
# DuckDB provides Ruby interface of DuckDB.
|
|
21
29
|
module DuckDB
|
data/sample/async_query.rb
CHANGED
data/sample/issue922.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'duckdb'
|
|
4
|
+
require 'polars-df'
|
|
5
|
+
|
|
6
|
+
class PolarsDataFrameTableAdapter
|
|
7
|
+
def call(data_frame, name, columns: nil)
|
|
8
|
+
columns ||= infer_columns(data_frame)
|
|
9
|
+
DuckDB::TableFunction.create(name:, columns:, &execute_block(data_frame))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def execute_block(data_frame)
|
|
15
|
+
counter = 0
|
|
16
|
+
height = data_frame.height
|
|
17
|
+
width = data_frame.width
|
|
18
|
+
proc do |_func_info, output|
|
|
19
|
+
next counter = 0 if counter >= height
|
|
20
|
+
|
|
21
|
+
write_row(data_frame, output, counter, width)
|
|
22
|
+
counter += 1
|
|
23
|
+
1
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def write_row(data_frame, output, counter, width)
|
|
28
|
+
width.times { |index| output.set_value(index, 0, data_frame[counter, index]) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def infer_columns(data_frame)
|
|
32
|
+
data_frame.columns.to_h { |header| [header, DuckDB::LogicalType::VARCHAR] }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
DuckDB::TableFunction.add_table_adapter(Polars::DataFrame, PolarsDataFrameTableAdapter.new)
|
|
37
|
+
|
|
38
|
+
df = Polars::DataFrame.new(
|
|
39
|
+
{
|
|
40
|
+
a: [1, 2, 3],
|
|
41
|
+
b: %w[one two three]
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
db = DuckDB::Database.open
|
|
46
|
+
con = db.connect
|
|
47
|
+
con.query('SET threads=1')
|
|
48
|
+
con.expose_as_table(df, 'polars_df')
|
|
49
|
+
result = con.query('SELECT * FROM polars_df()').to_a
|
|
50
|
+
p result
|
|
51
|
+
puts result.first.first == '1'
|
|
52
|
+
|
|
53
|
+
con.close
|
|
54
|
+
db.close
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Style/OneClassPerFile
|
|
4
|
+
require 'duckdb'
|
|
5
|
+
require 'polars-df'
|
|
6
|
+
require 'tmpdir'
|
|
7
|
+
|
|
8
|
+
class PolarsDataFrameTableAdapter
|
|
9
|
+
def call(data_frame, name, columns: nil)
|
|
10
|
+
columns ||= infer_columns(data_frame)
|
|
11
|
+
DuckDB::TableFunction.create(name:, columns:, &execute_block(data_frame))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def execute_block(data_frame)
|
|
17
|
+
counter = 0
|
|
18
|
+
height = data_frame.height
|
|
19
|
+
width = data_frame.width
|
|
20
|
+
proc do |_func_info, output|
|
|
21
|
+
next counter = 0 if counter >= height
|
|
22
|
+
|
|
23
|
+
write_row(data_frame, output, counter, width)
|
|
24
|
+
counter += 1
|
|
25
|
+
1
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def write_row(data_frame, output, counter, width)
|
|
30
|
+
width.times { |index| output.set_value(index, 0, data_frame[counter, index]) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def infer_columns(data_frame)
|
|
34
|
+
data_frame.columns.to_h { |header| [header, DuckDB::LogicalType::VARCHAR] }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Batch approach: write BATCH_SIZE rows per execute call to reduce Ruby<->C crossings
|
|
39
|
+
class PolarsDataFrameBatchTableAdapter
|
|
40
|
+
BATCH_SIZE = 2048
|
|
41
|
+
|
|
42
|
+
def call(data_frame, name, columns: nil)
|
|
43
|
+
columns ||= infer_columns(data_frame)
|
|
44
|
+
DuckDB::TableFunction.create(name:, columns:, &execute_block(data_frame))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def execute_block(data_frame)
|
|
50
|
+
offset = 0
|
|
51
|
+
height = data_frame.height
|
|
52
|
+
width = data_frame.width
|
|
53
|
+
proc do |_func_info, output|
|
|
54
|
+
next offset = 0 if offset >= height
|
|
55
|
+
|
|
56
|
+
rows = [height - offset, BATCH_SIZE].min
|
|
57
|
+
write_batch(data_frame, output, offset, rows, width)
|
|
58
|
+
offset += rows
|
|
59
|
+
rows
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def write_batch(data_frame, output, offset, rows, width)
|
|
64
|
+
rows.times do |row_idx|
|
|
65
|
+
width.times { |col_idx| output.set_value(col_idx, row_idx, data_frame[offset + row_idx, col_idx]) }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def infer_columns(data_frame)
|
|
70
|
+
data_frame.columns.to_h { |header| [header, DuckDB::LogicalType::VARCHAR] }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Optimized batch approach: pre-extract columns as Ruby arrays to avoid
|
|
75
|
+
# repeated Polars FFI calls, and use assign_string_element to skip type dispatch
|
|
76
|
+
class PolarsDataFrameOptimizedTableAdapter
|
|
77
|
+
BATCH_SIZE = 2048
|
|
78
|
+
|
|
79
|
+
def call(data_frame, name, columns: nil)
|
|
80
|
+
columns ||= infer_columns(data_frame)
|
|
81
|
+
DuckDB::TableFunction.create(name:, columns:, &execute_block(data_frame))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# rubocop:disable Metrics/MethodLength
|
|
87
|
+
def execute_block(data_frame)
|
|
88
|
+
col_arrays = extract_columns(data_frame)
|
|
89
|
+
offset = 0
|
|
90
|
+
height = data_frame.height
|
|
91
|
+
width = data_frame.width
|
|
92
|
+
proc do |_func_info, output|
|
|
93
|
+
next offset = 0 if offset >= height
|
|
94
|
+
|
|
95
|
+
rows = [height - offset, BATCH_SIZE].min
|
|
96
|
+
vectors = width.times.map { |i| output.get_vector(i) }
|
|
97
|
+
write_batch(col_arrays, vectors, offset, rows)
|
|
98
|
+
offset += rows
|
|
99
|
+
rows
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
# rubocop:enable Metrics/MethodLength
|
|
103
|
+
|
|
104
|
+
def extract_columns(data_frame)
|
|
105
|
+
data_frame.columns.map { |col| data_frame[col].cast(Polars::Utf8).to_a }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def write_batch(col_arrays, vectors, offset, rows)
|
|
109
|
+
col_arrays.each_with_index do |col_data, col_idx|
|
|
110
|
+
vec = vectors[col_idx]
|
|
111
|
+
rows.times { |row_idx| vec.assign_string_element(row_idx, col_data[offset + row_idx].to_s) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def infer_columns(data_frame)
|
|
116
|
+
data_frame.columns.to_h { |header| [header, DuckDB::LogicalType::VARCHAR] }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def query_via_parquet(con, data_frame, name, parquet_path)
|
|
121
|
+
data_frame.write_parquet(parquet_path)
|
|
122
|
+
con.query("CREATE OR REPLACE TABLE #{name} AS SELECT * FROM read_parquet('#{parquet_path}')")
|
|
123
|
+
con.query("SELECT * FROM #{name}").to_a
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
df = Polars::DataFrame.new(
|
|
127
|
+
{
|
|
128
|
+
id: 100_000.times.map { |i| i + 1 },
|
|
129
|
+
name: 100_000.times.map { |i| "Name#{i + 1}" },
|
|
130
|
+
age: 100_000.times.map { rand(0..100) }
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
db = DuckDB::Database.open
|
|
135
|
+
con = db.connect
|
|
136
|
+
con.query('SET threads=1')
|
|
137
|
+
|
|
138
|
+
DuckDB::TableFunction.add_table_adapter(Polars::DataFrame, PolarsDataFrameTableAdapter.new)
|
|
139
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
140
|
+
con.expose_as_table(df, 'polars_tf')
|
|
141
|
+
con.query('SELECT * FROM polars_tf()').to_a
|
|
142
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
143
|
+
|
|
144
|
+
DuckDB::TableFunction.add_table_adapter(Polars::DataFrame, PolarsDataFrameBatchTableAdapter.new)
|
|
145
|
+
start_time3 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
146
|
+
con.expose_as_table(df, 'polars_tf_batch')
|
|
147
|
+
con.query('SELECT * FROM polars_tf_batch()').to_a
|
|
148
|
+
end_time3 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
149
|
+
|
|
150
|
+
DuckDB::TableFunction.add_table_adapter(Polars::DataFrame, PolarsDataFrameOptimizedTableAdapter.new)
|
|
151
|
+
start_time4 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
152
|
+
con.expose_as_table(df, 'polars_tf_opt')
|
|
153
|
+
con.query('SELECT * FROM polars_tf_opt()').to_a
|
|
154
|
+
end_time4 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
155
|
+
|
|
156
|
+
parquet_path = File.join(Dir.tmpdir, 'issue922_benchmark.parquet')
|
|
157
|
+
start_time2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
158
|
+
query_via_parquet(con, df, 'polars_pq', parquet_path)
|
|
159
|
+
end_time2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
160
|
+
|
|
161
|
+
con.close
|
|
162
|
+
db.close
|
|
163
|
+
File.delete(parquet_path)
|
|
164
|
+
|
|
165
|
+
puts "Time taken for table function approach (1 row/call): #{end_time - start_time} seconds"
|
|
166
|
+
puts "Time taken for table function approach (batch/call): #{end_time3 - start_time3} seconds"
|
|
167
|
+
puts "Time taken for table function approach (batch + pre-extract): #{end_time4 - start_time4} seconds"
|
|
168
|
+
puts "Time taken for parquet file approach: #{end_time2 - start_time2} seconds"
|
|
169
|
+
# rubocop:enable Style/OneClassPerFile
|
data/sample/issue930.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'duckdb'
|
|
4
|
+
require 'csv'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
|
|
7
|
+
class CSVTableAdapter
|
|
8
|
+
def call(csv, name, columns: nil)
|
|
9
|
+
columns ||= infer_columns(csv)
|
|
10
|
+
DuckDB::TableFunction.create(name:, columns:) do |_func_info, output|
|
|
11
|
+
write_row(csv, output)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def write_row(csv, output)
|
|
18
|
+
line = csv.readline
|
|
19
|
+
if line
|
|
20
|
+
line.each_with_index { |cell, index| output.set_value(index, 0, cell[1]) }
|
|
21
|
+
1
|
|
22
|
+
else
|
|
23
|
+
csv.rewind
|
|
24
|
+
0
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def infer_columns(csv)
|
|
29
|
+
headers = csv.first.headers
|
|
30
|
+
csv.rewind
|
|
31
|
+
headers.to_h { |header| [header, DuckDB::LogicalType::VARCHAR] }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
DuckDB::TableFunction.add_table_adapter(CSV, CSVTableAdapter.new)
|
|
36
|
+
|
|
37
|
+
csv_data = "id,name,age\n1,Alice,30\n2,Bob,25\n3,Charlie,35"
|
|
38
|
+
csv_data += 30_000.times.map { |i| "\n#{i + 4},Name#{i + 4},#{rand(0..100)}" }.join
|
|
39
|
+
csv = CSV.new(StringIO.new(csv_data), headers: true)
|
|
40
|
+
|
|
41
|
+
db = DuckDB::Database.open
|
|
42
|
+
con = db.connect
|
|
43
|
+
con.query('SET threads=1')
|
|
44
|
+
con.expose_as_table(csv, 'csv_table')
|
|
45
|
+
result = con.query('SELECT * FROM csv_table()').to_a
|
|
46
|
+
|
|
47
|
+
p result
|
|
48
|
+
puts result.first.first == '1'
|
|
49
|
+
puts result.first[1] == 'Alice'
|