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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.beads-credential-key +1 -0
  3. data/.beads/.gitignore +66 -0
  4. data/.beads/README.md +85 -0
  5. data/.beads/config.yaml +56 -0
  6. data/.beads/hooks/post-checkout +24 -0
  7. data/.beads/hooks/post-merge +24 -0
  8. data/.beads/hooks/pre-commit +24 -0
  9. data/.beads/hooks/pre-push +24 -0
  10. data/.beads/hooks/prepare-commit-msg +24 -0
  11. data/.beads/metadata.json +7 -0
  12. data/.kiro/specs/advanced-sql-features-implementation/design.md +3 -1
  13. data/.kiro/specs/advanced-sql-features-implementation/requirements.md +1 -1
  14. data/.kiro/specs/advanced-sql-features-implementation/tasks.md +5 -1
  15. data/.kiro/specs/duckdb-sql-syntax-compatibility/design.md +15 -1
  16. data/.kiro/specs/duckdb-sql-syntax-compatibility/requirements.md +1 -1
  17. data/.kiro/specs/duckdb-sql-syntax-compatibility/tasks.md +13 -0
  18. data/.kiro/specs/edge-cases-and-validation-fixes/requirements.md +1 -1
  19. data/.kiro/specs/integration-test-database-setup/requirements.md +1 -1
  20. data/.kiro/specs/sequel-duckdb-adapter/design.md +8 -1
  21. data/.kiro/specs/sequel-duckdb-adapter/requirements.md +10 -10
  22. data/.kiro/specs/sequel-duckdb-adapter/tasks.md +48 -3
  23. data/.kiro/specs/sql-expression-handling-fix/design.md +34 -1
  24. data/.kiro/specs/sql-expression-handling-fix/requirements.md +1 -1
  25. data/.kiro/specs/sql-expression-handling-fix/tasks.md +3 -0
  26. data/.kiro/specs/test-infrastructure-improvements/requirements.md +1 -1
  27. data/.kiro/steering/product.md +5 -1
  28. data/.kiro/steering/structure.md +1 -1
  29. data/.kiro/steering/tech.md +14 -1
  30. data/.kiro/steering/testing.md +22 -1
  31. data/.mdformat.toml +2 -0
  32. data/.release-please-manifest.json +3 -0
  33. data/.rubocop.yml +116 -58
  34. data/.rubocop_todo.yml +323 -0
  35. data/AGENTS.md +154 -0
  36. data/API_DOCUMENTATION.md +73 -49
  37. data/CHANGELOG.md +46 -10
  38. data/FINAL_STATUS.md +99 -0
  39. data/LICENSE +1 -1
  40. data/MIGRATION_EXAMPLES.md +1 -1
  41. data/PERFORMANCE_OPTIMIZATIONS.md +4 -1
  42. data/README.md +90 -1
  43. data/REFACTORING_SUMMARY.md +264 -0
  44. data/Rakefile +21 -5
  45. data/TASK_10.2_IMPLEMENTATION_SUMMARY.md +19 -1
  46. data/docs/DUCKDB_SQL_PATTERNS.md +39 -1
  47. data/docs/TASK_12_VERIFICATION_SUMMARY.md +14 -1
  48. data/justfile +52 -0
  49. data/lib/sequel/adapters/duckdb.rb +137 -108
  50. data/lib/sequel/adapters/shared/duckdb.rb +292 -1490
  51. data/lib/sequel/duckdb/helpers/copier.rb +50 -0
  52. data/lib/sequel/duckdb/helpers/pathifier.rb +141 -0
  53. data/lib/sequel/duckdb/version.rb +5 -2
  54. data/plans/date_arithmetic.md +420 -0
  55. data/plans/engineering/Sequel.md +471 -0
  56. data/plans/engineering/duckdb.md +712 -0
  57. data/plans/engineering/sqlite.md +453 -0
  58. data/plans/mock_connection_bug.md +333 -0
  59. data/plans/mock_without_driver_gem.md +371 -0
  60. data/plans/over_engineering_analysis.md +122 -0
  61. data/plans/schema_management.md +383 -0
  62. data/release-please-config.json +14 -0
  63. metadata +49 -27
@@ -0,0 +1,471 @@
1
+ # Sequel Core Architecture - Engineering Insights
2
+
3
+ ## Overview
4
+
5
+ Sequel is a sophisticated Ruby ORM with a well-designed plugin architecture. Understanding its core mechanisms is essential for implementing adapters that leverage built-in functionality rather than reinventing the wheel.
6
+
7
+ ## Key Design Principles
8
+
9
+ 1. **Separation of Concerns**: Database vs Dataset responsibilities
10
+ 2. **Extension Points**: Minimal required overrides, extensive optional hooks
11
+ 3. **Battle-tested Utilities**: Logging, error handling, connection pooling built-in
12
+ 4. **Declarative Configuration**: Prefer data structures over procedural code
13
+
14
+ ## Core Components
15
+
16
+ ### 1. Logging System (`database/logging.rb`)
17
+
18
+ **Primary Method: `log_connection_yield(sql, conn, args=nil)`**
19
+
20
+ This is the ONE method adapters should use for all SQL execution logging. It handles:
21
+
22
+ - Timing (via `Sequel.start_timer` and `Sequel.elapsed_seconds_since`)
23
+ - Connection info (if `log_connection_info` is enabled)
24
+ - Arguments logging
25
+ - Exception logging (via `log_exception`)
26
+ - Duration logging (via `log_duration`)
27
+ - Slow query warnings (via `log_warn_duration` threshold)
28
+
29
+ **Pattern:**
30
+
31
+ ```ruby
32
+ log_connection_yield(sql, conn, log_args) do
33
+ conn.execute(sql, args)
34
+ end
35
+ ```
36
+
37
+ **Why Use It:**
38
+
39
+ - Consistent logging format across all adapters
40
+ - Automatic timing without manual Time.now calls
41
+ - Built-in slow query detection
42
+ - Respects user's log level configuration
43
+ - Zero code if logging is disabled
44
+
45
+ **Configuration:**
46
+
47
+ - `loggers` - Array of logger objects (empty = no logging)
48
+ - `sql_log_level` - :info (default) or :debug
49
+ - `log_warn_duration` - Numeric threshold for warnings
50
+ - `log_connection_info` - Boolean for connection ID in logs
51
+
52
+ **Helper Methods (rarely needed):**
53
+
54
+ - `log_info(message, args=nil)` - Log at info level
55
+ - `log_exception(exception, message)` - Log exceptions
56
+ - `log_duration(duration, message)` - Log with timing
57
+ - `log_connection_execute(conn, sql)` - For transaction commands
58
+
59
+ ### 2. Error Handling System (`database/misc.rb`, `exceptions.rb`)
60
+
61
+ **Exception Hierarchy:**
62
+
63
+ ```
64
+ Sequel::Error (base)
65
+ ├── Sequel::DatabaseError (generic database errors)
66
+ │ ├── Sequel::DatabaseConnectionError
67
+ │ ├── Sequel::DatabaseDisconnectError
68
+ │ ├── Sequel::ConstraintViolation
69
+ │ │ ├── Sequel::CheckConstraintViolation
70
+ │ │ ├── Sequel::ForeignKeyConstraintViolation
71
+ │ │ ├── Sequel::NotNullConstraintViolation
72
+ │ │ └── Sequel::UniqueConstraintViolation
73
+ │ ├── Sequel::SerializationFailure
74
+ │ └── Sequel::DatabaseLockTimeout
75
+ ```
76
+
77
+ **Primary Method: `raise_error(exception, opts=OPTS)`**
78
+
79
+ This converts driver exceptions to Sequel exceptions. Never raise Sequel exceptions directly.
80
+
81
+ **Pattern:**
82
+
83
+ ```ruby
84
+ def _execute(type, sql, opts, &block)
85
+ synchronize(opts[:server]) do |conn|
86
+ log_connection_yield(sql, conn) do
87
+ conn.execute(sql)
88
+ end
89
+ end
90
+ rescue DriverException => e
91
+ raise_error(e, opts)
92
+ end
93
+ ```
94
+
95
+ **Error Classification Methods (override these):**
96
+
97
+ **1. `database_error_regexps` - Declarative Regex Matching**
98
+
99
+ Return a Hash/Enumerable of `[regexp, exception_class]` pairs:
100
+
101
+ ```ruby
102
+ DATABASE_ERROR_REGEXPS = {
103
+ /unique.*constraint/i => UniqueConstraintViolation,
104
+ /foreign.*key/i => ForeignKeyConstraintViolation,
105
+ /not.*null/i => NotNullConstraintViolation,
106
+ /check.*constraint/i => CheckConstraintViolation,
107
+ /constraint/i => ConstraintViolation, # Generic, put last
108
+ }.freeze
109
+
110
+ def database_error_regexps
111
+ DATABASE_ERROR_REGEXPS
112
+ end
113
+ ```
114
+
115
+ **2. `database_specific_error_class(exception, opts)` - Error Code Matching**
116
+
117
+ For databases with numeric error codes:
118
+
119
+ ```ruby
120
+ def database_specific_error_class(exception, opts)
121
+ case error_code(exception)
122
+ when 1299
123
+ NotNullConstraintViolation
124
+ when 1555, 2067, 2579
125
+ UniqueConstraintViolation
126
+ when 787
127
+ ForeignKeyConstraintViolation
128
+ else
129
+ super # Falls back to regex matching
130
+ end
131
+ end
132
+ ```
133
+
134
+ **3. `database_exception_sqlstate(exception, opts)` - SQL State Codes**
135
+
136
+ For databases supporting ANSI SQL state codes:
137
+
138
+ ```ruby
139
+ def database_exception_sqlstate(exception, opts)
140
+ exception.sqlstate if exception.respond_to?(:sqlstate)
141
+ end
142
+ ```
143
+
144
+ Standard SQL State mappings are built-in:
145
+
146
+ - '23502' → NotNullConstraintViolation
147
+ - '23503', '23506', '23504' → ForeignKeyConstraintViolation
148
+ - '23505' → UniqueConstraintViolation
149
+ - '23513', '23514' → CheckConstraintViolation
150
+ - '40001' → SerializationFailure
151
+
152
+ **4. `disconnect_error?(exception, opts)` - Connection Failures**
153
+
154
+ Identifies errors that should trigger connection removal from pool:
155
+
156
+ ```ruby
157
+ def disconnect_error?(exception, opts)
158
+ opts[:disconnect] || # Explicit flag
159
+ !conn.ping || # Connection test failed
160
+ (exception.message =~ /connection.*closed/i)
161
+ end
162
+ ```
163
+
164
+ **Best Practices:**
165
+
166
+ - Use error codes when available (most reliable)
167
+ - Fall back to SQLState codes
168
+ - Use regex matching as last resort
169
+ - Put specific patterns before generic ones
170
+ - Always call `super` if no match found
171
+
172
+ ### 3. Connection Management (`database/connecting.rb`)
173
+
174
+ **Primary Method: `synchronize(server=nil, &block)`**
175
+
176
+ Acquires connection from pool, yields it, returns it automatically:
177
+
178
+ ```ruby
179
+ synchronize(opts[:server]) do |conn|
180
+ # Connection is automatically managed
181
+ conn.execute(sql)
182
+ end
183
+ # Connection returned to pool here
184
+ ```
185
+
186
+ **Never** access `@pool` directly. Always use `synchronize`.
187
+
188
+ **Methods to Override:**
189
+
190
+ **1. `connect(server)` - REQUIRED**
191
+
192
+ ```ruby
193
+ def connect(server)
194
+ opts = server_opts(server)
195
+ # Return driver connection object
196
+ DriverLib::Database.open(opts[:database])
197
+ end
198
+ ```
199
+
200
+ **2. `disconnect_connection(conn)` - OPTIONAL**
201
+
202
+ ```ruby
203
+ def disconnect_connection(conn)
204
+ conn.close
205
+ end
206
+ # Default is conn.close, override if different
207
+ ```
208
+
209
+ **3. `valid_connection?(conn)` - OPTIONAL**
210
+
211
+ ```ruby
212
+ def valid_connection?(conn)
213
+ conn.execute("SELECT 1")
214
+ true
215
+ rescue
216
+ false
217
+ end
218
+ # Default executes valid_connection_sql
219
+ ```
220
+
221
+ **Lifecycle Hooks:**
222
+
223
+ - `new_connection(server)` - Wraps `connect`, adds initialization
224
+ - `:after_connect` proc - User-configurable hook
225
+ - `:connect_sqls` - Array of SQL to execute on new connections
226
+
227
+ ### 4. Execution Pattern
228
+
229
+ **Standard `_execute` Pattern (SQLite example):**
230
+
231
+ ```ruby
232
+ def execute(sql, opts=OPTS, &block)
233
+ _execute(:select, sql, opts, &block)
234
+ end
235
+
236
+ def execute_dui(sql, opts=OPTS)
237
+ _execute(:update, sql, opts)
238
+ end
239
+
240
+ def execute_insert(sql, opts=OPTS)
241
+ _execute(:insert, sql, opts)
242
+ end
243
+
244
+ private
245
+
246
+ def _execute(type, sql, opts, &block)
247
+ synchronize(opts[:server]) do |conn|
248
+ # Handle prepared statements if sql is Symbol
249
+ return execute_prepared_statement(conn, type, sql, opts, &block) if sql.is_a?(Symbol)
250
+
251
+ # Extract arguments
252
+ log_args = opts[:arguments]
253
+ args = {}
254
+ opts.fetch(:arguments, OPTS).each{|k, v| args[k] = prepared_statement_argument(v)}
255
+
256
+ # Execute based on type
257
+ case type
258
+ when :select
259
+ log_connection_yield(sql, conn, log_args){conn.query(sql, args, &block)}
260
+ when :insert
261
+ log_connection_yield(sql, conn, log_args){conn.execute(sql, args)}
262
+ conn.last_insert_row_id
263
+ when :update
264
+ log_connection_yield(sql, conn, log_args){conn.execute_batch(sql, args)}
265
+ conn.changes
266
+ end
267
+ end
268
+ rescue DriverException => e
269
+ raise_error(e, opts)
270
+ end
271
+
272
+ def database_error_classes
273
+ [DriverException]
274
+ end
275
+ ```
276
+
277
+ **Key Points:**
278
+
279
+ - Use `synchronize` for connection pooling
280
+ - Use `log_connection_yield` for logging/timing
281
+ - Use `raise_error` for exception conversion
282
+ - Handle prepared statements (Symbol sql) if supported
283
+ - Return appropriate values (row count, insert ID, etc.)
284
+
285
+ ## Adapter Responsibilities
286
+
287
+ ### Database Class
288
+
289
+ **MUST Override:**
290
+
291
+ - `connect(server)` - Create and return connection
292
+ - `dataset_class_default` - Return Dataset subclass
293
+
294
+ **SHOULD Override:**
295
+
296
+ - `database_type` - Return :postgres, :mysql, :sqlite, :duckdb, etc.
297
+ - `database_error_classes` - Array of driver exception classes
298
+ - `database_error_regexps` - Hash of error patterns
299
+ - `database_specific_error_class(exception, opts)` - Error code mapping
300
+
301
+ **MAY Override:**
302
+
303
+ - `disconnect_connection(conn)` - Close connection (default: conn.close)
304
+ - `valid_connection?(conn)` - Test if alive (default: SELECT NULL)
305
+ - `begin_new_transaction(conn, opts)` - Custom BEGIN syntax
306
+ - `commit_transaction(conn, opts)` - Custom COMMIT syntax
307
+ - `rollback_transaction(conn, opts)` - Custom ROLLBACK syntax
308
+ - Feature detection: `supports_savepoints?`, `supports_returning?`, etc.
309
+
310
+ ### Dataset Class
311
+
312
+ **MUST Override:**
313
+
314
+ - `fetch_rows(sql, &block)` - Execute SQL, yield hash rows
315
+
316
+ **Pattern:**
317
+
318
+ ```ruby
319
+ def fetch_rows(sql)
320
+ execute(sql) do |result|
321
+ # Set self.columns with column names
322
+ cols = result.columns.map{|c| output_identifier(c)}
323
+ self.columns = cols
324
+
325
+ # Yield hash rows
326
+ result.each do |row_array|
327
+ row_hash = {}
328
+ cols.each_with_index{|col, i| row_hash[col] = row_array[i]}
329
+ yield row_hash
330
+ end
331
+ end
332
+ end
333
+ ```
334
+
335
+ **SHOULD Override:**
336
+
337
+ - `literal_*` methods - For database-specific literal formatting
338
+ - `select_sql`, `insert_sql`, `update_sql`, `delete_sql` - For custom SQL syntax
339
+
340
+ ## Real vs Shared Adapters
341
+
342
+ ### Real Adapter (`adapters/database_name.rb`)
343
+
344
+ **Purpose:** Driver-specific code that varies by Ruby driver
345
+
346
+ **Contents:**
347
+
348
+ - Driver gem require
349
+ - Type conversion procs (if needed)
350
+ - Database class with connection/execution
351
+ - Dataset class with fetch_rows
352
+ - Driver-specific workarounds
353
+
354
+ **Example:** SQLite has three real adapters:
355
+
356
+ - `sqlite.rb` - For sqlite3 gem
357
+ - `jdbc/sqlite.rb` - For JDBC on JRuby
358
+ - `tinytds.rb` - Different driver
359
+
360
+ ### Shared Adapter (`adapters/shared/database_name.rb`)
361
+
362
+ **Purpose:** Database-specific code that applies to all drivers
363
+
364
+ **Contents:**
365
+
366
+ - DatabaseMethods module
367
+ - DatasetMethods module
368
+ - Schema operations
369
+ - SQL generation
370
+ - Feature detection
371
+ - Error classification
372
+
373
+ **Pattern:**
374
+
375
+ ```ruby
376
+ module Sequel
377
+ module DatabaseName
378
+ Sequel::Database.set_shared_adapter_scheme(:database_name, self)
379
+
380
+ module DatabaseMethods
381
+ # Shared database features
382
+ end
383
+
384
+ module DatasetMethods
385
+ # Shared SQL generation
386
+ end
387
+ end
388
+ end
389
+ ```
390
+
391
+ ## Anti-Patterns to Avoid
392
+
393
+ 1. **Don't Reinvent Logging**
394
+
395
+ - ❌ `log_info("SQL: #{sql}")`
396
+ - ✅ `log_connection_yield(sql, conn) { execute }`
397
+
398
+ 2. **Don't Manually Time Operations**
399
+
400
+ - ❌ `start = Time.now; execute; Time.now - start`
401
+ - ✅ `log_connection_yield` handles timing
402
+
403
+ 3. **Don't Build Error Messages**
404
+
405
+ - ❌ `raise DatabaseError, "Error: #{e.message} SQL: #{sql}"`
406
+ - ✅ `raise_error(e, opts)` # Sequel formats it
407
+
408
+ 4. **Don't Manually Classify Errors**
409
+
410
+ - ❌ 45-line case statement in execute
411
+ - ✅ `database_error_regexps` hash or `database_specific_error_class`
412
+
413
+ 5. **Don't Access Pool Directly**
414
+
415
+ - ❌ `@pool.hold { |conn| ... }`
416
+ - ✅ `synchronize { |conn| ... }`
417
+
418
+ 6. **Don't Put Driver Code in Shared Adapter**
419
+
420
+ - Real adapter: Connection, execution, driver specifics
421
+ - Shared adapter: SQL generation, schema operations, features
422
+
423
+ ## Performance Considerations
424
+
425
+ Sequel's built-in mechanisms are highly optimized:
426
+
427
+ 1. **Connection Pooling**: Thread-safe, tested, configurable
428
+ 2. **Logging**: Skip-checks minimize overhead when disabled
429
+ 3. **Error Handling**: Exception class checking is fast
430
+ 4. **SQL Generation**: Cached where possible
431
+
432
+ Don't optimize prematurely. Use built-in features first, profile later.
433
+
434
+ ## Testing Support
435
+
436
+ Sequel provides `Sequel.mock` for testing adapters without database:
437
+
438
+ ```ruby
439
+ db = Sequel.mock(host: :duckdb)
440
+ ```
441
+
442
+ Adapters should implement `mock_adapter_setup` in shared module:
443
+
444
+ ```ruby
445
+ def self.mock_adapter_setup(db)
446
+ db.instance_exec do
447
+ def schema_parse_table(*)
448
+ []
449
+ end
450
+ singleton_class.send(:private, :schema_parse_table)
451
+ end
452
+ end
453
+ ```
454
+
455
+ ## Summary
456
+
457
+ **Use Sequel's Features:**
458
+
459
+ - `log_connection_yield` for all execution
460
+ - `raise_error` for all exception handling
461
+ - `synchronize` for all connection access
462
+ - `database_error_regexps` for error classification
463
+
464
+ **Implement Minimally:**
465
+
466
+ - `connect` and `dataset_class_default` (required)
467
+ - `_execute` pattern (20 lines max)
468
+ - Error classification (declarative, < 50 lines)
469
+ - Schema operations (database-specific)
470
+
471
+ **Result:** Simple, maintainable adapters that work like all other Sequel adapters.