diff --git a/src/mainconsole.cpp b/src/mainconsole.cpp --- a/src/mainconsole.cpp +++ b/src/mainconsole.cpp @@ -1,220 +1,229 @@ #include "exitcode.h" #include "mainconsole.h" #include "psettings.h" #include "updater.h" #include #include #include MainConsole::MainConsole(QObject* a_parent) : QObject(a_parent) , _exitCode(0) , _errorCount(0) , _os(stdout) , _updater(new DatabaseUpdater) { } MainConsole::~MainConsole() { delete _updater; } void MainConsole::run() { ProgramSettings pset = processArguments(); if (!pset.helpText.isEmpty()) { _os << pset.helpText; QTimer::singleShot(0, this, &MainConsole::quit); return; } connect(_updater, SIGNAL(progress(int)), this, SLOT(progress(int))); connect(_updater, SIGNAL(error(const QString&)), SLOT(error(const QString&))); connect(_updater, SIGNAL(message(const QString&)), SLOT(message(const QString&))); connect(_updater, SIGNAL(sqlError(const QString&, const QString&, const QString&)), SLOT(sqlError(const QString&, const QString&, const QString&))); connect(_updater, SIGNAL(logConnectionParameters(const QString&, const QString&, const QString&)), SLOT(logConnectionParameters(const QString&, const QString&, const QString&))); connect(&_futureWatcher, SIGNAL(finished()), SLOT(finishExecution())); QFuture rc = QtConcurrent::run(_updater, &DatabaseUpdater::run, pset); _futureWatcher.setFuture(rc); } void MainConsole::quit() { emit finished(); } void MainConsole::finishExecution() { //проверяем число ошибок, т.к. при выполнении без транзакции run() всегда возвращает 0 if (_errorCount == 0) { ExitCode rc(_updater->revisionBefore(), _updater->revisionAfter()); message(rc.message()); _exitCode = 0; } else { message(QStringLiteral("Выполнение прервано в результате ошибки.\nБаза данных не изменилась.")); _exitCode = 1; } emit finished(); } ProgramSettings MainConsole::processArguments() { ProgramSettings result; QString USERNAME_VALUE = QStringLiteral("пользователь"); QString PASSWORD_VALUE = QStringLiteral("пароль"); QString DATABASE_VALUE = QStringLiteral("база_данных"); QString HOST_VALUE = QStringLiteral("адрес_сервера"); QString PORT_VALUE = QStringLiteral("номер_порта"); QString FILE_VALUE = QStringLiteral("file.dmv"); + QString LOGFILE_VALUE = QStringLiteral("файл_журнала"); QString USERNAME_OPTION = QStringLiteral("U"); QString PASSWORD_OPTION = QStringLiteral("W"); QString DATABASE_OPTION = QStringLiteral("d"); QString DROPDB_OPTION = QStringLiteral("0"); QString HOST_OPTION = QStringLiteral("h"); QString PORT_OPTION = QStringLiteral("p"); QString FILE_OPTION = QStringLiteral("f"); + QString LOGFILE_OPTION = QStringLiteral("L"); + QString URIDB_OPTION = QStringLiteral("u"); QString HELP_OPTION = QStringLiteral("help"); //обработать аргументы командной строки QCommandLineParser parser; QCommandLineOption helpOption(HELP_OPTION, QStringLiteral("Показать справку") ); parser.addOption(helpOption); QCommandLineOption usernameOption(USERNAME_OPTION, QStringLiteral("Имя пользователя"), USERNAME_VALUE); parser.addOption(usernameOption); QCommandLineOption databaseOption(DATABASE_OPTION, QStringLiteral("Имя базы данных"), DATABASE_VALUE); parser.addOption(databaseOption); QCommandLineOption filenameOption(FILE_OPTION, QStringLiteral("Управляющий файл"), FILE_VALUE); parser.addOption(filenameOption); QCommandLineOption hostOption(HOST_OPTION, QStringLiteral("Адрес сервера"), HOST_VALUE); parser.addOption(hostOption); QCommandLineOption portOption(PORT_OPTION, QStringLiteral("Порт сервера"), PORT_VALUE); parser.addOption(portOption); QCommandLineOption passwordOption(PASSWORD_OPTION, QStringLiteral("Пароль"), PASSWORD_VALUE); parser.addOption(passwordOption); - QCommandLineOption dropdbOption(DROPDB_OPTION, QStringLiteral("Сначала удалить базу данных") ); + QCommandLineOption logfileOption(LOGFILE_OPTION, QStringLiteral("Сохранить протокол работы в файл"), LOGFILE_VALUE); + parser.addOption(logfileOption); + QCommandLineOption dropdbOption(DROPDB_OPTION, QStringLiteral("Сначала удалить базу данных")); parser.addOption(dropdbOption); + QCommandLineOption uridbOption(URIDB_OPTION, QStringLiteral("Не создавать учебную базу данных")); + parser.addOption(uridbOption); parser.addPositionalArgument(FILE_VALUE, QStringLiteral("Управляющий файл"), FILE_VALUE); parser.process(*qApp); //записать прочитанные значения в структуру ProgramSettings result.username = parser.value(usernameOption); result.password = parser.value(passwordOption); result.database = parser.value(databaseOption); result.host = parser.value(hostOption); result.port = parser.value(portOption); + result.logfile = parser.value(logfileOption); result.dropdb = parser.isSet(dropdbOption); + result.uridb = !parser.isSet(uridbOption); if (parser.isSet(filenameOption)) result.controlFile = parser.value(filenameOption); else if (parser.positionalArguments().size() > 0) result.controlFile = parser.positionalArguments().at(0); if (parser.isSet(helpOption)) result.helpText = parser.helpText(); return result; } void MainConsole::error(const QString& a_error) { static const char* CONSOLE_BRIGHT_RED_BEGIN = "\x1B[1m\x1B[31m"; static const char* CONSOLE_RESET = "\x1B(B\x1B[m"; hideProgress(); _os << CONSOLE_BRIGHT_RED_BEGIN << QStringLiteral("ОШИБКА") << CONSOLE_RESET << endl; message(a_error); _os << CONSOLE_RESET << endl; ++_errorCount; } void MainConsole::message(const QString& a_message) { hideProgress(); _os << toConsoleText(a_message) << endl; } void MainConsole::progress(int a_value) { static const char* CONSOLE_GREEN_BEGIN = "\x1B[0m\x1B[32m"; static const char* CONSOLE_RESET = "\x1B(B\x1B[m"; char bytes[50 + 2 + 1]; auto begin = std::begin(bytes); auto end = std::end(bytes) - 1; *begin++ = '['; *end-- = '\0'; *end = ']'; std::fill(begin, end, '.'); int done = a_value / 2; std::fill(begin, begin + done, 'O'); if (a_value & 1) bytes[done + 1] = 'o'; _os << '\r' << CONSOLE_GREEN_BEGIN << bytes << CONSOLE_RESET << flush; } void MainConsole::hideProgress() { static const char* CONSOLE_ERASE_LINE = "\r\x1B[K"; _os << CONSOLE_ERASE_LINE << flush; } QString MainConsole::toConsoleText(const QString& a_text) { QString result = a_text; static const QString BOLD = QStringLiteral("**"); static const QString CONSOLE_BOLD_BEGIN = QStringLiteral("\x1B[1m"); static const QString CONSOLE_RESET = QStringLiteral("\x1B(B\x1B[m"); static const QString CONSOLE_TAB = QStringLiteral(" "); static const QChar TAB('\t'); bool begin = true; int pos = result.indexOf(BOLD); while (pos >= 0) { result.replace(pos, BOLD.size(), begin ? CONSOLE_BOLD_BEGIN : CONSOLE_RESET); begin = !begin; pos = result.indexOf(BOLD, pos + 1); } if (result.contains(TAB)) result.replace(TAB, CONSOLE_TAB); if (result.endsWith(QChar('\n'))) result.chop(1); return result; } void MainConsole::logConnectionParameters(const QString& a_host, const QString& a_database, const QString& a_username) { message(QStringLiteral("Подключение к базе данных\n" "\tСервер: **%1**\n\tБаза данных: **%2**\n" "\tПользователь: **%3**") .arg(a_host, a_database, a_username)); } void MainConsole::sqlError(const QString& a_dbError, const QString& a_commandDescription, const QString& a_command) { QString errorText; if (!a_commandDescription.isEmpty()) errorText.append(QStringLiteral("**Операция:**\n")).append(a_commandDescription).append("\n\n"); QString dbError = a_dbError; errorText.append(QStringLiteral("**Сообщение об ошибке:**\n")).append(dbError.trimmed()).append("\n\n"); if (!a_command.isEmpty()) errorText.append(QStringLiteral("**Текст команды:**\n")).append(a_command); error(errorText); } diff --git a/src/psettings.h b/src/psettings.h --- a/src/psettings.h +++ b/src/psettings.h @@ -1,22 +1,24 @@ #if !defined PSETTINGS_H #define PSETTINGS_H #include class ProgramSettings { public: - ProgramSettings() : dbVersion(0), dropdb(false) {} + ProgramSettings() : dbVersion(0), dropdb(false), uridb(true) {} QString username; QString password; QString database; QString host; QString port; QString controlFile; + QString logfile; QString packageId; QString helpText; int dbVersion; bool dropdb; + bool uridb; }; #endif diff --git a/src/updater.cpp b/src/updater.cpp --- a/src/updater.cpp +++ b/src/updater.cpp @@ -1,851 +1,855 @@ #include "updater.h" #include "psettings.h" #include #include #include #include #include #include #include #include DatabaseUpdater::DatabaseUpdater() : QObject() , _pset(nullptr) , _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 = QDir::current().absoluteFilePath(_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(); //найти файл с расширением pki (pkginfo) и именем как у входного файла, если такого нет, //то использовать первый попавшийся файл с расширением pki (pkginfo); //прочитать из него идентификатор пакета и имя базы данных; //если файла нет, то и не надо const QString YAML_PACKAGE_INFO_EXT = QStringLiteral(".pki"); const QString PACKAGE_INFO_EXT = QStringLiteral(".pkginfo"); const QStringList PACKAGE_FILE_FILTER = {QStringLiteral("*.pki"), QStringLiteral("*.pkginfo")}; QFileInfo fi(controlFile); QStringList files = QDir(QFileInfo(controlFile).path(), QString(), QDir::NoSort, QDir::Files).entryList(PACKAGE_FILE_FILTER); if (!files.isEmpty()) { QString completeBaseName = QFileInfo(controlFile).completeBaseName(); QString pkgfile = completeBaseName + YAML_PACKAGE_INFO_EXT; if (!files.contains(pkgfile, Qt::CaseInsensitive)) { pkgfile = completeBaseName + PACKAGE_INFO_EXT; if (!files.contains(pkgfile, Qt::CaseInsensitive)) pkgfile = files.at(0); } pkgfile = QFileInfo(controlFile).path() + '/' + pkgfile; emit message(QStringLiteral("Загрузка конфигурации из файла\n%1").arg(QDir::toNativeSeparators(pkgfile))); if (pkgfile.endsWith(YAML_PACKAGE_INFO_EXT)) { QFile pkginfo(pkgfile); if (pkginfo.open(QIODevice::ReadOnly)) { QString content = QString::fromUtf8(pkginfo.readAll()); QRegularExpression rxDbname(QStringLiteral(R"(^\s*dbname:\s+['"]?([^\s'"]+))"), QRegularExpression::MultilineOption); QRegularExpressionMatch match = rxDbname.match(content); if (match.hasMatch()) { QString s = match.captured(1); if (!s.isEmpty() && _pset->database.isEmpty()) _pset->database = s; } QRegularExpression rxVersion(QStringLiteral(R"(^\s*version:\s+['"]?([^\s'"]+))"), QRegularExpression::MultilineOption); match = rxVersion.match(content); if (match.hasMatch()) { QStringList ver = match.captured(1).split(QChar('.')); if (ver.size() > 1) { QString s = ver.at(1); bool ok; int i = s.toInt(&ok); if (ok) _pset->dbVersion = i; } } QRegularExpression rxPackageId(QStringLiteral(R"(^\s*id:\s+['"]?([^\s'"]+))"), QRegularExpression::MultilineOption); match = rxPackageId.match(content); if (match.hasMatch()) _pset->packageId = match.captured(1); } } else if (pkgfile.endsWith(PACKAGE_INFO_EXT)) { 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 == "нет")); + //параметр командной строки отключает создание учебной БД и он приоритетнее, чем файл конфигурации + if (!_pset->uridb) + uri = false; + Package package(packageId, uri); //собираем информацию о скриптах данного пакета const QString SCRIPT_NODE("script"); const QString REVISION_ATTR("revision"); 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(); package.addScript(DatabaseScript(file, revision, comment)); } } //упорядочить скрипты по возрастанию номеров ревизий package.sortScripts(); _packages.append(package); return true; } //--------------------------------------------------------------------------- bool DatabaseUpdater::runScripts() { //определение пакета, скрипты которого будут выполняться auto pp = std::find_if(_packages.cbegin(), _packages.cend(), [this](const Package& p){return p.id() == _pset->packageId;}); if (pp == _packages.cend()) { emit error(QStringLiteral("Для пакета «%1» не задано ни одного " "сценария создания базы данных") .arg(_pset->packageId)); return false; } //проверка наличия файлов со скриптами на диске const Package& package = *pp; QStringList paths; QFileInfo fi(_pset->controlFile); paths << fi.canonicalPath() << fi.canonicalPath() + "/script" << QDir::currentPath(); const auto& packageScripts = package.scripts(); for (auto it = packageScripts.begin(); it != packageScripts.end(); ++it) { const DatabaseScript& script = *it; if (!script.findScript(paths)) { emit error(QStringLiteral("Не найден файл сценария создания " "базы данных %1") .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, "template1", _pset->username, _pset->password, _pset->port)) return false; QStringList template1Tables = databaseTableList(proc); //создание БД if (!createDatabase(proc, _pset, _pset->database)) return false; //создание учебной БД QString uriDatabase = _pset->database + "_u"; if (package.wantUri()) { 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))); //подключение к базам данных if (!proc.connectdb(_pset->host, _pset->database, _pset->username, _pset->password, _pset->port)) return false; if (package.wantUri()) { if (!uriProc.connectdb(_pset->host, uriDatabase, _pset->username, _pset->password, _pset->port)) return false; } StatusResult statusResult = databaseStatus(proc, package.id(), template1Tables); if (statusResult.failed()) return false; int databaseRevision = statusResult.databaseRevision(); int uriDatabaseRevision = 0; if (package.wantUri()) { UriGuard ug(this); statusResult = databaseStatus(uriProc, package.id(), template1Tables); if (statusResult.failed()) return false; uriDatabaseRevision = statusResult.databaseRevision(); } //создание языка plpgsql if (!createLanguagePlpgsql(proc, uriProc, package.wantUri())) return false; _revisionAfter = _revisionBefore = databaseRevision; //выполнение необходимых скриптов connect(&proc, SIGNAL(progress(int)), this, SIGNAL(progress(int))); connect(&uriProc, SIGNAL(progress(int)), this, SIGNAL(progress(int))); const auto& end = packageScripts.cend(); for (auto it = packageScripts.cbegin(); it != end; ++it) { const DatabaseScript& script = *it; int revision = script.revision(); //подготовка скрипта к выполнению QList preparedScript = prepareScript(script.script()); //выполнение скриптов в БД, пропуская уже установленные ревизии if (revision > databaseRevision) { if ( !runScript(proc, preparedScript, script, package.id()) ) return false; //сброс мандатных меток, если они есть в БД clearMaclabels(proc); } //выполнение скриптов в учебной БД, если задано её создание if (package.wantUri() && revision > uriDatabaseRevision) { UriGuard ug(this); if ( !runScript(uriProc, preparedScript, script, package.id()) ) return false; clearMaclabels(uriProc); } _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(); if (revision == 1) emit message(messageString(CREATE_DB_VERSION).arg(revision)); else emit message(messageString(UPDATE_DB_VERSION).arg(revision)); //выполнение скрипта 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; 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_proc.database() == a_dbname) a_proc.disconnectdb(); //если не подключены, то подключение к template1 if (a_proc.database().isNull()) { if (!a_proc.connectdb(a_pset->host, "template1", a_pset->username, a_pset->password, a_pset->port)) return false; } //удаление БД если задано параметром при запуске программы bool dbExists = databaseExists(a_proc, a_dbname); if (dbExists && _pset->dropdb) { emit message(messageString(DROP_DB).arg(a_dbname)); bool dropped = a_proc.execSQL(QString("DROP DATABASE \"%1\"").arg(a_dbname)) && a_proc.commandStatus() == QString("DROP DATABASE"); if (!dropped) return false; dbExists = false; } //создание БД если она не существует if (!dbExists) { emit message(messageString(CREATE_DB).arg(a_dbname)); bool created = a_proc.execSQL(QString("CREATE DATABASE \"%1\"").arg(a_dbname)) && a_proc.commandStatus() == QString("CREATE DATABASE"); if (!created) return false; } 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, static_cast(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, static_cast(qstrlen(pgvalue))).toInt() > 0) { pgvalue = PQgetvalue(pgresult, 0, 0); currentDatabaseRevision = QByteArray::fromRawData(pgvalue, static_cast(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; } _resetSearchPathScript = proc.parse(resFile); return true; } //--------------------------------------------------------------------------- // Подготавливает скрипт, считанный из файла, к выполнению в БД. // Подготовка заключается в следующем: // 1. Команды создания глобальных объектов // CREATE GROUP // CREATE USER // CREATE ROLE // CREATE TABLESPACE // заменяются вызовами функций-обёрток для их безопасного выполнения. // В скрипт добавляются команды создания функций-обёрток и их удаления. // 2. Команда подключения языка plpgsql // CREATE [ TRUSTED ] [ PROCEDURAL ] LANGUAGE plpgsql // удаляется из скрипта. // Создание языка plpgsql производится отдельно, перед началом выполнения скриптов. //--------------------------------------------------------------------------- QList DatabaseUpdater::prepareScript(const QString& a_filename) { //пропустим файл через парсер, на выходе список команд SqlProcessor proc; QList result = proc.parse(a_filename); //обход списка и анализ команд bool helperRequired = false; auto end = result.end(); for (auto it = result.begin(); it != end; ++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(); //переход на начало второго слова 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 == "create") { if (command2 == "group" || command2 == "user" || command2 == "role" || command2 == "tablespace") { //добавляем в выходной скрипт вызов безопасной функции-обёртки 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 public._rct_safe_create_"); safeCall.append(command2).append("('").append(name).append("','").append(parameters).append("');"); //заменяем строку в выходном массиве line = safeCall; } else if (command2 == "trusted" || command2 == "procedural" || command2 == "language") { //create language не нужен, заменяем пустой командой line = ";"; } } } //цикл по командам 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); QString host = a_host; if (host.isEmpty()) host = qEnvironmentVariable("PGHOST", QStringLiteral("Локальный")); QString username = a_username; if (username.isEmpty()) username = qEnvironmentVariable("PGUSER", qEnvironmentVariable("USER", qEnvironmentVariable("USERNAME"))); QString database = a_database; if (database.isEmpty()) database = qEnvironmentVariable("PGDATABASE", username); emit logConnectionParameters(host, database, 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) const { _scriptFile.replace(QChar('\\'), QChar('/')); for (const QString& path : a_paths) { QDir dir(path); if (dir.exists(_scriptFile)) { _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(); }); }