mirror of
https://github.com/silverqx/TinyORM.git
synced 2025-12-20 18:09:30 -06:00
added BuildsQueries concerns
Added chunk, each, chunkById, eachById, sole, tap in BuildsQueries and QueryBuilder::soleValue(). - added tests - added docs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
47
include/orm/exceptions/multiplerecordsfounderror.hpp
Normal file
47
include/orm/exceptions/multiplerecordsfounderror.hpp
Normal 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
|
||||
26
include/orm/exceptions/recordsnotfounderror.hpp
Normal file
26
include/orm/exceptions/recordsnotfounderror.hpp
Normal 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
|
||||
84
include/orm/query/concerns/buildsqueries.hpp
Normal file
84
include/orm/query/concerns/buildsqueries.hpp
Normal 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
|
||||
@@ -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>
|
||||
|
||||
219
src/orm/query/concerns/buildsqueries.cpp
Normal file
219
src/orm/query/concerns/buildsqueries.cpp
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user