diff --git a/src/updater.cpp b/src/updater.cpp --- a/src/updater.cpp +++ b/src/updater.cpp @@ -1,836 +1,796 @@ #include "updater.h" #include "psettings.h" #include #include #include #include #include #include #include DatabaseUpdater::DatabaseUpdater() : QObject() , _pset(0) , _revisionBefore(0) , _revisionAfter(0) + , _processingUri(false) { } DatabaseUpdater::~DatabaseUpdater() { delete _pset; } int DatabaseUpdater::run(ProgramSettings& a_pset) { _pset = new ProgramSettings(a_pset); if (!checkArguments()) return -1; if (!readConfig()) return -1; if (!loadScriptsFromResource()) return -1; if (!runScripts()) return -1; return 0; } //--------------------------------------------------------------------------- bool DatabaseUpdater::checkArguments() { if (_pset->controlFile.isEmpty()) { emit error(QStringLiteral("Не задан файл конфигурации.")); return false; } //если не задано полное имя файла конфигурации, то искать его в текущем каталоге QString filename = _pset->controlFile; if (!QDir::isAbsolutePath(filename)) filename = QStringLiteral("%1/%2").arg(QDir::currentPath()).arg(_pset->controlFile); //проверить наличие файла if (!QFile::exists(filename)) { emit error(QStringLiteral("Не найден файл конфигурации\n") + QDir::toNativeSeparators(filename)); return false; } _pset->controlFile = filename; return true; } //--------------------------------------------------------------------------- bool DatabaseUpdater::readConfig() { //открываем входной файл QFile controlFile(_pset->controlFile); if (!controlFile.open(QIODevice::ReadOnly)) { emit error(QStringLiteral("Сбой при загрузке файла конфигурации:\n" "Невозможно открыть для чтения файл %1") .arg(QDir::toNativeSeparators(_pset->controlFile))); return false; } //парсим входной файл QXmlStreamReader xml; xml.setDevice(&controlFile); emit message(QStringLiteral("Загрузка конфигурации из файла\n%1").arg(QDir::toNativeSeparators(_pset->controlFile))); //идём по элементам документа while (!xml.atEnd()) { xml.readNext(); if (xml.isStartElement() && xml.name() == "package") if (!readPackage(xml)) return false; } if (xml.hasError()) { emit error(QStringLiteral("Сбой при загрузке файла конфигурации:\n" "Ошибка: %1, строка %2, позиция %3") .arg(xml.errorString()) .arg(xml.lineNumber()) .arg(xml.columnNumber())); return false; } controlFile.close(); //найти файл с расширением pkginfo и именем как у входного файла, если такого нет, //то использовать первый попавшийся файл с расширением pkginfo; //прочитать из него идентификатор пакета и имя базы данных; //если файла нет, то и не надо QFileInfo fi(controlFile); QStringList files = QDir(QFileInfo(controlFile).path(), "*.pkginfo", QDir::NoSort, QDir::Files).entryList(); if (!files.isEmpty()) { QString pkgfile = QFileInfo(controlFile).completeBaseName() + ".pkginfo"; if (!files.contains(pkgfile, Qt::CaseInsensitive)) pkgfile = files.at(0); pkgfile = QFileInfo(controlFile).path() + '/' + pkgfile; emit message(QStringLiteral("Загрузка конфигурации из файла\n%1").arg(QDir::toNativeSeparators(pkgfile))); KSettings pkginfo(pkgfile); pkginfo.beginGroup("package"); QString s = pkginfo.value("dbname").toString(); if (!s.isEmpty() && _pset->database.isEmpty()) _pset->database = s; QStringList ver = pkginfo.value("version").toString().split(QChar('.')); if (ver.size() > 1) { s = ver.at(1); bool ok; int i = s.toInt(&ok); if (ok) _pset->dbVersion = i; } _pset->packageId = pkginfo.value("id").toString(); } //после загрузки конфигурации из файлов должно быть определено имя БД if (_pset->database.isEmpty()) { emit error(QStringLiteral("Не задано имя базы данных")); return false; } return true; } //--------------------------------------------------------------------------- bool DatabaseUpdater::readPackage(QXmlStreamReader& a_xml) { const QXmlStreamAttributes& packageAttrs = a_xml.attributes(); QString packageId = packageAttrs.value("id").toString(); QString uriAttr = packageAttrs.value("uri").toString(); bool uri = !((uriAttr == "no") || (uriAttr == "нет")); - Package* package = new Package(packageId, uri); - _packages.append(package); + Package package(packageId, uri); //собираем информацию о скриптах данного пакета const QString SCRIPT_NODE("script"); const QString REVISION_ATTR("revision"); const QString TRANSACTION_ATTR("transaction"); const QString COMMENT_ATTR("comment"); const QString SCRIPT_ATTR("file"); const QString NO_REQUIRED_ATTR = QStringLiteral("Неправильное содержание файла конфигурации:\n" "Не указан обязательный атрибут «%1» пакета «%2»"); while (!a_xml.atEnd()) { a_xml.readNext(); if (a_xml.isStartElement() && a_xml.name() == SCRIPT_NODE) { const QXmlStreamAttributes& attrs = a_xml.attributes(); //проверка наличия обязательных атрибутов if (!attrs.hasAttribute(REVISION_ATTR)) { emit error(NO_REQUIRED_ATTR.arg(REVISION_ATTR).arg(packageId)); return false; } if (!attrs.hasAttribute(SCRIPT_ATTR)) { emit error(NO_REQUIRED_ATTR.arg(SCRIPT_ATTR).arg(packageId)); return false; } //проверка правильности указания атрибутов bool ok; int revision = attrs.value(REVISION_ATTR).toString().toInt(&ok); if (!ok) { emit error(QStringLiteral( "Атрибут «revision» пакета «%1» должен быть числом") .arg(packageId)); return false; } //заносим информацию о скрипте в список скриптов пакета QString file = attrs.value(SCRIPT_ATTR).toString(); QString comment = attrs.value(COMMENT_ATTR).toString(); QString strans = attrs.value(TRANSACTION_ATTR).toString(); bool transaction = !((strans == "0") || (strans.compare("no", Qt::CaseInsensitive) == 0) || (strans.compare("нет", Qt::CaseInsensitive) == 0)); - DatabaseScript* script = new DatabaseScript(revision, transaction, - file, comment); - package->addScript(script); + package.addScript(DatabaseScript(file, transaction, revision, comment)); } } //упорядочить скрипты по возрастанию номеров ревизий - package->sortScripts(); + package.sortScripts(); + _packages.append(package); return true; } //--------------------------------------------------------------------------- bool DatabaseUpdater::runScripts() { //определение пакета, скрипты которого будут выполняться auto pp = std::find_if(_packages.cbegin(), _packages.cend(), - [this](Package* p){return p->id() == _pset->packageId;}); + [this](const Package& p){return p.id() == _pset->packageId;}); if (pp == _packages.cend()) { emit error(QStringLiteral("Для пакета «%1» не задано ни одного " "сценария создания базы данных") .arg(_pset->packageId)); return false; } //проверка наличия файлов со скриптами на диске - Package* package = *pp; + const Package& package = *pp; QStringList paths; QFileInfo fi(_pset->controlFile); paths << fi.canonicalPath() << fi.canonicalPath() + "/script" << QDir::currentPath(); - DatabaseScript* script = 0; - auto packageScripts = package->scripts(); - auto scriptsEnd = packageScripts.cend(); - for (auto it = packageScripts.cbegin(); it != scriptsEnd; ++it) + const auto& packageScripts = package.scripts(); + for (auto it = packageScripts.begin(); it != packageScripts.end(); ++it) { - script = *it; - if (!script->findScript(paths)) + const DatabaseScript& script = *it; + if (!script.findScript(paths)) { emit error(QStringLiteral("Не найден файл сценария создания " "базы данных %1") - .arg(script->script())); + .arg(script.script())); return false; } } QString curDate = QDate::currentDate().toString(Qt::ISODate); SqlProcessor proc; SqlProcessor uriProc; emit message(QStringLiteral("Подключение к серверу баз данных")); //пока идут служебные подключения к БД и запросы, подключаем только сигнал error connect(&proc, SIGNAL(error(const QString&, const QString&, const QString&)), this, SIGNAL(sqlError(const QString&, const QString&, const QString&))); connect(&uriProc, SIGNAL(error(const QString&, const QString&, const QString&)), this, SIGNAL(sqlError(const QString&, const QString&, const QString&))); //---- template1 ---- //попытка подключения к template1 и получение списка таблиц if (!proc.connectdb(_pset->host, _pset->port, "template1", _pset->username, _pset->password)) return false; QStringList template1Tables = databaseTableList(proc); - int currentDatabaseRevision = 0; //БД нет или пустая - //удаление БД если задано параметром при запуске программы - bool dbExists = databaseExists(proc, _pset->database); - if (dbExists && _pset->dropdb) - { - emit message(QStringLiteral("Удаление базы данных **%1**").arg(_pset->database)); - - if (!dropDatabase(proc, _pset)) - return false; - dbExists = false; - } - //создание БД если она не существует - if (!dbExists) - { - emit message(QStringLiteral("Создание базы данных **%1**").arg(_pset->database)); - - if (!createDatabase(proc, _pset)) - return false; - } + //создание БД + if (!createDatabase(proc, _pset, _pset->database)) + return false; + //создание учебной БД QString uriDatabase = _pset->database + "_u"; - if (package->uri()) + if (package.wantUri()) { - //удаление учебной БД если задано параметром при запуске программы - bool uriDbExists = databaseExists(proc, uriDatabase); - if (uriDbExists && _pset->dropdb) - { - emit message(QStringLiteral("Удаление учебной базы данных **%1**").arg(uriDatabase)); - - if (!dropDatabase(proc, _pset, uriDatabase)) - return false; - uriDbExists = false; - } - //создание учебной БД если она не существует - if (!uriDbExists) - { - emit message(QStringLiteral("Создание учебной базы данных **%1**").arg(uriDatabase)); - - if (!createDatabase(proc, _pset, uriDatabase)) - return false; - } + UriGuard ug(this); + if (!createDatabase(proc, _pset, uriDatabase)) + return false; } connect(&proc, SIGNAL(afterConnect(QString, QString, QString, QString, QString)), this, SLOT(afterConnect(QString, QString, QString, QString, QString))); + connect(&uriProc, SIGNAL(afterConnect(QString, QString, QString, QString, QString)), + this, SLOT(afterConnect(QString, QString, QString, QString, QString))); - //---- database ---- - //подключение к созданной/существующей БД и получение списка таблиц + //подключение к базам данных if (!proc.connectdb(_pset->host, _pset->port, _pset->database, _pset->username, _pset->password)) return false; - QStringList databaseTables = databaseTableList(proc); - - //подключение к учебной БД и получение списка таблиц - QStringList uriDatabaseTables; - if (package->uri()) + if (package.wantUri()) { - //---- database_u ---- - if (!uriProc.connectdb(_pset->host, _pset->port, uriDatabase, - _pset->username, _pset->password)) + if (!uriProc.connectdb(_pset->host, _pset->port, uriDatabase, _pset->username, + _pset->password)) return false; - - uriDatabaseTables = databaseTableList(uriProc); - } - - //считывание данных о версии БД из таблицы dm_version, - //эта таблица существует только в основной БД, версия учебной должна совпадать - bool legacyDatabase = false; - proc.execSQL("select relnatts from pg_class " - "where relname='dm_version' and relkind='r'"); - - PGresult* pgresult = proc.result().pgresult; - - //если пустой результат запроса, значит таблицы dm_version нет в БД - // bool createDmVersion = data.size() == 0; - bool createDmVersion = PQntuples(pgresult) == 0; - - //если таблица есть, то сравнить число её атрибутов с эталоном - if (!createDmVersion) - { - const char* pgvalue = PQgetvalue(pgresult, 0, 0); - if (QByteArray::fromRawData(pgvalue, qstrlen(pgvalue)).toInt() != DM_VERSION_FIELDS_COUNT) - { //не та структура, удаляем таблицу - if (!proc.execSQL("drop table dm_version")) - return false; - createDmVersion = true; - } } - if (createDmVersion) - { //БД уже существовала, а таблицы dm_version в ней нет, или не та структура - if (databaseTables != template1Tables) - { - legacyDatabase = true; - currentDatabaseRevision = 1; - } - - emit message(QStringLiteral("Создание таблицы изменений БД")); - proc.execSQL("begin"); - if (!proc.execute(_createDmVersionScript)) - { - emit error(QStringLiteral("Сбой при создании таблицы изменений БД")); - return false; - } + StatusResult statusResult = databaseStatus(proc, package.id(), template1Tables); + if (statusResult.failed()) + return false; - if (legacyDatabase) - { //запись информации о версии №1 в unversioned БД - if (!proc.execSQL(QStringLiteral("insert into dm_version " - "(revision,package_id,comment,gen_date) values " - "(1,'%1','Изначальная версия','%2')") - .arg(package->id(), curDate))) - return false; - } - proc.execSQL("commit"); - } - else + int databaseRevision = statusResult.databaseRevision(); + int uriDatabaseRevision = 0; + if (package.wantUri()) { - proc.execSQL(QStringLiteral("select max(revision), count(*) from dm_version " - "where package_id='%1'") - .arg(package->id())); - PGresult* pgresult = proc.result().pgresult; - if (PQntuples(pgresult)) - { - const char* pgvalue = PQgetvalue(pgresult, 0, 1); - if (QByteArray::fromRawData(pgvalue, qstrlen(pgvalue)).toInt() > 0) - { - pgvalue = PQgetvalue(pgresult, 0, 0); - currentDatabaseRevision = QByteArray::fromRawData(pgvalue, qstrlen(pgvalue)).toInt(); - } - } + UriGuard ug(this); + statusResult = databaseStatus(uriProc, package.id(), template1Tables); + if (statusResult.failed()) + return false; + uriDatabaseRevision = statusResult.databaseRevision(); } //создание языка plpgsql - if (!createLanguagePlpgsql(proc, uriProc, package->uri())) + if (!createLanguagePlpgsql(proc, uriProc, package.wantUri())) return false; - _revisionAfter = _revisionBefore = currentDatabaseRevision; + _revisionAfter = _revisionBefore = databaseRevision; //выполнение необходимых скриптов connect(&proc, SIGNAL(progress(int)), this, SIGNAL(progress(int))); connect(&uriProc, SIGNAL(progress(int)), this, SIGNAL(progress(int))); - scriptsEnd = packageScripts.cend(); - for (auto it = packageScripts.cbegin(); it != scriptsEnd; ++it) + const auto& end = packageScripts.cend(); + for (auto it = packageScripts.cbegin(); it != end; ++it) { - script = *it; - int revision = script->revision(); - bool transaction = script->transaction(); - - //если была найдена изначальная БД, то всё уже сделано для 1-й версии - if ((revision == 1) && legacyDatabase) - continue; - - //пропускаем уже установленные версии БД - if (revision <= currentDatabaseRevision) - continue; - - if (revision == 1) - emit message(QStringLiteral("Создание базы данных версии %1").arg(revision)); - else - emit message(QStringLiteral("Обновление базы данных до версии %1").arg(revision)); + const DatabaseScript& script = *it; + int revision = script.revision(); //подготовка скрипта к выполнению - QList mainScript; - QList globalsScript; - prepareScripts(script->script(), mainScript, globalsScript); + QList preparedScript = prepareScript(script.script()); - //если в скрипте были команды создания глобальных объектов, то выполнить - //их в отдельной транзакции - if (globalsScript.size() > 0) + //выполнение скриптов в БД, пропуская уже установленные ревизии + if (revision > databaseRevision) { - proc.execSQL("begin"); - if (!proc.execute(globalsScript)) - return false; - proc.execSQL("commit"); + if ( !runScript(proc, preparedScript, script, package.id()) ) + return false; + //сброс мандатных меток, если они есть в БД + clearMaclabels(proc); } - //выполнение скрипта - if (transaction) - proc.execSQL("begin"); - - if (!proc.execute(mainScript)) - return false; - //выполнение скриптов в учебной БД, если задано её создание - if (package->uri()) + if (package.wantUri() && revision > uriDatabaseRevision) { - if (transaction) - uriProc.execSQL("begin"); - - if (revision == 1) - emit message(QStringLiteral("Создание учебной базы данных версии %1").arg(revision)); - else - emit message(QStringLiteral("Обновление учебной базы данных до версии %1").arg(revision)); - - if (!uriProc.execute(mainScript)) - { - if (transaction) - proc.execSQL("rollback"); - return false; - } - if (!uriProc.execute(_setSearchPathScript)) - return false; + UriGuard ug(this); + if ( !runScript(uriProc, preparedScript, script, package.id()) ) + return false; clearMaclabels(uriProc); - if (transaction) - uriProc.execSQL("commit"); } - //сброс параметра search_path, он мог быть изменён при выполнении скрипта - if (!proc.execute(_setSearchPathScript)) - return false; - - //запись информации о версии в БД - if (!proc.execSQL(QStringLiteral("insert into dm_version " - "(revision,package_id,comment,gen_date) values " - "(%1,'%2','%3','%4')") - .arg(revision) - .arg(package->id()) - .arg(script->comment()) - .arg(curDate))) - return false; - - //сброс мандатных меток, если они есть в БД - clearMaclabels(proc); - - if (transaction) - proc.execSQL("commit"); - _revisionAfter = revision; } return true; } //--------------------------------------------------------------------------- +bool DatabaseUpdater::runScript(SqlProcessor& a_proc, + const QList& a_script, + const DatabaseScript& a_databaseScript, + const QString& a_packageId) +{ + int revision = a_databaseScript.revision(); + bool transaction = a_databaseScript.transaction(); + + if (revision == 1) + emit message(messageString(CREATE_DB_VERSION).arg(revision)); + else + emit message(messageString(UPDATE_DB_VERSION).arg(revision)); + + //выполнение скрипта + if (transaction) + a_proc.execSQL("begin"); + + auto script = a_script; + if (!a_proc.execute(script)) + return false; + + //сброс параметра search_path, он мог быть изменён при выполнении скрипта + if (!a_proc.execute(_resetSearchPathScript)) + return false; + + //запись информации о версии в БД + if (!a_proc.execSQL(QStringLiteral("insert into dm_version " + "(revision,package_id,comment,gen_date) values " + "(%1,'%2','%3',CURRENT_DATE)") + .arg(revision) + .arg(a_packageId) + .arg(a_databaseScript.comment()))) + return false; + + if (transaction) + a_proc.execSQL("commit"); + + return true; +} + +//--------------------------------------------------------------------------- bool DatabaseUpdater::databaseExists(SqlProcessor& a_proc, const QString& a_dbname) { a_proc.execSQL(QStringLiteral("select 1 from pg_database where datname='%1\'").arg(a_dbname)); return PQntuples(a_proc.result().pgresult) == 1; } //--------------------------------------------------------------------------- bool DatabaseUpdater::createDatabase(SqlProcessor& a_proc, ProgramSettings* a_pset, const QString& a_dbname) { - if (a_dbname.isEmpty() && a_pset->database.isEmpty()) - return false; - - QString dbname = (a_dbname.isEmpty() ? a_pset->database : a_dbname); - //если уже подключены к указанной БД, то отключение - if (a_proc.database() == dbname) + if (a_proc.database() == a_dbname) a_proc.disconnectdb(); //если не подключены, то подключение к template1 if (a_proc.database().isNull()) { if (!a_proc.connectdb(a_pset->host, a_pset->port, "template1", a_pset->username, a_pset->password)) return false; } - //если БД уже существует, то создавать не надо - if (databaseExists(a_proc, dbname)) - return true; + //удаление БД если задано параметром при запуске программы + bool dbExists = databaseExists(a_proc, a_dbname); + if (dbExists && _pset->dropdb) + { + emit message(messageString(DROP_DB).arg(a_dbname)); - return a_proc.execSQL(QString("create database \"%1\"").arg(dbname)) - && a_proc.commandStatus() == QString("CREATE DATABASE"); -} + bool dropped = a_proc.execSQL(QString("drop database \"%1\"").arg(a_dbname)) + && a_proc.commandStatus() == QString("DROP DATABASE"); + + if (!dropped) + return false; -//--------------------------------------------------------------------------- -bool DatabaseUpdater::dropDatabase(SqlProcessor& a_proc, ProgramSettings* a_pset, - const QString& a_dbname) -{ - if (a_pset->database.isEmpty()) - return false; - - QString dbname = (a_dbname.isEmpty() ? a_pset->database : a_dbname); + dbExists = false; + } - //если уже подключены к указанной БД, то отключение - if (a_proc.database() == dbname) - a_proc.disconnectdb(); + //создание БД если она не существует + if (!dbExists) + { + emit message(messageString(CREATE_DB).arg(a_dbname)); - //если не подключены, то подключение к template1 - if (a_proc.database().isNull()) - { - if (!a_proc.connectdb(a_pset->host, a_pset->port, "template1", - a_pset->username, a_pset->password)) + bool created = a_proc.execSQL(QString("create database \"%1\"").arg(a_dbname)) + && a_proc.commandStatus() == QString("CREATE DATABASE"); + + if (!created) return false; } - //если БД не существует, то удалять не надо - if (!databaseExists(a_proc, dbname)) - return true; - - return a_proc.execSQL(QString("drop database \"%1\"").arg(dbname)) - && a_proc.commandStatus() == QString("DROP DATABASE"); + return true; } //--------------------------------------------------------------------------- // Получение списка таблиц в БД //--------------------------------------------------------------------------- QStringList DatabaseUpdater::databaseTableList(SqlProcessor& a_proc) { QStringList result; if (!a_proc.execSQL("select tablename from pg_tables order by tablename")) return result; SqlResult sqlresult = a_proc.result(); PGresult* pgresult = sqlresult.pgresult; QTextCodec* codec = sqlresult.codec; int numTuples = PQntuples(pgresult); for (int i = 0; i < numTuples; ++i) { if (PQgetisnull(pgresult, i, 0)) result.append(QString()); else result.append(codec->toUnicode(PQgetvalue(pgresult, i, 0))); } return result; } //--------------------------------------------------------------------------- +// Получение номера текущей ревизии базы данных +// +// При необходимости создаётся таблица dm_version. +//--------------------------------------------------------------------------- +DatabaseUpdater::StatusResult DatabaseUpdater::databaseStatus(SqlProcessor& a_proc, + const QString& a_packageId, + const QStringList& a_template1Tables) +{ + int currentDatabaseRevision = 0; //БД нет или пустая + + //считывание данных о версии БД из таблицы dm_version, + //эта таблица существует только в основной БД, версия учебной должна совпадать + a_proc.execSQL("select relnatts from pg_class where relname='dm_version' and relkind='r'"); + + PGresult* pgresult = a_proc.result().pgresult; + + //если пустой результат запроса, значит таблицы dm_version нет в БД + bool createDmVersion = PQntuples(pgresult) == 0; + + //если таблица есть, то сравнить число её атрибутов с эталоном + if (!createDmVersion) + { + const char* pgvalue = PQgetvalue(pgresult, 0, 0); + if (QByteArray::fromRawData(pgvalue, qstrlen(pgvalue)).toInt() != DM_VERSION_FIELDS_COUNT) + { //не та структура, удаляем таблицу + if (!a_proc.execSQL("drop table dm_version")) + return StatusResult(false); + createDmVersion = true; + } + } + + if (createDmVersion) + { + emit message(messageString(CREATE_VERSION_TABLE)); + a_proc.execSQL("begin"); + if (!a_proc.execute(_createDmVersionScript)) + { + emit error(messageString(ERROR_VERSION_TABLE)); + return StatusResult(false); + } + + //если БД уже существует и её схема отличается от схемы template1, + //т.е. в ней есть таблицы, то считается, что это ревизия №1 + QStringList databaseTables = databaseTableList(a_proc); + databaseTables.removeOne("dm_version"); + + if (databaseTables != a_template1Tables) + { + if (!a_proc.execSQL(QStringLiteral("insert into dm_version " + "(revision,package_id,comment,gen_date) values " + "(1,'%1','Изначальная версия',CURRENT_DATE)") + .arg(a_packageId))) + return StatusResult(false, currentDatabaseRevision); + currentDatabaseRevision = 1; + } + a_proc.execSQL("commit"); + } + else + { + a_proc.execSQL(QStringLiteral("select max(revision), count(*) from dm_version " + "where package_id='%1'") + .arg(a_packageId)); + PGresult* pgresult = a_proc.result().pgresult; + if (PQntuples(pgresult)) + { + const char* pgvalue = PQgetvalue(pgresult, 0, 1); + if (QByteArray::fromRawData(pgvalue, qstrlen(pgvalue)).toInt() > 0) + { + pgvalue = PQgetvalue(pgresult, 0, 0); + currentDatabaseRevision = QByteArray::fromRawData(pgvalue, qstrlen(pgvalue)).toInt(); + } + } + } + + return StatusResult(true, currentDatabaseRevision); +} + +//--------------------------------------------------------------------------- bool DatabaseUpdater::loadScriptsFromResource() { QString errmess = QStringLiteral("Сбой при чтении ресурсов программы"); SqlProcessor proc; QFile resFile(":/dm_version.sql"); if (!(resFile.open(QIODevice::ReadOnly))) { emit error(errmess); return false; } _createDmVersionScript = proc.parse(resFile); resFile.close(); resFile.setFileName(":/create_helper_functions.sql"); if (!(resFile.open(QIODevice::ReadOnly))) { emit error(errmess); return false; } _createHelperFunctionsScript = proc.parse(resFile); resFile.close(); resFile.setFileName(":/drop_helper_functions.sql"); if (!(resFile.open(QIODevice::ReadOnly))) { emit error(errmess); return false; } _dropHelperFunctionsScript = proc.parse(resFile); resFile.close(); resFile.setFileName(":/set_search_path.sql"); if (!(resFile.open(QIODevice::ReadOnly))) { emit error(errmess); return false; } - _setSearchPathScript = proc.parse(resFile); + _resetSearchPathScript = proc.parse(resFile); return true; } //--------------------------------------------------------------------------- // Подготавливает скрипт, считанный из файла, к выполнению в БД. -// Команды анализируются и разделяются на два скрипта — основной и глобальных -// объектов. Обработка заключается в следующем: +// Подготовка заключается в следующем: // 1. Команды создания глобальных объектов // CREATE GROUP // CREATE USER // CREATE ROLE // CREATE TABLESPACE // заменяются вызовами функций-обёрток для их безопасного выполнения. -// Эти вызовы вставляются в скрипт глобальных объектов. В этот же скрипт -// добавляются команды создания функций-обёрток и их удаления. -// 2. Команды установки кодировки клиента -// SET CLIENT_ENCODING -// SET NAMES -// \encoding -// добавляются в оба скрипта. -// 3. Команда подключения языка plpgsql +// В скрипт добавляются команды создания функций-обёрток и их удаления. +// 2. Команда подключения языка plpgsql // CREATE [ TRUSTED ] [ PROCEDURAL ] LANGUAGE plpgsql -// игнорируется и не попадает ни в один скрипт. Создание языка plpgsql -// производится отдельно, перед началом выполнения скриптов. +// удаляется из скрипта. +// Создание языка plpgsql производится отдельно, перед началом выполнения скриптов. //--------------------------------------------------------------------------- -void DatabaseUpdater::prepareScripts(const QString& a_filename, - QList& oa_mainScript, - QList& oa_globalsScript) +QList DatabaseUpdater::prepareScript(const QString& a_filename) { //пропустим файл через парсер, на выходе список команд - bool helperInjected = false; SqlProcessor proc; QList result = proc.parse(a_filename); //обход списка и анализ команд - auto end = result.cend(); - for (auto it = result.cbegin(); it != end; ++it) + bool helperRequired = false; + auto end = result.end(); + for (auto it = result.begin(); it != end; ++it) { - const QByteArray& line = *it; + QByteArray& line = *it; int i = 0; int end = line.size(); //пропускаем первое слово, перед ним пробелов нет, парсер их убирает while (i != end && !QChar(line.at(i)).isSpace()) ++i; - //анализируем первое слово + //выделяем первое слово QByteArray command1 = line.left(i).toLower(); - if (command1 == "\\encoding") - { - oa_mainScript.append(line); - oa_globalsScript.append(line); - continue; - } //переход на начало второго слова while (i != end && QChar(line.at(i)).isSpace()) ++i; int p = i; //пропускаем второе слово while (i != end && !QChar(line.at(i)).isSpace()) ++i; - //анализируем команду + + //выделяем второе слово QByteArray command2 = line.mid(p, i - p).toLower(); - if (command1 == "set") - { - oa_mainScript.append(line); - if (command2 == "names" || command2.startsWith("client_encoding")) - oa_globalsScript.append(line); - } - else if (command1 == "create") + + //анализируем команду + if (command1 == "create") { if (command2 == "group" || command2 == "user" || command2 == "role" || command2 == "tablespace") - { //добавляем в выходной скрипт globals вызов безопасной функции-обёртки - if (!helperInjected) - { //но сначала вставляем команды создания функций-обёрток - helperInjected = true; - for (const auto& helperScript: _createHelperFunctionsScript) - oa_globalsScript.append(helperScript); - } + { //добавляем в выходной скрипт вызов безопасной функции-обёртки + helperRequired = true; + //переход на начало имени объекта while (i != end && QChar(line.at(i)).isSpace()) ++i; QByteArray name; if (i != end) { if (line.at(i) == '"') { //если имя объекта начинается с двойной кавычки, то пропуск до следующей //двойной кавычки ++i; //пропустить открывающую двойную кавычку p = i; while (i != end && line.at(i) != '"') ++i; name = line.mid(p, i - p); if (i != end) ++i; //пропустить закрывающую двойную кавычку } else { //пропуск до конца слова p = i; while (i != end && !QChar(line.at(i)).isSpace()) ++i; name = line.mid(p, i - p); if (name.size() > 0 && name.at(name.size() - 1) == ';') name.chop(1); } } QByteArray parameters(line.right(end - i)); if (parameters.size() > 0 && parameters.at(parameters.size() - 1) == ';') parameters.chop(1); QByteArray safeCall("select _rct_safe_create_"); safeCall.append(command2).append("('").append(name).append("','").append(parameters).append("');"); - oa_globalsScript.append(safeCall); + //заменяем строку в выходном массиве + line = safeCall; } else if (command2 == "trusted" || command2 == "procedural" || command2 == "language") { //create language не нужен - continue; + line.clear(); } - else - { //create неглобального объекта, в основной скрипт - oa_mainScript.append(line); - } - } - else - { //все остальные команды в основной скрипт - oa_mainScript.append(line); } } //цикл по командам - if (helperInjected) - oa_globalsScript.append(_dropHelperFunctionsScript); + if (helperRequired) + { + for (const auto& helperScript: _createHelperFunctionsScript) + result.prepend(helperScript); + + result.append(_dropHelperFunctionsScript); + } + + return result; } //--------------------------------------------------------------------------- void DatabaseUpdater::clearMaclabels(SqlProcessor& a_proc) { a_proc.execSQL("select distinct 1 from pg_attribute a join pg_class c " "on (a.attrelid=c.oid) where c.relname='pg_class' and a.attname='relmaclabel'"); if (PQntuples(a_proc.result().pgresult) == 1) a_proc.execSQL("update pg_class set relmaclabel=null"); } //--------------------------------------------------------------------------- bool DatabaseUpdater::createLanguagePlpgsql(SqlProcessor& a_proc, SqlProcessor& a_uriProc, bool a_uri) { const QString CREATE_SQL = QStringLiteral("create language plpgsql"); const QString CHECK_SQL = QStringLiteral("select 1 from pg_language where lanname='plpgsql'"); a_proc.execSQL(CHECK_SQL); if (PQntuples(a_proc.result().pgresult) == 0) { if (!a_proc.execSQL(CREATE_SQL)) return false; } if (a_uri) { a_uriProc.execSQL(CHECK_SQL); if (PQntuples(a_uriProc.result().pgresult) == 0) { if (!a_uriProc.execSQL(CREATE_SQL)) return false; } } return true; } //--------------------------------------------------------------------------- void DatabaseUpdater::afterConnect(const QString& a_host, const QString& a_port, const QString& a_database, const QString& a_username, const QString& a_password) { Q_UNUSED(a_port); Q_UNUSED(a_password); emit logConnectionParameters(a_host, a_database, a_username); } //--------------------------------------------------------------------------- +QString DatabaseUpdater::messageString(DatabaseUpdater::MessageId a_message) +{ + QHash strings = { + {CREATE_DB, QStringLiteral("Создание базы данных **%1**")}, + {DROP_DB, QStringLiteral("Удаление базы данных **%1**")}, + {CREATE_DB_VERSION, QStringLiteral("Создание базы данных версии %1")}, + {UPDATE_DB_VERSION, QStringLiteral("Обновление базы данных до версии %1")}, + {CREATE_VERSION_TABLE, QStringLiteral("Создание таблицы изменений базы данных")}, + {ERROR_VERSION_TABLE, QStringLiteral("Сбой при создании таблицы изменений базы данных")} }; + + QString result = strings[a_message]; + if (_processingUri) + result.replace(QStringLiteral(" базы"), QStringLiteral(" учебной базы")); + return result; +} + //--------------------------------------------------------------------------- -bool DatabaseScript::findScript(const QStringList& a_paths) +//--------------------------------------------------------------------------- +bool DatabaseScript::findScript(const QStringList& a_paths) const { - _script.replace(QChar('\\'), QChar('/')); + _scriptFile.replace(QChar('\\'), QChar('/')); foreach (QString path, a_paths) { QDir dir(path); - if (dir.exists(_script)) + if (dir.exists(_scriptFile)) { - _script = dir.absoluteFilePath(_script); + _scriptFile = dir.absoluteFilePath(_scriptFile); return true; } } return false; } + +//--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- +void Package::sortScripts() +{ + std::sort(_scripts.begin(), _scripts.end(), + [](DatabaseScript& s1, DatabaseScript& s2){ return s1.revision() < s2.revision(); }); +} diff --git a/src/updater.h b/src/updater.h --- a/src/updater.h +++ b/src/updater.h @@ -1,123 +1,150 @@ #ifndef UPDATER_H #define UPDATER_H #include #include +#include class DatabaseScript { - friend class DatabaseUpdater; - public: - DatabaseScript(int a_revision, bool a_transaction, const QString& a_script, + DatabaseScript(const QString& a_scriptFile, bool a_transaction, int a_revision, const QString& a_comment) : _revision(a_revision) , _transaction(a_transaction) - , _script(a_script) + , _scriptFile(a_scriptFile) , _comment(a_comment) { } int revision() const { return _revision; } bool transaction() const { return _transaction; } - QString script() const { return _script; } + QString script() const { return _scriptFile; } QString comment() const { return _comment; } - bool findScript(const QStringList& a_paths); + bool findScript(const QStringList& a_paths) const; private: int _revision; bool _transaction; - QString _script; + mutable QString _scriptFile; QString _comment; }; class Package { public: - Package(const QString& a_id, bool a_uri = true) + Package(const QString& a_id, bool a_wantUri = true) : _id(a_id) - , _uri(a_uri) + , _wantUri(a_wantUri) { } - void addScript(DatabaseScript* a_script) { _scripts.append(a_script); } + void addScript(const DatabaseScript& a_script) { _scripts.append(a_script); } + void sortScripts(); - void sortScripts() { qSort(_scripts.begin(), _scripts.end(), scriptRevisionLessThan); } - static bool scriptRevisionLessThan(DatabaseScript* a_script1, - DatabaseScript* a_script2) { return a_script1->revision() < a_script2->revision(); } - - bool uri() const { return _uri; } + bool wantUri() const { return _wantUri; } QString id() const { return _id; } - QList scripts() const { return _scripts; } + QList scripts() const { return _scripts; } private: - QList _scripts; + QList _scripts; QString _id; - bool _uri; + bool _wantUri; }; class SqlProcessor; class ProgramSettings; class QByteArray; class QByteArrayMatcher; class QXmlStreamReader; class DatabaseUpdater : public QObject { Q_OBJECT + friend class UriGuard; public: DatabaseUpdater(); ~DatabaseUpdater() override; int run(ProgramSettings& a_pset); int revisionBefore() const { return _revisionBefore; } int revisionAfter() const { return _revisionAfter; } signals: void error(const QString& a_message); void sqlError(const QString& a_dbError, const QString& a_commandDescription, const QString& a_command); void logConnectionParameters(const QString& a_host, const QString& a_database, const QString& a_username); void progress(int); void message(const QString& a_message); public slots: void afterConnect(const QString& a_host, const QString& a_port, const QString& a_database, const QString& a_username, const QString& a_password); private: enum { DM_VERSION_FIELDS_COUNT = 5 }; + enum MessageId + { + CREATE_DB, DROP_DB, CREATE_DB_VERSION, UPDATE_DB_VERSION, + CREATE_VERSION_TABLE, ERROR_VERSION_TABLE + }; + + class StatusResult + { + public: + StatusResult(bool a_ok = false, int a_databaseRevision = 0) + : _ok(a_ok), _databaseRevision(a_databaseRevision) {} + bool succeeded() const { return _ok; } + bool failed() const { return !_ok; } + int databaseRevision() const { return _databaseRevision; } + private: + bool _ok; + int _databaseRevision; + }; + ProgramSettings* _pset; QString _logText; - QList _packages; + QList _packages; QList _createDmVersionScript; QList _createHelperFunctionsScript; QList _dropHelperFunctionsScript; - QList _setSearchPathScript; + QList _resetSearchPathScript; int _revisionBefore; int _revisionAfter; + bool _processingUri; QStringList databaseTableList(SqlProcessor& a_proc); + StatusResult databaseStatus(SqlProcessor& a_proc, const QString& a_packageId, const QStringList& a_template1Tables); bool checkArguments(); bool readConfig(); bool readPackage(QXmlStreamReader& a_xml); bool loadScriptsFromResource(); bool runScripts(); + bool runScript(SqlProcessor& a_proc, const QList& a_script, + const DatabaseScript& a_databaseScript, const QString& a_packageId); bool databaseExists(SqlProcessor& a_proc, const QString& a_dbname); - bool createDatabase(SqlProcessor& a_proc, ProgramSettings* a_pset, - const QString& a_dbname = QString()); - bool dropDatabase(SqlProcessor& a_proc, ProgramSettings* a_pset, - const QString& a_dbname = QString()); + bool createDatabase(SqlProcessor& a_proc, ProgramSettings* a_pset, const QString& a_dbname); bool createLanguagePlpgsql(SqlProcessor& a_proc, SqlProcessor& a_uriProc, bool a_uri); - void prepareScripts(const QString& a_filename, QList& oa_mainScript, - QList& oa_globalsScript); + QList prepareScript(const QString& a_filename); void clearMaclabels(SqlProcessor& a_proc); + QString messageString( MessageId a_message ); +}; + +class UriGuard +{ +public: + UriGuard(DatabaseUpdater* a_updater) + : _updater(a_updater) { _updater->_processingUri = true; } + ~UriGuard() { _updater->_processingUri = false; } +private: + DatabaseUpdater* _updater; }; //!главный класс приложения #endif //UPDATER_H