Skip to content

Instantly share code, notes, and snippets.

@spruce-bruce
Last active November 22, 2022 13:27
Show Gist options
  • Save spruce-bruce/97ed3d0fddab3a93082b71c228c7e5a8 to your computer and use it in GitHub Desktop.
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 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();
}
});
@kapobajza
Copy link

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 a version property on your db object to be db.version = migrations.length. Another option is to set the db version in the execute method of the RNSqliteMigration class. Just before line 96, this line: if (!result.rows.length) {, you can add db.version = this.version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment