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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97d9df88d9ae4959d62c3013357973776dc52924ef38a0fadfd40fc402adb24f
4
- data.tar.gz: c4606b422388e260c70522896dc14773d01c963e3cbdb77dc1fbb03e3676904a
3
+ metadata.gz: 6149c058384a777c2bbc0aa55a23a323564a4d65abe060ef9314316c183637bc
4
+ data.tar.gz: 965d8e4e0a75bfbe53a8e966aaa1aa4cb9ec4e5097b354985c25dfc163be8e73
5
5
  SHA512:
6
- metadata.gz: 0ec798db2626955dc20f0f5b531d669bb9352669f8fc3f25975da97f3ec8a083fd110c266dcd58e3167a4e0a4a6b516ee7ad128b17537a530508ffba25fd1bd9
7
- data.tar.gz: 48674bbaaf8518907bfaec87833c0547706d2c5d05d117972f045496977a4edf32a8ee2c0d9c4ffa96c3eee11c2e8236eb374728e7fbb3656afdd4081d71f8e5
6
+ metadata.gz: 2dd86c7023c230c2d9e59cc388842bc6b6686a1107de0dd7b73044c91e79697637f0755f0f6d86b17d75d34d843f0300ca7f2bcc0ab65d46c4833d14476dae0d
7
+ data.tar.gz: 057250bbcadfb2dc784d9f076c290548b63a0d9424e7792ccb8e616ef11035794d0d2e5f6afbf4bbc57a7bab6d2a6fa74d8653d6be4008d5d3ce6cf9c1fa840e
data/.rubocop.yml CHANGED
@@ -5,7 +5,7 @@ plugins:
5
5
  - rubocop-thread_safety
6
6
 
7
7
  AllCops:
8
- TargetRubyVersion: 3.4
8
+ TargetRubyVersion: 4.0
9
9
  NewCops: enable
10
10
  UseCache: true
11
11
 
@@ -57,9 +57,22 @@ Naming/MethodParameterName:
57
57
  Naming/PredicatePrefix:
58
58
  Enabled: false
59
59
 
60
+ RSpec/InstanceVariable:
61
+ Exclude:
62
+ - spec/active_record/connection_adapters/duckdb/database_statements_spec.rb
63
+
64
+ RSpec/MultipleDescribes:
65
+ Enabled: false
66
+
67
+ RSpec/NestedGroups:
68
+ Max: 4
69
+
60
70
  Style/Documentation:
61
71
  Enabled: false
62
72
 
73
+ Style/OneClassPerFile:
74
+ Enabled: false
75
+
63
76
  Style/ParallelAssignment:
64
77
  Enabled: false
65
78
 
@@ -76,6 +89,12 @@ Style/StringLiteralsInInterpolation:
76
89
  RSpec/DescribeClass:
77
90
  Exclude:
78
91
  - spec/activerecord-duckdb_spec.rb
92
+ - spec/ducklake_spec.rb
93
+ - spec/active_record/connection_adapters/duckdb/ducklake_migration_spec.rb
94
+ - spec/active_record/connection_adapters/duckdb/ducklake_schema_compatibility_spec.rb
95
+ - spec/active_record/connection_adapters/duckdb/schema_dumper_spec.rb
96
+ - spec/active_record/connection_adapters/duckdb/timestamp_timezone_spec.rb
97
+ - spec/active_record/connection_adapters/duckdb/transaction_spec.rb
79
98
 
80
99
  RSpec/ExampleLength:
81
100
  Enabled: false
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.4.2
1
+ ruby 4.0.5
data/Appraisals ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Test against multiple Rails/ActiveRecord versions
4
+ # Usage:
5
+ # bundle exec appraisal install # Generate gemfiles
6
+ # bundle exec appraisal rspec # Run specs against all versions
7
+ # bundle exec appraisal rails-7.2 rspec # Run against specific version
8
+
9
+ appraise 'rails-7.2' do
10
+ gem 'activerecord', '~> 7.2.0'
11
+ end
12
+
13
+ appraise 'rails-8.0' do
14
+ gem 'activerecord', '~> 8.0.0'
15
+ end
16
+
17
+ appraise 'rails-8.1' do
18
+ gem 'activerecord', '~> 8.1.0'
19
+ end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.1] - 2026-07-08
4
+
5
+ - Cleanup code so all linters pass
6
+ - Adjust tests for better test coverage
7
+ - Ensure Duckdb is using the ActiveRecord timezone rather than a monkey patch
8
+ - Cleanup and fix tests for all specs are passing
9
+ - Update github action for installing duckdb
10
+ - Change minimum RUBY_VERSION to require >= 3.3
11
+ - Update gems versions
12
+ - Cleanup minor deadcode and nil returns
13
+ - Minor touchup to README.md contents
14
+
3
15
  ## [0.1.0] - 2025-06-18
4
16
 
5
17
  - Initial release
data/README.md CHANGED
@@ -98,6 +98,138 @@ production:
98
98
  database: :memory
99
99
  ```
100
100
 
101
+ ### Advanced Connection Configuration
102
+
103
+ The adapter supports advanced configuration options for extensions, settings, secrets, and database attachments. These are configured in your `database.yml` file.
104
+
105
+ #### Extensions
106
+
107
+ Install and load DuckDB extensions automatically on connection:
108
+
109
+ ```yaml
110
+ development:
111
+ adapter: duckdb
112
+ database: db/development.duckdb
113
+ extensions:
114
+ - httpfs
115
+ - postgres_scanner
116
+ - parquet
117
+ ```
118
+
119
+ #### Settings
120
+
121
+ Configure DuckDB settings. The adapter applies secure defaults which you can override:
122
+
123
+ ```yaml
124
+ development:
125
+ adapter: duckdb
126
+ database: db/development.duckdb
127
+ settings:
128
+ threads: 4
129
+ memory_limit: "2GB"
130
+ max_temp_directory_size: "8GB"
131
+ ```
132
+
133
+ **Default Settings:**
134
+
135
+ | Setting | Default Value | Description |
136
+ | ------------------------------ | ------------- | --------------------------------------- |
137
+ | `allow_persistent_secrets` | `false` | Disable persistent secrets for security |
138
+ | `allow_community_extensions` | `false` | Disable community extensions |
139
+ | `autoinstall_known_extensions` | `false` | Disable auto-installing extensions |
140
+ | `autoload_known_extensions` | `false` | Disable auto-loading extensions |
141
+ | `threads` | `1` | Number of threads for query execution |
142
+ | `memory_limit` | `'1GB'` | Maximum memory usage |
143
+ | `max_temp_directory_size` | `'4GB'` | Maximum temp directory size |
144
+
145
+ **Notes:**
146
+
147
+ - `allow_persistent_secrets` and `allow_community_extensions` are applied before loading extensions
148
+ - `lock_configuration = true` is automatically applied at the end to lock all settings
149
+
150
+ #### Secrets
151
+
152
+ Configure secrets for accessing external services (S3, PostgreSQL, etc.). Two styles are supported:
153
+
154
+ **Style 1: Unnamed secrets** (key is the secret type):
155
+
156
+ ```yaml
157
+ development:
158
+ adapter: duckdb
159
+ database: ducklake
160
+ secrets:
161
+ postgres:
162
+ host: localhost
163
+ database: mydb
164
+ user: admin
165
+ password: secret
166
+ s3:
167
+ key_id: AKIAIOSFODNN7EXAMPLE
168
+ secret: wJalrXUtnFEMI/K7MDENG
169
+ region: us-east-1
170
+ ```
171
+
172
+ **Style 2: Named secrets** (explicit `type` key, hash key becomes secret name):
173
+
174
+ ```yaml
175
+ development:
176
+ adapter: duckdb
177
+ database: ducklake
178
+ secrets:
179
+ my_prod_bucket:
180
+ type: s3
181
+ key_id: AKIAIOSFODNN7EXAMPLE
182
+ secret: wJalrXUtnFEMI/K7MDENG
183
+ region: us-east-1
184
+ scope: "s3://prod-bucket"
185
+ my_dev_bucket:
186
+ type: s3
187
+ key_id: AKIAIOSFODNN7EXAMPLE2
188
+ secret: anotherSecretKey
189
+ region: us-west-2
190
+ scope: "s3://dev-bucket"
191
+ ```
192
+
193
+ Named secrets allow multiple secrets of the same type with different scopes.
194
+
195
+ #### Database Attachments
196
+
197
+ Attach external databases (PostgreSQL, MySQL, DuckLake, etc.):
198
+
199
+ ```yaml
200
+ development:
201
+ adapter: duckdb
202
+ database: ducklake
203
+ extensions:
204
+ - postgres_scanner
205
+ - ducklake
206
+ secrets:
207
+ postgres:
208
+ host: localhost
209
+ database: mydb
210
+ user: admin
211
+ password: secret
212
+ attachments:
213
+ - name: pg_db
214
+ connection_string: "postgres:"
215
+ type: POSTGRES
216
+ - name: ducklake
217
+ connection_string: "ducklake:postgres:"
218
+ options: "DATA_PATH 's3://my-bucket', ENCRYPTED"
219
+ ```
220
+
221
+ If you need to switch to a specific attached database after configuration, you can use the `use_database` option:
222
+
223
+ ```yaml
224
+ development:
225
+ adapter: duckdb
226
+ database: db/development.duckdb
227
+ attachments:
228
+ - name: analytics
229
+ connection_string: "s3://bucket/analytics.duckdb"
230
+ use_database: analytics # Switch to the attached database
231
+ ```
232
+
101
233
  ### Sample App setup
102
234
 
103
235
  The following steps are required to setup a sample application using the `activerecord-duckdb` gem:
@@ -121,7 +253,7 @@ gem 'activerecord-duckdb'
121
253
  ```yaml
122
254
  development:
123
255
  adapter: duckdb
124
- database: db/development.db
256
+ database: db/development.duckdb
125
257
 
126
258
  test:
127
259
  adapter: duckdb
@@ -0,0 +1,386 @@
1
+ # Query Execution Call Graph
2
+
3
+ This document maps the query execution methods in the DuckDB adapter and how they interact across Rails versions.
4
+
5
+ ## Overview
6
+
7
+ The DuckDB adapter uses **version-specific modules** to integrate with Rails' query execution infrastructure. This approach:
8
+ - Minimizes code duplication
9
+ - Uses Rails' native implementations where possible
10
+ - Maintains compatibility across Rails 7.2, 8.0, and 8.1
11
+
12
+ ## Architecture
13
+
14
+ ```mermaid
15
+ flowchart TB
16
+ subgraph "Shared Module: DatabaseStatements"
17
+ execute["execute(sql, name)"]
18
+ cast_result["cast_result(result)"]
19
+ affected_rows["affected_rows(raw_result)"]
20
+ end
21
+
22
+ subgraph "Rails 7.2: DatabaseStatementsRails72"
23
+ internal_exec_query_72["internal_exec_query()"]
24
+ exec_delete_72["exec_delete()"]
25
+ end
26
+
27
+ subgraph "Rails 8.0+: DatabaseStatementsRails8"
28
+ raw_execute_8["raw_execute()"]
29
+ end
30
+
31
+ subgraph "Rails Base Class"
32
+ exec_query_base["exec_query()"]
33
+ exec_delete_base["exec_delete()"]
34
+ internal_exec_query_base["internal_exec_query()"]
35
+ end
36
+
37
+ subgraph "DuckDB Driver"
38
+ raw_query["conn.query(sql, *binds)"]
39
+ end
40
+
41
+ %% Rails 7.2 flow
42
+ exec_query_base -->|"7.2"| internal_exec_query_72
43
+ internal_exec_query_72 --> raw_query
44
+ internal_exec_query_72 --> cast_result
45
+ exec_delete_72 --> internal_exec_query_72
46
+
47
+ %% Rails 8.0+ flow
48
+ exec_query_base -->|"8.0+"| internal_exec_query_base
49
+ internal_exec_query_base --> raw_execute_8
50
+ raw_execute_8 --> raw_query
51
+ internal_exec_query_base --> cast_result
52
+ exec_delete_base -->|"8.0+"| affected_rows
53
+ ```
54
+
55
+ ## Rails Version Differences
56
+
57
+ ### Rails 7.2
58
+
59
+ In Rails 7.2, `internal_exec_query` in the base class raises `NotImplementedError`. Adapters **must** implement it.
60
+
61
+ ```mermaid
62
+ flowchart TB
63
+ subgraph "Rails 7.2 Base Class"
64
+ exec_query["exec_query()"]
65
+ internal_exec_base["internal_exec_query()<br/>raises NotImplementedError"]
66
+ exec_delete_base["exec_delete()<br/>returns ActiveRecord::Result"]
67
+ end
68
+
69
+ subgraph "DuckDB Adapter (DatabaseStatementsRails72)"
70
+ internal_exec_impl["internal_exec_query()"]
71
+ exec_delete_impl["exec_delete()"]
72
+ cast_result["cast_result()"]
73
+ end
74
+
75
+ exec_query --> internal_exec_base
76
+ internal_exec_base -.->|"must override"| internal_exec_impl
77
+ internal_exec_impl --> cast_result
78
+
79
+ exec_delete_base -.->|"override for Integer"| exec_delete_impl
80
+ exec_delete_impl -->|"delegates to"| internal_exec_impl
81
+ exec_delete_impl -->|"extracts count from"| result_rows["result.rows[0][0]"]
82
+
83
+ style internal_exec_base fill:#ffcccc
84
+ style exec_delete_base fill:#ffcccc
85
+ style internal_exec_impl fill:#ccffcc
86
+ style exec_delete_impl fill:#ccffcc
87
+ ```
88
+
89
+ **Required implementations in `DatabaseStatementsRails72`:**
90
+ - `internal_exec_query(sql, name, binds, prepare:, async:, allow_retry:)` → `ActiveRecord::Result`
91
+ - `exec_delete(sql, name, binds)` → `Integer` (delegates to `internal_exec_query`, extracts count)
92
+
93
+ ### Rails 8.0 & 8.1
94
+
95
+ In Rails 8.0+, the base class provides working implementations of `internal_exec_query` and `exec_delete` that delegate to lower-level methods. Adapters implement `raw_execute`, `cast_result`, and `affected_rows`.
96
+
97
+ ```mermaid
98
+ flowchart TB
99
+ subgraph "Rails 8.0+ Base Class"
100
+ exec_query["exec_query()"]
101
+ internal_exec_query["internal_exec_query()"]
102
+ internal_execute["internal_execute()"]
103
+ raw_execute_base["raw_execute()<br/>adapter implements"]
104
+ exec_delete["exec_delete()"]
105
+ affected_rows_call["affected_rows()"]
106
+ cast_result_call["cast_result()"]
107
+ end
108
+
109
+ subgraph "DuckDB Adapter (DatabaseStatementsRails8)"
110
+ raw_execute_impl["raw_execute()"]
111
+ end
112
+
113
+ subgraph "DuckDB Adapter (Shared)"
114
+ cast_result_impl["cast_result()"]
115
+ affected_rows_impl["affected_rows()"]
116
+ end
117
+
118
+ exec_query --> internal_exec_query
119
+ internal_exec_query --> internal_execute
120
+ internal_execute --> raw_execute_base
121
+ raw_execute_base -.->|"implemented by"| raw_execute_impl
122
+ internal_exec_query --> cast_result_call
123
+ cast_result_call -.->|"implemented by"| cast_result_impl
124
+
125
+ exec_delete --> internal_execute
126
+ exec_delete --> affected_rows_call
127
+ affected_rows_call -.->|"implemented by"| affected_rows_impl
128
+
129
+ style raw_execute_impl fill:#ccffcc
130
+ style cast_result_impl fill:#ccffcc
131
+ style affected_rows_impl fill:#ccffcc
132
+ ```
133
+
134
+ **Required implementations for Rails 8.0+:**
135
+ - `raw_execute(sql, name, binds, prepare:, async:, allow_retry:, materialize_transactions:, batch:)` → `DuckDB::Result`
136
+ - `cast_result(raw_result)` → `ActiveRecord::Result` (shared)
137
+ - `affected_rows(raw_result)` → `Integer` (shared)
138
+
139
+ ## File Structure
140
+
141
+ ```
142
+ lib/active_record/connection_adapters/duckdb/
143
+ ├── database_statements.rb # Shared: execute, cast_result, affected_rows
144
+ ├── database_statements_rails72.rb # Rails 7.2: internal_exec_query, exec_delete
145
+ └── database_statements_rails8.rb # Rails 8.0+: raw_execute
146
+ ```
147
+
148
+ **Conditional inclusion in `duckdb_adapter.rb`:**
149
+
150
+ ```ruby
151
+ include Duckdb::DatabaseStatements
152
+
153
+ if ActiveRecord::VERSION::MAJOR >= 8
154
+ require 'active_record/connection_adapters/duckdb/database_statements_rails8'
155
+ include Duckdb::DatabaseStatementsRails8
156
+ else
157
+ require 'active_record/connection_adapters/duckdb/database_statements_rails72'
158
+ include Duckdb::DatabaseStatementsRails72
159
+ end
160
+ ```
161
+
162
+ ## Method Signatures
163
+
164
+ ### Shared (all Rails versions)
165
+
166
+ ```ruby
167
+ # Raw SQL execution - returns native DuckDB result
168
+ def execute(sql, name = nil)
169
+ # → DuckDB::Result
170
+ end
171
+
172
+ # Convert DuckDB result to ActiveRecord result
173
+ def cast_result(result)
174
+ # → ActiveRecord::Result
175
+ end
176
+
177
+ # Extract row count from raw result
178
+ def affected_rows(raw_result)
179
+ # → Integer (raw_result.rows_changed)
180
+ end
181
+ ```
182
+
183
+ ### Rails 7.2 Specific
184
+
185
+ ```ruby
186
+ # Query execution - required since base class raises NotImplementedError
187
+ def internal_exec_query(sql, name = 'SQL', binds = [],
188
+ prepare: false, async: false, allow_retry: false)
189
+ # → ActiveRecord::Result (via cast_result)
190
+ end
191
+
192
+ # Delete/Update - extracts count from DuckDB's result set
193
+ def exec_delete(sql, name = nil, binds = [])
194
+ result = internal_exec_query(sql, name, binds)
195
+ result.rows.first&.first || 0 # DuckDB returns [[count]]
196
+ end
197
+ alias exec_update exec_delete
198
+ ```
199
+
200
+ ### Rails 8.0+ Specific
201
+
202
+ ```ruby
203
+ # Raw execution - base class internal_exec_query delegates here
204
+ def raw_execute(sql, name = nil, binds = [],
205
+ prepare: false, async: false, allow_retry: false,
206
+ materialize_transactions: true, batch: false)
207
+ casted_binds = type_casted_binds(binds)
208
+ log(sql, name, binds, casted_binds, async: async) do
209
+ with_raw_connection(allow_retry:, materialize_transactions:) do |conn|
210
+ casted_binds.empty? ? conn.query(sql) : conn.query(sql, *casted_binds)
211
+ end
212
+ end
213
+ # → DuckDB::Result (base class wraps with cast_result)
214
+ end
215
+ ```
216
+
217
+ ## Query Flow Examples
218
+
219
+ ### SELECT Query
220
+
221
+ ```mermaid
222
+ sequenceDiagram
223
+ participant App as Application
224
+ participant AR as ActiveRecord
225
+ participant Base as Rails Base
226
+ participant Adapter as DuckDB Adapter
227
+ participant DB as DuckDB
228
+
229
+ App->>AR: User.find(1)
230
+ AR->>Base: exec_query(sql, binds)
231
+
232
+ alt Rails 7.2
233
+ Base->>Adapter: internal_exec_query()
234
+ Adapter->>DB: conn.query(sql, *binds)
235
+ DB-->>Adapter: DuckDB::Result
236
+ Adapter->>Adapter: cast_result()
237
+ Adapter-->>Base: ActiveRecord::Result
238
+ else Rails 8.0+
239
+ Base->>Base: internal_exec_query()
240
+ Base->>Adapter: raw_execute()
241
+ Adapter->>DB: conn.query(sql, *binds)
242
+ DB-->>Adapter: DuckDB::Result
243
+ Adapter-->>Base: DuckDB::Result
244
+ Base->>Adapter: cast_result()
245
+ Adapter-->>Base: ActiveRecord::Result
246
+ end
247
+
248
+ Base-->>AR: ActiveRecord::Result
249
+ AR-->>App: User instance
250
+ ```
251
+
252
+ ### DELETE Query
253
+
254
+ ```mermaid
255
+ sequenceDiagram
256
+ participant App as Application
257
+ participant AR as ActiveRecord
258
+ participant Base as Rails Base
259
+ participant Adapter as DuckDB Adapter
260
+ participant DB as DuckDB
261
+
262
+ App->>AR: User.delete_all
263
+ AR->>Base: exec_delete(sql, binds)
264
+
265
+ alt Rails 7.2
266
+ Base->>Adapter: exec_delete()
267
+ Adapter->>Adapter: internal_exec_query()
268
+ Adapter->>DB: conn.query(sql, *binds)
269
+ DB-->>Adapter: DuckDB::Result with [[count]]
270
+ Adapter->>Adapter: cast_result()
271
+ Adapter->>Adapter: result.rows[0][0]
272
+ Adapter-->>Base: Integer
273
+ else Rails 8.0+
274
+ Base->>Base: internal_execute()
275
+ Base->>Adapter: raw_execute()
276
+ Adapter->>DB: conn.query(sql, *binds)
277
+ DB-->>Adapter: DuckDB::Result
278
+ Adapter-->>Base: DuckDB::Result
279
+ Base->>Adapter: affected_rows()
280
+ Adapter-->>Base: Integer (rows_changed)
281
+ end
282
+
283
+ Base-->>AR: Integer
284
+ AR-->>App: rows deleted count
285
+ ```
286
+
287
+ ## Design Decisions
288
+
289
+ ### 1. Version-Specific Modules Over Shadowing
290
+
291
+ **Before:** Single `internal_exec_query` that shadowed Rails 8's base implementation.
292
+
293
+ **After:** Separate modules for each Rails version:
294
+ - Rails 7.2: Implements `internal_exec_query` (required)
295
+ - Rails 8.0+: Implements `raw_execute` (lets base handle higher-level methods)
296
+
297
+ **Benefits:**
298
+ - Uses Rails' native query infrastructure for logging, retries, async
299
+ - Cleaner separation of concerns
300
+ - Easier to adapt to future Rails changes
301
+
302
+ ### 2. DuckDB DELETE Returns Count in Result Set
303
+
304
+ DuckDB's DELETE/UPDATE statements return the affected row count as a result set:
305
+
306
+ ```sql
307
+ DELETE FROM users WHERE id = 1;
308
+ -- Returns: columns=["Count"], rows=[[1]]
309
+ ```
310
+
311
+ Also available via `result.rows_changed`.
312
+
313
+ **Rails 7.2:** Extract from `result.rows[0][0]` after `internal_exec_query`
314
+ **Rails 8.0+:** Use `affected_rows(result)` → `result.rows_changed`
315
+
316
+ ### 3. Shared vs Version-Specific Methods
317
+
318
+ | Method | Location | Reason |
319
+ |--------|----------|--------|
320
+ | `execute` | Shared | Same across all versions |
321
+ | `cast_result` | Shared | Used by all query paths |
322
+ | `affected_rows` | Shared | Rails 8.0+ base class calls it |
323
+ | `internal_exec_query` | Rails 7.2 only | Base class provides in 8.0+ |
324
+ | `exec_delete` | Rails 7.2 only | Base class handles in 8.0+ |
325
+ | `raw_execute` | Rails 8.0+ only | New API in Rails 8 |
326
+
327
+ ---
328
+
329
+ ## Schema Statements
330
+
331
+ The `SchemaStatements` module also uses version-specific modules due to a breaking change in Rails 8.1's `Column` class constructor.
332
+
333
+ ### Column Constructor Change (Rails 8.1)
334
+
335
+ ```mermaid
336
+ flowchart TB
337
+ subgraph "Rails 7.2 / 8.0"
338
+ new_col_80["new_column_from_field()"]
339
+ col_ctor_80["Column.new(name, default, sql_type_metadata, null, default_function, ...)"]
340
+ end
341
+
342
+ subgraph "Rails 8.1+"
343
+ new_col_81["new_column_from_field()"]
344
+ col_ctor_81["Column.new(name, cast_type, default, sql_type_metadata, null, default_function, ...)"]
345
+ lookup_cast["lookup_cast_type_from_column()"]
346
+ end
347
+
348
+ new_col_80 --> col_ctor_80
349
+ new_col_81 --> lookup_cast
350
+ lookup_cast --> col_ctor_81
351
+ ```
352
+
353
+ ### Schema File Structure
354
+
355
+ ```
356
+ lib/active_record/connection_adapters/duckdb/
357
+ ├── schema_statements.rb # Shared: tables, indexes, create_table, etc.
358
+ ├── schema_statements_rails80.rb # Rails 7.2/8.0: new_column_from_field
359
+ └── schema_statements_rails81.rb # Rails 8.1+: new_column_from_field (with cast_type)
360
+ ```
361
+
362
+ ### Module Inclusion
363
+
364
+ ```ruby
365
+ # In duckdb_adapter.rb
366
+ if ActiveRecord::VERSION::MAJOR > 8 ||
367
+ (ActiveRecord::VERSION::MAJOR == 8 && ActiveRecord::VERSION::MINOR >= 1)
368
+ require 'active_record/connection_adapters/duckdb/schema_statements_rails81'
369
+ include Duckdb::SchemaStatementsRails81
370
+ else
371
+ require 'active_record/connection_adapters/duckdb/schema_statements_rails80'
372
+ include Duckdb::SchemaStatementsRails80
373
+ end
374
+ ```
375
+
376
+ ### Schema Methods Overview
377
+
378
+ | Method | Location | Purpose |
379
+ |--------|----------|---------|
380
+ | `tables` | Shared | List all tables |
381
+ | `indexes` | Shared | List indexes for a table |
382
+ | `create_table` | Shared | Create table with sequence handling |
383
+ | `create_sequence` | Shared | Create DuckDB sequence |
384
+ | `type_to_sql` | Shared | Convert Rails types to DuckDB SQL |
385
+ | `new_column_from_field` | Version-specific | Create Column from DB metadata |
386
+ | `fetch_type_metadata` | Shared | Parse DuckDB type strings |