Page MenuHomePhabricator

No OneTemporary

diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1,444 +1,474 @@
#include "mainwindow.h"
#include "psettings.h"
#include "sqlerrordlg.h"
#include "updater.h"
#include <kactivityindicator.h>
#include <ksettings.h>
#include <sqlproc.h>
#ifdef Q_OS_WIN32
#include <klogon_win.h>
#endif
#include <QApplication>
#include <QCloseEvent>
#include <QEvent>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QLabel>
#include <QProgressBar>
#include <QPushButton>
#include <QShortcut>
#include <QStyle>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QtConcurrentRun>
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
MainWindow::MainWindow(QWidget* parent, Qt::WindowFlags flags)
: QDialog(parent, flags)
, _exitCode(0)
, _actionEventType(QEvent::User)
, _errorCount(0)
, _updater(new DatabaseUpdater)
{
setWindowTitle(QStringLiteral("Создание/обновление базы данных"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setSizeGripEnabled(true);
QHBoxLayout* mainLayout = new QHBoxLayout;
QVBoxLayout* leftLayout = new QVBoxLayout;
QVBoxLayout* rightLayout = new QVBoxLayout;
QHBoxLayout* bottomRightLayout = new QHBoxLayout;
leftLayout->setContentsMargins(0,
2 * style()->pixelMetric(QStyle::PM_LayoutTopMargin),
style()->pixelMetric(QStyle::PM_LayoutRightMargin), 0);
QLabel* picture = new QLabel;
picture->setPixmap(QPixmap(":/picture.png"));
//кнопки, скрытые до завершения процесса
_btnSave = new QPushButton(QStringLiteral("Сохранить"));
_btnClose = new QPushButton(QStringLiteral("Закрыть"));
_btnSave->setVisible(false);
_btnClose->setVisible(false);
_btnClose->setDefault(true);
leftLayout->addWidget(picture);
leftLayout->addStretch();
leftLayout->addWidget(_btnSave);
leftLayout->addWidget(_btnClose);
QLabel* label = new QLabel(QStringLiteral("Протокол работы"));
rightLayout->addWidget(label);
_protocol = new QTextEdit();
_protocol->setReadOnly(true);
label->setBuddy(_protocol);
rightLayout->addWidget(_protocol);
//индикаторы выполнения
_progress = new QProgressBar;
_progress->setTextVisible(false);
_ai = new KActivityIndicator(KActivityIndicator::Gray);
_ai->setVisible(false);
bottomRightLayout->addWidget(_progress);
bottomRightLayout->addWidget(_ai);
rightLayout->addLayout(bottomRightLayout);
mainLayout->addLayout(leftLayout);
mainLayout->addLayout(rightLayout);
setLayout(mainLayout);
//нажатие Esc не соединяется со слотом, чтобы окно не закрывалось
new QShortcut(QKeySequence(Qt::Key_Escape), this);
//соединить сигналы со слотами
connect(_btnSave, SIGNAL(clicked()), this, SLOT(saveProtocol()));
connect(_btnClose, SIGNAL(clicked()), this, SLOT(closeDialog()));
//получить незадействованный тип события
_actionEventType = QEvent::registerEventType();
}
MainWindow::~MainWindow()
{
delete _updater;
}
//---------------------------------------------------------------------------
void MainWindow::showEvent(QShowEvent* a_event)
{
QDialog::showEvent(a_event);
qApp->postEvent(this, new QEvent(QEvent::Type(_actionEventType)));
}
//---------------------------------------------------------------------------
bool MainWindow::event(QEvent* a_event)
{
if (a_event->type() == _actionEventType)
{
startExecution();
a_event->accept();
return true;
}
return QWidget::event(a_event);
}
//---------------------------------------------------------------------------
void MainWindow::closeEvent(QCloseEvent* a_event)
{
if (_futureWatcher.isRunning())
a_event->ignore();
else
a_event->accept();
}
//---------------------------------------------------------------------------
void MainWindow::startExecution()
{
if (_futureWatcher.isRunning())
return;
ProgramSettings pset;
processArguments(pset);
logon(pset);
connect(_updater, SIGNAL(progress(int)), _progress, SLOT(setValue(int)));
connect(_updater, SIGNAL(error(const QString&)), this, SLOT(error(const QString&)));
connect(_updater, SIGNAL(message(const QString&)), this, SLOT(message(const QString&)));
connect(_updater, SIGNAL(sqlError(const QString&, const QString&, const QString&)),
this, SLOT(sqlError(const QString&, const QString&, const QString&)));
connect(_updater, SIGNAL(logConnectionParameters(const QString&, const QString&, const QString&)),
this, SLOT(logConnectionParameters(const QString&, const QString&, const QString&)));
connect(&_futureWatcher, SIGNAL(finished()), this, SLOT(finishExecution()));
_ai->setVisible(true);
_ai->start();
//int rc = _updater.run( pset );
QFuture<int> rc = QtConcurrent::run(_updater, &DatabaseUpdater::run, pset);
_futureWatcher.setFuture(rc);
}
//---------------------------------------------------------------------------
void MainWindow::finishExecution()
{
_ai->stop();
_ai->setVisible(false);
//проверяем число ошибок, т.к. при выполнении без транзакции run() всегда возвращает 0
if (_errorCount == 0)
{
int before = _updater->revisionBefore();
int after = _updater->revisionAfter();
if (before == after)
message(QStringLiteral("База данных в актуальном состоянии."));
else if (before == 0)
message(QStringLiteral("База данных создана и обновлена до версии %1.").arg(after));
else
message(QStringLiteral("База данных версии %1 обновлена до версии %2.").arg(before).arg(after));
_exitCode = (before & 0xFFFF) | ((after & 0xFFFF) << 16);
closeDialog(); //при успешном завершении, то окно закрывается автоматически
}
else
{
_btnClose->show();
_btnSave->show();
_btnClose->setFocus();
message(QStringLiteral("Выполнение прервано в результате ошибки."
"<br>База данных не изменилась."));
_exitCode = -1;
}
}
//---------------------------------------------------------------------------
int MainWindow::executeAction()
{
ProgramSettings pset;
processArguments(pset);
logon(pset);
_ai->start();
DatabaseUpdater updater;
connect(&updater, SIGNAL(progress(int)), _progress, SLOT(setValue(int)));
connect(&updater, SIGNAL(error(const QString&)), this, SLOT(error(const QString&)));
connect(&updater, SIGNAL(message(const QString&)), this, SLOT(message(const QString&)));
connect(&updater, SIGNAL(sqlError(const QString&, const QString&, const QString&)),
this, SLOT(sqlError(const QString&, const QString&, const QString&)));
connect(&updater, SIGNAL(logConnectionParameters(const QString&, const QString&, const QString&)),
this, SLOT(logConnectionParameters(const QString&, const QString&, const QString&)));
int rc = updater.run(pset);
//проверяем число ошибок, т.к. при выполнении без транзакции run() всегда возвращает 0
if (_errorCount == 0)
{
int before = updater.revisionBefore();
int after = updater.revisionAfter();
if (before == after)
message(QStringLiteral("База данных в актуальном состоянии."));
else if (before == 0)
message(QStringLiteral("База данных создана и обновлена до версии %1.").arg(after));
else
message(QStringLiteral("База данных версии %1 обновлена до версии %2.").arg(before).arg(after));
rc = (before & 0xFFFF) | ((after & 0xFFFF) << 16);
}
else
{
_btnClose->show();
_btnSave->show();
_btnClose->setFocus();
message(QStringLiteral("Выполнение прервано в результате ошибки."
"<br>База данных не изменилась."));
rc = -1;
}
return rc;
}
void MainWindow::processArguments(ProgramSettings& a_pset)
{
QString USERNAME_PARAM("username");
QString PASSWORD_PARAM("password");
QString DATABASE_PARAM("database");
QString DROPDB_PARAM("dropdb");
QString HOST_PARAM("host");
QString PORT_PARAM("port");
QString FILE_PARAM("file");
//заполнить параметры default значениями
QHash<QString, QString> params;
params.insert(USERNAME_PARAM, QString());
params.insert(PASSWORD_PARAM, QString());
params.insert(DATABASE_PARAM, QString());
params.insert(DROPDB_PARAM, QString());
params.insert(HOST_PARAM, QString());
params.insert(PORT_PARAM, QString());
params.insert(FILE_PARAM, QString());
//чтение аргументов
QString key;
QStringList arguments = QApplication::arguments();
auto end = arguments.cend();
auto begin = arguments.cbegin();
for (auto it = begin + 1; it != end; ++it) //пропускаем первый параметр - имя программы
{
QString curArg = *it;
if (curArg.at(0) == QChar('-'))
{ //обработка короткого параметра
key.clear();
switch (curArg.at(1).toLatin1())
{
case 'U':
key = USERNAME_PARAM;
break;
case 'd':
key = DATABASE_PARAM;
break;
case 'f':
key = FILE_PARAM;
break;
case 'h':
key = HOST_PARAM;
break;
case 'p':
key = PORT_PARAM;
break;
case 'W':
key = PASSWORD_PARAM;
break;
case '0':
params[ DROPDB_PARAM ] = '1';
break;
}
}
else
{ //обработка одиночного параметра
if (key.isEmpty())
key = FILE_PARAM;
params[ key ] = curArg;
key.clear();
}
}
//записать прочитанные значения в структуру ProgramSettings
a_pset.username = params.value(USERNAME_PARAM);
a_pset.password = params.value(PASSWORD_PARAM);
a_pset.database = params.value(DATABASE_PARAM);
a_pset.host = params.value(HOST_PARAM);
a_pset.port = params.value(PORT_PARAM);
a_pset.controlFile = params.value(FILE_PARAM);
a_pset.dropdb = !params.value(DROPDB_PARAM).isEmpty();
}
//---------------------------------------------------------------------------
void MainWindow::logon(ProgramSettings& a_pset)
{
if (!a_pset.host.isEmpty() || !a_pset.username.isEmpty())
return;
#if defined(Q_OS_WIN)
KLogon* logon = KLogon::create();
if (!logon)
return;
QString defaultDSN;
HKEY k;
DWORD cb = 0;
QString valuename("System Directory");
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\irs\\b03"), 0, KEY_QUERY_VALUE, &k) == ERROR_SUCCESS
&& RegQueryValueEx(k, LPCTSTR(valuename.constData()), 0, 0, 0, &cb) == ERROR_SUCCESS
&& cb > 0)
{
QString systemdir;
systemdir.resize((cb / sizeof(QChar)) - 1);
RegQueryValueEx(k, LPCTSTR(valuename.constData()), 0, 0, LPBYTE(systemdir.data()), &cb);
RegCloseKey(k);
KSettings systemini(systemdir + "/system.ini");
systemini.beginGroup("Database");
defaultDSN = systemini.value("DefaultDSN").toString();
}
QString username = QString::fromLocal8Bit(qgetenv("USERNAME"));
logon->setParent(winId());
logon->setUsername(username);
if (!defaultDSN.isNull())
logon->setDSN(defaultDSN);
if (!logon->execute())
emit message(QStringLiteral("Отказ от ввода имени и пароля пользователя"));
else
{
a_pset.host = logon->host();
a_pset.username = logon->username();
a_pset.password = logon->password();
}
delete logon;
#else
QString username = QString::fromLocal8Bit(getenv("USER"));
#endif
}
//---------------------------------------------------------------------------
void MainWindow::logConnectionParameters(const QString& a_host,
const QString& a_database,
const QString& a_username)
{
- message(QStringLiteral("Подключение к базе данных<br>"
- "&nbsp;&nbsp;Сервер: <b>%1</b><br>&nbsp;&nbsp;База данных: <b>%2</b><br>"
- "&nbsp;&nbsp;Пользователь: <b>%3</b>")
+ message(QStringLiteral("Подключение к базе данных\n"
+ "\tСервер: **%1**\n\tБаза данных: **%2**\n"
+ "\tПользователь: **%3**")
.arg(a_host, a_database, a_username));
}
//---------------------------------------------------------------------------
void MainWindow::sqlError(const QString& a_dbError,
const QString& a_commandDescription,
const QString& a_command)
{
QString errorHtml;
if (!a_commandDescription.isEmpty())
errorHtml.append(QStringLiteral("<font color=\"#0000FF\">Операция:</font><br>")).append(a_commandDescription).append("<br>");
QString dbError = a_dbError;
errorHtml.append(QStringLiteral("<font color=\"#0000FF\">Сообщение об ошибке:</font><pre>")).append(dbError.trimmed().replace('\n', "<br>")).append("</pre>");
if (!a_command.isEmpty())
errorHtml.append(QStringLiteral("<font color=\"#0000FF\">Текст команды:</font><pre>")).append(a_command).append("</pre>");
errorHtmlToLog(errorHtml);
}
//---------------------------------------------------------------------------
void MainWindow::error(const QString& a_error)
{
QString errorHtml = a_error;
errorHtmlToLog(errorHtml.replace('<', "&lt;").replace('>', "&gt;").replace('\n', "<br>"));
}
//---------------------------------------------------------------------------
void MainWindow::errorHtmlToLog(const QString& a_errorHtml)
{
QString errorHtml = a_errorHtml;
QTextCursor cursor = _protocol->textCursor();
cursor.insertHtml(QStringLiteral("<font color=\"#0000FF\">Ошибка</font><br>"));
cursor.insertHtml(errorHtml);
cursor.insertHtml(QStringLiteral("<hr>"));
_protocol->moveCursor(QTextCursor::End);
++_errorCount;
}
+QString MainWindow::toHtml(const QString& a_text)
+{
+ QString result = a_text;
+
+ static const QString BOLD = QStringLiteral("**");
+ static const QString HTML_BOLD_BEGIN = QStringLiteral("<b>");
+ static const QString HTML_BOLD_END = QStringLiteral("</b>");
+ static const QString HTML_BR = QStringLiteral("<br>");
+ static const QString HTML_NBSP = QStringLiteral("&nbsp;&nbsp;");
+ static const QChar NL('\n');
+ static const QChar TAB('\t');
+
+ bool begin = true;
+ int pos = result.indexOf(BOLD);
+ while (pos >= 0)
+ {
+ result.replace(pos, BOLD.size(), begin ? HTML_BOLD_BEGIN : HTML_BOLD_END);
+ begin = !begin;
+ pos = result.indexOf(BOLD, pos + 1);
+ }
+
+ if (result.contains(NL))
+ result.replace(NL, HTML_BR);
+
+ if (result.contains(TAB))
+ result.replace(TAB, HTML_NBSP);
+
+ return result;
+}
+
//---------------------------------------------------------------------------
void MainWindow::message(const QString& a_message)
{
QString message = a_message;
- _protocol->textCursor().insertHtml(message.replace(QChar('\n'), "<br>").append("<br>"));
+ _protocol->textCursor().insertHtml(toHtml(message).append(QStringLiteral("<br>")));
_protocol->moveCursor(QTextCursor::End);
}
//---------------------------------------------------------------------------
void MainWindow::closeDialog()
{
QDialog::done(_exitCode);
}
//---------------------------------------------------------------------------
void MainWindow::saveProtocol()
{
QString filename = QFileDialog::getSaveFileName(this,
QStringLiteral("Сохранить протокол в файл…"), QString(),
QStringLiteral("Текстовые файлы (*.txt);;Все файлы (*.*)"));
if (!filename.isEmpty())
{
QFile outf(filename);
if (outf.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
QTextStream os(&outf);
os.setGenerateByteOrderMark(true);
os.setCodec("UTF-8");
os << _protocol->toPlainText();
}
}
}
//!главное окно приложения
diff --git a/src/mainwindow.h b/src/mainwindow.h
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -1,65 +1,66 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QDialog>
#include <QFutureWatcher>
#include <QString>
class QLabel;
class QListWidget;
class QPushButton;
class QProgressBar;
class QTextEdit;
class ProgramSettings;
class SqlErrorDialog;
class SqlProcessor;
class DatabaseUpdater;
class ProgramSettings;
class KActivityIndicator;
class MainWindow : public QDialog
{
Q_OBJECT
public:
MainWindow(QWidget* parent = 0, Qt::WindowFlags flags = 0);
~MainWindow() override;
private slots:
void saveProtocol();
void closeDialog();
void startExecution();
void finishExecution();
void error(const QString& a_error);
void message(const QString& a_message);
void logConnectionParameters(const QString& a_host, const QString& a_database,
const QString& a_username);
void sqlError(const QString& a_dbError, const QString& a_commandDescription,
const QString& a_command);
protected:
void showEvent(QShowEvent* a_event) override;
void closeEvent(QCloseEvent* a_event) override;
bool event(QEvent* a_event) override;
private:
int executeAction();
void logon(ProgramSettings& a_pset);
void processArguments(ProgramSettings& a_pset);
void errorHtmlToLog(const QString& a_errorHtml);
+ QString toHtml(const QString& a_text);
int _exitCode;
int _actionEventType;
int _errorCount;
QTextEdit* _protocol;
QLabel* _lblWait;
QPushButton* _btnSave;
QPushButton* _btnClose;
KActivityIndicator* _ai;
QProgressBar* _progress;
QFutureWatcher<int> _futureWatcher;
DatabaseUpdater* _updater;
};
//!главное окно приложения
#endif //MAINWINDOW_H
diff --git a/src/updater.cpp b/src/updater.cpp
--- a/src/updater.cpp
+++ b/src/updater.cpp
@@ -1,836 +1,836 @@
#include "updater.h"
#include "psettings.h"
#include <QDate>
#include <QDir>
#include <QTextCodec>
#include <QXmlStreamReader>
#include <ksettings.h>
#include <libpq-fe.h>
#include <sqlproc.h>
DatabaseUpdater::DatabaseUpdater()
: QObject()
, _pset(0)
, _revisionBefore(0)
, _revisionAfter(0)
{
}
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);
//собираем информацию о скриптах данного пакета
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->sortScripts();
return true;
}
//---------------------------------------------------------------------------
bool DatabaseUpdater::runScripts()
{
//определение пакета, скрипты которого будут выполняться
auto pp = std::find_if(_packages.cbegin(), _packages.cend(),
[this](Package* p){return p->id() == _pset->packageId;});
if (pp == _packages.cend())
{
emit error(QStringLiteral("Для пакета «%1» не задано ни одного "
"сценария создания базы данных")
.arg(_pset->packageId));
return false;
}
//проверка наличия файлов со скриптами на диске
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)
{
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, _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("Удаление базы данных <b>%1</b>").arg(_pset->database));
+ emit message(QStringLiteral("Удаление базы данных **%1**").arg(_pset->database));
if (!dropDatabase(proc, _pset))
return false;
dbExists = false;
}
//создание БД если она не существует
if (!dbExists)
{
- emit message(QStringLiteral("Создание базы данных <b>%1</b>").arg(_pset->database));
+ emit message(QStringLiteral("Создание базы данных **%1**").arg(_pset->database));
if (!createDatabase(proc, _pset))
return false;
}
QString uriDatabase = _pset->database + "_u";
if (package->uri())
{
//удаление учебной БД если задано параметром при запуске программы
bool uriDbExists = databaseExists(proc, uriDatabase);
if (uriDbExists && _pset->dropdb)
{
- emit message(QStringLiteral("Удаление учебной базы данных <b>%1</b>").arg(uriDatabase));
+ emit message(QStringLiteral("Удаление учебной базы данных **%1**").arg(uriDatabase));
if (!dropDatabase(proc, _pset, uriDatabase))
return false;
uriDbExists = false;
}
//создание учебной БД если она не существует
if (!uriDbExists)
{
- emit message(QStringLiteral("Создание учебной базы данных <b>%1</b>").arg(uriDatabase));
+ emit message(QStringLiteral("Создание учебной базы данных **%1**").arg(uriDatabase));
if (!createDatabase(proc, _pset, uriDatabase))
return false;
}
}
connect(&proc, 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())
{
//---- database_u ----
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;
}
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
{
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();
}
}
}
//создание языка plpgsql
if (!createLanguagePlpgsql(proc, uriProc, package->uri()))
return false;
_revisionAfter = _revisionBefore = currentDatabaseRevision;
//выполнение необходимых скриптов
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)
{
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));
//подготовка скрипта к выполнению
QList<QByteArray> mainScript;
QList<QByteArray> globalsScript;
prepareScripts(script->script(), mainScript, globalsScript);
//если в скрипте были команды создания глобальных объектов, то выполнить
//их в отдельной транзакции
if (globalsScript.size() > 0)
{
proc.execSQL("begin");
if (!proc.execute(globalsScript))
return false;
proc.execSQL("commit");
}
//выполнение скрипта
if (transaction)
proc.execSQL("begin");
if (!proc.execute(mainScript))
return false;
//выполнение скриптов в учебной БД, если задано её создание
if (package->uri())
{
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;
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::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)
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;
return a_proc.execSQL(QString("create database \"%1\"").arg(dbname))
&& a_proc.commandStatus() == QString("CREATE DATABASE");
}
//---------------------------------------------------------------------------
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);
//если уже подключены к указанной БД, то отключение
if (a_proc.database() == 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;
return a_proc.execSQL(QString("drop database \"%1\"").arg(dbname))
&& a_proc.commandStatus() == QString("DROP DATABASE");
}
//---------------------------------------------------------------------------
// Получение списка таблиц в БД
//---------------------------------------------------------------------------
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;
}
//---------------------------------------------------------------------------
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);
return true;
}
//---------------------------------------------------------------------------
// Подготавливает скрипт, считанный из файла, к выполнению в БД.
// Команды анализируются и разделяются на два скрипта — основной и глобальных
// объектов. Обработка заключается в следующем:
// 1. Команды создания глобальных объектов
// CREATE GROUP
// CREATE USER
// CREATE ROLE
// CREATE TABLESPACE
// заменяются вызовами функций-обёрток для их безопасного выполнения.
// Эти вызовы вставляются в скрипт глобальных объектов. В этот же скрипт
// добавляются команды создания функций-обёрток и их удаления.
// 2. Команды установки кодировки клиента
// SET CLIENT_ENCODING
// SET NAMES
// \encoding
// добавляются в оба скрипта.
// 3. Команда подключения языка plpgsql
// CREATE [ TRUSTED ] [ PROCEDURAL ] LANGUAGE plpgsql
// игнорируется и не попадает ни в один скрипт. Создание языка plpgsql
// производится отдельно, перед началом выполнения скриптов.
//---------------------------------------------------------------------------
void DatabaseUpdater::prepareScripts(const QString& a_filename,
QList<QByteArray>& oa_mainScript,
QList<QByteArray>& oa_globalsScript)
{
//пропустим файл через парсер, на выходе список команд
bool helperInjected = false;
SqlProcessor proc;
QList<QByteArray> result = proc.parse(a_filename);
//обход списка и анализ команд
auto end = result.cend();
for (auto it = result.cbegin(); it != end; ++it)
{
const 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 (command2 == "group" || command2 == "user" || command2 == "role" || command2 == "tablespace")
{ //добавляем в выходной скрипт globals вызов безопасной функции-обёртки
if (!helperInjected)
{ //но сначала вставляем команды создания функций-обёрток
helperInjected = true;
for (const auto& helperScript: _createHelperFunctionsScript)
oa_globalsScript.append(helperScript);
}
//переход на начало имени объекта
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);
}
else if (command2 == "trusted" || command2 == "procedural" || command2 == "language")
{ //create language не нужен
continue;
}
else
{ //create неглобального объекта, в основной скрипт
oa_mainScript.append(line);
}
}
else
{ //все остальные команды в основной скрипт
oa_mainScript.append(line);
}
} //цикл по командам
if (helperInjected)
oa_globalsScript.append(_dropHelperFunctionsScript);
}
//---------------------------------------------------------------------------
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);
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
bool DatabaseScript::findScript(const QStringList& a_paths)
{
_script.replace(QChar('\\'), QChar('/'));
foreach (QString path, a_paths)
{
QDir dir(path);
if (dir.exists(_script))
{
_script = dir.absoluteFilePath(_script);
return true;
}
}
return false;
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jul 29, 5:46 AM (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
139609

Event Timeline