Added Timetable.kt which was a PAIN because SQLite does not support ADD FOREIGN KEY on ALTER TABLE, and as such, the required workaround is to create a temporary table with the new constraint added, insert the values from the old table into the new table, drop the old table, and finally rename the new table to the name of the old table so that the app thinks that nothing ever happened... UGH

This commit is contained in:
denizk0461
2023-07-03 15:47:55 +02:00
parent 97b713a578
commit 694d962896
9 changed files with 480 additions and 11 deletions
@@ -0,0 +1,387 @@
{
"formatVersion": 1,
"database": {
"version": 22,
"identityHash": "d55bef3b8778cd6346bf65455993008d",
"entities": [
{
"tableName": "timetables",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "studip_events",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timetableId` INTEGER NOT NULL, `title` TEXT NOT NULL, `lecturer` TEXT NOT NULL, `room` TEXT NOT NULL, `day` INTEGER NOT NULL, `timeslotStart` TEXT NOT NULL, `timeslotEnd` TEXT NOT NULL, `timeslotId` INTEGER NOT NULL, `colour` INTEGER NOT NULL, FOREIGN KEY(`timetableId`) REFERENCES `timetables`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "eventId",
"columnName": "eventId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timetableId",
"columnName": "timetableId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lecturer",
"columnName": "lecturer",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "room",
"columnName": "room",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "day",
"columnName": "day",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timeslotStart",
"columnName": "timeslotStart",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timeslotEnd",
"columnName": "timeslotEnd",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timeslotId",
"columnName": "timeslotId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "colour",
"columnName": "colour",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"eventId"
]
},
"indices": [],
"foreignKeys": [
{
"table": "timetables",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"timetableId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "offer_date",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `date` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "offer_canteen",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `canteen` TEXT NOT NULL, `openingHours` TEXT NOT NULL, `news` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canteen",
"columnName": "canteen",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "openingHours",
"columnName": "openingHours",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "news",
"columnName": "news",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "offer_category",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dateId` INTEGER NOT NULL, `canteenId` INTEGER NOT NULL, `category` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`dateId`) REFERENCES `offer_date`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`canteenId`) REFERENCES `offer_canteen`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateId",
"columnName": "dateId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canteenId",
"columnName": "canteenId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "category",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": [
{
"table": "offer_date",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"dateId"
],
"referencedColumns": [
"id"
]
},
{
"table": "offer_canteen",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"canteenId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "offer_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` INTEGER NOT NULL, `categoryId` INTEGER NOT NULL, `title` TEXT NOT NULL, `price` TEXT NOT NULL, `dietary_preferences` TEXT NOT NULL, `allergens` TEXT NOT NULL, PRIMARY KEY(`itemId`), FOREIGN KEY(`categoryId`) REFERENCES `offer_category`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "itemId",
"columnName": "itemId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryId",
"columnName": "categoryId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "price",
"columnName": "price",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dietaryPreferences",
"columnName": "dietary_preferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "allergens",
"columnName": "allergens",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"itemId"
]
},
"indices": [],
"foreignKeys": [
{
"table": "offer_category",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"categoryId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "event_tasks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`taskId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` INTEGER NOT NULL, `dueDate` INTEGER NOT NULL, `notifyDate` INTEGER NOT NULL, `title` TEXT NOT NULL, `notes` TEXT NOT NULL, `room` TEXT NOT NULL, `isFinished` INTEGER NOT NULL, FOREIGN KEY(`eventId`) REFERENCES `studip_events`(`eventId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "taskId",
"columnName": "taskId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "eventId",
"columnName": "eventId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dueDate",
"columnName": "dueDate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notifyDate",
"columnName": "notifyDate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notes",
"columnName": "notes",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "room",
"columnName": "room",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isFinished",
"columnName": "isFinished",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"taskId"
]
},
"indices": [],
"foreignKeys": [
{
"table": "studip_events",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"eventId"
],
"referencedColumns": [
"eventId"
]
}
]
}
],
"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, 'd55bef3b8778cd6346bf65455993008d')"
]
}
}
@@ -37,6 +37,12 @@ class MainActivity : FragmentActivity() {
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// if (viewModel.getTimetableCount() == 0) {
// viewModel.insertTimetable(
// Timetable(0, "default")
// )
// }
/* /*
* Check whether the user is launching the app for the first time in order to show a * Check whether the user is launching the app for the first time in order to show a
* tutorial. This value will be changed to 'false' upon completing the tutorial, thus not * tutorial. This value will be changed to 'false' upon completing the tutorial, thus not
@@ -73,6 +73,11 @@ interface AppDAO {
@Query("DELETE FROM studip_events") @Query("DELETE FROM studip_events")
fun nukeEvents() fun nukeEvents()
/* --- timetables --- */
@Insert
fun insertTimetable(timetable: Timetable)
/* --- event tasks --- */ /* --- event tasks --- */
/** /**
@@ -14,6 +14,7 @@ import com.denizk0461.weserplaner.model.*
*/ */
@Database( @Database(
entities = [ entities = [
Timetable::class,
StudIPEvent::class, StudIPEvent::class,
OfferDate::class, OfferDate::class,
OfferCanteen::class, OfferCanteen::class,
@@ -21,10 +22,11 @@ import com.denizk0461.weserplaner.model.*
OfferItem::class, OfferItem::class,
EventTask::class, EventTask::class,
], ],
version = 21, version = 22,
exportSchema = true, exportSchema = true,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 20, to = 21), AutoMigration(from = 20, to = 21),
// AutoMigration(from = 21, to = 22),
], ],
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@@ -58,6 +60,7 @@ abstract class AppDatabase : RoomDatabase() {
"event_db", "event_db",
).addMigrations( ).addMigrations(
migrationAddNewsToOfferCanteen_19_20, migrationAddNewsToOfferCanteen_19_20,
migration_21_22,
// migrationAddTimetableId_20_21, // migrationAddTimetableId_20_21,
// migrationAddEventTask_20_21, // migrationAddEventTask_20_21,
) )
@@ -79,5 +82,48 @@ abstract class AppDatabase : RoomDatabase() {
database.execSQL("ALTER TABLE offer_canteen ADD COLUMN news TEXT NOT NULL DEFAULT ''") database.execSQL("ALTER TABLE offer_canteen ADD COLUMN news TEXT NOT NULL DEFAULT ''")
} }
} }
private val migration_21_22 = object : Migration(21, 22) {
override fun migrate(database: SupportSQLiteDatabase) {
// Add 'timetables' table
database.execSQL("CREATE TABLE 'timetables' ('id' INTEGER NOT NULL UNIQUE, 'name' TEXT NOT NULL, PRIMARY KEY('id' AUTOINCREMENT));")
// Insert a timetable
database.execSQL("INSERT INTO timetables VALUES (0, 'default')")
/*
* Create temporary Stud.IP event table. This is necessary because SQLite doesn't
* implement ADD FOREIGN KEY on ALTER TABLE >:(
*/
database.execSQL("""
CREATE TABLE IF NOT EXISTS studip_events_temp (
'eventId' INTEGER NOT NULL UNIQUE,
'timetableId' INTEGER NOT NULL,
'title' TEXT NOT NULL,
'lecturer' TEXT NOT NULL,
'room' TEXT NOT NULL,
'day' INTEGER NOT NULL,
'timeslotStart' TEXT NOT NULL,
'timeslotEnd' TEXT NOT NULL,
'timeslotId' INTEGER NOT NULL,
'colour' INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY('timetableId') REFERENCES 'timetables'('id') ON DELETE CASCADE,
PRIMARY KEY('eventId' AUTOINCREMENT)
);
""".trimIndent())
// Copy data from old table to new table
database.execSQL("""
INSERT INTO studip_events_temp (eventId, timetableId, title, lecturer, room, day, timeslotStart, timeslotEnd, timeslotId, colour)
SELECT eventId, timetableId, title, lecturer, room, day, timeslotStart, timeslotEnd, timeslotId, colour FROM studip_events
""".trimIndent())
// Delete old table
database.execSQL("DROP TABLE studip_events")
// Rename new table to the old table's name
database.execSQL("ALTER TABLE studip_events_temp RENAME TO studip_events")
}
}
} }
} }
@@ -159,6 +159,10 @@ class AppRepository(app: Application) {
dao.delete(event) dao.delete(event)
} }
fun insertTimetable(timetable: Timetable) {
dao.insertTimetable(timetable)
}
/** /**
* Retrieves all canteen offer date objects. * Retrieves all canteen offer date objects.
* *
@@ -4,6 +4,7 @@ import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
/** /**
@@ -20,10 +21,20 @@ import androidx.room.PrimaryKey
* @param timeslotId minute the event starts at - used for ordering * @param timeslotId minute the event starts at - used for ordering
* @param colour user-defined colour the event will be shown in - UNIMPLEMENTED * @param colour user-defined colour the event will be shown in - UNIMPLEMENTED
*/ */
@Entity(tableName = "studip_events") @Entity(
tableName = "studip_events",
foreignKeys = [
ForeignKey(
Timetable::class,
["id"],
["timetableId"],
ForeignKey.CASCADE,
),
],
)
data class StudIPEvent( data class StudIPEvent(
@PrimaryKey(autoGenerate = true) val eventId: Int = 0, @PrimaryKey(autoGenerate = true) val eventId: Int = 0,
@ColumnInfo(defaultValue = "0") val timetableId: Int, val timetableId: Int,
val title: String, val title: String,
val lecturer: String, val lecturer: String,
val room: String, val room: String,
@@ -0,0 +1,17 @@
package com.denizk0461.weserplaner.model
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* Timetable parent object that [StudIPEvent]s are assigned to. These can be named to denote
* different semesters.
*
* @param id unique primary key value
* @param name title of the timetable
*/
@Entity(tableName = "timetables")
data class Timetable(
@PrimaryKey(autoGenerate = true) val id: Int,
val name: String,
)
@@ -1,6 +1,7 @@
package com.denizk0461.weserplaner.viewmodel package com.denizk0461.weserplaner.viewmodel
import android.app.Application import android.app.Application
import com.denizk0461.weserplaner.model.Timetable
import com.denizk0461.weserplaner.values.AppLayout import com.denizk0461.weserplaner.values.AppLayout
/** /**
-8
View File
@@ -1,8 +0,0 @@
<resources>
<style name="Theme.StudIPTimetable" parent="Base.Theme.StudIPTimetable">
<!-- Transparent system bars for edge-to-edge. -->
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">?attr/isLightTheme</item>
</style>
</resources>