feat(reminders): ReviewReminder schema migration#18856
Conversation
This comment was marked as resolved.
This comment was marked as resolved.
|
@ericli3690 Is this ready to be un-drafted? |
795f444 to
ff9031f
Compare
ff9031f to
6198c94
Compare
|
@david-allison Sorry for the delay!! Ready! |
AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ScheduleRemindersTest.kt
Outdated
Show resolved
Hide resolved
GSoC 2025: Review Reminders - Added a `catchDatabaseExceptions` wrapper method to ScheduleReminders. By wrapping calls to the database with this function, any database errors caught fail gracefully and show an error dialog to the user. If the database access takes too long, a progress bar shows up, too. - Previously, this was a method that also included schema migration logic etc., but that's now been moved to ankidroid#18856, so it's now possible for me to move this small snippet here instead.
6198c94 to
ee8de53
Compare
ee8de53 to
a21a1e3
Compare
There was a problem hiding this comment.
Pull Request Overview
This PR implements schema migration functionality for the ReviewReminder system to handle database schema changes without user data loss. The implementation adds version tracking to stored review reminders and provides an automated migration system that can upgrade old schemas to new ones.
Key changes:
- Added schema versioning system with
ReviewReminderSchemaVersionandStoredReviewRemindersMap - Implemented step-by-step migration logic in
ReviewRemindersDatabase.performSchemaMigration - Created comprehensive test suite covering migration scenarios and edge cases
Reviewed Changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| ReviewRemindersDatabase.kt | Converted to object, added migration logic and schema versioning |
| ReviewReminderMigrationSettings.kt | New file defining schema versions and migration chains |
| ReviewReminder.kt | Updated to implement ReviewReminderSchema interface |
| ReviewRemindersDatabaseTest.kt | Enhanced tests with migration scenarios and custom matchers |
| ReviewReminderMigrationSettingsTest.kt | New test file validating schema version consistency |
AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewReminderMigrationSettings.kt
Outdated
Show resolved
Hide resolved
AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersDatabaseTest.kt
Show resolved
Hide resolved
AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewRemindersDatabase.kt
Show resolved
Hide resolved
AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewRemindersDatabase.kt
Outdated
Show resolved
Hide resolved
a21a1e3 to
3566f0c
Compare
|
See PR description for full changelog. |
| @RunWith(JUnit4::class) | ||
| class ReviewReminderMigrationSettingsTest { | ||
| @Test | ||
| fun `current schema version points to ReviewReminder`() { | ||
| assertThat(ReviewReminderMigrationSettings.SCHEMA_VERSION.value, equalTo(1)) | ||
| assertThat( | ||
| ReviewReminderMigrationSettings.oldReviewReminderSchemasForMigration.keys | ||
| .last() | ||
| .value, | ||
| equalTo(1), | ||
| ) | ||
| assertThat(ReviewReminderMigrationSettings.oldReviewReminderSchemasForMigration.values.last(), equalTo(ReviewReminder::class)) | ||
| } | ||
| } |
GSoC 2025: Review Reminders - Added a `catchDatabaseExceptions` wrapper method to ScheduleReminders. By wrapping calls to the database with this function, any database errors caught fail gracefully and show an error dialog to the user. If the database access takes too long, a progress bar shows up, too. - Previously, this was a method that also included schema migration logic etc., but that's now been moved to ankidroid#18856, so it's now possible for me to move this small snippet here instead.
GSoC 2025: Review Reminders - Added a `catchDatabaseExceptions` wrapper method to ScheduleReminders. By wrapping calls to the database with this function, any database errors caught fail gracefully and show an error dialog to the user. If the database access takes too long, a progress bar shows up, too. - Previously, this was a method that also included schema migration logic etc., but that's now been moved to ankidroid#18856, so it's now possible for me to move this small snippet here instead.
Other values than TimeManager may need to be set in test and reset otherwise. In particular it could be useful for ankidroid#18856. I tried to abtsract this class. As the stack always had a single value, I removed it. I don't expect that defining a child class will be useful all the time. But in this case, introducing `time` avoid changing many many files and keep the code more readble.
Arthur-Milchior
left a comment
There was a problem hiding this comment.
I need to go to sleep, I'll finish reviewing another time
| * version A -> B -> C but may sometimes jump from A -> C. | ||
| * [ScheduleReminders.catchDatabaseExceptions] will attempt to migrate from all old schemas present in the list. | ||
| * so that [ReviewRemindersDatabase.performSchemaMigration] can migrate their reminders to the new schema. | ||
| * Use an [ReviewReminderSchema] to store the old schema and to define a method for migrating to the new schema. |
There was a problem hiding this comment.
"uses a reviewRereminderSchema" I think. I don't "an" is appropriate
| * Schema migration settings for testing purposes. | ||
| * Consult these as an example of how to save old schemas and define their migrate methods. | ||
| */ | ||
| object TestingReviewReminderMigrationSettings { |
There was a problem hiding this comment.
I'm sorry but I fail to understand why the Testing object is found in the main code and note the test part.
There was a problem hiding this comment.
Fair enough. I was worried about that but initially decided against it because I was uncomfortable with making the schema version visible for testing. Since you advise it, though, I've gone ahead and done that instead. Thanks!
|
|
||
| /** | ||
| * Schema migration settings for testing purposes. | ||
| * Consult these as an example of how to save old schemas and define their migrate methods. |
There was a problem hiding this comment.
"these" seems to be plural. I don't understand what this refer to. There is a single object below.
| * When [ReviewReminder] is updated by a developer, implement this interface in a new data class which | ||
| * has the same fields as the old version of [ReviewReminder], then implement the [migrate] method which | ||
| * transforms old [ReviewReminder]s to new [ReviewReminder]s. Also ensure that the previous [ReviewReminderSchema] | ||
| * in the migration version chain ([ReviewRemindersDatabase.oldReviewReminderSchemasForMigration]) has its migrate method |
There was a problem hiding this comment.
Here and above, you should add [migrate] to indicate it's the name of the method. I am confused about why you didn't do it when you use those brackets a lot in this file
| * | ||
| * We assume that the version is accurate; e.x. if the version is 3, then the [ReviewReminder] stored is indeed | ||
| * of schema version 3. This should be safe to assume since writing this data class to SharedPreferences is an | ||
| * atomic operation: i.e., it is written all at once. |
There was a problem hiding this comment.
I am not certain that it's worth explaining what atomic mean. Hopefuly, the word is clear enough. It's a case where, if the reader don't know it, looking it up maybe better than looking at your small explanation.
There was a problem hiding this comment.
Removed the unnecessary explanation.
| * @see [ReviewReminderMigrationSettings.SCHEMA_VERSION] | ||
| */ | ||
| val SCHEMA_VERSION = | ||
| if (isRobolectric) { |
There was a problem hiding this comment.
To be frank, I'm not a fan of having test code in non test folder if we can avoid it.
You can look at TimeManager to see how it's done. Even if honestly, I don't perfectly understand why it uses a stack.
I feel like it would be better to have a variable, visible for testing, and set it in tests when you need it.
There was a problem hiding this comment.
As mentioned above, I agree and have now marked the fields as visible for testing instead. Thanks!
GSoC 2025: Review Reminders - Added a `catchDatabaseExceptions` wrapper method to ScheduleReminders. By wrapping calls to the database with this function, any database errors caught fail gracefully and show an error dialog to the user. If the database access takes too long, a progress bar shows up, too. - Previously, this was a method that also included schema migration logic etc., but that's now been moved to ankidroid#18856, so it's now possible for me to move this small snippet here instead.
GSoC 2025: Review Reminders - Added a `catchDatabaseExceptions` wrapper method to ScheduleReminders. By wrapping calls to the database with this function, any database errors caught fail gracefully and show an error dialog to the user. If the database access takes too long, a progress bar shows up, too. - Previously, this was a method that also included schema migration logic etc., but that's now been moved to ankidroid#18856, so it's now possible for me to move this small snippet here instead.
|
Acttually I don't see anything else to comment heer. Please ping me on discord when my request are done |
GSoC 2025: Review Reminders - Added a `catchDatabaseExceptions` wrapper method to ScheduleReminders. By wrapping calls to the database with this function, any database errors caught fail gracefully and show an error dialog to the user. If the database access takes too long, a progress bar shows up, too. - Previously, this was a method that also included schema migration logic etc., but that's now been moved to #18856, so it's now possible for me to move this small snippet here instead.
GSoC 2025: Review Reminders Added review reminder schema migration handling. If a future developer updates `ReviewReminder`, so long as they also update the schema migration code, data should be transferred safely on all user devices from the old schema to the new system. Without this code, that data transfer would result in SerializationExceptions and IllegalArgumentExceptions. **Changes, grouped by file:** ReviewReminder: - Fixed up a docstring explaining the migration process. - Made the ReviewReminder class implement the ReviewReminderSchema interface. This allows it to also be stored in the oldReviewReminderSchemasForMigration map. This also means ID now needs to become an override field; see ReviewReminderSchema in ReviewReminderMigrationSettings. ReviewRemindersDatabase: - Converted it into an object. There was no need for it to be a class, I was always using `val database = ReviewRemindersDatabase()` everywhere anyways. - Added a `StoredReviewRemindersMap` data class. This is a wrapper around a serialized map of review reminder IDs to review reminders. Previously, these maps were written to SharedPreferences in single chunks; now, we serialize the map to a string and store it as a part of a `StoredReviewReminderMap`. The `StoredReviewReminderMap` has a version field, which allows us to determine which schema to use to deserialize the JSON string field of the `StoredReviewReminderMap`. - `schemaVersion` and `oldReviewReminderSchemasForMigration`. These hold the current and past review reminder schemas. - Includes a SchemaVersion inline value class. - Includes the ReviewReminderSchema interface. All schemas, both old and current, implement this interface. Objects implementing this interface are dynamically stored in the oldReviewReminderSchemasForMigration field so they can be used for the schema migration process. An ID is set here because it's necessary for the migration process: whenever a migration occurs from an old schema to a new schema, we need to store it in a map with the key set to the ID of the review reminder; hence all ReviewReminderSchemas must have an ID. - `performSchemaMigration` is the key method that performs a migration. It finds the schema version specified from a `StoredReviewReminderMap` in the `oldReviewReminderSchemasForMigration`, uses it to deserialize the JSON string of the map from review reminder IDs to review reminders, then runs the `migrate` method specified in the `ReviewReminderSchema` interface repeatedly until the current schema version is reached. That updated map is then re-written to SharedPreferences, completing the migration. This process differs slightly from my old process of performing review reminder migrations; whereas before the migration happened across all stored review reminders in SharedPreferences all at once, now we perform the migration for each individual read if an old schema is detected. I believe this makes the system more clean. It also means I can delete a bunch of the helper methods at the bottom of the file: `getAllReviewReminderSharedPrefsAsMap`, `deleteAllReviewReminderSharedPrefs`, and `writeAllReviewReminderSharedPrefsAsMap`. I previously only used these for the migration process and they are no longer needed, so I deleted them. - Modified `decodeJson` so it calls the `performSchemaMigration` method if it detects an old schema. ReviewRemindersDatabaseTest: - Updated to support the fact that ReviewRemindersDatabase is now an object. - Includes a TestingReviewReminderMigrationSettings object. This serves two purposes. On one hand, it is tested by ReviewRemindersDatabaseTest to validate that the migration system works. On the other hand, it serves as an example class for future developers to read when figuring out how to use the migration framework I've constructed. See the example old schemas in this object for more details. - Added some new deserialization failure case tests to validate what happens if a StoredReviewRemindersMap is not stored correctly. - Created a new Hamcrest matcher. We need to check if, after a migration, the essential fields of the ReviewReminders are still the same. However, we don't really care if the IDs have changed, since the ID is an internal property of the data representation and not important to the user. In fact, due to the way I have constructed ReviewReminder, the only way to "edit" a ReviewReminder object is to... delete the old one and create a new one (which I think is a good thing! it increases data privacy). Hence, the migration process consumes some IDs, and the IDs are different afterwards. - Added a long and detailed test of the migration process. ScheduleReminders: - Edited to reflect the fact that ReviewRemindersDatabase is now an object, not a class.
3566f0c to
bce77f7
Compare
| } | ||
|
|
||
| /** | ||
| * This is the up-to-date schema, we cannot migrate to a newer version. |
There was a problem hiding this comment.
I'd have phrased it as "there is no new version to migrate to". I feel like the phrasing here state that it's not possible to do it. The truth is that it would not even make sense to request it
There was a problem hiding this comment.
Hm, fair enough. Since it's already merged, though, I'll leave it as is. Feel free to open a PR if you'd like! Thanks
Purpose / Description
Added review reminder schema migration handling. If a future developer updates
ReviewReminder, so long as they also update the schema migration code, data should be transferred safely on all user devices from the old schema to the new system. Without this code, that data transfer would result in SerializationExceptions and IllegalArgumentExceptions.Changes, grouped by file:
ReviewReminder:
ReviewRemindersDatabase:
val database = ReviewRemindersDatabase()everywhere anyways.StoredReviewRemindersMapdata class. This is a wrapper around a serialized map of review reminder IDs to review reminders. Previously, these maps were written to SharedPreferences in single chunks; now, we serialize the map to a string and store it as a part of aStoredReviewReminderMap. TheStoredReviewReminderMaphas a version field, which allows us to determine which schema to use to deserialize the JSON string field of theStoredReviewReminderMap.schemaVersionandoldReviewReminderSchemasForMigration. These hold the current and past review reminder schemas.performSchemaMigrationis the key method that performs a migration. It finds the schema version specified from aStoredReviewReminderMapin theoldReviewReminderSchemasForMigration, uses it to deserialize the JSON string of the map from review reminder IDs to review reminders, then runs themigratemethod specified in theReviewReminderSchemainterface repeatedly until the current schema version is reached. That updated map is then re-written to SharedPreferences, completing the migration. This process differs slightly from my old process of performing review reminder migrations; whereas before the migration happened across all stored review reminders in SharedPreferences all at once, now we perform the migration for each individual read if an old schema is detected. I believe this makes the system more clean. It also means I can delete a bunch of the helper methods at the bottom of the file:getAllReviewReminderSharedPrefsAsMap,deleteAllReviewReminderSharedPrefs, andwriteAllReviewReminderSharedPrefsAsMap. I previously only used these for the migration process and they are no longer needed, so I deleted them.decodeJsonso it calls theperformSchemaMigrationmethod if it detects an old schema.ReviewRemindersDatabaseTest:
ScheduleReminders:
Fixes
Approach
ReviewReminderchanges, more and more old schema classes can be added to this list.How Has This Been Tested?
Checklist