sequel-duckdb 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.beads/.beads-credential-key +1 -0
- data/.beads/.gitignore +66 -0
- data/.beads/README.md +85 -0
- data/.beads/config.yaml +56 -0
- data/.beads/hooks/post-checkout +24 -0
- data/.beads/hooks/post-merge +24 -0
- data/.beads/hooks/pre-commit +24 -0
- data/.beads/hooks/pre-push +24 -0
- data/.beads/hooks/prepare-commit-msg +24 -0
- data/.beads/metadata.json +7 -0
- data/.kiro/specs/advanced-sql-features-implementation/design.md +3 -1
- data/.kiro/specs/advanced-sql-features-implementation/requirements.md +1 -1
- data/.kiro/specs/advanced-sql-features-implementation/tasks.md +5 -1
- data/.kiro/specs/duckdb-sql-syntax-compatibility/design.md +15 -1
- data/.kiro/specs/duckdb-sql-syntax-compatibility/requirements.md +1 -1
- data/.kiro/specs/duckdb-sql-syntax-compatibility/tasks.md +13 -0
- data/.kiro/specs/edge-cases-and-validation-fixes/requirements.md +1 -1
- data/.kiro/specs/integration-test-database-setup/requirements.md +1 -1
- data/.kiro/specs/sequel-duckdb-adapter/design.md +8 -1
- data/.kiro/specs/sequel-duckdb-adapter/requirements.md +10 -10
- data/.kiro/specs/sequel-duckdb-adapter/tasks.md +48 -3
- data/.kiro/specs/sql-expression-handling-fix/design.md +34 -1
- data/.kiro/specs/sql-expression-handling-fix/requirements.md +1 -1
- data/.kiro/specs/sql-expression-handling-fix/tasks.md +3 -0
- data/.kiro/specs/test-infrastructure-improvements/requirements.md +1 -1
- data/.kiro/steering/product.md +5 -1
- data/.kiro/steering/structure.md +1 -1
- data/.kiro/steering/tech.md +14 -1
- data/.kiro/steering/testing.md +22 -1
- data/.mdformat.toml +2 -0
- data/.release-please-manifest.json +3 -0
- data/.rubocop.yml +116 -58
- data/.rubocop_todo.yml +323 -0
- data/AGENTS.md +154 -0
- data/API_DOCUMENTATION.md +73 -49
- data/CHANGELOG.md +46 -10
- data/FINAL_STATUS.md +99 -0
- data/LICENSE +1 -1
- data/MIGRATION_EXAMPLES.md +1 -1
- data/PERFORMANCE_OPTIMIZATIONS.md +4 -1
- data/README.md +90 -1
- data/REFACTORING_SUMMARY.md +264 -0
- data/Rakefile +21 -5
- data/TASK_10.2_IMPLEMENTATION_SUMMARY.md +19 -1
- data/docs/DUCKDB_SQL_PATTERNS.md +39 -1
- data/docs/TASK_12_VERIFICATION_SUMMARY.md +14 -1
- data/justfile +52 -0
- data/lib/sequel/adapters/duckdb.rb +137 -108
- data/lib/sequel/adapters/shared/duckdb.rb +292 -1490
- data/lib/sequel/duckdb/helpers/copier.rb +50 -0
- data/lib/sequel/duckdb/helpers/pathifier.rb +141 -0
- data/lib/sequel/duckdb/version.rb +5 -2
- data/plans/date_arithmetic.md +420 -0
- data/plans/engineering/Sequel.md +471 -0
- data/plans/engineering/duckdb.md +712 -0
- data/plans/engineering/sqlite.md +453 -0
- data/plans/mock_connection_bug.md +333 -0
- data/plans/mock_without_driver_gem.md +371 -0
- data/plans/over_engineering_analysis.md +122 -0
- data/plans/schema_management.md +383 -0
- data/release-please-config.json +14 -0
- metadata +49 -27
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Mock Connection Bug Analysis
|
|
2
|
+
|
|
3
|
+
## Error Summary
|
|
4
|
+
|
|
5
|
+
The downstream project is encountering a `NoMethodError` when using `Sequel::Mock::Connection`:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
undefined method 'query' for an instance of Sequel::Mock::Connection (NoMethodError)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The error occurs at [duckdb.rb:928](../lib/sequel/adapters/shared/duckdb.rb#L928) in the `execute_statement` method.
|
|
12
|
+
|
|
13
|
+
## Root Cause
|
|
14
|
+
|
|
15
|
+
The `execute_statement` method in [duckdb.rb:906-965](../lib/sequel/adapters/shared/duckdb.rb#L906-L965) directly calls DuckDB-specific methods on the connection object:
|
|
16
|
+
|
|
17
|
+
1. **Line 917**: `conn.prepare(sql)` - Calls DuckDB::Connection#prepare
|
|
18
|
+
2. **Line 928**: `conn.query(sql)` - Calls DuckDB::Connection#query
|
|
19
|
+
|
|
20
|
+
However, `Sequel::Mock::Connection` only implements:
|
|
21
|
+
|
|
22
|
+
- `execute(sql)` - Delegates to the mock database's `_execute` method
|
|
23
|
+
- No `query()` method
|
|
24
|
+
- No `prepare()` method
|
|
25
|
+
|
|
26
|
+
## Problem Details
|
|
27
|
+
|
|
28
|
+
### Where It Fails
|
|
29
|
+
|
|
30
|
+
The error occurs in the `execute_statement` method when there are no parameters:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
def execute_statement(conn, sql, params = [], _opts = {})
|
|
34
|
+
# ...
|
|
35
|
+
if params && !params.empty?
|
|
36
|
+
# Line 917: This will also fail with Mock::Connection
|
|
37
|
+
stmt = conn.prepare(sql)
|
|
38
|
+
# ...
|
|
39
|
+
else
|
|
40
|
+
# Line 928: THIS IS WHERE THE ERROR OCCURS
|
|
41
|
+
result = conn.query(sql)
|
|
42
|
+
end
|
|
43
|
+
# ...
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Why It Happens
|
|
48
|
+
|
|
49
|
+
The adapter assumes it will always receive a `::DuckDB::Connection` object, which has:
|
|
50
|
+
|
|
51
|
+
- `query(sql)` - Execute SQL and return results
|
|
52
|
+
- `prepare(sql)` - Create a prepared statement
|
|
53
|
+
- `execute()` - Execute a prepared statement
|
|
54
|
+
|
|
55
|
+
But when using `Sequel.mock('duckdb://...')`, the connection pool contains `Sequel::Mock::Connection` objects instead, which only support the generic `execute(sql)` method.
|
|
56
|
+
|
|
57
|
+
## Impact
|
|
58
|
+
|
|
59
|
+
This bug prevents using the DuckDB adapter with Sequel's mock testing framework. Any test code that uses `Sequel.mock` with the DuckDB adapter will fail when attempting to execute queries.
|
|
60
|
+
|
|
61
|
+
The error manifests in the test suite when:
|
|
62
|
+
|
|
63
|
+
1. Using `Sequel.mock` to create a mock DuckDB database
|
|
64
|
+
2. Attempting any query operation (SELECT, INSERT, UPDATE, DELETE)
|
|
65
|
+
3. The `execute_statement` method tries to call `conn.query()` or `conn.prepare()`
|
|
66
|
+
|
|
67
|
+
## Solution Approach
|
|
68
|
+
|
|
69
|
+
### Research: How SQLite Adapter Works with Mock
|
|
70
|
+
|
|
71
|
+
**The Answer:** SQLite's adapter **DOES NOT** call adapter-specific connection methods directly in its `execute_statement`-equivalent code. Instead, it uses a completely different architecture.
|
|
72
|
+
|
|
73
|
+
**Key Files:**
|
|
74
|
+
|
|
75
|
+
- `sequel-5.96.0/lib/sequel/adapters/sqlite.rb` (lines 169-176, 251-270)
|
|
76
|
+
- `sequel-5.96.0/lib/sequel/adapters/mock.rb` (lines 7-30, 111-113, 143-173)
|
|
77
|
+
|
|
78
|
+
**How SQLite Works:**
|
|
79
|
+
|
|
80
|
+
1. **Database-level execute methods** (sqlite.rb:169-176):
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# Public database methods that route through _execute
|
|
84
|
+
def execute(sql, opts=OPTS, &block)
|
|
85
|
+
_execute(:select, sql, opts, &block)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def execute_dui(sql, opts=OPTS)
|
|
89
|
+
_execute(:update, sql, opts)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def execute_insert(sql, opts=OPTS)
|
|
93
|
+
_execute(:insert, sql, opts)
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
2. **Private \_execute method** (sqlite.rb:251-270):
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
def _execute(type, sql, opts, &block)
|
|
101
|
+
synchronize(opts[:server]) do |conn|
|
|
102
|
+
# ONLY calls conn.query/execute INSIDE synchronize block
|
|
103
|
+
# with REAL SQLite connections
|
|
104
|
+
case type
|
|
105
|
+
when :select
|
|
106
|
+
log_connection_yield(sql, conn){conn.query(sql, args, &block)}
|
|
107
|
+
when :insert
|
|
108
|
+
log_connection_yield(sql, conn){conn.execute(sql, args)}
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
3. **How Mock Database overrides this** (mock.rb:111-113):
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# Mock::Database OVERRIDES execute to bypass _execute!
|
|
118
|
+
def execute(sql, opts=OPTS, &block)
|
|
119
|
+
synchronize(opts[:server]){|c| _execute(c, sql, opts, &block)}
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
4. **Mock::Connection delegation** (mock.rb:27-29):
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# When conn.execute(sql) is called, it goes back to database
|
|
127
|
+
def execute(sql)
|
|
128
|
+
@db.send(:_execute, self, sql, :log=>false)
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**The Critical Difference:**
|
|
133
|
+
|
|
134
|
+
**SQLite approach:**
|
|
135
|
+
|
|
136
|
+
- Database has `execute()`, `execute_dui()`, `execute_insert()` methods
|
|
137
|
+
- These call private `_execute(type, sql, opts)`
|
|
138
|
+
- `_execute` calls `synchronize` which yields the connection
|
|
139
|
+
- Inside `synchronize`, it calls `conn.query()` or `conn.execute()`
|
|
140
|
+
- **Mock::Database OVERRIDES the public execute() methods**, so `_execute` never calls `conn.query()`!
|
|
141
|
+
|
|
142
|
+
**DuckDB current approach (BROKEN):**
|
|
143
|
+
|
|
144
|
+
- DuckDB's **shared** adapter defines `execute()` (unlike SQLite's shared adapter which doesn't)
|
|
145
|
+
- This `execute()` calls `synchronize(){|conn| execute_statement(conn, ...)}`
|
|
146
|
+
- `execute_statement` DIRECTLY calls `conn.query()` and `conn.prepare()`
|
|
147
|
+
- When mocking, `conn` IS a Mock::Connection, which doesn't have those methods
|
|
148
|
+
- Mock::Database can't override this because the shared adapter's `execute()` is what gets extended
|
|
149
|
+
|
|
150
|
+
### Implementation Strategy
|
|
151
|
+
|
|
152
|
+
**Approach: Follow SQLite's Pattern - Don't Call Connection Methods Directly in Private Helpers**
|
|
153
|
+
|
|
154
|
+
The issue is that `execute_statement` is a **helper method** that receives an already-obtained connection and tries to call adapter-specific methods on it. SQLite doesn't do this - it only calls adapter-specific connection methods **inside the synchronize block** in `_execute`.
|
|
155
|
+
|
|
156
|
+
**Solution:** DuckDB's adapter should **NOT** have connection-specific logic in `execute_statement` when mocking. Instead:
|
|
157
|
+
|
|
158
|
+
1. **Check if connection is a Mock::Connection at the START of execute_statement**
|
|
159
|
+
2. **If mock, call conn.execute() and return immediately** (which delegates to database's `_execute`)
|
|
160
|
+
3. **If real DuckDB connection, use the existing logic**
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
def execute_statement(conn, sql, params = [], _opts = {})
|
|
164
|
+
# Pattern: Check for Mock::Connection first (inspired by sqlite.rb architecture)
|
|
165
|
+
# Mock connections should be handled before any adapter-specific calls
|
|
166
|
+
if conn.is_a?(Sequel::Mock::Connection)
|
|
167
|
+
# Mock connections only support execute(), which delegates back to
|
|
168
|
+
# database's _execute method (mock.rb:27-29)
|
|
169
|
+
# The database's _execute handles logging and result formatting
|
|
170
|
+
return conn.execute(sql)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Real DuckDB connection - use native methods
|
|
174
|
+
start_time = Time.now
|
|
175
|
+
|
|
176
|
+
begin
|
|
177
|
+
log_sql_query(sql, params)
|
|
178
|
+
|
|
179
|
+
if params && !params.empty?
|
|
180
|
+
stmt = conn.prepare(sql)
|
|
181
|
+
params.each_with_index do |param, index|
|
|
182
|
+
stmt.bind(index + 1, param)
|
|
183
|
+
end
|
|
184
|
+
result = stmt.execute
|
|
185
|
+
else
|
|
186
|
+
result = conn.query(sql)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# ... rest of existing logic for real connections
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Why `is_a?(Sequel::Mock::Connection)` instead of `respond_to?`:**
|
|
195
|
+
|
|
196
|
+
- SQLite doesn't need this check because Mock::Database overrides at database level
|
|
197
|
+
- DuckDB has connection-level logic, so we need to detect mocks explicitly
|
|
198
|
+
- `is_a?` is clearer about intent: "handle mocks differently"
|
|
199
|
+
- `respond_to?(:query)` could give false negatives if DuckDB adds new connection types
|
|
200
|
+
|
|
201
|
+
**Why this pattern works:**
|
|
202
|
+
|
|
203
|
+
- Mock connections have a special delegation pattern that needs early detection
|
|
204
|
+
- Real connections get the full DuckDB-specific optimizations
|
|
205
|
+
- Follows the principle: "Detect special cases early, handle normal cases normally"
|
|
206
|
+
- Mirrors SQLite's architectural approach of keeping adapter-specific calls away from generic code paths
|
|
207
|
+
|
|
208
|
+
## Testing Implications
|
|
209
|
+
|
|
210
|
+
Once fixed, the adapter should support:
|
|
211
|
+
|
|
212
|
+
1. **Mock Testing**: Full compatibility with `Sequel.mock` for unit testing
|
|
213
|
+
2. **SQL Verification**: Tests can verify SQL generation without a real database
|
|
214
|
+
3. **Behavior Testing**: Test application logic without DuckDB dependency
|
|
215
|
+
4. **Test Isolation**: Fast, isolated tests that don't require database setup
|
|
216
|
+
|
|
217
|
+
## Files to Modify
|
|
218
|
+
|
|
219
|
+
- [lib/sequel/adapters/shared/duckdb.rb](../lib/sequel/adapters/shared/duckdb.rb) - Modify `execute_statement` method (lines 906-965)
|
|
220
|
+
|
|
221
|
+
## Validation
|
|
222
|
+
|
|
223
|
+
After implementation, verify:
|
|
224
|
+
|
|
225
|
+
1. Existing tests still pass with real DuckDB connections
|
|
226
|
+
2. Mock connections work correctly
|
|
227
|
+
3. Both parameterized and non-parameterized queries work with mocks
|
|
228
|
+
4. Error handling still functions properly
|
|
229
|
+
5. SQL logging works with mock connections
|
|
230
|
+
|
|
231
|
+
## Can We Move Execute Logic to Real Adapter (Like SQLite)?
|
|
232
|
+
|
|
233
|
+
**Short answer: Yes, we could, but there's no good reason to.**
|
|
234
|
+
|
|
235
|
+
**Comparison:**
|
|
236
|
+
|
|
237
|
+
**SQLite adapter:**
|
|
238
|
+
|
|
239
|
+
- 461 lines total
|
|
240
|
+
- 27 methods
|
|
241
|
+
- Simple execution logic (no custom logging, basic error handling)
|
|
242
|
+
- Real adapter has execute logic
|
|
243
|
+
- Shared adapter only has schema introspection (no execute methods)
|
|
244
|
+
|
|
245
|
+
**DuckDB adapter:**
|
|
246
|
+
|
|
247
|
+
- 2,483 lines in shared adapter alone
|
|
248
|
+
- 141 methods
|
|
249
|
+
- Sophisticated execution features:
|
|
250
|
+
- Custom SQL query/timing/error logging (log_sql_query, log_sql_timing, log_sql_error)
|
|
251
|
+
- Enhanced error mapping (database_exception_class maps 10+ error patterns)
|
|
252
|
+
- Enhanced error messages (database_exception_message adds SQL + params context)
|
|
253
|
+
- Parameterized query support with binding
|
|
254
|
+
- Row hash conversion from DuckDB result arrays
|
|
255
|
+
- Result type detection (::DuckDB::Result)
|
|
256
|
+
|
|
257
|
+
**Why DuckDB's execution logic is in shared adapter:**
|
|
258
|
+
|
|
259
|
+
1. **Code reuse** - Both real adapter AND mock need the same sophisticated error handling
|
|
260
|
+
2. **Testing** - Mocks should get the same error messages, logging, etc.
|
|
261
|
+
3. **Architecture** - Real adapter is minimal (just connection lifecycle), shared adapter is the "brain"
|
|
262
|
+
|
|
263
|
+
**What's DuckDB-specific that can't be shared:**
|
|
264
|
+
|
|
265
|
+
- Everything in execute_statement references `::DuckDB::Connection`, `::DuckDB::Result`, `::DuckDB::Error`
|
|
266
|
+
- These types don't exist when mocking
|
|
267
|
+
|
|
268
|
+
**Could we move execute() to real adapter?**
|
|
269
|
+
|
|
270
|
+
- Yes, technically possible
|
|
271
|
+
- Would require duplicating all the error handling, logging, parameter binding logic
|
|
272
|
+
- OR would require the shared adapter to still have helper methods that call `conn.query()`
|
|
273
|
+
- Either way, we'd still need Mock::Connection detection somewhere
|
|
274
|
+
|
|
275
|
+
**~~Better~~ Band-aid solution:**
|
|
276
|
+
|
|
277
|
+
- ~~Keep architecture as-is (it's well-designed)~~
|
|
278
|
+
- ~~Add Mock::Connection detection to `execute_statement()`~~
|
|
279
|
+
- ~~This is a 3-line fix vs restructuring the entire adapter~~
|
|
280
|
+
|
|
281
|
+
**ACTUAL Better solution:**
|
|
282
|
+
|
|
283
|
+
- The adapter is over-engineered (see [over_engineering_analysis.md](over_engineering_analysis.md))
|
|
284
|
+
- DuckDB reimplements what Sequel already provides (logging, error handling, timing)
|
|
285
|
+
- SQLite uses `log_connection_yield()` in ONE LINE - handles all logging/timing/errors
|
|
286
|
+
- DuckDB should be simplified to follow SQLite's patterns
|
|
287
|
+
- This would both fix the Mock issue AND reduce code from 2,483 to ~750 lines
|
|
288
|
+
|
|
289
|
+
## Summary
|
|
290
|
+
|
|
291
|
+
**Root Cause:** The DuckDB adapter calls adapter-specific connection methods (`conn.query()`, `conn.prepare()`) in a helper method that receives pre-obtained connections. Mock::Connection doesn't implement these methods.
|
|
292
|
+
|
|
293
|
+
**Why SQLite works:**
|
|
294
|
+
|
|
295
|
+
- Real SQLite adapter (sqlite.rb:169-270) defines `execute()` and `_execute()` that call `conn.query()`
|
|
296
|
+
- **Shared SQLite adapter (shared/sqlite.rb) does NOT define execute methods** - only schema introspection
|
|
297
|
+
- Mock::Database (mock.rb:111-113, 143-173) **overrides execute() and provides its own \_execute()** that NEVER calls connection methods
|
|
298
|
+
- So when mocking, Mock::Database's execute() is used, which bypasses all adapter-specific connection method calls
|
|
299
|
+
|
|
300
|
+
**Why DuckDB breaks (architectural difference):**
|
|
301
|
+
|
|
302
|
+
DuckDB uses a different adapter architecture than SQLite:
|
|
303
|
+
|
|
304
|
+
**SQLite architecture:**
|
|
305
|
+
|
|
306
|
+
- Real adapter (sqlite.rb) defines `execute()` and `_execute()` - contains ALL execution logic
|
|
307
|
+
- Shared adapter (shared/sqlite.rb) defines ZERO execution methods - only schema introspection
|
|
308
|
+
- When mocking, Mock::Database's execute() is used, bypassing the real adapter entirely
|
|
309
|
+
|
|
310
|
+
**DuckDB architecture:**
|
|
311
|
+
|
|
312
|
+
- Real adapter (duckdb.rb) defines ONLY connection lifecycle methods (`connect`, `disconnect_connection`)
|
|
313
|
+
- **Shared adapter (shared/duckdb.rb:75) defines ALL execution logic including `execute()`**
|
|
314
|
+
- Real adapter includes the shared adapter's DatabaseMethods module
|
|
315
|
+
- Both real AND mock use the same execute() from the shared adapter
|
|
316
|
+
|
|
317
|
+
**The breakage:**
|
|
318
|
+
|
|
319
|
+
- When you call `Sequel.mock('duckdb://...')`, Mock::Database extends shared/duckdb.rb
|
|
320
|
+
- Mock::Database gets the shared adapter's `execute()` method
|
|
321
|
+
- This execute() calls `synchronize(){|conn| execute_statement(conn, ...)}`
|
|
322
|
+
- execute_statement() calls `conn.query()` on what IS a Mock::Connection
|
|
323
|
+
- Mock::Connection doesn't implement query() → NoMethodError
|
|
324
|
+
|
|
325
|
+
**Why this architecture was chosen:**
|
|
326
|
+
|
|
327
|
+
- All execution logic in shared adapter = code reuse
|
|
328
|
+
- Real adapter only handles DuckDB-specific connection details
|
|
329
|
+
- But this means the shared adapter MUST handle mocking too
|
|
330
|
+
|
|
331
|
+
**Fix:** Add an early check for `Sequel::Mock::Connection` at the start of `execute_statement()` and delegate to `conn.execute()` which will route back to the Mock::Database's `_execute` method for proper handling.
|
|
332
|
+
|
|
333
|
+
**Inspiration:** SQLite adapter's architecture (sequel-5.96.0/lib/sequel/adapters/sqlite.rb:169-270) combined with understanding of Mock::Connection delegation pattern (sequel-5.96.0/lib/sequel/adapters/mock.rb:27-29, 111-113).
|