sequel-duckdb 0.2.1 → 0.3.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/lib/sequel/adapters/duckdb.rb +3 -3
  4. data/lib/sequel/adapters/shared/duckdb.rb +17 -17
  5. data/lib/sequel/duckdb/version.rb +1 -1
  6. metadata +21 -60
  7. data/.beads/.beads-credential-key +0 -1
  8. data/.beads/.gitignore +0 -66
  9. data/.beads/README.md +0 -85
  10. data/.beads/config.yaml +0 -56
  11. data/.beads/hooks/post-checkout +0 -24
  12. data/.beads/hooks/post-merge +0 -24
  13. data/.beads/hooks/pre-commit +0 -24
  14. data/.beads/hooks/pre-push +0 -24
  15. data/.beads/hooks/prepare-commit-msg +0 -24
  16. data/.beads/metadata.json +0 -7
  17. data/.kiro/specs/advanced-sql-features-implementation/design.md +0 -26
  18. data/.kiro/specs/advanced-sql-features-implementation/requirements.md +0 -43
  19. data/.kiro/specs/advanced-sql-features-implementation/tasks.md +0 -28
  20. data/.kiro/specs/duckdb-sql-syntax-compatibility/design.md +0 -272
  21. data/.kiro/specs/duckdb-sql-syntax-compatibility/requirements.md +0 -84
  22. data/.kiro/specs/duckdb-sql-syntax-compatibility/tasks.md +0 -107
  23. data/.kiro/specs/edge-cases-and-validation-fixes/requirements.md +0 -32
  24. data/.kiro/specs/integration-test-database-setup/design.md +0 -0
  25. data/.kiro/specs/integration-test-database-setup/requirements.md +0 -117
  26. data/.kiro/specs/sequel-duckdb-adapter/design.md +0 -549
  27. data/.kiro/specs/sequel-duckdb-adapter/requirements.md +0 -202
  28. data/.kiro/specs/sequel-duckdb-adapter/tasks.md +0 -292
  29. data/.kiro/specs/sql-expression-handling-fix/design.md +0 -331
  30. data/.kiro/specs/sql-expression-handling-fix/requirements.md +0 -86
  31. data/.kiro/specs/sql-expression-handling-fix/tasks.md +0 -25
  32. data/.kiro/specs/test-infrastructure-improvements/requirements.md +0 -106
  33. data/.kiro/steering/product.md +0 -26
  34. data/.kiro/steering/structure.md +0 -88
  35. data/.kiro/steering/tech.md +0 -137
  36. data/.kiro/steering/testing.md +0 -213
  37. data/.mdformat.toml +0 -2
  38. data/.rubocop.yml +0 -161
  39. data/.rubocop_todo.yml +0 -323
  40. data/.yardopts +0 -8
  41. data/AGENTS.md +0 -180
  42. data/API_DOCUMENTATION.md +0 -943
  43. data/FINAL_STATUS.md +0 -99
  44. data/MIGRATION_EXAMPLES.md +0 -740
  45. data/PERFORMANCE_OPTIMIZATIONS.md +0 -726
  46. data/REFACTORING_SUMMARY.md +0 -264
  47. data/Rakefile +0 -43
  48. data/TASK_10.2_IMPLEMENTATION_SUMMARY.md +0 -182
  49. data/docs/DUCKDB_SQL_PATTERNS.md +0 -448
  50. data/docs/TASK_12_VERIFICATION_SUMMARY.md +0 -135
  51. data/justfile +0 -50
  52. data/plans/date_arithmetic.md +0 -420
  53. data/plans/engineering/Sequel.md +0 -471
  54. data/plans/engineering/duckdb.md +0 -712
  55. data/plans/engineering/sqlite.md +0 -453
  56. data/plans/mock_connection_bug.md +0 -333
  57. data/plans/mock_without_driver_gem.md +0 -371
  58. data/plans/over_engineering_analysis.md +0 -122
  59. data/plans/schema_management.md +0 -383
  60. data/sig/sequel/duckdb.rbs +0 -6
@@ -1,453 +0,0 @@
1
- # SQLite Adapter - Engineering Insights
2
-
3
- ## Overview
4
-
5
- The SQLite adapter is a masterclass in simplicity and proper use of Sequel's built-in features. Written by Jeremy Evans (Sequel's author), it demonstrates the "right way" to implement a Sequel adapter.
6
-
7
- **File Structure:**
8
-
9
- - `adapters/sqlite.rb` (462 lines) - Real adapter
10
- - `adapters/shared/sqlite.rb` (1074 lines) - Shared adapter
11
- - **Total: ~1536 lines**
12
-
13
- ## Real Adapter Analysis (`adapters/sqlite.rb`)
14
-
15
- ### Type Conversion System (Lines 1-79)
16
-
17
- SQLite stores everything as strings/blobs, so type conversion is critical.
18
-
19
- **Pattern:** Callable objects in a hash
20
-
21
- ```ruby
22
- boolean = Object.new
23
- def boolean.call(s)
24
- s = s.downcase if s.is_a?(String)
25
- !FALSE_VALUES.include?(s)
26
- end
27
-
28
- SQLITE_TYPES = {
29
- 'bool' => boolean,
30
- 'boolean' => boolean,
31
- 'integer' => integer,
32
- 'date' => date,
33
- # ...
34
- }.freeze
35
- ```
36
-
37
- **Why:** Fast lookup, easy to extend, frozen for thread-safety
38
-
39
- ### Database Class (Lines 85-354)
40
-
41
- **Connection (Lines 128-154):**
42
-
43
- ```ruby
44
- def connect(server)
45
- opts = server_opts(server)
46
- opts[:database] = ':memory:' if blank_object?(opts[:database])
47
-
48
- db = ::SQLite3::Database.new(opts[:database].to_s, sqlite3_opts)
49
- db.busy_timeout(typecast_value_integer(opts.fetch(:timeout, 5000)))
50
- db.extended_result_codes = true if USE_EXTENDED_RESULT_CODES
51
-
52
- # Apply connection pragmas using log_connection_yield
53
- connection_pragmas.each{|s| log_connection_yield(s, db){db.execute_batch(s)}}
54
-
55
- db
56
- end
57
- ```
58
-
59
- **Key Points:**
60
-
61
- - Returns raw driver connection object
62
- - Configuration via pragmas, not API calls
63
- - Uses `log_connection_yield` even for setup
64
- - No error handling here - let `raise_error` catch it
65
-
66
- **Execution (Lines 169-270):**
67
-
68
- The entire execution system is **ONE method**:
69
-
70
- ```ruby
71
- def execute(sql, opts=OPTS, &block)
72
- _execute(:select, sql, opts, &block)
73
- end
74
-
75
- def execute_dui(sql, opts=OPTS)
76
- _execute(:update, sql, opts)
77
- end
78
-
79
- def execute_insert(sql, opts=OPTS)
80
- _execute(:insert, sql, opts)
81
- end
82
-
83
- private
84
-
85
- def _execute(type, sql, opts, &block)
86
- synchronize(opts[:server]) do |conn|
87
- return execute_prepared_statement(conn, type, sql, opts, &block) if sql.is_a?(Symbol)
88
-
89
- log_args = opts[:arguments]
90
- args = {}
91
- opts.fetch(:arguments, OPTS).each{|k, v| args[k] = prepared_statement_argument(v)}
92
-
93
- case type
94
- when :select
95
- log_connection_yield(sql, conn, log_args){conn.query(sql, args, &block)}
96
- when :insert
97
- log_connection_yield(sql, conn, log_args){conn.execute(sql, args)}
98
- conn.last_insert_row_id
99
- when :update
100
- log_connection_yield(sql, conn, log_args){conn.execute_batch(sql, args)}
101
- conn.changes
102
- end
103
- end
104
- rescue SQLite3::Exception => e
105
- raise_error(e)
106
- end
107
- ```
108
-
109
- **What It Does:**
110
-
111
- 1. `synchronize` - Get connection from pool
112
- 2. Check for prepared statement (Symbol sql)
113
- 3. Extract arguments
114
- 4. Dispatch by type
115
- 5. Use `log_connection_yield` for ALL execution
116
- 6. Return type-appropriate value
117
- 7. Rescue and use `raise_error` for conversion
118
-
119
- **What It Doesn't Do:**
120
-
121
- - No manual timing (log_connection_yield does it)
122
- - No manual logging (log_connection_yield does it)
123
- - No manual error classification (raise_error does it)
124
- - No try/catch/ensure complexity
125
- - No connection management (synchronize does it)
126
-
127
- **LOC: 20 lines of actual logic**
128
-
129
- ### Dataset Class (Lines 356-459)
130
-
131
- **Type Conversion During Fetch (Lines 406-428):**
132
-
133
- ```ruby
134
- def fetch_rows(sql)
135
- execute(sql) do |result|
136
- cps = db.conversion_procs
137
- type_procs = result.types.map{|t| cps[base_type_name(t)]}
138
- j = -1
139
- cols = result.columns.map{|c| [output_identifier(c), type_procs[(j+=1)]]}
140
- self.columns = cols.map(&:first)
141
- max = cols.length
142
-
143
- result.each do |values|
144
- row = {}
145
- i = -1
146
- while (i += 1) < max
147
- name, type_proc = cols[i]
148
- v = values[i]
149
- v = type_proc.call(v) if type_proc && v
150
- row[name] = v
151
- end
152
- yield row
153
- end
154
- end
155
- end
156
- ```
157
-
158
- **Pattern:**
159
-
160
- 1. Build type conversion proc array from column types
161
- 2. Build column name array with output identifiers
162
- 3. Set `self.columns` for Sequel
163
- 4. For each row: convert values and yield hash
164
-
165
- **Why:** Efficient - builds conversion procs once, applies many times
166
-
167
- ## Shared Adapter Analysis (`adapters/shared/sqlite.rb`)
168
-
169
- ### DatabaseMethods (Lines 24-580)
170
-
171
- **Configuration (Lines 24-65):**
172
-
173
- - Transaction modes (deferred/immediate/exclusive)
174
- - Integer booleans setting
175
- - UTC timestamp setting
176
- - All via accessors, no complex logic
177
-
178
- **Schema Introspection (Lines 69-187):**
179
-
180
- - Uses SQLite PRAGMA statements
181
- - `foreign_key_list` - PRAGMA foreign_key_list
182
- - `indexes` - PRAGMA index_list + index_info
183
- - `tables` - Query sqlite_master
184
- - Clean, simple queries
185
-
186
- **Error Classification (Lines 372-404):**
187
-
188
- **The Entire System:**
189
-
190
- ```ruby
191
- DATABASE_ERROR_REGEXPS = {
192
- /(is|are) not unique\z|PRIMARY KEY must be unique\z|UNIQUE constraint failed: .+\z/ => UniqueConstraintViolation,
193
- /foreign key constraint failed\z/i => ForeignKeyConstraintViolation,
194
- /\ASQLITE ERROR 3091/ => CheckConstraintViolation,
195
- /\A(SQLITE ERROR 275 \(CONSTRAINT_CHECK\) : )?CHECK constraint failed/ => CheckConstraintViolation,
196
- /\A(SQLITE ERROR 19 \(CONSTRAINT\) : )?constraint failed\z/ => ConstraintViolation,
197
- /\Acannot store [A-Z]+ value in [A-Z]+ column / => ConstraintViolation,
198
- /may not be NULL\z|NOT NULL constraint failed: .+\z/ => NotNullConstraintViolation,
199
- /\ASQLITE ERROR \d+ \(\) : CHECK constraint failed: / => CheckConstraintViolation
200
- }.freeze
201
-
202
- def database_error_regexps
203
- DATABASE_ERROR_REGEXPS
204
- end
205
-
206
- def database_specific_error_class(exception, opts)
207
- case sqlite_error_code(exception)
208
- when 1299 then NotNullConstraintViolation
209
- when 1555, 2067, 2579 then UniqueConstraintViolation
210
- when 787 then ForeignKeyConstraintViolation
211
- when 275 then CheckConstraintViolation
212
- when 19 then ConstraintViolation
213
- when 517 then SerializationFailure
214
- else
215
- super # Falls back to regex matching
216
- end
217
- end
218
- ```
219
-
220
- **That's it. 33 lines total. Handles all error cases.**
221
-
222
- **Pattern:**
223
-
224
- 1. Try error codes first (most reliable)
225
- 2. Fall back to regex (for older sqlite3 gem versions)
226
- 3. Always call `super` for unmatched cases
227
-
228
- **ALTER TABLE Support (Lines 237-294):**
229
-
230
- SQLite has limited ALTER TABLE support, so the adapter implements `duplicate_table` pattern:
231
-
232
- 1. Rename table to backup
233
- 2. Create new table with changes
234
- 3. Copy data from backup to new table
235
- 4. Drop backup table
236
-
237
- This is complex but isolated to one method. Good separation of concerns.
238
-
239
- ### DatasetMethods (Lines 582-1072)
240
-
241
- **SQL Generation Customizations:**
242
-
243
- - CURRENT_TIMESTAMP in UTC → convert to localtime
244
- - LIKE operator → no ESCAPE clause needed
245
- - Exponentiation → emulate with multiplication
246
- - Extract → use strftime
247
- - Multi-row VALUES → support since 3.7.11
248
-
249
- **All handled via:**
250
-
251
- - Override specific `_sql` methods
252
- - Override `complex_expression_sql_append`
253
- - Override `literal_*` methods
254
-
255
- **No wholesale SQL generation rewrite.**
256
-
257
- **INSERT conflict resolution (Lines 771-792):**
258
-
259
- SQLite's unique INSERT OR IGNORE/REPLACE syntax:
260
-
261
- ```ruby
262
- def insert_conflict(opts = :ignore)
263
- case opts
264
- when Symbol, String
265
- unless INSERT_CONFLICT_RESOLUTIONS.include?(opts.to_s.upcase)
266
- raise Error, "Invalid value..."
267
- end
268
- clone(:insert_conflict => opts)
269
- when Hash
270
- clone(:insert_on_conflict => opts)
271
- else
272
- raise Error, "Invalid value..."
273
- end
274
- end
275
- ```
276
-
277
- Then in SQL generation:
278
-
279
- ```ruby
280
- def insert_conflict_sql(sql)
281
- if resolution = @opts[:insert_conflict]
282
- sql << " OR " << resolution.to_s.upcase
283
- end
284
- end
285
- ```
286
-
287
- **Clean separation:** Configuration via dataset options, generation via \_sql methods.
288
-
289
- ## Key Architectural Patterns
290
-
291
- ### 1. Minimal Real Adapter
292
-
293
- The real adapter contains ONLY:
294
-
295
- - Type conversions (SQLite-specific)
296
- - Connection management (sqlite3 gem API)
297
- - Execution dispatch (sqlite3 gem methods)
298
-
299
- No schema operations, no SQL generation.
300
-
301
- ### 2. Declarative Error Handling
302
-
303
- Error classification is DATA:
304
-
305
- ```ruby
306
- DATABASE_ERROR_REGEXPS = {
307
- /pattern/ => ExceptionClass,
308
- # ...
309
- }.freeze
310
- ```
311
-
312
- Not procedural code with 45-line case statements.
313
-
314
- ### 3. Single Execution Path
315
-
316
- One `_execute` method handles all SQL types. Benefits:
317
-
318
- - Single place for logging
319
- - Single place for connection management
320
- - Single place for error handling
321
- - Easy to understand and debug
322
-
323
- ### 4. Trust Sequel
324
-
325
- Don't reimplement:
326
-
327
- - Logging → `log_connection_yield`
328
- - Timing → `log_connection_yield`
329
- - Connection pooling → `synchronize`
330
- - Error conversion → `raise_error`
331
-
332
- These are battle-tested and optimized.
333
-
334
- ### 5. Override Only What's Different
335
-
336
- Dataset SQL generation:
337
-
338
- - Inherit 90% from Sequel::Dataset
339
- - Override only SQLite-specific syntax
340
- - Use specific methods: `literal_date`, `complex_expression_sql_append`
341
- - Don't rewrite `select_sql` wholesale
342
-
343
- ## Complexity Budget
344
-
345
- **Real Adapter Complexity:**
346
-
347
- - Connection: 15 lines
348
- - Execution: 20 lines
349
- - Dataset: 30 lines
350
- - Type conversion: 100 lines (necessary for SQLite)
351
-
352
- **Shared Adapter Complexity:**
353
-
354
- - Error handling: 33 lines (declarative)
355
- - Schema introspection: 150 lines (PRAGMA queries)
356
- - SQL generation overrides: ~200 lines (only what differs)
357
- - ALTER TABLE emulation: 250 lines (necessary for SQLite limitations)
358
-
359
- **Total Unique Code:** ~800 lines
360
- **Boilerplate/Comments:** ~700 lines
361
-
362
- ## Lessons for DuckDB Adapter
363
-
364
- ### What to Copy
365
-
366
- 1. **Execution pattern:**
367
-
368
- ```ruby
369
- def _execute(type, sql, opts, &block)
370
- synchronize(opts[:server]) do |conn|
371
- case type
372
- when :select
373
- log_connection_yield(sql, conn) { conn.query(sql, &block) }
374
- # ...
375
- end
376
- end
377
- rescue DuckDB::Error => e
378
- raise_error(e)
379
- end
380
- ```
381
-
382
- 2. **Error classification:**
383
-
384
- ```ruby
385
- DATABASE_ERROR_REGEXPS = {
386
- /unique.*constraint/i => UniqueConstraintViolation,
387
- # ...
388
- }.freeze
389
-
390
- def database_specific_error_class(exception, opts)
391
- case error_code(exception)
392
- when code then ExceptionClass
393
- else super
394
- end
395
- end
396
- ```
397
-
398
- 3. **Connection setup:**
399
-
400
- ```ruby
401
- def connect(server)
402
- opts = server_opts(server)
403
- db = DuckDB::Database.open(opts[:database])
404
- # Configure via SQL, not API:
405
- config_sqls.each{|s| log_connection_yield(s, db){db.execute(s)}}
406
- db
407
- end
408
- ```
409
-
410
- ### What NOT to Copy
411
-
412
- 1. **Duplicate table logic** - DuckDB likely supports proper ALTER TABLE
413
- 2. **Type conversion complexity** - DuckDB has better type support than SQLite
414
- 3. **Multi-row VALUES emulation** - DuckDB supports it natively
415
-
416
- ## File Size Breakdown
417
-
418
- ```
419
- Real Adapter (462 lines):
420
- - Type conversion: 100 lines
421
- - Connection: 50 lines
422
- - Execution: 100 lines
423
- - Dataset: 100 lines
424
- - Comments/whitespace: 112 lines
425
-
426
- Shared Adapter (1074 lines):
427
- - DatabaseMethods: 556 lines
428
- - Configuration: 50
429
- - Schema introspection: 150
430
- - ALTER TABLE: 250
431
- - Error handling: 33
432
- - Other: 73
433
- - DatasetMethods: 490 lines
434
- - SQL generation overrides: 300
435
- - Feature detection: 50
436
- - Helpers: 140
437
- - Comments/whitespace: 28 lines
438
- ```
439
-
440
- ## Summary
441
-
442
- **SQLite adapter is simple because it:**
443
-
444
- 1. Uses `log_connection_yield` for all execution
445
- 2. Uses `raise_error` for all error handling
446
- 3. Uses declarative error classification
447
- 4. Overrides only database-specific SQL
448
- 5. Trusts Sequel's built-in features
449
-
450
- **Total actual logic: ~800 lines**
451
- **No wheel reinvention: 0 lines**
452
-
453
- This is the gold standard for Sequel adapters.