diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt index 1ccd1a940765..f4a2a3779599 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt @@ -171,7 +171,7 @@ class ContentProviderTest : InstrumentedTest() { col.decks.count(), ) // Delete test note type - col.modSchemaNoCheck() + col.modSchema(check = false) removeAllNoteTypesByName(col, BASIC_NOTE_TYPE_NAME) removeAllNoteTypesByName(col, TEST_NOTE_TYPE_NAME) } @@ -642,7 +642,7 @@ class ContentProviderTest : InstrumentedTest() { } } finally { // Delete the note type (this will force a full-sync) - col.modSchemaNoCheck() + col.modSchema(check = false) try { val noteType = col.notetypes.get(noteTypeId) assertNotNull("Check note type", noteType) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt index 85f29417227b..af688b729dee 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt @@ -1357,7 +1357,7 @@ open class CardTemplateEditor : */ private fun executeWithSyncCheck(schemaChangingAction: Runnable) { try { - templateEditor.getColUnsafe.modSchema() + templateEditor.getColUnsafe.modSchema(check = true) schemaChangingAction.run() templateEditor.loadTemplatePreviewerFragmentIfFragmented() } catch (e: ConfirmModSchemaException) { @@ -1366,7 +1366,7 @@ open class CardTemplateEditor : d.setArgs(resources.getString(R.string.full_sync_confirmation)) val confirm = Runnable { - templateEditor.getColUnsafe.modSchemaNoCheck() + templateEditor.getColUnsafe.modSchema(check = false) schemaChangingAction.run() templateEditor.dismissAllDialogFragments() } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt index 2c01e7b67448..fc4800d912bb 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt @@ -33,7 +33,6 @@ import androidx.lifecycle.coroutineScope import androidx.lifecycle.viewModelScope import anki.collection.Progress import com.ichi2.anki.CollectionManager.TR -import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.CrashReportData.Companion.throwIfDialogUnusable import com.ichi2.anki.CrashReportData.Companion.toCrashReportData import com.ichi2.anki.CrashReportData.HelpAction @@ -41,17 +40,14 @@ import com.ichi2.anki.CrashReportData.HelpAction.AnkiBackendLink import com.ichi2.anki.CrashReportData.HelpAction.OpenDeckOptions import com.ichi2.anki.common.annotations.UseContextParameter import com.ichi2.anki.exception.StorageAccessException -import com.ichi2.anki.libanki.Collection import com.ichi2.anki.pages.DeckOptionsDestination import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.utils.openUrl import com.ichi2.utils.create import com.ichi2.utils.message -import com.ichi2.utils.negativeButton import com.ichi2.utils.neutralButton import com.ichi2.utils.positiveButton import com.ichi2.utils.setupEnterKeyHandler -import com.ichi2.utils.show import com.ichi2.utils.title import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CancellationException @@ -79,8 +75,6 @@ import org.jetbrains.annotations.VisibleForTesting import timber.log.Timber import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine import kotlin.time.Duration /** Overridable reference to [Dispatchers.IO]. Useful if tests can't use it */ @@ -549,53 +543,6 @@ private fun ProgressContext.updateDialog(dialog: android.app.ProgressDialog) { dialog.setMessage(text + progressText) } -/** - * If a one-way sync is not already required, confirm the user wishes to proceed. - * If the user agrees, the schema is bumped and the routine will return true. - * On false, calling routine should abort. - */ -suspend fun AnkiActivity.userAcceptsSchemaChange(col: Collection): Boolean { - if (col.schemaChanged()) { - return true - } - return suspendCoroutine { coroutine -> - AlertDialog.Builder(this).show { - message(text = col.tr.deckConfigWillRequireFullSync()) // generic message - positiveButton(R.string.dialog_ok) { - col.modSchemaNoCheck() - coroutine.resume(true) - } - negativeButton(R.string.dialog_cancel) { coroutine.resume(false) } - setOnCancelListener { coroutine.resume(false) } - } - } -} - -/** - * Returns whether we are allowed to change the schema. - * - * If changing the schema would require the next sync to be a full sync, and it's not already required, ask - * the user whether or not they still allow the schema change. - */ -suspend fun AnkiActivity.userAcceptsSchemaChange(): Boolean { - if (withCol { schemaChanged() }) { - return true - } - val hasAcceptedSchemaChange = - suspendCoroutine { coroutine -> - AlertDialog.Builder(this).show { - message(text = TR.deckConfigWillRequireFullSync().replace("\\s+".toRegex(), " ")) - positiveButton(R.string.dialog_ok) { coroutine.resume(true) } - negativeButton(R.string.dialog_cancel) { coroutine.resume(false) } - setOnCancelListener { coroutine.resume(false) } - } - } - if (hasAcceptedSchemaChange) { - withCol { modSchemaNoCheck() } - } - return hasAcceptedSchemaChange -} - /** * Ensures that current continuation is not [cancelled][CancellableContinuation.isCancelled]. * diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 6e51e6a72df0..ec1335c598e9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -163,6 +163,7 @@ import com.ichi2.anki.settings.Prefs import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider import com.ichi2.anki.snackbar.SnackbarBuilder import com.ichi2.anki.snackbar.showSnackbar +import com.ichi2.anki.sync.launchCatchingRequiringOneWaySyncDiscardUndo import com.ichi2.anki.ui.ResizablePaneManager import com.ichi2.anki.ui.animations.fadeIn import com.ichi2.anki.ui.animations.fadeOut @@ -686,7 +687,7 @@ open class DeckPicker : SchedulerUpgradeDialog( activity = this, onUpgrade = { - launchCatchingRequiringOneWaySync { + launchCatchingRequiringOneWaySyncDiscardUndo { this@DeckPicker.withProgress { withCol { sched.upgradeToV2() } } showThemedToast(this@DeckPicker, TR.schedulingUpdateDone(), false) } @@ -1727,7 +1728,7 @@ open class DeckPicker : if (recommendOneWaySync) { recommendOneWaySync = false try { - getColUnsafe.modSchema() + getColUnsafe.modSchema(check = true) } catch (e: ConfirmModSchemaException) { Timber.w("Forcing one-way sync") e.log() @@ -2474,7 +2475,7 @@ class OneWaySyncDialog( val confirm = Runnable { // Bypass the check once the user confirms - CollectionManager.getColUnsafe().modSchemaNoCheck() + CollectionManager.getColUnsafe().modSchema(check = false) } dialog.setConfirm(confirm) dialog.setArgs(message) @@ -2492,30 +2493,5 @@ class OneWaySyncDialog( } } -/** - * [launchCatchingTask], showing a one-way sync dialog: [R.string.full_sync_confirmation] - */ -fun AnkiActivity.launchCatchingRequiringOneWaySync(block: suspend () -> Unit) = - launchCatchingTask { - try { - block() - } catch (e: ConfirmModSchemaException) { - e.log() - - // .also is used to ensure the activity is used as context - val confirmModSchemaDialog = - ConfirmationDialog().also { dialog -> - dialog.setArgs(message = getString(R.string.full_sync_confirmation)) - dialog.setConfirm { - launchCatchingTask { - withCol { modSchemaNoCheck() } - block() - } - } - } - showDialogFragment(confirmModSchemaDialog) - } - } - val ActivityHomescreenBinding.studyoptionsFrame: FragmentContainerView? get() = studyoptionsFragment diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt index 7f26df1ef013..64c20ab68604 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt @@ -152,6 +152,7 @@ import com.ichi2.anki.servicelayer.NoteService import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider import com.ichi2.anki.snackbar.SnackbarBuilder import com.ichi2.anki.snackbar.showSnackbar +import com.ichi2.anki.sync.userAcceptsSchemaChange import com.ichi2.anki.ui.setupNoteTypeSpinner import com.ichi2.anki.utils.RunOnlyOnce import com.ichi2.anki.utils.ext.sharedPrefs diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteTypeFieldEditor.kt index 2d4210dbc6cd..e4f5383d2cf4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/NoteTypeFieldEditor.kt @@ -238,9 +238,9 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { fieldName ?: return // Name is valid, now field is added if (modSchemaCheck) { - getColUnsafe.modSchema() + getColUnsafe.modSchema(check = true) } else { - getColUnsafe.modSchemaNoCheck() + getColUnsafe.modSchema(check = false) } launchCatchingTask { Timber.d("doInBackgroundAddField") @@ -259,18 +259,21 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { private fun deleteFieldDialog() { val confirm = Runnable { - getColUnsafe.modSchemaNoCheck() + getColUnsafe.modSchema(check = false) deleteField() // This ensures that the context menu closes after the field has been deleted - supportFragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + supportFragmentManager.popBackStackImmediate( + null, + FragmentManager.POP_BACK_STACK_INCLUSIVE, + ) } if (fieldsLabels.size < 2) { showThemedToast(this, resources.getString(R.string.toast_last_field), true) } else { try { - getColUnsafe.modSchema() + getColUnsafe.modSchema(check = true) val fieldName = noteFields[currentPos].name ConfirmationDialog().let { it.setArgs( @@ -342,7 +345,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { c.setArgs(resources.getString(R.string.full_sync_confirmation)) val confirm = Runnable { - getColUnsafe.modSchemaNoCheck() + getColUnsafe.modSchema(check = false) try { renameField() } catch (e1: ConfirmModSchemaException) { @@ -400,7 +403,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { Timber.i("Repositioning field from %d to %d", currentPos, newPosition) try { - getColUnsafe.modSchema() + getColUnsafe.modSchema(check = true) repositionField(newPosition - 1) } catch (e: ConfirmModSchemaException) { e.log() @@ -411,7 +414,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { val confirm = Runnable { try { - getColUnsafe.modSchemaNoCheck() + getColUnsafe.modSchema(check = false) repositionField(newPosition - 1) } catch (e1: JSONException) { throw RuntimeException(e1) @@ -466,7 +469,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { */ private fun sortByField() { try { - getColUnsafe.modSchema() + getColUnsafe.modSchema(check = true) launchCatchingTask { changeSortField(notetype, currentPos) } } catch (e: ConfirmModSchemaException) { e.log() @@ -475,7 +478,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { c.setArgs(resources.getString(R.string.full_sync_confirmation)) val confirm = Runnable { - getColUnsafe.modSchemaNoCheck() + getColUnsafe.modSchema(check = false) launchCatchingTask { changeSortField(notetype, currentPos) } } c.setConfirm(confirm) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ChangeNoteTypeDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ChangeNoteTypeDialog.kt index e5ad2234a29f..25907aa935f8 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ChangeNoteTypeDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ChangeNoteTypeDialog.kt @@ -62,13 +62,13 @@ import com.ichi2.anki.dialogs.ConversionType.CLOZE_TO_CLOZE import com.ichi2.anki.dialogs.ConversionType.CLOZE_TO_REGULAR import com.ichi2.anki.dialogs.ConversionType.REGULAR_TO_CLOZE import com.ichi2.anki.dialogs.ConversionType.REGULAR_TO_REGULAR -import com.ichi2.anki.launchCatchingRequiringOneWaySync import com.ichi2.anki.launchCatchingTask import com.ichi2.anki.libanki.NoteId import com.ichi2.anki.libanki.NoteTypeId import com.ichi2.anki.requireAnkiActivity import com.ichi2.anki.showError import com.ichi2.anki.snackbar.showSnackbar +import com.ichi2.anki.sync.launchCatchingRequiringOneWaySync import com.ichi2.anki.ui.BasicItemSelectedListener import com.ichi2.anki.ui.internationalization.toSentenceCase import com.ichi2.anki.utils.InitStatus diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ChangeNoteTypeViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ChangeNoteTypeViewModel.kt index 0d1783beabcd..0f0769d83ee7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ChangeNoteTypeViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/ChangeNoteTypeViewModel.kt @@ -316,8 +316,6 @@ class ChangeNoteTypeViewModel( Timber.d("Field map: %s", fieldChangeMap) Timber.d("Card map: %s", templateChangeMap) - withCol { modSchema() } - val changes = changeNoteTypeOfNotes( noteIds = noteIds, diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNotetypes.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNotetypes.kt index 86b1b576b38f..cec573f936d3 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNotetypes.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNotetypes.kt @@ -41,7 +41,7 @@ import com.ichi2.anki.dialogs.showLoadingDialog import com.ichi2.anki.launchCatchingTask import com.ichi2.anki.notetype.ManageNoteTypesState.UserMessage import com.ichi2.anki.snackbar.showSnackbar -import com.ichi2.anki.userAcceptsSchemaChange +import com.ichi2.anki.sync.userAcceptsSchemaChange import com.ichi2.anki.utils.Destination import com.ichi2.ui.AccessibleSearchView import com.ichi2.utils.getInputField diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/SyncSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/SyncSettingsFragment.kt index 5c457be4b37b..ce67ce319109 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/SyncSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/SyncSettingsFragment.kt @@ -68,7 +68,7 @@ class SyncSettingsFragment : SettingsFragment() { setMessage(TR.preferencesOnNextSyncForceChangesIn()) setPositiveButton(R.string.dialog_ok) { _, _ -> launchCatchingTask { - withCol { modSchemaNoCheck() } + withCol { modSchema(check = false) } showSnackbar(R.string.one_way_sync_confirmation, Snackbar.LENGTH_SHORT) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/sync/SchemaChanges.kt b/AnkiDroid/src/main/java/com/ichi2/anki/sync/SchemaChanges.kt new file mode 100644 index 000000000000..a3acec5d7e13 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/sync/SchemaChanges.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2026 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +package com.ichi2.anki.sync + +import androidx.appcompat.app.AlertDialog +import com.ichi2.anki.AnkiActivity +import com.ichi2.anki.CollectionManager.TR +import com.ichi2.anki.CollectionManager.withCol +import com.ichi2.anki.R +import com.ichi2.anki.dialogs.ConfirmationDialog +import com.ichi2.anki.launchCatchingTask +import com.ichi2.anki.libanki.Collection +import com.ichi2.anki.libanki.exception.ConfirmModSchemaException +import com.ichi2.anki.utils.ext.showDialogFragment +import com.ichi2.utils.message +import com.ichi2.utils.negativeButton +import com.ichi2.utils.positiveButton +import com.ichi2.utils.show +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +/** + * [launchCatchingTask], showing a one-way sync dialog: [R.string.full_sync_confirmation] + * + * @param block calls a backend method which unconditionally performs a schema change, + * such as [Collection.changeNotetypeRaw] + */ +fun AnkiActivity.launchCatchingRequiringOneWaySync(block: suspend () -> Unit) = + launchCatchingTask { + if (withCol { !schemaChanged() }) { + // .also is used to ensure the activity is used as context + val confirmModSchemaDialog = + ConfirmationDialog().also { dialog -> + dialog.setArgs(message = getString(R.string.full_sync_confirmation)) + dialog.setConfirm { + launchCatchingTask { + block() + } + } + } + showDialogFragment(confirmModSchemaDialog) + return@launchCatchingTask + } + // TODO: use context(SchemaChangedConfirmed) after bug #20247 is fixed (upstream-issue) + block() + } + +/** + * [launchCatchingTask], showing a one-way sync dialog: [R.string.full_sync_confirmation] + * + * **This method discards the undo and study queues when consent is provided** + */ +fun AnkiActivity.launchCatchingRequiringOneWaySyncDiscardUndo(block: suspend () -> Unit) = + launchCatchingTask { + try { + block() + } catch (e: ConfirmModSchemaException) { + e.log() + + // .also is used to ensure the activity is used as context + val confirmModSchemaDialog = + ConfirmationDialog().also { dialog -> + dialog.setArgs(message = getString(R.string.full_sync_confirmation)) + dialog.setConfirm { + launchCatchingTask { + withCol { modSchema(check = false) } + block() + } + } + } + showDialogFragment(confirmModSchemaDialog) + } + } + +/** + * Returns whether we are allowed to change the schema. + * + * If changing the schema would require the next sync to be a full sync, and it's not already required, ask + * the user whether or not they still allow the schema change. + */ +suspend fun AnkiActivity.userAcceptsSchemaChange(): Boolean { + if (withCol { schemaChanged() }) { + return true + } + val hasAcceptedSchemaChange = + suspendCoroutine { coroutine -> + AlertDialog.Builder(this).show { + message(text = TR.deckConfigWillRequireFullSync().replace("\\s+".toRegex(), " ")) + positiveButton(R.string.dialog_ok) { coroutine.resume(true) } + negativeButton(R.string.dialog_cancel) { coroutine.resume(false) } + setOnCancelListener { coroutine.resume(false) } + } + } + if (hasAcceptedSchemaChange) { + withCol { modSchema(check = false) } + } + return hasAcceptedSchemaChange +} diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/Collection.kt b/libanki/src/main/java/com/ichi2/anki/libanki/Collection.kt index 5a1305abfd39..5913f69093a9 100644 --- a/libanki/src/main/java/com/ichi2/anki/libanki/Collection.kt +++ b/libanki/src/main/java/com/ichi2/anki/libanki/Collection.kt @@ -337,14 +337,14 @@ class Collection( config = Config(backend) } - /** Mark schema modified to force a full - * sync, but with the confirmation checking function disabled This - * is equivalent to `modSchema(False)` in Anki. A distinct method - * is used so that the type does not states that an exception is - * thrown when in fact it is never thrown. + /** + * Marks the schema as modified to cause a one-way sync. + * + * **This method discards the undo and study queues when returning successfully** */ - @NotInPyLib - fun modSchemaNoCheck() { + @LibAnkiAlias("set_schema_modified") + @RustCleanup("Anki only sets scm, not mod") + fun setSchemaModified() { db.execute( "update col set scm=?, mod=?", TimeManager.time.intTimeMS(), @@ -352,23 +352,31 @@ class Collection( ) } - /** Mark schema modified to cause a one-way sync. - * ConfirmModSchemaException will be thrown if the user needs to be prompted to confirm the action. - * If the user chooses to confirm then modSchemaNoCheck should be called, after which the exception can - * be safely ignored, and the outer code called again. + /** + * Marks the schema as modified to cause a one-way sync, throwing [ConfirmModSchemaException] + * when [check] is `true` and a one-way sync was not previously required + * (the schema was not previously modified). * - * @throws ConfirmModSchemaException + * **This method discards the undo and study queues when returning successfully** + * ([check] == `false` OR [check] == `true` and the schema was previously modified). + * + * @param check whether to throw [ConfirmModSchemaException] if the schema is unchanged. + * Use `false` after catching the exception and obtaining user consent. + * + * @throws ConfirmModSchemaException if the schema is currently unchanged and [check] is `true` */ @LibAnkiAlias("mod_schema") - fun modSchema() { + fun modSchema(check: Boolean) { if (!schemaChanged()) { - /* In Android we can't show a dialog which blocks the main UI thread - Therefore we can't wait for the user to confirm if they want to do - a one-way sync here, and we instead throw an exception asking the outer - code to handle the user's choice */ - throw ConfirmModSchemaException() + if (check) { + /* In Android we can't show a dialog which blocks the main UI thread + Therefore we can't wait for the user to confirm if they want to do + a one-way sync here, and we instead throw an exception asking the outer + code to handle the user's choice */ + throw ConfirmModSchemaException() + } } - modSchemaNoCheck() + setSchemaModified() } /** `true` if schema changed since last sync. */ diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/DB.kt b/libanki/src/main/java/com/ichi2/anki/libanki/DB.kt index ba561bceb86e..124ee78a3b33 100644 --- a/libanki/src/main/java/com/ichi2/anki/libanki/DB.kt +++ b/libanki/src/main/java/com/ichi2/anki/libanki/DB.kt @@ -120,20 +120,27 @@ class DB( return results } + /** + * Execute a single SQL statement that does not return data. + * + * This method discards the undo and study queues unless the statement is a `SELECT`. + */ fun execute( sql: String, vararg `object`: Any?, ) { - val s = sql.trim().lowercase() - // mark modified? - for (mo in MOD_SQL_STATEMENTS) { - if (s.startsWith(mo)) { - break - } + // permalink: https://github.com/ankitects/anki/blob/83c615cc7f9aef3c336936fa797671965538f89c/rslib/src/backend/dbproxy.rs#L170-L178 + if (!sql.lowercase().trim().startsWith("select")) { + Timber.i("clearing undo and study queues") } database.execSQL(sql, `object`) } + /** + * Executes a collection of ';'-delimited SQL statements which do not return data. + * + * This method discards the undo and study queues unless all statements are `SELECT`. + */ @KotlinCleanup("""Use Kotlin string. Change split so that there is no empty string after last ";".""") fun executeScript(sql: String) { val queries = sql.split(";") diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/Notetypes.kt b/libanki/src/main/java/com/ichi2/anki/libanki/Notetypes.kt index 79edc1a278e2..25e852aaacc8 100644 --- a/libanki/src/main/java/com/ichi2/anki/libanki/Notetypes.kt +++ b/libanki/src/main/java/com/ichi2/anki/libanki/Notetypes.kt @@ -561,10 +561,12 @@ class Notetypes( * * Each value represents the index in the previous notetype. * -1 indicates the original value will be discarded. + * + * **This method updates the schema without confirmation** */ @LibAnkiAlias("change_notetype_of_notes") fun changeNotetypeOfNotes(input: ChangeNotetypeRequest): OpChanges { - val opBytes = this.col.backend.changeNotetypeRaw(input.toByteArray()) + val opBytes = col.backend.changeNotetypeRaw(input.toByteArray()) return OpChanges.parseFrom(opBytes) } diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/sched/Scheduler.kt b/libanki/src/main/java/com/ichi2/anki/libanki/sched/Scheduler.kt index b8281e3a714f..cd959e55aac1 100644 --- a/libanki/src/main/java/com/ichi2/anki/libanki/sched/Scheduler.kt +++ b/libanki/src/main/java/com/ichi2/anki/libanki/sched/Scheduler.kt @@ -229,7 +229,7 @@ open class Scheduler( * @throws com.ichi2.anki.libanki.exception.ConfirmModSchemaException */ fun upgradeToV2() { - col.modSchema() + col.modSchema(check = true) col.backend.upgradeScheduler() col._loadScheduler() }