Search code examples
androidsqliteandroid-sqlite

Android Studio SQLite error: Incorrect column ("no such column: _id")


I have set up a SQLite db in an Android app but to focus on the immeidate problem will only reference the Users table. In DatabaseContract the Users table is defined as:

public final class DatabaseContract {
    private DatabaseContract() {}

    public static class Users implements BaseColumns {
        public static final String TABLE_NAME = "users";
        public static final String COLUMN_USER_ID = "_id";
        public static final String COLUMN_USER_NAME = "user_name";
        public static final String COLUMN_PASSWORD = "password";
        public static final String COLUMN_EMAIL = "email";
        public static final String COLUMN_DISPLAY_NAME = "display_name";
    }
    ...rest of tables in db
}

In DatabaseHelper class this is Users table:

    private static final String SQL_CREATE_USERS_TABLE =
            "CREATE TABLE " + DatabaseContract.Users.TABLE_NAME + " (" +
                    DatabaseContract.Users.COLUMN_USER_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    DatabaseContract.Users.COLUMN_USER_NAME + " TEXT NOT NULL, " +
                    DatabaseContract.Users.COLUMN_PASSWORD + " TEXT NOT NULL, " +
                    DatabaseContract.Users.COLUMN_EMAIL + " TEXT, " +
                    DatabaseContract.Users.COLUMN_DISPLAY_NAME + " TEXT)";

In my AddUserActivity class run this method addUserToDatabase(String name, String email, String password, String displayName) which seems to work however I wish to get the auto-generated 'id' from the Users table and then retrieve the 'id' here:

        // Check if the insertion was successful
        if (newRowId != -1) {
            // User was added successfully
            Toast.makeText(this, "User added successfully!", Toast.LENGTH_SHORT).show();

            // Get the user_id of the newly added user
            int user_id = getUserId(db, name);
      ...

Here is the method getUserId(db, name):

   private int getUserId(SQLiteDatabase db, String name) {
        int user_id = -1; // Default value if no user is found

        // Define the columns you want to retrieve
        String[] projection = {
                DatabaseContract.Users.COLUMN_USER_ID
        };

        // Define the selection based on the user name
        String selection = DatabaseContract.Users.COLUMN_USER_NAME + " = ?";
        String[] selectionArgs = { name };

        // Define the sort order (if needed)
        String sortOrder = null;

        Cursor cursor = db.query(
                DatabaseContract.Users.TABLE_NAME,
                projection,
                selection,
                selectionArgs,
                null,
                null,
                sortOrder
        );

        if (cursor.moveToFirst()) {
            // Get the user_id from the cursor
            int usersIdIndex = cursor.getColumnIndexOrThrow(DatabaseContract.Users.COLUMN_USER_ID);
            user_id = cursor.getInt(usersIdIndex);
        }

        // Close the cursor (don't close the database here)
        cursor.close();

        return user_id;
    }

Then I create an intent to pass this data to the WelcomeActivity page:

      ...
            // Create an Intent to navigate to WelcomeActivity
            Intent intent = new Intent(AddUserActivity.this, WelcomeActivity.class);

            int testNumber = 777;
            // Pass user data and user_id as extras in the Intent
            intent.putExtra("USER_ID", user_id);//testNumber
            intent.putExtra("USER_NAME", name);
            intent.putExtra("USER_EMAIL", email);
            intent.putExtra("USER_DISPLAY_NAME", displayName);

            // Start the WelcomeActivity
            startActivity(intent);

            // Close this activity
            finish();

At this point the program crashes with this error:

E/SQLiteLog: (1) no such column: _id in "SELECT _id FROM users WHERE user_name = ?"
D/AndroidRuntime: Shutting down VM
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.enetapplications.songtracker, PID: 5937
    android.database.sqlite.SQLiteException: no such column: _id (code 1 SQLITE_ERROR[1]): , while compiling: SELECT _id FROM users WHERE user_name = ?
        at android.database.sqlite.SQLiteConnection.nativePrepareStatement(Native Method)
        at android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:1478)
        at android.database.sqlite.SQLiteConnection.prepare(SQLiteConnection.java:916)
        at android.database.sqlite.SQLiteSession.prepare(SQLiteSession.java:590)
        at android.database.sqlite.SQLiteProgram.<init>(SQLiteProgram.java:63)
        at android.database.sqlite.SQLiteQuery.<init>(SQLiteQuery.java:37)
        at android.database.sqlite.SQLiteDirectCursorDriver.query(SQLiteDirectCursorDriver.java:46)
        at android.database.sqlite.SQLiteDatabase.rawQueryWithFactory(SQLiteDatabase.java:2088)
        at android.database.sqlite.SQLiteDatabase.queryWithFactory(SQLiteDatabase.java:1935)
        at android.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1806)
        at android.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1974)
        at com.enetapplications.songtracker.AddUserActivity.getUserId(AddUserActivity.java:124)
        at com.enetapplications.songtracker.AddUserActivity.addUserToDatabase(AddUserActivity.java:84)
        at com.enetapplications.songtracker.AddUserActivity.access$400(AddUserActivity.java:20)
        at com.enetapplications.songtracker.AddUserActivity$1.onClick(AddUserActivity.java:58)
        at android.view.View.performClick(View.java:7792)
        at android.widget.TextView.performClick(TextView.java:16112)
        at android.view.View.performClickInternal(View.java:7769)
        at android.view.View.access$3800(View.java:910)
        at android.view.View$PerformClick.run(View.java:30218)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loopOnce(Looper.java:226)
        at android.os.Looper.loop(Looper.java:313)
        at android.app.ActivityThread.main(ActivityThread.java:8663)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1135)

I am new to SQLite thus getting accommodated to SQLite - but very comfortable with standard SQL/MySQL. My tendency would be to name the key to the Users table as user_id however it seems that the SQLite standard is to keep the key to the table as _id

Thanks in advance for reviewing and commenting!

best, Brian


Solution

  • This was a very odd problem

    No it is not, it is how it should be. Unlike Activities etc where onCreate is frequently called a Database is a persistent entity, it would be useless otherwise (at least for it's intended purpose). The data stored within needs to kept irrespective of the state of the device or App (unless uninstalled when the user has chosen to not want the data any longer). Furthermore the data may well have to be kept when an App is updated (which may or may not involve a change to the database's schema).

    Simply put the onCreate method reflects the fact that it is called only when the database is created.

    If changes are then needed then that is when onUpdate comes into play (or the seldom used onDownGrade). These both being driven by the database version, as is passed when construction an class that extends SQLiteOpenHelper.

    I kept getting errors and couldn't undo the origenal db configuration.

    As per the comment. The SQLiteOpenHelper's (and therefore a class that extends SQLiteOpenHelper) onCreate method is called just once for the lifetime of the database. As such any changes to the code, typically where the tables are initially created, will not then be reflected as the code does not run.

    If developing the App before it has been published and the data held by the database can be lost then all that is needed is for the database to be deleted. This can be achieved by uninstalling the App. As there is then no database the onCreate will then be invoked.

    If the App has been published and or the data needs to be retained then the use of the databases version and the onUpdate method can be used to migrate the database from a lower version.

    The solution was to change the db name - just this addition '2': private static final String DATABASE_NAME = "song_tracker2.db";

    This is because you have now specified another database as the database to be used and hence the onCreate method has been invoked and the schema is as expected. However, the original database will still exist and waste some storage.

    Demo Migration

    The following is a working demonstration of how you could utilise the onUpgrade method to introduce the change (even though just uninstalling the App would have sufficed as you were happy to utilise another database).

    What the demo does do is show how data can be retained when utilising the onUpgrade method. It shows a basic way of applying virtually any change to a table. In this case changing the primary key's column name from users_id to _id.

    So consider the following for the DatabaseHelper class (obviously you will have your own):-

    class DatabaseHelper extends SQLiteOpenHelper {
    
        private static final String SQL_CREATE_USERS_TABLE =
                "CREATE TABLE " + DatabaseContract.Users.TABLE_NAME + " (" +
                        DatabaseContract.Users.COLUMN_USER_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                        DatabaseContract.Users.COLUMN_USER_NAME + " TEXT NOT NULL, " +
                        DatabaseContract.Users.COLUMN_PASSWORD + " TEXT NOT NULL, " +
                        DatabaseContract.Users.COLUMN_EMAIL + " TEXT, " +
                        DatabaseContract.Users.COLUMN_DISPLAY_NAME + " TEXT)";
    
        private static volatile DatabaseHelper instance;
    
        private DatabaseHelper(Context context) {
            super(context, DatabaseContract.DATABASE_NAME, null, DatabaseContract.DATABASE_VERSION);
        }
    
        public static DatabaseHelper getInstance(Context context) {
            if (instance == null) {
                instance = new DatabaseHelper(context);
            }
            return instance;
        }
    
    
        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(SQL_CREATE_USERS_TABLE);
            // .... other tables
        }
    
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            if (newVersion <= 2) {migrateDBFromVersion1ToVersion2(db);}
        }
    
        private void migrateDBFromVersion1ToVersion2(SQLiteDatabase db) {
            final String ORIGINAL_PREFIX = "original_";
            db.beginTransaction();
            try {
                db.execSQL("DROP TABLE IF EXISTS " + ORIGINAL_PREFIX + DatabaseContract.Users.TABLE_NAME);
                db.execSQL(
                        "ALTER TABLE " + DatabaseContract.Users.TABLE_NAME +
                                " RENAME TO " + ORIGINAL_PREFIX + DatabaseContract.Users.TABLE_NAME
                );
                db.execSQL(SQL_CREATE_USERS_TABLE);
                db.execSQL("INSERT INTO " + DatabaseContract.Users.TABLE_NAME +
                        " SELECT * FROM " + ORIGINAL_PREFIX + DatabaseContract.Users.TABLE_NAME);
                db.execSQL("DROP TABLE IF EXISTS " + ORIGINAL_PREFIX + DatabaseContract.Users.TABLE_NAME);
                db.setTransactionSuccessful();
            } catch (SQLiteException e) {
                e.printStackTrace();
                //....
            }
            db.endTransaction();
        }
    
        public void logSchema() {
            Cursor csr = instance.getWritableDatabase().rawQuery("SELECT * FROM sqlite_master;", null);
            Log.i("DBVERSION","Database version is " + instance.getWritableDatabase().getVersion() +
                    "; Path to database is " + instance.getWritableDatabase().getPath());
            DatabaseUtils.dumpCursor(csr);
            csr.close();
        }
    }
    

    The important factors in the above are:-

    • the coding within the onUpgrade method, which will if the upgrade is from a version less than 2 to version 2 invoke the migrateDBFromVersion1ToVersion2

    • the basic process followed in migrateDBFromVersion1ToVersion2 that is to:-

      • drop the table that the original table will be renamed to (just in case)
      • rename the original table
      • create the replacement table using the new/changed SQL (which should always also be reflected in the onCreate method, so that new App installs have the same schema)
      • populate the table with the data from the original/previous version of the table (i.e. the table that was renamed)
      • drop the renamed original table (commented out for the running of the demo)
      • all the above undertaken in a single transaction that is committed only if there are no SQLite exceptions. If there is an SQLite Exception then the transaction will be rolled back so no changes are applied.
    • The logSchema method which is akin to your getTableMetaData method.

    The DatabaseContract class then comes into play as the driver, it being changed to include the database name and the version as per:-

    public final class DatabaseContract {
        private DatabaseContract() {}
    
        public static final String DATABASE_NAME = "song_tracker.db";
        public static final int DATABASE_VERSION = 1;
    
        public static class Users implements BaseColumns {
            public static final String TABLE_NAME = "users";
            //public static final String COLUMN_USER_ID = "_id"; /* Version 2 */
            public static final String COLUMN_USER_ID = "users_id"; /* Version 1 */
            public static final String COLUMN_USER_NAME = "user_name";
            public static final String COLUMN_PASSWORD = "password";
            public static final String COLUMN_EMAIL = "email";
            public static final String COLUMN_DISPLAY_NAME = "display_name";
        }
        // ...rest of tables in db
    }
    

    Obviously something needs to be done in an activity so:-

    public class MainActivity extends AppCompatActivity {
    
        DatabaseHelper dbHelper;
    
    
        @SuppressLint("Range")
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            dbHelper = DatabaseHelper.getInstance(this);
            if (DatabaseContract.DATABASE_VERSION == 1) {
                SQLiteDatabase db = dbHelper.getReadableDatabase();
                ContentValues cv = new ContentValues();
                cv.put(DatabaseContract.Users.COLUMN_DISPLAY_NAME,"Fred");
                cv.put(DatabaseContract.Users.COLUMN_EMAIL,"fred.bloggs.nomail.moc");
                cv.put(DatabaseContract.Users.COLUMN_PASSWORD,"0987654321");
                cv.put(DatabaseContract.Users.COLUMN_USER_NAME,"USER00001");
                db.insert(DatabaseContract.Users.TABLE_NAME,null,cv);
            }
            dbHelper.logSchema();
            Cursor csr = dbHelper.getWritableDatabase().query(
                    DatabaseContract.Users.TABLE_NAME,
                    null,null,null,null,null,null
            );
            while (csr.moveToNext()) {
                Log.i(
                        "USERDATA",
                        "User Name is " + csr.getString(csr.getColumnIndex(DatabaseContract.Users.COLUMN_USER_NAME))
                        + " Email is " + csr.getString(csr.getColumnIndex(DatabaseContract.Users.COLUMN_EMAIL))
                        // ....
                );
            }
            csr.close();
        }
    }
    

    So this will:-

    • get an instance of the DatabaseHelper
    • if the database version, as per the code, is 1, then insert a row
      • note at this stage the actual database version may not be the same as the coded version. The actual version of the database is stored in the header of the database file. SQLiteOpenHelper ascertains the version when the database is opened. If there is a difference between the code and the actual version then either onUpgrade or onDowngrade will be invoked.
    • the logSchema function outputs the schema to the log.
    • the data from the users table is extracted and written to the log.

    Result/Test 1 - Fresh Install of the App at Version 1

    The log includes:-

    2023-09-14 07:53:24.666 I/DBVERSION: Database version is 1; Path to database is /data/user/0/a.a.so77093284javasqlitemigration/databases/song_tracker.db
    2023-09-14 07:53:24.666 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@98fd0f5
    2023-09-14 07:53:24.667 I/System.out: 0 {
    2023-09-14 07:53:24.667 I/System.out:    type=table
    2023-09-14 07:53:24.667 I/System.out:    name=android_metadata
    2023-09-14 07:53:24.667 I/System.out:    tbl_name=android_metadata
    2023-09-14 07:53:24.667 I/System.out:    rootpage=3
    2023-09-14 07:53:24.667 I/System.out:    sql=CREATE TABLE android_metadata (locale TEXT)
    2023-09-14 07:53:24.667 I/System.out: }
    2023-09-14 07:53:24.667 I/System.out: 1 {
    2023-09-14 07:53:24.667 I/System.out:    type=table
    2023-09-14 07:53:24.667 I/System.out:    name=users
    2023-09-14 07:53:24.668 I/System.out:    tbl_name=users
    2023-09-14 07:53:24.668 I/System.out:    rootpage=4
    2023-09-14 07:53:24.668 I/System.out:    sql=CREATE TABLE users (users_id INTEGER PRIMARY KEY AUTOINCREMENT, user_name TEXT NOT NULL, password TEXT NOT NULL, email TEXT, display_name TEXT)
    2023-09-14 07:53:24.668 I/System.out: }
    2023-09-14 07:53:24.668 I/System.out: 2 {
    2023-09-14 07:53:24.668 I/System.out:    type=table
    2023-09-14 07:53:24.668 I/System.out:    name=sqlite_sequence
    2023-09-14 07:53:24.668 I/System.out:    tbl_name=sqlite_sequence
    2023-09-14 07:53:24.668 I/System.out:    rootpage=5
    2023-09-14 07:53:24.668 I/System.out:    sql=CREATE TABLE sqlite_sequence(name,seq)
    2023-09-14 07:53:24.668 I/System.out: }
    2023-09-14 07:53:24.668 I/System.out: <<<<<
    2023-09-14 07:53:24.669 I/USERDATA: User Name is USER00001 Email is fred.bloggs.nomail.moc
    

    So the users table has

    • columns users_id, user_name, password, email and display_name. As per the original version.
    • 1 row of data for USER00001

    Result/Test2 - App just rerun with changed code

    For this test 3 lines of code are changed:-

    • public static final int DATABASE_VERSION = 2; (was 1)
    • public static final String COLUMN_USER_ID = "_id"; /* Version 2 */ (was commented out)
    • //public static final String COLUMN_USER_ID = "users_id"; /* Version 1 */ (was not commented out)

    The Log now includes:-

    2023-09-14 08:03:06.896 I/DBVERSION: Database version is 2; Path to database is /data/user/0/a.a.so77093284javasqlitemigration/databases/song_tracker.db
    2023-09-14 08:03:06.896 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@98fd0f5
    2023-09-14 08:03:06.897 I/System.out: 0 {
    2023-09-14 08:03:06.897 I/System.out:    type=table
    2023-09-14 08:03:06.897 I/System.out:    name=android_metadata
    2023-09-14 08:03:06.897 I/System.out:    tbl_name=android_metadata
    2023-09-14 08:03:06.897 I/System.out:    rootpage=3
    2023-09-14 08:03:06.897 I/System.out:    sql=CREATE TABLE android_metadata (locale TEXT)
    2023-09-14 08:03:06.897 I/System.out: }
    2023-09-14 08:03:06.897 I/System.out: 1 {
    2023-09-14 08:03:06.897 I/System.out:    type=table
    2023-09-14 08:03:06.897 I/System.out:    name=original_users
    2023-09-14 08:03:06.897 I/System.out:    tbl_name=original_users
    2023-09-14 08:03:06.897 I/System.out:    rootpage=4
    2023-09-14 08:03:06.898 I/System.out:    sql=CREATE TABLE "original_users" (users_id INTEGER PRIMARY KEY AUTOINCREMENT, user_name TEXT NOT NULL, password TEXT NOT NULL, email TEXT, display_name TEXT)
    2023-09-14 08:03:06.898 I/System.out: }
    2023-09-14 08:03:06.898 I/System.out: 2 {
    2023-09-14 08:03:06.898 I/System.out:    type=table
    2023-09-14 08:03:06.898 I/System.out:    name=sqlite_sequence
    2023-09-14 08:03:06.898 I/System.out:    tbl_name=sqlite_sequence
    2023-09-14 08:03:06.898 I/System.out:    rootpage=5
    2023-09-14 08:03:06.898 I/System.out:    sql=CREATE TABLE sqlite_sequence(name,seq)
    2023-09-14 08:03:06.898 I/System.out: }
    2023-09-14 08:03:06.898 I/System.out: 3 {
    2023-09-14 08:03:06.898 I/System.out:    type=table
    2023-09-14 08:03:06.898 I/System.out:    name=users
    2023-09-14 08:03:06.899 I/System.out:    tbl_name=users
    2023-09-14 08:03:06.899 I/System.out:    rootpage=6
    2023-09-14 08:03:06.899 I/System.out:    sql=CREATE TABLE users (_id INTEGER PRIMARY KEY AUTOINCREMENT, user_name TEXT NOT NULL, password TEXT NOT NULL, email TEXT, display_name TEXT)
    2023-09-14 08:03:06.899 I/System.out: }
    2023-09-14 08:03:06.899 I/System.out: <<<<<
    2023-09-14 08:03:06.900 I/USERDATA: User Name is USER00001 Email is fred.bloggs.nomail.moc
    

    i.e:-

    • The Database version (actual as extracted from the database) has increased to 2.

    • The users table's columns are now"-

      • _id, AS PER THE CHANGE
      • user_name,
      • password,
      • email, and
      • display_name
    • As the execution of the final DROP TABLE original_users was commented out, the now not needed original_users table still exists and it has the users_id column as opposed to the new _id column.

    Result/Test3 Fresh Install - Version 2 code

    The App is uninstalled and then run the log includes:-

    2023-09-14 08:12:01.230 I/DBVERSION: Database version is 2; Path to database is /data/user/0/a.a.so77093284javasqlitemigration/databases/song_tracker.db
    2023-09-14 08:12:01.230 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@98fd0f5
    2023-09-14 08:12:01.230 I/System.out: 0 {
    2023-09-14 08:12:01.230 I/System.out:    type=table
    2023-09-14 08:12:01.230 I/System.out:    name=android_metadata
    2023-09-14 08:12:01.230 I/System.out:    tbl_name=android_metadata
    2023-09-14 08:12:01.230 I/System.out:    rootpage=3
    2023-09-14 08:12:01.231 I/System.out:    sql=CREATE TABLE android_metadata (locale TEXT)
    2023-09-14 08:12:01.231 I/System.out: }
    2023-09-14 08:12:01.231 I/System.out: 1 {
    2023-09-14 08:12:01.231 I/System.out:    type=table
    2023-09-14 08:12:01.231 I/System.out:    name=users
    2023-09-14 08:12:01.231 I/System.out:    tbl_name=users
    2023-09-14 08:12:01.231 I/System.out:    rootpage=4
    2023-09-14 08:12:01.231 I/System.out:    sql=CREATE TABLE users (_id INTEGER PRIMARY KEY AUTOINCREMENT, user_name TEXT NOT NULL, password TEXT NOT NULL, email TEXT, display_name TEXT)
    2023-09-14 08:12:01.231 I/System.out: }
    2023-09-14 08:12:01.231 I/System.out: 2 {
    2023-09-14 08:12:01.231 I/System.out:    type=table
    2023-09-14 08:12:01.231 I/System.out:    name=sqlite_sequence
    2023-09-14 08:12:01.231 I/System.out:    tbl_name=sqlite_sequence
    2023-09-14 08:12:01.231 I/System.out:    rootpage=5
    2023-09-14 08:12:01.231 I/System.out:    sql=CREATE TABLE sqlite_sequence(name,seq)
    2023-09-14 08:12:01.231 I/System.out: }
    2023-09-14 08:12:01.231 I/System.out: <<<<<
    
    • The column is _id,
    • there is no dormant original_users table, and
    • there is no data (as the App only adds data whne the coded version is 1)