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,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.
|