activerecord-duckdb 0.1.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 +7 -0
- data/.fasterer.yml +31 -0
- data/.rspec +3 -0
- data/.rubocop.yml +105 -0
- data/.simplecov +18 -0
- data/.tool-versions +1 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +131 -0
- data/LICENSE.txt +21 -0
- data/README.md +168 -0
- data/Rakefile +19 -0
- data/TODO.md +16 -0
- data/docs/TABLE_DEFINITION.md +322 -0
- data/lib/active_record/connection_adapters/duckdb/column.rb +67 -0
- data/lib/active_record/connection_adapters/duckdb/database_limits.rb +54 -0
- data/lib/active_record/connection_adapters/duckdb/database_statements.rb +303 -0
- data/lib/active_record/connection_adapters/duckdb/database_tasks.rb +22 -0
- data/lib/active_record/connection_adapters/duckdb/quoting.rb +59 -0
- data/lib/active_record/connection_adapters/duckdb/schema_creation.rb +43 -0
- data/lib/active_record/connection_adapters/duckdb/schema_definitions.rb +145 -0
- data/lib/active_record/connection_adapters/duckdb/schema_dumper.rb +165 -0
- data/lib/active_record/connection_adapters/duckdb/schema_statements.rb +599 -0
- data/lib/active_record/connection_adapters/duckdb_adapter.rb +436 -0
- data/lib/active_record/tasks/duckdb_database_tasks.rb +170 -0
- data/lib/activerecord/duckdb/version.rb +11 -0
- data/lib/activerecord-duckdb.rb +60 -0
- data/sig/activerecord/duckdb.rbs +6 -0
- data/tmp/.gitkeep +0 -0
- metadata +107 -0
@@ -0,0 +1,303 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This module provides database statements for the DuckDB adapter.
|
4
|
+
module ActiveRecord
|
5
|
+
module ConnectionAdapters
|
6
|
+
module Duckdb
|
7
|
+
module DatabaseStatements
|
8
|
+
# Determines if a SQL query is a write operation
|
9
|
+
# @param _sql [String] The SQL query to check (unused in DuckDB implementation)
|
10
|
+
# @return [Boolean] always returns false for DuckDB
|
11
|
+
def write_query?(_sql)
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
# Executes a SQL statement against the DuckDB database
|
16
|
+
# @param sql [String] The SQL statement to execute
|
17
|
+
# @param name [String, nil] Optional name for logging purposes
|
18
|
+
# @return [DuckDB::Result] The result of the query execution
|
19
|
+
def execute(sql, name = nil) # :nodoc:
|
20
|
+
# In case a query is being executed before the connection is open, reconnect.
|
21
|
+
reconnect unless raw_connection
|
22
|
+
|
23
|
+
log(sql, name) do
|
24
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
25
|
+
raw_connection.query(sql)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Performs a query with optional bind parameters
|
31
|
+
# @param raw_connection [DuckDB::Connection] The raw database connection
|
32
|
+
# @param sql [String] The SQL query to execute
|
33
|
+
# @param binds [Array] Array of bind parameters
|
34
|
+
# @param type_casted_binds [Array] Type-casted bind parameters
|
35
|
+
# @param prepare [Boolean] Whether to prepare the statement (unused)
|
36
|
+
# @param notification_payload [Hash, nil] Payload for notifications (unused)
|
37
|
+
# @param batch [Boolean] Whether this is a batch operation (unused)
|
38
|
+
# @param async [Boolean] Whether to execute asynchronously (unused)
|
39
|
+
# @param kwargs [Hash] Additional keyword arguments
|
40
|
+
# @return [DuckDB::Result] The query result
|
41
|
+
def perform_query(raw_connection,
|
42
|
+
sql,
|
43
|
+
binds,
|
44
|
+
type_casted_binds,
|
45
|
+
prepare: false,
|
46
|
+
notification_payload: nil,
|
47
|
+
batch: false,
|
48
|
+
async: false,
|
49
|
+
**kwargs)
|
50
|
+
result = if binds.any?
|
51
|
+
# Use the modern parameter binding approach
|
52
|
+
exec_query_with_binds(sql, binds)
|
53
|
+
else
|
54
|
+
# Fallback to direct execution with quoted values
|
55
|
+
# Your existing quote method will handle any unquoted values
|
56
|
+
raw_connection.query(sql)
|
57
|
+
end
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
# Casts a DuckDB result to ActiveRecord::Result format
|
62
|
+
# @param result [DuckDB::Result, nil] The DuckDB result to cast
|
63
|
+
# @return [ActiveRecord::Result] The ActiveRecord-compatible result
|
64
|
+
def cast_result(result)
|
65
|
+
return ActiveRecord::Result.empty(affected_rows: @affected_rows_before_warnings) if result.nil?
|
66
|
+
|
67
|
+
columns = result.columns.map do |col|
|
68
|
+
if col.respond_to?(:name)
|
69
|
+
col.name
|
70
|
+
elsif col.respond_to?(:column_name)
|
71
|
+
col.column_name
|
72
|
+
else
|
73
|
+
col.to_s
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
ActiveRecord::Result.new(columns, result.to_a)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Executes a query and returns an ActiveRecord::Result
|
81
|
+
# @param sql [String] The SQL query to execute
|
82
|
+
# @param name [String, nil] Optional name for logging
|
83
|
+
# @param binds [Array] Array of bind parameters
|
84
|
+
# @param prepare [Boolean] Whether to prepare the statement (unused)
|
85
|
+
# @return [ActiveRecord::Result] The query result
|
86
|
+
def exec_query(sql, name = nil, binds = [], prepare: false)
|
87
|
+
reconnect unless raw_connection
|
88
|
+
|
89
|
+
log(sql, name, binds) do
|
90
|
+
result = if binds.any?
|
91
|
+
# Use the modern parameter binding approach
|
92
|
+
exec_query_with_binds(sql, binds)
|
93
|
+
else
|
94
|
+
# Fallback to direct execution with quoted values
|
95
|
+
# Your existing quote method will handle any unquoted values
|
96
|
+
raw_connection.query(sql)
|
97
|
+
end
|
98
|
+
|
99
|
+
build_result(result)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Executes a DELETE statement and returns the number of affected rows
|
104
|
+
# @param sql [String] The DELETE SQL statement
|
105
|
+
# @param name [String, nil] Optional name for logging
|
106
|
+
# @param binds [Array] Array of bind parameters
|
107
|
+
# @return [Integer] Number of rows affected by the delete
|
108
|
+
def exec_delete(sql, name = nil, binds = []) # :nodoc:
|
109
|
+
reconnect unless raw_connection
|
110
|
+
|
111
|
+
if binds.any?
|
112
|
+
# For bound queries, handle them with proper logging
|
113
|
+
log(sql, name, binds) do
|
114
|
+
bind_values = binds.map(&:value_for_database)
|
115
|
+
raw_connection.query(sql, *bind_values)
|
116
|
+
end.rows_changed
|
117
|
+
else
|
118
|
+
# For non-bound queries, use execute directly
|
119
|
+
result = execute(sql, name)
|
120
|
+
result.rows_changed
|
121
|
+
end
|
122
|
+
end
|
123
|
+
alias exec_update exec_delete
|
124
|
+
|
125
|
+
# Executes an INSERT statement with optional RETURNING clause
|
126
|
+
# @param sql [String] The INSERT SQL statement
|
127
|
+
# @param name [String, nil] Optional name for logging
|
128
|
+
# @param binds [Array] Array of bind parameters
|
129
|
+
# @param pk [String, nil] Primary key column name
|
130
|
+
# @param sequence_name [String, nil] Sequence name (unused)
|
131
|
+
# @param returning [Array, nil] Columns to return after insert
|
132
|
+
# @return [ActiveRecord::Result] The insert result with returned values
|
133
|
+
def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil)
|
134
|
+
# Rails 8 will pass returning: [pk] when it wants the ID back
|
135
|
+
if returning&.any?
|
136
|
+
returning_columns = returning.map { |c| quote_column_name(c) }.join(', ')
|
137
|
+
sql = "#{sql} RETURNING #{returning_columns}" unless sql.include?('RETURNING')
|
138
|
+
elsif pk && !sql.include?('RETURNING')
|
139
|
+
# Add RETURNING for the primary key
|
140
|
+
sql = "#{sql} RETURNING #{quote_column_name(pk)}"
|
141
|
+
end
|
142
|
+
|
143
|
+
# Execute the query and return the result
|
144
|
+
# Rails 8 will handle extracting the ID from the result
|
145
|
+
exec_query(sql, name, binds)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns columns that should be included in INSERT statements
|
149
|
+
# @param table_name [String] The name of the table
|
150
|
+
# @return [Array<ActiveRecord::ConnectionAdapters::Column>] Columns to include in INSERT
|
151
|
+
def columns_for_insert(table_name)
|
152
|
+
columns(table_name).reject do |column|
|
153
|
+
# Exclude columns that have a default function (like nextval)
|
154
|
+
column.default_function.present?
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Determines if a column value should be returned after insert
|
159
|
+
# This is crucial - it tells Rails which columns should use RETURNING
|
160
|
+
# @param column [ActiveRecord::ConnectionAdapters::Column] The column to check
|
161
|
+
# @return [Boolean] true if column value should be returned after insert
|
162
|
+
def return_value_after_insert?(column)
|
163
|
+
# Return true for any column with a sequence default
|
164
|
+
column.default_function&.include?('nextval') || super
|
165
|
+
end
|
166
|
+
|
167
|
+
# Extracts the last inserted ID from an insert result
|
168
|
+
# @param result [ActiveRecord::Result] The result from an insert operation
|
169
|
+
# @return [Object] The last inserted ID value
|
170
|
+
def last_inserted_id(result)
|
171
|
+
# Handle ActiveRecord::Result from RETURNING clause
|
172
|
+
if result.is_a?(ActiveRecord::Result) && result.rows.any?
|
173
|
+
id_value = result.rows.first.first
|
174
|
+
return id_value
|
175
|
+
end
|
176
|
+
super
|
177
|
+
end
|
178
|
+
|
179
|
+
# Builds an ActiveRecord::Result from a DuckDB result
|
180
|
+
# @param result [DuckDB::Result, nil] The DuckDB result to convert
|
181
|
+
# @return [ActiveRecord::Result] The converted ActiveRecord result
|
182
|
+
def build_result(result)
|
183
|
+
# Handle DuckDB result format
|
184
|
+
return ActiveRecord::Result.empty if result.nil?
|
185
|
+
|
186
|
+
# DuckDB results have .columns and .to_a, not .rows
|
187
|
+
columns = result.columns.map do |col|
|
188
|
+
if col.respond_to?(:name)
|
189
|
+
col.name
|
190
|
+
elsif col.respond_to?(:column_name)
|
191
|
+
col.column_name
|
192
|
+
else
|
193
|
+
col.to_s
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
rows = result.to_a
|
198
|
+
ActiveRecord::Result.new(columns, rows)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Fetches type metadata for a SQL type string
|
202
|
+
# @param sql_type [String] The SQL type to get metadata for
|
203
|
+
# @return [ActiveRecord::ConnectionAdapters::SqlTypeMetadata] The type metadata
|
204
|
+
def fetch_type_metadata(sql_type)
|
205
|
+
# Parse DuckDB types and map to Rails types
|
206
|
+
type, limit, precision, scale = parse_type_info(sql_type)
|
207
|
+
|
208
|
+
ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
|
209
|
+
sql_type: sql_type,
|
210
|
+
type: type.to_sym,
|
211
|
+
limit: limit,
|
212
|
+
precision: precision,
|
213
|
+
scale: scale
|
214
|
+
)
|
215
|
+
end
|
216
|
+
|
217
|
+
private
|
218
|
+
|
219
|
+
# Executes a query with bind parameters using positional parameters
|
220
|
+
# @param sql [String] The SQL query with bind placeholders
|
221
|
+
# @param binds [Array] Array of bind parameters
|
222
|
+
# @return [DuckDB::Result] The query result
|
223
|
+
def exec_query_with_binds(sql, binds)
|
224
|
+
# For DuckDB, use positional parameters instead of named parameters
|
225
|
+
bind_values = binds.map do |bind|
|
226
|
+
if bind.respond_to?(:value_for_database)
|
227
|
+
bind.value_for_database
|
228
|
+
elsif bind.is_a?(Symbol)
|
229
|
+
bind.to_s
|
230
|
+
else
|
231
|
+
bind
|
232
|
+
end
|
233
|
+
end
|
234
|
+
raw_connection.query(sql, *bind_values)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Parses SQL type information to extract Rails type, limit, precision, and scale
|
238
|
+
# @param sql_type [String] The SQL type string to parse
|
239
|
+
# @return [Array] Array containing [type, limit, precision, scale]
|
240
|
+
def parse_type_info(sql_type)
|
241
|
+
case sql_type.to_s.upcase
|
242
|
+
when /^INTEGER(\((\d+),(\d+)\))?/i
|
243
|
+
precision, scale = ::Regexp.last_match(2)&.to_i, ::Regexp.last_match(3)&.to_i
|
244
|
+
[:integer, nil, precision, scale]
|
245
|
+
when /^VARCHAR(\((\d+)\))?/i, /^TEXT/i
|
246
|
+
limit = ::Regexp.last_match(2)&.to_i
|
247
|
+
[:string, limit, nil, nil]
|
248
|
+
when /^DOUBLE/i, /^REAL/i
|
249
|
+
[:float, nil, nil, nil]
|
250
|
+
when /^BOOLEAN/i
|
251
|
+
[:boolean, nil, nil, nil]
|
252
|
+
when /^DATE$/i
|
253
|
+
[:date, nil, nil, nil]
|
254
|
+
when /^TIMESTAMP/i, /^DATETIME/i
|
255
|
+
[:datetime, nil, nil, nil]
|
256
|
+
when /^DECIMAL(\((\d+),(\d+)\))?/i, /^NUMERIC(\((\d+),(\d+)\))?/i
|
257
|
+
precision, scale = ::Regexp.last_match(2)&.to_i, ::Regexp.last_match(3)&.to_i
|
258
|
+
[:decimal, nil, precision, scale]
|
259
|
+
else
|
260
|
+
[:string, nil, nil, nil] # Default fallback
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Extracts and converts default values from DuckDB column defaults
|
265
|
+
# @param default [String, nil] The default value from column definition
|
266
|
+
# @return [Object, nil] The converted default value
|
267
|
+
def extract_value_from_default(default)
|
268
|
+
return nil if default.nil?
|
269
|
+
|
270
|
+
# IMPORTANT: Return nil for sequence defaults so Rails doesn't set id=0
|
271
|
+
return nil if default.to_s.include?('nextval(')
|
272
|
+
|
273
|
+
# Handle DuckDB default value formats
|
274
|
+
case default.to_s
|
275
|
+
when /^'(.*)'$/
|
276
|
+
::Regexp.last_match(1) # Remove quotes from string defaults
|
277
|
+
when 'NULL'
|
278
|
+
nil
|
279
|
+
when /^\d+$/
|
280
|
+
default.to_i # Integer defaults
|
281
|
+
when /^\d+\.\d+$/
|
282
|
+
default.to_f # Float defaults
|
283
|
+
else
|
284
|
+
default
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Substitutes bind parameters in SQL with quoted values
|
289
|
+
# @param sql [String] The SQL string with bind placeholders
|
290
|
+
# @param binds [Array] Array of bind parameters
|
291
|
+
# @return [String] SQL with substituted values
|
292
|
+
def substitute_binds(sql, binds)
|
293
|
+
bind_index = 0
|
294
|
+
sql.gsub('?') do
|
295
|
+
value = quote(binds[bind_index].value)
|
296
|
+
bind_index += 1
|
297
|
+
value
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module Duckdb
|
6
|
+
# DuckDB-specific database tasks for database lifecycle management
|
7
|
+
# Provides stub implementations for database creation and destruction operations
|
8
|
+
module DatabaseTasks
|
9
|
+
# Creates a DuckDB database (stub implementation)
|
10
|
+
# @param database [String] The database name or path
|
11
|
+
# @param options [Hash] Additional options for database creation (unused)
|
12
|
+
# @return [void]
|
13
|
+
def create_database(database, options = {}); end
|
14
|
+
|
15
|
+
# Drops a DuckDB database (stub implementation)
|
16
|
+
# @param database [String] The database name or path
|
17
|
+
# @return [void]
|
18
|
+
def drop_database(database); end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module Duckdb
|
6
|
+
module Quoting
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Quotes a column name for use in SQL statements
|
11
|
+
# @param name [String, Symbol] The column name to quote
|
12
|
+
# @return [String] The quoted column name wrapped in double quotes
|
13
|
+
def quote_column_name(name)
|
14
|
+
%("#{name}")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Quotes a table name for use in SQL statements
|
19
|
+
# @param name [String, Symbol] The table name to quote
|
20
|
+
# @return [String] The quoted table name (delegates to quote_column_name)
|
21
|
+
|
22
|
+
def quote_table_name(name)
|
23
|
+
quote_column_name(name)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Quotes a column name for use in SQL statements
|
27
|
+
# @param name [String, Symbol] The column name to quote
|
28
|
+
# @return [String] The quoted column name wrapped in double quotes
|
29
|
+
def quote_column_name(name)
|
30
|
+
%("#{name}")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Quotes a value for safe inclusion in SQL statements
|
34
|
+
# @param value [Object] The value to quote
|
35
|
+
# @return [String] The appropriately quoted value for SQL
|
36
|
+
def quote(value)
|
37
|
+
case value
|
38
|
+
when String
|
39
|
+
"'#{value.gsub("'", "''")}'"
|
40
|
+
when nil
|
41
|
+
'NULL'
|
42
|
+
when true
|
43
|
+
'TRUE'
|
44
|
+
when false
|
45
|
+
'FALSE'
|
46
|
+
when Numeric
|
47
|
+
value.to_s
|
48
|
+
when Time, DateTime
|
49
|
+
"'#{value.utc.strftime("%Y-%m-%d %H:%M:%S")}'"
|
50
|
+
when Date
|
51
|
+
"'#{value.strftime("%Y-%m-%d")}'"
|
52
|
+
else
|
53
|
+
"'#{value.to_s.gsub("'", "''")}'"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module Duckdb
|
6
|
+
# DuckDB-specific schema creation functionality
|
7
|
+
# Extends Rails' SchemaCreation to handle DuckDB-specific SQL generation
|
8
|
+
class SchemaCreation < SchemaCreation
|
9
|
+
private
|
10
|
+
|
11
|
+
# Indicates whether DuckDB supports USING clause in index creation
|
12
|
+
# No USING clause is supported - DuckDB automatically determines the appropriate index type.
|
13
|
+
# https://duckdb.org/docs/stable/sql/statements/create_index.html
|
14
|
+
# https://duckdb.org/docs/stable/sql/indexes.html
|
15
|
+
# @return [Boolean] always returns false as DuckDB doesn't support USING clause
|
16
|
+
def supports_index_using?
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds column options to SQL, with special handling for DuckDB sequence defaults
|
21
|
+
# Override to handle nextval() defaults properly for DuckDB sequences
|
22
|
+
# @param sql [String] The SQL string being built
|
23
|
+
# @param options [Hash] Column options hash
|
24
|
+
# @return [void]
|
25
|
+
def add_column_options!(sql, options)
|
26
|
+
# Handle nextval() function calls - don't quote them
|
27
|
+
if options[:default]&.to_s&.include?('nextval(')
|
28
|
+
# Extract the sequence name and add it properly
|
29
|
+
default_value = options[:default].to_s
|
30
|
+
# Remove any extra quotes that might have been added
|
31
|
+
default_value = default_value.gsub(/^['"]|['"]$/, '')
|
32
|
+
sql << " DEFAULT #{default_value}"
|
33
|
+
# Create a copy of options without the default to prevent Rails from processing it again
|
34
|
+
options = options.except(:default)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Let Rails handle all other column options normally
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record/connection_adapters/abstract/schema_definitions'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
module Duckdb
|
8
|
+
# DuckDB-specific column method definitions for table creation
|
9
|
+
# Provides both standard Rails column types and DuckDB-specific data types
|
10
|
+
module ColumnMethods
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
extend ConnectionAdapters::ColumnMethods::ClassMethods
|
13
|
+
|
14
|
+
define_column_methods(
|
15
|
+
# binary
|
16
|
+
:blob,
|
17
|
+
# Integer variants
|
18
|
+
:tinyint, # TINYINT (1 byte: -128 to 127)
|
19
|
+
:smallint, # SMALLINT (2 bytes: -32,768 to 32,767)
|
20
|
+
:hugeint, # HUGEINT (16 bytes: very large integers)
|
21
|
+
:varint, # VARINT (variable precision integer, up to 1.2M digits)
|
22
|
+
# Unsigned integer variants
|
23
|
+
:utinyint, # UTINYINT (1 byte: 0 to 255)
|
24
|
+
:usmallint, # USMALLINT (2 bytes: 0 to 65,535)
|
25
|
+
:uinteger, # UINTEGER (4 bytes: 0 to 4,294,967,295)
|
26
|
+
:ubigint, # UBIGINT (8 bytes: 0 to 18,446,744,073,709,551,615)
|
27
|
+
:uhugeint, # UHUGEINT (16 bytes: 0 to 2^128-1)
|
28
|
+
# Special data types
|
29
|
+
:uuid, # UUID type for unique identifiers
|
30
|
+
:interval, # INTERVAL for time periods
|
31
|
+
:bit, # BIT for bit strings
|
32
|
+
# Complex/nested types
|
33
|
+
:list, # LIST (variable-length array)
|
34
|
+
:struct, # STRUCT (composite type with named fields)
|
35
|
+
:map, # MAP (key-value pairs)
|
36
|
+
:union, # UNION (value can be one of several types)
|
37
|
+
:enum # ENUM (predefined set of values)
|
38
|
+
# JSON can't be a column type, but can be a queried type of data
|
39
|
+
# :json # JSON documents (check DuckDB version compatibility)
|
40
|
+
)
|
41
|
+
|
42
|
+
alias binary blob
|
43
|
+
|
44
|
+
# Creates a LIST column with specified element type
|
45
|
+
# @param name [String, Symbol] The column name
|
46
|
+
# @param element_type [Symbol] The type of elements in the list (default: :string)
|
47
|
+
# @return [void]
|
48
|
+
# @example Create a list of strings
|
49
|
+
# t.list :tags, element_type: :string
|
50
|
+
def list(name, element_type: :string, **)
|
51
|
+
column(name, "#{element_type.to_s.upcase}[]", **)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creates a STRUCT column with named fields
|
55
|
+
# @param name [String, Symbol] The column name
|
56
|
+
# @param fields [Hash] Hash mapping field names to their types (default: {})
|
57
|
+
# @return [void]
|
58
|
+
# @example Create an address struct
|
59
|
+
# t.struct :address, fields: { street: :string, city: :string, zip: :integer }
|
60
|
+
def struct(name, fields: {}, **)
|
61
|
+
field_definitions = fields.map { |field_name, field_type| "#{field_name} #{field_type.to_s.upcase}" }
|
62
|
+
column(name, "STRUCT(#{field_definitions.join(", ")})", **)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Creates a MAP column with specified key and value types
|
66
|
+
# @param name [String, Symbol] The column name
|
67
|
+
# @param key_type [Symbol] The type of map keys (default: :string)
|
68
|
+
# @param value_type [Symbol] The type of map values (default: :string)
|
69
|
+
# @return [void]
|
70
|
+
# @example Create a string-to-string map
|
71
|
+
# t.map :metadata, key_type: :string, value_type: :string
|
72
|
+
def map(name, key_type: :string, value_type: :string, **)
|
73
|
+
column(name, "MAP(#{key_type.to_s.upcase}, #{value_type.to_s.upcase})", **)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Creates an ENUM column with predefined values
|
77
|
+
# @param name [String, Symbol] The column name
|
78
|
+
# @param values [Array] Array of allowed enum values (default: [])
|
79
|
+
# @return [void]
|
80
|
+
# @example Create a status enum
|
81
|
+
# t.enum :status, values: ['active', 'inactive', 'pending']
|
82
|
+
def enum(name, values: [], **)
|
83
|
+
enum_values = values.map { |v| "'#{v}'" }.join(', ')
|
84
|
+
column(name, "ENUM(#{enum_values})", **)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# DuckDB-specific table definition for CREATE TABLE statements
|
89
|
+
# Extends Rails' TableDefinition with DuckDB column types and features
|
90
|
+
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
|
91
|
+
include Duckdb::ColumnMethods
|
92
|
+
|
93
|
+
# Initialize a new DuckDB table definition
|
94
|
+
# @param conn [ActiveRecord::ConnectionAdapters::DuckdbAdapter] The database adapter
|
95
|
+
# @param name [String, Symbol] The table name
|
96
|
+
# @param temporary [Boolean] Whether this is a temporary table
|
97
|
+
# @param if_not_exists [Boolean] Whether to use IF NOT EXISTS clause
|
98
|
+
# @param options [Hash, nil] Additional table options
|
99
|
+
# @param as [String, nil] SELECT statement for CREATE TABLE AS
|
100
|
+
# @param comment [String, nil] Table comment
|
101
|
+
# @param table_options [Hash] Additional keyword table options
|
102
|
+
def initialize(conn, name, temporary: false, if_not_exists: false,
|
103
|
+
options: nil, as: nil, comment: nil, **table_options)
|
104
|
+
super
|
105
|
+
@conn = conn
|
106
|
+
@table_name = name
|
107
|
+
end
|
108
|
+
|
109
|
+
# Creates a column definition for the table
|
110
|
+
# Note: sequence defaults are handled by ALTER TABLE after table creation
|
111
|
+
# @param name [String, Symbol] The column name
|
112
|
+
# @param type [Symbol] The column type
|
113
|
+
# @param index [Boolean, Hash, nil] Whether to create an index on this column
|
114
|
+
# @param options [Hash] Additional column options
|
115
|
+
# @return [void]
|
116
|
+
def column(name, type, index: nil, **options)
|
117
|
+
# Don't set sequence defaults here - they're handled in create_table via ALTER TABLE
|
118
|
+
super
|
119
|
+
end
|
120
|
+
|
121
|
+
# Creates a primary key column definition
|
122
|
+
# Note: sequence defaults are handled by ALTER TABLE after table creation
|
123
|
+
# @param name [String, Symbol] The primary key column name
|
124
|
+
# @param type [Symbol] The primary key column type (default: :primary_key)
|
125
|
+
# @param options [Hash] Additional column options
|
126
|
+
# @return [void]
|
127
|
+
def primary_key(name, type = :primary_key, **options)
|
128
|
+
# Don't set sequence defaults here - they're handled in create_table via ALTER TABLE
|
129
|
+
super
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# DuckDB-specific table modification for ALTER TABLE statements
|
134
|
+
# Extends Rails' Table with DuckDB column types and features
|
135
|
+
class Table < ActiveRecord::ConnectionAdapters::Table
|
136
|
+
include Duckdb::ColumnMethods
|
137
|
+
end
|
138
|
+
|
139
|
+
# DuckDB-specific table alteration functionality
|
140
|
+
# Extends Rails' AlterTable for DuckDB-specific schema changes
|
141
|
+
class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|