activerecord-duckdb 0.1.0 → 0.1.1
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/.rubocop.yml +20 -1
- data/.tool-versions +1 -1
- data/Appraisals +19 -0
- data/CHANGELOG.md +12 -0
- data/README.md +133 -1
- data/docs/QUERY_EXECUTION_CALL_GRAPH.md +386 -0
- data/docs/RAILS_QUERY_EXECUTION.md +131 -0
- data/gemfiles/rails_7.2.gemfile +30 -0
- data/gemfiles/rails_7.2.gemfile.lock +196 -0
- data/gemfiles/rails_8.0.gemfile +30 -0
- data/gemfiles/rails_8.0.gemfile.lock +198 -0
- data/gemfiles/rails_8.1.gemfile +30 -0
- data/gemfiles/rails_8.1.gemfile.lock +197 -0
- data/lib/active_record/connection_adapters/duckdb/database_statements.rb +69 -201
- data/lib/active_record/connection_adapters/duckdb/database_statements_rails72.rb +53 -0
- data/lib/active_record/connection_adapters/duckdb/database_statements_rails8.rb +39 -0
- data/lib/active_record/connection_adapters/duckdb/schema_definitions.rb +7 -2
- data/lib/active_record/connection_adapters/duckdb/schema_dumper.rb +117 -3
- data/lib/active_record/connection_adapters/duckdb/schema_statements.rb +310 -91
- data/lib/active_record/connection_adapters/duckdb/schema_statements_rails80.rb +32 -0
- data/lib/active_record/connection_adapters/duckdb/schema_statements_rails81.rb +34 -0
- data/lib/active_record/connection_adapters/duckdb/timestamp_monkey_patch.rb +114 -0
- data/lib/active_record/connection_adapters/duckdb/type/interval.rb +174 -0
- data/lib/active_record/connection_adapters/duckdb_adapter.rb +404 -42
- data/lib/activerecord/duckdb/version.rb +1 -1
- metadata +19 -4
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
require 'duckdb'
|
|
4
4
|
require 'active_record'
|
|
5
5
|
require 'active_record/connection_adapters/abstract_adapter'
|
|
6
|
+
|
|
6
7
|
require 'active_record/connection_adapters/duckdb/column'
|
|
8
|
+
require 'active_record/connection_adapters/duckdb/type/interval'
|
|
7
9
|
require 'active_record/connection_adapters/duckdb/database_limits'
|
|
8
10
|
require 'active_record/connection_adapters/duckdb/database_statements'
|
|
9
11
|
require 'active_record/connection_adapters/duckdb/quoting'
|
|
@@ -17,17 +19,33 @@ require 'active_record/connection_adapters/duckdb/schema_dumper'
|
|
|
17
19
|
# sqlite3 adapter: https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
|
|
18
20
|
|
|
19
21
|
module ActiveRecord
|
|
22
|
+
module ConnectionAdapters
|
|
23
|
+
# Raised when attempting to use savepoints with DuckDB, which does not support them.
|
|
24
|
+
class SavepointsNotSupported < NotImplementedError
|
|
25
|
+
def initialize
|
|
26
|
+
super('DuckDB does not support savepoints. Avoid using transaction(requires_new: true) or nested transactions that rely on savepoint isolation.')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
20
31
|
module ConnectionHandling
|
|
21
32
|
# Establishes a connection to a DuckDB database
|
|
33
|
+
#
|
|
34
|
+
# @deprecated This method is provided for legacy compatibility only.
|
|
35
|
+
# In Rails 8+, adapters are registered via ActiveRecord::ConnectionAdapters.register
|
|
36
|
+
# and connections are created by calling AdapterClass.new(config) directly.
|
|
37
|
+
# Use ActiveRecord::Base.establish_connection instead for standard Rails usage.
|
|
38
|
+
#
|
|
22
39
|
# @param config [Hash] Database configuration options
|
|
23
40
|
# @return [ActiveRecord::ConnectionAdapters::DuckdbAdapter] The database adapter instance
|
|
24
41
|
# @raise [ActiveRecord::ConnectionNotEstablished] If connection fails
|
|
25
42
|
def duckdb_connection(config)
|
|
26
43
|
config = config.symbolize_keys
|
|
27
44
|
begin
|
|
28
|
-
# Create adapter
|
|
45
|
+
# Create adapter and establish connection via Rails lifecycle
|
|
46
|
+
# connect! calls verify! which calls reconnect! and configure_connection
|
|
29
47
|
adapter = ConnectionAdapters::DuckdbAdapter.new(nil, logger, {}, config)
|
|
30
|
-
adapter.
|
|
48
|
+
adapter.connect!
|
|
31
49
|
adapter
|
|
32
50
|
rescue StandardError => e
|
|
33
51
|
raise ActiveRecord::ConnectionNotEstablished,
|
|
@@ -45,7 +63,29 @@ module ActiveRecord
|
|
|
45
63
|
include Duckdb::DatabaseStatements
|
|
46
64
|
include Duckdb::Quoting
|
|
47
65
|
include Duckdb::SchemaStatements
|
|
48
|
-
|
|
66
|
+
|
|
67
|
+
# Include Rails version-specific database statements.
|
|
68
|
+
# Rails 8.0+: Use raw_execute, let base class handle internal_exec_query.
|
|
69
|
+
# Rails 7.2: Must implement internal_exec_query directly.
|
|
70
|
+
if ActiveRecord::VERSION::MAJOR >= 8
|
|
71
|
+
require 'active_record/connection_adapters/duckdb/database_statements_rails8'
|
|
72
|
+
include Duckdb::DatabaseStatementsRails8
|
|
73
|
+
else
|
|
74
|
+
require 'active_record/connection_adapters/duckdb/database_statements_rails72'
|
|
75
|
+
include Duckdb::DatabaseStatementsRails72
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Include Rails version-specific schema statements.
|
|
79
|
+
# Rails 8.1+: Column constructor includes cast_type parameter.
|
|
80
|
+
# Rails 7.2/8.0: Column constructor without cast_type parameter.
|
|
81
|
+
if ActiveRecord::VERSION::MAJOR > 8 ||
|
|
82
|
+
(ActiveRecord::VERSION::MAJOR == 8 && ActiveRecord::VERSION::MINOR >= 1)
|
|
83
|
+
require 'active_record/connection_adapters/duckdb/schema_statements_rails81'
|
|
84
|
+
include Duckdb::SchemaStatementsRails81
|
|
85
|
+
else
|
|
86
|
+
require 'active_record/connection_adapters/duckdb/schema_statements_rails80'
|
|
87
|
+
include Duckdb::SchemaStatementsRails80
|
|
88
|
+
end
|
|
49
89
|
|
|
50
90
|
# Allow customization of primary key type like PostgreSQL and MySQL adapters do
|
|
51
91
|
class_attribute :primary_key_type, default: :bigint
|
|
@@ -53,34 +93,113 @@ module ActiveRecord
|
|
|
53
93
|
# DB configuration if used in memory mode
|
|
54
94
|
MEMORY_MODE_KEYS = [:memory, 'memory', ':memory:', ':memory'].freeze
|
|
55
95
|
|
|
96
|
+
class << self
|
|
97
|
+
# Creates a new DuckDB database connection
|
|
98
|
+
# @param config [Hash] Database configuration
|
|
99
|
+
# @return [DuckDB::Connection] The raw database connection
|
|
100
|
+
def new_client(config)
|
|
101
|
+
database = config[:database] || :memory
|
|
102
|
+
db = if MEMORY_MODE_KEYS.include?(database)
|
|
103
|
+
DuckDB::Database.open
|
|
104
|
+
else
|
|
105
|
+
DuckDB::Database.open(database.to_s)
|
|
106
|
+
end
|
|
107
|
+
db.connect
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# Initializes the type map with DuckDB-specific type mappings
|
|
113
|
+
# @param m [ActiveRecord::Type::TypeMap] The type map to initialize
|
|
114
|
+
def initialize_type_map(m)
|
|
115
|
+
m.register_type(/^boolean$/i, Type::Boolean.new)
|
|
116
|
+
m.register_type(/^date$/i, Type::Date.new)
|
|
117
|
+
m.register_type(/^time$/i, Type::Time.new)
|
|
118
|
+
m.register_type(/^timestamp$/i, Type::DateTime.new)
|
|
119
|
+
m.register_type(/^datetime$/i, Type::DateTime.new)
|
|
120
|
+
m.register_type(/^float$/i, Type::Float.new)
|
|
121
|
+
m.register_type(/^real$/i, Type::Float.new)
|
|
122
|
+
m.register_type(/^double$/i, Type::Float.new)
|
|
123
|
+
|
|
124
|
+
# Integer types with proper byte limits
|
|
125
|
+
m.register_type(/^tinyint$/i) { Type::Integer.new(limit: 1) }
|
|
126
|
+
m.register_type(/^smallint$/i) { Type::Integer.new(limit: 2) }
|
|
127
|
+
m.register_type(/^integer$/i) { Type::Integer.new(limit: 4) }
|
|
128
|
+
# BigInteger handles unlimited bytes
|
|
129
|
+
m.register_type(/^bigint$/i) { Type::BigInteger.new }
|
|
130
|
+
m.register_type(/^hugeint$/i) { Type::BigInteger.new }
|
|
131
|
+
|
|
132
|
+
# Unsigned integer types
|
|
133
|
+
m.register_type(/^utinyint$/i) { Type::UnsignedInteger.new(limit: 1) }
|
|
134
|
+
m.register_type(/^usmallint$/i) { Type::UnsignedInteger.new(limit: 2) }
|
|
135
|
+
m.register_type(/^uinteger$/i) { Type::UnsignedInteger.new(limit: 4) }
|
|
136
|
+
m.register_type(/^ubigint$/i) { Type::UnsignedInteger.new(limit: 8) }
|
|
137
|
+
m.register_type(/^uhugeint$/i) { Type::BigInteger.new }
|
|
138
|
+
|
|
139
|
+
# String types
|
|
140
|
+
m.register_type(/^varchar/i, Type::String.new)
|
|
141
|
+
m.register_type(/^text$/i, Type::Text.new)
|
|
142
|
+
m.register_type(/^uuid$/i, Type::String.new)
|
|
143
|
+
|
|
144
|
+
# Binary
|
|
145
|
+
m.register_type(/^blob$/i, Type::Binary.new)
|
|
146
|
+
m.register_type(/^bytea$/i, Type::Binary.new)
|
|
147
|
+
|
|
148
|
+
# Decimal with precision/scale
|
|
149
|
+
m.register_type(/^decimal/i) { Type::Decimal.new }
|
|
150
|
+
m.register_type(/^numeric/i) { Type::Decimal.new }
|
|
151
|
+
|
|
152
|
+
# Interval type - maps to ActiveSupport::Duration
|
|
153
|
+
m.register_type(/^interval$/i) { Duckdb::Type::Interval.new }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Type map for DuckDB SQL types to ActiveRecord types
|
|
158
|
+
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
|
|
159
|
+
|
|
56
160
|
# https://duckdb.org/docs/stable/sql/data_types/overview.html
|
|
161
|
+
# Integer limits (in bytes): tinyint=1, smallint=2, integer=4, bigint=8
|
|
57
162
|
NATIVE_DATABASE_TYPES = {
|
|
58
163
|
primary_key: 'INTEGER PRIMARY KEY',
|
|
59
164
|
string: { name: 'VARCHAR' },
|
|
60
|
-
integer: { name: 'INTEGER' },
|
|
165
|
+
integer: { name: 'INTEGER', limit: 4 },
|
|
61
166
|
float: { name: 'REAL' },
|
|
62
167
|
decimal: { name: 'DECIMAL' },
|
|
63
168
|
datetime: { name: 'TIMESTAMP' },
|
|
64
169
|
time: { name: 'TIME' },
|
|
65
170
|
date: { name: 'DATE' },
|
|
66
|
-
bigint: { name: 'BIGINT' },
|
|
171
|
+
bigint: { name: 'BIGINT', limit: 8 },
|
|
67
172
|
binary: { name: 'BLOB' },
|
|
68
173
|
boolean: { name: 'BOOLEAN' },
|
|
69
|
-
uuid: { name: 'UUID' }
|
|
174
|
+
uuid: { name: 'UUID' },
|
|
175
|
+
# DuckDB-specific signed integer types
|
|
176
|
+
tinyint: { name: 'TINYINT', limit: 1 },
|
|
177
|
+
smallint: { name: 'SMALLINT', limit: 2 },
|
|
178
|
+
hugeint: { name: 'HUGEINT' },
|
|
179
|
+
# DuckDB-specific unsigned integer types
|
|
180
|
+
utinyint: { name: 'UTINYINT', limit: 1 },
|
|
181
|
+
usmallint: { name: 'USMALLINT', limit: 2 },
|
|
182
|
+
uinteger: { name: 'UINTEGER', limit: 4 },
|
|
183
|
+
ubigint: { name: 'UBIGINT', limit: 8 },
|
|
184
|
+
uhugeint: { name: 'UHUGEINT' },
|
|
185
|
+
# Other DuckDB types
|
|
186
|
+
interval: { name: 'INTERVAL' }
|
|
70
187
|
}.freeze
|
|
71
188
|
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
189
|
+
# Settings that MUST be applied before loading extensions
|
|
190
|
+
EARLY_SETTINGS = %i[allow_persistent_secrets allow_community_extensions].freeze
|
|
191
|
+
|
|
192
|
+
# Default DuckDB settings for secure and predictable behavior
|
|
193
|
+
# Note: lock_configuration is handled separately at the end of configure_connection
|
|
194
|
+
DEFAULT_SETTINGS = {
|
|
195
|
+
allow_persistent_secrets: false,
|
|
196
|
+
allow_community_extensions: false,
|
|
197
|
+
autoinstall_known_extensions: false,
|
|
198
|
+
autoload_known_extensions: false,
|
|
199
|
+
threads: 1,
|
|
200
|
+
memory_limit: '1GiB',
|
|
201
|
+
max_temp_directory_size: '4GiB'
|
|
202
|
+
}.freeze
|
|
84
203
|
|
|
85
204
|
# Disconnects from the DuckDB database and cleans up the connection
|
|
86
205
|
# @return [void]
|
|
@@ -101,6 +220,15 @@ module ActiveRecord
|
|
|
101
220
|
@raw_connection || @connection
|
|
102
221
|
end
|
|
103
222
|
|
|
223
|
+
# Looks up the cast type for a given SQL type string
|
|
224
|
+
# Uses the DuckDB TYPE_MAP to return appropriate ActiveRecord types
|
|
225
|
+
# This ensures BIGINT columns use BigInteger type for full 8-byte range support
|
|
226
|
+
# @param sql_type [String] The SQL type string (e.g., 'BIGINT', 'INTEGER')
|
|
227
|
+
# @return [ActiveRecord::Type::Value] The corresponding ActiveRecord type
|
|
228
|
+
def lookup_cast_type(sql_type)
|
|
229
|
+
TYPE_MAP.lookup(sql_type)
|
|
230
|
+
end
|
|
231
|
+
|
|
104
232
|
class << self
|
|
105
233
|
# Opens the DuckDB command line console
|
|
106
234
|
# @param config [ActiveRecord::DatabaseConfigurations::DatabaseConfig] Database configuration
|
|
@@ -127,15 +255,50 @@ module ActiveRecord
|
|
|
127
255
|
end
|
|
128
256
|
|
|
129
257
|
# Indicates whether the adapter supports INSERT...RETURNING syntax
|
|
130
|
-
#
|
|
258
|
+
# DuckLake does NOT support INSERT...RETURNING, so we check for DuckLake mode
|
|
259
|
+
# @return [Boolean] true for regular DuckDB, false for DuckLake
|
|
131
260
|
def use_insert_returning?
|
|
132
|
-
|
|
261
|
+
!ducklake?
|
|
133
262
|
end
|
|
134
263
|
|
|
135
264
|
# Indicates whether the adapter supports INSERT RETURNING for Rails 8
|
|
136
|
-
#
|
|
265
|
+
# DuckLake does NOT support INSERT...RETURNING, so we check for DuckLake mode
|
|
266
|
+
# @return [Boolean] true for regular DuckDB, false for DuckLake
|
|
137
267
|
def supports_insert_returning?
|
|
138
|
-
|
|
268
|
+
!ducklake?
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Detects if the current database is a DuckLake database
|
|
272
|
+
# Uses a metadata query to check the database type
|
|
273
|
+
# @return [Boolean] true if current database is DuckLake, false otherwise
|
|
274
|
+
def ducklake?
|
|
275
|
+
return @ducklake if defined?(@ducklake)
|
|
276
|
+
|
|
277
|
+
@ducklake = begin
|
|
278
|
+
with_raw_connection do |conn|
|
|
279
|
+
result = conn.query('SELECT type FROM duckdb_databases() WHERE database_name = current_database()')
|
|
280
|
+
db_type = result.first&.first
|
|
281
|
+
db_type.to_s.downcase == 'ducklake'
|
|
282
|
+
end
|
|
283
|
+
rescue DuckDB::Error
|
|
284
|
+
false
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Checks if the DuckLake extension is available and can be loaded
|
|
289
|
+
# @return [Boolean] true if DuckLake extension is available, false otherwise
|
|
290
|
+
def ducklake_extension_available?
|
|
291
|
+
return @ducklake_extension_available if defined?(@ducklake_extension_available)
|
|
292
|
+
|
|
293
|
+
@ducklake_extension_available = begin
|
|
294
|
+
with_raw_connection do |conn|
|
|
295
|
+
conn.execute('INSTALL ducklake')
|
|
296
|
+
conn.execute('LOAD ducklake')
|
|
297
|
+
end
|
|
298
|
+
true
|
|
299
|
+
rescue DuckDB::Error
|
|
300
|
+
false
|
|
301
|
+
end
|
|
139
302
|
end
|
|
140
303
|
|
|
141
304
|
# Indicates whether the adapter supports INSERT ON DUPLICATE SKIP syntax
|
|
@@ -157,6 +320,25 @@ module ActiveRecord
|
|
|
157
320
|
false
|
|
158
321
|
end
|
|
159
322
|
|
|
323
|
+
# DuckDB does not support savepoints at the SQL level.
|
|
324
|
+
# @return [Boolean] always returns false
|
|
325
|
+
def supports_savepoints? = false
|
|
326
|
+
|
|
327
|
+
# DuckDB does not support savepoints.
|
|
328
|
+
# @param _name [String] The savepoint name (ignored)
|
|
329
|
+
# @raise [SavepointsNotSupported] always raises since DuckDB doesn't support savepoints
|
|
330
|
+
def create_savepoint(_name = nil) = raise SavepointsNotSupported
|
|
331
|
+
|
|
332
|
+
# DuckDB does not support savepoints.
|
|
333
|
+
# @param _name [String] The savepoint name (ignored)
|
|
334
|
+
# @raise [SavepointsNotSupported] always raises since DuckDB doesn't support savepoints
|
|
335
|
+
def exec_rollback_to_savepoint(_name = nil) = raise SavepointsNotSupported
|
|
336
|
+
|
|
337
|
+
# DuckDB does not support savepoints.
|
|
338
|
+
# @param _name [String] The savepoint name (ignored)
|
|
339
|
+
# @raise [SavepointsNotSupported] always raises since DuckDB doesn't support savepoints
|
|
340
|
+
def release_savepoint(_name = nil) = raise SavepointsNotSupported
|
|
341
|
+
|
|
160
342
|
# Determines if primary key should be prefetched before insert
|
|
161
343
|
# @param _table_name [String] The table name (unused)
|
|
162
344
|
# @return [Boolean] always returns false to exclude auto-increment columns from INSERT
|
|
@@ -192,9 +374,47 @@ module ActiveRecord
|
|
|
192
374
|
end
|
|
193
375
|
|
|
194
376
|
# Returns the native database types supported by DuckDB
|
|
377
|
+
# DuckLake doesn't support PRIMARY KEY constraints, so we return a modified version
|
|
195
378
|
# @return [Hash] Hash mapping ActiveRecord types to DuckDB native types
|
|
196
379
|
def native_database_types
|
|
197
|
-
NATIVE_DATABASE_TYPES
|
|
380
|
+
return NATIVE_DATABASE_TYPES unless ducklake?
|
|
381
|
+
|
|
382
|
+
# DuckLake doesn't support PRIMARY KEY/UNIQUE constraints
|
|
383
|
+
@native_database_types ||= NATIVE_DATABASE_TYPES.merge(
|
|
384
|
+
primary_key: 'INTEGER'
|
|
385
|
+
)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Configures the DuckDB connection with extensions, settings, secrets, and attachments.
|
|
389
|
+
# DuckDB locks configuration permanently after initial setup, so we skip reconfiguration
|
|
390
|
+
# if the connection is already configured. This is necessary because Rails/test-prof
|
|
391
|
+
# may call reset! which triggers configure_connection again.
|
|
392
|
+
# @return [void]
|
|
393
|
+
def configure_connection
|
|
394
|
+
# Skip reconfiguration if already configured - DuckDB locks configuration permanently
|
|
395
|
+
return if configuration_locked?
|
|
396
|
+
|
|
397
|
+
super
|
|
398
|
+
DuckDB.default_timezone = ActiveRecord.default_timezone
|
|
399
|
+
apply_early_settings
|
|
400
|
+
install_extensions
|
|
401
|
+
apply_settings
|
|
402
|
+
create_secrets
|
|
403
|
+
attach_databases
|
|
404
|
+
use_database
|
|
405
|
+
lock_configuration
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Checks if DuckDB configuration is locked.
|
|
409
|
+
# Once lock_configuration is set to true, no configuration changes can be made.
|
|
410
|
+
# @return [Boolean] true if configuration is locked
|
|
411
|
+
def configuration_locked?
|
|
412
|
+
return false unless raw_connection
|
|
413
|
+
|
|
414
|
+
result = raw_connection.query("SELECT current_setting('lock_configuration')")
|
|
415
|
+
result.first&.first == true
|
|
416
|
+
rescue DuckDB::Error
|
|
417
|
+
false
|
|
198
418
|
end
|
|
199
419
|
|
|
200
420
|
# Returns column definitions for a table using PRAGMA table_info
|
|
@@ -237,18 +457,19 @@ module ActiveRecord
|
|
|
237
457
|
end
|
|
238
458
|
|
|
239
459
|
# Convert PRAGMA results to match information_schema format
|
|
460
|
+
# Note: DuckLake returns booleans (true/false) while regular DuckDB may return integers (1/0)
|
|
240
461
|
[
|
|
241
|
-
column_name,
|
|
242
|
-
formatted_type,
|
|
243
|
-
column_default,
|
|
244
|
-
not_null
|
|
245
|
-
nil,
|
|
246
|
-
nil,
|
|
247
|
-
nil,
|
|
248
|
-
nil,
|
|
249
|
-
nil,
|
|
250
|
-
nil,
|
|
251
|
-
pk
|
|
462
|
+
column_name, # column_name
|
|
463
|
+
formatted_type, # formatted_type
|
|
464
|
+
column_default, # column_default
|
|
465
|
+
not_null.in?([1, true]), # not_null (true if NOT NULL constraint)
|
|
466
|
+
nil, # type_id
|
|
467
|
+
nil, # type_modifier
|
|
468
|
+
nil, # collation_name
|
|
469
|
+
nil, # comment
|
|
470
|
+
nil, # identity
|
|
471
|
+
nil, # generated
|
|
472
|
+
pk.in?([1, true]) # primary_key flag (true if primary key)
|
|
252
473
|
]
|
|
253
474
|
end
|
|
254
475
|
end
|
|
@@ -270,7 +491,7 @@ module ActiveRecord
|
|
|
270
491
|
# @param sequence_name [String] The name of the sequence
|
|
271
492
|
# @return [String] SQL expression for next sequence value
|
|
272
493
|
def next_sequence_value(sequence_name)
|
|
273
|
-
"nextval(
|
|
494
|
+
"nextval(#{quote(sequence_name)})"
|
|
274
495
|
end
|
|
275
496
|
|
|
276
497
|
# Generates default sequence name following PostgreSQL/Oracle conventions
|
|
@@ -378,6 +599,14 @@ module ActiveRecord
|
|
|
378
599
|
end
|
|
379
600
|
end
|
|
380
601
|
|
|
602
|
+
# Creates a schema dumper instance for DuckDB
|
|
603
|
+
# Uses the DuckDB-specific SchemaDumper class to handle DuckDB types and DuckLake features
|
|
604
|
+
# @param options [Hash] Schema dumper options
|
|
605
|
+
# @return [Duckdb::SchemaDumper] The schema dumper instance
|
|
606
|
+
def create_schema_dumper(options) # :nodoc:
|
|
607
|
+
Duckdb::SchemaDumper.create(self, options)
|
|
608
|
+
end
|
|
609
|
+
|
|
381
610
|
# Returns table options for schema dumping
|
|
382
611
|
# @param table_name [String] The name of the table
|
|
383
612
|
# @return [Hash] Hash of table options for schema dumping
|
|
@@ -420,16 +649,149 @@ module ActiveRecord
|
|
|
420
649
|
|
|
421
650
|
private
|
|
422
651
|
|
|
652
|
+
# Reconnects to the database by closing existing connection and establishing new one.
|
|
653
|
+
# Called by Rails' reconnect! which will also call configure_connection.
|
|
654
|
+
# We reset the @duckdb_configured flag so the new connection gets configured.
|
|
655
|
+
# @return [void]
|
|
656
|
+
def reconnect
|
|
657
|
+
@raw_connection&.close
|
|
658
|
+
@duckdb_configured = false # Reset so configure_connection will run on new connection
|
|
659
|
+
remove_instance_variable(:@ducklake) if defined?(@ducklake) # Reset ducklake detection
|
|
660
|
+
connect
|
|
661
|
+
end
|
|
662
|
+
|
|
423
663
|
# Establishes the actual connection to the DuckDB database
|
|
424
664
|
# @return [void]
|
|
425
665
|
def connect
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
@
|
|
666
|
+
@raw_connection = self.class.new_client(@config)
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# Returns merged settings (defaults + user config)
|
|
670
|
+
# @return [Hash] Merged settings hash
|
|
671
|
+
def merged_settings
|
|
672
|
+
@merged_settings ||= DEFAULT_SETTINGS.merge((@config[:settings] || {}).transform_keys(&:to_sym))
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# Applies settings that must be set before loading extensions
|
|
676
|
+
# @return [void]
|
|
677
|
+
def apply_early_settings
|
|
678
|
+
merged_settings.each do |key, value|
|
|
679
|
+
next unless EARLY_SETTINGS.include?(key)
|
|
680
|
+
|
|
681
|
+
execute_setting(key, value)
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# Installs and loads configured extensions
|
|
686
|
+
# @return [void]
|
|
687
|
+
def install_extensions
|
|
688
|
+
extensions = @config[:extensions] || []
|
|
689
|
+
extensions.each do |extension|
|
|
690
|
+
raw_connection.execute("INSTALL #{extension}")
|
|
691
|
+
raw_connection.execute("LOAD #{extension}")
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# Applies settings that can be set after loading extensions
|
|
696
|
+
# @return [void]
|
|
697
|
+
def apply_settings
|
|
698
|
+
merged_settings.each do |key, value|
|
|
699
|
+
next if EARLY_SETTINGS.include?(key)
|
|
700
|
+
|
|
701
|
+
execute_setting(key, value)
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# Executes a single SET statement
|
|
706
|
+
# @param key [Symbol, String] Setting name
|
|
707
|
+
# @param value [Object] Setting value
|
|
708
|
+
# @return [void]
|
|
709
|
+
def execute_setting(key, value)
|
|
710
|
+
formatted_value = case value
|
|
711
|
+
when String then "'#{value}'"
|
|
712
|
+
when TrueClass, FalseClass then value.to_s
|
|
713
|
+
else value.to_s
|
|
714
|
+
end
|
|
715
|
+
raw_connection.execute("SET #{key} = #{formatted_value}")
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
# Creates secrets from configuration
|
|
719
|
+
# If a secret has an explicit 'type' key, the hash key becomes the secret name
|
|
720
|
+
# Otherwise, the hash key is the secret type (unnamed secret)
|
|
721
|
+
# @return [void]
|
|
722
|
+
def create_secrets
|
|
723
|
+
secrets = @config[:secrets] || {}
|
|
724
|
+
secrets.each do |key, fields|
|
|
725
|
+
fields = fields.transform_keys(&:to_sym)
|
|
726
|
+
type = fields.delete(:type)
|
|
727
|
+
name = type ? key : nil # Named secret: key is the name, type is explicit
|
|
728
|
+
type ||= key # Unnamed secret: key is the type
|
|
729
|
+
formatted_fields = format_secret_fields(fields)
|
|
730
|
+
sql = +'CREATE SECRET'
|
|
731
|
+
sql << " #{name}" if name
|
|
732
|
+
sql << " (TYPE #{type}"
|
|
733
|
+
sql << ", #{formatted_fields}" unless formatted_fields.empty?
|
|
734
|
+
sql << ')'
|
|
735
|
+
raw_connection.execute(sql)
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# Formats secret fields for CREATE SECRET statement
|
|
740
|
+
# @param fields [Hash] Secret fields
|
|
741
|
+
# @return [String] Formatted fields string
|
|
742
|
+
def format_secret_fields(fields)
|
|
743
|
+
fields.filter_map do |key, value|
|
|
744
|
+
next if value.nil?
|
|
745
|
+
|
|
746
|
+
formatted_value = case value
|
|
747
|
+
when Integer then value.to_s
|
|
748
|
+
else "'#{value}'"
|
|
749
|
+
end
|
|
750
|
+
"#{key.to_s.upcase} #{formatted_value}"
|
|
751
|
+
end.join(', ')
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# Attaches configured databases
|
|
755
|
+
# @return [void]
|
|
756
|
+
def attach_databases
|
|
757
|
+
attachments = @config[:attachments] || []
|
|
758
|
+
attachments.each do |attachment|
|
|
759
|
+
attachment = attachment.transform_keys(&:to_sym)
|
|
760
|
+
name = attachment[:name]
|
|
761
|
+
connection_string = attachment[:connection_string]
|
|
762
|
+
type = attachment[:type]
|
|
763
|
+
options = attachment[:options]
|
|
764
|
+
|
|
765
|
+
sql = "ATTACH '#{connection_string}' AS #{name}"
|
|
766
|
+
params = []
|
|
767
|
+
params << "TYPE #{type}" if type
|
|
768
|
+
params << options if options
|
|
769
|
+
sql += " (#{params.join(", ")})" unless params.empty?
|
|
770
|
+
|
|
771
|
+
raw_connection.execute(sql)
|
|
772
|
+
end
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
# Sets the active database using USE statement
|
|
776
|
+
# This is only needed when you want to switch to a different attached database
|
|
777
|
+
# The main database opened via DuckDB::Database.open is already active
|
|
778
|
+
# @return [void]
|
|
779
|
+
def use_database
|
|
780
|
+
# The use_database config option allows switching to a specific attached database
|
|
781
|
+
# This is NOT the same as the database config option which specifies the file to open
|
|
782
|
+
use_db = @config[:use_database]
|
|
783
|
+
return if use_db.nil?
|
|
784
|
+
|
|
785
|
+
raw_connection.execute("USE #{use_db}")
|
|
786
|
+
# Reset ducklake? memoization since current_database has changed
|
|
787
|
+
remove_instance_variable(:@ducklake) if defined?(@ducklake)
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
# Locks the DuckDB configuration to prevent further changes
|
|
791
|
+
# This should be called at the very end of configure_connection
|
|
792
|
+
# @return [void]
|
|
793
|
+
def lock_configuration
|
|
794
|
+
raw_connection.execute('SET lock_configuration = true')
|
|
433
795
|
end
|
|
434
796
|
end
|
|
435
797
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-duckdb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brandon Hicks
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|
|
@@ -57,22 +57,37 @@ files:
|
|
|
57
57
|
- ".simplecov"
|
|
58
58
|
- ".tool-versions"
|
|
59
59
|
- ".yardopts"
|
|
60
|
+
- Appraisals
|
|
60
61
|
- CHANGELOG.md
|
|
61
62
|
- CODE_OF_CONDUCT.md
|
|
62
63
|
- LICENSE.txt
|
|
63
64
|
- README.md
|
|
64
65
|
- Rakefile
|
|
65
66
|
- TODO.md
|
|
67
|
+
- docs/QUERY_EXECUTION_CALL_GRAPH.md
|
|
68
|
+
- docs/RAILS_QUERY_EXECUTION.md
|
|
66
69
|
- docs/TABLE_DEFINITION.md
|
|
70
|
+
- gemfiles/rails_7.2.gemfile
|
|
71
|
+
- gemfiles/rails_7.2.gemfile.lock
|
|
72
|
+
- gemfiles/rails_8.0.gemfile
|
|
73
|
+
- gemfiles/rails_8.0.gemfile.lock
|
|
74
|
+
- gemfiles/rails_8.1.gemfile
|
|
75
|
+
- gemfiles/rails_8.1.gemfile.lock
|
|
67
76
|
- lib/active_record/connection_adapters/duckdb/column.rb
|
|
68
77
|
- lib/active_record/connection_adapters/duckdb/database_limits.rb
|
|
69
78
|
- lib/active_record/connection_adapters/duckdb/database_statements.rb
|
|
79
|
+
- lib/active_record/connection_adapters/duckdb/database_statements_rails72.rb
|
|
80
|
+
- lib/active_record/connection_adapters/duckdb/database_statements_rails8.rb
|
|
70
81
|
- lib/active_record/connection_adapters/duckdb/database_tasks.rb
|
|
71
82
|
- lib/active_record/connection_adapters/duckdb/quoting.rb
|
|
72
83
|
- lib/active_record/connection_adapters/duckdb/schema_creation.rb
|
|
73
84
|
- lib/active_record/connection_adapters/duckdb/schema_definitions.rb
|
|
74
85
|
- lib/active_record/connection_adapters/duckdb/schema_dumper.rb
|
|
75
86
|
- lib/active_record/connection_adapters/duckdb/schema_statements.rb
|
|
87
|
+
- lib/active_record/connection_adapters/duckdb/schema_statements_rails80.rb
|
|
88
|
+
- lib/active_record/connection_adapters/duckdb/schema_statements_rails81.rb
|
|
89
|
+
- lib/active_record/connection_adapters/duckdb/timestamp_monkey_patch.rb
|
|
90
|
+
- lib/active_record/connection_adapters/duckdb/type/interval.rb
|
|
76
91
|
- lib/active_record/connection_adapters/duckdb_adapter.rb
|
|
77
92
|
- lib/active_record/tasks/duckdb_database_tasks.rb
|
|
78
93
|
- lib/activerecord-duckdb.rb
|
|
@@ -94,14 +109,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
94
109
|
requirements:
|
|
95
110
|
- - ">="
|
|
96
111
|
- !ruby/object:Gem::Version
|
|
97
|
-
version: 3.
|
|
112
|
+
version: 3.3.0
|
|
98
113
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
99
114
|
requirements:
|
|
100
115
|
- - ">="
|
|
101
116
|
- !ruby/object:Gem::Version
|
|
102
117
|
version: '0'
|
|
103
118
|
requirements: []
|
|
104
|
-
rubygems_version:
|
|
119
|
+
rubygems_version: 4.0.15
|
|
105
120
|
specification_version: 4
|
|
106
121
|
summary: A DuckDB database adapter for ActiveRecord.
|
|
107
122
|
test_files: []
|