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.
@@ -0,0 +1,436 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'duckdb'
4
+ require 'active_record'
5
+ require 'active_record/connection_adapters/abstract_adapter'
6
+ require 'active_record/connection_adapters/duckdb/column'
7
+ require 'active_record/connection_adapters/duckdb/database_limits'
8
+ require 'active_record/connection_adapters/duckdb/database_statements'
9
+ require 'active_record/connection_adapters/duckdb/quoting'
10
+ require 'active_record/connection_adapters/duckdb/schema_creation'
11
+ require 'active_record/connection_adapters/duckdb/schema_statements'
12
+ require 'active_record/connection_adapters/duckdb/schema_definitions'
13
+ require 'active_record/connection_adapters/duckdb/schema_dumper'
14
+
15
+ # Inspired by the SQLite adapter
16
+ # duckdb: https://github.com/duckdb/duckdb-ruby
17
+ # sqlite3 adapter: https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
18
+
19
+ module ActiveRecord
20
+ module ConnectionHandling
21
+ # Establishes a connection to a DuckDB database
22
+ # @param config [Hash] Database configuration options
23
+ # @return [ActiveRecord::ConnectionAdapters::DuckdbAdapter] The database adapter instance
24
+ # @raise [ActiveRecord::ConnectionNotEstablished] If connection fails
25
+ def duckdb_connection(config)
26
+ config = config.symbolize_keys
27
+ begin
28
+ # Create adapter first, then let it establish connection
29
+ adapter = ConnectionAdapters::DuckdbAdapter.new(nil, logger, {}, config)
30
+ adapter.send(:connect)
31
+ adapter
32
+ rescue StandardError => e
33
+ raise ActiveRecord::ConnectionNotEstablished,
34
+ "Could not connect to DuckDB database: #{e.message}"
35
+ end
36
+ end
37
+ end
38
+
39
+ module ConnectionAdapters
40
+ # This adapter provides a connection to a DuckDB database.
41
+ class DuckdbAdapter < AbstractAdapter
42
+ ADAPTER_NAME = 'DuckDB'
43
+
44
+ include Duckdb::DatabaseLimits
45
+ include Duckdb::DatabaseStatements
46
+ include Duckdb::Quoting
47
+ include Duckdb::SchemaStatements
48
+ include Duckdb::SchemaDumper
49
+
50
+ # Allow customization of primary key type like PostgreSQL and MySQL adapters do
51
+ class_attribute :primary_key_type, default: :bigint
52
+
53
+ # DB configuration if used in memory mode
54
+ MEMORY_MODE_KEYS = [:memory, 'memory', ':memory:', ':memory'].freeze
55
+
56
+ # https://duckdb.org/docs/stable/sql/data_types/overview.html
57
+ NATIVE_DATABASE_TYPES = {
58
+ primary_key: 'INTEGER PRIMARY KEY',
59
+ string: { name: 'VARCHAR' },
60
+ integer: { name: 'INTEGER' },
61
+ float: { name: 'REAL' },
62
+ decimal: { name: 'DECIMAL' },
63
+ datetime: { name: 'TIMESTAMP' },
64
+ time: { name: 'TIME' },
65
+ date: { name: 'DATE' },
66
+ bigint: { name: 'BIGINT' },
67
+ binary: { name: 'BLOB' },
68
+ boolean: { name: 'BOOLEAN' },
69
+ uuid: { name: 'UUID' }
70
+ }.freeze
71
+
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
84
+
85
+ # Disconnects from the DuckDB database and cleans up the connection
86
+ # @return [void]
87
+ def disconnect
88
+ @connection&.close
89
+ @connection = nil
90
+ end
91
+
92
+ # Checks if the database connection is active
93
+ # @return [Boolean] true if connection is active, false otherwise
94
+ def active?
95
+ !!(@raw_connection || @connection)
96
+ end
97
+
98
+ # Returns the raw DuckDB connection object
99
+ # @return [DuckDB::Connection, nil] The raw connection object or nil if not connected
100
+ def raw_connection
101
+ @raw_connection || @connection
102
+ end
103
+
104
+ class << self
105
+ # Opens the DuckDB command line console
106
+ # @param config [ActiveRecord::DatabaseConfigurations::DatabaseConfig] Database configuration
107
+ # @param options [Hash] Additional options for the console
108
+ # @return [void]
109
+ def dbconsole(config, options = {})
110
+ db_config = config.configuration_hash
111
+ args = []
112
+ args << db_config[:database] if db_config[:database] && !MEMORY_MODE_KEYS.include?(db_config[:database])
113
+
114
+ find_cmd_and_exec('duckdb', *args)
115
+ end
116
+
117
+ # Checks if the DuckDB database file exists
118
+ # @param config [Hash] Database configuration containing database path
119
+ # @return [Boolean] true if database exists or is in-memory, false otherwise
120
+ def database_exists?(config)
121
+ # Logic to check if database exists
122
+ database_path = config[:database]
123
+ return true if MEMORY_MODE_KEYS.include?(database_path)
124
+
125
+ File.exist?(database_path.to_s)
126
+ end
127
+ end
128
+
129
+ # Indicates whether the adapter supports INSERT...RETURNING syntax
130
+ # @return [Boolean] always returns true for DuckDB
131
+ def use_insert_returning?
132
+ true
133
+ end
134
+
135
+ # Indicates whether the adapter supports INSERT RETURNING for Rails 8
136
+ # @return [Boolean] always returns true for DuckDB
137
+ def supports_insert_returning?
138
+ true
139
+ end
140
+
141
+ # Indicates whether the adapter supports INSERT ON DUPLICATE SKIP syntax
142
+ # @return [Boolean] always returns false for DuckDB
143
+ def supports_insert_on_duplicate_skip?
144
+ false
145
+ end
146
+
147
+ # Determines if a column value should be returned after insert
148
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The column to check
149
+ # @return [Boolean] true if column has default function or parent method returns true
150
+ def return_value_after_insert?(column)
151
+ column.default_function.present? || super
152
+ end
153
+
154
+ # Indicates whether the adapter supports INSERT ON DUPLICATE UPDATE syntax
155
+ # @return [Boolean] always returns false for DuckDB
156
+ def supports_insert_on_duplicate_update?
157
+ false
158
+ end
159
+
160
+ # Determines if primary key should be prefetched before insert
161
+ # @param _table_name [String] The table name (unused)
162
+ # @return [Boolean] always returns false to exclude auto-increment columns from INSERT
163
+ def prefetch_primary_key?(_table_name)
164
+ false
165
+ end
166
+
167
+ # Returns the sequence name for a serial column
168
+ # @param table [String] The table name
169
+ # @param column [String] The column name
170
+ # @return [nil] always returns nil as DuckDB doesn't use named sequences
171
+ def serial_sequence(table, column)
172
+ # Return sequence name if using sequences, nil otherwise
173
+ nil
174
+ end
175
+
176
+ # Returns the primary key columns for a table
177
+ # @param table_name [String] The name of the table
178
+ # @return [Array<String>] Array of primary key column names
179
+ # @raise [ArgumentError] if table_name is blank
180
+ def primary_keys(table_name) # :nodoc:
181
+ raise ArgumentError, 'table_name cannot be blank' unless table_name.present?
182
+
183
+ results = execute("PRAGMA table_info(#{quote(table_name.to_s)})", 'SCHEMA')
184
+ results.filter_map do |result|
185
+ _cid, name, _type, _notnull, _dflt_value, pk = result
186
+
187
+ # pk can be true, false, 1, 0, or nil in DuckDB PRAGMA table_info
188
+ # true or 1 means it's a primary key, false, 0, or nil means it's not
189
+ pk_value = pk == true ? 1 : pk
190
+ [pk_value, name] if [true, 1].include?(pk)
191
+ end.sort_by(&:first).map(&:last)
192
+ end
193
+
194
+ # Returns the native database types supported by DuckDB
195
+ # @return [Hash] Hash mapping ActiveRecord types to DuckDB native types
196
+ def native_database_types
197
+ NATIVE_DATABASE_TYPES
198
+ end
199
+
200
+ # Returns column definitions for a table using PRAGMA table_info
201
+ # @param table_name [String] The name of the table
202
+ # @return [Array<Array>] Array of column definition arrays
203
+ def column_definitions(table_name)
204
+ # Use PRAGMA table_info which gives us more accurate type information
205
+ # including primary key detection that information_schema might miss
206
+ pragma_results = execute("PRAGMA table_info(#{quote(table_name.to_s)})", 'SCHEMA')
207
+ pragma_results.map do |row|
208
+ _cid, column_name, data_type, not_null, column_default, pk = row
209
+
210
+ # Format the type properly - DuckDB PRAGMA gives us the actual type
211
+ # Preserve VARCHAR limits and other type information
212
+ formatted_type = case data_type.to_s.upcase
213
+ when /^BIGINT$/i
214
+ 'BIGINT'
215
+ when /^INTEGER$/i
216
+ 'INTEGER'
217
+ when /^VARCHAR$/i, /^VARCHAR\(\d+\)$/i
218
+ data_type.to_s # Preserve the full VARCHAR(n) format
219
+ when /^DECIMAL\(\d+,\d+\)$/i
220
+ data_type.to_s # Preserve DECIMAL(p,s) format
221
+ when /^TIMESTAMP$/i
222
+ 'TIMESTAMP'
223
+ when /^BOOLEAN$/i
224
+ 'BOOLEAN'
225
+ when /^UUID$/i
226
+ 'UUID'
227
+ when /^BLOB$/i
228
+ 'BLOB'
229
+ when /^DATE$/i
230
+ 'DATE'
231
+ when /^TIME$/i
232
+ 'TIME'
233
+ when /^REAL$/i, /^DOUBLE$/i
234
+ data_type.to_s
235
+ else
236
+ data_type.to_s
237
+ end
238
+
239
+ # Convert PRAGMA results to match information_schema format
240
+ [
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)
252
+ ]
253
+ end
254
+ end
255
+
256
+ # Indicates whether the adapter supports a specific primary key type
257
+ # Support Rails id: :uuid convention
258
+ # @param type [Symbol] The primary key type to check
259
+ # @return [Boolean] true if the type is supported, false otherwise
260
+ def supports_primary_key_type?(type)
261
+ case type
262
+ when :uuid, :string, :integer, :bigint, :primary_key
263
+ true
264
+ else
265
+ false
266
+ end
267
+ end
268
+
269
+ # Generates SQL for getting the next sequence value
270
+ # @param sequence_name [String] The name of the sequence
271
+ # @return [String] SQL expression for next sequence value
272
+ def next_sequence_value(sequence_name)
273
+ "nextval('#{sequence_name}')"
274
+ end
275
+
276
+ # Generates default sequence name following PostgreSQL/Oracle conventions
277
+ # @param table_name [String] The name of the table
278
+ # @param column_name [String] The name of the column (defaults to 'id')
279
+ # @return [String] The generated sequence name
280
+ def default_sequence_name(table_name, column_name = 'id')
281
+ "#{table_name}_#{column_name}_seq"
282
+ end
283
+
284
+ # Configures the primary key type at the class level
285
+ # @param type [Symbol] The primary key type to set
286
+ # @return [Symbol] The configured primary key type
287
+ def self.configure_primary_key_type(type)
288
+ self.primary_key_type = type
289
+ end
290
+
291
+ # Returns the primary key column name for a table
292
+ # @param table_name [String] The name of the table
293
+ # @return [String, nil] The primary key column name, or nil if no single primary key
294
+ def primary_key(table_name)
295
+ pk_columns = primary_keys(table_name)
296
+ pk_columns.size == 1 ? pk_columns.first : nil
297
+ end
298
+
299
+ # Override to prevent Rails schema dumper from detecting sequence defaults as table defaults
300
+ # @param table_name [String, Symbol] The name of the table
301
+ # @return [nil] Always returns nil to prevent table-level defaults
302
+ def primary_key_definition(table_name)
303
+ # Always return nil to prevent any table-level defaults from sequence columns
304
+ nil
305
+ end
306
+
307
+ # Override to ensure sequence defaults never appear at table level
308
+ # @param table_name [String, Symbol] The name of the table
309
+ # @return [nil] Always returns nil as defaults belong to columns
310
+ def table_default_value(table_name)
311
+ # Never return defaults at table level - they belong to columns
312
+ nil
313
+ end
314
+
315
+ # Returns default value for table in schema dumping
316
+ # @param table_name [String] The name of the table
317
+ # @return [nil] Always returns nil to prevent sequence defaults at table level
318
+ def default_value_for_table(table_name)
319
+ nil
320
+ end
321
+
322
+ # Maps DuckDB column types to ActiveRecord schema types
323
+ # Override schema dumping to fix type detection
324
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The column object
325
+ # @return [Symbol] The Rails schema type for the column
326
+ def schema_type(column)
327
+ case column.sql_type
328
+ when /^BIGINT$/i
329
+ :bigint
330
+ when /^INTEGER$/i
331
+ :integer
332
+ when /^VARCHAR$/i, /^VARCHAR\(\d+\)$/i
333
+ :string
334
+ when /^TIMESTAMP$/i
335
+ :datetime
336
+ when /^BOOLEAN$/i
337
+ :boolean
338
+ when /^UUID$/i
339
+ :uuid
340
+ else
341
+ super
342
+ end
343
+ end
344
+
345
+ # Returns indexes for a table or all tables
346
+ # @param table_name [String, nil] The table name, or nil for all tables
347
+ # @return [Array] Array of index definitions
348
+ def indexes(table_name = nil)
349
+ if table_name
350
+ # Delegate to the schema statements implementation
351
+ super
352
+ else
353
+ # This shouldn't happen in normal schema dumping, but handle gracefully
354
+ []
355
+ end
356
+ end
357
+
358
+ # Determines the primary key type for schema dumping
359
+ # @param table_name [String] The name of the table
360
+ # @return [Symbol, nil] The primary key type symbol, or nil if no primary key
361
+ def primary_key_type_for_schema_dump(table_name)
362
+ pk_column = primary_key(table_name)
363
+ return nil unless pk_column
364
+
365
+ # Get the actual column definition
366
+ col_def = columns(table_name).find { |c| c.name == pk_column }
367
+ return nil unless col_def
368
+
369
+ case col_def.sql_type.to_s.upcase
370
+ when 'BIGINT'
371
+ :bigint
372
+ when 'INTEGER'
373
+ :integer
374
+ when 'UUID'
375
+ :uuid
376
+ when /^VARCHAR/
377
+ :string
378
+ end
379
+ end
380
+
381
+ # Returns table options for schema dumping
382
+ # @param table_name [String] The name of the table
383
+ # @return [Hash] Hash of table options for schema dumping
384
+ def table_options(table_name)
385
+ options = {}
386
+
387
+ # Check if primary key has sequence default
388
+ pk_name = primary_key(table_name)
389
+ pk_column = pk_name ? columns(table_name).find { |c| c.name == pk_name } : nil
390
+ has_sequence_default = pk_column&.default_function&.include?('nextval(')
391
+
392
+ pk_type = primary_key_type_for_schema_dump(table_name)
393
+ if has_sequence_default
394
+ # Force explicit primary key inclusion when sequence is involved
395
+ # This prevents Rails from putting sequence default at table level
396
+ options[:id] = pk_type || :bigint
397
+ elsif pk_type && pk_type != :bigint
398
+ # Set the correct primary key type only when different from default
399
+ options[:id] = pk_type # Rails 5+ default is bigint
400
+ end
401
+
402
+ # NEVER include defaults at table level - sequence defaults belong to columns
403
+ options
404
+ end
405
+
406
+ # Check if a type is valid for schema dumping
407
+ # @param type [Symbol, String] The type to validate
408
+ # @return [Boolean] true if type is valid for DuckDB schema dumping
409
+ def valid_type?(type)
410
+ case type.to_s.to_sym
411
+ when :string, :text, :integer, :bigint, :float, :decimal, :datetime, :timestamp,
412
+ :time, :date, :binary, :boolean, :uuid, :interval, :bit,
413
+ :hugeint, :tinyint, :smallint, :utinyint, :usmallint, :uinteger, :ubigint, :uhugeint,
414
+ :varint, :blob, :list, :struct, :map, :enum, :union, :real, :double, :numeric
415
+ true
416
+ else
417
+ false
418
+ end
419
+ end
420
+
421
+ private
422
+
423
+ # Establishes the actual connection to the DuckDB database
424
+ # @return [void]
425
+ 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
433
+ end
434
+ end
435
+ end
436
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/tasks/database_tasks'
4
+ require 'duckdb'
5
+
6
+ module ActiveRecord
7
+ module Tasks # :nodoc:
8
+ # Database tasks implementation for DuckDB adapter
9
+ class DuckdbDatabaseTasks # :nodoc:
10
+ # Keys that indicate in-memory database mode
11
+ MEMORY_MODE_KEYS = [:memory, 'memory', ':memory:', ':memory'].freeze
12
+ TRUE_VALUES = [true, 'true', 'TRUE', 1, '1'].freeze
13
+
14
+ # Indicates whether this adapter uses database configurations
15
+ # @return [Boolean] always returns true
16
+ def self.using_database_configurations?
17
+ true
18
+ end
19
+
20
+ # Initializes a new DuckDB database tasks instance
21
+ # @param db_config [ActiveRecord::DatabaseConfigurations::DatabaseConfig] Database configuration
22
+ # @param root_path [String] Root path for the Rails application
23
+ def initialize(db_config, root_path = ActiveRecord::Tasks::DatabaseTasks.root)
24
+ @db_config = db_config
25
+ @configuration_hash = db_config.configuration_hash
26
+ @root_path = root_path
27
+ end
28
+
29
+ # Creates a new DuckDB database file or skips if in-memory mode
30
+ # @return [void]
31
+ # @raise [StandardError] if database creation fails
32
+ def create
33
+ # For DuckDB, creating a database means creating the file (if not in-memory)
34
+ database_path = @configuration_hash[:database]
35
+ return if MEMORY_MODE_KEYS.include?(database_path)
36
+
37
+ # Ensure the directory exists
38
+ FileUtils.mkdir_p(File.dirname(database_path)) if database_path.include?('/')
39
+
40
+ # Create the database file by opening a connection
41
+ DuckDB::Database.open(database_path).connect.close
42
+ rescue StandardError => e
43
+ warn "Couldn't create '#{database_path}' database. Please check your configuration."
44
+ warn "Error: #{e.message}"
45
+ raise
46
+ end
47
+
48
+ # Drops the DuckDB database by removing the database file
49
+ # @return [void]
50
+ # @raise [StandardError] if database drop fails
51
+ def drop
52
+ db_path = @configuration_hash[:database]
53
+ return if MEMORY_MODE_KEYS.include?(db_path)
54
+
55
+ db_file_path = File.absolute_path?(db_path) ? db_path : File.join(root_path, db_path)
56
+ FileUtils.rm_f(db_file_path)
57
+ rescue StandardError => e
58
+ warn "Couldn't drop database '#{db_path}'"
59
+ warn "Error: #{e.message}"
60
+ raise
61
+ end
62
+
63
+ # Purges the database by dropping and recreating it
64
+ # @return [void]
65
+ def purge
66
+ drop
67
+ create
68
+ end
69
+
70
+ # Returns the character set used by DuckDB
71
+ # @return [String] always returns 'UTF-8'
72
+ def charset
73
+ 'UTF-8'
74
+ end
75
+
76
+ # Returns the collation used by DuckDB
77
+ # @return [nil] always returns nil as DuckDB doesn't use explicit collations
78
+ def collation
79
+ nil
80
+ end
81
+
82
+ # Dumps the database structure to a SQL file
83
+ # @param filename [String] The filename to write the structure dump to
84
+ # @param extra_flags [String, nil] Additional flags for the dump (unused)
85
+ # @return [void]
86
+ def structure_dump(filename, extra_flags = nil)
87
+ # Export the database schema
88
+ establish_connection
89
+
90
+ File.open(filename, 'w') do |file|
91
+ # Get all tables using DuckDB's information schema
92
+ tables_sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' AND table_type = 'BASE TABLE'"
93
+ tables = connection.query(tables_sql)
94
+
95
+ tables.each do |table_row|
96
+ table_name = table_row[0]
97
+ next if %w[ar_internal_metadata schema_migrations].include?(table_name)
98
+
99
+ # Get table schema using PRAGMA (DuckDB supports SQLite-compatible PRAGMA)
100
+ table_info = connection.query("PRAGMA table_info('#{table_name}')")
101
+
102
+ # Build CREATE TABLE statement
103
+ # [[0, "id", "BIGINT", true, nil, true], [1, "name", "VARCHAR", false, 'default_value', false], [2, "email", "VARCHAR", false, nil, false], [3, "created_at", "TIMESTAMP", true, nil, false], [4, "updated_at", "TIMESTAMP", true, nil, false]]
104
+ columns = table_info.map do |col|
105
+ col_def = "#{col[1]} #{col[2]}"
106
+ col_def += ' NOT NULL' if TRUE_VALUES.include?(col[3])
107
+ col_def += " DEFAULT #{col[4]}" if col[4]
108
+ col_def += ' PRIMARY KEY' if TRUE_VALUES.include?(col[5])
109
+ col_def
110
+ end
111
+
112
+ file.puts "CREATE TABLE #{table_name} ("
113
+ file.puts " #{columns.join(",\n ")}"
114
+ file.puts ');'
115
+ file.puts
116
+ end
117
+
118
+ # Get list of sequences used for the table
119
+ sequences = connection.query('SELECT sequencename, start_value, min_value, max_value, increment_by, cycle FROM pg_catalog.pg_sequences').to_a
120
+
121
+ # Build CREATE SEQUENCE statements
122
+ sequences.each do |seq|
123
+ seq_def = "CREATE SEQUENCE #{seq[0]}"
124
+ seq_def += " START WITH #{seq[1]}" if seq[1] && seq[1] != 1
125
+ seq_def += " INCREMENT BY #{seq[4]}" if seq[4] && seq[4] != 1
126
+ seq_def += " MINVALUE #{seq[2]}" if seq[2] != -9_223_372_036_854_775_808
127
+ seq_def += " MAXVALUE #{seq[3]}" if seq[3] != 9_223_372_036_854_775_807
128
+ seq_def += ' CYCLE' if seq[5]
129
+ seq_def += ';'
130
+ file.puts seq_def
131
+ end
132
+ end
133
+ end
134
+
135
+ # Loads database structure from a SQL file
136
+ # @param filename [String] The filename to load the structure from
137
+ # @param extra_flags [String, nil] Additional flags for the load (unused)
138
+ # @return [void]
139
+ # @raise [LoadError] if the schema file does not exist
140
+ def structure_load(filename, extra_flags = nil)
141
+ establish_connection
142
+ raise(LoadError, 'Database scheme file does not exist') unless File.exist?(filename)
143
+
144
+ sql = File.read(filename)
145
+ connection.query(sql)
146
+ end
147
+
148
+ private
149
+
150
+ attr_reader :configuration_hash, :db_config, :root_path
151
+
152
+ # Gets a database connection from the connection pool
153
+ # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter] The database connection
154
+ def connection
155
+ if ActiveRecord::Base.connection_pool
156
+ ActiveRecord::Base.connection_pool.checkout
157
+ else
158
+ ActiveRecord::Base.lease_connection
159
+ end
160
+ end
161
+
162
+ # Establishes a connection to the database
163
+ # @param config [ActiveRecord::DatabaseConfigurations::DatabaseConfig] Database configuration to use
164
+ # @return [void]
165
+ def establish_connection(config = db_config)
166
+ ActiveRecord::Base.establish_connection(config)
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveRecord adapter for DuckDB database integration
4
+ module Activerecord
5
+ # DuckDB specific functionality and constants
6
+ module Duckdb
7
+ # Current version of the activerecord-duckdb gem
8
+ # @return [String] the version string in semantic version format
9
+ VERSION = '0.1.0'
10
+ end
11
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Main entry point for the activerecord-duckdb gem
4
+ # Loads the DuckDB adapter and registers it with ActiveRecord
5
+ # Provides Rails integration when Rails is present
6
+
7
+ # Load the adapter regardless of Rails presence
8
+ require 'active_record/connection_adapters/duckdb_adapter'
9
+
10
+ # Register the DuckDB adapter with ActiveRecord
11
+ # This works for both Rails and non-Rails environments
12
+ # @return [void]
13
+ ActiveRecord::ConnectionAdapters.register(
14
+ 'duckdb',
15
+ 'ActiveRecord::ConnectionAdapters::DuckdbAdapter',
16
+ 'active_record/connection_adapters/duckdb_adapter'
17
+ )
18
+
19
+ if defined?(Rails)
20
+ module ActiveRecord
21
+ module ConnectionAdapters
22
+ # Rails integration for the DuckDB adapter
23
+ # Provides rake tasks and ActiveRecord integration when Rails is present
24
+ class DuckdbRailtie < ::Rails::Railtie
25
+ # Registers DuckDB database tasks with Rails
26
+ # @return [void]
27
+ rake_tasks do
28
+ require 'active_record/tasks/duckdb_database_tasks'
29
+ end
30
+
31
+ # Sets up DuckDB adapter integration when ActiveRecord loads
32
+ # @return [void]
33
+ ActiveSupport.on_load(:active_record) do
34
+ # Register the database tasks - try multiple approaches for compatibility
35
+ if ActiveRecord::Tasks::DatabaseTasks.respond_to?(:register_task)
36
+ ActiveRecord::Tasks::DatabaseTasks.register_task(
37
+ 'duckdb',
38
+ 'ActiveRecord::Tasks::DuckdbDatabaseTasks'
39
+ )
40
+ else
41
+ # Fallback for older Rails versions
42
+ ActiveRecord::Tasks::DatabaseTasks.module_eval do
43
+ def self.class_for_adapter(adapter)
44
+ case adapter
45
+ when 'duckdb'
46
+ ActiveRecord::Tasks::DuckdbDatabaseTasks
47
+ else
48
+ super
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ else
58
+ # Non-Rails environment - require the tasks after adapter registration
59
+ require 'active_record/tasks/duckdb_database_tasks'
60
+ end
@@ -0,0 +1,6 @@
1
+ module Activerecord
2
+ module Duckdb
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
data/tmp/.gitkeep ADDED
File without changes