Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions libqf/libqfcore/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ add_library(libqfcore SHARED
src/sql/query.cpp
src/sql/querybuilder.cpp
src/sql/transaction.cpp
src/sql/qxrecchng.h src/sql/qxrecchng.cpp
src/sql/qxsql.h src/sql/qxsql.cpp

src/utils/clioptions.cpp
src/utils/crypt.cpp
Expand Down
1 change: 1 addition & 0 deletions libqf/libqfcore/include/qf/core/sql/qxrecchng.h
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#include "../../../../src/sql/qxrecchng.h"
1 change: 1 addition & 0 deletions libqf/libqfcore/include/qf/core/sql/qxsql.h
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#include "../../../../src/sql/qxsql.h"
53 changes: 53 additions & 0 deletions libqf/libqfcore/src/sql/qxrecchng.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include "qxrecchng.h"

#include "../core/log.h"

namespace qf::core::sql {
namespace {
RecOp recopFromString(const QString &s)
{
if (s == "Insert") return RecOp::Insert;
if (s == "Delete") return RecOp::Delete;
if (s == "Update") return RecOp::Update;
qfWarning() << "Undefined record operation:" << s;
return RecOp::Update;
}
}
QVariantMap QxRecChng::toVariantMap() const
{
QVariantMap ret;
ret["table"] = table;
ret["op"] = recopToString(op);
ret["id"] = id;
if (op != RecOp::Delete) {
ret["record"] = record;
}
if (!issuer.isEmpty()) {
ret["issuer"] = issuer;
}
return ret;
}

QxRecChng QxRecChng::fromVariantMap(const QVariantMap &m)
{
QxRecChng ret;
ret.table = m.value("table").toString();
ret.op = recopFromString(m.value("op").toString());
ret.id = m.value("id").value<qint64>();
if (ret.op != RecOp::Delete) {
ret.record = m.value("record").toMap();
}
ret.issuer = m.value("issuer").toString();
return ret;
}

QString QxRecChng::recopToString(RecOp op)
{
switch (op) {
case RecOp::Insert: return "Insert";
case RecOp::Update: return "Update";
case RecOp::Delete: return "Delete";
}
}

}
28 changes: 28 additions & 0 deletions libqf/libqfcore/src/sql/qxrecchng.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#pragma once

#include "../core/coreglobal.h"

#include <QString>
#include <QVariantMap>

namespace qf::core::sql {

enum class RecOp {
Insert,
Update,
Delete,
};

struct QFCORE_DECL_EXPORT QxRecChng
{
QString table;
qint64 id = 0;
QVariantMap record;
RecOp op = RecOp::Update;
QString issuer;

QVariantMap toVariantMap() const;
static QxRecChng fromVariantMap(const QVariantMap &m);
static QString recopToString(RecOp op);
};
}
192 changes: 192 additions & 0 deletions libqf/libqfcore/src/sql/qxsql.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#include "qxsql.h"

#include <qf/core/exception.h>

#include <QSqlQuery>
#include <QSqlError>
#include <QSqlRecord>

namespace qf::core::sql {

//================================================================
// QxSqlApi
//================================================================
qint64 QxSqlApi::createRecord(const QString &table, const Record &record, const QString &issuer)
{
Q_UNUSED(issuer)
QStringList keys;
QStringList vals;

for (auto it = record.begin(); it != record.end(); ++it) {
keys << it.key();
vals << (":" + it.key());
}

QString qs = QString("INSERT INTO %1 (%2) VALUES (%3) RETURNING id")
.arg(table, keys.join(", "), vals.join(", "));

QueryResult result = query(qs, record);

if (result.rows.isEmpty() || result.rows[0].isEmpty()) {
throw qf::core::Exception("Insert should return an ID");
}

bool ok = false;
qint64 id = result.rows[0][0].toLongLong(&ok);

if (!ok) {
throw qf::core::Exception("Insert should return an integer ID");
}

return id;
}

std::optional<Record> QxSqlApi::readRecord(const QString &table, qint64 id, const std::optional<QStringList> &fields)
{
auto records = listOneOrMoreRecords(table, fields, id, 1);
if (!records.isEmpty()) {
return records.first();
}
return std::nullopt;
}

bool QxSqlApi::updateRecord(const QString &table, qint64 id, const Record &record, const QString &issuer)
{
Q_UNUSED(issuer)
QStringList keyVals;

for (auto it = record.begin(); it != record.end(); ++it) {
keyVals << QString("%1 = :%1").arg(it.key());
}

QString qs = QString("UPDATE %1 SET %2 WHERE id = %3")
.arg(table, keyVals.join(", "), QString::number(id));

ExecResult res = exec(qs, record);
return res.rowsAffected == 1;
}

bool QxSqlApi::deleteRecord(const QString &table, qint64 id, const QString &issuer)
{
Q_UNUSED(issuer)
QString qs = QString("DELETE FROM %1 WHERE id = %2").arg(table, QString::number(id));

ExecResult res = exec(qs, {});
return res.rowsAffected == 1;
}

QList<Record> QxSqlApi::listOneOrMoreRecords(const QString &table, const std::optional<QStringList> &fields, const std::optional<qint64> &id, const std::optional<qint64> &limit)
{
QString fieldsStr =
(fields && !fields->isEmpty()) ? fields->join(", ") : "*";

QString qs = QString("SELECT %1 FROM %2").arg(fieldsStr, table);

if (id.has_value()) {
qs += QString(" WHERE id >= %1").arg(*id);
}

if (limit.has_value()) {
qs += QString(" LIMIT %1").arg(*limit);
}

QueryResult result = query(qs, {});

QList<Record> records;
records.reserve(result.rows.size()); // still valid for QList (Qt 6)

for (int i = 0; i < result.rows.size(); ++i) {
auto rec = result.record(i);
if (rec.has_value()) {
records.append(*rec);
}
}

return records;
}

//================================================================
// QxSql
//================================================================
QxSql::QxSql(const QSqlDatabase &db)
: m_db(db)
{

}

QueryResult QxSql::query(const QString &query, const QVariantMap &params)
{
QSqlQuery q(m_db);
q.prepare(query);
for (const auto &[key, val] : params.asKeyValueRange()) {
q.bindValue(QStringLiteral(":%1").arg(key), val);
}
if (!q.exec()) {
throw qf::core::Exception(q.lastError().text());
}
QueryResult result;
for (int i = 0; i < q.record().count(); ++i) {
result.columns.append(q.record().fieldName(i).toLower());
}
while (q.next()) {
QList<QVariant> row;
for (int i = 0; i < q.record().count(); ++i) {
row.append(q.value(i));
}
result.rows.insert(result.rows.size(), row);
}
return result;
}

ExecResult QxSql::exec(const QString &query, const QVariantMap &params)
{
QSqlQuery q(m_db);
q.prepare(query);
for (const auto &[key, val] : params.asKeyValueRange()) {
q.bindValue(QStringLiteral(":%1").arg(key), val);
}
if (!q.exec()) {
throw qf::core::Exception(q.lastError().text());
}
ExecResult result;
result.rowsAffected = q.numRowsAffected();
return result;
}

qint64 QxSql::createRecord(const QString &table, const Record &record, const QString &issuer)
{
auto id = QxSqlApi::createRecord(table, record, issuer);
emit dbRecChng(qf::core::sql::QxRecChng{.table = table,
.id = id,
.record = record,
.op = qf::core::sql::RecOp::Insert,
.issuer = issuer});
return id;
}

bool QxSql::updateRecord(const QString &table, qint64 id, const Record &record, const QString &issuer)
{
auto ok = QxSqlApi::updateRecord(table, id, record, issuer);
if (ok) {
emit dbRecChng(qf::core::sql::QxRecChng{.table = table,
.id = id,
.record = record,
.op = qf::core::sql::RecOp::Update,
.issuer = issuer});
}
return ok;
}

bool QxSql::deleteRecord(const QString &table, qint64 id, const QString &issuer)
{
auto ok = QxSqlApi::deleteRecord(table, id, issuer);
if (ok) {
emit dbRecChng(qf::core::sql::QxRecChng{.table = table,
.id = id,
.record = {},
.op = qf::core::sql::RecOp::Delete,
.issuer = issuer});
}
return ok;
}
}
89 changes: 89 additions & 0 deletions libqf/libqfcore/src/sql/qxsql.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#pragma once

#include "qxrecchng.h"

#include <QList>
#include <QMap>
#include <QString>
#include <QStringList>
#include <QVariant>
#include <QSqlDatabase>

#include <optional>

namespace qf::core::sql {

using Record = QVariantMap;

struct QFCORE_DECL_EXPORT QueryResult
{
QStringList columns;
QList<QList<QVariant>> rows;

std::optional<Record> record(int i) const
{
if (i < 0 || i >= rows.size()) {
return std::nullopt;
}
Record r;
const auto &row = rows[i];
for (int j = 0; j < columns.size(); ++j) {
r[columns[j]] = row.value(j);
}
return r;
}
};

struct QFCORE_DECL_EXPORT ExecResult
{
qint64 rowsAffected = 0;
};

class QFCORE_DECL_EXPORT QxSqlApi
{
public:
virtual ~QxSqlApi() = default;
virtual QueryResult query(const QString &query, const QVariantMap &params) = 0;
virtual ExecResult exec(const QString &query, const QVariantMap &params) = 0;

QList<Record> listRecords(
const QString &table,
const std::optional<QStringList> &fields = std::nullopt,
const std::optional<qint64> &fromId = std::nullopt,
const std::optional<qint64> &limit = std::nullopt)
{
return listOneOrMoreRecords(table, fields, fromId, limit);
}

virtual qint64 createRecord(const QString &table, const Record &record, const QString &issuer);
virtual std::optional<Record> readRecord(const QString &table, qint64 id, const std::optional<QStringList> &fields = std::nullopt);
virtual bool updateRecord(const QString &table, qint64 id, const Record &record, const QString &issuer);
virtual bool deleteRecord(const QString &table, qint64 id, const QString &issuer);
protected:
QList<Record> listOneOrMoreRecords(
const QString &table,
const std::optional<QStringList> &fields,
const std::optional<qint64> &id,
const std::optional<qint64> &limit);
};

class QFCORE_DECL_EXPORT QxSql : public QObject, public QxSqlApi
{
Q_OBJECT
public:
QxSql(const QSqlDatabase &db = QSqlDatabase());
~QxSql() override = default;

Q_SIGNAL void dbRecChng(const qf::core::sql::QxRecChng &recchng);

QueryResult query(const QString &query, const QVariantMap &params) override;
ExecResult exec(const QString &query, const QVariantMap &params) override;

qint64 createRecord(const QString &table, const Record &record, const QString &issuer) override;
bool updateRecord(const QString &table, qint64 id, const Record &record, const QString &issuer) override;
bool deleteRecord(const QString &table, qint64 id, const QString &issuer) override;
private:
QSqlDatabase m_db;
};

} // namespace qf::core::sql
Loading
Loading