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.
@@ -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 first, then let it establish connection
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.send(:connect)
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
- include Duckdb::SchemaDumper
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
- # Initializes a new DuckDB adapter instance
73
- # @param args [Array] Arguments passed to the parent AbstractAdapter
74
- def initialize(*args)
75
- super
76
- end
77
-
78
- # Reconnects to the DuckDB database by disconnecting and connecting again
79
- # @return [void]
80
- def reconnect
81
- disconnect
82
- connect
83
- end
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
- # @return [Boolean] always returns true for DuckDB
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
- true
261
+ !ducklake?
133
262
  end
134
263
 
135
264
  # Indicates whether the adapter supports INSERT RETURNING for Rails 8
136
- # @return [Boolean] always returns true for DuckDB
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
- true
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, # column_name
242
- formatted_type, # formatted_type
243
- column_default, # column_default
244
- not_null == 1, # not_null (true if NOT NULL constraint)
245
- nil, # type_id
246
- nil, # type_modifier
247
- nil, # collation_name
248
- nil, # comment
249
- nil, # identity
250
- nil, # generated
251
- pk == 1 # primary_key flag (true if primary key)
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('#{sequence_name}')"
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
- database = @config[:database] || :memory
427
- db = if MEMORY_MODE_KEYS.include?(database)
428
- DuckDB::Database.open
429
- else
430
- DuckDB::Database.open(database)
431
- end
432
- @raw_connection = db.connect
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
@@ -6,6 +6,6 @@ module Activerecord
6
6
  module Duckdb
7
7
  # Current version of the activerecord-duckdb gem
8
8
  # @return [String] the version string in semantic version format
9
- VERSION = '0.1.0'
9
+ VERSION = '0.1.1'
10
10
  end
11
11
  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.0
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: 2025-06-20 00:00:00.000000000 Z
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.1.0
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: 3.6.3
119
+ rubygems_version: 4.0.15
105
120
  specification_version: 4
106
121
  summary: A DuckDB database adapter for ActiveRecord.
107
122
  test_files: []