-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Summary
dart_node_better_sqlite3 wraps roughly 20% of the better-sqlite3 npm API. The biggest gap is the missing db.transaction() wrapper — without it, error-safe transactional code requires manual BEGIN/COMMIT/ROLLBACK with no automatic rollback on exceptions. Large result sets must be loaded entirely into memory (no iterate()), and only positional parameters are supported (no named $param / :param).
What exists today
openDatabase(path)— opens DB, auto-enables WAL mode + busy timeoutdb.prepare(sql)— returnsStatementdb.exec(sql)— raw SQL executiondb.close()— close connectiondb.pragma(value)— set pragmasdb.isOpen— check if openstmt.run([params])— execute withchanges+lastInsertRowidstmt.get([params])— fetch first row asMap<String, Object?>stmt.all([params])— fetch all rows asList<Map<String, Object?>>
All public methods return Result<T, String>. This is solid.
What's missing — grouped by priority
P0: Production hazards
db.transaction(fn) — automatic transaction wrapper
File to reference: packages/dart_node_better_sqlite3/lib/src/database.dart
The better-sqlite3 transaction() method provides:
- Automatic
BEGINbefore the function runs - Automatic
COMMITif the function returns normally - Automatic
ROLLBACKif the function throws - Nested transaction support via savepoints
.deferred(),.immediate(),.exclusive()modes
Current workaround:
db.exec('BEGIN');
// If this throws, the transaction is left DANGLING
// No automatic rollback, connection is in a broken state
final result = stmt.run(params);
db.exec('COMMIT');Impact: In production, any exception between BEGIN and COMMIT leaves a dangling transaction. The connection is stuck in a transaction state, and subsequent operations may fail silently or see stale data. The too_many_cooks example uses this manual pattern (see examples/too_many_cooks/src/db.dart) and is exposed to this risk.
What to implement:
Result<T, String> transaction<T>(T Function() fn) {
// Wrap better-sqlite3's transaction() method
// Automatic BEGIN/COMMIT/ROLLBACK
}stmt.iterate([params]) — row iterator for large results
Impact: all() loads every row into memory. A query returning 100k rows will consume significant memory and potentially OOM. iterate() returns one row at a time via an iterator pattern.
What to implement:
Result<Iterable<Map<String, Object?>>, String> iterate(
Statement stmt, [List<Object?>? params]
)P1: Standard SQLite usage patterns
Named parameters ($name, :name, @name)
Currently only positional ? parameters are supported because params are passed as a List<Object?>.
What to implement: Accept Map<String, Object?> for named parameters:
// Current (positional only):
stmt.get([userId])
// Needed (named):
stmt.get({r'$userId': userId, r'$status': 'active'})Named parameters are standard practice in SQLite. Most real-world queries use them for readability and safety.
Database constructor options
openDatabase() accepts only a path string. Missing options:
readonly: true— open read-only (important for replicas, read-heavy workloads)fileMustExist: true— don't create DB if missing (catches deployment bugs)timeout: ms— custom busy timeout (currently hardcoded to 5000ms)verbose: fn— debug logging
Structured error types
Errors are returned as String. You cannot programmatically distinguish:
- "table not found" vs "syntax error" vs "database locked" vs "constraint violation"
What to implement: A typed error enum or sealed class:
sealed class SqliteError {
case syntaxError(String sql, String message);
case constraintViolation(String constraint, String message);
case databaseLocked(String message);
case ioError(String message);
// etc.
}P2: Power user features
stmt.columns()— column metadata (name, type, table) for introspectionstmt.bind([params])— pre-bind parameters for repeated executionstmt.raw()— return rows as arrays instead of maps (faster)stmt.pluck()— return single column value instead of map (convenient)db.function(name, fn)— register custom SQL functionsdb.aggregate(name, options)— register custom aggregate functionsdb.backup(filename)— online backupdb.serialize()— serialize database to Bufferdb.loadExtension(path)— load SQLite extensionsdb.table(name, factory)— virtual table supportstmt.reader/stmt.readonly/stmt.source— statement metadatadb.defaultSafeIntegers()— BigInt support for large rowids
Test gaps
File: packages/dart_node_better_sqlite3/test/database_test.dart (23 tests)
Well tested:
- CRUD operations (INSERT, SELECT, UPDATE, DELETE)
- Data types (integer, real, text, null, large integers)
- Error cases (invalid path, invalid SQL, constraint violations)
- Manual BEGIN/COMMIT and BEGIN/ROLLBACK
- Parameterized queries
Not tested:
- BLOB data type — schema defines
blob_colbut no test reads/writes blob data - Multiple parameterized values in a single query
- Concurrent access / multiple connections to same file
- Reusing a statement across multiple calls
- Using a closed database (does it return proper Error result?)
- Using a statement after its database is closed
- Very large result sets (memory behavior)
- WAL mode actually being set (verify with PRAGMA)
- Busy timeout behavior under contention
Files to modify
packages/dart_node_better_sqlite3/lib/src/database.dart— addtransaction(), database options, structured errorspackages/dart_node_better_sqlite3/lib/src/statement.dart— additerate(), named params,columns(),raw(),pluck()packages/dart_node_better_sqlite3/lib/src/errors.dart— new file for structured error typespackages/dart_node_better_sqlite3/test/database_test.dart— add tests for new features + missing edge cases