diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e45b245..b372eca 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -33,8 +33,8 @@ android {
targetSdk = 35
// Bump both for every release installed on the phone — versionName for
// humans, versionCode so dumpsys/install history can tell builds apart.
- versionCode = 4
- versionName = "0.4.0"
+ versionCode = 5
+ versionName = "0.5.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/schemas/no.naiv.meddetsamme.data.MedDatabase/4.json b/app/schemas/no.naiv.meddetsamme.data.MedDatabase/4.json
new file mode 100644
index 0000000..45f17c4
--- /dev/null
+++ b/app/schemas/no.naiv.meddetsamme.data.MedDatabase/4.json
@@ -0,0 +1,389 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 4,
+ "identityHash": "56932b6862c83744cc926f80ab7875e0",
+ "entities": [
+ {
+ "tableName": "inventory_item",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `strength` TEXT NOT NULL, `unit` TEXT NOT NULL, `form` TEXT NOT NULL, `atcCode` TEXT, `packageSize` REAL, `stockUnits` REAL NOT NULL, `lowStockLeadDays` INTEGER NOT NULL, `rxExpiryEpochDay` INTEGER, `refillsRemaining` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "strength",
+ "columnName": "strength",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unit",
+ "columnName": "unit",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "form",
+ "columnName": "form",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "atcCode",
+ "columnName": "atcCode",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "packageSize",
+ "columnName": "packageSize",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "stockUnits",
+ "columnName": "stockUnits",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lowStockLeadDays",
+ "columnName": "lowStockLeadDays",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rxExpiryEpochDay",
+ "columnName": "rxExpiryEpochDay",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "refillsRemaining",
+ "columnName": "refillsRemaining",
+ "affinity": "INTEGER"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "medication",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `itemId` INTEGER NOT NULL, `withFood` INTEGER NOT NULL, `notes` TEXT NOT NULL, `active` INTEGER NOT NULL, FOREIGN KEY(`itemId`) REFERENCES `inventory_item`(`id`) ON UPDATE NO ACTION ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "itemId",
+ "columnName": "itemId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "withFood",
+ "columnName": "withFood",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notes",
+ "columnName": "notes",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "active",
+ "columnName": "active",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_medication_itemId",
+ "unique": false,
+ "columnNames": [
+ "itemId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_medication_itemId` ON `${TABLE_NAME}` (`itemId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "inventory_item",
+ "onDelete": "RESTRICT",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "itemId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "dose_time",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `medId` INTEGER NOT NULL, `minuteOfDay` INTEGER NOT NULL, `amount` REAL NOT NULL, `daysOfWeekMask` INTEGER NOT NULL, `intervalDays` INTEGER NOT NULL, `anchorEpochDay` INTEGER, `cycleActiveDays` INTEGER NOT NULL DEFAULT 0, `cycleLengthDays` INTEGER NOT NULL DEFAULT 0, `startEpochDay` INTEGER, `endEpochDay` INTEGER, `seasonStartMmdd` INTEGER NOT NULL DEFAULT 0, `seasonEndMmdd` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`medId`) REFERENCES `medication`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "medId",
+ "columnName": "medId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "minuteOfDay",
+ "columnName": "minuteOfDay",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "daysOfWeekMask",
+ "columnName": "daysOfWeekMask",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "intervalDays",
+ "columnName": "intervalDays",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "anchorEpochDay",
+ "columnName": "anchorEpochDay",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "cycleActiveDays",
+ "columnName": "cycleActiveDays",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "cycleLengthDays",
+ "columnName": "cycleLengthDays",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "startEpochDay",
+ "columnName": "startEpochDay",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "endEpochDay",
+ "columnName": "endEpochDay",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "seasonStartMmdd",
+ "columnName": "seasonStartMmdd",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "seasonEndMmdd",
+ "columnName": "seasonEndMmdd",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_dose_time_medId",
+ "unique": false,
+ "columnNames": [
+ "medId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_dose_time_medId` ON `${TABLE_NAME}` (`medId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "medication",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "medId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "dose_log",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `medId` INTEGER NOT NULL, `doseTimeId` INTEGER, `scheduledAtMillis` INTEGER NOT NULL, `amount` REAL NOT NULL, `status` TEXT NOT NULL, `actionedAtMillis` INTEGER, `nagCount` INTEGER NOT NULL, FOREIGN KEY(`medId`) REFERENCES `medication`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`doseTimeId`) REFERENCES `dose_time`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "medId",
+ "columnName": "medId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "doseTimeId",
+ "columnName": "doseTimeId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "scheduledAtMillis",
+ "columnName": "scheduledAtMillis",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actionedAtMillis",
+ "columnName": "actionedAtMillis",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "nagCount",
+ "columnName": "nagCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_dose_log_medId",
+ "unique": false,
+ "columnNames": [
+ "medId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_dose_log_medId` ON `${TABLE_NAME}` (`medId`)"
+ },
+ {
+ "name": "index_dose_log_doseTimeId",
+ "unique": false,
+ "columnNames": [
+ "doseTimeId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_dose_log_doseTimeId` ON `${TABLE_NAME}` (`doseTimeId`)"
+ },
+ {
+ "name": "index_dose_log_doseTimeId_scheduledAtMillis",
+ "unique": false,
+ "columnNames": [
+ "doseTimeId",
+ "scheduledAtMillis"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_dose_log_doseTimeId_scheduledAtMillis` ON `${TABLE_NAME}` (`doseTimeId`, `scheduledAtMillis`)"
+ },
+ {
+ "name": "index_dose_log_status",
+ "unique": false,
+ "columnNames": [
+ "status"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_dose_log_status` ON `${TABLE_NAME}` (`status`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "medication",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "medId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "dose_time",
+ "onDelete": "SET NULL",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "doseTimeId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "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, '56932b6862c83744cc926f80ab7875e0')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/no/naiv/meddetsamme/data/MigrationTest.kt b/app/src/androidTest/java/no/naiv/meddetsamme/data/MigrationTest.kt
index e57272e..0a46990 100644
--- a/app/src/androidTest/java/no/naiv/meddetsamme/data/MigrationTest.kt
+++ b/app/src/androidTest/java/no/naiv/meddetsamme/data/MigrationTest.kt
@@ -108,6 +108,35 @@ class MigrationTest {
}
}
+ @Test
+ fun migrate3To4_addsSeasonColumnsDefaultingToNoSeason() {
+ helper.createDatabase(TEST_DB, 3).use { db ->
+ db.execSQL(
+ "INSERT INTO inventory_item (id, name, strength, unit, form, stockUnits, " +
+ "lowStockLeadDays) VALUES (1, 'Cetirizin', '10 mg', 'tablett', 'TABLET', 30.0, 7)",
+ )
+ db.execSQL("INSERT INTO medication (id, itemId, withFood, notes, active) VALUES (1, 1, 0, '', 1)")
+ db.execSQL(
+ "INSERT INTO dose_time (id, medId, minuteOfDay, amount, daysOfWeekMask, " +
+ "intervalDays, anchorEpochDay, cycleActiveDays, cycleLengthDays) " +
+ "VALUES (3, 1, 480, 1.0, 127, 0, NULL, 0, 0)",
+ )
+ }
+
+ // Validates the migrated schema against 4.json AND checks the new columns
+ // default to "no season" while the existing row survives.
+ helper.runMigrationsAndValidate(TEST_DB, 4, true, MedDatabase.MIGRATION_3_4).use { db ->
+ db.query(
+ "SELECT amount, seasonStartMmdd, seasonEndMmdd FROM dose_time WHERE id = 3",
+ ).use { c ->
+ assertTrue(c.moveToFirst())
+ assertEquals(1.0, c.getDouble(0), 0.0)
+ assertEquals(0, c.getInt(1)) // seasonStartMmdd
+ assertEquals(0, c.getInt(2)) // seasonEndMmdd
+ }
+ }
+ }
+
private companion object {
const val TEST_DB = "migration-test.db"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0895d71..dba3ffc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -63,6 +63,7 @@
+