121

I am using sqlite for my iphone and I anticipate the database schema might change over time. What are the gotchas, naming conventions and things to watch out for to do a successful migration each time?

For example, I have thought of appending a version to the database name (e.g. Database_v1).

Boon
  • 40,656
  • 60
  • 209
  • 315

9 Answers9

136

I maintain an application that periodically needs to update a sqlite database and migrate old databases to the new schema and here's what I do:

For tracking the database version, I use the built in user-version variable that sqlite provides (sqlite does nothing with this variable, you are free to use it however you please). It starts at 0, and you can get/set this variable with the following sqlite statements:

> PRAGMA user_version;  
> PRAGMA user_version = 1;

When the app starts, I check the current user-version, apply any changes that are needed to bring the schema up to date, and then update the user-version. I wrap the updates in a transaction so that if anything goes wrong, the changes aren't committed.

For making schema changes, sqlite supports "ALTER TABLE" syntax for certain operations (renaming the table or adding a column). This is an easy way to update existing tables in-place. See the documentation here: http://www.sqlite.org/lang_altertable.html. For deleting columns or other changes that aren't supported by the "ALTER TABLE" syntax, I create a new table, migrate date into it, drop the old table, and rename the new table to the original name.

Rngbus
  • 2,911
  • 3
  • 22
  • 17
  • 2
    I am trying to have same logic, but for some reason when i execute "pragma user_version = ?" programmatically, it fails... any idea ? – Unicorn May 17 '11 at 16:54
  • 7
    pragma settings do not support parameters, you'll have to provide the actual value: "pragma user_version = 1". – csgero Dec 21 '11 at 09:42
  • 3
    I have one question. Let's say if you a initial version 1. And current version is 5. There are some updates in version 2,3,4. The end user only downloaded your version 1, and now upgrade to version 5. What should you do? – Bagusflyer Apr 10 '14 at 04:09
  • 9
    Update the database in several steps, applying the changes necessary to go from version 1 to version 2, then version 2 to version 3, etc... until it is up to date. An easy way to do this is to have a switch statement where each "case" statement updates the database by one version. You "switch" to the current database version, and the case statements fall through until the update is complete. Whenever you update the database, just add a new case statement. See the answer below by Billy Gray for a detailed example of this. – Rngbus Apr 12 '14 at 02:25
  • You can also use `application_id` instead of `user_version` for that purpose. – Konstantin Tarkus Dec 29 '15 at 18:59
  • 1
    @KonstantinTarkus, according to [documentation](https://sqlite.org/pragma.html#pragma_application_id) `application_id` is an extra bit for identifying file format by `file` utility for example, not for versions of database. – xaizek Dec 12 '16 at 22:10
  • I wonder what if i forget to update the version while doing major changes to the DB. So migration code will not run and can lead to issues. Is there any way to give runtime error like realm gives if there is alteration in schema? – Ammar Mujeeb Jun 13 '18 at 07:48
  • Note however that SQLite explicitly document they give no guarantee of backward compatibility with regard to pragma. It seems very unlikely they remove `user_version`, but still worth considering. Quoting the doc: > "Specific pragma statements may be removed and others added in future releases of SQLite. There is no guarantee of backwards compatibility. " https://www.sqlite.org/pragma.html – Antoine Poinsot Nov 21 '22 at 15:52
35

The answer from Just Curious is dead-on (you got my point!), and it's what we use to track the version of the database schema that is currently in the app.

To run through the migrations that need to occur to get user_version matching the app's expected schema version, we use a switch statement. Here's a cut-up example of what this look like in our app Strip:

- (void) migrateToSchemaFromVersion:(NSInteger)fromVersion toVersion:(NSInteger)toVersion { 
    // allow migrations to fall thru switch cases to do a complete run
    // start with current version + 1
    [self beginTransaction];
    switch (fromVersion + 1) {
        case 3:
            // change pin type to mode 'pin' for keyboard handling changes
            // removing types from previous schema
            sqlite3_exec(db, "DELETE FROM types;", NULL, NULL, NULL);
            NSLog(@"installing current types");
            [self loadInitialData];
        case 4:
            //adds support for recent view tracking
            sqlite3_exec(db, "ALTER TABLE entries ADD COLUMN touched_at TEXT;", NULL, NULL, NULL);
        case 5:
            {
                sqlite3_exec(db, "ALTER TABLE categories ADD COLUMN image TEXT;", NULL, NULL, NULL);
                sqlite3_exec(db, "ALTER TABLE categories ADD COLUMN entry_count INTEGER;", NULL, NULL, NULL);
                sqlite3_exec(db, "CREATE INDEX IF NOT EXISTS categories_id_idx ON categories(id);", NULL, NULL, NULL);
                sqlite3_exec(db, "CREATE INDEX IF NOT EXISTS categories_name_id ON categories(name);", NULL, NULL, NULL);
                sqlite3_exec(db, "CREATE INDEX IF NOT EXISTS entries_id_idx ON entries(id);", NULL, NULL, NULL);

               // etc...
            }
    }

    [self setSchemaVersion];
    [self endTransaction];
}
Tomas Andrle
  • 13,132
  • 15
  • 75
  • 92
Billy Gray
  • 1,747
  • 4
  • 18
  • 23
  • 1
    Well, I did not see where you use ``toVersion``in your code? How is it handled when you are on version 0 and there are two more versions after that. This means you have to migrate from 0 to 1 and from 1 to 2. How do you handle this? – Michael Sep 22 '15 at 21:09
  • 1
    @confile there are no `break` statements in the `switch`, so all subsequent migrations will also happen. – matte Jun 20 '17 at 14:19
  • The Strip link does not exists – Pedro Luz Mar 31 '19 at 10:02
20

Let me share some migration code with FMDB and MBProgressHUD.

Here's how you read and write the schema version number (this is presumably part of a model class, in my case it's a singleton class called Database):

- (int)databaseSchemaVersion {
    FMResultSet *resultSet = [[self database] executeQuery:@"PRAGMA user_version"];
    int version = 0;
    if ([resultSet next]) {
        version = [resultSet intForColumnIndex:0];
    }
    return version;
}

- (void)setDatabaseSchemaVersion:(int)version {
    // FMDB cannot execute this query because FMDB tries to use prepared statements
    sqlite3_exec([self database].sqliteHandle, [[NSString stringWithFormat:@"PRAGMA user_version = %d", DatabaseSchemaVersionLatest] UTF8String], NULL, NULL, NULL);
}

Here's [self database] method that lazily opens the database:

- (FMDatabase *)database {
    if (!_databaseOpen) {
        _databaseOpen = YES;

        NSString *documentsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
        NSString *databaseName = [NSString stringWithFormat:@"userdata.sqlite"];

        _database = [[FMDatabase alloc] initWithPath:[documentsDir stringByAppendingPathComponent:databaseName]];
        _database.logsErrors = YES;

        if (![_database openWithFlags:SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FILEPROTECTION_COMPLETE]) {
            _database = nil;
        } else {
            NSLog(@"Database schema version is %d", [self databaseSchemaVersion]);
        }
    }
    return _database;
}

And here are migration methods called from the view controller:

- (BOOL)databaseNeedsMigration {
    return [self databaseSchemaVersion] < databaseSchemaVersionLatest;
}

- (void)migrateDatabase {
    int version = [self databaseSchemaVersion];
    if (version >= databaseSchemaVersionLatest)
        return;

    NSLog(@"Migrating database schema from version %d to version %d", version, databaseSchemaVersionLatest);

    // ...the actual migration code...
    if (version < 1) {
        [[self database] executeUpdate:@"CREATE TABLE foo (...)"];
    }

    [self setDatabaseSchemaVersion:DatabaseSchemaVersionLatest];
    NSLog(@"Database schema version after migration is %d", [self databaseSchemaVersion]);
}

And here's the root view controller code that invokes the migration, using MBProgressHUD to display a progress bezel:

- (void)viewDidAppear {
    [super viewDidAppear];
    if ([[Database sharedDatabase] userDatabaseNeedsMigration]) {
        MBProgressHUD *hud = [[MBProgressHUD alloc] initWithView:self.view.window];
        [self.view.window addSubview:hud];
        hud.removeFromSuperViewOnHide = YES;
        hud.graceTime = 0.2;
        hud.minShowTime = 0.5;
        hud.labelText = @"Upgrading data";
        hud.taskInProgress = YES;
        [[UIApplication sharedApplication] beginIgnoringInteractionEvents];

        [hud showAnimated:YES whileExecutingBlock:^{
            [[Database sharedDatabase] migrateUserDatabase];
        } onQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0) completionBlock:^{
            [[UIApplication sharedApplication] endIgnoringInteractionEvents];
        }];
    }
}
Andrey Tarantsov
  • 8,965
  • 7
  • 54
  • 58
  • Note: I'm not fully satisfied with how the code is organized (I'd prefer opening and migration to be parts of a single operation, preferably invoked by the app delegate), but it works, and I thought I'd share anyway. – Andrey Tarantsov Sep 17 '12 at 23:29
  • Why do you use "setDatabaseSchemaVersion" method to return "user_version" ? "user_version" and "schema_version" are two different pragmas I think. – Paweł Brewczynski Jan 05 '15 at 09:34
  • @PaulBrewczynski Because I prefer the commonly used terms, not SQLite terms, and also I'm calling it by what it is (the version of my database schema). I don't care about SQLite-specific terms in this case, and `schema_version` pragma isn't normally something people deal with either. – Andrey Tarantsov Jan 05 '15 at 13:43
  • You've written: // FMDB cannot execute this query because FMDB tries to use prepared statements. What do you mean by this? This should work: NSString *query = [NSString stringWithFormat:@"PRAGMA USER_VERSION = %i", userVersion]; [_db executeUpdate:query]; As noted here:http://stackoverflow.com/a/21244261/1364174 – Paweł Brewczynski Jan 06 '15 at 18:46
  • 1
    (related to my comment above) NOTE: FMDB library now features: userVersion and setUserVersion: methods! So you don't have to use verbose @Andrey Tarantsov's methods: - (int)databaseSchemaVersion! and (void)setDatabaseSchemaVersion:(int)version. FMDB documentation: http://ccgus.github.io/fmdb/html/Categories/FMDatabase+FMDatabaseAdditions.html#//api/name/setUserVersion: – Paweł Brewczynski Jan 06 '15 at 20:45
  • @PaulBrewczynski Cool, I'm looking forward to trimming my code when I get to it. (And yes, previously PRAGMA didn't work with executeUpdate.) – Andrey Tarantsov Jan 07 '15 at 12:50
11

1. Create /migrations folder with the list of SQL-based migrations, where each migration looks something like this:

/migrations/001-categories.sql

-- Up
CREATE TABLE Category (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO Category (id, name) VALUES (1, 'Test');

-- Down
DROP TABLE Category;

/migrations/002-posts.sql

-- Up
CREATE TABLE Post (id INTEGER PRIMARY KEY, categoryId INTEGER, text TEXT);

-- Down
DROP TABLE Post;

2. Create db table containing the list of applied migrations, for example:

CREATE TABLE Migration (name TEXT);

3. Update application bootstrap logic so that before it starts, it grabs the list of migrations from the /migrations folder and runs the migrations that have not yet been applied.

Here is an example implemented with JavaScript: SQLite Client for Node.js Apps

Kerem Baydoğan
  • 10,475
  • 1
  • 43
  • 50
Konstantin Tarkus
  • 37,618
  • 14
  • 135
  • 121
4

The best solution IMO is to build a SQLite upgrade framework. I had the same problem (in the C# world) and I built my own such framework. You can read about it here. It works perfectly and makes my (previously nightmarish) upgrades work with minimal effort on my side.

Although the library is implemented in C#, the ideas presented there should work fine in your case also.

Andrew Barber
  • 39,603
  • 20
  • 94
  • 123
Liron Levi
  • 1,139
  • 6
  • 10
2

For .net you can use lib:

EntityFrameworkCore.Sqlite.Migrations

It is simple, so for any other platform you can easily implement the same behavior as in lib.

ichensky
  • 41
  • 1
  • 6
2

Some tips...

1) I recommend putting all the code to migrate your database into an NSOperation and running it in the background thread. You can show a custom UIAlertView with a spinner while the database is being migrated.

2) Make sure you are copying your database from the bundle into the app's documents and using it from that location, otherwise you will just overwrite the whole database with each app update, and then migrate the new empty database.

3) FMDB is great, but its executeQuery method can't do PRAGMA queries for some reason. You'll need to write your own method that uses sqlite3 directly if you want to check the schema version using PRAGMA user_version.

4) This code structure will ensure that your updates are executed in order, and that all updates are executed, no matter how long the user goes between app updates. It could be refactored further, but this is a very simple way to look at it. This method can safely be run every time your data singleton is instantiated, and only costs one tiny db query that only happens once per session if you set up your data singleton properly.

- (void)upgradeDatabaseIfNeeded {
    if ([self databaseSchemaVersion] < 3)
    {
        if ([self databaseSchemaVersion] < 2)
        {
            if ([self databaseSchemaVersion] < 1)
            {
                // run statements to upgrade from 0 to 1
            }
            // run statements to upgrade from 1 to 2
        }
        // run statements to upgrade from 2 to 3

        // and so on...

        // set this to the latest version number
        [self setDatabaseSchemaVersion:3];
    }
}
Rich Joslin
  • 204
  • 2
  • 9
1

In my article Simple declarative schema migration for SQLite we work out the schema changes automatically by creating a pristine in-memory database, and comparing the schema against your current database by querying the "sqlite_schema" tables from both. Then we follow the 12 step procedure from the SQLite documentation to safely modify the tables.

You can define the schema however you like (an ORM, or plain SQL "CREATE TABLE" statements, etc) as long as you can use it to create a new in-memory database. This means that you only have to maintain the schema in one place, and changes are applied automatically when your application starts up.

Of course there are limitations — in particular this doesn't handle data migrations, only schema migrations; and new columns must allow null or have a default value specified. But overall it's a joy to work with.

David Röthlisberger
  • 1,786
  • 15
  • 20
1

If you change the database schema and all code that's using it in lockstep, as is likely to be the case in embedded and phone-located apps, the problem is actually well under control (nothing comparable to the nightmare that's schema migration on an enterprise DB that may be serving hundreds of apps -- not all under the DBA's control either;-).

Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395