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 (whereArgs or 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 NULL in the SQL, not with = ?
and a null argument (whereArgs values must be non-null; raw arguments
may contain null).
query clauses (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) (or firstIntValue from
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 (on insert and update): replace deletes 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 {...}) begins BEGIN 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 on db (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; use commit()
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
openDatabase are 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
db instead of txn inside transaction(): deadlock, then the
10 s "database has been locked" warning.
whereArgs: [list] for an IN clause, or 'col = ?' with [null].
- Storing
bool, DateTime, List or Map values 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.
1---2name: sqflite-crud-and-transactions3description: 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).4---56# sqflite: CRUD, transactions and batches78Every SQL call goes through a `DatabaseExecutor`, which is either the9`Database` or the `Transaction` passed to `transaction()`. Helpers (`insert`,10`query`, `update`, `delete`) build the SQL for you; raw methods take a SQL11string plus a positional `?` argument list. Each call runs exactly one12statement and is serialized on the database.1314```dart15import 'package:sqflite/sqflite.dart';1617Future<int> addTodo(Database db, String title) =>18 db.insert('Todo', {'title': title, 'done': 0});1920Future<List<Map<String, Object?>>> pendingTodos(Database db) =>21 db.query('Todo', where: 'done = ?', whereArgs: [0], orderBy: 'id');22```2324## Guidelines2526### Statements and arguments2728* `execute(sql, [args])` is for DDL and statements without a result (`CREATE29 TABLE`, `PRAGMA`, `DROP`). One statement per call; `;`-separated strings30 are rejected.31* `insert(table, values, {nullColumnHack, conflictAlgorithm})` returns the32 inserted row id (`rawInsert` too). `update(...)` / `delete(...)` and33 `rawUpdate` / `rawDelete` return the number of rows changed. `query(...)`34 / `rawQuery(...)` return `List<Map<String, Object?>>`.35* Always bind values with `?` and an argument list (`whereArgs` or the raw36 `arguments`). Never interpolate user data into the SQL string. `?NNN`37 positional references (`?1`, `?2`) are supported.38* `IN (?)` does not accept a list argument. Generate one `?` per value:39 `'id IN (${List.filled(ids.length, '?').join(',')})'` with `whereArgs: ids`.40* Test for null with `IS NULL` / `IS NOT NULL` in the SQL, not with `= ?`41 and a `null` argument (`whereArgs` values must be non-null; raw `arguments`42 may contain `null`).43* `query` clauses (`where`, `orderBy`, `groupBy`, `having`, `columns`,44 `limit`, `offset`, `distinct`) are raw SQL fragments without the keyword:45 `orderBy: 'name COLLATE NOCASE DESC'`, `where: 'a = ? AND b > ?'`.46 `having` requires `groupBy`.47* Column values must be `int`, `num` (`REAL`), `String` (`TEXT`),48 `Uint8List` (`BLOB`) or `null`. `bool` is stored as `INTEGER` 0/1;49 `DateTime` as `millisecondsSinceEpoch` (`INTEGER`) or `toIso8601String()`50 (`TEXT`); nested maps/lists as a JSON `TEXT`. Passing another type prints a51 warning in debug mode and will throw in the future.52* Table and column names that are SQLite keywords (`group`, `order`,53 `table`, `values`, ...) are escaped automatically by the helpers54 (`insert`, `query`, `update`, `delete` and the `columns` list) but not55 inside `where`, `orderBy` or raw SQL: write `'"group" = ?'` there, or call56 `escapeName(name)` from `package:sqflite/sql.dart`. Prefer names that are57 not keywords.58* Query results are read-only: copy with `Map<String, Object?>.from(row)` /59 `List.of(rows)` before mutating. A row is a `Map<String, Object?>` keyed by60 column name (or alias); a `COUNT(*)` column is keyed `'COUNT(*)'`.61* `Sqflite.firstIntValue(rows)` (or `firstIntValue` from62 `package:sqflite/utils/utils.dart`) reads the first value of the first row63 as an `int?`, the idiom for `SELECT COUNT(*)`. `Sqflite.hex(bytes)` builds64 the argument for `'hex(blob_column) = ?'`.65* `ConflictAlgorithm` (on `insert` and `update`): `replace` deletes the66 conflicting row(s) and inserts (upsert by primary/unique key), `ignore`67 skips the row without error (`insert` then returns 0), `abort` (default),68 `fail`, `rollback`. Alternatively catch `DatabaseException` and check69 `isUniqueConstraintError()`.7071### Transactions7273* `db.transaction((txn) async {...})` begins `BEGIN IMMEDIATE` (`exclusive:74 true` for `BEGIN EXCLUSIVE`), runs the callback, then `COMMIT`; if the75 callback throws it runs `ROLLBACK` and rethrows. Return a value from the76 callback to get it from `transaction<T>`.77* Inside the callback use only `txn`. Any call on `db` (or on another78 `Database` object for the same file) waits for the transaction and79 deadlocks; after 10 s sqflite prints "Warning database has been locked".80* A caught exception inside the callback does not roll back: the transaction81 commits with the successful statements. Rethrow (or throw your own error)82 to cancel.83* Transactions are exclusive and serialized: no concurrent read while one is84 running. Keep them short and never await UI or network inside.85* `db.readTransaction(...)` is experimental; on sqflite it is a normal86 transaction that is always rolled back. Use `transaction` unless you are on87 an implementation that documents it.8889### Batches9091* `final batch = db.batch(); batch.insert(...); batch.update(...);92 final results = await batch.commit();` sends all operations in one native93 call inside a transaction managed by sqflite. `results` holds one entry per94 operation (insert id, change count, query rows) in order.95* `commit(noResult: true)` skips result collection (faster for large96 imports). `commit(continueOnError: true)` runs every operation and puts a97 `DatabaseException` in the result slot of the failed ones instead of98 stopping.99* `txn.batch()` inside a transaction is committed with that transaction:100 `await batch.commit()` is still required but the data only lands on101 `COMMIT` of the enclosing transaction. Inside `onCreate` / `onUpgrade` the102 same applies.103* `batch.apply()` runs the statements without a transaction; use `commit()`104 unless you manage `BEGIN`/`COMMIT` yourself.105* A batch is a list of statements decided up front; if a later statement106 depends on a query result, use a `transaction` instead.107108### Large results109110* `db.query(...)` loads every row in memory. For big tables use111 `queryCursor` / `rawQueryCursor` (`bufferSize`, default 100 rows) and112 `while (await cursor.moveNext()) { cursor.current }` in a `try/finally`113 that calls `cursor.close()`, or the `queryIterate` / `rawQueryIterate`114 extensions (`SqfliteDatabaseExecutorIterateExt`) whose `onRow` callback115 returns `false` to stop and which close the cursor for you.116* Rows over roughly 1 MB fail on Android (`CursorWindow`); store big blobs in117 files and keep a reference in the database.118* `SqfliteSqlCommand.query/insert/update/delete/rawQuery/...` builds a119 reusable command (`sql` + `arguments`); run it later with120 `cmd.query(executor)`, `cmd.insert(executor)`, `cmd.iterate(executor,121 onRow: ...)`.122123### Errors124125* Every native error surfaces as `DatabaseException`. Use its helpers rather126 than parsing messages: `isNoSuchTableError([table])`,127 `isDuplicateColumnError([column])`, `isSyntaxError()`,128 `isUniqueConstraintError([field])`, `isNotNullConstraintError([field])`,129 `isOpenFailedError()`, `isDatabaseClosedError()`, `isReadOnlyError()`,130 `getResultCode()` (extended code on Android, primary code on iOS).131* Errors thrown by `openDatabase` are usually thrown by your own callbacks;132 read the message, it contains the failing SQL and arguments.133134## Examples135136### A DAO with the helpers137138```dart139import 'package:sqflite/sqflite.dart';140141const tableTodo = 'Todo';142const columnId = 'id';143const columnTitle = 'title';144const columnDone = 'done';145146class Todo {147 Todo({this.id, required this.title, this.done = false});148149 int? id;150 String title;151 bool done;152153 Map<String, Object?> toMap() => {154 if (id != null) columnId: id,155 columnTitle: title,156 columnDone: done ? 1 : 0,157 };158159 factory Todo.fromMap(Map<String, Object?> map) => Todo(160 id: map[columnId] as int?,161 title: map[columnTitle] as String,162 done: map[columnDone] == 1,163 );164}165166class TodoDao {167 TodoDao(this.db);168169 final Database db;170171 Future<Todo> insert(Todo todo) async {172 todo.id = await db.insert(tableTodo, todo.toMap());173 return todo;174 }175176 Future<Todo?> get(int id) async {177 final rows = await db.query(178 tableTodo,179 columns: [columnId, columnTitle, columnDone],180 where: '$columnId = ?',181 whereArgs: [id],182 limit: 1,183 );184 return rows.isEmpty ? null : Todo.fromMap(rows.first);185 }186187 Future<List<Todo>> search(String prefix) async {188 final rows = await db.query(189 tableTodo,190 where: '$columnTitle LIKE ?',191 whereArgs: ['$prefix%'],192 orderBy: '$columnTitle COLLATE NOCASE',193 );194 return rows.map(Todo.fromMap).toList();195 }196197 Future<int> update(Todo todo) => db.update(198 tableTodo,199 todo.toMap(),200 where: '$columnId = ?',201 whereArgs: [todo.id],202 );203204 Future<int> delete(int id) =>205 db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]);206207 Future<int> count() async =>208 Sqflite.firstIntValue(209 await db.rawQuery('SELECT COUNT(*) FROM $tableTodo'),210 ) ??211 0;212}213```214215### Raw SQL with bound arguments216217```dart218import 'package:sqflite/sqflite.dart';219220Future<void> rawDemo(Database db) async {221 final id = await db.rawInsert(222 'INSERT INTO Todo(title, done) VALUES (?, ?)',223 ['Buy milk', 0],224 );225 final updated = await db.rawUpdate(226 'UPDATE Todo SET done = ? WHERE id = ?',227 [1, id],228 );229 final ids = [1, 2, 3];230 final rows = await db.rawQuery(231 'SELECT * FROM Todo WHERE id IN (${List.filled(ids.length, '?').join(',')})',232 ids,233 );234 final deleted = await db.rawDelete('DELETE FROM Todo WHERE done = ?', [1]);235 print('$updated $deleted ${rows.length}');236}237```238239### Transaction: read, decide, write atomically240241```dart242import 'package:sqflite/sqflite.dart';243244Future<void> transfer(Database db, int from, int to, int amount) async {245 await db.transaction((txn) async {246 // Only txn is used inside the callback, never db.247 final rows = await txn.query(248 'Account',249 columns: ['balance'],250 where: 'id = ?',251 whereArgs: [from],252 );253 final balance = rows.first['balance'] as int;254 if (balance < amount) {255 // Throwing rolls back everything done in this transaction.256 throw StateError('insufficient funds');257 }258 await txn.rawUpdate(259 'UPDATE Account SET balance = balance - ? WHERE id = ?',260 [amount, from],261 );262 await txn.rawUpdate(263 'UPDATE Account SET balance = balance + ? WHERE id = ?',264 [amount, to],265 );266 });267}268```269270### Upsert with ConflictAlgorithm or by catching the constraint error271272```dart273import 'package:sqflite/sqflite.dart';274275/// Table Product(id TEXT PRIMARY KEY, title TEXT)276Future<void> upsertProduct(Database db, String id, String title) =>277 db.insert(278 'Product',279 {'id': id, 'title': title},280 conflictAlgorithm: ConflictAlgorithm.replace,281 );282283Future<void> insertOrUpdate(Database db, String id, String title) async {284 try {285 await db.insert('Product', {'id': id, 'title': title});286 } on DatabaseException catch (e) {287 if (!e.isUniqueConstraintError()) rethrow;288 await db.update(289 'Product',290 {'title': title},291 where: 'id = ?',292 whereArgs: [id],293 );294 }295}296```297298### Batch import299300```dart301import 'package:sqflite/sqflite.dart';302303Future<void> importProducts(Database db, List<Map<String, Object?>> items) async {304 final batch = db.batch();305 batch.delete('Product');306 for (final item in items) {307 batch.insert('Product', item, conflictAlgorithm: ConflictAlgorithm.ignore);308 }309 // One native round trip, in a transaction; no per-operation results needed.310 await batch.commit(noResult: true);311}312313Future<List<Object?>> batchWithResults(Database db) async {314 final batch = db.batch();315 batch.insert('Product', {'id': 'p1', 'title': 'One'});316 batch.update('Product', {'title': 'Uno'}, where: 'id = ?', whereArgs: ['p1']);317 batch.query('Product', where: 'id = ?', whereArgs: ['p1']);318 // [insertedId, updateCount, List<Map<String, Object?>>]319 return batch.commit();320}321```322323### Streaming a large table with a cursor324325```dart326import 'package:sqflite/sqflite.dart';327328Future<int> sumSizes(Database db) async {329 var total = 0;330 await db.queryIterate(331 'File',332 columns: ['size'],333 orderBy: 'id',334 bufferSize: 200,335 onRow: (row) {336 total += row['size'] as int;337 return true; // false stops early; the cursor is closed either way338 },339 );340 return total;341}342343Future<void> manualCursor(Database db) async {344 final cursor = await db.rawQueryCursor('SELECT * FROM File', null, bufferSize: 50);345 try {346 while (await cursor.moveNext()) {347 print(cursor.current['name']);348 }349 } finally {350 await cursor.close();351 }352}353```354355### Schema introspection and errors356357```dart358import 'package:sqflite/sqflite.dart';359360Future<bool> tableExists(DatabaseExecutor db, String table) async {361 final count = Sqflite.firstIntValue(await db.query(362 'sqlite_master',363 columns: ['COUNT(*)'],364 where: 'type = ? AND name = ?',365 whereArgs: ['table', table],366 ));367 return (count ?? 0) > 0;368}369370Future<List<Map<String, Object?>>> safeQuery(Database db) async {371 try {372 return await db.query('Missing');373 } on DatabaseException catch (e) {374 if (e.isNoSuchTableError('Missing')) return const [];375 rethrow;376 }377}378```379380## Common mistakes381382* Using `db` instead of `txn` inside `transaction()`: deadlock, then the383 10 s "database has been locked" warning.384* `whereArgs: [list]` for an `IN` clause, or `'col = ?'` with `[null]`.385* Storing `bool`, `DateTime`, `List` or `Map` values directly.386* Mutating a row map returned by `query` (read-only) instead of copying it.387* Expecting `batch.commit()` in a transaction to be visible outside before the388 transaction commits, or expecting `apply()` to be atomic.389* Building `'... WHERE name = "$name"'` by interpolation instead of binding.390* Assuming UPSERT syntax (`ON CONFLICT DO UPDATE`) or JSON functions exist:391 they depend on the OS SQLite version (`SELECT sqlite_version()`);392 `ConflictAlgorithm.replace` or a transaction works everywhere.393394## More395396See [references/sql.md](references/sql.md) for the escaped keyword list, the397supported-type table, `escapeName`/`unescapeName`, `SqfliteSqlCommand`398factories and the `sqlite_master` recipes. Opening and migrating: see the399`sqflite-open-database` skill.