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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6149c058384a777c2bbc0aa55a23a323564a4d65abe060ef9314316c183637bc
|
|
4
|
+
data.tar.gz: 965d8e4e0a75bfbe53a8e966aaa1aa4cb9ec4e5097b354985c25dfc163be8e73
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
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
|
|
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.
|
|
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 |
|