Agent Skillstekartik/sqflite › sqflite-crud-and-transactions

sqflite-crud-and-transactions

GitHub

提供 sqflite 库的数据库操作指南,涵盖 CRUD、事务、批量处理及参数绑定。指导正确执行 SQL 语句、处理数据类型与冲突算法,确保数据操作安全高效。

sqflite/skills/sqflite-crud-and-transactions/SKILL.md tekartik/sqflite

Trigger Scenarios

需要读写 SQLite 数据库记录 执行数据库事务或批量操作 构建复杂的 SQL 查询条件

Install

npx skills add tekartik/sqflite --skill sqflite-crud-and-transactions -g -y
More Options

Non-standard path

npx skills add https://github.com/tekartik/sqflite/tree/master/sqflite/skills/sqflite-crud-and-transactions -g -y

Use without installing

npx skills use tekartik/sqflite@sqflite-crud-and-transactions

指定 Agent (Claude Code)

npx skills add tekartik/sqflite --skill sqflite-crud-and-transactions -a claude-code -g -y

安装 repo 全部 skill

npx skills add tekartik/sqflite --all -g -y

预览 repo 内 skill

npx skills add tekartik/sqflite --list

SKILL.md

Frontmatter
{
    "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 (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.

Version History

  • aaabf90 Current 2026-09-22 03:38

Same Skill Collection

packages/sqflite_common_ffi_async_test/skills/sqflite-common-ffi-async-test-suite/SKILL.md
packages/sqflite_common_ffi_async/skills/sqflite-common-ffi-async-factory/SKILL.md
sqflite_android/skills/sqflite-android-setup/SKILL.md
sqflite_common_ffi/skills/sqflite-common-ffi-testing/SKILL.md
packages_flutter/sqflite_ffi/skills/sqflite-ffi-flutter/SKILL.md
packages_web/sqflite_common_ffi_web_test/skills/sqflite-common-ffi-web-test-suite/SKILL.md
packages_web/sqflite_common_ffi_web/skills/sqflite-common-ffi-web-options/SKILL.md
packages_web/sqflite_common_ffi_web/skills/sqflite-common-ffi-web-setup/SKILL.md
sqflite_common_ffi/skills/sqflite-common-ffi-desktop/SKILL.md
sqflite_common_test/skills/sqflite-common-test-suite/SKILL.md
sqflite_common/skills/sqflite-common-api/SKILL.md
sqflite_darwin/skills/sqflite-darwin-setup/SKILL.md
sqflite_platform_interface/skills/sqflite-platform-interface-implementers/SKILL.md
sqflite/skills/sqflite-open-database/SKILL.md
sqflite/skills/sqflite-testing-and-platforms/SKILL.md

Metadata

Files
0
Version
aaabf90
Hash
c1e37a92
Indexed
2026-09-22 03:38

Accueil - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-09-22 09:46
浙ICP备14020137号-1