sequel-duckdb 0.1.0 → 0.2.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.
- 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/.rubocop.yml +116 -58
- data/.rubocop_todo.yml +323 -0
- data/AGENTS.md +180 -0
- data/API_DOCUMENTATION.md +73 -49
- data/CHANGELOG.md +47 -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 +50 -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 +2 -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
- metadata +47 -27
|
@@ -0,0 +1,453 @@
|
|
|
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.
|