sqflite: opening and migrating a database
package:sqflite is the Flutter plugin for SQLite on Android, iOS and macOS.
A database is a file identified by a path; a relative path is resolved against
getDatabasesPath(). openDatabase runs a small version-based migration
mechanism (onCreate / onUpgrade / onDowngrade) inside a transaction and
returns a Database that you keep open for the life of the app.
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
Future<Database> openAppDatabase() async {
final path = join(await getDatabasesPath(), 'app.db');
return openDatabase(
path,
version: 1,
onConfigure: (db) => db.execute('PRAGMA foreign_keys = ON'),
onCreate: (db, version) async {
await db.execute(
'CREATE TABLE Todo (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done INTEGER NOT NULL)',
);
},
);
}
Guidelines
Imports and platforms
import 'package:sqflite/sqflite.dart'; gives the global functions
(openDatabase, openReadOnlyDatabase, getDatabasesPath,
deleteDatabase, databaseExists), the global databaseFactory, the
Sqflite helper class and every type of package:sqflite/sqlite_api.dart
(Database, Transaction, Batch, DatabaseFactory,
OpenDatabaseOptions, ConflictAlgorithm, DatabaseException,
inMemoryDatabasePath, ...).
sqflite only works on Android, iOS and macOS. For Linux, Windows, the Dart
VM and unit tests use package:sqflite_common_ffi; for the web use
package:sqflite_common_ffi_web; both plug in through
databaseFactory = ... and the code below stays unchanged. Code that must
not depend on Flutter should import package:sqflite_common/sqlite_api.dart
and receive a DatabaseFactory.
- Build paths with
join from package:path, never with string
concatenation. getDatabasesPath() is data/data/<package>/databases on
Android and the Documents directory on iOS/macOS; on iOS the Library
directory from path_provider (getLibraryDirectory()) is the recommended
location instead.
- The plugin creates the parent directory of a read-write database on open.
When you write the file yourself (asset copy) create the directory first
with
Directory(dirname(path)).create(recursive: true).
Versioning callbacks
- Pass
version (an int > 0) to enable migrations. Callbacks run in this
order: onConfigure, then exactly one of onCreate / onUpgrade /
onDowngrade, then onOpen. Without version only onConfigure and
onOpen run.
onCreate(db, version) runs when the file does not exist. onUpgrade(db, oldVersion, newVersion) runs when the stored version is lower than
version (and also instead of onCreate, with oldVersion == 0, when no
onCreate is given). onDowngrade runs when the stored version is higher;
pass onDatabaseDowngradeDelete to delete and recreate the database in that
case, or onDatabaseVersionChangeError to fail.
onCreate, onUpgrade and onDowngrade already run inside a transaction:
use the db they receive directly (or db.batch() + commit()), never call
db.transaction() inside them. The version is stored (PRAGMA user_version) when the callback completes without throwing.
- Write migrations as a chain:
if (oldVersion < 2) {...} if (oldVersion < 3) {...} so any old version reaches the newest schema. Put schema statements
in a Batch, one statement per execute (multi-statement strings separated
by ; are not supported).
onConfigure is the place for PRAGMA foreign_keys = ON,
db.setJournalMode('WAL') (extension SqfliteDatabaseExt, handles the
Android quirk where execute fails), PRAGMA auto_vacuum (only when
await db.getVersion() == 0, i.e. a new file) and, on Android,
db.androidSetLocale('fr-FR') (extension SqfliteDatabaseAndroidExt).
They must be re-applied at every open, which is why they belong there.
db.getVersion() / db.setVersion() exist (extension
SqfliteDatabaseExecutorExt) but do not drive migrations with them; use
version and the callbacks.
openDatabase(path, options: OpenDatabaseOptions(...)) is equivalent to
the named parameters; when options is given all other parameters are
ignored. DatabaseFactory.openDatabase only takes options.
Instances, closing, deleting
- Open the database once and keep the
Database; store the Future<Database>
(not the Database) in a field so concurrent callers share the same open
call. Many apps never close it.
singleInstance: true (default) returns the same Database for the same
path; a second openDatabase on that path returns the existing instance and
ignores its callbacks. It is forced to false for inMemoryDatabasePath
(':memory:'). Opening the same file twice with singleInstance: false
causes "database is locked" errors on Android.
rollbackActiveTransactionOnOpen (OpenDatabaseOptions, default: true in
debug, false in release) rolls back a transaction left open by a previous
isolate/hot restart when singleInstance is true. Keep the default unless
you deliberately use several isolates.
readOnly: true (or openReadOnlyDatabase(path)) ignores every callback,
never starts a transaction and fails on the first write. Use it for shipped
asset databases.
- Delete with
deleteDatabase(path), never with File(path).delete(): it
closes the open instance, handles the hot-restart state and removes the
-wal, -shm and -journal side files.
databaseExists(path) checks the file; db.isOpen tells if close() was
called; db.path is the resolved absolute path.
databaseFactory.readDatabaseBytes(path) / writeDatabaseBytes(path, bytes) copy a whole database file (backup, restore, asset import) in a way
that also works with the ffi and web factories.
factory.sandbox(path: root) (extension
SqfliteDatabaseFactorySandboxExtension) returns a DatabaseFactory whose
relative paths live under root and whose absolute paths must stay inside
it. Use it to isolate tests or per-user data.
Isolates and hot restart
- Use the database from the main isolate: native calls already run on a
background thread and the transaction lock is not cross-isolate. If a
background isolate (push notification, work manager) must read the database,
open it there with
singleInstance: false and do not close it.
- After changing the schema during development restart the app; a hot reload
keeps the native connection open with the old schema.
Examples
Migration chain with batches and a downgrade policy
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
const _version = 2;
void _createV1(Batch batch) {
batch.execute('DROP TABLE IF EXISTS Company');
batch.execute(
'CREATE TABLE Company (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)',
);
}
void _upgradeV1ToV2(Batch batch) {
batch.execute('ALTER TABLE Company ADD description TEXT');
batch.execute('''CREATE TABLE Employee (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
companyId INTEGER,
FOREIGN KEY (companyId) REFERENCES Company(id) ON DELETE CASCADE)''');
}
Future<Database> openCompanyDb() async {
final path = join(await getDatabasesPath(), 'company.db');
return openDatabase(
path,
version: _version,
onConfigure: (db) async {
await db.execute('PRAGMA foreign_keys = ON');
},
onCreate: (db, version) async {
// Fresh install: build the latest schema through the same steps.
final batch = db.batch();
_createV1(batch);
_upgradeV1ToV2(batch);
await batch.commit();
},
onUpgrade: (db, oldVersion, newVersion) async {
final batch = db.batch();
if (oldVersion < 2) {
_upgradeV1ToV2(batch);
}
await batch.commit();
},
onDowngrade: onDatabaseDowngradeDelete,
);
}
One shared instance for the whole app
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
class AppDatabase {
Future<Database>? _db;
/// Safe to call concurrently: the first call starts the open, others await it.
Future<Database> get database => _db ??= _open();
Future<Database> _open() async {
final path = join(await getDatabasesPath(), 'app.db');
return openDatabase(
path,
version: 1,
onConfigure: (db) => db.setJournalMode('WAL'),
onCreate: (db, _) => db.execute(
'CREATE TABLE Note (id INTEGER PRIMARY KEY, content TEXT)',
),
);
}
Future<void> close() async {
final db = await _db;
_db = null;
await db?.close();
}
}
Copy a bundled asset database on first launch
import 'dart:io';
import 'package:flutter/services.dart' show rootBundle;
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
Future<Database> openAssetDatabase() async {
final path = join(await getDatabasesPath(), 'catalog.db');
if (!await databaseExists(path)) {
await Directory(dirname(path)).create(recursive: true);
final data = await rootBundle.load(url.join('assets', 'catalog.db'));
await databaseFactory.writeDatabaseBytes(
path,
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes),
);
}
// Shipped data: open read-only, no callbacks run.
return openReadOnlyDatabase(path);
}
Delete and recreate, in-memory database
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
Future<void> resetDatabase() async {
final path = join(await getDatabasesPath(), 'app.db');
await deleteDatabase(path); // also closes the open instance
}
Future<Database> openScratchDb() =>
openDatabase(inMemoryDatabasePath); // singleInstance is forced to false
Opening through an explicit factory
import 'package:sqflite/sqflite.dart';
/// Works with databaseFactory (sqflite), databaseFactoryFfi, databaseFactoryFfiWeb...
Future<Database> openWith(DatabaseFactory factory, String path) {
return factory.openDatabase(
path,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) =>
db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)'),
),
);
}
Common mistakes
- Calling
db.transaction() inside onCreate / onUpgrade: they are already
in a transaction, use db directly or a batch.
- Creating two tables in one
execute string. One statement per call.
- Forgetting to bump
version after changing onCreate; existing installs
never see the new schema. Handle it in onUpgrade too.
- Deleting the file with
dart:io instead of deleteDatabase, then wondering
why onCreate does not run after a hot restart.
- Opening the database in every widget/repository call with
singleInstance: false, leading to database is locked (code 5).
- Putting
PRAGMA foreign_keys / setJournalMode in onCreate: they are per
connection and must go in onConfigure.
- Calling
openDatabase on Linux/Windows/web without first setting
databaseFactory from sqflite_common_ffi / sqflite_common_ffi_web
(StateError: databaseFactory not initialized).
More
Queries, transactions and batches: see the sqflite-crud-and-transactions
skill. Unit tests, desktop/web factories and logging: see the
sqflite-testing-and-platforms skill.
1---2name: sqflite-open-database3description: Use when opening, creating, migrating, closing or deleting a SQLite database with package:sqflite in a Flutter app (Android, iOS, macOS): openDatabase, openReadOnlyDatabase, OpenDatabaseOptions, version, onConfigure, onCreate, onUpgrade, onDowngrade, onDatabaseDowngradeDelete, onOpen, getDatabasesPath, deleteDatabase, databaseExists, inMemoryDatabasePath, singleInstance, readOnly, databaseFactory, getVersion, setJournalMode (WAL), foreign keys, copying an asset database, and the "database is locked" pitfalls.4---56# sqflite: opening and migrating a database78`package:sqflite` is the Flutter plugin for SQLite on Android, iOS and macOS.9A database is a file identified by a path; a relative path is resolved against10`getDatabasesPath()`. `openDatabase` runs a small version-based migration11mechanism (`onCreate` / `onUpgrade` / `onDowngrade`) inside a transaction and12returns a `Database` that you keep open for the life of the app.1314```dart15import 'package:path/path.dart';16import 'package:sqflite/sqflite.dart';1718Future<Database> openAppDatabase() async {19 final path = join(await getDatabasesPath(), 'app.db');20 return openDatabase(21 path,22 version: 1,23 onConfigure: (db) => db.execute('PRAGMA foreign_keys = ON'),24 onCreate: (db, version) async {25 await db.execute(26 'CREATE TABLE Todo (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done INTEGER NOT NULL)',27 );28 },29 );30}31```3233## Guidelines3435### Imports and platforms3637* `import 'package:sqflite/sqflite.dart';` gives the global functions38 (`openDatabase`, `openReadOnlyDatabase`, `getDatabasesPath`,39 `deleteDatabase`, `databaseExists`), the global `databaseFactory`, the40 `Sqflite` helper class and every type of `package:sqflite/sqlite_api.dart`41 (`Database`, `Transaction`, `Batch`, `DatabaseFactory`,42 `OpenDatabaseOptions`, `ConflictAlgorithm`, `DatabaseException`,43 `inMemoryDatabasePath`, ...).44* `sqflite` only works on Android, iOS and macOS. For Linux, Windows, the Dart45 VM and unit tests use `package:sqflite_common_ffi`; for the web use46 `package:sqflite_common_ffi_web`; both plug in through47 `databaseFactory = ...` and the code below stays unchanged. Code that must48 not depend on Flutter should import `package:sqflite_common/sqlite_api.dart`49 and receive a `DatabaseFactory`.50* Build paths with `join` from `package:path`, never with string51 concatenation. `getDatabasesPath()` is `data/data/<package>/databases` on52 Android and the Documents directory on iOS/macOS; on iOS the Library53 directory from `path_provider` (`getLibraryDirectory()`) is the recommended54 location instead.55* The plugin creates the parent directory of a read-write database on open.56 When you write the file yourself (asset copy) create the directory first57 with `Directory(dirname(path)).create(recursive: true)`.5859### Versioning callbacks6061* Pass `version` (an `int` > 0) to enable migrations. Callbacks run in this62 order: `onConfigure`, then exactly one of `onCreate` / `onUpgrade` /63 `onDowngrade`, then `onOpen`. Without `version` only `onConfigure` and64 `onOpen` run.65* `onCreate(db, version)` runs when the file does not exist. `onUpgrade(db,66 oldVersion, newVersion)` runs when the stored version is lower than67 `version` (and also instead of `onCreate`, with `oldVersion == 0`, when no68 `onCreate` is given). `onDowngrade` runs when the stored version is higher;69 pass `onDatabaseDowngradeDelete` to delete and recreate the database in that70 case, or `onDatabaseVersionChangeError` to fail.71* `onCreate`, `onUpgrade` and `onDowngrade` already run inside a transaction:72 use the `db` they receive directly (or `db.batch()` + `commit()`), never call73 `db.transaction()` inside them. The version is stored (`PRAGMA74 user_version`) when the callback completes without throwing.75* Write migrations as a chain: `if (oldVersion < 2) {...} if (oldVersion < 3)76 {...}` so any old version reaches the newest schema. Put schema statements77 in a `Batch`, one statement per `execute` (multi-statement strings separated78 by `;` are not supported).79* `onConfigure` is the place for `PRAGMA foreign_keys = ON`,80 `db.setJournalMode('WAL')` (extension `SqfliteDatabaseExt`, handles the81 Android quirk where `execute` fails), `PRAGMA auto_vacuum` (only when82 `await db.getVersion() == 0`, i.e. a new file) and, on Android,83 `db.androidSetLocale('fr-FR')` (extension `SqfliteDatabaseAndroidExt`).84 They must be re-applied at every open, which is why they belong there.85* `db.getVersion()` / `db.setVersion()` exist (extension86 `SqfliteDatabaseExecutorExt`) but do not drive migrations with them; use87 `version` and the callbacks.88* `openDatabase(path, options: OpenDatabaseOptions(...))` is equivalent to89 the named parameters; when `options` is given all other parameters are90 ignored. `DatabaseFactory.openDatabase` only takes `options`.9192### Instances, closing, deleting9394* Open the database once and keep the `Database`; store the `Future<Database>`95 (not the `Database`) in a field so concurrent callers share the same open96 call. Many apps never close it.97* `singleInstance: true` (default) returns the same `Database` for the same98 path; a second `openDatabase` on that path returns the existing instance and99 ignores its callbacks. It is forced to `false` for `inMemoryDatabasePath`100 (`':memory:'`). Opening the same file twice with `singleInstance: false`101 causes "database is locked" errors on Android.102* `rollbackActiveTransactionOnOpen` (`OpenDatabaseOptions`, default: true in103 debug, false in release) rolls back a transaction left open by a previous104 isolate/hot restart when `singleInstance` is true. Keep the default unless105 you deliberately use several isolates.106* `readOnly: true` (or `openReadOnlyDatabase(path)`) ignores every callback,107 never starts a transaction and fails on the first write. Use it for shipped108 asset databases.109* Delete with `deleteDatabase(path)`, never with `File(path).delete()`: it110 closes the open instance, handles the hot-restart state and removes the111 `-wal`, `-shm` and `-journal` side files.112* `databaseExists(path)` checks the file; `db.isOpen` tells if `close()` was113 called; `db.path` is the resolved absolute path.114* `databaseFactory.readDatabaseBytes(path)` / `writeDatabaseBytes(path,115 bytes)` copy a whole database file (backup, restore, asset import) in a way116 that also works with the ffi and web factories.117* `factory.sandbox(path: root)` (extension118 `SqfliteDatabaseFactorySandboxExtension`) returns a `DatabaseFactory` whose119 relative paths live under `root` and whose absolute paths must stay inside120 it. Use it to isolate tests or per-user data.121122### Isolates and hot restart123124* Use the database from the main isolate: native calls already run on a125 background thread and the transaction lock is not cross-isolate. If a126 background isolate (push notification, work manager) must read the database,127 open it there with `singleInstance: false` and do not close it.128* After changing the schema during development restart the app; a hot reload129 keeps the native connection open with the old schema.130131## Examples132133### Migration chain with batches and a downgrade policy134135```dart136import 'package:path/path.dart';137import 'package:sqflite/sqflite.dart';138139const _version = 2;140141void _createV1(Batch batch) {142 batch.execute('DROP TABLE IF EXISTS Company');143 batch.execute(144 'CREATE TABLE Company (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)',145 );146}147148void _upgradeV1ToV2(Batch batch) {149 batch.execute('ALTER TABLE Company ADD description TEXT');150 batch.execute('''CREATE TABLE Employee (151 id INTEGER PRIMARY KEY AUTOINCREMENT,152 name TEXT,153 companyId INTEGER,154 FOREIGN KEY (companyId) REFERENCES Company(id) ON DELETE CASCADE)''');155}156157Future<Database> openCompanyDb() async {158 final path = join(await getDatabasesPath(), 'company.db');159 return openDatabase(160 path,161 version: _version,162 onConfigure: (db) async {163 await db.execute('PRAGMA foreign_keys = ON');164 },165 onCreate: (db, version) async {166 // Fresh install: build the latest schema through the same steps.167 final batch = db.batch();168 _createV1(batch);169 _upgradeV1ToV2(batch);170 await batch.commit();171 },172 onUpgrade: (db, oldVersion, newVersion) async {173 final batch = db.batch();174 if (oldVersion < 2) {175 _upgradeV1ToV2(batch);176 }177 await batch.commit();178 },179 onDowngrade: onDatabaseDowngradeDelete,180 );181}182```183184### One shared instance for the whole app185186```dart187import 'package:path/path.dart';188import 'package:sqflite/sqflite.dart';189190class AppDatabase {191 Future<Database>? _db;192193 /// Safe to call concurrently: the first call starts the open, others await it.194 Future<Database> get database => _db ??= _open();195196 Future<Database> _open() async {197 final path = join(await getDatabasesPath(), 'app.db');198 return openDatabase(199 path,200 version: 1,201 onConfigure: (db) => db.setJournalMode('WAL'),202 onCreate: (db, _) => db.execute(203 'CREATE TABLE Note (id INTEGER PRIMARY KEY, content TEXT)',204 ),205 );206 }207208 Future<void> close() async {209 final db = await _db;210 _db = null;211 await db?.close();212 }213}214```215216### Copy a bundled asset database on first launch217218```dart219import 'dart:io';220221import 'package:flutter/services.dart' show rootBundle;222import 'package:path/path.dart';223import 'package:sqflite/sqflite.dart';224225Future<Database> openAssetDatabase() async {226 final path = join(await getDatabasesPath(), 'catalog.db');227 if (!await databaseExists(path)) {228 await Directory(dirname(path)).create(recursive: true);229 final data = await rootBundle.load(url.join('assets', 'catalog.db'));230 await databaseFactory.writeDatabaseBytes(231 path,232 data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes),233 );234 }235 // Shipped data: open read-only, no callbacks run.236 return openReadOnlyDatabase(path);237}238```239240### Delete and recreate, in-memory database241242```dart243import 'package:path/path.dart';244import 'package:sqflite/sqflite.dart';245246Future<void> resetDatabase() async {247 final path = join(await getDatabasesPath(), 'app.db');248 await deleteDatabase(path); // also closes the open instance249}250251Future<Database> openScratchDb() =>252 openDatabase(inMemoryDatabasePath); // singleInstance is forced to false253```254255### Opening through an explicit factory256257```dart258import 'package:sqflite/sqflite.dart';259260/// Works with databaseFactory (sqflite), databaseFactoryFfi, databaseFactoryFfiWeb...261Future<Database> openWith(DatabaseFactory factory, String path) {262 return factory.openDatabase(263 path,264 options: OpenDatabaseOptions(265 version: 1,266 onCreate: (db, version) =>267 db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)'),268 ),269 );270}271```272273## Common mistakes274275* Calling `db.transaction()` inside `onCreate` / `onUpgrade`: they are already276 in a transaction, use `db` directly or a batch.277* Creating two tables in one `execute` string. One statement per call.278* Forgetting to bump `version` after changing `onCreate`; existing installs279 never see the new schema. Handle it in `onUpgrade` too.280* Deleting the file with `dart:io` instead of `deleteDatabase`, then wondering281 why `onCreate` does not run after a hot restart.282* Opening the database in every widget/repository call with `singleInstance:283 false`, leading to `database is locked (code 5)`.284* Putting `PRAGMA foreign_keys` / `setJournalMode` in `onCreate`: they are per285 connection and must go in `onConfigure`.286* Calling `openDatabase` on Linux/Windows/web without first setting287 `databaseFactory` from `sqflite_common_ffi` / `sqflite_common_ffi_web`288 (`StateError: databaseFactory not initialized`).289290## More291292Queries, transactions and batches: see the `sqflite-crud-and-transactions`293skill. Unit tests, desktop/web factories and logging: see the294`sqflite-testing-and-platforms` skill.