/**************************************************************************
**
** This file is part of Nut project.
** https://github.com/HamedMasafi/Nut
**
** Nut is free software: you can redistribute it and/or modify
** it under the terms of the GNU Lesser General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** Nut is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Lesser General Public License for more details.
**
** You should have received a copy of the GNU Lesser General Public License
** along with Nut. If not, see .
**
**************************************************************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "database.h"
#include "table.h"
#include "tableset.h"
#include "database_p.h"
#include "defines.h"
#include "tablemodel.h"
#include "generators/postgresqlgenerator.h"
#include "generators/mysqlgenerator.h"
#include "generators/sqlitegenerator.h"
#include "generators/sqlservergenerator.h"
#include "query.h"
#include "changelogtable.h"
#include
#include
#ifndef __CHANGE_LOG_TABLE_NAME
# define __CHANGE_LOG_TABLE_NAME "__change_logs"
#endif
NUT_BEGIN_NAMESPACE
qulonglong DatabasePrivate::lastId = 0;
QMap DatabasePrivate::allTableMaps;
DatabasePrivate::DatabasePrivate(Database *parent) : q_ptr(parent),
port(0), sqlGenerator(nullptr), changeLogs(nullptr),
isDatabaseNew(false)
{
}
bool DatabasePrivate::open(bool update)
{
if (db.isOpen())
return true;
Q_Q(Database);
// if (update)
connectionName = q->metaObject()->className()
+ QString::number(DatabasePrivate::lastId);
db = QSqlDatabase::addDatabase(driver, connectionName);
db.setHostName(hostName);
if (port)
db.setPort(port);
db.setDatabaseName(databaseName);
db.setUserName(userName);
db.setPassword(password);
if (driver.startsWith("qsqlite", Qt::CaseInsensitive)
&& !QFile::exists(databaseName)) {
//Force to execute update database
isDatabaseNew = true;
update = true;
}
bool ok = db.open();
if (!ok) {
qWarning("Could not connect to database, error = %s",
db.lastError().text().toLocal8Bit().data());
if (db.lastError().text().contains("database \"" + databaseName
+ "\" does not exist")
|| db.lastError().text().contains("Cannot open database")
|| db.lastError().text().contains("Unknown database '"
+ databaseName + "'")) {
db.close();
db.setDatabaseName(sqlGenerator->masterDatabaseName(databaseName));
ok = db.open();
qDebug("Creating database");
if (ok) {
db.exec("CREATE DATABASE " + databaseName);
db.close();
if (db.lastError().type() != QSqlError::NoError) {
qWarning("Creating database error: %s",
db.lastError().text().toLatin1().data());
return false;
}
isDatabaseNew = true;
return open(update);
}
qWarning("Unknown error detecting change logs, %s",
db.lastError().text().toLatin1().data());
}
return false;
}
// if(update)
return updateDatabase();
// else
// return true;
}
bool DatabasePrivate::updateDatabase()
{
Q_Q(Database);
if (!getCurrectSchema())
return true;
DatabaseModel last = isDatabaseNew ? DatabaseModel() : getLastSchema();
DatabaseModel current = currentModel;
if (last == current) {
qDebug("Database is up-to-date");
//TODO: crash without this and I don't know why!
changeLogs->clearChilds();
return true;
}
foreach (TableModel *tm, current) {
foreach (FieldModel *fm, tm->fields()) {
if (sqlGenerator->fieldType(fm).isEmpty()) {
qWarning("The type (%s) is not supported for field %s::%s",
QMetaType::typeName(fm->type),
qPrintable(tm->className()),
qPrintable(fm->name));
return false;
}
}
}
if (!last.count())
qDebug("Database is new");
else
qDebug("Database is changed");
QStringList sql = sqlGenerator->diff(last, current);
db.transaction();
foreach (QString s, sql) {
db.exec(s);
if (db.lastError().type() != QSqlError::NoError) {
qWarning("Error executing sql command `%s`, %s",
qPrintable(s),
db.lastError().text().toLatin1().data());
return false;
}
}
putModelToDatabase();
bool ok = db.commit();
if (db.lastError().type() == QSqlError::NoError) {
q->databaseUpdated(last.version(), current.version());
if (!last.count())
q->databaseCreated();
} else {
qWarning("Unable update database, error = %s",
db.lastError().text().toLatin1().data());
}
return ok;
}
bool DatabasePrivate::getCurrectSchema()
{
Q_Q(Database);
//is not first instanicate of this class
if (allTableMaps.contains(QString::fromUtf8(q->metaObject()->className()))) {
currentModel = allTableMaps[QString::fromUtf8(q->metaObject()->className())];
return false;
}
QMap tables;
tables.clear();
// TODO: change logs must not be in model
int changeLogTypeId = qRegisterMetaType();
currentModel.append(
new TableModel(changeLogTypeId, QStringLiteral(__CHANGE_LOG_TABLE_NAME)));
tables.insert(QString::fromUtf8(ChangeLogTable::staticMetaObject.className()),
QStringLiteral(__CHANGE_LOG_TABLE_NAME));
changeLogs = new TableSet(q);
for (int i = 0; i < q->metaObject()->classInfoCount(); i++) {
QString type;
QString name;
QString value;
if (!nutClassInfoString(q->metaObject()->classInfo(i),
type, name, value)) {
errorMessage = QStringLiteral("No valid table in %1")
.arg(q->metaObject()->classInfo(i).value());
continue;
}
if (type == QStringLiteral(__nut_TABLE)) {
//name: table class name
//value: table variable name (table name in db)
tables.insert(name, value);
int typeId = QMetaType::type(name.toLocal8Bit() + "*");
if (!typeId)
qFatal("The class %s is not registered with qt meta object", qPrintable(name));
TableModel *sch = new TableModel(typeId, value);
currentModel.append(sch);
}
if (type == QStringLiteral(__nut_DB_VERSION)) {
bool ok;
int version = value.toInt(&ok);
if (!ok)
qFatal("NUT_DB_VERSION macro accept version in format 'x'");
currentModel.setVersion(version);
}
}
for (int i = 1; i < q->metaObject()->propertyCount(); i++) {
QMetaProperty tableProperty = q->metaObject()->property(i);
int typeId = QMetaType::type(tableProperty.typeName());
if (tables.values().contains(QString::fromUtf8(tableProperty.name()))
&& (unsigned)typeId >= QVariant::UserType) {
TableModel *sch = new TableModel(typeId, QString::fromUtf8(tableProperty.name()));
currentModel.append(sch);
}
}
foreach (TableModel *table, currentModel) {
foreach (FieldModel *f, table->fields()) {
if (f->isPrimaryKey && ! sqlGenerator->supportPrimaryKey(f->type))
qFatal("The field of type %s does not support as primary key",
qPrintable(f->typeName));
if (f->isAutoIncrement && ! sqlGenerator->supportAutoIncrement(f->type))
qFatal("The field of type %s does not support as auto increment",
qPrintable(f->typeName));
}
foreach (RelationModel *fk, table->foreignKeys())
fk->masterTable = currentModel.tableByClassName(fk->masterClassName);
}
allTableMaps.insert(QString::fromUtf8(q->metaObject()->className()), currentModel);
return true;
}
DatabaseModel DatabasePrivate::getLastSchema()
{
Row u = changeLogs->query()
->orderBy(!ChangeLogTable::idField())
->first();
// DatabaseModel ret(q->metaObject()->className());
if (u) {
QJsonParseError e;
QJsonObject json = QJsonDocument::fromJson(u->data()
.replace(QStringLiteral("\\\""),
QStringLiteral("\""))
.toUtf8(),
&e)
.object();
DatabaseModel ret = json;
return ret;
/*
foreach (QString key, json.keys()) {
TableModel *sch = new TableModel(json.value(key).toObject(), key);
ret.append(sch);
}*/
}
return DatabaseModel();
// QSqlQuery query = q->exec("select * from __change_logs order by id
// desc limit 1");
// DatabaseModel ret;
// if(query.next()){
// QJsonObject json =
// QJsonDocument::fromJson(query.value("data").toByteArray()).object();
// foreach (QString key, json.keys()) {
// TableModel *sch = new TableModel(json.value(key).toObject(),
// key);
// ret.append(sch);
// }
// }
// return ret;
}
bool DatabasePrivate::putModelToDatabase()
{
Q_Q(Database);
DatabaseModel current = currentModel;
/*current.remove(__CHANGE_LOG_TABLE_NAME)*/;
auto changeLog = create();
changeLog->setData(QString(QJsonDocument(current.toJson()).toJson(QJsonDocument::Compact)));
changeLog->setVersion(current.version());
changeLogs->append(changeLog);
q->saveChanges(true);
changeLog->deleteLater();
return true;
// QSqlQuery query(db);
// query.prepare("insert into __change_logs (data) values (:data)");
// query.bindValue(":data",
// QString(QJsonDocument(currentModel.toJson()).toJson()));
// bool ret = query.exec();
// if(query.lastError().type() != QSqlError::NoError)
// qWarning(QString("storeSchemaInDB" +
// query.lastError().text()).toLatin1().data());
// return ret;
}
void DatabasePrivate::createChangeLogs()
{
// currentModel.model("change_log")
QStringList diff = sqlGenerator->diff(nullptr, currentModel.tableByName(QStringLiteral("__change_log")));
foreach (QString s, diff)
db.exec(s);
}
/*!
* \class Database
* \brief Database class
*/
Database::Database(QObject *parent)
: QObject(parent), d_ptr(new DatabasePrivate(this))
{
// _d = new QSharedDataPointer(new DatabasePrivate(this));
DatabasePrivate::lastId++;
}
Database::Database(const Database &other)
: QObject(other.parent()), d_ptr(new DatabasePrivate(this))
{
DatabasePrivate::lastId++;
// _d = other._d;
setDriver(other.driver());
setHostName(other.hostName());
setPort(other.port());
setDatabaseName(other.databaseName());
setUserName(other.userName());
setPassword(other.password());
}
Database::Database(const QSqlDatabase &other)
{
//TODO: make a polish here
DatabasePrivate::lastId++;
// setDriver(other.driver());
setHostName(other.hostName());
setPort(other.port());
setDatabaseName(other.databaseName());
setUserName(other.userName());
setPassword(other.password());
qRegisterMetaType();
}
Database::~Database()
{
Q_D(Database);
if (d->db.isOpen())
d->db.close();
delete d_ptr;
}
QString Database::databaseName() const
{
Q_D(const Database);
return d->databaseName;
}
QString Database::hostName() const
{
Q_D(const Database);
return d->hostName;
}
int Database::port() const
{
Q_D(const Database);
return d->port;
}
QString Database::userName() const
{
Q_D(const Database);
return d->userName;
}
QString Database::password() const
{
Q_D(const Database);
return d->password;
}
/*!
* \brief Database::connectionName
* \return Connection name of current Database \l QSqlDatabase::connectionName
*/
QString Database::connectionName() const
{
Q_D(const Database);
return d->connectionName;
}
QString Database::driver() const
{
Q_D(const Database);
return d->driver;
}
/*!
* \brief Database::model
* \return The model of this database
*/
DatabaseModel Database::model() const
{
Q_D(const Database);
return d->currentModel;
}
QString Database::tableName(QString className)
{
TableModel *m = model().tableByClassName(className);
if (m)
return m->name();
else
return QString();
}
void Database::setDatabaseName(QString databaseName)
{
Q_D(Database);
d->databaseName = databaseName;
}
void Database::setHostName(QString hostName)
{
Q_D(Database);
d->hostName = hostName;
}
void Database::setPort(int port)
{
Q_D(Database);
d->port = port;
}
void Database::setUserName(QString username)
{
Q_D(Database);
d->userName = username;
}
void Database::setPassword(QString password)
{
Q_D(Database);
d->password = password;
}
void Database::setConnectionName(QString connectionName)
{
Q_D(Database);
d->connectionName = connectionName;
}
void Database::setDriver(QString driver)
{
Q_D(Database);
d->driver = driver.toUpper();
}
SqlGeneratorBase *Database::sqlGenerator() const
{
Q_D(const Database);
return d->sqlGenerator;
}
QSqlDatabase Database::database()
{
Q_D(Database);
return d->db;
}
void Database::databaseCreated()
{
}
void Database::databaseUpdated(int oldVersion, int newVersion)
{
Q_UNUSED(oldVersion)
Q_UNUSED(newVersion)
}
/**
* @brief Database::open
* Opens the database connection using the current connection values.
* Returns true on success; otherwise returns false.
* @return bool
*/
bool Database::open()
{
return open(true);
}
bool Database::open(bool updateDatabase)
{
Q_D(Database);
if (d->driver == QStringLiteral("QPSQL") || d->driver == QStringLiteral("QPSQL7"))
d->sqlGenerator = new PostgreSqlGenerator(this);
else if (d->driver == QStringLiteral("QMYSQL") || d->driver == QStringLiteral("QMYSQL3"))
d->sqlGenerator = new MySqlGenerator(this);
else if (d->driver == QStringLiteral("QSQLITE") || d->driver == QStringLiteral("QSQLITE3"))
d->sqlGenerator = new SqliteGenerator(this);
else if (d->driver == QStringLiteral("QODBC") || d->driver == QStringLiteral("QODBC3")) {
QString driverName = QString();
QStringList parts = d->databaseName.toLower().split(';');
foreach (QString p, parts)
if (p.trimmed().startsWith(QStringLiteral("driver=")))
driverName = p.split('=').at(1).toLower().trimmed();
// if (driverName == "{sql server}")
d->sqlGenerator = new SqlServerGenerator(this);
// TODO: add ODBC driver for mysql, postgres, ...
}
if (!d->sqlGenerator) {
qFatal("Sql generator for driver %s not found",
driver().toLatin1().constData());
}
return d->open(updateDatabase);
}
void Database::close()
{
Q_D(Database);
d->db.close();
}
QSqlQuery Database::exec(const QString &sql)
{
Q_D(Database);
QSqlQuery q = d->db.exec(sql);
if (d->db.lastError().type() != QSqlError::NoError)
qWarning("Error executing sql command: %s; Command=%s",
d->db.lastError().text().toLatin1().data(),
sql.toUtf8().constData());
return q;
}
void Database::add(TableSetBase *t)
{
Q_D(Database);
d->tableSets.insert(t);
}
int Database::saveChanges(bool cleanUp)
{
Q_D(Database);
if (!d->db.isOpen()) {
qWarning("Database is not open");
return 0;
}
int rowsAffected = 0;
foreach (TableSetBase *ts, d->tableSets)
rowsAffected += ts->save(this, cleanUp);
return rowsAffected;
}
void Database::cleanUp()
{
Q_D(Database);
foreach (TableSetBase *ts, d->tableSets)
ts->clearChilds();
}
NUT_END_NAMESPACE