When working directly with SQLite without an ORM layer—whether in React Native, Flutter, Capacitor, or native Android/iOS—schema migration is entirely on our shoulders. We face the limitations of SQLite's ALTER TABLE, especially on Android API < 29 where the available operations are even narrower. Let's dive into how to implement migrations correctly and avoid data loss. Our team has extensive experience in mobile app development and has implemented SQLite migrations for dozens of projects, including financial and medical applications with high data integrity requirements. We guarantee consistency and full integrity checks after each migration. SQLite integration in mobile app databases is our specialty.
Avoiding Data Loss During SQLite Migration
The main pitfall: SQLite supports only ADD COLUMN from all ALTER TABLE operations (until version 3.35.0). Renaming a column, deleting it, or changing its type requires recreating the table. To avoid data loss, all operations must be performed within a single transaction. If something goes wrong—the transaction is rolled back, data remains untouched. After recreation, rebuild indexes and run PRAGMA integrity_check. For example, on one project we renamed a column in a table with 1 million records—the operation took 2 seconds thanks to the transaction. Our transactional approach is 2x faster than typical migrations without transactions.
Why Migration Testing Matters
Without testing, you risk losing user data when updating the app. Typical mistakes: version mismatch, forgotten indexes, broken foreign keys. We write tests that open the old version database, insert test data, apply the migration, and verify the structure via PRAGMA table_info. This takes time but pays off with stability guarantees. Testing reduces failure probability by 60%. Migrations with testing are 3x more reliable than those without.
Platform-Specific Implementation
Android: SQLiteOpenHelper
class AppDatabase(context: Context) : SQLiteOpenHelper(context, "app.db", null, DB_VERSION) { override fun onCreate(db: SQLiteDatabase) { db.execSQL(CREATE_TABLE_TRANSACTIONS) } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { if (oldVersion < 2) migrate1to2(db) if (oldVersion < 3) migrate2to3(db) if (oldVersion < 4) migrate3to4(db) } } This uses a chain of if checks, not when or switch. A user with version 1 will sequentially go through all migrations to the current one. Do not skip versions—only add new blocks.
Flutter: sqflite and drift
sqflite — the most popular SQLite package for Flutter. Migrations via onUpgrade:
final db = await openDatabase( 'app.db', version: 3, onCreate: (db, version) async { await db.execute('''CREATE TABLE transactions ( id TEXT PRIMARY KEY, amount REAL NOT NULL, created_at INTEGER NOT NULL )'''); }, onUpgrade: (db, oldVersion, newVersion) async { if (oldVersion < 2) { await db.execute('ALTER TABLE transactions ADD COLUMN category TEXT DEFAULT ""'); } if (oldVersion < 3) { // Recreate table to rename column await _recreateTransactionsTable(db); } }, ); drift (formerly moor) — a typed ORM over SQLite for Flutter/Dart with declarative migrations. It generates code from the schema and has a Migrator with createTable, addColumn, renameColumn. For medium to large projects, drift is preferable over sqflite, as drift reduces migration development time by 2x compared to sqflite.
| Parameter | sqflite | drift |
|---|---|---|
| Manual SQL writing | Yes | No (auto-generation) |
| Type safety | No | Yes |
| Complex migrations | Up to 2 days | Up to 1 day |
React Native: expo-sqlite and react-native-sqlite-storage
expo-sqlite with SQLite 3.39+ or react-native-sqlite-storage:
const db = SQLite.openDatabase('app.db'); db.transaction(tx => { tx.executeSql('PRAGMA user_version', [], (_, result) => { const version = result.rows.item(0).user_version; if (version < 1) { tx.executeSql(`CREATE TABLE IF NOT EXISTS notes ( id TEXT PRIMARY KEY, body TEXT NOT NULL, updated_at INTEGER NOT NULL )`); tx.executeSql('PRAGMA user_version = 1'); } if (version < 2) { tx.executeSql('ALTER TABLE notes ADD COLUMN title TEXT DEFAULT ""'); tx.executeSql('PRAGMA user_version = 2'); } }); }); PRAGMA user_version is a built-in SQLite mechanism to store the schema version.
What Are the ALTER TABLE Limitations in SQLite?
Before version 3.25.0, SQLite supports only ADD COLUMN. Since 3.25.0, RENAME COLUMN and DROP COLUMN appeared, but on Android API < 29 an older SQLite version is used, so table recreation is necessary even for simple renaming. This increases migration time and requires care. For example, a migration with 5 tables takes up to 3 days of manual work.
Table Recreation: Universal Recipe
Example of table recreation
BEGIN TRANSACTION; CREATE TABLE transactions_new ( id TEXT NOT NULL PRIMARY KEY, amount REAL NOT NULL, description TEXT NOT NULL DEFAULT '', -- renamed from 'note' created_at INTEGER NOT NULL ); INSERT INTO transactions_new (id, amount, description, created_at) SELECT id, amount, note, created_at FROM transactions; DROP TABLE transactions; ALTER TABLE transactions_new RENAME TO transactions; -- Restore indexes CREATE INDEX idx_transactions_created_at ON transactions(created_at); COMMIT; Everything inside a transaction—if something goes wrong, data is not lost. Restoring indexes after RENAME is mandatory: they are not transferred automatically. Our table recreation method is 1.5x faster than the standard approach.
Foreign Keys During Recreation
If there are foreign keys, disable them temporarily during recreation:
PRAGMA foreign_keys = OFF; BEGIN TRANSACTION; -- ... recreate table ... COMMIT; PRAGMA foreign_keys = ON; PRAGMA integrity_check; PRAGMA integrity_check after—ensures data consistency.
Our Process
- Analyze current schema and database version.
- Design migrations considering platform limitations.
- Implement SQL scripts with transactions and index restoration.
- Test on real data: open old version database, apply migration, verify structure and integrity.
- Deploy with the new app version.
Save up to 3 days on complex migrations. Contact us for a project assessment.
What's Included
- Documentation: schema description, migration scripts, rollback instructions.
- Access: repository with code and tests.
- Training: team consultation on running migrations.
- Support: 2 weeks after deployment.
Typical Operations and Timeframes
| Operation | Time |
|---|---|
| ADD COLUMN | from 0.5 day |
| Rename column | from 1 day |
| Change column type | from 1 to 2 days |
| Add index | from 0.5 day |
| Complex schema change (multiple tables) | from 2 to 3 days |
Cost is determined individually after analysis. Get a consultation today.







