Last active
November 22, 2022 13:27
-
-
Save spruce-bruce/97ed3d0fddab3a93082b71c228c7e5a8 to your computer and use it in GitHub Desktop.
Sqlite migrations for react native for use with react-native-sqlite-storage and react-native-sqlite-2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* this class decides what migrations to run and runs them | |
*/ | |
class RNSqliteMigrator { | |
constructor(db) { | |
this._db = db; | |
this.initialize(); | |
this._migrationsMap = {}; | |
this._versions = []; | |
this._cursor = 0; | |
} | |
_next(e) { | |
if (this._versions[this._cursor]) { | |
const next = this._versions[this._cursor]; | |
this._migrationsMap[next].execute(this._db, () => { | |
this._cursor++; | |
this._next(); | |
}); | |
} else { | |
// done! | |
this._cursor = 0; | |
// update version stored in the database to current version | |
this._db.transaction(txn => { | |
txn.executeSql("UPDATE version SET version = :version", [this._db.version]); | |
}); | |
} | |
} | |
initialize() { | |
this._db.transaction(txn => { | |
txn.executeSql("SELECT name FROM sqlite_master WHERE type='table' AND name=:name", ['version'], (tx, result) => { | |
if (!result.rows.length) { | |
tx.executeSql('CREATE TABLE IF NOT EXISTS version (version INTEGER)'); | |
tx.executeSql('CREATE INDEX IF NOT EXISTS version_idx ON version (version);'); | |
tx.executeSql('INSERT INTO version (version) VALUES (:version)', [0]); | |
tx.executeSql('CREATE TABLE IF NOT EXISTS executed_migrations (migration INTEGER)'); | |
tx.executeSql('CREATE INDEX IF NOT EXISTS migration_idx ON executed_migrations (migration);') | |
this.currentVersion = 0; | |
} else { | |
tx.executeSql('SELECT version FROM version LIMIT 1', [], (tx, result) => { | |
this.currentVersion = result.rows.item(0).version; | |
}); | |
} | |
}); | |
}, error => console.error(error), this.migrate.bind(this)); | |
} | |
migrate() { | |
if (typeof this.currentVersion === 'undefined') return; // haven't initialized yet | |
if (this._cursor === 0 && this.currentVersion < this._db.version) { | |
let found = false; | |
for (let i = 0; i < this._versions.length && !found; i++) { | |
if (this.currentVersion >= this._versions[i]) { | |
this._cursor = i + 1; | |
} else { | |
found = true; | |
} | |
} | |
// call migrations | |
this._next(); | |
} | |
} | |
up(migration) { | |
if (this._migrationsMap[migration.version]) throw new Error('Migration with that version number already exists'); | |
this._migrationsMap[migration.version] = migration; | |
this._versions.push(migration.version); | |
this._versions.sort((a, b) => a - b); | |
} | |
} | |
/** | |
* extend this to make new migrations | |
*/ | |
import { Sentry } from 'react-native-sentry'; | |
class RNSqliteMigration { | |
constructor(version, sql) { | |
this.version = version; | |
this.sql = sql; | |
} | |
execute = (db, next) => { | |
function exec(tx, sql) { | |
tx.executeSql(sql); | |
} | |
db.transaction(txn => { | |
txn.executeSql("SELECT migration FROM executed_migrations WHERE migration=:migration", [this.version], (tx, result) => { | |
if (!result.rows.length) { | |
if (Array.isArray(this.sql)) { | |
this.sql.forEach(s => exec(tx, s)); | |
} else { | |
exec(tx, this.sql); | |
} | |
tx.executeSql('INSERT INTO executed_migrations (migration) VALUES (:migration)', [this.version]); | |
} | |
}); | |
}, error => { | |
Sentry.captureException(error, { logger: 'sqlite.migrations' }); | |
next(error); | |
}, next); | |
} | |
} | |
/** | |
* USAGE | |
*/ | |
import db from '../db' // connection for react-native-sqlite-storage OR react-native-sqlite-2 | |
const makeTable = ` | |
CREATE TABLE test ( | |
field TEXT | |
) | |
`; | |
const alterTable = ` | |
ALTER TABLE test ADD COLUMN another_field TEXT | |
`; | |
const migrations = [ | |
new RNSqliteMigration(1, makeTable), | |
new RNSqliteMigration(2, makeAnotherTable), | |
]; | |
const migrator = new RNSqliteMigrator(db); | |
migrator.up(RNSqliteMigration(1, makeTable)) | |
migrator.up(RNSqliteMigration(2, alterTable)) | |
// attempt to run migrations anytime the app state changes to active | |
// if there are new migrations to run (ie, your user has updated the app | |
// and there are new migrations) then the migrator will run only those | |
// migrations | |
AppState.addEventListener('change', () => { | |
if (AppState.currentState === 'active') { | |
migrator.migrate(); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The
this._db.version
is supposed to be the latest version of your database. The version is actually the length or total number of your migrations:migrations.length
. So you can set aversion
property on yourdb
object to bedb.version = migrations.length
. Another option is to set the db version in theexecute
method of theRNSqliteMigration
class. Just before line 96, this line:if (!result.rows.length) {
, you can adddb.version = this.version
.