15

I am getting an error from Logcat saying that a certain column (in my SQLiteOpenHelper subclass) does not exist. I thought I could upgrade the database by changing the DATABASE_CREATE string. But apparently not, so how can I (step-by-step) upgrade my SQLite Database from version 1 to version 2?

I apologize if the question seems "noobish", but I am still learning about Android.

@Pentium10 This is what I do in onUpgrade:

private static final int DATABASE_VERSION = 1;

....

switch (upgradeVersion) {
case 1:
    db.execSQL("ALTER TABLE task ADD body TEXT");
    upgradeVersion = 2;
    break;
}

...
Pratik Butani
  • 60,504
  • 58
  • 273
  • 437
Mohit Deshpande
  • 53,877
  • 76
  • 193
  • 251
  • See also https://stackoverflow.com/questions/8133597/android-upgrading-db-version-and-adding-new-table – Suragch Feb 26 '18 at 07:32

3 Answers3

49

Ok, before you run into bigger problems you should know that SQLite is limited on the ALTER TABLE command, it allows add and rename only no remove/drop which is done with recreation of the table.

You should always have the new table creation query at hand, and use that for upgrade and transfer any existing data. Note: that the onUpgrade methods runs one for your sqlite helper object and you need to handle all the tables in it.

So what is recommended onUpgrade:

  • beginTransaction
  • run a table creation with if not exists (we are doing an upgrade, so the table might not exists yet, it will fail alter and drop)
  • put in a list the existing columns List<String> columns = DBUtils.GetColumns(db, TableName);
  • backup table (ALTER table " + TableName + " RENAME TO 'temp_" + TableName)
  • create new table (the newest table creation schema)
  • get the intersection with the new columns, this time columns taken from the upgraded table (columns.retainAll(DBUtils.GetColumns(db, TableName));)
  • restore data (String cols = StringUtils.join(columns, ","); db.execSQL(String.format( "INSERT INTO %s (%s) SELECT %s from temp_%s", TableName, cols, cols, TableName)); )
  • remove backup table (DROP table 'temp_" + TableName)
  • setTransactionSuccessful

.

public static List<String> GetColumns(SQLiteDatabase db, String tableName) {
    List<String> ar = null;
    Cursor c = null;
    try {
        c = db.rawQuery("select * from " + tableName + " limit 1", null);
        if (c != null) {
            ar = new ArrayList<String>(Arrays.asList(c.getColumnNames()));
        }
    } catch (Exception e) {
        Log.v(tableName, e.getMessage(), e);
        e.printStackTrace();
    } finally {
        if (c != null)
            c.close();
    }
    return ar;
}

public static String join(List<String> list, String delim) {
    StringBuilder buf = new StringBuilder();
    int num = list.size();
    for (int i = 0; i < num; i++) {
        if (i != 0)
            buf.append(delim);
        buf.append((String) list.get(i));
    }
    return buf.toString();
}
Pentium10
  • 204,586
  • 122
  • 423
  • 502
  • Could I haved used CREATE TEMPORARY TABLE IF NOT EXISTS instead of CREATE TABLE IF NOT EXISTS? – Mohit Deshpande Aug 06 '10 at 15:33
  • No, as you will lose it when the connection drops. Always keep your table schema as `CREATE TABLE IF NOT EXISTS` – Pentium10 Aug 06 '10 at 15:38
  • How does this incorporate the version of the database? – Mohit Deshpande Aug 06 '10 at 16:03
  • Everytime the onUpgrade method runs it detects a version change. You don't need to keep record of each change of your database (as in your question). You just need the **latest** table creation schema. The method I described creates a brand new table with the new table schema and transfers the existing records. – Pentium10 Aug 06 '10 at 16:13
  • So am I doing it right in my code sample? Everytime I would change the DATABASE_VERSION up one. Then change the upgradeVersion up one? – Mohit Deshpande Aug 06 '10 at 16:20
  • When you edit a change in your table schemas you increase DATABASE_VERSION. As far I see you don't need the upgradeVersion. – Pentium10 Aug 06 '10 at 16:22
  • So I use the switch statement for DATABASE_VERSION? Instead of oldVersion. – Mohit Deshpande Aug 06 '10 at 16:31
  • You probably don't understand clearly the idea I described. Why do you want to do a switch statement on the version? What do you want to do different for each version apart? – Pentium10 Aug 06 '10 at 19:14
  • Ok so I would change create a new table schema based on the version. Then when I want to upgrade, I would change the schema again in the onUpgrade method? Could you please post a code sample including and onUpgrade in version 1 and onUpgrade in version 2. (Use whatever table schema) – Mohit Deshpande Aug 07 '10 at 17:27
  • My onUpgrade method just contains the above stuff, plus all of the code is inside of a cycle(on a list) where I step on all tables, so I do the upgrade on all tables. – Pentium10 Aug 07 '10 at 17:39
  • 3
    I don't use the versions. You don't need. You always upgrade to latest version, you don't need to upgrade for all. Take this: you have v1, v10, v50, v80. And your current defined version is v100. Even if you upgrade from v1 or v50, you need to have the table schema of v100. So I just create **latest** (that means the v100) table schema and I don't bother what version I upgraded from. I don't need to know if it was v1 or v50, it's version independent. You always upgrade to latest and you don't need to know from what version. – Pentium10 Aug 07 '10 at 17:42
  • When you say "run a table creation" what do you mean? – Mohit Deshpande Aug 07 '10 at 19:45
  • create table if not exits query – Pentium10 Aug 07 '10 at 19:59
  • AFAIK android does not support that. – Pentium10 Aug 12 '10 at 19:37
  • [SQLiteOpenHelper onDowngrade](https://developer.android.com/reference/android/database/sqlite/SQLiteOpenHelper.html#onDowngrade(android.database.sqlite.SQLiteDatabase,%20int,%20int)) – Suragch Feb 26 '18 at 07:43
  • Could be issues if the table name gets changed. See [this article](https://riggaroo.co.za/android-sqlite-database-use-onupgrade-correctly/). – Suragch Feb 26 '18 at 07:45
2

Wouldn't something like the following be easier for the vast majority of cases? Just add the new column for each version upgrade:

private static final String DATABASE_ALTER_TEAM_1 = "ALTER TABLE "
    + TABLE_TEAM + " ADD COLUMN " + COLUMN_COACH + " string;";

private static final String DATABASE_ALTER_TEAM_2 = "ALTER TABLE "
    + TABLE_TEAM + " ADD COLUMN " + COLUMN_STADIUM + " string;";

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    if (oldVersion < 2) {
         db.execSQL(DATABASE_ALTER_TEAM_1);
    }
    if (oldVersion < 3) {
         db.execSQL(DATABASE_ALTER_TEAM_2);
    }
}

For a bit more on this, check out this blog.

PFranchise
  • 6,642
  • 11
  • 56
  • 73
1

Here is how I upgrade my database.

In a previous version of my app, the gameType column doesn't exist. In the new version, it does.

  void upgradeDatabase() throws IOException {
    try {
      String column = DatabaseConstants.GAME_TYPE_COLUMN_NAME; // gameType
      String table = DatabaseConstants.RECORDS_TABLE;
      String query = String.format("SELECT %s FROM %s LIMIT 1", column, table);
      database.rawQuery(query, null);
      return;
    }
    catch (Exception e) {
      // Column doesn't exist. User had old version of app installed, so upgrade database.
    }

    // Save all old data
    String query = "SELECT * FROM " + DatabaseConstants.USERS_TABLE;
    Cursor c = database.rawQuery(query, null);
    List<List<Object>> values1 = new ArrayList<List<Object>>();
    if (c.moveToFirst()) {
      while (!c.isAfterLast()) {
        List<Object> record = new ArrayList<Object>();
        record.add(c.getInt(0));
        record.add(c.getString(1));
        values1.add(record);
        c.moveToNext();
      }
    }
    c.close();

    query = "SELECT * FROM " + DatabaseConstants.RECORDS_TABLE;
    c = database.rawQuery(query, null);
    List<List<Object>> values2 = new ArrayList<List<Object>>();
    if (c.moveToFirst()) {
      while (!c.isAfterLast()) {
        List<Object> record = new ArrayList<Object>();
        record.add(c.getInt(0));
        record.add(c.getInt(1));
        record.add(c.getInt(2));
        record.add(c.getInt(3));
        values2.add(record);
        c.moveToNext();
      }
    }
    c.close();

    // Copy empty database with new schema
    copyDatabase();

    // Restore all old data
    for (List<Object> record : values1) {
      ContentValues cv = new ContentValues();
      cv.put(DatabaseConstants.ID_COLUMN_NAME, (Integer) record.get(0));
      cv.put(DatabaseConstants.USERNAME_COLUMN_NAME, record.get(1).toString());
      database.insert(DatabaseConstants.USERS_TABLE, null, cv);
    }
    for (List<Object> record : values2) {
      ContentValues cv = new ContentValues();
      cv.put(DatabaseConstants.USER_ID_COLUMN_NAME, (Integer) record.get(0));
      cv.put(DatabaseConstants.GAME_TYPE_COLUMN_NAME, GameType.CLASSIC.name());
      cv.put(DatabaseConstants.WINS_COLUMN_NAME, (Integer) record.get(1));
      cv.put(DatabaseConstants.LOSSES_COLUMN_NAME, (Integer) record.get(2));
      cv.put(DatabaseConstants.TIES_COLUMN_NAME, (Integer) record.get(3));
      database.insert(DatabaseConstants.RECORDS_TABLE, null, cv);
    }
  }

Here's the code to copy the database file. The database is initially empty, and I created it outside my app. (I used a program called Navicat for SQLite.)

  public DatabaseHelper(Context context) {
    super(context, DatabaseConstants.DATABASE_NAME, null, 1);
    this.context = context;
    databasePath = context.getDatabasePath(DatabaseConstants.DATABASE_NAME).getPath();
  }

  void copyDatabase() throws IOException {
    InputStream is = context.getAssets().open(DatabaseConstants.DATABASE_NAME); // data.db
    OutputStream os = new FileOutputStream(databasePath);

    byte[] buffer = new byte[1024];
    int length;
    while ((length = is.read(buffer)) > 0) {
      os.write(buffer, 0, length);
    }

    // Close the streams.
    os.flush();
    os.close();
    is.close();
  }
BJ Dela Cruz
  • 5,194
  • 13
  • 51
  • 84