Skill v1.0.0
currentAutomated scan73/100version: "1.0.0" name: sqflite-crud-and-transactions description: >- Use when reading or writing rows with package:sqflite: execute, insert, query, update, delete and their raw variants (rawInsert, rawQuery, rawUpdate, rawDelete), where/whereArgs binding, ConflictAlgorithm (upsert with replace/ignore), transaction (txn) and rollback, Batch commit/apply, queryCursor/queryIterate for large results, SqfliteSqlCommand, supported column types (int, num, String, Uint8List, no bool/DateTime), reserved-name escaping (escapeName), Sqflite.firstIntValue for COUNT(*), and DatabaseException handling (isUniqueConstraintError, isNoSuchTableError).
sqflite: CRUD, transactions and batches
Every SQL call goes through a DatabaseExecutor, which is either the Database or the Transaction passed to transaction(). Helpers (insert, query, update, delete) build the SQL for you; raw methods take a SQL string plus a positional ? argument list. Each call runs exactly one statement and is serialized on the database.
import 'package:sqflite/sqflite.dart';Future<int> addTodo(Database db, String title) =>db.insert('Todo', {'title': title, 'done': 0});Future<List<Map<String, Object?>>> pendingTodos(Database db) =>db.query('Todo', where: 'done = ?', whereArgs: [0], orderBy: 'id');
Guidelines
Statements and arguments
execute(sql, [args])is for DDL and statements without a result (`CREATE
TABLE, PRAGMA, DROP). One statement per call; ;`-separated strings are rejected.
insert(table, values, {nullColumnHack, conflictAlgorithm})returns the
inserted row id (rawInsert too). update(...) / delete(...) and rawUpdate / rawDelete return the number of rows changed. query(...) / rawQuery(...) return List<Map<String, Object?>>.
- Always bind values with
?and an argument list (whereArgsor the raw
arguments). Never interpolate user data into the SQL string. ?NNN positional references (?1, ?2) are supported.
IN (?)does not accept a list argument. Generate one?per value:
'id IN (${List.filled(ids.length, '?').join(',')})' with whereArgs: ids.
- Test for null with
IS NULL/IS NOT NULLin the SQL, not with= ?
and a null argument (whereArgs values must be non-null; raw arguments may contain null).
queryclauses (where,orderBy,groupBy,having,columns,
limit, offset, distinct) are raw SQL fragments without the keyword: orderBy: 'name COLLATE NOCASE DESC', where: 'a = ? AND b > ?'. having requires groupBy.
- Column values must be
int,num(REAL),String(TEXT),
Uint8List (BLOB) or null. bool is stored as INTEGER 0/1; DateTime as millisecondsSinceEpoch (INTEGER) or toIso8601String() (TEXT); nested maps/lists as a JSON TEXT. Passing another type prints a warning in debug mode and will throw in the future.
- Table and column names that are SQLite keywords (
group,order,
table, values, ...) are escaped automatically by the helpers (insert, query, update, delete and the columns list) but not inside where, orderBy or raw SQL: write '"group" = ?' there, or call escapeName(name) from package:sqflite/sql.dart. Prefer names that are not keywords.
- Query results are read-only: copy with
Map<String, Object?>.from(row)/
List.of(rows) before mutating. A row is a Map<String, Object?> keyed by column name (or alias); a COUNT(*) column is keyed 'COUNT(*)'.
Sqflite.firstIntValue(rows)(orfirstIntValuefrom
package:sqflite/utils/utils.dart) reads the first value of the first row as an int?, the idiom for SELECT COUNT(*). Sqflite.hex(bytes) builds the argument for 'hex(blob_column) = ?'.
ConflictAlgorithm(oninsertandupdate):replacedeletes the
conflicting row(s) and inserts (upsert by primary/unique key), ignore skips the row without error (insert then returns 0), abort (default), fail, rollback. Alternatively catch DatabaseException and check isUniqueConstraintError().
Transactions
db.transaction((txn) async {...})beginsBEGIN IMMEDIATE(`exclusive:
true for BEGIN EXCLUSIVE), runs the callback, then COMMIT; if the callback throws it runs ROLLBACK and rethrows. Return a value from the callback to get it from transaction<T>`.
- Inside the callback use only
txn. Any call ondb(or on another
Database object for the same file) waits for the transaction and deadlocks; after 10 s sqflite prints "Warning database has been locked".
- A caught exception inside the callback does not roll back: the transaction
commits with the successful statements. Rethrow (or throw your own error) to cancel.
- Transactions are exclusive and serialized: no concurrent read while one is
running. Keep them short and never await UI or network inside.
db.readTransaction(...)is experimental; on sqflite it is a normal
transaction that is always rolled back. Use transaction unless you are on an implementation that documents it.
Batches
- `final batch = db.batch(); batch.insert(...); batch.update(...);
final results = await batch.commit(); sends all operations in one native call inside a transaction managed by sqflite. results` holds one entry per operation (insert id, change count, query rows) in order.
commit(noResult: true)skips result collection (faster for large
imports). commit(continueOnError: true) runs every operation and puts a DatabaseException in the result slot of the failed ones instead of stopping.
txn.batch()inside a transaction is committed with that transaction:
await batch.commit() is still required but the data only lands on COMMIT of the enclosing transaction. Inside onCreate / onUpgrade the same applies.
batch.apply()runs the statements without a transaction; usecommit()
unless you manage BEGIN/COMMIT yourself.
- A batch is a list of statements decided up front; if a later statement
depends on a query result, use a transaction instead.
Large results
db.query(...)loads every row in memory. For big tables use
queryCursor / rawQueryCursor (bufferSize, default 100 rows) and while (await cursor.moveNext()) { cursor.current } in a try/finally that calls cursor.close(), or the queryIterate / rawQueryIterate extensions (SqfliteDatabaseExecutorIterateExt) whose onRow callback returns false to stop and which close the cursor for you.
- Rows over roughly 1 MB fail on Android (
CursorWindow); store big blobs in
files and keep a reference in the database.
SqfliteSqlCommand.query/insert/update/delete/rawQuery/...builds a
reusable command (sql + arguments); run it later with cmd.query(executor), cmd.insert(executor), cmd.iterate(executor, onRow: ...).
Errors
- Every native error surfaces as
DatabaseException. Use its helpers rather
than parsing messages: isNoSuchTableError([table]), isDuplicateColumnError([column]), isSyntaxError(), isUniqueConstraintError([field]), isNotNullConstraintError([field]), isOpenFailedError(), isDatabaseClosedError(), isReadOnlyError(), getResultCode() (extended code on Android, primary code on iOS).
- Errors thrown by
openDatabaseare usually thrown by your own callbacks;
read the message, it contains the failing SQL and arguments.
Examples
A DAO with the helpers
import 'package:sqflite/sqflite.dart';const tableTodo = 'Todo';const columnId = 'id';const columnTitle = 'title';const columnDone = 'done';class Todo {Todo({this.id, required this.title, this.done = false});int? id;String title;bool done;Map<String, Object?> toMap() => {if (id != null) columnId: id,columnTitle: title,columnDone: done ? 1 : 0,};factory Todo.fromMap(Map<String, Object?> map) => Todo(id: map[columnId] as int?,title: map[columnTitle] as String,done: map[columnDone] == 1,);}class TodoDao {TodoDao(this.db);final Database db;Future<Todo> insert(Todo todo) async {todo.id = await db.insert(tableTodo, todo.toMap());return todo;}Future<Todo?> get(int id) async {final rows = await db.query(tableTodo,columns: [columnId, columnTitle, columnDone],where: '$columnId = ?',whereArgs: [id],limit: 1,);return rows.isEmpty ? null : Todo.fromMap(rows.first);}Future<List<Todo>> search(String prefix) async {final rows = await db.query(tableTodo,where: '$columnTitle LIKE ?',whereArgs: ['$prefix%'],orderBy: '$columnTitle COLLATE NOCASE',);return rows.map(Todo.fromMap).toList();}Future<int> update(Todo todo) => db.update(tableTodo,todo.toMap(),where: '$columnId = ?',whereArgs: [todo.id],);Future<int> delete(int id) =>db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]);Future<int> count() async =>Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM $tableTodo'),) ??0;}
Raw SQL with bound arguments
import 'package:sqflite/sqflite.dart';Future<void> rawDemo(Database db) async {final id = await db.rawInsert('INSERT INTO Todo(title, done) VALUES (?, ?)',['Buy milk', 0],);final updated = await db.rawUpdate('UPDATE Todo SET done = ? WHERE id = ?',[1, id],);final ids = [1, 2, 3];final rows = await db.rawQuery('SELECT * FROM Todo WHERE id IN (${List.filled(ids.length, '?').join(',')})',ids,);final deleted = await db.rawDelete('DELETE FROM Todo WHERE done = ?', [1]);print('$updated $deleted ${rows.length}');}
Transaction: read, decide, write atomically
import 'package:sqflite/sqflite.dart';Future<void> transfer(Database db, int from, int to, int amount) async {await db.transaction((txn) async {// Only txn is used inside the callback, never db.final rows = await txn.query('Account',columns: ['balance'],where: 'id = ?',whereArgs: [from],);final balance = rows.first['balance'] as int;if (balance < amount) {// Throwing rolls back everything done in this transaction.throw StateError('insufficient funds');}await txn.rawUpdate('UPDATE Account SET balance = balance - ? WHERE id = ?',[amount, from],);await txn.rawUpdate('UPDATE Account SET balance = balance + ? WHERE id = ?',[amount, to],);});}
Upsert with ConflictAlgorithm or by catching the constraint error
import 'package:sqflite/sqflite.dart';/// Table Product(id TEXT PRIMARY KEY, title TEXT)Future<void> upsertProduct(Database db, String id, String title) =>db.insert('Product',{'id': id, 'title': title},conflictAlgorithm: ConflictAlgorithm.replace,);Future<void> insertOrUpdate(Database db, String id, String title) async {try {await db.insert('Product', {'id': id, 'title': title});} on DatabaseException catch (e) {if (!e.isUniqueConstraintError()) rethrow;await db.update('Product',{'title': title},where: 'id = ?',whereArgs: [id],);}}
Batch import
import 'package:sqflite/sqflite.dart';Future<void> importProducts(Database db, List<Map<String, Object?>> items) async {final batch = db.batch();batch.delete('Product');for (final item in items) {batch.insert('Product', item, conflictAlgorithm: ConflictAlgorithm.ignore);}// One native round trip, in a transaction; no per-operation results needed.await batch.commit(noResult: true);}Future<List<Object?>> batchWithResults(Database db) async {final batch = db.batch();batch.insert('Product', {'id': 'p1', 'title': 'One'});batch.update('Product', {'title': 'Uno'}, where: 'id = ?', whereArgs: ['p1']);batch.query('Product', where: 'id = ?', whereArgs: ['p1']);// [insertedId, updateCount, List<Map<String, Object?>>]return batch.commit();}
Streaming a large table with a cursor
import 'package:sqflite/sqflite.dart';Future<int> sumSizes(Database db) async {var total = 0;await db.queryIterate('File',columns: ['size'],orderBy: 'id',bufferSize: 200,onRow: (row) {total += row['size'] as int;return true; // false stops early; the cursor is closed either way},);return total;}Future<void> manualCursor(Database db) async {final cursor = await db.rawQueryCursor('SELECT * FROM File', null, bufferSize: 50);try {while (await cursor.moveNext()) {print(cursor.current['name']);}} finally {await cursor.close();}}
Schema introspection and errors
import 'package:sqflite/sqflite.dart';Future<bool> tableExists(DatabaseExecutor db, String table) async {final count = Sqflite.firstIntValue(await db.query('sqlite_master',columns: ['COUNT(*)'],where: 'type = ? AND name = ?',whereArgs: ['table', table],));return (count ?? 0) > 0;}Future<List<Map<String, Object?>>> safeQuery(Database db) async {try {return await db.query('Missing');} on DatabaseException catch (e) {if (e.isNoSuchTableError('Missing')) return const [];rethrow;}}
Common mistakes
- Using
dbinstead oftxninsidetransaction(): deadlock, then the
10 s "database has been locked" warning.
whereArgs: [list]for anINclause, or'col = ?'with[null].- Storing
bool,DateTime,ListorMapvalues directly. - Mutating a row map returned by
query(read-only) instead of copying it. - Expecting
batch.commit()in a transaction to be visible outside before the
transaction commits, or expecting apply() to be atomic.
- Building
'... WHERE name = "$name"'by interpolation instead of binding. - Assuming UPSERT syntax (
ON CONFLICT DO UPDATE) or JSON functions exist:
they depend on the OS SQLite version (SELECT sqlite_version()); ConflictAlgorithm.replace or a transaction works everywhere.
More
See references/sql.md for the escaped keyword list, the supported-type table, escapeName/unescapeName, SqfliteSqlCommand factories and the sqlite_master recipes. Opening and migrating: see the sqflite-open-database skill.