Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F193401
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
31 KB
Subscribers
None
View Options
diff --git a/src/updater.cpp b/src/updater.cpp
--- a/src/updater.cpp
+++ b/src/updater.cpp
@@ -1,796 +1,794 @@
#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)
, _processingUri(false)
{
}
DatabaseUpdater::~DatabaseUpdater()
{
delete _pset;
}
int DatabaseUpdater::run(ProgramSettings& a_pset)
{
_pset = new ProgramSettings(a_pset);
if (!checkArguments())
return -1;
if (!readConfig())
return -1;
if (!loadScriptsFromResource())
return -1;
if (!runScripts())
return -1;
return 0;
}
//---------------------------------------------------------------------------
bool DatabaseUpdater::checkArguments()
{
if (_pset->controlFile.isEmpty())
{
emit error(QStringLiteral("Не задан файл конфигурации."));
return false;
}
//если не задано полное имя файла конфигурации, то искать его в текущем каталоге
QString filename = _pset->controlFile;
if (!QDir::isAbsolutePath(filename))
filename = QStringLiteral("%1/%2").arg(QDir::currentPath()).arg(_pset->controlFile);
//проверить наличие файла
if (!QFile::exists(filename))
{
emit error(QStringLiteral("Не найден файл конфигурации\n") + QDir::toNativeSeparators(filename));
return false;
}
_pset->controlFile = filename;
return true;
}
//---------------------------------------------------------------------------
bool DatabaseUpdater::readConfig()
{
//открываем входной файл
QFile controlFile(_pset->controlFile);
if (!controlFile.open(QIODevice::ReadOnly))
{
emit error(QStringLiteral("Сбой при загрузке файла конфигурации:\n"
"Невозможно открыть для чтения файл %1")
.arg(QDir::toNativeSeparators(_pset->controlFile)));
return false;
}
//парсим входной файл
QXmlStreamReader xml;
xml.setDevice(&controlFile);
emit message(QStringLiteral("Загрузка конфигурации из файла\n%1").arg(QDir::toNativeSeparators(_pset->controlFile)));
//идём по элементам документа
while (!xml.atEnd())
{
xml.readNext();
if (xml.isStartElement() && xml.name() == "package")
if (!readPackage(xml))
return false;
}
if (xml.hasError())
{
emit error(QStringLiteral("Сбой при загрузке файла конфигурации:\n"
"Ошибка: %1, строка %2, позиция %3")
.arg(xml.errorString())
.arg(xml.lineNumber())
.arg(xml.columnNumber()));
return false;
}
controlFile.close();
//найти файл с расширением pkginfo и именем как у входного файла, если такого нет,
//то использовать первый попавшийся файл с расширением pkginfo;
//прочитать из него идентификатор пакета и имя базы данных;
//если файла нет, то и не надо
QFileInfo fi(controlFile);
QStringList files = QDir(QFileInfo(controlFile).path(), "*.pkginfo",
QDir::NoSort, QDir::Files).entryList();
if (!files.isEmpty())
{
QString pkgfile = QFileInfo(controlFile).completeBaseName() + ".pkginfo";
if (!files.contains(pkgfile, Qt::CaseInsensitive))
pkgfile = files.at(0);
pkgfile = QFileInfo(controlFile).path() + '/' + pkgfile;
emit message(QStringLiteral("Загрузка конфигурации из файла\n%1").arg(QDir::toNativeSeparators(pkgfile)));
KSettings pkginfo(pkgfile);
pkginfo.beginGroup("package");
QString s = pkginfo.value("dbname").toString();
if (!s.isEmpty() && _pset->database.isEmpty())
_pset->database = s;
QStringList ver = pkginfo.value("version").toString().split(QChar('.'));
if (ver.size() > 1)
{
s = ver.at(1);
bool ok;
int i = s.toInt(&ok);
if (ok)
_pset->dbVersion = i;
}
_pset->packageId = pkginfo.value("id").toString();
}
//после загрузки конфигурации из файлов должно быть определено имя БД
if (_pset->database.isEmpty())
{
emit error(QStringLiteral("Не задано имя базы данных"));
return false;
}
return true;
}
//---------------------------------------------------------------------------
bool DatabaseUpdater::readPackage(QXmlStreamReader& a_xml)
{
const QXmlStreamAttributes& packageAttrs = a_xml.attributes();
QString packageId = packageAttrs.value("id").toString();
QString uriAttr = packageAttrs.value("uri").toString();
bool uri = !((uriAttr == "no") || (uriAttr == "нет"));
Package package(packageId, uri);
//собираем информацию о скриптах данного пакета
const QString SCRIPT_NODE("script");
const QString REVISION_ATTR("revision");
const QString TRANSACTION_ATTR("transaction");
const QString COMMENT_ATTR("comment");
const QString SCRIPT_ATTR("file");
const QString NO_REQUIRED_ATTR = QStringLiteral("Неправильное содержание файла конфигурации:\n"
"Не указан обязательный атрибут «%1» пакета «%2»");
while (!a_xml.atEnd())
{
a_xml.readNext();
if (a_xml.isStartElement() && a_xml.name() == SCRIPT_NODE)
{
const QXmlStreamAttributes& attrs = a_xml.attributes();
//проверка наличия обязательных атрибутов
if (!attrs.hasAttribute(REVISION_ATTR))
{
emit error(NO_REQUIRED_ATTR.arg(REVISION_ATTR).arg(packageId));
return false;
}
if (!attrs.hasAttribute(SCRIPT_ATTR))
{
emit error(NO_REQUIRED_ATTR.arg(SCRIPT_ATTR).arg(packageId));
return false;
}
//проверка правильности указания атрибутов
bool ok;
int revision = attrs.value(REVISION_ATTR).toString().toInt(&ok);
if (!ok)
{
emit error(QStringLiteral(
"Атрибут «revision» пакета «%1» должен быть числом")
.arg(packageId));
return false;
}
//заносим информацию о скрипте в список скриптов пакета
QString file = attrs.value(SCRIPT_ATTR).toString();
QString comment = attrs.value(COMMENT_ATTR).toString();
QString strans = attrs.value(TRANSACTION_ATTR).toString();
bool transaction = !((strans == "0") || (strans.compare("no", Qt::CaseInsensitive) == 0) || (strans.compare("нет", Qt::CaseInsensitive) == 0));
package.addScript(DatabaseScript(file, transaction, 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, _pset->port, "template1", _pset->username,
_pset->password))
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->port, _pset->database, _pset->username,
_pset->password))
return false;
if (package.wantUri())
{
if (!uriProc.connectdb(_pset->host, _pset->port, uriDatabase, _pset->username,
_pset->password))
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();
bool transaction = a_databaseScript.transaction();
if (revision == 1)
emit message(messageString(CREATE_DB_VERSION).arg(revision));
else
emit message(messageString(UPDATE_DB_VERSION).arg(revision));
//выполнение скрипта
- if (transaction)
- a_proc.execSQL("begin");
+ a_proc.execSQL("begin");
auto script = a_script;
if (!a_proc.execute(script))
return false;
//сброс параметра search_path, он мог быть изменён при выполнении скрипта
if (!a_proc.execute(_resetSearchPathScript))
return false;
//запись информации о версии в БД
if (!a_proc.execSQL(QStringLiteral("insert into dm_version "
"(revision,package_id,comment,gen_date) values "
"(%1,'%2','%3',CURRENT_DATE)")
.arg(revision)
.arg(a_packageId)
.arg(a_databaseScript.comment())))
return false;
- if (transaction)
- a_proc.execSQL("commit");
+ 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, a_pset->port, "template1",
a_pset->username, a_pset->password))
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, qstrlen(pgvalue)).toInt() != DM_VERSION_FIELDS_COUNT)
{ //не та структура, удаляем таблицу
if (!a_proc.execSQL("drop table dm_version"))
return StatusResult(false);
createDmVersion = true;
}
}
if (createDmVersion)
{
emit message(messageString(CREATE_VERSION_TABLE));
a_proc.execSQL("begin");
if (!a_proc.execute(_createDmVersionScript))
{
emit error(messageString(ERROR_VERSION_TABLE));
return StatusResult(false);
}
//если БД уже существует и её схема отличается от схемы template1,
//т.е. в ней есть таблицы, то считается, что это ревизия №1
QStringList databaseTables = databaseTableList(a_proc);
databaseTables.removeOne("dm_version");
if (databaseTables != a_template1Tables)
{
if (!a_proc.execSQL(QStringLiteral("insert into dm_version "
"(revision,package_id,comment,gen_date) values "
"(1,'%1','Изначальная версия',CURRENT_DATE)")
.arg(a_packageId)))
return StatusResult(false, currentDatabaseRevision);
currentDatabaseRevision = 1;
}
a_proc.execSQL("commit");
}
else
{
a_proc.execSQL(QStringLiteral("select max(revision), count(*) from dm_version "
"where package_id='%1'")
.arg(a_packageId));
PGresult* pgresult = a_proc.result().pgresult;
if (PQntuples(pgresult))
{
const char* pgvalue = PQgetvalue(pgresult, 0, 1);
if (QByteArray::fromRawData(pgvalue, qstrlen(pgvalue)).toInt() > 0)
{
pgvalue = PQgetvalue(pgresult, 0, 0);
currentDatabaseRevision = QByteArray::fromRawData(pgvalue, qstrlen(pgvalue)).toInt();
}
}
}
return StatusResult(true, currentDatabaseRevision);
}
//---------------------------------------------------------------------------
bool DatabaseUpdater::loadScriptsFromResource()
{
QString errmess = QStringLiteral("Сбой при чтении ресурсов программы");
SqlProcessor proc;
QFile resFile(":/dm_version.sql");
if (!(resFile.open(QIODevice::ReadOnly)))
{
emit error(errmess);
return false;
}
_createDmVersionScript = proc.parse(resFile);
resFile.close();
resFile.setFileName(":/create_helper_functions.sql");
if (!(resFile.open(QIODevice::ReadOnly)))
{
emit error(errmess);
return false;
}
_createHelperFunctionsScript = proc.parse(resFile);
resFile.close();
resFile.setFileName(":/drop_helper_functions.sql");
if (!(resFile.open(QIODevice::ReadOnly)))
{
emit error(errmess);
return false;
}
_dropHelperFunctionsScript = proc.parse(resFile);
resFile.close();
resFile.setFileName(":/set_search_path.sql");
if (!(resFile.open(QIODevice::ReadOnly)))
{
emit error(errmess);
return false;
}
_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);
emit logConnectionParameters(a_host, a_database, a_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('/'));
foreach (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
Details
Attached
Mime Type
text/x-diff
Expires
Wed, Jun 11, 8:07 PM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
127125
Attached To
rRCT Программа создания/обновления структуры БД
Event Timeline
Log In to Comment