From dd0391b387add3ee3dc88a5298f59b3cdde19652 Mon Sep 17 00:00:00 2001 From: Dmitriy Purgin Date: Wed, 8 Oct 2025 10:46:24 +0200 Subject: [PATCH] Optimize performance by adding support for count queries. --- src/orm/qormglobal.h | 7 ++-- src/orm/qormquerybuilder.cpp | 24 ++++++++++--- src/orm/qormquerybuilder.h | 15 ++++---- src/orm/qormqueryresult.h | 27 ++++++++++++-- src/orm/qormsqliteprovider.cpp | 28 +++++++++++++-- src/orm/qormsqlitestatementgenerator_p.cpp | 17 ++++++--- tests/auto/qormsession/tst_ormsession.cpp | 36 +++++++++++++++++-- .../tst_sqlitestatementgenerator.cpp | 28 +++++++++++++-- 8 files changed, 154 insertions(+), 28 deletions(-) diff --git a/src/orm/qormglobal.h b/src/orm/qormglobal.h index ef270e7..2ff6f98 100644 --- a/src/orm/qormglobal.h +++ b/src/orm/qormglobal.h @@ -1,6 +1,6 @@ /* - * Copyright (C) 2019-2022 Dmitriy Purgin - * Copyright (C) 2019-2022 sequality software engineering e.U. + * Copyright (C) 2019-2025 Dmitriy Purgin + * Copyright (C) 2019-2025 sequality software engineering e.U. * * This file is part of QtOrm library. * @@ -108,7 +108,8 @@ namespace QOrm Read, Update, Delete, - Merge + Merge, + Count }; extern Q_ORM_EXPORT QDebug operator<<(QDebug dbg, Operation operation); diff --git a/src/orm/qormquerybuilder.cpp b/src/orm/qormquerybuilder.cpp index 024b2d1..addbaf0 100644 --- a/src/orm/qormquerybuilder.cpp +++ b/src/orm/qormquerybuilder.cpp @@ -1,6 +1,6 @@ /* - * Copyright (C) 2019 Dmitriy Purgin - * Copyright (C) 2019 sequality software engineering e.U. + * Copyright (C) 2019-2025 Dmitriy Purgin + * Copyright (C) 2019-2025 sequality software engineering e.U. * * This file is part of QtOrm library. * @@ -119,11 +119,11 @@ namespace QOrmPrivate { } - QueryBuilderHelper::QueryBuilderHelper(QueryBuilderHelper&&) = default; + QueryBuilderHelper::QueryBuilderHelper(QueryBuilderHelper&&) noexcept = default; QueryBuilderHelper::~QueryBuilderHelper() = default; - QueryBuilderHelper& QueryBuilderHelper::operator=(QueryBuilderHelper&&) = default; + QueryBuilderHelper& QueryBuilderHelper::operator=(QueryBuilderHelper&&) noexcept = default; void QueryBuilderHelper::setInstance(const QMetaObject& qMetaObject, QObject* instance) { @@ -169,7 +169,8 @@ namespace QOrmPrivate return QOrmQuery{operation, *d->m_relation.mapping(), d->m_entityInstance}; } - else if (operation == QOrm::Operation::Read || operation == QOrm::Operation::Delete) + else if (operation == QOrm::Operation::Read || operation == QOrm::Operation::Delete || + operation == QOrm::Operation::Count) { FoldedFilters filters = foldFilters(d->m_relation, d->m_filters); QOrmQuery query = QOrmQuery{operation, @@ -196,6 +197,19 @@ namespace QOrmPrivate { return d->m_session->execute(build(QOrm::Operation::Delete, QOrm::QueryFlags::None)); } + + QOrmQueryResult QueryBuilderHelper::count() const + { + QOrmQueryResult result = + d->m_session->execute(build(QOrm::Operation::Count, QOrm::QueryFlags::None)); + + if (result.hasError()) + { + return QOrmQueryResult{result.error(), result.numRowsAffected()}; + } + + return QOrmQueryResult{result.numRowsAffected()}; + } } // namespace QOrmPrivate QT_END_NAMESPACE diff --git a/src/orm/qormquerybuilder.h b/src/orm/qormquerybuilder.h index 6a1379e..9f75057 100644 --- a/src/orm/qormquerybuilder.h +++ b/src/orm/qormquerybuilder.h @@ -1,6 +1,6 @@ /* - * Copyright (C) 2019 Dmitriy Purgin - * Copyright (C) 2019 sequality software engineering e.U. + * Copyright (C) 2019-2025 Dmitriy Purgin + * Copyright (C) 2019-2025 sequality software engineering e.U. * * This file is part of QtOrm library. * @@ -51,11 +51,11 @@ namespace QOrmPrivate public: QueryBuilderHelper(QOrmSession* session, const QOrmRelation& relation); QueryBuilderHelper(const QueryBuilderHelper&) = delete; - QueryBuilderHelper(QueryBuilderHelper&&); + QueryBuilderHelper(QueryBuilderHelper&&) noexcept; ~QueryBuilderHelper(); QueryBuilderHelper& operator=(const QueryBuilderHelper&) = delete; - QueryBuilderHelper& operator=(QueryBuilderHelper&&); + QueryBuilderHelper& operator=(QueryBuilderHelper&&) noexcept; void setInstance(const QMetaObject& qMetaObject, QObject* instance); void addFilter(const QOrmFilter& filter); @@ -66,10 +66,9 @@ namespace QOrmPrivate Q_REQUIRED_RESULT QOrmQuery build(QOrm::Operation operation, QOrm::QueryFlags flags) const; - Q_REQUIRED_RESULT - QOrmQueryResult select(QOrm::QueryFlags flags) const; - + [[nodiscard]] QOrmQueryResult select(QOrm::QueryFlags flags) const; [[nodiscard]] QOrmQueryResult remove() const; + [[nodiscard]] QOrmQueryResult count() const; private: std::unique_ptr d; @@ -150,6 +149,8 @@ class QOrmQueryBuilder [[nodiscard]] QOrmQueryResult remove() { return m_helper.remove(); } + [[nodiscard]] QOrmQueryResult count() const { return m_helper.count(); } + Q_REQUIRED_RESULT QOrmQuery build(QOrm::Operation operation, QOrm::QueryFlags flags = QOrm::QueryFlags::None) const { return m_helper.build(operation, flags); } diff --git a/src/orm/qormqueryresult.h b/src/orm/qormqueryresult.h index 1ddd345..d49f95a 100644 --- a/src/orm/qormqueryresult.h +++ b/src/orm/qormqueryresult.h @@ -1,6 +1,6 @@ /* - * Copyright (C) 2019 Dmitriy Purgin - * Copyright (C) 2019 sequality software engineering e.U. + * Copyright (C) 2019-2025 Dmitriy Purgin + * Copyright (C) 2019-2025 sequality software engineering e.U. * * This file is part of QtOrm library. * @@ -240,6 +240,29 @@ class QOrmQueryResult : public QtOrmPrivate::QOrmQueryResultBase } }; +template<> +class QOrmQueryResult : public QtOrmPrivate::QOrmQueryResultBase +{ + using Base = QOrmQueryResultBase; + +public: + explicit QOrmQueryResult(int value) + : Base{QOrmError{QOrm::ErrorType::None, QString{}}, QVariant{}, 0} + , m_value{value} + { + } + + explicit QOrmQueryResult(const QOrmError& error, int numRowsAffected) + : Base{error, QVariant{}, numRowsAffected} + { + } + + [[nodiscard]] int value() const { return m_value; } + +private: + int m_value{0}; +}; + QT_END_NAMESPACE #endif diff --git a/src/orm/qormsqliteprovider.cpp b/src/orm/qormsqliteprovider.cpp index 38e4bff..3403157 100644 --- a/src/orm/qormsqliteprovider.cpp +++ b/src/orm/qormsqliteprovider.cpp @@ -1,7 +1,7 @@ /* * Copyright (C) 2020-2024 Dmitriy Purgin - * Copyright (C) 2019-2024 Dmitriy Purgin - * Copyright (C) 2019-2024 sequality software engineering e.U. + * Copyright (C) 2019-2025 Dmitriy Purgin + * Copyright (C) 2019-2025 sequality software engineering e.U. * * This file is part of QtOrm library. * @@ -103,6 +103,7 @@ class QOrmSqliteProviderPrivate QOrmQueryResult merge(const QOrmQuery& query); QOrmQueryResult remove(const QOrmQuery& query, QOrmEntityInstanceCache& entityInstanceCache); + QOrmQueryResult count(const QOrmQuery& query); [[nodiscard]] bool foreignKeysEnabled(); [[nodiscard]] QOrmError setForeignKeysEnabled(bool enabled); @@ -981,6 +982,26 @@ QOrmQueryResult QOrmSqliteProviderPrivate::remove( return QOrmQueryResult{resultSet, sqlQuery.numRowsAffected()}; } +QOrmQueryResult QOrmSqliteProviderPrivate::count(const QOrmQuery& query) +{ + Q_ASSERT(query.operation() == QOrm::Operation::Count); + + auto [statement, boundParameters] = m_statementGenerator.generate(query); + + QSqlQuery sqlQuery = prepareAndExecute(statement, boundParameters); + + if (sqlQuery.lastError().type() != QSqlError::NoError || !sqlQuery.next()) + { + return QOrmQueryResult{QOrmError{QOrm::ErrorType::Provider, + sqlQuery.lastError().text()}, + sqlQuery.numRowsAffected()}; + } + + int count = sqlQuery.value(0).toInt(); + + return QOrmQueryResult{QVector{}, count}; +} + bool QOrmSqliteProviderPrivate::foreignKeysEnabled() { QSqlQuery query{m_database}; @@ -1208,6 +1229,9 @@ QOrmQueryResult QOrmSqliteProvider::execute(const QOrmQuery& query, case QOrm::Operation::Delete: return d->remove(query, entityInstanceCache); + case QOrm::Operation::Count: + return d->count(query); + case QOrm::Operation::Merge: Q_ORM_UNEXPECTED_STATE; } diff --git a/src/orm/qormsqlitestatementgenerator_p.cpp b/src/orm/qormsqlitestatementgenerator_p.cpp index 0ce62b5..04812a9 100644 --- a/src/orm/qormsqlitestatementgenerator_p.cpp +++ b/src/orm/qormsqlitestatementgenerator_p.cpp @@ -1,6 +1,6 @@ /* - * Copyright (C) 2019-2022 Dmitriy Purgin - * Copyright (C) 2019-2022 sequality software engineering e.U. + * Copyright (C) 2019-2025 Dmitriy Purgin + * Copyright (C) 2019-2025 sequality software engineering e.U. * * This file is part of QtOrm library. * @@ -102,6 +102,7 @@ QString QOrmSqliteStatementGenerator::generate(const QOrmQuery& query, QVariantM boundParameters); case QOrm::Operation::Read: + case QOrm::Operation::Count: return generateSelectStatement(query, boundParameters); case QOrm::Operation::Delete: @@ -211,9 +212,17 @@ QString QOrmSqliteStatementGenerator::generateUpdateStatement(const QOrmMetadata QString QOrmSqliteStatementGenerator::generateSelectStatement(const QOrmQuery& query, QVariantMap& boundParameters) { - Q_ASSERT(query.operation() == QOrm::Operation::Read); + Q_ASSERT(query.operation() == QOrm::Operation::Read || + query.operation() == QOrm::Operation::Count); - QStringList parts = {"SELECT *", generateFromClause(query.relation(), boundParameters)}; + QString projection = query.operation() == QOrm::Operation::Read + ? QString{"*"} + : QString{"COUNT(*) AS %1"}.arg(escapeIdentifier("count")); + ; + + QStringList parts = {"SELECT", + projection, + generateFromClause(query.relation(), boundParameters)}; if (query.expressionFilter().has_value()) parts += generateWhereClause(*query.expressionFilter(), boundParameters); diff --git a/tests/auto/qormsession/tst_ormsession.cpp b/tests/auto/qormsession/tst_ormsession.cpp index 3ff6ef1..f626a72 100644 --- a/tests/auto/qormsession/tst_ormsession.cpp +++ b/tests/auto/qormsession/tst_ormsession.cpp @@ -1,7 +1,7 @@ /* * Copyright (C) 2020-2021 Dmitriy Purgin - * Copyright (C) 2019-2022 Dmitriy Purgin - * Copyright (C) 2019-2022 sequality software engineering e.U. + * Copyright (C) 2019-2025 Dmitriy Purgin + * Copyright (C) 2019-2025 sequality software engineering e.U. * * This file is part of QtOrm library. * @@ -65,6 +65,9 @@ private slots: void testSelectWithLimitOffset(); void testSelectWithOverwriteCachedInstances(); + void testCount(); + void testCountWithFilter(); + void testMergeFailsWithInconsistentReferences(); void testMergeOfExistingUncachedEntitiesWithExplicitIdsUpdates(); void testMergeNewEntitiesNoAutogeneratedIds(); @@ -543,6 +546,33 @@ void SqliteSessionTest::testSelectWithOverwriteCachedInstances() QVERIFY(!session.entityInstanceCache()->isModified(upperAustria)); } +void SqliteSessionTest::testCount() +{ + QOrmSession session; + session.merge(new Province(QString::fromUtf8("Oberösterreich")), + new Province(QString::fromUtf8("Niederösterreich")), + new Province(QString::fromUtf8("Salzburg"))); + + auto result = session.from().count(); + + QCOMPARE(result.error().type(), QOrm::ErrorType::None); + QCOMPARE(result.value(), 3); +} + +void SqliteSessionTest::testCountWithFilter() +{ + QOrmSession session; + session.merge(new Province(QString::fromUtf8("Oberösterreich")), + new Province(QString::fromUtf8("Niederösterreich")), + new Province(QString::fromUtf8("Salzburg"))); + + auto result = + session.from().filter(Q_ORM_CLASS_PROPERTY(name).contains("österreich")).count(); + + QCOMPARE(result.error().type(), QOrm::ErrorType::None); + QCOMPARE(result.value(), 2); +} + void SqliteSessionTest::testMergeFailsWithInconsistentReferences() { QOrmSession session; @@ -953,7 +983,7 @@ void SqliteSessionTest::testRemoveInstance() Province* upperAustria = new Province{QString::fromUtf8("Oberösterreich")}; QVERIFY(session.merge(upperAustria)); QVERIFY(session.remove(upperAustria)); - QVERIFY(session.from().select().toVector().empty()); + QVERIFY(session.from().select().toVector().empty()); } void SqliteSessionTest::testRemoveWithFilter() diff --git a/tests/auto/qormsqlitestatementgenerator/tst_sqlitestatementgenerator.cpp b/tests/auto/qormsqlitestatementgenerator/tst_sqlitestatementgenerator.cpp index 739e002..14441b4 100644 --- a/tests/auto/qormsqlitestatementgenerator/tst_sqlitestatementgenerator.cpp +++ b/tests/auto/qormsqlitestatementgenerator/tst_sqlitestatementgenerator.cpp @@ -1,7 +1,7 @@ /* * Copyright (C) 2020-2021 Dmitriy Purgin - * Copyright (C) 2019-2022 Dmitriy Purgin - * Copyright (C) 2019-2022 sequality software engineering e.U. + * Copyright (C) 2019-2025 Dmitriy Purgin + * Copyright (C) 2019-2025 sequality software engineering e.U. * * This file is part of QtOrm library. * @@ -78,6 +78,8 @@ private slots: void testSelectWithNamespace(); void testLimitOffset(); void testLimitOffset_data(); + + void testCount(); }; void SqliteStatementGenerator::init() @@ -550,6 +552,28 @@ void SqliteStatementGenerator::testLimitOffset_data() << QVariant{} << QVariant{42} << "LIMIT :limit OFFSET :offset"; } +void SqliteStatementGenerator::testCount() +{ + QOrmMetadataCache cache; + + QOrmRelation relation{cache.get()}; + QOrmMetadata projection{cache.get()}; + + QOrmQuery query{QOrm::Operation::Count, + relation, + projection, + std::nullopt, + std::nullopt, + {}, + QOrm::QueryFlags::None}; + QMap boundParameters; + QString actual = QOrmSqliteStatementGenerator{}.generate(query, boundParameters).simplified(); + QString expected{R"(SELECT COUNT(*) AS "count" FROM "Town")"}; + + QCOMPARE(actual, expected); + QVERIFY(boundParameters.empty()); +} + QTEST_APPLESS_MAIN(SqliteStatementGenerator) #include "tst_sqlitestatementgenerator.moc"