duckdb 0.3.1.0 → 0.3.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/workflows/test_on_macos.yml +31 -9
- data/.github/workflows/test_on_ubuntu.yml +11 -3
- data/.github/workflows/test_on_windows.yml +16 -7
- data/CHANGELOG.md +18 -0
- data/Gemfile.lock +4 -3
- data/README.md +32 -3
- data/ext/duckdb/appender.c +7 -59
- data/ext/duckdb/appender.h +0 -5
- data/ext/duckdb/blob.c +1 -5
- data/ext/duckdb/blob.h +0 -4
- data/ext/duckdb/column.c +73 -0
- data/ext/duckdb/column.h +14 -0
- data/ext/duckdb/config.c +2 -4
- data/ext/duckdb/connection.c +2 -3
- data/ext/duckdb/duckdb.c +2 -11
- data/ext/duckdb/error.c +1 -2
- data/ext/duckdb/extconf.rb +17 -21
- data/ext/duckdb/prepared_statement.c +100 -46
- data/ext/duckdb/result.c +51 -20
- data/ext/duckdb/ruby-duckdb.h +7 -20
- data/ext/duckdb/util.c +45 -0
- data/ext/duckdb/util.h +13 -0
- data/lib/duckdb/appender.rb +207 -261
- data/lib/duckdb/column.rb +55 -0
- data/lib/duckdb/config.rb +1 -4
- data/lib/duckdb/converter.rb +52 -0
- data/lib/duckdb/prepared_statement.rb +111 -8
- data/lib/duckdb/version.rb +1 -1
- data/lib/duckdb.rb +2 -0
- metadata +9 -3
data/lib/duckdb/appender.rb
CHANGED
@@ -1,298 +1,244 @@
|
|
1
1
|
require 'date'
|
2
2
|
require 'time'
|
3
|
+
require_relative './converter'
|
3
4
|
|
4
5
|
module DuckDB
|
5
|
-
|
6
|
-
|
6
|
+
# The DuckDB::Appender encapsulates DuckDB Appender.
|
7
|
+
#
|
8
|
+
# require 'duckdb'
|
9
|
+
# db = DuckDB::Database.open
|
10
|
+
# con = db.connect
|
11
|
+
# con.query('CREATE TABLE users (id INTEGER, name VARCHAR)')
|
12
|
+
# appender = con.appender('users')
|
13
|
+
# appender.append_row(1, 'Alice')
|
14
|
+
#
|
15
|
+
class Appender
|
16
|
+
include DuckDB::Converter
|
17
|
+
|
18
|
+
RANGE_INT16 = -32_768..32_767
|
19
|
+
RANGE_INT32 = -2_147_483_648..2_147_483_647
|
20
|
+
RANGE_INT64 = -9_223_372_036_854_775_808..9_223_372_036_854_775_807
|
21
|
+
|
22
|
+
#
|
23
|
+
# appends huge int value.
|
7
24
|
#
|
8
25
|
# require 'duckdb'
|
9
26
|
# db = DuckDB::Database.open
|
10
27
|
# con = db.connect
|
11
|
-
# con.query('CREATE TABLE
|
12
|
-
# appender = con.appender('
|
13
|
-
# appender
|
28
|
+
# con.query('CREATE TABLE numbers (num HUGEINT)')
|
29
|
+
# appender = con.appender('numbers')
|
30
|
+
# appender
|
31
|
+
# .begin_row
|
32
|
+
# .append_hugeint(-170_141_183_460_469_231_731_687_303_715_884_105_727)
|
33
|
+
# .end_row
|
14
34
|
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
# db = DuckDB::Database.open
|
25
|
-
# con = db.connect
|
26
|
-
# con.query('CREATE TABLE numbers (num HUGEINT)')
|
27
|
-
# appender = con.appender('numbers')
|
28
|
-
# appender
|
29
|
-
# .begin_row
|
30
|
-
# .append_hugeint(-170_141_183_460_469_231_731_687_303_715_884_105_727)
|
31
|
-
# .end_row
|
32
|
-
#
|
33
|
-
def append_hugeint(value)
|
34
|
-
case value
|
35
|
-
when Integer
|
36
|
-
if respond_to?(:_append_hugeint, true)
|
37
|
-
half = 1 << 64
|
38
|
-
upper = value / half
|
39
|
-
lower = value - upper * half
|
40
|
-
_append_hugeint(lower, upper)
|
41
|
-
else
|
42
|
-
append_varchar(value.to_s)
|
43
|
-
end
|
44
|
-
else
|
45
|
-
raise(ArgumentError, "2nd argument `#{value}` must be Integer.")
|
46
|
-
end
|
35
|
+
def append_hugeint(value)
|
36
|
+
case value
|
37
|
+
when Integer
|
38
|
+
half = 1 << 64
|
39
|
+
upper = value / half
|
40
|
+
lower = value - upper * half
|
41
|
+
_append_hugeint(lower, upper)
|
42
|
+
else
|
43
|
+
raise(ArgumentError, "2nd argument `#{value}` must be Integer.")
|
47
44
|
end
|
45
|
+
end
|
48
46
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
end
|
47
|
+
#
|
48
|
+
# appends date value.
|
49
|
+
#
|
50
|
+
# require 'duckdb'
|
51
|
+
# db = DuckDB::Database.open
|
52
|
+
# con = db.connect
|
53
|
+
# con.query('CREATE TABLE dates (date_value DATE)')
|
54
|
+
# appender = con.appender('dates')
|
55
|
+
# appender.begin_row
|
56
|
+
# appender.append_date(Date.today)
|
57
|
+
# # or
|
58
|
+
# # appender.append_date(Time.now)
|
59
|
+
# # appender.append_date('2021-10-10')
|
60
|
+
# appender.end_row
|
61
|
+
# appender.flush
|
62
|
+
#
|
63
|
+
def append_date(value)
|
64
|
+
date = case value
|
65
|
+
when Date, Time
|
66
|
+
value
|
67
|
+
else
|
68
|
+
begin
|
69
|
+
Date.parse(value)
|
70
|
+
rescue
|
71
|
+
raise(ArgumentError, "Cannot parse argument `#{value}` to Date.")
|
75
72
|
end
|
73
|
+
end
|
76
74
|
|
77
|
-
|
78
|
-
|
75
|
+
_append_date(date.year, date.month, date.day)
|
76
|
+
end
|
79
77
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
end
|
78
|
+
#
|
79
|
+
# appends time value.
|
80
|
+
#
|
81
|
+
# require 'duckdb'
|
82
|
+
# db = DuckDB::Database.open
|
83
|
+
# con = db.connect
|
84
|
+
# con.query('CREATE TABLE times (time_value TIME)')
|
85
|
+
# appender = con.appender('times')
|
86
|
+
# appender.begin_row
|
87
|
+
# appender.append_time(Time.now)
|
88
|
+
# # or
|
89
|
+
# # appender.append_time('01:01:01')
|
90
|
+
# appender.end_row
|
91
|
+
# appender.flush
|
92
|
+
#
|
93
|
+
def append_time(value)
|
94
|
+
time = case value
|
95
|
+
when Time
|
96
|
+
value
|
97
|
+
else
|
98
|
+
begin
|
99
|
+
Time.parse(value)
|
100
|
+
rescue
|
101
|
+
raise(ArgumentError, "Cannot parse argument `#{value}` to Time.")
|
105
102
|
end
|
103
|
+
end
|
106
104
|
|
107
|
-
|
108
|
-
|
105
|
+
_append_time(time.hour, time.min, time.sec, time.usec)
|
106
|
+
end
|
109
107
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
end
|
108
|
+
#
|
109
|
+
# appends timestamp value.
|
110
|
+
#
|
111
|
+
# require 'duckdb'
|
112
|
+
# db = DuckDB::Database.open
|
113
|
+
# con = db.connect
|
114
|
+
# con.query('CREATE TABLE timestamps (timestamp_value TIMESTAMP)')
|
115
|
+
# appender = con.appender('timestamps')
|
116
|
+
# appender.begin_row
|
117
|
+
# appender.append_time(Time.now)
|
118
|
+
# # or
|
119
|
+
# # appender.append_time(Date.today)
|
120
|
+
# # appender.append_time('2021-08-01 01:01:01')
|
121
|
+
# appender.end_row
|
122
|
+
# appender.flush
|
123
|
+
#
|
124
|
+
def append_timestamp(value)
|
125
|
+
time = case value
|
126
|
+
when Time
|
127
|
+
value
|
128
|
+
when Date
|
129
|
+
value.to_time
|
130
|
+
else
|
131
|
+
begin
|
132
|
+
Time.parse(value)
|
133
|
+
rescue
|
134
|
+
raise(ArgumentError, "Cannot parse argument `#{value}` to Time or Date.")
|
138
135
|
end
|
136
|
+
end
|
139
137
|
|
140
|
-
|
141
|
-
|
138
|
+
_append_timestamp(time.year, time.month, time.day, time.hour, time.min, time.sec, time.nsec / 1000)
|
139
|
+
end
|
142
140
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
141
|
+
#
|
142
|
+
# appends interval.
|
143
|
+
# The argument must be ISO8601 duration format.
|
144
|
+
# WARNING: This method is expremental.
|
145
|
+
#
|
146
|
+
# require 'duckdb'
|
147
|
+
# db = DuckDB::Database.open
|
148
|
+
# con = db.connect
|
149
|
+
# con.query('CREATE TABLE intervals (interval_value INTERVAL)')
|
150
|
+
# appender = con.appender('intervals')
|
151
|
+
# appender
|
152
|
+
# .begin_row
|
153
|
+
# .append_interval('P1Y2D') # => append 1 year 2 days interval.
|
154
|
+
# .end_row
|
155
|
+
# .flush
|
156
|
+
#
|
157
|
+
def append_interval(value)
|
158
|
+
raise ArgumentError, "Argument `#{value}` must be a string." unless value.is_a?(String)
|
161
159
|
|
162
|
-
|
160
|
+
hash = iso8601_interval_to_hash(value)
|
163
161
|
|
164
|
-
|
162
|
+
months, days, micros = hash_to__append_interval_args(hash)
|
165
163
|
|
166
|
-
|
167
|
-
|
164
|
+
_append_interval(months, days, micros)
|
165
|
+
end
|
168
166
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
167
|
+
#
|
168
|
+
# appends value.
|
169
|
+
#
|
170
|
+
# require 'duckdb'
|
171
|
+
# db = DuckDB::Database.open
|
172
|
+
# con = db.connect
|
173
|
+
# con.query('CREATE TABLE users (id INTEGER, name VARCHAR)')
|
174
|
+
# appender = con.appender('users')
|
175
|
+
# appender.begin_row
|
176
|
+
# appender.append(1)
|
177
|
+
# appender.append('Alice')
|
178
|
+
# appender.end_row
|
179
|
+
#
|
180
|
+
def append(value)
|
181
|
+
case value
|
182
|
+
when NilClass
|
183
|
+
append_null
|
184
|
+
when Float
|
185
|
+
append_double(value)
|
186
|
+
when Integer
|
183
187
|
case value
|
184
|
-
when
|
185
|
-
|
186
|
-
when
|
187
|
-
|
188
|
-
when
|
189
|
-
|
190
|
-
when RANGE_INT16
|
191
|
-
append_int16(value)
|
192
|
-
when RANGE_INT32
|
193
|
-
append_int32(value)
|
194
|
-
when RANGE_INT64
|
195
|
-
append_int64(value)
|
196
|
-
else
|
197
|
-
append_hugeint(value)
|
198
|
-
end
|
199
|
-
when String
|
200
|
-
if defined?(DuckDB::Blob)
|
201
|
-
blob?(value) ? append_blob(value) : append_varchar(value)
|
202
|
-
else
|
203
|
-
append_varchar(value)
|
204
|
-
end
|
205
|
-
when TrueClass, FalseClass
|
206
|
-
append_bool(value)
|
207
|
-
when Time
|
208
|
-
if respond_to?(:append_timestamp)
|
209
|
-
append_timestamp(value)
|
210
|
-
else
|
211
|
-
append_varchar(value.strftime('%Y-%m-%d %H:%M:%S.%N'))
|
212
|
-
end
|
213
|
-
when Date
|
214
|
-
if respond_to?(:append_date)
|
215
|
-
append_date(value)
|
216
|
-
else
|
217
|
-
append_varchar(value.strftime('%Y-%m-%d'))
|
218
|
-
end
|
188
|
+
when RANGE_INT16
|
189
|
+
append_int16(value)
|
190
|
+
when RANGE_INT32
|
191
|
+
append_int32(value)
|
192
|
+
when RANGE_INT64
|
193
|
+
append_int64(value)
|
219
194
|
else
|
220
|
-
|
195
|
+
append_hugeint(value)
|
221
196
|
end
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
# appender.begin_row
|
232
|
-
# appender.append(1)
|
233
|
-
# appender.append('Alice')
|
234
|
-
# appender.end_row
|
235
|
-
#
|
236
|
-
def append_row(*args)
|
237
|
-
begin_row
|
238
|
-
args.each do |arg|
|
239
|
-
append(arg)
|
197
|
+
when String
|
198
|
+
blob?(value) ? append_blob(value) : append_varchar(value)
|
199
|
+
when TrueClass, FalseClass
|
200
|
+
append_bool(value)
|
201
|
+
when Time
|
202
|
+
if respond_to?(:append_timestamp)
|
203
|
+
append_timestamp(value)
|
204
|
+
else
|
205
|
+
append_varchar(value.strftime('%Y-%m-%d %H:%M:%S.%N'))
|
240
206
|
end
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
def blob?(value)
|
247
|
-
value.instance_of?(DuckDB::Blob) || value.encoding == Encoding::BINARY
|
248
|
-
end
|
249
|
-
|
250
|
-
def iso8601_interval_to_hash(value)
|
251
|
-
digit = ''
|
252
|
-
time = false
|
253
|
-
hash = {}
|
254
|
-
hash.default = 0
|
255
|
-
|
256
|
-
value.each_char do |c|
|
257
|
-
if '-0123456789.'.include?(c)
|
258
|
-
digit += c
|
259
|
-
elsif c == 'T'
|
260
|
-
time = true
|
261
|
-
digit = ''
|
262
|
-
elsif c == 'M'
|
263
|
-
m_interval_to_hash(hash, digit, time)
|
264
|
-
digit = ''
|
265
|
-
elsif c == 'S'
|
266
|
-
s_interval_to_hash(hash, digit)
|
267
|
-
digit = ''
|
268
|
-
elsif 'YDH'.include?(c)
|
269
|
-
hash[c] = digit.to_i
|
270
|
-
digit = ''
|
271
|
-
elsif c != 'P'
|
272
|
-
raise ArgumentError, "The argument `#{value}` can't be parse."
|
273
|
-
end
|
207
|
+
when Date
|
208
|
+
if respond_to?(:append_date)
|
209
|
+
append_date(value)
|
210
|
+
else
|
211
|
+
append_varchar(value.strftime('%Y-%m-%d'))
|
274
212
|
end
|
275
|
-
|
213
|
+
else
|
214
|
+
raise(DuckDB::Error, "not supported type #{value} (#{value.class})")
|
276
215
|
end
|
216
|
+
end
|
277
217
|
|
278
|
-
|
279
|
-
|
280
|
-
|
218
|
+
#
|
219
|
+
# append a row.
|
220
|
+
#
|
221
|
+
# appender.append_row(1, 'Alice')
|
222
|
+
#
|
223
|
+
# is same as:
|
224
|
+
#
|
225
|
+
# appender.begin_row
|
226
|
+
# appender.append(1)
|
227
|
+
# appender.append('Alice')
|
228
|
+
# appender.end_row
|
229
|
+
#
|
230
|
+
def append_row(*args)
|
231
|
+
begin_row
|
232
|
+
args.each do |arg|
|
233
|
+
append(arg)
|
281
234
|
end
|
235
|
+
end_row
|
236
|
+
end
|
282
237
|
|
283
|
-
|
284
|
-
sec, msec = digit.split('.')
|
285
|
-
hash['S'] = sec.to_i
|
286
|
-
hash['MS'] = "#{msec}000000"[0, 6].to_i
|
287
|
-
hash['MS'] *= -1 if hash['S'].negative?
|
288
|
-
end
|
238
|
+
private
|
289
239
|
|
290
|
-
|
291
|
-
|
292
|
-
days = hash['D']
|
293
|
-
micros = (hash['H'] * 3600 + hash['TM'] * 60 + hash['S']) * 1_000_000 + hash['MS']
|
294
|
-
[months, days, micros]
|
295
|
-
end
|
240
|
+
def blob?(value)
|
241
|
+
value.instance_of?(DuckDB::Blob) || value.encoding == Encoding::BINARY
|
296
242
|
end
|
297
243
|
end
|
298
244
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module DuckDB
|
2
|
+
class Column
|
3
|
+
#
|
4
|
+
# returns column type symbol
|
5
|
+
# `:unknown` means that the column type is unknown/unsupported by ruby-duckdb.
|
6
|
+
# `:invalid` means that the column type is invalid in duckdb.
|
7
|
+
#
|
8
|
+
# require 'duckdb'
|
9
|
+
# db = DuckDB::Database.open
|
10
|
+
# con = db.connect
|
11
|
+
# con.query('CREATE TABLE users (id INTEGER, name VARCHAR(30))')
|
12
|
+
#
|
13
|
+
# users = con.query('SELECT * FROM users')
|
14
|
+
# columns = users.columns
|
15
|
+
# columns.first.type #=> :integer
|
16
|
+
#
|
17
|
+
def type
|
18
|
+
types = %i[
|
19
|
+
invalid
|
20
|
+
boolean
|
21
|
+
tinyint
|
22
|
+
smallint
|
23
|
+
integer
|
24
|
+
bigint
|
25
|
+
utinyint
|
26
|
+
usmallint
|
27
|
+
uinteger
|
28
|
+
ubigint
|
29
|
+
float
|
30
|
+
double
|
31
|
+
timestamp
|
32
|
+
date
|
33
|
+
time
|
34
|
+
interval
|
35
|
+
hugeint
|
36
|
+
varchar
|
37
|
+
blob
|
38
|
+
decimal
|
39
|
+
timestamp_s
|
40
|
+
timestamp_ms
|
41
|
+
timestamp_ns
|
42
|
+
enum
|
43
|
+
list
|
44
|
+
struct
|
45
|
+
map
|
46
|
+
uuid
|
47
|
+
json
|
48
|
+
]
|
49
|
+
index = _type
|
50
|
+
return :unknown if index >= types.size
|
51
|
+
|
52
|
+
types[index]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/duckdb/config.rb
CHANGED
@@ -34,10 +34,7 @@ module DuckDB
|
|
34
34
|
# configs['default_order'] # => "The order type used when none is specified ([ASC] or DESC)"
|
35
35
|
#
|
36
36
|
def key_descriptions
|
37
|
-
|
38
|
-
|
39
|
-
n = size
|
40
|
-
@key_descriptions = (0...n).each_with_object({}) do |i, hash|
|
37
|
+
@key_descriptions ||= (0...size).each_with_object({}) do |i, hash|
|
41
38
|
key, description = key_description(i)
|
42
39
|
hash[key] = description
|
43
40
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module DuckDB
|
2
|
+
module Converter
|
3
|
+
private
|
4
|
+
|
5
|
+
def iso8601_interval_to_hash(value)
|
6
|
+
digit = ''
|
7
|
+
time = false
|
8
|
+
hash = {}
|
9
|
+
hash.default = 0
|
10
|
+
|
11
|
+
value.each_char do |c|
|
12
|
+
if '-0123456789.'.include?(c)
|
13
|
+
digit += c
|
14
|
+
elsif c == 'T'
|
15
|
+
time = true
|
16
|
+
digit = ''
|
17
|
+
elsif c == 'M'
|
18
|
+
m_interval_to_hash(hash, digit, time)
|
19
|
+
digit = ''
|
20
|
+
elsif c == 'S'
|
21
|
+
s_interval_to_hash(hash, digit)
|
22
|
+
digit = ''
|
23
|
+
elsif 'YDH'.include?(c)
|
24
|
+
hash[c] = digit.to_i
|
25
|
+
digit = ''
|
26
|
+
elsif c != 'P'
|
27
|
+
raise ArgumentError, "The argument `#{value}` can't be parse."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
hash
|
31
|
+
end
|
32
|
+
|
33
|
+
def m_interval_to_hash(hash, digit, time)
|
34
|
+
key = time ? 'TM' : 'M'
|
35
|
+
hash[key] = digit.to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
def s_interval_to_hash(hash, digit)
|
39
|
+
sec, msec = digit.split('.')
|
40
|
+
hash['S'] = sec.to_i
|
41
|
+
hash['MS'] = "#{msec}000000"[0, 6].to_i
|
42
|
+
hash['MS'] *= -1 if hash['S'].negative?
|
43
|
+
end
|
44
|
+
|
45
|
+
def hash_to__append_interval_args(hash)
|
46
|
+
months = hash['Y'] * 12 + hash['M']
|
47
|
+
days = hash['D']
|
48
|
+
micros = (hash['H'] * 3600 + hash['TM'] * 60 + hash['S']) * 1_000_000 + hash['MS']
|
49
|
+
[months, days, micros]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|