summaryrefslogtreecommitdiffstats
path: root/src/database/schemamanager.cpp
blob: 4d7750ef44f5e6c612616c070dc5b1beba109353 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#include "database/schemamanager.h"

#include "util/db/fwdsqlquery.h"
#include "util/db/sqltransaction.h"
#include "util/xml.h"
#include "util/logger.h"
#include "util/assert.h"


const QString SchemaManager::SETTINGS_VERSION_STRING = "mixxx.schema.version";
const QString SchemaManager::SETTINGS_MINCOMPATIBLE_STRING = "mixxx.schema.min_compatible_version";

namespace {
    mixxx::Logger kLogger("SchemaManager");

    int readCurrentSchemaVersion(const SettingsDAO& settings) {
        QString settingsValue = settings.getValue(SchemaManager::SETTINGS_VERSION_STRING);
        // May be a null string if the schema has not been created. We default the
        // startVersion to 0 so that we automatically try to upgrade to revision 1.
        if (settingsValue.isNull()) {
            return 0; // initial version
        } else {
            bool ok = false;
            int schemaVersion = settingsValue.toInt(&ok);
            VERIFY_OR_DEBUG_ASSERT(ok && (schemaVersion >= 0)) {
                kLogger.critical()
                        << "Invalid database schema version" << settingsValue;
            }
            return schemaVersion;
        }
    }
    } // namespace

SchemaManager::SchemaManager(const QSqlDatabase& database)
        : m_database(database),
          m_settingsDao(m_database),
          m_currentVersion(readCurrentSchemaVersion(m_settingsDao)) {
}

bool SchemaManager::isBackwardsCompatibleWithVersion(int targetVersion) const {
    QString backwardsCompatibleVersion =
            m_settingsDao.getValue(SETTINGS_MINCOMPATIBLE_STRING);
    bool ok = false;
    int iBackwardsCompatibleVersion = backwardsCompatibleVersion.toInt(&ok);

    // If the current backwards compatible schema version is not stored in the
    // settings table, assume the current schema version is only backwards
    // compatible with itself.
    if (backwardsCompatibleVersion.isNull() || !ok) {
        // rryan 11/2010 We just added the backwards compatible flags, and some
        // people using the Mixxx trunk are already on schema version 7. This
        // special case is for them. Schema version 7 is backwards compatible
        // with schema version 3.
        if (m_currentVersion == 7) {
            iBackwardsCompatibleVersion = 3;
        } else {
            iBackwardsCompatibleVersion = m_currentVersion;
        }
    }

    // If the target version is greater than the minimum compatible version of
    // the current schema, then the current schema is backwards compatible with
    // targetVersion.
    return iBackwardsCompatibleVersion <= targetVersion;
}

SchemaManager::Result SchemaManager::upgradeToSchemaVersion(
        const QString& schemaFilename,
        int targetVersion) {
    VERIFY_OR_DEBUG_ASSERT(m_currentVersion >= 0) {
        return Result::UpgradeFailed;
    }

    if (m_currentVersion == targetVersion) {
        kLogger.info()
                << "Database schema is up-to-date"
                << "at version" << m_currentVersion;
        return Result::CurrentVersion;
    } else if (m_currentVersion < targetVersion) {
        kLogger.info()
                << "Upgrading database schema"
                << "from version" << m_currentVersion
                << "to version" << targetVersion;
    } else {
        if (isBackwardsCompatibleWithVersion(targetVersion)) {
            kLogger.info()
                    << "Current database schema is newer"
                    << "at version" << m_currentVersion
                    << "and backwards compatible"
                    << "with version" << targetVersion;
            return Result::NewerVersionBackwardsCompatible;
        } else {
            kLogger.warning()
                    << "Current database schema is newer"
                    << "at version" << m_currentVersion
                    << "and incompatible"
                    << "with version" << targetVersion;
            return Result::NewerVersionIncompatible;
        }
    }

    if (kLogger.debugEnabled()) {
        kLogger.debug()
                << "Loading database schema migrations from"
                << schemaFilename;
    }
    QDomElement schemaRoot = XmlParse::openXMLFile(schemaFilename, "schema");
    if (schemaRoot.isNull()) {
        kLogger.critical()
                << "Failed to load database schema migrations from"
                << schemaFilename;
        return Result::SchemaError;
    }

    QDomNodeList revisions = schemaRoot.childNodes();

    QMap<int, QDomElement> revisionMap;

    for (int i = 0; i < revisions.count(); i++) {
        QDomElement revision = revisions.at(i).toElement();
        QString version = revision.attribute("version");
        VERIFY_OR_DEBUG_ASSERT(!version.isNull()) {
            kLogger.critical()
                    << "Failed to parse database schema migrations from"
                    << schemaFilename;
            return Result::SchemaError;
        }
        int iVersion = version.toInt();
        revisionMap[iVersion] = revision;
    }

    // The checks above guarantee that currentVersion < targetVersion when we
    // get here.
    while (m_currentVersion < targetVersion) {
        int nextVersion = m_currentVersion + 1;

        // Now that we bake the schema.xml into the binary it is a programming
        // error if we include a schema.xml that does not have information on
        // how to get all the way to targetVersion.
        VERIFY_OR_DEBUG_ASSERT(revisionMap.contains(nextVersion)) {
            kLogger.critical()
                     << "Migration path for upgrading database schema"
                     << "from version" << m_currentVersion
                     << "to version" << nextVersion
                     << "is missing";
            return Result::SchemaError;
        }

        QDomElement revision = revisionMap[nextVersion];
        QDomElement eDescription = revision.firstChildElement("description");
        QDomElement eSql = revision.firstChildElement("sql");
        QString minCompatibleVersion = revision.attribute("min_compatible");

        // Default the min-compatible version to the current version string if
        // it's not in the schema.xml
        if (minCompatibleVersion.isNull()) {
            minCompatibleVersion = QString::number(nextVersion);
        }

        VERIFY_OR_DEBUG_ASSERT(!eSql.isNull()) {
            kLogger.critical()
                    << "Failed to parse database schema migrations from"
                    << schemaFilename;
            return Result::SchemaError;
        }

        QString description = eDescription.text();
        QString sql = eSql.text();

        kLogger.info()
                << "Upgrading to database schema to version"
                << nextVersion << ":"
                << description.trimmed();

        SqlTransaction transaction(m_database);

        // TODO(XXX) We can't have semicolons in schema.xml for anything other
        // than statement separators.
        QStringList sqlStatements = sql.split(";");

        QStringListIterator it(sqlStatements);

        bool result = true;
        while (result && it.hasNext()) {
            QString statement = it.next().trimmed();
            if (statement.isEmpty()) {
                // skip blank lines
                continue;
            }
            FwdSqlQuery query(m_database, statement);
            result = query.isPrepared() && query.execPrepared();
            if (!result &&
                    query.hasError() &&
                    query.lastError().databaseText().startsWith(
                            QStringLiteral("duplicate column name: "))) {
                // New columns may have already been added during a previous
                // migration to a different (= preceding) schema version. This
                // is a very common situation during development when switching
                // between schema versions. Since SQLite does not allow to add
                // new columns only if they do not yet exist we need to account
                // for and handle those errors here after they occurred. If the
                // remaining migration finishes without other errors this is
                // probably ok.
                kLogger.warning()
                        << "Ignoring failed statement"
                        << statement
                        << "and continuing with schema migration";
                result = true;
            }
        }

        if (result) {
            m_settingsDao.setValue(SETTINGS_VERSION_STRING, nextVersion);
            m_settingsDao.setValue(SETTINGS_MINCOMPATIBLE_STRING, minCompatibleVersion);
            transaction.commit();
            m_currentVersion = nextVersion;
            kLogger.info()
                    << "Upgraded database schema"
                    << "to version" << m_currentVersion;
        } else {
            kLogger.critical()
                    << "Failed to upgrade database schema from version"
                    << m_currentVersion << "to version" << nextVersion;
            transaction.rollback();
            return Result::UpgradeFailed;
        }
    }
    return Result::UpgradeSucceeded;
}