Page MenuHomePhabricator

No OneTemporary

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 <QCommandLineParser>
#include <QTimer>
#include <QtConcurrentRun>
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<int> 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 <QString>
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 <QDate>
#include <QDir>
#include <QRegularExpression>
#include <QTextCodec>
#include <QXmlStreamReader>
#include <ksettings.h>
#include <libpq-fe.h>
#include <sqlproc.h>
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<QByteArray> 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<QByteArray>& 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<int>(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<int>(qstrlen(pgvalue))).toInt() > 0)
{
pgvalue = PQgetvalue(pgresult, 0, 0);
currentDatabaseRevision = QByteArray::fromRawData(pgvalue, static_cast<int>(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<QByteArray> DatabaseUpdater::prepareScript(const QString& a_filename)
{
//пропустим файл через парсер, на выходе список команд
SqlProcessor proc;
QList<QByteArray> 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<MessageId, QString> 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(); });
}

File Metadata

Mime Type
text/x-diff
Expires
Wed, Jun 11, 8:15 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
127141

Event Timeline