Search code examples
androidandroid-sqliteandroid-room

Pre-packaged database has an invalid schema DESPITE not using createFromFile/Asset


I've been searching everywhere for the cause of this error:

TableInfo{name='recipes', columns={cook_time=Column{name='cook_time', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, tier_rating=Column{name='tier_rating', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, notes=Column{name='notes', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, tom_rating=Column{name='tom_rating', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, prep_time=Column{name='prep_time', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, serves=Column{name='serves', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='1'}, name=Column{name='name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}, url=Column{name='url', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[Index{name='index_recipes_name', unique=true, columns=[name]}]}
     Found:
TableInfo{name='recipes', columns={}, foreignKeys=[], indices=[]}
at androidx.room.RoomOpenHelper.checkIdentity(RoomOpenHelper.java:163)
    at androidx.room.RoomOpenHelper.onOpen(RoomOpenHelper.java:135)
    at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onOpen(FrameworkSQLiteOpenHelper.java:195)
    at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:428)
    at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:317)
    at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:145)
    at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:106)
    at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:622)
    at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:399)
    at androidx.room.RoomDatabase.query(RoomDatabase.java:442)
    at androidx.room.util.DBUtil.query(DBUtil.java:83)
    at com.example.shoppinglistapp2.db.dao.IngListItemDao_Impl$10.call(IngListItemDao_Impl.java:1036)
    at com.example.shoppinglistapp2.db.dao.IngListItemDao_Impl$10.call(IngListItemDao_Impl.java:1033)
    at androidx.room.RoomTrackingLiveData$1.run(RoomTrackingLiveData.java:90)

Every other similar problem seems to have at least SOME table elements, or is using the databaseBuilder.createFromAsset feature. I however am not, my database code is pasted below. I've tried removing all my migrations and just falling back to destructive migration. I've looked at the exported schema files, which clearly show all the properties correctly. Everything was working fine in my app, until I tried installing it on a new device which had no existing instance of the database. I can run it fine on a device with existing database data from previous builds/use of the app, but fresh installs crash immediately on startup and I get the above error. I hadn't tried this before, so I don't know at what point this problem may have been introduced.

I was using Room version '2.4.0-alpha01', but I've also tried going back to 2.3.0 with no difference. Could there be some external library that is trying to pre-populate that I don't know about? I'm not sure where it's even finding the schema with empty tables showing up in the "found" section - I don't even HAVE an /assets folder that it could be trying to get createFromAsset resource from even if I was calling that.

SlaDatabase.java

@Database(entities = {Recipe.class, IngList.class, IngListItem.class, Tag.class, MealPlan.class, Meal.class},
        version = 17,
        exportSchema = true
//        autoMigrations = {
//                @AutoMigration(from = 15,to = 16),
//                @AutoMigration(from = 16, to = 17)
//        }
)
public abstract class SlaDatabase extends RoomDatabase {
    public abstract RecipeDao recipeDao();
    public abstract IngListItemDao ingListItemDao();
    public abstract MealDao mealDao();
    public abstract TagDao tagDao();
    public abstract MealPlanDao mealPlanDao();
    public abstract IngListDao ingListDao();

    private static volatile SlaDatabase INSTANCE;
    private static final int NUMBER_OF_THREADS = 4;
    static final ListeningExecutorService databaseWriteExecutor =
            MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(NUMBER_OF_THREADS));

    static SlaDatabase getDatabase(final Context context) {
        if (INSTANCE == null) {
            synchronized (SlaDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                            SlaDatabase.class, "sla_database")
                            .fallbackToDestructiveMigration()
                            .addCallback(sRoomDatabaseCallback)
                            .build();
                }
            }
        }
        return INSTANCE;
    }

    //ensure the shopping list IngList (id = 0) exists
    private static RoomDatabase.Callback sRoomDatabaseCallback = new RoomDatabase.Callback(){
        @Override
        public void onCreate(@NonNull SupportSQLiteDatabase db) {
            super.onCreate(db);
            db.beginTransaction();
            try {
                db.execSQL("INSERT OR IGNORE INTO ing_lists DEFAULT VALUES");
            } catch(Exception e){
                Log.d("TOM_TEST", e.toString());;
            }
            finally {
                db.endTransaction();
            }
        }
    };

Recipe.java

@Entity(
        tableName = "recipes",
        indices = {@Index(value = {"name"}, unique = true)}//the recipe name must be unique
)
public class Recipe {

    @PrimaryKey(autoGenerate = true)
    private int id;

    @NonNull
    private String name;

    @ColumnInfo(name="prep_time")
    private int prepTime;

    @ColumnInfo(name = "cook_time")
    private int cookTime;

    @ColumnInfo(defaultValue = "1")
    private int serves = 1;

    private String url;
    private String notes;

    private int tom_rating;
    private int tier_rating;
    //getters and setters...

Exported schema for version 17 of SlaDatabase

{
  "formatVersion": 1,
  "database": {
    "version": 17,
    "identityHash": "fe979e62da4f94c5bb89144e955361e0",
    "entities": [
      {
        "tableName": "recipes",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `prep_time` INTEGER NOT NULL, `cook_time` INTEGER NOT NULL, `serves` INTEGER NOT NULL DEFAULT 1, `url` TEXT, `notes` TEXT, `tom_rating` INTEGER NOT NULL, `tier_rating` INTEGER NOT NULL)",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "name",
            "columnName": "name",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "prepTime",
            "columnName": "prep_time",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "cookTime",
            "columnName": "cook_time",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "serves",
            "columnName": "serves",
            "affinity": "INTEGER",
            "notNull": true,
            "defaultValue": "1"
          },
          {
            "fieldPath": "url",
            "columnName": "url",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "notes",
            "columnName": "notes",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "tom_rating",
            "columnName": "tom_rating",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "tier_rating",
            "columnName": "tier_rating",
            "affinity": "INTEGER",
            "notNull": true
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_recipes_name",
            "unique": true,
            "columnNames": [
              "name"
            ],
            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_recipes_name` ON `${TABLE_NAME}` (`name`)"
          }
        ],
        "foreignKeys": []
      },
      {
        "tableName": "ing_lists",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` INTEGER, `meal_plan_id` INTEGER, FOREIGN KEY(`recipe_id`) REFERENCES `recipes`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`meal_plan_id`) REFERENCES `meal_plans`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "recipeId",
            "columnName": "recipe_id",
            "affinity": "INTEGER",
            "notNull": false
          },
          {
            "fieldPath": "mealPlanId",
            "columnName": "meal_plan_id",
            "affinity": "INTEGER",
            "notNull": false
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_ing_lists_recipe_id",
            "unique": false,
            "columnNames": [
              "recipe_id"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_ing_lists_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
          },
          {
            "name": "index_ing_lists_meal_plan_id",
            "unique": false,
            "columnNames": [
              "meal_plan_id"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_ing_lists_meal_plan_id` ON `${TABLE_NAME}` (`meal_plan_id`)"
          }
        ],
        "foreignKeys": [
          {
            "table": "recipes",
            "onDelete": "CASCADE",
            "onUpdate": "NO ACTION",
            "columns": [
              "recipe_id"
            ],
            "referencedColumns": [
              "id"
            ]
          },
          {
            "table": "meal_plans",
            "onDelete": "CASCADE",
            "onUpdate": "NO ACTION",
            "columns": [
              "meal_plan_id"
            ],
            "referencedColumns": [
              "id"
            ]
          }
        ]
      },
      {
        "tableName": "ing_list_items",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `list_id` INTEGER NOT NULL, `volume_unit` TEXT, `volume_qty` REAL NOT NULL, `mass_unit` TEXT, `mass_qty` REAL NOT NULL, `whole_item_qty` REAL NOT NULL, `other_unit` TEXT, `other_qty` REAL NOT NULL, `checked` INTEGER NOT NULL DEFAULT 0, `list_order` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`list_id`) REFERENCES `ing_lists`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "name",
            "columnName": "name",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "listId",
            "columnName": "list_id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "volumeUnit",
            "columnName": "volume_unit",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "volumeQty",
            "columnName": "volume_qty",
            "affinity": "REAL",
            "notNull": true
          },
          {
            "fieldPath": "massUnit",
            "columnName": "mass_unit",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "massQty",
            "columnName": "mass_qty",
            "affinity": "REAL",
            "notNull": true
          },
          {
            "fieldPath": "wholeItemQty",
            "columnName": "whole_item_qty",
            "affinity": "REAL",
            "notNull": true
          },
          {
            "fieldPath": "otherUnit",
            "columnName": "other_unit",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "otherQty",
            "columnName": "other_qty",
            "affinity": "REAL",
            "notNull": true
          },
          {
            "fieldPath": "checked",
            "columnName": "checked",
            "affinity": "INTEGER",
            "notNull": true,
            "defaultValue": "0"
          },
          {
            "fieldPath": "listOrder",
            "columnName": "list_order",
            "affinity": "INTEGER",
            "notNull": true,
            "defaultValue": "0"
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_ing_list_items_name",
            "unique": false,
            "columnNames": [
              "name"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_ing_list_items_name` ON `${TABLE_NAME}` (`name`)"
          },
          {
            "name": "index_ing_list_items_list_id",
            "unique": false,
            "columnNames": [
              "list_id"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_ing_list_items_list_id` ON `${TABLE_NAME}` (`list_id`)"
          }
        ],
        "foreignKeys": [
          {
            "table": "ing_lists",
            "onDelete": "CASCADE",
            "onUpdate": "NO ACTION",
            "columns": [
              "list_id"
            ],
            "referencedColumns": [
              "id"
            ]
          }
        ]
      },
      {
        "tableName": "tags",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `recipe_id` INTEGER NOT NULL, FOREIGN KEY(`recipe_id`) REFERENCES `recipes`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "name",
            "columnName": "name",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "recipeId",
            "columnName": "recipe_id",
            "affinity": "INTEGER",
            "notNull": true
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_tags_recipe_id",
            "unique": false,
            "columnNames": [
              "recipe_id"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_tags_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
          }
        ],
        "foreignKeys": [
          {
            "table": "recipes",
            "onDelete": "CASCADE",
            "onUpdate": "NO ACTION",
            "columns": [
              "recipe_id"
            ],
            "referencedColumns": [
              "id"
            ]
          }
        ]
      },
      {
        "tableName": "meal_plans",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT)",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "name",
            "columnName": "name",
            "affinity": "TEXT",
            "notNull": false
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [],
        "foreignKeys": []
      },
      {
        "tableName": "meals",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `plan_id` INTEGER NOT NULL, `day_id` INTEGER NOT NULL, `day_title` TEXT, `recipe_id` INTEGER, `notes` TEXT, FOREIGN KEY(`recipe_id`) REFERENCES `recipes`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`plan_id`) REFERENCES `meal_plans`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "planId",
            "columnName": "plan_id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "dayId",
            "columnName": "day_id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "dayTitle",
            "columnName": "day_title",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "recipeId",
            "columnName": "recipe_id",
            "affinity": "INTEGER",
            "notNull": false
          },
          {
            "fieldPath": "notes",
            "columnName": "notes",
            "affinity": "TEXT",
            "notNull": false
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_meals_plan_id",
            "unique": false,
            "columnNames": [
              "plan_id"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_meals_plan_id` ON `${TABLE_NAME}` (`plan_id`)"
          },
          {
            "name": "index_meals_recipe_id",
            "unique": false,
            "columnNames": [
              "recipe_id"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_meals_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
          }
        ],
        "foreignKeys": [
          {
            "table": "recipes",
            "onDelete": "SET NULL",
            "onUpdate": "NO ACTION",
            "columns": [
              "recipe_id"
            ],
            "referencedColumns": [
              "id"
            ]
          },
          {
            "table": "meal_plans",
            "onDelete": "CASCADE",
            "onUpdate": "NO ACTION",
            "columns": [
              "plan_id"
            ],
            "referencedColumns": [
              "id"
            ]
          }
        ]
      }
    ],
    "views": [],
    "setupQueries": [
      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fe979e62da4f94c5bb89144e955361e0')"
    ]
  }
}

Solution

  • You are ending the transaction without using setTransactionSuccessful. Without, endTransaction will roll back changes, effectively undoing changes including the creation of the tables that Room has created.

    Thus, due to the rollback, the first table inspection finds no (Recipe) table that matches the expected (Recipe) table hence the found consisting of no columns etc.

    • It says pre-packaged database because Room appears to assume that this non-conformant database must have come from elsewhere (it's not what Room would create); as such it is a little misleading (but what should it say? (rhetorical)).

    You need to use/apply the setTransactionSuccessful() method e.g.

    private static RoomDatabase.Callback sRoomDatabaseCallback = new RoomDatabase.Callback() {
        @Override
        public void onCreate(@NonNull SupportSQLiteDatabase db) {
            super.onCreate(db);
            db.beginTransaction();
            try {
                db.execSQL("INSERT OR IGNORE INTO ing_lists DEFAULT VALUES");
            } catch (Exception e) {
                Log.d("TOM_TEST", e.toString());
            } finally {
                db.setTransactionSuccessful(); //<<<<<<<<<<
                db.endTransaction();
            }
        }
    };
    

    An alternative solution would be to not use a transaction e.g.

    private static RoomDatabase.Callback sRoomDatabaseCallback = new RoomDatabase.Callback() {
        @Override
        public void onCreate(@NonNull SupportSQLiteDatabase db) {
            super.onCreate(db);
            try {
                db.execSQL("INSERT OR IGNORE INTO ing_lists DEFAULT VALUES");
            } catch (Exception e) {
                Log.d("TOM_TEST", e.toString());
            }
        }
    };
    

    There is no disadvantage as executing a single SQL statement is in itself a transaction.