diff --git a/resource/appicon.ico b/resource/appicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0b3a71ef144d053009d3ad1b8624a2120b16a076 GIT binary patch literal 17542 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 JT`g%A_CE!e>Er+a diff --git a/resource/project.qrc b/resource/project.qrc new file mode 100644 --- /dev/null +++ b/resource/project.qrc @@ -0,0 +1,9 @@ + + + dm_version.sql + create_helper_functions.sql + drop_helper_functions.sql + set_search_path.sql + picture.png + + diff --git a/resource/project.rc b/resource/project.rc new file mode 100644 --- /dev/null +++ b/resource/project.rc @@ -0,0 +1,1 @@ +IDI_ICON1 ICON DISCARDABLE "appicon.ico" diff --git a/resource/set_search_path.sql b/resource/set_search_path.sql new file mode 100644 --- /dev/null +++ b/resource/set_search_path.sql @@ -0,0 +1,1 @@ +set search_path=public; diff --git a/restruct.pro b/restruct.pro new file mode 100644 --- /dev/null +++ b/restruct.pro @@ -0,0 +1,43 @@ +###################################################################### +# Project started 11.01.2008 at 13:10 +###################################################################### + +QT += gui xml +TEMPLATE = app +TARGET = restruct +DESTDIR = . +INCLUDEPATH += . ./src ../include +PRECOMPILED_HEADER = ./src/stable.h +QMAKE_LIBDIR += ../lib + +RESOURCES += resource/project.qrc + +SOURCES += src/restruct.cpp \ + src/mainwindow.cpp \ + src/updater.cpp \ + src/sqlerrordlg.cpp + +HEADERS += src/mainwindow.h \ + src/updater.h \ + src/psettings.h \ + src/sqlerrordlg.h + +LIBS += -lktools -lpq + +win32 { + RC_FILE = resource/project.rc + LIBS += -lgdi32 -luser32 +} +win32-msvc* { + CONFIG(debug, debug|release) { + QMAKE_LFLAGS *= /NODEFAULTLIB:msvcrt + LIBS = $$replace(LIBS,-lktools,-lktoolsd) + } + DEFINES += _CRT_SECURE_NO_WARNINGS +} + +#unix { +# SOURCES += src/ +# HEADERS += src/ +#} + diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp new file mode 100644 --- /dev/null +++ b/src/mainwindow.cpp @@ -0,0 +1,441 @@ +#include "stable.h" +#include "mainwindow.h" +#include "updater.h" +#include "psettings.h" +#include "sqlerrordlg.h" +#include +#include +#include +#include +#ifdef Q_OS_WIN32 +#include +#endif + +//--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- +MainWindow::MainWindow( QWidget* parent, Qt::WindowFlags flags ) + : QDialog( parent, flags ) + , _exitCode( 0 ) + , _actionEventType( QEvent::User ) + , _errorCount( 0 ) + , _updater( new DatabaseUpdater ) +{ + setWindowTitle( QString::fromUtf8( "Создание/обновление базы данных" ) ); + 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( QString::fromUtf8( "Сохранить" ) ); + _btnClose = new QPushButton( QString::fromUtf8( "Закрыть" ) ); + _btnSave->setVisible( false ); + _btnClose->setVisible( false ); + _btnClose->setDefault( true ); + + leftLayout->addWidget( picture ); + leftLayout->addStretch(); + leftLayout->addWidget( _btnSave ); + leftLayout->addWidget( _btnClose ); + + QLabel* label = new QLabel( QString::fromUtf8( "Протокол работы" ) ); + 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 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( QString::fromUtf8( "База данных в актуальном состоянии." ) ); + else if ( before == 0 ) + message( QString::fromUtf8( "База данных создана и обновлена до версии %1." ).arg( after ) ); + else + message( QString::fromUtf8( "База данных версии %1 обновлена до версии %2." ). + arg( before ).arg( after ) ); + _exitCode = (before & 0xFFFF) | ((after & 0xFFFF) << 16); + + closeDialog(); //при успешном завершении, то окно закрывается автоматически + } + else + { + _btnClose->show(); + _btnSave->show(); + _btnClose->setFocus(); + message( QString::fromUtf8( "Выполнение прервано в результате ошибки." + "
База данных не изменилась.") ); + _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( QString::fromUtf8( "База данных в актуальном состоянии." ) ); + else if ( before == 0 ) + message( QString::fromUtf8( "База данных создана и обновлена до версии %1." ).arg( after ) ); + else + message( QString::fromUtf8( "База данных версии %1 обновлена до версии %2." ). + arg( before ).arg( after ) ); + rc = (before & 0xFFFF) | ((after & 0xFFFF) << 16); + } + else + { + _btnClose->show(); + _btnSave->show(); + _btnClose->setFocus(); + message( QString::fromUtf8( "Выполнение прервано в результате ошибки." + "
База данных не изменилась.") ); + 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 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; + QString curArg; + QStringListIterator args( QApplication::arguments() ); + args.next(); //пропускаем первый параметр - имя программы + while ( args.hasNext() ) + { + curArg = args.next(); + + if ( curArg.startsWith( "--" ) ) + {//обработка длинного параметра + curArg.remove( 0, 2 ); + QMutableHashIterator option( params ); + while ( option.hasNext() ) + { + option.next(); + key = option.key(); + if ( curArg.startsWith( key ) && curArg.at( key.size() ) == '=' ) + option.setValue( curArg.right( curArg.size() - key.size() - 1 ) ); + } + key.clear(); + } + else 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( QString::fromUtf8( "Отказ от ввода имени и пароля пользователя" ) ); + 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( QString::fromUtf8( "Подключение к базе данных
" + "  Сервер: %1
  База данных: %2
" + "  Пользователь: %3").arg( a_host, a_database, a_username ) ); +} + +//--------------------------------------------------------------------------- +void MainWindow::sqlError( const QString& a_dbError, + const QString& a_commandDescription, + const QString& a_command ) +{ + QString s; + + if ( !a_commandDescription.isEmpty() ) + s.append( QString::fromUtf8( "Операция:
%1
" ).arg( a_commandDescription ) ); + + s.append( QString::fromUtf8( "Сообщение об ошибке:
%1" ).arg( a_dbError ) ); + + if ( !a_command.isEmpty() ) + s.append( QString::fromUtf8( "Текст команды:
%1
" ).arg( a_command ) ); + + error( s ); +} + +//--------------------------------------------------------------------------- +void MainWindow::error( const QString& a_error ) +{ + QString error = a_error; + QTextCursor cursor = _protocol->textCursor(); + cursor.insertHtml( QString::fromUtf8( "Ошибка
" ) ); + cursor.insertHtml( error.replace( QChar('\n'), "
" ) ); + cursor.insertHtml( QString::fromUtf8( "
" ) ); + _protocol->moveCursor( QTextCursor::End ); + ++_errorCount; +} + +//--------------------------------------------------------------------------- +void MainWindow::message( const QString& a_message ) +{ + QString message = a_message; + _protocol->textCursor().insertHtml( message.replace( QChar('\n'), "
" ).append( "
" ) ); + _protocol->moveCursor( QTextCursor::End ); +} + +//--------------------------------------------------------------------------- +void MainWindow::closeDialog() +{ + QDialog::done( _exitCode ); +} + +//--------------------------------------------------------------------------- +void MainWindow::saveProtocol() +{ + QString filename = QFileDialog::getSaveFileName( this, + QString::fromUtf8( "Сохранить протокол в файл…" ), QString(), + QString::fromUtf8( "Текстовые файлы (*.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(); + //toPlainText(); + } + } +} + +//!главное окно приложения diff --git a/src/mainwindow.h b/src/mainwindow.h new file mode 100644 --- /dev/null +++ b/src/mainwindow.h @@ -0,0 +1,64 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include + +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 ); + virtual ~MainWindow(); + +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: + virtual void showEvent( QShowEvent* a_event ); + virtual void closeEvent( QCloseEvent* a_event ); + virtual bool event( QEvent* a_event ); + +private: + int executeAction(); + void logon( ProgramSettings& a_pset ); + void processArguments( ProgramSettings& a_pset ); + + int _exitCode; + int _actionEventType; + int _errorCount; + QTextEdit* _protocol; + QLabel* _lblWait; + QPushButton* _btnSave; + QPushButton* _btnClose; + KActivityIndicator* _ai; + QProgressBar* _progress; + QFutureWatcher _futureWatcher; + DatabaseUpdater* _updater; +}; + +//!главное окно приложения +#endif //MAINWINDOW_H diff --git a/src/psettings.h b/src/psettings.h new file mode 100644 --- /dev/null +++ b/src/psettings.h @@ -0,0 +1,19 @@ +#if !defined PSETTINGS_H +#define PSETTINGS_H + +class ProgramSettings +{ +public: + ProgramSettings() : dbVersion(0), dropdb(false) {} + QString username; + QString password; + QString database; + QString host; + QString port; + QString controlFile; + QString packageId; + int dbVersion; + bool dropdb; +}; + +#endif diff --git a/src/restruct.cpp b/src/restruct.cpp new file mode 100644 --- /dev/null +++ b/src/restruct.cpp @@ -0,0 +1,54 @@ +#include "stable.h" +#include "mainwindow.h" + +#ifdef Q_OS_WIN32 +#include +#endif + +int main( int argc, char* argv[] ) +{ + + //int width = 504; + //int height = 262; + int width = 453; + int height = 280; + + QApplication app( argc, argv ); + + //загрузка русской локализации библиотеки Qt + QTranslator translator; + translator.load( QString("qt_ru") ); + app.installTranslator(&translator); + + //создание главного окна + MainWindow window; + +#ifdef Q_OS_WIN32 + //масштабирование для случая увеличенного шрифта + int dpi = GetDeviceCaps( app.desktop()->getDC(), LOGPIXELSY ); + if ( dpi != 96 ) + { + width = (width * dpi) / 96; + height = (height * dpi) / 96; + } + + //шрифт окна такой же, как в системе подписи к иконкам + LOGFONTW lf; + SystemParametersInfoW( SPI_GETICONTITLELOGFONT, sizeof(lf), &lf, 0 ); + int fh = qAbs( (lf.lfHeight * 72) / dpi ); + QString fn( (const QChar*)lf.lfFaceName, wcslen(lf.lfFaceName) ); + window.setStyleSheet( + QString( "*{ font-family: %1; font-size: %2pt; }" ).arg( fn ).arg( fh ) ); +#endif + + //перемещение главного окна в центр экрана + window.setMinimumSize( width, height ); + QRect wg = window.geometry(); + wg.setSize( window.minimumSize() ); + wg.moveCenter( qApp->desktop()->screenGeometry().center() ); + window.setGeometry( wg ); + + int exitCode = window.exec(); + return exitCode; +} +//!приложение restruct diff --git a/src/sqlerrordlg.cpp b/src/sqlerrordlg.cpp new file mode 100644 --- /dev/null +++ b/src/sqlerrordlg.cpp @@ -0,0 +1,45 @@ +#include "stable.h" +#include "sqlerrordlg.h" + +//--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- +SqlErrorDialog::SqlErrorDialog( QWidget* parent, Qt::WindowFlags flags ) + : QDialog( parent, flags ) +{ + setWindowTitle( QString::fromUtf8( "Список ошибок" ) ); + setWindowFlags( windowFlags() & ~Qt::WindowContextHelpButtonHint ); + setSizeGripEnabled( true ); + setMinimumSize( QSize( 512, 428 ) ); + + QVBoxLayout* mainLayout = new QVBoxLayout( this ); + QHBoxLayout* layout = new QHBoxLayout; + mainLayout->addLayout( layout ); + + QLabel* label = new QLabel( + QString( "Во время выполнения команд произошли\nследующие ошибки:" ) ); + layout->addWidget( label ); + layout->addStretch(); + + _btnClose = new QPushButton( QString( "Закрыть" ) ); + _btnClose->setDefault( true ); + layout->addWidget( _btnClose ); + + _errorLog = new QTextEdit; + _errorLog->setReadOnly( true ); + mainLayout->addWidget( _errorLog ); + + connect( _btnClose, SIGNAL(clicked()), this, SLOT(close()) ); +} + +SqlErrorDialog::~SqlErrorDialog() +{ +} + +void SqlErrorDialog::setLogText( const QString& a_text ) +{ + _errorLog->clear(); + _errorLog->setHtml( a_text ); +} + + +//!главное окно приложения diff --git a/src/sqlerrordlg.h b/src/sqlerrordlg.h new file mode 100644 --- /dev/null +++ b/src/sqlerrordlg.h @@ -0,0 +1,24 @@ +#ifndef SQLERRORDLG_H +#define SQLERRORDLG_H + +#include + +class QPushButton; +class QString; +class QTextEdit; + +class SqlErrorDialog : public QDialog +{ +public: + SqlErrorDialog( QWidget * parent = 0, Qt::WindowFlags flags = 0 ); + virtual ~SqlErrorDialog(); + + void setLogText( const QString& a_text ); + +private: + QPushButton* _btnClose; + QTextEdit* _errorLog; +}; + +//!главное окно приложения +#endif //SQLERRORDLG_H diff --git a/src/stable.h b/src/stable.h new file mode 100644 --- /dev/null +++ b/src/stable.h @@ -0,0 +1,26 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/src/stable.h.cpp b/src/stable.h.cpp new file mode 100644 --- /dev/null +++ b/src/stable.h.cpp @@ -0,0 +1,16 @@ +/*-------------------------------------------------------------------- +* Precompiled header source file used by Visual Studio.NET to generate +* the .pch file. +* +* Due to issues with the dependencies checker within the IDE, it +* sometimes fails to recompile the PCH file, if we force the IDE to +* create the PCH file directly from the header file. +* +* This file is auto-generated by qmake since no PRECOMPILED_SOURCE was +* specified, and is used as the common stdafx.cpp. The file is only +* generated when creating .vcproj project files, and is not used for +* command line compilations by nmake. +* +* WARNING: All changes made in this file will be lost. +--------------------------------------------------------------------*/ +#include "stable.h" diff --git a/src/updater.cpp b/src/updater.cpp new file mode 100644 --- /dev/null +++ b/src/updater.cpp @@ -0,0 +1,873 @@ +#include "stable.h" +#include "updater.h" +#include "psettings.h" +#include +#include +#include +#include + +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( QString::fromUtf8( "Не задан файл конфигурации." ) ); + return false; + } + + //если не задано полное имя файла конфигурации, то искать его в текущем каталоге + QString filename = _pset->controlFile; + + if ( !QDir::isAbsolutePath( filename ) ) + filename = QString( "%1/%2" ).arg( QDir::currentPath() ).arg( _pset->controlFile ); + + //проверить наличие файла + if ( !QFile::exists( filename ) ) + { + emit error( QString::fromUtf8( "Не найден файл конфигурации\n" ) + + QDir::convertSeparators( filename ) ); + return false; + } + + _pset->controlFile = filename; + return true; +} + +//--------------------------------------------------------------------------- +bool DatabaseUpdater::readConfig() +{ + //открываем входной файл + QFile controlFile( _pset->controlFile ); + if ( !controlFile.open( QIODevice::ReadOnly ) ) + { + emit error( QString::fromUtf8( "Сбой при загрузке файла конфигурации:\n" + "Невозможно открыть для чтения файл %1" ).arg( QDir::convertSeparators( + _pset->controlFile ) ) ); + return false; + } + + //парсим входной файл в DOM + int domErrorLine; + int domErrorColumn; + QString domErrorMsg; + QDomDocument dom; + if ( !dom.setContent( &controlFile, false, &domErrorMsg, &domErrorLine, &domErrorColumn ) ) + { + emit error( QString::fromUtf8( "Сбой при загрузке файла конфигурации:\n" + "Ошибка: %1, строка %2, позиция %3"). + arg( domErrorMsg ).arg( domErrorLine ).arg( domErrorColumn ) ); + controlFile.close(); + return false; + } + + controlFile.close(); + + emit message( QString::fromUtf8( "Загрузка конфигурации из файла\n%1" ). + arg( QDir::convertSeparators( _pset->controlFile ) ) ); + + //идём по дереву узлов документа + QDomElement document = dom.documentElement(); + QDomNode node = document.firstChild(); + while ( !node.isNull() ) + { + if ( node.isElement() && (node.nodeName() == "package") ) + { + if ( !readPackage( node ) ) + return false; + } + node = node.nextSibling(); + } + + //найти файл с расширением 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( QString::fromUtf8( "Загрузка конфигурации из файла\n%1" ). + arg( QDir::convertSeparators( 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( QString::fromUtf8( "Не задано имя базы данных" ) ); + return false; + } + return true; +} + +//--------------------------------------------------------------------------- +bool DatabaseUpdater::readPackage( const QDomNode& a_node ) +{ + QDomElement elt = a_node.toElement(); + QString packageId = elt.attribute( "id" ); + QString uriAttr = elt.attribute( "uri" ); + bool uri = !((uriAttr == "no") || (uriAttr == "нет")); + + Package* package = new Package( packageId, uri ); + _packages.append( package ); + + //собираем информацию о скриптах данного пакета + QString SCRIPT_NODE( "script" ); + QString REVISION_ATTR( "revision" ); + QString TRANSACTION_ATTR( "transaction" ); + QString COMMENT_ATTR( "comment" ); + QString SCRIPT_ATTR( "file" ); + QString URI_DATA_ATTR( "udata" ); + QString NO_REQUIRED_ATTR = QString::fromUtf8( "Неправильное содержание файла конфигурации:\n" + "Не указан обязательный атрибут «%1» пакета «%2»" ); + + QDomNode node = elt.firstChild(); + while ( !node.isNull() ) + { + if ( node.isElement() && (node.nodeName() == SCRIPT_NODE) ) + { + elt = node.toElement(); + //проверка наличия обязательных атрибутов + if ( !elt.hasAttribute( REVISION_ATTR ) ) + { + emit error( NO_REQUIRED_ATTR.arg( REVISION_ATTR ).arg( packageId ) ); + return false; + } + if ( !elt.hasAttribute( SCRIPT_ATTR ) ) + { + emit error( NO_REQUIRED_ATTR.arg( SCRIPT_ATTR ).arg( packageId ) ); + return false; + } + //проверка правильности указания атрибутов + bool ok; + int revision = elt.attribute( REVISION_ATTR ).toInt( &ok ); + if ( !ok ) + { + emit error( QString::fromUtf8( + "Атрибут «revision» пакета «%1» должен быть числом" ). + arg( packageId ) ); + return false; + } + + //заносим информацию о скрипте в список скриптов пакета + QString file = elt.attribute( SCRIPT_ATTR ); + QString uriScript = elt.attribute( URI_DATA_ATTR ); + QString comment = elt.attribute( COMMENT_ATTR ); + QString strans = elt.attribute( TRANSACTION_ATTR ); + bool transaction = !((strans == "0") || + (strans.compare( "no", Qt::CaseInsensitive ) == 0) || + (strans.compare( "нет", Qt::CaseInsensitive ) == 0)); + + DatabaseScript* script = new DatabaseScript( revision, transaction, + file, uriScript, comment ); + package->addScript( script ); + } + node = node.nextSibling(); + } + + //упорядочить скрипты по возрастанию номеров ревизий + package->sortScripts(); + + return true; +} + +//--------------------------------------------------------------------------- +bool DatabaseUpdater::runScripts() +{ + //определение пакета, скрипты которого будут выполняться + Package* package = 0; + QListIterator it( _packages ); + while ( it.hasNext() ) + { + package = it.next(); + if ( package->id() == _pset->packageId ) + break; + else + package = 0; + } + + if ( !package ) + { + emit error( QString::fromUtf8( "Для пакета «%1» не задано ни одного " + "сценария создания базы данных" ).arg( _pset->packageId ) ); + return false; + } + + //проверка наличия файлов со скриптами на диске + QStringList paths; + QFileInfo fi( _pset->controlFile ); + paths << fi.canonicalPath() << fi.canonicalPath() + "/script" << QDir::currentPath(); + + DatabaseScript* script = 0; + QListIterator it2( package->scripts() ); + while ( it2.hasNext() ) + { + script = it2.next(); + if ( !script->findScript( paths ) ) + { + emit error( QString::fromUtf8( "Не найден файл сценария создания " + "базы данных %1" ).arg( script->script() ) ); + return false; + } + if ( package->uri() && !script->findUriScript( paths ) ) + { + emit error( QString::fromUtf8( "Не найден файл условно-реальной " + "информации %1" ).arg( script->uriScript() ) ); + return false; + } + } + + QString curDate = QDate::currentDate().toString( Qt::ISODate ); + SqlProcessor proc; + SqlProcessor uriProc; + + emit message( QString::fromUtf8( "Подключение к серверу баз данных" ) ); + + //пока идут служебные подключения к БД и запросы, подключаем только сигнал 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&)) ); + + qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); + + //---- 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( QString::fromUtf8( "Удаление базы данных %1" ).arg( _pset->database ) ); + qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); + + if ( !dropDatabase( proc, _pset ) ) + return false; + dbExists = false; + } + //создание БД если она не существует + if ( !dbExists ) + { + emit message( QString::fromUtf8( "Создание базы данных %1" ).arg( _pset->database ) ); + qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); + + 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( QString::fromUtf8( "Удаление учебной базы данных %1" ).arg( uriDatabase ) ); + qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); + + if ( !dropDatabase( proc, _pset, uriDatabase ) ) + return false; + uriDbExists = false; + } + //создание учебной БД если она не существует + if ( !uriDbExists ) + { + emit message( QString::fromUtf8( "Создание учебной базы данных %1" ).arg( uriDatabase ) ); + qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); + + if ( !createDatabase( proc, _pset, uriDatabase ) ) + return false; + } + } + + connect( &proc, SIGNAL(afterConnect(const SqlProcessor&)), + this, SLOT(afterConnect(const SqlProcessor&)) ); + + //---- 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'" ); + KSimpleDataSet data = proc.result(); + + //если пустой результат запроса, значит таблицы dm_version нет в БД + bool createDmVersion = data.size() == 0; + + //если таблица есть, то сравнить число её атрибутов с эталоном + data.first(); + if ( data.isValid() && data.value( 0 ).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( QString::fromUtf8( "Создание таблицы изменений БД" ) ); + proc.execSQL( "begin" ); + if ( !proc.execute( _createDmVersionScript ) ) + { + emit error( QString::fromUtf8( "Сбой при создании таблицы изменений БД" ) ); + return false; + } + + if ( legacyDatabase ) + { //запись информации о версии №1 в unversioned БД + if ( !proc.execSQL( QString::fromUtf8( "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( QString("select max(revision), count(*) from dm_version " + "where package_id='%1'" ).arg( package->id() ) ); + data = proc.result(); + data.first(); + if ( data.isValid() && (data.value( 1 ) > 0) ) + currentDatabaseRevision = data.value( 0 ).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)) ); + + QListIterator iter( package->scripts() ); + while ( iter.hasNext() ) + { + script = iter.next(); + int revision = script->revision(); + bool transaction = script->transaction(); + + //если была найдена изначальная БД, то всё уже сделано для 1-й версии + if ( (revision == 1) && legacyDatabase ) + continue; + + //пропускаем уже установленные версии БД + if ( revision <= currentDatabaseRevision ) + continue; + + if ( revision == 1 ) + emit message( QString::fromUtf8( "Создание базы данных версии %1" ).arg( revision ) ); + else + emit message( QString::fromUtf8( "Обновление базы данных до версии %1" ).arg( revision ) ); + + //подготовка скрипта к выполнению + QList mainScript; + QList 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( QString::fromUtf8( "Создание учебной базы данных версии %1" ).arg( revision ) ); + else + emit message( QString::fromUtf8( "Обновление учебной базы данных до версии %1" ).arg( revision ) ); + + if ( !uriProc.execute( mainScript ) ) + { + if ( transaction ) + proc.execSQL( "rollback" ); + return false; + } + if ( !script->uriScript().isEmpty() ) + { + emit message( QString::fromUtf8( "Загрузка условной информации в учебную базу данных" ) ); + + if ( !uriProc.execute( script->uriScript() ) ) + { + 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( QString::fromUtf8( "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( QString( "select 1 from pg_database where datname='%1\'" ).arg(a_dbname) ); + return a_proc.result().size() == 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, QString( "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, QString( "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; + + KSimpleDataSet data = a_proc.result(); + data.first(); + while ( data.isValid() ) + { + result.append( data.value( 0 ) ); + data.next(); + } + return result; +} + +//--------------------------------------------------------------------------- +bool DatabaseUpdater::loadScriptsFromResource() +{ + QString errmess = QString::fromUtf8( "Сбой при чтении ресурсов программы" ); + SqlProcessor proc; + + QFile resFile( ":/dm_version.sql" ); + if ( !(resFile.open( QIODevice::ReadOnly ) ) ) + { + emit error( errmess ); + return false; + } + _createDmVersionScript = proc.parse( resFile ); + + resFile.setFileName( ":/create_helper_functions.sql"); + if ( !(resFile.open( QIODevice::ReadOnly ) ) ) + { + emit error( errmess ); + return false; + } + _createHelperFunctionsScript = proc.parse( resFile ); + + resFile.setFileName( ":/drop_helper_functions.sql"); + if ( !(resFile.open( QIODevice::ReadOnly ) ) ) + { + emit error( errmess ); + return false; + } + _dropHelperFunctionsScript = proc.parse( resFile ); + + 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& oa_mainScript, + QList& oa_globalsScript ) +{ + //пропустим файл через парсер, на выходе список команд + bool helperInjected = false; + SqlProcessor proc; + QList result = proc.parse( a_filename ); + + //обход списка и анализ команд + QListIterator it( result ); + while ( it.hasNext() ) + { + const QByteArray originalLine = it.next(); + QByteArray line = originalLine.toLower(); + + int i = 0; + int end = line.size(); + + //пропускаем первое слово, перед ним пробелов нет, парсер их убирает + while ( i != end && !QChar(line.at( i )).isSpace() ) + ++i; + + //анализируем первое слово + QByteArray command1 = line.left( i ); + if ( command1 == "\\encoding" ) + { + oa_mainScript.append( originalLine ); + oa_globalsScript.append( originalLine ); + 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 ); + if ( command1 == "set" ) + { + oa_mainScript.append( originalLine ); + if ( command2 == "names" || command2 == "client_encoding" ) + oa_globalsScript.append( originalLine ); + } + else if ( command1 == "create" ) + { + if ( command2 == "group" || command2 == "user" || + command2 == "role" || command2 == "tablespace" ) + { //добавляем в выходной скрипт globals вызов безопасной функции-обёртки + if ( !helperInjected ) + { //но сначала вставляем команды создания функций-обёрток + helperInjected = true; + QListIterator helperIter( _createHelperFunctionsScript ); + while ( helperIter.hasNext() ) + oa_globalsScript.append( helperIter.next() ); + } + //переход на начало имени объекта + 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; + //т.к. имя без двойных кавычек, то перевести его в нижний регистр, + //как это делает PostgreSQL + name = line.mid( p, i - p ).toLower(); + 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 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( originalLine ); + } + } + else + { //все остальные команды в основной скрипт + oa_mainScript.append( originalLine ); + } + } //цикл по командам + + 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 ( a_proc.result().size() == 1 ) + a_proc.execSQL( "update pg_class set relmaclabel=null" ); +} + +//--------------------------------------------------------------------------- +bool DatabaseUpdater::createLanguagePlpgsql( SqlProcessor& a_proc, + SqlProcessor& a_uriProc, + bool a_uri ) +{ + static const char* CREATE_SQL = "create language plpgsql"; + static const char* CHECK_SQL = "select 1 from pg_language where lanname='plpgsql'"; + + a_proc.execSQL( CHECK_SQL ); + if ( a_proc.result().size() == 0 ) + { + if ( !a_proc.execSQL( CREATE_SQL ) ) + return false; + } + if ( a_uri ) + { + a_uriProc.execSQL( CHECK_SQL ); + if ( a_uriProc.result().size() == 0 ) + { + if ( !a_uriProc.execSQL( CREATE_SQL ) ) + return false; + } + } + return true; +} + +//--------------------------------------------------------------------------- +void DatabaseUpdater::afterConnect( const SqlProcessor& a_proc ) +{ + emit logConnectionParameters( a_proc.host(), a_proc.database(), a_proc.username() ); +} + +//--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- +bool DatabaseScript::findScript( const QStringList& a_paths ) +{ + foreach( QString path, a_paths ) + { + QString filename = path + "/" + _script; + if ( QFile::exists( filename ) ) + { + _script = filename; + return true; + } + } + return false; +} + +//--------------------------------------------------------------------------- +bool DatabaseScript::findUriScript( const QStringList& a_paths ) +{ + if ( _uriScript.isEmpty() ) + return true; + + foreach( QString path, a_paths ) + { + QString filename = path + "/" + _uriScript; + if ( QFile::exists( filename ) ) + { + _uriScript = filename; + return true; + } + } + return false; +} + diff --git a/src/updater.h b/src/updater.h new file mode 100644 --- /dev/null +++ b/src/updater.h @@ -0,0 +1,115 @@ +#ifndef UPDATER_H +#define UPDATER_H + +#include +#include +#include +#include + +class DatabaseScript +{ + friend class DatabaseUpdater; +public: + DatabaseScript( int a_revision, bool a_transaction, const QString& a_script, + const QString& a_uriScript, const QString& a_comment ) + : _revision(a_revision), _transaction(a_transaction) + ,_script(a_script), _uriScript(a_uriScript), _comment(a_comment) {} + + int revision() const { return _revision; } + bool transaction() const { return _transaction; } + QString script() const { return _script; } + QString uriScript() const { return _uriScript; } + QString comment() const { return _comment; } + + bool findScript( const QStringList& a_paths ); + bool findUriScript( const QStringList& a_paths ); + +private: + int _revision; + bool _transaction; + QString _script; + QString _uriScript; + QString _comment; +}; + +class Package +{ +public: + Package( const QString& a_id, bool a_uri = true ) + : _id(a_id), _uri(a_uri) {} + void addScript( DatabaseScript* a_script ) { _scripts.append( a_script ); } + + void sortScripts() { qSort( _scripts.begin(), _scripts.end(), scriptRevisionLessThan ); } + static bool scriptRevisionLessThan( DatabaseScript* a_script1, + DatabaseScript* a_script2 ) { return a_script1->revision() < a_script2->revision(); } + + bool uri() const { return _uri; } + QString id() const { return _id; } + QList scripts() const { return _scripts; } + +private: + QList _scripts; + QString _id; + bool _uri; +}; + +class SqlProcessor; +class ProgramSettings; +class QByteArray; +class QByteArrayMatcher; +class DatabaseUpdater : public QObject +{ + Q_OBJECT + +public: + DatabaseUpdater(); + virtual ~DatabaseUpdater(); + + int run( ProgramSettings& a_pset ); + int revisionBefore() const { return _revisionBefore; } + int revisionAfter() const { return _revisionAfter; } + +signals: + void error( const QString& a_message ); + void sqlError( const QString& a_dbError, const QString& a_commandDescription, + const QString& a_command ); + void logConnectionParameters( const QString& a_host, const QString& a_database, + const QString& a_username ); + void progress( int ); + void message( const QString& a_message ); + +public slots: + void afterConnect( const SqlProcessor& a_proc ); + +private: + enum { DM_VERSION_FIELDS_COUNT = 5 }; + ProgramSettings* _pset; + QString _logText; + QList _packages; + QList _createDmVersionScript; + QList _createHelperFunctionsScript; + QList _dropHelperFunctionsScript; + QList _setSearchPathScript; + int _revisionBefore; + int _revisionAfter; + + QStringList databaseTableList( SqlProcessor& a_proc ); + bool checkArguments(); + bool readConfig(); + bool readPackage( const QDomNode& a_node ); + bool loadScriptsFromResource(); + bool runScripts(); + bool databaseExists( SqlProcessor& a_proc, const QString& a_dbname ); + bool createDatabase( SqlProcessor& a_proc, ProgramSettings* a_pset, + const QString& a_dbname = QString() ); + bool dropDatabase( SqlProcessor& a_proc, ProgramSettings* a_pset, + const QString& a_dbname = QString() ); + bool createLanguagePlpgsql( SqlProcessor& a_proc, SqlProcessor& a_uriProc, + bool a_uri ); + void prepareScripts( const QString& a_filename, QList& oa_mainScript, + QList& oa_globalsScript ); + void clearMaclabels( SqlProcessor& a_proc ); +}; + +//!главный класс приложения +#endif //UPDATER_H