/*
 * Copyright (C) 2014-2024 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <QDir>
#include <QFileInfo>
#include <QMutexLocker>
#include <QSqlError>
#include <QSqlQuery>
#include <QVariant>

#include "src/datovka_shared/io/sqlite/db.h"
#include "src/datovka_shared/io/sqlite/table.h"
#include "src/datovka_shared/log/log.h"

const QString SQLiteDb::memoryLocation(":memory:");
const QString SQLiteDb::dbDriverType("QSQLITE");

/*!
 * @brief Make an SQL operation which does not return any query result.
 *
 * @param[in] db SQLite database object.
 * @param[in] queryStr String with SQL query.
 * @return True on success, false on failure.
 */
static
bool queryOperation(QSqlDatabase &db, const QString &queryStr)
{
	QSqlQuery query(db);

	if (Q_UNLIKELY(queryStr.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	return true;
}

/*!
 * @brief Make a query whose result is a single number.
 *
 * @param[in] db SQLite database object.
 * @param[in] queryStr String with SQL query.
 * @param[out] ok Value is set to false on any error.
 * @return Result number. Returns -1 on error.
 */
static
qint64 queryNumber(QSqlDatabase &db, const QString &queryStr,
    bool *ok = Q_NULLPTR)
{
	QSqlQuery query(db);
	qint64 num = -1;

	if (Q_UNLIKELY(queryStr.isEmpty())) {
		Q_ASSERT(0);
		goto fail;
	}

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL command: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			bool iOk = false;
			num = query.value(0).toLongLong(&iOk);
			if (Q_UNLIKELY(!iOk)) {
				goto fail;
			}
		} else {
			goto fail;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (ok != Q_NULLPTR) {
		*ok = true;
	}
	return num;

fail:
	if (ok != Q_NULLPTR) {
		*ok = false;
	}
	return -1;
}

/*!
 * @brief Attaches a database file to opened database.
 *
 * @param[in,out] query Query to work with.
 * @param[in]     fileName File containing database to be attached.
 * @return False on error.
 */
static
bool queryAttach(QSqlQuery &query, const QString &fileName)
{
	QString queryStr("ATTACH DATABASE :fileName AS " DB2);
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":fileName", fileName);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return true;

fail:
	return false;
}

/*!
 * @brief Detaches attached database file from opened database.
 *
 * @param[in,out] query Query to work with.
 * @return False on error.
 */
static
bool queryDetach(QSqlQuery &query)
{
	QString queryStr("DETACH DATABASE " DB2);
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return true;

fail:
	return false;
}

SQLiteDb::SQLiteDb(const QString &connectionName)
    : m_db(),
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
    m_lock() /* Is QRecursiveMutex. Allow multiple locks from a single thread. */
#else /* < Qt-5.14 */
    m_lock(QMutex::Recursive) /* Allow multiple locks from a single thread. */
#endif /* >= Qt-5.14 */
{
	m_db = QSqlDatabase::addDatabase(dbDriverType, connectionName);
}

SQLiteDb::~SQLiteDb(void)
{
	m_db.close();
	const QString connectionName = m_db.connectionName();
	if (!connectionName.isEmpty()) {
		/*
		 * There should be no open connection and queries when removing
		 * a connection.
		 */
		m_db = QSqlDatabase();
		QSqlDatabase::removeDatabase(connectionName);
	}
}

QString SQLiteDb::fileName(void) const
{
	return m_db.databaseName();
}

#define queryBeginTransaction(db) \
	queryOperation((db), "BEGIN EXCLUSIVE TRANSACTION")

#define queryCommitTransaction(db) \
	queryOperation((db), "COMMIT TRANSACTION")

#define queryRollbackTransaction(db) \
	queryOperation((db), "ROLLBACK TRANSACTION")

bool SQLiteDb::beginTransaction(void)
{
	return queryBeginTransaction(m_db);
}

bool SQLiteDb::commitTransaction(void)
{
	return queryCommitTransaction(m_db);
}

bool SQLiteDb::savePoint(const QString &savePointName)
{
	if (Q_UNLIKELY(savePointName.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}

	QSqlQuery query(m_db);
	QString queryStr("SAVEPOINT :name");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", savePointName);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	return true;
}

bool SQLiteDb::releaseSavePoint(const QString &savePointName)
{
	if (Q_UNLIKELY(savePointName.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}

	QSqlQuery query(m_db);
	QString queryStr("RELEASE SAVEPOINT :name");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", savePointName);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	return true;
}

bool SQLiteDb::rollbackTransaction(const QString &savePointName)
{
	if (savePointName.isEmpty()) {
		return queryRollbackTransaction(m_db);
	} else {
		QSqlQuery query(m_db);

		QString queryStr("ROLLBACK TRANSACTION TO SAVEPOINT :name");
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			return false;
		}
		query.bindValue(":name", savePointName);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			return false;
		}
		return true;
	}
}

void SQLiteDb::lock(void)
{
	m_lock.lock();
}

void SQLiteDb::unlock(void)
{
	m_lock.unlock();
}

bool SQLiteDb::dbDriverSupport(void)
{
	QStringList driversList = QSqlDatabase::drivers();
	return driversList.contains(dbDriverType, Qt::CaseSensitive);
}

void SQLiteDb::closeDb(void)
{
	m_db.close();
}

bool SQLiteDb::checkDb(bool quick)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(m_db);
	bool ret = false;

	QString queryStr;
	if (quick) {
		queryStr = "PRAGMA quick_check";
	} else {
		queryStr = "PRAGMA integrity_check";
	}
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			ret = query.value(0).toBool();
		} else {
			ret = false;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return ret;

fail:
	return false;
}

bool SQLiteDb::vacuum(void)
{
	return queryOperation(m_db, "VACUUM");
}

/*
 * To simulate mysql's @SHOW CREATE TABLE;@ query you cane use:
 * select `sql` from sqlite_master WHERE tbl_name = 'your_table_name'
 *
 * To list table names you can use:
 * SELECT name FROM sqlite_master WHERE type='table';
 *
 * To copy content attach a second database and use:
 * INSERT INTO your_table_name SELECT * FROM second.your_table_name;
 */

/*!
 * @brief Copy the entire database structure.
 *
 * @param[in]     query SQL query result where the string entries contain SQLite
 *                      'CREATE TABLE' statements.
 * @param[in,out] tgtDb Target database where to create the tables in.
 * @return True on success.
 */
static
bool createDatabaseStructure(QSqlQuery &query, QSqlDatabase &tgtDb)
{
	if (Q_UNLIKELY((!query.isActive()) || (!tgtDb.isOpen()))) {
		Q_ASSERT(0);
		return false;
	}

	if (Q_UNLIKELY(!queryBeginTransaction(tgtDb))) {
		return false;
	}

	query.first();
	while (query.isValid()) {
		QString queryStr(query.value(0).toString());
		if (Q_UNLIKELY(!queryOperation(tgtDb, queryStr))) {
			goto fail;
		}
		query.next();
	}

	if (Q_UNLIKELY(!queryCommitTransaction(tgtDb))) {
		goto fail;
	}

	return true;

fail:
	queryRollbackTransaction(tgtDb);
	return false;
}

/*!
 * @brief Copy database structure.
 *
 * @note The target database must be empty.
 *
 * @param[in]     srcDb Source database.
 * @param[in,out] tgtDb Target database where to create the tables in.
 * @return True on success.
 */
static
bool copyDatabaseStructure(const QSqlDatabase &srcDb, QSqlDatabase &tgtDb)
{
	if (Q_UNLIKELY((!srcDb.isOpen()) || (!tgtDb.isOpen()))) {
		Q_ASSERT(0);
		return false;
	}

	QSqlQuery query(srcDb);

	QString srcQueryStr("SELECT sql FROM sqlite_master WHERE type = :type");
	if (Q_UNLIKELY(!query.prepare(srcQueryStr))) {
		logErrorNL("Cannot prepare SQL command: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":type", "table");
	if (query.exec() && query.isActive()) {
		if (Q_UNLIKELY(!createDatabaseStructure(query, tgtDb))) {
			goto fail;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return true;

fail:
	return false;
}

/*!
 * @brief Copy database table.
 *
 * @note The back-up table us attached into the existing database.
 *
 * @param[in] query Query result containing table names of the source database.
 * @param[in] srcDb Source database.
 * @param[in] fileName Back-up database file name.
 * @return True on success, false on failure.
 */
static
bool copyDatabaseTables(QSqlQuery &query, QSqlDatabase &srcDb,
    const QString &fileName)
{
	if (Q_UNLIKELY((!query.isActive()) || (!srcDb.isOpen()) ||
	               fileName.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}

	QSqlQuery iQuery(srcDb); /* Internal query. */

	if (Q_UNLIKELY(!queryAttach(iQuery, fileName))) {
		goto fail;
	}

	if (Q_UNLIKELY(!queryBeginTransaction(srcDb))) {
		goto fail_detach;
	}

	/* Don't confuse query with iQuery. */
	query.first();
	while (query.isValid()) {
		const QString tableName(query.value(0).toString());
		QString queryStr("INSERT INTO " DB2 ".%1 SELECT * FROM %1");
		if (Q_UNLIKELY(!queryOperation(srcDb, queryStr.arg(tableName)))) {
			goto fail_detach;
		}
		query.next();
	}

	if (Q_UNLIKELY(!queryCommitTransaction(srcDb))) {
		goto fail_detach;
	}

	if (Q_UNLIKELY(!queryDetach(iQuery))) {
		goto fail;
	}

	return true;

fail_detach:
	queryRollbackTransaction(srcDb);
	queryDetach(iQuery);
fail:
	return false;
}

/*!
 * @brief Copy the database content.
 *
 * @note The target database must have its structure already constructed.
 *
 * @param[in] srcDb Source database.
 * @param[in] fileName Back-up database file name.
 * @return True on success, false on failure.
 */
static
bool copyDatabaseContent(QSqlDatabase &srcDb, const QString &fileName)
{
	if (Q_UNLIKELY((!srcDb.isOpen()) || fileName.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}

	QSqlQuery query(srcDb);

	/* Copy database content. */
	QString srcQueryStr("SELECT name FROM sqlite_master WHERE type = :type");
	if (Q_UNLIKELY(!query.prepare(srcQueryStr))) {
		logErrorNL("Cannot prepare SQL command: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":type", "table");
	if (query.exec() && query.isActive()) {
		if (Q_UNLIKELY(!copyDatabaseTables(query, srcDb, fileName))) {
			goto fail;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return true;

fail:
	return false;
}

bool SQLiteDb::backup(const QString &fileName)
{
	if (Q_UNLIKELY(fileName.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}

	/* Erase target if exists. */
	QFile::remove(fileName);

	QSqlDatabase backupDb(
	    QSqlDatabase::addDatabase(dbDriverType, "BACKUP_FILE_DATABASE"));

	backupDb.setDatabaseName(QDir::toNativeSeparators(fileName));

	QMutexLocker locker(&m_lock);

	if (Q_UNLIKELY(!backupDb.open())) {
		logErrorNL("Cannot open back-up database file '%s'.",
		    fileName.toUtf8().constData());
		goto fail;
	}

	if (Q_UNLIKELY(!copyDatabaseStructure(m_db, backupDb))) {
		logErrorNL("Cannot copy database structure into back-up file '%s.'",
		    fileName.toUtf8().constData());
		goto fail;
	}

	backupDb.close();

	if (Q_UNLIKELY(!copyDatabaseContent(m_db, fileName))) {
		logErrorNL("Cannot copy database content into back-up file '%s'.",
		    fileName.toUtf8().constData());
		goto fail;
	}

	return true;

fail:
	if (backupDb.isOpen()) {
		backupDb.close();
	}
	QFile::remove(fileName); /* Erase incomplete target. */
	return false;
}

qint64 SQLiteDb::dbSize(void)
{
	QMutexLocker locker(&m_lock);

	qint64 pageSize = -1;
	qint64 pageCount = -1;
	bool iOk = false;
	pageSize = queryNumber(m_db, "PRAGMA page_size", &iOk);
	if (iOk) {
		pageCount = queryNumber(m_db, "PRAGMA page_count", &iOk);
	}

	if (iOk) {
		return pageSize * pageCount;
	} else {
		return -1;
	}
}

qint64 SQLiteDb::fileSize(void) const
{
	const QString fName(fileName());
	if (fName == memoryLocation) {
		return -1;
	}
	const QFileInfo fileInfo(fName);
	if (fileInfo.exists()) {
		return fileInfo.size();
	} else {
		return 0;
	}
}

bool SQLiteDb::assureConsistency(void)
{
	logInfoNL("No consistency checks performed in database '%s'.",
	    fileName().toUtf8().constData());
	return true;
}

bool SQLiteDb::enableFunctionality(void)
{
	logInfoNL(
	    "Using default behaviour of the SQLite engine for database '%s'.",
	    fileName().toUtf8().constData());
	return true;
}

bool SQLiteDb::copyDb(const QString &newFileName, enum OpenFlag flag)
{
	if (Q_UNLIKELY(newFileName.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}
	if (Q_UNLIKELY(fileName() == memoryLocation)) {
		logErrorNL("%s",
		    "Trying to copy database that resides in memory.");
		Q_ASSERT(0);
		return false;
	}
	if (Q_UNLIKELY(newFileName == memoryLocation)) {
		/* TODO -- handle this situation. */
		Q_ASSERT(0);
		return false;
	}
	if (Q_UNLIKELY(flag & ~CREATE_MISSING)) {
		logErrorNL("Unsupported flag '%d'.", flag);
		Q_ASSERT(0);
		return false;
	}

	/* Back up old file name. */
	QString oldFileName = fileName();

	/* Fail if target equals the source. */
	/* TODO -- Perform a more reliable check than string comparison. */
	if (Q_UNLIKELY(oldFileName == newFileName)) {
		logWarningNL(
		    "Copying of database file '%s' aborted. Target and source are equal.",
		    oldFileName.toUtf8().constData());
		return false;
	}

	bool copy_ret, open_ret;

	/* Close database. */
	m_db.close();

	logInfoNL("Copying database file '%s' to location '%s'.",
	    oldFileName.toUtf8().constData(),
	    newFileName.toUtf8().constData());

	/* Erase target if exists. */
	QFile::remove(newFileName);

	/* Copy database file. */
	copy_ret = QFile::copy(oldFileName, newFileName);

	/* Open database. */
	open_ret = openDb(copy_ret ? newFileName : oldFileName, flag);
	if (Q_UNLIKELY(!open_ret)) {
		logErrorNL("File '%s' could not be opened.",
		    copy_ret ?
		        newFileName.toUtf8().constData() :
		        oldFileName.toUtf8().constData());
		Q_ASSERT(0);
		/* TODO -- qFatal() ? */
		return false;
	}

	return copy_ret;
}

bool SQLiteDb::reopenDb(const QString &newFileName, enum OpenFlag flag)
{
	if (Q_UNLIKELY(newFileName.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}
	if (Q_UNLIKELY(fileName() == memoryLocation)) {
		logErrorNL("%s",
		    "Trying to reopen database that resides in memory.");
		Q_ASSERT(0);
		return false;
	}
	if (Q_UNLIKELY(newFileName == memoryLocation)) {
		/* TODO -- handle this situation. */
		Q_ASSERT(0);
		return false;
	}
	if (Q_UNLIKELY(flag & ~CREATE_MISSING)) {
		logErrorNL("Unsupported flag '%d'.", flag);
		Q_ASSERT(0);
		return false;
	}

	/* Back up old file name. */
	QString oldFileName = fileName();

	/* Fail if target equals the source. */
	/* TODO -- Perform a more reliable check than string comparison. */
	if (Q_UNLIKELY(oldFileName == newFileName)) {
		logWarningNL(
		    "Re-opening of database file '%s' aborted. Target and source are equal.",
		    oldFileName.toUtf8().constData());
		return false;
	}

	bool reopen_ret, open_ret;

	/* Close database. */
	m_db.close();

	logInfoNL("Closing database file '%s' re-opening file '%s'.",
	    oldFileName.toUtf8().constData(),
	    newFileName.toUtf8().constData());

	/* Erase target if exists. */
	QFile::remove(newFileName);

	/* Open new database file. */
	reopen_ret = openDb(newFileName, flag);

	/* Open database. */
	if (!reopen_ret) {
		open_ret = openDb(oldFileName, flag);
		if (Q_UNLIKELY(!open_ret)) {
			logErrorNL("File '%s' could not be opened.",
			    oldFileName.toUtf8().constData());
			/* TODO -- qFatal() ? */
			return false;
		}
	}

	return reopen_ret;
}

bool SQLiteDb::moveDb(const QString &newFileName, enum OpenFlag flag)
{
	if (Q_UNLIKELY(newFileName.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}
	if (Q_UNLIKELY(fileName() == memoryLocation)) {
		logErrorNL("%s",
		    "Trying to move database that resides in memory.");
		Q_ASSERT(0);
		return false;
	}
	if (Q_UNLIKELY(newFileName == memoryLocation)) {
		/* TODO -- handle this situation. */
		Q_ASSERT(0);
		return false;
	}
	if (Q_UNLIKELY(flag & ~CREATE_MISSING)) {
		logErrorNL("Unsupported flag '%d'.", flag);
		Q_ASSERT(0);
		return false;
	}

	/* Back up old file name. */
	QString oldFileName = fileName();

	/* Fail if target equals the source. */
	/* TODO -- Perform a more reliable check than string comparison. */
	if (Q_UNLIKELY(oldFileName == newFileName)) {
		logWarningNL(
		    "Moving of database file '%s' aborted. Target and source are equal.",
		    oldFileName.toUtf8().constData());
		return false;
	}

	bool move_ret, open_ret;

	/* Close database. */
	m_db.close();

	logInfoNL("Moving database file '%s' to location '%s'.",
	    oldFileName.toUtf8().constData(),
	    newFileName.toUtf8().constData());

	/* Erase target if exists. */
	QFile::remove(newFileName);

	/* Move database file. */
	move_ret = QFile::rename(oldFileName, newFileName);

	/* Open database. */
	open_ret = openDb(move_ret ? newFileName : oldFileName, flag);
	if (Q_UNLIKELY(!open_ret)) {
		Q_ASSERT(0);
		logErrorNL("File '%s' could not be opened.",
		    move_ret ?
		        newFileName.toUtf8().constData() :
		        oldFileName.toUtf8().constData());
		/* TODO -- qFatal() ? */
		return false;
	}

	return move_ret;
}

bool SQLiteDb::openDb(const QString &fileName, OpenFlags flags)
{
	if (Q_UNLIKELY((!(flags & FORCE_IN_MEMORY)) && fileName.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}
	if (Q_UNLIKELY((!(flags & FORCE_IN_MEMORY)) && (fileName == memoryLocation))) {
		Q_ASSERT(0);
		return false;
	}

	if (m_db.isOpen()) {
		m_db.close();
	}

	if (!(flags & FORCE_IN_MEMORY)) {
		m_db.setDatabaseName(QDir::toNativeSeparators(fileName));
	} else {
		m_db.setDatabaseName(memoryLocation);
	}

	bool ret = m_db.open();

	if (flags & CREATE_MISSING) {
		if (ret) {
			/* Ensure database contains all tables. */
			ret = createEmptyMissingTables(listOfTables());
		}

		if (ret) {
			ret = assureConsistency();
		}
	}

	if (ret) {
		ret = enableFunctionality();
	}

	if (Q_UNLIKELY(!ret)) {
		m_db.close();
	}

	return ret;
}

bool SQLiteDb::attachDb2(QSqlQuery &query, const QString &attachFileName)
{
	return queryAttach(query, attachFileName);
}

bool SQLiteDb::detachDb2(QSqlQuery &query)
{
	return queryDetach(query);
}

bool SQLiteDb::createEmptyMissingTables(const QList<class SQLiteTbl *> &tables)
{
	foreach (const SQLiteTbl *tblPtr, tables) {
		if (Q_UNLIKELY(Q_NULLPTR == tblPtr)) {
			Q_ASSERT(0);
			continue;
		}
		if (Q_UNLIKELY(!tblPtr->createEmpty(m_db))) {
			goto fail; /* TODO -- Proper recovery? */
		}
	}
	return true;

fail:
	return false;
}
