added BuildsQueries concerns

Added chunk, each, chunkById, eachById, sole, tap in BuildsQueries and
QueryBuilder::soleValue().

 - added tests
 - added docs
This commit is contained in:
silverqx
2022-07-27 08:38:02 +02:00
parent f8f7d955a9
commit 2557f66594
13 changed files with 1247 additions and 9 deletions

View File

@@ -40,7 +40,10 @@ function(tinyorm_sources out_headers out_sources)
exceptions/invalidformaterror.hpp
exceptions/invalidtemplateargumenterror.hpp
exceptions/logicerror.hpp
exceptions/multiplerecordsfounderror.hpp
exceptions/ormerror.hpp
exceptions/queryerror.hpp
exceptions/recordsnotfounderror.hpp
exceptions/runtimeerror.hpp
exceptions/sqlerror.hpp
exceptions/sqltransactionerror.hpp
@@ -57,6 +60,7 @@ function(tinyorm_sources out_headers out_sources)
ormconcepts.hpp
ormtypes.hpp
postgresconnection.hpp
query/concerns/buildsqueries.hpp
query/expression.hpp
query/grammars/grammar.hpp
query/grammars/mysqlgrammar.hpp
@@ -177,6 +181,7 @@ function(tinyorm_sources out_headers out_sources)
libraryinfo.cpp
mysqlconnection.cpp
postgresconnection.cpp
query/concerns/buildsqueries.cpp
query/grammars/grammar.cpp
query/grammars/mysqlgrammar.cpp
query/grammars/postgresgrammar.cpp

View File

@@ -9,6 +9,7 @@ keywords: [c++ orm, sql, c++ sql, c++ query builder, database, query builder, ti
- [Introduction](#introduction)
- [Running Database Queries](#running-database-queries)
- [Chunking Results](#chunking-results)
- [Aggregates](#aggregates)
- [Select Statements](#select-statements)
- [Raw Expressions](#raw-expressions)
@@ -125,6 +126,47 @@ The `implode` method can be used to join column values. For example, you may use
DB::table("orders")->where("price", ">", 100).implode("price", ", ");
### Chunking Results
If you need to work with thousands of database records, consider using the `chunk` method provided by the `DB` facade. This method retrieves a small chunk of results at a time and feeds each chunk into a lambda expression for processing. For example, let's retrieve the entire `users` table in chunks of 100 records at a time:
DB::table("users")->orderBy("id").chunk(100, [](QSqlQuery &users, const int page)
{
while (users.next()) {
//
}
return true;
});
You may stop further chunks from being processed by returning `false` from the closure:
DB::table("users")->orderBy("id").chunk(100, [](QSqlQuery &users, const int page)
{
// Process the records...
return false;
});
If you are updating database records while chunking results, your chunk results could change in unexpected ways. If you plan to update the retrieved records while chunking, it is always best to use the `chunkById` method instead. This method will automatically paginate the results based on the record's primary key:
DB::table("users")
->whereEq("active", false)
.orderBy("id")
.chunkById(100, [](QSqlQuery &users, const int /*unused*/)
{
while (users.next())
DB::table("users")
->whereEq("id", users.value("id"))
.update({{"active", true}});
return true;
});
:::caution
When updating or deleting records inside the chunk callback, any changes to the primary key or foreign keys could affect the chunk query. This could potentially result in records not being included in the chunked results, it can be avoided using the `chunkById` method.
:::
### Aggregates
The query builder also provides a variety of methods for retrieving aggregate values like `count`, `max`, `min`, `avg`, and `sum`. You may call any of these methods after constructing your query:

View File

@@ -8,7 +8,7 @@ keywords: [c++ orm, supported compilers, supported build systems, tinyorm]
# Supported Compilers
Following compilers are backed up by the GitHub Action [workflows](https://github.com/silverqx/TinyORM/tree/main/.github/workflows) (CI pipelines), these workflows also include more then __1058 unit tests__ 😮💥.
Following compilers are backed up by the GitHub Action [workflows](https://github.com/silverqx/TinyORM/tree/main/.github/workflows) (CI pipelines), these workflows also include more then __1181 unit tests__ 😮💥.
<div id="supported-compilers">

View File

@@ -33,8 +33,10 @@ headersList += \
$$PWD/orm/exceptions/invalidformaterror.hpp \
$$PWD/orm/exceptions/invalidtemplateargumenterror.hpp \
$$PWD/orm/exceptions/logicerror.hpp \
$$PWD/orm/exceptions/multiplerecordsfounderror.hpp \
$$PWD/orm/exceptions/ormerror.hpp \
$$PWD/orm/exceptions/queryerror.hpp \
$$PWD/orm/exceptions/recordsnotfounderror.hpp \
$$PWD/orm/exceptions/runtimeerror.hpp \
$$PWD/orm/exceptions/sqlerror.hpp \
$$PWD/orm/exceptions/sqltransactionerror.hpp \
@@ -52,6 +54,7 @@ headersList += \
$$PWD/orm/ormconcepts.hpp \
$$PWD/orm/ormtypes.hpp \
$$PWD/orm/postgresconnection.hpp \
$$PWD/orm/query/concerns/buildsqueries.hpp \
$$PWD/orm/query/expression.hpp \
$$PWD/orm/query/grammars/grammar.hpp \
$$PWD/orm/query/grammars/mysqlgrammar.hpp \

View File

@@ -0,0 +1,47 @@
#pragma once
#ifndef ORM_EXCEPTIONS_MULTIPLERECORDSFOUNDERROR_HPP
#define ORM_EXCEPTIONS_MULTIPLERECORDSFOUNDERROR_HPP
#include "orm/macros/systemheader.hpp"
TINY_SYSTEM_HEADER
#include "orm/exceptions/runtimeerror.hpp"
TINYORM_BEGIN_COMMON_NAMESPACE
namespace Orm::Exceptions
{
/*! Found more that one record (used by Builder::sole()). */
class MultipleRecordsFoundError : public RuntimeError // clazy:exclude=copyable-polymorphic
{
public:
/*! Constructor. */
inline explicit MultipleRecordsFoundError(int count);
/*! Get the number of records found. */
inline int count() const noexcept;
protected:
/*! The number of records found. */
int m_count;
};
/* public */
MultipleRecordsFoundError::MultipleRecordsFoundError(const int count)
: RuntimeError(QStringLiteral("%1 records were found.").arg(count)
.toUtf8().constData())
, m_count(count)
{}
int MultipleRecordsFoundError::count() const noexcept
{
return m_count;
}
} // namespace Orm::Exceptions
TINYORM_END_COMMON_NAMESPACE
#endif // ORM_EXCEPTIONS_MULTIPLERECORDSFOUNDERROR_HPP

View File

@@ -0,0 +1,26 @@
#pragma once
#ifndef ORM_EXCEPTIONS_RECORDSNOTFOUNDERROR_HPP
#define ORM_EXCEPTIONS_RECORDSNOTFOUNDERROR_HPP
#include "orm/macros/systemheader.hpp"
TINY_SYSTEM_HEADER
#include "orm/exceptions/runtimeerror.hpp"
TINYORM_BEGIN_COMMON_NAMESPACE
namespace Orm::Exceptions
{
/*! Found zero records (used by Builder::sole()). */
class RecordsNotFoundError : public RuntimeError // clazy:exclude=copyable-polymorphic
{
/*! Inherit constructors. */
using RuntimeError::RuntimeError;
};
} // namespace Orm::Exceptions
TINYORM_END_COMMON_NAMESPACE
#endif // ORM_EXCEPTIONS_RECORDSNOTFOUNDERROR_HPP

View File

@@ -0,0 +1,84 @@
#pragma once
#ifndef ORM_QUERY_CONCERNS_BUILDSQUERIES_HPP
#define ORM_QUERY_CONCERNS_BUILDSQUERIES_HPP
#include "orm/macros/systemheader.hpp"
TINY_SYSTEM_HEADER
#include "orm/macros/export.hpp"
#include "orm/ormtypes.hpp"
class QSqlQuery;
TINYORM_BEGIN_COMMON_NAMESPACE
namespace Orm::Query
{
class Builder;
namespace Concerns
{
// TODO buildsqueries, missing chunkMap() silverqx
/*! More complex 'Retrieving results' methods that internally build queries. */
class SHAREDLIB_EXPORT BuildsQueries // clazy:exclude=copyable-polymorphic
{
public:
/*! Default constructor. */
inline BuildsQueries() = default;
/*! Virtual destructor, to pass -Weffc++. */
inline virtual ~BuildsQueries() = default;
/*! Copy constructor. */
inline BuildsQueries(const BuildsQueries &) = default;
/*! Deleted copy assignment operator (QueryBuilder class constains reference and
const). */
BuildsQueries &operator=(const BuildsQueries &) = delete;
/*! Move constructor. */
inline BuildsQueries(BuildsQueries &&) = default;
/*! Deleted move assignment operator (QueryBuilder class constains reference and
const). */
BuildsQueries &operator=(BuildsQueries &&) = delete;
/*! Chunk the results of the query. */
bool chunk(int count,
const std::function<bool(QSqlQuery &results, int page)> &callback);
/*! Execute a callback over each item while chunking. */
bool each(const std::function<bool(QSqlQuery &row, int index)> &callback,
int count = 1000);
/*! Run a map over each item while chunking. */
// QVector<QSqlQuery>
// chunkMap(const std::function<void(QSqlQuery &row)> &callback, int count = 1000);
/*! Chunk the results of a query by comparing IDs. */
bool chunkById(int count,
const std::function<bool(QSqlQuery &results, int page)> &callback,
const QString &column = "", const QString &alias = "");
/*! Execute a callback over each item while chunking by ID. */
bool eachById(const std::function<bool(QSqlQuery &row, int index)> &callback,
int count = 1000, const QString &column = "",
const QString &alias = "");
/*! Execute the query and get the first result if it's the sole matching
record. */
QSqlQuery sole(const QVector<Column> &columns = {ASTERISK});
/*! Pass the query to a given callback. */
Builder &tap(const std::function<void(Builder &query)> &callback);
private:
/*! Static cast *this to the QueryBuilder & derived type. */
Builder &builder();
/*! Static cast *this to the QueryBuilder & derived type, const version. */
const Builder &builder() const;
};
} // namespace Concerns
} // namespace Orm::Query
TINYORM_END_COMMON_NAMESPACE
#endif // ORM_QUERY_CONCERNS_BUILDSQUERIES_HPP

View File

@@ -10,7 +10,7 @@ TINY_SYSTEM_HEADER
#include <unordered_set>
#include "orm/ormconcepts.hpp"
#include "orm/ormtypes.hpp"
#include "orm/query/concerns/buildsqueries.hpp"
#include "orm/query/grammars/grammar.hpp"
#include "orm/utils/query.hpp"
@@ -25,23 +25,26 @@ namespace Orm::Query
concept Remove = std::convertible_to<T, quint64> ||
std::same_as<T, Query::Expression>;
// TODO add inRandomOrder() silverqx
// TODO QueryBuilder::updateOrInsert() silverqx
// TODO querybuilder, upsert, whereDay/Month/..., whereBetween, whereFullText silverqx
// FUTURE querybuilder, paginator silverqx
/*! Database query builder. */
class SHAREDLIB_EXPORT Builder // clazy:exclude=copyable-polymorphic
class SHAREDLIB_EXPORT Builder : public Concerns::BuildsQueries // clazy:exclude=copyable-polymorphic
{
/*! Alias for the query grammar. */
using QueryGrammar = Query::Grammars::Grammar;
/*! Alias for query utils. */
using QueryUtils = Orm::Utils::Query;
// To access enforceOrderBy(), defaultKeyName(), clone(), forPageAfterId()
friend Concerns::BuildsQueries;
public:
/*! Constructor. */
Builder(DatabaseConnection &connection, const QueryGrammar &grammar);
/* Need to be the polymorphic type because of dynamic_cast<>
in Grammar::concatenateWhereClauses(). */
/*! Virtual destructor. */
inline virtual ~Builder() = default;
inline ~Builder() override = default;
/*! Copy constructor. */
inline Builder(const Builder &) = default;
@@ -79,6 +82,10 @@ namespace Orm::Query
QSqlQuery first(const QVector<Column> &columns = {ASTERISK});
/*! Get a single column's value from the first result of a query. */
QVariant value(const Column &column);
/*! Get a single column's value from the first result of a query if it's
the sole matching record. */
QVariant soleValue(const Column &column);
/*! Get the vector with the values of a given column. */
QVector<QVariant> pluck(const QString &column);
/*! Get the vector with the values of a given column. */
@@ -544,6 +551,14 @@ namespace Orm::Query
Builder &skip(int value);
/*! Set the limit and offset for a given page. */
Builder &forPage(int page, int perPage = 30);
/*! Constrain the query to the previous "page" of results before a given ID. */
Builder &forPageBeforeId(int perPage = 30, const QVariant &lastId = {},
const QString &column = Orm::Constants::ID,
bool prependOrder = false);
/*! Constrain the query to the next "page" of results after a given ID. */
Builder &forPageAfterId(int perPage = 30, const QVariant &lastId = {},
const QString &column = Orm::Constants::ID,
bool prependOrder = false);
/* Others */
/*! Increment a column's value by a given amount. */
@@ -578,6 +593,8 @@ namespace Orm::Query
void dd(bool replaceBindings = true, bool simpleBindings = false);
/* Getters / Setters */
/*! Get the default key name of the table. */
const QString &defaultKeyName() const;
/*! Get a database connection. */
inline DatabaseConnection &getConnection() const;
/*! Get the query grammar instance. */
@@ -663,6 +680,8 @@ namespace Orm::Query
COLUMNS,
};
/*! Clone the query. */
inline Builder clone() const;
/*! Clone the query without the given properties. */
Builder cloneWithout(const std::unordered_set<PropertyType> &properties) const;
/*! Clone the query without the given bindings. */
@@ -732,6 +751,11 @@ namespace Orm::Query
/*! Strip off the table name or alias from a column identifier. */
QString stripTableForPluck(const QString &column) const;
/*! Throw an exception if the query doesn't have an orderBy clause. */
void enforceOrderBy() const;
/*! Get an array with all orders with a given column removed. */
QVector<OrderByItem> removeExistingOrdersFor(const QString &column) const;
/* Getters / Setters */
/*! Set the aggregate property without running the query. */
Builder &setAggregate(const QString &function,
@@ -1481,6 +1505,11 @@ namespace Orm::Query
return m_lock;
}
Builder Builder::clone() const
{
return *this;
}
/* protected */
std::shared_ptr<Builder>

View File

@@ -0,0 +1,219 @@
#include "orm/query/concerns/buildsqueries.hpp"
#include "orm/databaseconnection.hpp"
#include "orm/exceptions/multiplerecordsfounderror.hpp"
#include "orm/exceptions/recordsnotfounderror.hpp"
#include "orm/query/querybuilder.hpp"
#include "orm/utils/type.hpp"
using QueryUtils = Orm::Utils::Query;
TINYORM_BEGIN_COMMON_NAMESPACE
namespace Orm::Query::Concerns
{
/* public */
bool BuildsQueries::chunk(const int count,
const std::function<bool(QSqlQuery &, int)> &callback)
{
builder().enforceOrderBy();
int page = 1;
int countResults = 0;
do {
/* We'll execute the query for the given page and get the results. If there are
no results we can just break and return from here. When there are results
we will call the callback with the current chunk of these results here. */
auto results = builder().forPage(page, count).get();
countResults = QueryUtils::queryResultSize(results);
if (countResults == 0)
break;
/* On each chunk result set, we will pass them to the callback and then let the
developer take care of everything within the callback, which allows us to
keep the memory low for spinning through large result sets for working. */
if (const auto result = std::invoke(callback, results, page);
!result
)
return false;
++page;
} while (countResults == count);
return true;
}
bool BuildsQueries::each(const std::function<bool(QSqlQuery &, int)> &callback,
const int count)
{
return chunk(count, [&callback](QSqlQuery &results, const int /*unused*/)
{
int index = 0;
while (results.next())
if (const auto result = std::invoke(callback, results, index++);
!result
)
return false;
return true;
});
}
/* This is trash as the QSqlQuery is passed to the callback, I need to pass something
like std::map<std::pair<int, QString>, QVariant> so an user can modify it and return */
//QVector<QSqlQuery>
//BuildsQueries::chunkMap(const std::function<void(QSqlQuery &)> &callback, const int count)
//{
// /* This method is weird, it should return one merged collection with all rows, but
// it's impossible to merge more QSqlQuery-ies into the one QSqlQuery, so I have
// decided to return the vector of these QSqlQueries.
// It's not completely useless, only one difference will be that an user will have
// to loop over all QSqlQuery-ies, instead of one big QSqlQuery.
// Another confusing thing is that map-related algorithms are moving a value into
// the callback (not non-const reference like here) and returning a new mapped value,
// but it's not possible in this case as the QSqlQuery holds all other rows,
// it's only a cursor. So I have to pass non-const reference and if all rows are
// processed/looped then move a whole QSqlQuery into the result vector. */
// QVector<QSqlQuery> result;
// chunk(count, [&result, &callback](QSqlQuery &results, const int /*unused*/)
// {
// while (results.next())
// std::invoke(callback, results);
// result << std::move(results);
// return true;
// });
// return result;
//}
bool BuildsQueries::chunkById(
const int count, const std::function<bool(QSqlQuery &, int)> &callback,
const QString &column, const QString &alias)
{
const auto columnName = column.isEmpty() ? builder().defaultKeyName() : column;
const auto aliasName = alias.isEmpty() ? columnName : alias;
int page = 1;
int countResults = 0;
QVariant lastId;
do {
auto clone = builder().clone();
/* We'll execute the query for the given page and get the results. If there are
no results we can just break and return from here. When there are results
we will call the callback with the current chunk of these results here. */
auto results = clone.forPageAfterId(count, lastId, columnName, true).get();
countResults = QueryUtils::queryResultSize(results);
if (countResults == 0)
break;
/* Obtain the lastId before the results is passed to the user's callback because
an user can leave the results (QSqlQuery) in the invalid state. */
results.last();
lastId = results.value(aliasName);
// Restore a cursor position
results.seek(QSql::BeforeFirstRow);
/* And the check can also be made before a callback invocation, it saves
the unnecessary invocation if the lastId is invalid. It also helps to avoid
passing invalid data to the user. */
if (!lastId.isValid() || lastId.isNull())
throw Exceptions::RuntimeError(
QStringLiteral("The chunkById operation was aborted because the "
"[%1] column is not present in the query result.")
.arg(aliasName));
/* On each chunk result set, we will pass them to the callback and then let the
developer take care of everything within the callback, which allows us to
keep the memory low for spinning through large result sets for working. */
if (const auto result = std::invoke(callback, results, page);
!result
)
return false;
++page;
} while (countResults == count);
return true;
}
bool BuildsQueries::eachById(
const std::function<bool(QSqlQuery &, int)> &callback,
const int count, const QString &column, const QString &alias)
{
return chunkById(count, [&callback, count](QSqlQuery &results, const int page)
{
int index = 0;
while (results.next())
if (const auto result = std::invoke(callback, results,
((page - 1) * count) + index++);
!result
)
return false;
return true;
}, column, alias);
}
// CUR buildsqueries, check if all are pretending compatible silverqx
QSqlQuery BuildsQueries::sole(const QVector<Column> &columns)
{
auto query = builder().take(2).get(columns);
if (builder().getConnection().pretending())
return query;
const auto count = QueryUtils::queryResultSize(query);
if (count == 0)
throw Exceptions::RecordsNotFoundError(
QStringLiteral("No records found in %1().").arg(__tiny_func__));
if (count > 1)
throw Exceptions::MultipleRecordsFoundError(count);
query.first();
return query;
}
Builder &BuildsQueries::tap(const std::function<void(Builder &)> &callback)
{
std::invoke(callback, builder());
return builder();
}
/* private */
Builder &BuildsQueries::builder()
{
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast)
return static_cast<Builder &>(*this);
}
const Builder &BuildsQueries::builder() const
{
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast)
return static_cast<const Builder &>(*this);
}
} // namespace Orm::Query::Concerns
TINYORM_END_COMMON_NAMESPACE

View File

@@ -1,11 +1,12 @@
#include "orm/query/querybuilder.hpp"
#include <QDebug>
#include <range/v3/view/remove_if.hpp>
#include "orm/databaseconnection.hpp"
#include "orm/exceptions/invalidargumenterror.hpp"
#include "orm/query/joinclause.hpp"
#include "orm/utils/query.hpp"
using QueryUtils = Orm::Utils::Query;
TINYORM_BEGIN_COMMON_NAMESPACE
@@ -81,6 +82,24 @@ QVariant Builder::value(const Column &column)
return query.value(column_);
}
QVariant Builder::soleValue(const Column &column)
{
// Expression support
QString column_;
if (std::holds_alternative<Expression>(column))
column_ = std::get<Expression>(column).getValue().value<QString>();
else
column_ = std::get<QString>(column);
const auto query = sole({column});
if (m_connection.pretending())
return {};
return query.value(column_);
}
QVector<QVariant> Builder::pluck(const QString &column)
{
/* First, we will need to select the results of the query accounting for the
@@ -822,6 +841,40 @@ Builder &Builder::forPage(const int page, const int perPage)
return offset((page - 1) * perPage).limit(perPage);
}
// NOTE api little different, added bool prependOrder parameter silverqx
Builder &Builder::forPageBeforeId(const int perPage, const QVariant &lastId,
const QString &column, const bool prependOrder)
{
m_orders = removeExistingOrdersFor(column);
if (lastId.isValid() && !lastId.isNull())
where(column, LT, lastId);
if (prependOrder)
m_orders.prepend({column, DESC});
else
orderBy(column, DESC);
return limit(perPage);
}
// NOTE api little different, added bool prependOrder parameter silverqx
Builder &Builder::forPageAfterId(const int perPage, const QVariant &lastId,
const QString &column, const bool prependOrder)
{
m_orders = removeExistingOrdersFor(column);
if (lastId.isValid() && !lastId.isNull())
where(column, GT, lastId);
if (prependOrder)
m_orders.prepend({column, ASC});
else
orderBy(column, ASC);
return limit(perPage);
}
/* Pessimistic Locking */
Builder &Builder::lockForUpdate()
@@ -903,6 +956,11 @@ void Builder::dd(const bool replaceBindings, const bool simpleBindings)
/* Getters / Setters */
const QString &Builder::defaultKeyName() const
{
return ID;
}
QVector<QVariant> Builder::getBindings() const
{
QVector<QVariant> flattenBindings;
@@ -1231,6 +1289,26 @@ QString Builder::stripTableForPluck(const QString &column) const
return column.split(as).last().trimmed();
}
void Builder::enforceOrderBy() const
{
if (m_orders.isEmpty())
throw Exceptions::RuntimeError(
"You must specify an orderBy clause when using this function.");
}
QVector<OrderByItem> Builder::removeExistingOrdersFor(const QString &column) const
{
return m_orders
| ranges::views::remove_if([&column](const OrderByItem &order)
{
if (std::holds_alternative<Expression>(order.column))
return false;
return std::get<QString>(order.column) == column;
})
| ranges::to<QVector<OrderByItem>>();
}
/* Getters / Setters */
Builder &Builder::setAggregate(const QString &function, const QVector<Column> &columns)

View File

@@ -25,6 +25,7 @@ sourcesList += \
$$PWD/orm/libraryinfo.cpp \
$$PWD/orm/mysqlconnection.cpp \
$$PWD/orm/postgresconnection.cpp \
$$PWD/orm/query/concerns/buildsqueries.cpp \
$$PWD/orm/query/grammars/grammar.cpp \
$$PWD/orm/query/grammars/mysqlgrammar.cpp \
$$PWD/orm/query/grammars/postgresgrammar.cpp \

View File

@@ -18,9 +18,11 @@ using Orm::Constants::NAME;
using Orm::Constants::SIZE;
using Orm::DB;
using Orm::Exceptions::RuntimeError;
using Orm::Query::Builder;
using QueryBuilder = Orm::Query::Builder;
using QueryUtils = Orm::Utils::Query;
using TestUtils::Databases;
@@ -63,6 +65,33 @@ private Q_SLOTS:
void limit() const;
/* Builds Queries */
void chunk() const;
void chunk_ReturnFalse() const;
void chunk_EnforceOrderBy() const;
void chunk_EmptyResult() const;
void each() const;
void each_ReturnFalse() const;
void each_EnforceOrderBy() const;
void each_EmptyResult() const;
void chunkById() const;
void chunkById_ReturnFalse() const;
void chunkById_EmptyResult() const;
void chunkById_WithAlias() const;
void chunkById_ReturnFalse_WithAlias() const;
void chunkById_EmptyResult_WithAlias() const;
void eachById() const;
void eachById_ReturnFalse() const;
void eachById_EmptyResult() const;
void eachById_WithAlias() const;
void eachById_ReturnFalse_WithAlias() const;
void eachById_EmptyResult_WithAlias() const;
// NOLINTNEXTLINE(readability-redundant-access-specifiers)
private:
/*! Create QueryBuilder instance for the given connection. */
@@ -764,6 +793,558 @@ void tst_QueryBuilder::limit() const
}
}
/* Builds Queries */
void tst_QueryBuilder::chunk() const
{
QFETCH_GLOBAL(QString, connection);
// <page, chunk_rowsCount>
const std::unordered_map<int, int> expectedRows {{1, 3}, {2, 3}, {3, 2}};
/* Can't be inside the chunk's callback because QCOMPARE internally calls 'return;'
and it causes compile error. */
const auto compareResultSize = [&expectedRows](QSqlQuery &query, const int page)
{
QCOMPARE(QueryUtils::queryResultSize(query), expectedRows.at(page));
};
std::vector<quint64> ids;
ids.reserve(8);
auto result = createQuery(connection)->from("file_property_properties")
.orderBy(ID)
.chunk(3, [&compareResultSize, &ids](QSqlQuery &query, const int page)
{
compareResultSize(query, page);
while (query.next())
ids.emplace_back(query.value(ID).value<quint64>());
return true;
});
QVERIFY(result);
std::vector<quint64> expectedIds {1, 2, 3, 4, 5, 6, 7, 8};
QVERIFY(ids.size() == expectedIds.size());
QCOMPARE(ids, expectedIds);
}
void tst_QueryBuilder::chunk_ReturnFalse() const
{
QFETCH_GLOBAL(QString, connection);
// <page, chunk_rowsCount> (I leave it here also in this test, doesn't matter much
const std::unordered_map<int, int> expectedRows {{1, 3}, {2, 3}, {3, 2}};
/* Can't be inside the chunk's callback because QCOMPARE internally calls 'return;'
and it causes compile error. */
const auto compareResultSize = [&expectedRows](QSqlQuery &query, const int page)
{
QCOMPARE(QueryUtils::queryResultSize(query), expectedRows.at(page));
};
std::vector<quint64> ids;
ids.reserve(5);
auto result = createQuery(connection)->from("file_property_properties")
.orderBy(ID)
.chunk(3, [&compareResultSize, &ids](QSqlQuery &query, const int page)
{
compareResultSize(query, page);
while (query.next()) {
auto id = query.value(ID).value<quint64>();
ids.emplace_back(id);
// Intetrupt chunk-ing
if (id == 5)
return false;
}
return true;
});
QVERIFY(!result);
std::vector<quint64> expectedIds {1, 2, 3, 4, 5};
QVERIFY(ids.size() == expectedIds.size());
QCOMPARE(ids, expectedIds);
}
void tst_QueryBuilder::chunk_EnforceOrderBy() const
{
QFETCH_GLOBAL(QString, connection);
QVERIFY_EXCEPTION_THROWN(createQuery(connection)->from("file_property_properties")
.chunk(3, [](QSqlQuery &/*unused*/, const int /*unused*/)
{
return true;
}),
RuntimeError);
}
void tst_QueryBuilder::chunk_EmptyResult() const
{
QFETCH_GLOBAL(QString, connection);
auto result = createQuery(connection)->from("file_property_properties")
.whereEq(NAME, QStringLiteral("dummy-NON_EXISTENT"))
.orderBy(ID)
.chunk(3, [](QSqlQuery &/*unused*/, const int /*unused*/)
{
return true;
});
QVERIFY(result);
}
void tst_QueryBuilder::each() const
{
QFETCH_GLOBAL(QString, connection);
std::vector<int> indexes;
indexes.reserve(8);
std::vector<quint64> ids;
ids.reserve(8);
auto result = createQuery(connection)->from("file_property_properties")
.orderBy(ID)
.each([&indexes, &ids](QSqlQuery &query, const int index)
{
indexes.emplace_back(index);
ids.emplace_back(query.value(ID).value<quint64>());
return true;
});
QVERIFY(result);
std::vector<int> expectedIndexes {0, 1, 2, 3, 4, 5, 6, 7};
std::vector<quint64> expectedIds {1, 2, 3, 4, 5, 6, 7, 8};
QVERIFY(indexes.size() == expectedIndexes.size());
QCOMPARE(indexes, expectedIndexes);
QVERIFY(ids.size() == expectedIds.size());
QCOMPARE(ids, expectedIds);
}
void tst_QueryBuilder::each_ReturnFalse() const
{
QFETCH_GLOBAL(QString, connection);
std::vector<int> indexes;
indexes.reserve(5);
std::vector<quint64> ids;
ids.reserve(5);
auto result = createQuery(connection)->from("file_property_properties")
.orderBy(ID)
.each([&indexes, &ids](QSqlQuery &query, const int index)
{
indexes.emplace_back(index);
ids.emplace_back(query.value(ID).value<quint64>());
return index != 4; // false/interrupt on 4
});
QVERIFY(!result);
std::vector<int> expectedIndexes {0, 1, 2, 3, 4};
std::vector<quint64> expectedIds {1, 2, 3, 4, 5};
QVERIFY(indexes.size() == expectedIndexes.size());
QCOMPARE(indexes, expectedIndexes);
QVERIFY(ids.size() == expectedIds.size());
QCOMPARE(ids, expectedIds);
}
void tst_QueryBuilder::each_EnforceOrderBy() const
{
QFETCH_GLOBAL(QString, connection);
QVERIFY_EXCEPTION_THROWN(createQuery(connection)->from("file_property_properties")
.each([](QSqlQuery &/*unused*/, const int /*unused*/)
{
return true;
}),
RuntimeError);
}
void tst_QueryBuilder::each_EmptyResult() const
{
QFETCH_GLOBAL(QString, connection);
auto result = createQuery(connection)->from("file_property_properties")
.whereEq(NAME, QStringLiteral("dummy-NON_EXISTENT"))
.orderBy(ID)
.each([](QSqlQuery &/*unused*/, const int /*unused*/)
{
return true;
});
QVERIFY(result);
}
void tst_QueryBuilder::chunkById() const
{
QFETCH_GLOBAL(QString, connection);
// <page, chunk_rowsCount>
const std::unordered_map<int, int> expectedRows {{1, 3}, {2, 3}, {3, 2}};
/* Can't be inside the chunk's callback because QCOMPARE internally calls 'return;'
and it causes compile error. */
const auto compareResultSize = [&expectedRows](QSqlQuery &query, const int page)
{
QCOMPARE(QueryUtils::queryResultSize(query), expectedRows.at(page));
};
std::vector<quint64> ids;
ids.reserve(8);
auto result = createQuery(connection)->from("file_property_properties")
.orderBy(ID)
.chunkById(3, [&compareResultSize, &ids]
(QSqlQuery &query, const int page)
{
compareResultSize(query, page);
while (query.next())
ids.emplace_back(query.value(ID).value<quint64>());
return true;
});
QVERIFY(result);
std::vector<quint64> expectedIds {1, 2, 3, 4, 5, 6, 7, 8};
QVERIFY(ids.size() == expectedIds.size());
QCOMPARE(ids, expectedIds);
}
void tst_QueryBuilder::chunkById_ReturnFalse() const
{
QFETCH_GLOBAL(QString, connection);
// <page, chunk_rowsCount> (I leave it here also in this test, doesn't matter much
const std::unordered_map<int, int> expectedRows {{1, 3}, {2, 3}, {3, 2}};
/* Can't be inside the chunk's callback because QCOMPARE internally calls 'return;'
and it causes compile error. */
const auto compareResultSize = [&expectedRows](QSqlQuery &query, const int page)
{
QCOMPARE(QueryUtils::queryResultSize(query), expectedRows.at(page));
};
std::vector<quint64> ids;
ids.reserve(5);
auto result = createQuery(connection)->from("file_property_properties")
.orderBy(ID)
.chunkById(3, [&compareResultSize, &ids]
(QSqlQuery &query, const int page)
{
compareResultSize(query, page);
while (query.next()) {
auto id = query.value(ID).value<quint64>();
ids.emplace_back(id);
// Intetrupt chunk-ing
if (id == 5)
return false;
}
return true;
});
QVERIFY(!result);
std::vector<quint64> expectedIds {1, 2, 3, 4, 5};
QVERIFY(ids.size() == expectedIds.size());
QCOMPARE(ids, expectedIds);
}
void tst_QueryBuilder::chunkById_EmptyResult() const
{
QFETCH_GLOBAL(QString, connection);
auto result = createQuery(connection)->from("file_property_properties")
.whereEq(NAME, QStringLiteral("dummy-NON_EXISTENT"))
.orderBy(ID)
.chunkById(3, [](QSqlQuery &/*unused*/, const int /*unused*/)
{
return true;
});
QVERIFY(result);
}
void tst_QueryBuilder::chunkById_WithAlias() const
{
QFETCH_GLOBAL(QString, connection);
// <page, chunk_rowsCount>
const std::unordered_map<int, int> expectedRows {{1, 3}, {2, 3}, {3, 2}};
/* Can't be inside the chunk's callback because QCOMPARE internally calls 'return;'
and it causes compile error. */
const auto compareResultSize = [&expectedRows](QSqlQuery &query, const int page)
{
QCOMPARE(QueryUtils::queryResultSize(query), expectedRows.at(page));
};
std::vector<quint64> ids;
ids.reserve(8);
auto result = createQuery(connection)->from("file_property_properties")
.select({ASTERISK, "id as id_as"})
.orderBy(ID)
.chunkById(3, [&compareResultSize, &ids]
(QSqlQuery &query, const int page)
{
compareResultSize(query, page);
while (query.next())
ids.emplace_back(query.value(ID).value<quint64>());
return true;
},
ID, "id_as");
QVERIFY(result);
std::vector<quint64> expectedIds {1, 2, 3, 4, 5, 6, 7, 8};
QVERIFY(ids.size() == expectedIds.size());
QCOMPARE(ids, expectedIds);
}
void tst_QueryBuilder::chunkById_ReturnFalse_WithAlias() const
{
QFETCH_GLOBAL(QString, connection);
// <page, chunk_rowsCount> (I leave it here also in this test, doesn't matter much
const std::unordered_map<int, int> expectedRows {{1, 3}, {2, 3}, {3, 2}};
/* Can't be inside the chunk's callback because QCOMPARE internally calls 'return;'
and it causes compile error. */
const auto compareResultSize = [&expectedRows](QSqlQuery &query, const int page)
{
QCOMPARE(QueryUtils::queryResultSize(query), expectedRows.at(page));
};
std::vector<quint64> ids;
ids.reserve(5);
auto result = createQuery(connection)->from("file_property_properties")
.select({ASTERISK, "id as id_as"})
.orderBy(ID)
.chunkById(3, [&compareResultSize, &ids]
(QSqlQuery &query, const int page)
{
compareResultSize(query, page);
while (query.next()) {
auto id = query.value(ID).value<quint64>();
ids.emplace_back(id);
// Intetrupt chunk-ing
if (id == 5)
return false;
}
return true;
},
ID, "id_as");
QVERIFY(!result);
std::vector<quint64> expectedIds {1, 2, 3, 4, 5};
QVERIFY(ids.size() == expectedIds.size());
QCOMPARE(ids, expectedIds);
}
void tst_QueryBuilder::chunkById_EmptyResult_WithAlias() const
{
QFETCH_GLOBAL(QString, connection);
auto result = createQuery(connection)->from("file_property_properties")
.select({ASTERISK, "id as id_as"})
.whereEq(NAME, QStringLiteral("dummy-NON_EXISTENT"))
.orderBy(ID)
.chunkById(3, [](QSqlQuery &/*unused*/, const int /*unused*/)
{
return true;
},
ID, "id_as");
QVERIFY(result);
}
void tst_QueryBuilder::eachById() const
{
QFETCH_GLOBAL(QString, connection);
std::vector<int> indexes;
indexes.reserve(8);
std::vector<quint64> ids;
ids.reserve(8);
auto result = createQuery(connection)->from("file_property_properties")
.orderBy(ID)
.eachById([&indexes, &ids](QSqlQuery &query, const int index)
{
indexes.emplace_back(index);
ids.emplace_back(query.value(ID).value<quint64>());
return true;
});
QVERIFY(result);
std::vector<int> expectedIndexes {0, 1, 2, 3, 4, 5, 6, 7};
std::vector<quint64> expectedIds {1, 2, 3, 4, 5, 6, 7, 8};
QVERIFY(indexes.size() == expectedIndexes.size());
QCOMPARE(indexes, expectedIndexes);
QVERIFY(ids.size() == expectedIds.size());
QCOMPARE(ids, expectedIds);
}
void tst_QueryBuilder::eachById_ReturnFalse() const
{
QFETCH_GLOBAL(QString, connection);
std::vector<int> indexes;
indexes.reserve(5);
std::vector<quint64> ids;
ids.reserve(5);
auto result = createQuery(connection)->from("file_property_properties")
.orderBy(ID)
.eachById([&indexes, &ids](QSqlQuery &query, const int index)
{
indexes.emplace_back(index);
ids.emplace_back(query.value(ID).value<quint64>());
return index != 4; // false/interrupt on 4
});
QVERIFY(!result);
std::vector<int> expectedIndexes {0, 1, 2, 3, 4};
std::vector<quint64> expectedIds {1, 2, 3, 4, 5};
QVERIFY(indexes.size() == expectedIndexes.size());
QCOMPARE(indexes, expectedIndexes);
QVERIFY(ids.size() == expectedIds.size());
QCOMPARE(ids, expectedIds);
}
void tst_QueryBuilder::eachById_EmptyResult() const
{
QFETCH_GLOBAL(QString, connection);
auto result = createQuery(connection)->from("file_property_properties")
.whereEq(NAME, QStringLiteral("dummy-NON_EXISTENT"))
.orderBy(ID)
.eachById([](QSqlQuery &/*unused*/, const int /*unused*/)
{
return true;
});
QVERIFY(result);
}
void tst_QueryBuilder::eachById_WithAlias() const
{
QFETCH_GLOBAL(QString, connection);
std::vector<int> indexes;
indexes.reserve(8);
std::vector<quint64> ids;
ids.reserve(8);
auto result = createQuery(connection)->from("file_property_properties")
.select({ASTERISK, "id as id_as"})
.orderBy(ID)
.eachById([&indexes, &ids](QSqlQuery &query, const int index)
{
indexes.emplace_back(index);
ids.emplace_back(query.value(ID).value<quint64>());
return true;
},
1000, ID, "id_as");
QVERIFY(result);
std::vector<int> expectedIndexes {0, 1, 2, 3, 4, 5, 6, 7};
std::vector<quint64> expectedIds {1, 2, 3, 4, 5, 6, 7, 8};
QVERIFY(indexes.size() == expectedIndexes.size());
QCOMPARE(indexes, expectedIndexes);
QVERIFY(ids.size() == expectedIds.size());
QCOMPARE(ids, expectedIds);
}
void tst_QueryBuilder::eachById_ReturnFalse_WithAlias() const
{
QFETCH_GLOBAL(QString, connection);
std::vector<int> indexes;
indexes.reserve(5);
std::vector<quint64> ids;
ids.reserve(5);
auto result = createQuery(connection)->from("file_property_properties")
.select({ASTERISK, "id as id_as"})
.orderBy(ID)
.eachById([&indexes, &ids](QSqlQuery &query, const int index)
{
indexes.emplace_back(index);
ids.emplace_back(query.value(ID).value<quint64>());
return index != 4; // false/interrupt on 4
},
1000, ID, "id_as");
QVERIFY(!result);
std::vector<int> expectedIndexes {0, 1, 2, 3, 4};
std::vector<quint64> expectedIds {1, 2, 3, 4, 5};
QVERIFY(indexes.size() == expectedIndexes.size());
QCOMPARE(indexes, expectedIndexes);
QVERIFY(ids.size() == expectedIds.size());
QCOMPARE(ids, expectedIds);
}
void tst_QueryBuilder::eachById_EmptyResult_WithAlias() const
{
QFETCH_GLOBAL(QString, connection);
auto result = createQuery(connection)->from("file_property_properties")
.select({ASTERISK, "id as id_as"})
.whereEq(NAME, QStringLiteral("dummy-NON_EXISTENT"))
.orderBy(ID)
.eachById([](QSqlQuery &/*unused*/, const int /*unused*/)
{
return true;
},
1000, ID, "id_as");
QVERIFY(result);
}
/* private */
std::shared_ptr<QueryBuilder>

View File

@@ -2,6 +2,8 @@
#include <QtTest>
#include "orm/db.hpp"
#include "orm/exceptions/multiplerecordsfounderror.hpp"
#include "orm/exceptions/recordsnotfounderror.hpp"
#include "orm/query/querybuilder.hpp"
#include "databases.hpp"
@@ -18,6 +20,8 @@ using Orm::Constants::OR;
using Orm::Constants::SIZE;
using Orm::DB;
using Orm::Exceptions::MultipleRecordsFoundError;
using Orm::Exceptions::RecordsNotFoundError;
using Orm::Query::Builder;
using Orm::Query::Expression;
@@ -180,6 +184,19 @@ private Q_SLOTS:
void remove() const;
void remove_WithExpression() const;
/* Builds Queries */
void tap() const;
void sole() const;
void sole_RecordsNotFoundError() const;
void sole_MultipleRecordsFoundError() const;
void sole_Pretending() const;
void soleValue() const;
void soleValue_RecordsNotFoundError() const;
void soleValue_MultipleRecordsFoundError() const;
void soleValue_Pretending() const;
// NOLINTNEXTLINE(readability-redundant-access-specifiers)
private:
/*! Create QueryBuilder instance for the given connection. */
@@ -2634,6 +2651,112 @@ void tst_MySql_QueryBuilder::remove_WithExpression() const
QVERIFY(firstLog.boundValues.isEmpty());
}
/* Builds Queries */
void tst_MySql_QueryBuilder::tap() const
{
auto builder = createQuery();
auto callbackInvoked = false;
auto &tappedBuilder = builder->tap([&callbackInvoked](QueryBuilder &query)
{
callbackInvoked = true;
return query;
});
QVERIFY(callbackInvoked);
// It must be the same QueryBuilder (the same memory address)
QVERIFY(reinterpret_cast<uintptr_t>(&*builder)
== reinterpret_cast<uintptr_t>(&tappedBuilder));
}
void tst_MySql_QueryBuilder::sole() const
{
auto query = createQuery()->from("torrents").whereEq(ID, 1).sole();
QVERIFY(query.isValid() && query.isActive() && query.isSelect());
QCOMPARE(query.value(ID).value<quint64>(), static_cast<quint64>(1));
QCOMPARE(query.value(NAME).value<QString>(), QStringLiteral("test1"));
}
void tst_MySql_QueryBuilder::sole_RecordsNotFoundError() const
{
QVERIFY_EXCEPTION_THROWN(
createQuery()->from("torrents").whereEq("name", "dummy-NON_EXISTENT").sole(),
RecordsNotFoundError);
}
void tst_MySql_QueryBuilder::sole_MultipleRecordsFoundError() const
{
QVERIFY_EXCEPTION_THROWN(
createQuery()->from("torrents").whereEq("user_id", 1).sole(),
MultipleRecordsFoundError);
}
void tst_MySql_QueryBuilder::sole_Pretending() const
{
auto log = DB::connection(m_connection).pretend([](auto &connection)
{
connection.query()->from("torrents").whereEq("name", "dummy-NON_EXISTENT").sole();
});
QVERIFY(!log.isEmpty());
const auto &firstLog = log.first();
QCOMPARE(log.size(), 1);
QCOMPARE(firstLog.query,
"select * from `torrents` where `name` = ? limit 2");
QCOMPARE(firstLog.boundValues,
QVector<QVariant>({QVariant(QString("dummy-NON_EXISTENT"))}));
}
void tst_MySql_QueryBuilder::soleValue() const
{
auto value = createQuery()->from("torrents").whereEq(ID, 1).soleValue(NAME);
QVERIFY((std::is_same_v<decltype (value), QVariant>));
QVERIFY(value.isValid() && !value.isNull());
QCOMPARE(value, QVariant(QStringLiteral("test1")));
}
void tst_MySql_QueryBuilder::soleValue_RecordsNotFoundError() const
{
QVERIFY_EXCEPTION_THROWN(
createQuery()->from("torrents")
.whereEq("name", "dummy-NON_EXISTENT")
.soleValue(NAME),
RecordsNotFoundError);
}
void tst_MySql_QueryBuilder::soleValue_MultipleRecordsFoundError() const
{
QVERIFY_EXCEPTION_THROWN(
createQuery()->from("torrents")
.whereEq("user_id", 1)
.soleValue(NAME),
MultipleRecordsFoundError);
}
void tst_MySql_QueryBuilder::soleValue_Pretending() const
{
auto log = DB::connection(m_connection).pretend([](auto &connection)
{
connection.query()->from("torrents")
.whereEq("name", "dummy-NON_EXISTENT")
.soleValue(NAME);
});
QVERIFY(!log.isEmpty());
const auto &firstLog = log.first();
QCOMPARE(log.size(), 1);
QCOMPARE(firstLog.query,
"select `name` from `torrents` where `name` = ? limit 2");
QCOMPARE(firstLog.boundValues,
QVector<QVariant>({QVariant(QString("dummy-NON_EXISTENT"))}));
}
/* private */
std::shared_ptr<QueryBuilder>