sequel-duckdb 0.2.1 → 0.3.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/lib/sequel/duckdb/version.rb +1 -1
  4. metadata +3 -57
  5. data/.beads/.beads-credential-key +0 -1
  6. data/.beads/.gitignore +0 -66
  7. data/.beads/README.md +0 -85
  8. data/.beads/config.yaml +0 -56
  9. data/.beads/hooks/post-checkout +0 -24
  10. data/.beads/hooks/post-merge +0 -24
  11. data/.beads/hooks/pre-commit +0 -24
  12. data/.beads/hooks/pre-push +0 -24
  13. data/.beads/hooks/prepare-commit-msg +0 -24
  14. data/.beads/metadata.json +0 -7
  15. data/.kiro/specs/advanced-sql-features-implementation/design.md +0 -26
  16. data/.kiro/specs/advanced-sql-features-implementation/requirements.md +0 -43
  17. data/.kiro/specs/advanced-sql-features-implementation/tasks.md +0 -28
  18. data/.kiro/specs/duckdb-sql-syntax-compatibility/design.md +0 -272
  19. data/.kiro/specs/duckdb-sql-syntax-compatibility/requirements.md +0 -84
  20. data/.kiro/specs/duckdb-sql-syntax-compatibility/tasks.md +0 -107
  21. data/.kiro/specs/edge-cases-and-validation-fixes/requirements.md +0 -32
  22. data/.kiro/specs/integration-test-database-setup/design.md +0 -0
  23. data/.kiro/specs/integration-test-database-setup/requirements.md +0 -117
  24. data/.kiro/specs/sequel-duckdb-adapter/design.md +0 -549
  25. data/.kiro/specs/sequel-duckdb-adapter/requirements.md +0 -202
  26. data/.kiro/specs/sequel-duckdb-adapter/tasks.md +0 -292
  27. data/.kiro/specs/sql-expression-handling-fix/design.md +0 -331
  28. data/.kiro/specs/sql-expression-handling-fix/requirements.md +0 -86
  29. data/.kiro/specs/sql-expression-handling-fix/tasks.md +0 -25
  30. data/.kiro/specs/test-infrastructure-improvements/requirements.md +0 -106
  31. data/.kiro/steering/product.md +0 -26
  32. data/.kiro/steering/structure.md +0 -88
  33. data/.kiro/steering/tech.md +0 -137
  34. data/.kiro/steering/testing.md +0 -213
  35. data/.mdformat.toml +0 -2
  36. data/.rubocop.yml +0 -161
  37. data/.rubocop_todo.yml +0 -323
  38. data/.yardopts +0 -8
  39. data/AGENTS.md +0 -180
  40. data/API_DOCUMENTATION.md +0 -943
  41. data/FINAL_STATUS.md +0 -99
  42. data/MIGRATION_EXAMPLES.md +0 -740
  43. data/PERFORMANCE_OPTIMIZATIONS.md +0 -726
  44. data/REFACTORING_SUMMARY.md +0 -264
  45. data/Rakefile +0 -43
  46. data/TASK_10.2_IMPLEMENTATION_SUMMARY.md +0 -182
  47. data/docs/DUCKDB_SQL_PATTERNS.md +0 -448
  48. data/docs/TASK_12_VERIFICATION_SUMMARY.md +0 -135
  49. data/justfile +0 -50
  50. data/plans/date_arithmetic.md +0 -420
  51. data/plans/engineering/Sequel.md +0 -471
  52. data/plans/engineering/duckdb.md +0 -712
  53. data/plans/engineering/sqlite.md +0 -453
  54. data/plans/mock_connection_bug.md +0 -333
  55. data/plans/mock_without_driver_gem.md +0 -371
  56. data/plans/over_engineering_analysis.md +0 -122
  57. data/plans/schema_management.md +0 -383
  58. data/sig/sequel/duckdb.rbs +0 -6
@@ -1,333 +0,0 @@
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).