diff --git a/anki/deck.py b/anki/deck.py index 505a06050..0e674c7a8 100644 --- a/anki/deck.py +++ b/anki/deck.py @@ -28,6 +28,7 @@ from anki.hooks import runHook, hookEmpty from anki.template import render from anki.media import updateMediaCount, mediaFiles, \ rebuildMediaDir +from anki.upgrade import upgradeSchema, updateIndices, upgradeDeck import anki.latex # sets up hook # ensure all the DB metadata in other files is loaded before proceeding @@ -3155,7 +3156,7 @@ where id = :id""", fid=f.id, cmid=m.cardModels[0].id, id=id) "Please restore from automatic backup (see FAQ).") # ensure correct views and indexes are available self.updateProgress() - DeckStorage._addIndices(self) + updateIndices(self) # does the user have a model? self.updateProgress() if not self.s.scalar("select count(id) from models"): @@ -3556,15 +3557,7 @@ class DeckStorage(object): metadata.create_all(engine) deck = DeckStorage._init(s) else: - ver = s.scalar("select version from decks limit 1") - # add a checksum to fields - if ver < 71: - try: - s.execute( - "alter table fields add column chksum text "+ - "not null default ''") - except: - pass + ver = upgradeSchema(s) # add any possibly new tables if we're upgrading if ver < DECK_VERSION: metadata.create_all(engine) @@ -3589,10 +3582,7 @@ class DeckStorage(object): deck.s = SessionHelper(s, lock=lock) # force a write lock deck.s.execute("update decks set modified = modified") - needUnpack = False - if deck.utcOffset in (-1, -2): - # do the rest later - needUnpack = deck.utcOffset == -1 + if deck.utcOffset == -2: # make sure we do this before initVars DeckStorage._setUTCOffset(deck) deck.created = time.time() @@ -3606,19 +3596,14 @@ class DeckStorage(object): deck.s.execute("vacuum") # add tags and indices initTagTables(deck.s) - DeckStorage._addIndices(deck) + updateIndices(deck) deck.s.statement("analyze") deck._initVars() else: if backup: DeckStorage.backup(deck, path) deck._initVars() - try: - deck = DeckStorage._upgradeDeck(deck, path) - except: - traceback.print_exc() - deck.fixIntegrity() - deck = DeckStorage._upgradeDeck(deck, path) + upgradeDeck(deck) except OperationalError, e: engine.dispose() if (str(e.orig).startswith("database table is locked") or @@ -3632,12 +3617,6 @@ class DeckStorage(object): deck._globalStats = globalStats(deck) deck._dailyStats = dailyStats(deck) return deck - if needUnpack: - deck.startProgress() - DeckStorage._addIndices(deck) - for m in deck.models: - deck.updateCardsFromModel(m) - deck.finishProgress() oldMod = deck.modified # fix a bug with current model being unset if not deck.currentModel and deck.models: @@ -3700,236 +3679,6 @@ class DeckStorage(object): return deck _init = staticmethod(_init) - def _addIndices(deck): - "Add indices to the DB." - # counts, failed cards - deck.s.statement(""" -create index if not exists ix_cards_typeCombined on cards -(type, combinedDue, factId)""") - # scheduler-agnostic type - deck.s.statement(""" -create index if not exists ix_cards_relativeDelay on cards -(relativeDelay)""") - # index on modified, to speed up sync summaries - deck.s.statement(""" -create index if not exists ix_cards_modified on cards -(modified)""") - deck.s.statement(""" -create index if not exists ix_facts_modified on facts -(modified)""") - # priority - temporary index to make compat code faster. this can be - # removed when all clients are on 1.2, as can the ones below - deck.s.statement(""" -create index if not exists ix_cards_priority on cards -(priority)""") - # average factor - deck.s.statement(""" -create index if not exists ix_cards_factor on cards -(type, factor)""") - # card spacing - deck.s.statement(""" -create index if not exists ix_cards_factId on cards (factId)""") - # stats - deck.s.statement(""" -create index if not exists ix_stats_typeDay on stats (type, day)""") - # fields - deck.s.statement(""" -create index if not exists ix_fields_factId on fields (factId)""") - deck.s.statement(""" -create index if not exists ix_fields_fieldModelId on fields (fieldModelId)""") - deck.s.statement(""" -create index if not exists ix_fields_chksum on fields (chksum)""") - # media - deck.s.statement(""" -create unique index if not exists ix_media_filename on media (filename)""") - deck.s.statement(""" -create index if not exists ix_media_originalPath on media (originalPath)""") - # deletion tracking - deck.s.statement(""" -create index if not exists ix_cardsDeleted_cardId on cardsDeleted (cardId)""") - deck.s.statement(""" -create index if not exists ix_modelsDeleted_modelId on modelsDeleted (modelId)""") - deck.s.statement(""" -create index if not exists ix_factsDeleted_factId on factsDeleted (factId)""") - deck.s.statement(""" -create index if not exists ix_mediaDeleted_factId on mediaDeleted (mediaId)""") - # tags - txt = "create unique index if not exists ix_tags_tag on tags (tag)" - try: - deck.s.statement(txt) - except: - deck.s.statement(""" -delete from tags where exists (select 1 from tags t2 where tags.tag = t2.tag -and tags.rowid > t2.rowid)""") - deck.s.statement(txt) - deck.s.statement(""" -create index if not exists ix_cardTags_tagCard on cardTags (tagId, cardId)""") - deck.s.statement(""" -create index if not exists ix_cardTags_cardId on cardTags (cardId)""") - _addIndices = staticmethod(_addIndices) - - def _upgradeDeck(deck, path): - "Upgrade deck to the latest version." - if deck.version < DECK_VERSION: - prog = True - deck.startProgress() - deck.updateProgress(_("Upgrading Deck...")) - if deck.utcOffset == -1: - # we're opening a shared deck with no indices - we'll need - # them if we want to rebuild the queue - DeckStorage._addIndices(deck) - oldmod = deck.modified - else: - prog = False - deck.path = path - if deck.version < 43: - raise Exception("oldDeckVersion") - if deck.version < 44: - # leaner indices - deck.s.statement("drop index if exists ix_cards_factId") - deck.version = 44 - deck.s.commit() - if deck.version < 48: - deck.updateFieldCache(deck.s.column0("select id from facts")) - deck.version = 48 - deck.s.commit() - if deck.version < 52: - dname = deck.name() - sname = deck.syncName - if sname and dname != sname: - deck.notify(_("""\ -When syncing, Anki now uses the same deck name on the server as the deck \ -name on your computer. Because you had '%(dname)s' set to sync to \ -'%(sname)s' on the server, syncing has been temporarily disabled. - -If you want to keep your changes to the online version, please use \ -File>Download>Personal Deck to download the online version. - -If you want to keep the version on your computer, please enable \ -syncing again via Settings>Deck Properties>Synchronisation. - -If you have syncing disabled in the preferences, you can ignore \ -this message. (ERR-0101)""") % { - 'sname':sname, 'dname':dname}) - deck.disableSyncing() - elif sname: - deck.enableSyncing() - deck.version = 52 - deck.s.commit() - if deck.version < 53: - if deck.getBool("perDay"): - if deck.hardIntervalMin == 0.333: - deck.hardIntervalMin = max(1.0, deck.hardIntervalMin) - deck.hardIntervalMax = max(1.1, deck.hardIntervalMax) - deck.version = 53 - deck.s.commit() - if deck.version < 54: - # broken versions of the DB orm die if this is a bool with a - # non-int value - deck.s.statement("update fieldModels set editFontFamily = 1"); - deck.version = 54 - deck.s.commit() - if deck.version < 61: - # do our best to upgrade templates to the new style - txt = '''\ -%s''' - for m in deck.models: - unstyled = [] - for fm in m.fieldModels: - # find which fields had explicit formatting - if fm.quizFontFamily or fm.quizFontSize or fm.quizFontColour: - pass - else: - unstyled.append(fm.name) - # fill out missing info - fm.quizFontFamily = fm.quizFontFamily or u"Arial" - fm.quizFontSize = fm.quizFontSize or 20 - fm.quizFontColour = fm.quizFontColour or "#000000" - fm.editFontSize = fm.editFontSize or 20 - unstyled = set(unstyled) - for cm in m.cardModels: - # embed the old font information into card templates - cm.qformat = txt % ( - cm.questionFontFamily, - cm.questionFontSize, - cm.questionFontColour, - cm.qformat) - cm.aformat = txt % ( - cm.answerFontFamily, - cm.answerFontSize, - cm.answerFontColour, - cm.aformat) - # escape fields that had no previous styling - for un in unstyled: - cm.qformat = cm.qformat.replace("%("+un+")s", "{{{%s}}}"%un) - cm.aformat = cm.aformat.replace("%("+un+")s", "{{{%s}}}"%un) - # rebuild q/a for the above & because latex has changed - for m in deck.models: - deck.updateCardsFromModel(m, dirty=False) - # rebuild the media db based on new format - rebuildMediaDir(deck, dirty=False) - deck.version = 61 - deck.s.commit() - if deck.version < 62: - # updated indices - deck.s.statement("drop index if exists ix_cards_typeCombined") - DeckStorage._addIndices(deck) - deck.version = 62 - deck.s.commit() - if deck.version < 64: - # remove old static indices, as all clients should be libanki1.2+ - for d in ("ix_cards_duePriority", - "ix_cards_priorityDue"): - deck.s.statement("drop index if exists %s" % d) - deck.version = 64 - deck.s.commit() - # note: we keep the priority index for now - if deck.version < 65: - # we weren't correctly setting relativeDelay when answering cards - # in previous versions, so ensure everything is set correctly - deck.rebuildTypes() - deck.version = 65 - deck.s.commit() - # skip a few to allow for updates to stable tree - if deck.version < 70: - # update dynamic indices given we don't use priority anymore - for d in ("intervalDesc", "intervalAsc", "randomOrder", - "dueAsc", "dueDesc"): - deck.s.statement("drop index if exists ix_cards_%s2" % d) - deck.s.statement("drop index if exists ix_cards_%s" % d) - deck.updateDynamicIndices() - # remove old views - for v in ("failedCards", "revCardsOld", "revCardsNew", - "revCardsDue", "revCardsRandom", "acqCardsRandom", - "acqCardsOld", "acqCardsNew"): - deck.s.statement("drop view if exists %s" % v) - deck.version = 70 - deck.s.commit() - if deck.version < 71: - # remove the expensive value cache - deck.s.statement("drop index if exists ix_fields_value") - # add checksums and index - deck.updateAllFieldChecksums() - DeckStorage._addIndices(deck) - deck.s.execute("vacuum") - deck.s.execute("analyze") - deck.version = 71 - deck.s.commit() - # executing a pragma here is very slow on large decks, so we store - # our own record - if not deck.getInt("pageSize") == 4096: - deck.s.commit() - deck.s.execute("pragma page_size = 4096") - deck.s.execute("pragma legacy_file_format = 0") - deck.s.execute("vacuum") - deck.setVar("pageSize", 4096, mod=False) - deck.s.commit() - if prog: - assert deck.modified == oldmod - deck.finishProgress() - return deck - _upgradeDeck = staticmethod(_upgradeDeck) - def _setUTCOffset(deck): # 4am deck.utcOffset = time.timezone + 60*60*4 diff --git a/anki/upgrade.py b/anki/upgrade.py new file mode 100644 index 000000000..09da06086 --- /dev/null +++ b/anki/upgrade.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +def upgradeSchema(s): + "Alter tables prior to ORM initialization." + ver = s.scalar("select version from decks limit 1") + # add a checksum to fields + if ver < 71: + try: + s.execute( + "alter table fields add column chksum text "+ + "not null default ''") + except: + pass + return ver + +def updateIndices(deck): + "Add indices to the DB." + # counts, failed cards + deck.s.statement(""" +create index if not exists ix_cards_typeCombined on cards +(type, combinedDue, factId)""") + # scheduler-agnostic type + deck.s.statement(""" +create index if not exists ix_cards_relativeDelay on cards +(relativeDelay)""") + # index on modified, to speed up sync summaries + deck.s.statement(""" +create index if not exists ix_cards_modified on cards +(modified)""") + deck.s.statement(""" +create index if not exists ix_facts_modified on facts +(modified)""") + # priority - temporary index to make compat code faster. this can be + # removed when all clients are on 1.2, as can the ones below + deck.s.statement(""" +create index if not exists ix_cards_priority on cards +(priority)""") + # average factor + deck.s.statement(""" +create index if not exists ix_cards_factor on cards +(type, factor)""") + # card spacing + deck.s.statement(""" +create index if not exists ix_cards_factId on cards (factId)""") + # stats + deck.s.statement(""" +create index if not exists ix_stats_typeDay on stats (type, day)""") + # fields + deck.s.statement(""" +create index if not exists ix_fields_factId on fields (factId)""") + deck.s.statement(""" +create index if not exists ix_fields_fieldModelId on fields (fieldModelId)""") + deck.s.statement(""" +create index if not exists ix_fields_chksum on fields (chksum)""") + # media + deck.s.statement(""" +create unique index if not exists ix_media_filename on media (filename)""") + deck.s.statement(""" +create index if not exists ix_media_originalPath on media (originalPath)""") + # deletion tracking + deck.s.statement(""" +create index if not exists ix_cardsDeleted_cardId on cardsDeleted (cardId)""") + deck.s.statement(""" +create index if not exists ix_modelsDeleted_modelId on modelsDeleted (modelId)""") + deck.s.statement(""" +create index if not exists ix_factsDeleted_factId on factsDeleted (factId)""") + deck.s.statement(""" +create index if not exists ix_mediaDeleted_factId on mediaDeleted (mediaId)""") + # tags + txt = "create unique index if not exists ix_tags_tag on tags (tag)" + try: + deck.s.statement(txt) + except: + deck.s.statement(""" +delete from tags where exists (select 1 from tags t2 where tags.tag = t2.tag +and tags.rowid > t2.rowid)""") + deck.s.statement(txt) + deck.s.statement(""" +create index if not exists ix_cardTags_tagCard on cardTags (tagId, cardId)""") + deck.s.statement(""" +create index if not exists ix_cardTags_cardId on cardTags (cardId)""") + +def upgradeDeck(deck): + "Upgrade deck to the latest version." + from anki.deck import DECK_VERSION + if deck.version < DECK_VERSION: + prog = True + deck.startProgress() + deck.updateProgress(_("Upgrading Deck...")) + if deck.utcOffset == -1: + # we're opening a shared deck with no indices - we'll need + # them if we want to rebuild the queue + updateIndices(deck) + oldmod = deck.modified + else: + prog = False + if deck.version < 43: + raise Exception("oldDeckVersion") + if deck.version < 44: + # leaner indices + deck.s.statement("drop index if exists ix_cards_factId") + deck.version = 44 + deck.s.commit() + if deck.version < 48: + deck.updateFieldCache(deck.s.column0("select id from facts")) + deck.version = 48 + deck.s.commit() + if deck.version < 52: + dname = deck.name() + sname = deck.syncName + if sname and dname != sname: + deck.notify(_("""\ +When syncing, Anki now uses the same deck name on the server as the deck \ +name on your computer. Because you had '%(dname)s' set to sync to \ +'%(sname)s' on the server, syncing has been temporarily disabled. + +If you want to keep your changes to the online version, please use \ +File>Download>Personal Deck to download the online version. + +If you want to keep the version on your computer, please enable \ +syncing again via Settings>Deck Properties>Synchronisation. + +If you have syncing disabled in the preferences, you can ignore \ +this message. (ERR-0101)""") % { + 'sname':sname, 'dname':dname}) + deck.disableSyncing() + elif sname: + deck.enableSyncing() + deck.version = 52 + deck.s.commit() + if deck.version < 53: + if deck.getBool("perDay"): + if deck.hardIntervalMin == 0.333: + deck.hardIntervalMin = max(1.0, deck.hardIntervalMin) + deck.hardIntervalMax = max(1.1, deck.hardIntervalMax) + deck.version = 53 + deck.s.commit() + if deck.version < 54: + # broken versions of the DB orm die if this is a bool with a + # non-int value + deck.s.statement("update fieldModels set editFontFamily = 1"); + deck.version = 54 + deck.s.commit() + if deck.version < 61: + # do our best to upgrade templates to the new style + txt = '''\ +%s''' + for m in deck.models: + unstyled = [] + for fm in m.fieldModels: + # find which fields had explicit formatting + if fm.quizFontFamily or fm.quizFontSize or fm.quizFontColour: + pass + else: + unstyled.append(fm.name) + # fill out missing info + fm.quizFontFamily = fm.quizFontFamily or u"Arial" + fm.quizFontSize = fm.quizFontSize or 20 + fm.quizFontColour = fm.quizFontColour or "#000000" + fm.editFontSize = fm.editFontSize or 20 + unstyled = set(unstyled) + for cm in m.cardModels: + # embed the old font information into card templates + cm.qformat = txt % ( + cm.questionFontFamily, + cm.questionFontSize, + cm.questionFontColour, + cm.qformat) + cm.aformat = txt % ( + cm.answerFontFamily, + cm.answerFontSize, + cm.answerFontColour, + cm.aformat) + # escape fields that had no previous styling + for un in unstyled: + cm.qformat = cm.qformat.replace("%("+un+")s", "{{{%s}}}"%un) + cm.aformat = cm.aformat.replace("%("+un+")s", "{{{%s}}}"%un) + # rebuild q/a for the above & because latex has changed + for m in deck.models: + deck.updateCardsFromModel(m, dirty=False) + # rebuild the media db based on new format + rebuildMediaDir(deck, dirty=False) + deck.version = 61 + deck.s.commit() + if deck.version < 62: + # updated indices + deck.s.statement("drop index if exists ix_cards_typeCombined") + updateIndices(deck) + deck.version = 62 + deck.s.commit() + if deck.version < 64: + # remove old static indices, as all clients should be libanki1.2+ + for d in ("ix_cards_duePriority", + "ix_cards_priorityDue"): + deck.s.statement("drop index if exists %s" % d) + deck.version = 64 + deck.s.commit() + # note: we keep the priority index for now + if deck.version < 65: + # we weren't correctly setting relativeDelay when answering cards + # in previous versions, so ensure everything is set correctly + deck.rebuildTypes() + deck.version = 65 + deck.s.commit() + # skip a few to allow for updates to stable tree + if deck.version < 70: + # update dynamic indices given we don't use priority anymore + for d in ("intervalDesc", "intervalAsc", "randomOrder", + "dueAsc", "dueDesc"): + deck.s.statement("drop index if exists ix_cards_%s2" % d) + deck.s.statement("drop index if exists ix_cards_%s" % d) + deck.updateDynamicIndices() + # remove old views + for v in ("failedCards", "revCardsOld", "revCardsNew", + "revCardsDue", "revCardsRandom", "acqCardsRandom", + "acqCardsOld", "acqCardsNew"): + deck.s.statement("drop view if exists %s" % v) + deck.version = 70 + deck.s.commit() + if deck.version < 71: + # remove the expensive value cache + deck.s.statement("drop index if exists ix_fields_value") + # add checksums and index + deck.updateAllFieldChecksums() + updateIndices(deck) + deck.s.execute("vacuum") + deck.s.execute("analyze") + deck.version = 71 + deck.s.commit() + # executing a pragma here is very slow on large decks, so we store + # our own record + if not deck.getInt("pageSize") == 4096: + deck.s.commit() + deck.s.execute("pragma page_size = 4096") + deck.s.execute("pragma legacy_file_format = 0") + deck.s.execute("vacuum") + deck.setVar("pageSize", 4096, mod=False) + deck.s.commit() + if prog: + assert deck.modified == oldmod + deck.finishProgress()