mirror of
https://github.com/silverqx/TinyORM.git
synced 2026-01-24 20:08:51 -06:00
1586 lines
57 KiB
C++
1586 lines
57 KiB
C++
#pragma once
|
|
#ifndef ORM_TINY_TINYBUILDER_HPP
|
|
#define ORM_TINY_TINYBUILDER_HPP
|
|
|
|
#include "orm/macros/systemheader.hpp"
|
|
TINY_SYSTEM_HEADER
|
|
|
|
#include "orm/macros/sqldrivermappings.hpp"
|
|
#include TINY_INCLUDE_TSqlRecord
|
|
|
|
#include <range/v3/action/transform.hpp>
|
|
|
|
#include "orm/databaseconnection.hpp"
|
|
#include "orm/utils/helpers.hpp"
|
|
|
|
#include "orm/tiny/concerns/buildsqueries.hpp"
|
|
#include "orm/tiny/concerns/buildssoftdeletes.hpp"
|
|
#include "orm/tiny/concerns/queriesrelationships.hpp"
|
|
#include "orm/tiny/exceptions/modelnotfounderror.hpp"
|
|
#include "orm/tiny/tinybuilderproxies.hpp"
|
|
|
|
#ifdef TINYORM_NO_DEBUG
|
|
# include "orm/utils/query.hpp"
|
|
#endif
|
|
|
|
TINYORM_BEGIN_COMMON_NAMESPACE
|
|
|
|
namespace Orm::Tiny
|
|
{
|
|
|
|
/*! ORM Tiny builder (returns hydrated models instead of the QSqlQuery). */
|
|
template<typename Model>
|
|
class Builder : public Concerns::BuildsQueries<Model>,
|
|
public BuilderProxies<Model>,
|
|
public Concerns::QueriesRelationships<Model>,
|
|
public Concerns::BuildsSoftDeletes<Model, Model::extendsSoftDeletes()>
|
|
{
|
|
// Used by TinyBuilderProxies::where/latest/oldest/update()
|
|
friend BuilderProxies<Model>;
|
|
// To access enforceOrderBy(), and defaultKeyName()
|
|
friend class Concerns::BuildsQueries<Model>;
|
|
|
|
/*! Alias for the attribute utils. */
|
|
using AttributeUtils = Orm::Tiny::Utils::Attribute;
|
|
/*! Alias for the helper utils. */
|
|
using Helpers = Orm::Utils::Helpers;
|
|
/*! Alias for the query utils. */
|
|
using QueryUtils = Orm::Utils::Query;
|
|
/*! Alias for the type utils. */
|
|
using TypeUtils = Orm::Utils::Type;
|
|
|
|
public:
|
|
/*! Constructor. */
|
|
Builder(std::shared_ptr<QueryBuilder> &&query, const Model &model); // NOLINT(modernize-pass-by-value)
|
|
/*! Default destructor. */
|
|
~Builder() = default;
|
|
|
|
/*! Copy constructor (needed by the chunkById() -> clone() method). */
|
|
Builder(const Builder &) = default;
|
|
/*! Deleted copy assignment operator (not needed). */
|
|
Builder &operator=(const Builder &) = delete;
|
|
|
|
/*! Move constructor (copy ctor needed so enable also the move ctor). */
|
|
Builder(Builder &&) noexcept = default;
|
|
/*! Deleted move assignment operator (not needed). */
|
|
Builder &operator=(Builder &&) = delete;
|
|
|
|
/*! Get the SQL representation of the query. */
|
|
inline QString toSql();
|
|
/*! Get the current query value bindings as flattened QList. */
|
|
inline QList<QVariant> getBindings() const;
|
|
|
|
/* Retrieving results */
|
|
/*! Execute the query as a "select" statement. */
|
|
ModelsCollection<Model> get(const QList<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 a vector with values in the given column. */
|
|
QList<QVariant> pluck(const Column &column);
|
|
/*! Get a map with values in the given column and keyed by values in the key
|
|
column. */
|
|
template<typename T>
|
|
std::map<T, QVariant> pluck(const Column &column, const Column &key);
|
|
|
|
/*! Find a model by its primary key. */
|
|
std::optional<Model>
|
|
find(const QVariant &id, const QList<Column> &columns = {ASTERISK});
|
|
/*! Find a model by its primary key or return fresh model instance. */
|
|
Model findOrNew(const QVariant &id, const QList<Column> &columns = {ASTERISK});
|
|
/*! Find a model by its primary key or throw an exception. */
|
|
Model findOrFail(const QVariant &id,
|
|
const QList<Column> &columns = {ASTERISK});
|
|
/*! Find multiple models by their primary keys. */
|
|
ModelsCollection<Model>
|
|
findMany(const QList<QVariant> &ids,
|
|
const QList<Column> &columns = {ASTERISK});
|
|
|
|
/*! Execute a query for a single record by ID or call a callback. */
|
|
std::optional<Model>
|
|
findOr(const QVariant &id, const QList<Column> &columns,
|
|
const std::function<void()> &callback = nullptr);
|
|
/*! Execute a query for a single record by ID or call a callback. */
|
|
std::optional<Model>
|
|
findOr(const QVariant &id, const std::function<void()> &callback = nullptr);
|
|
|
|
/*! Execute a query for a single record by ID or call a callback. */
|
|
template<typename R>
|
|
std::pair<std::optional<Model>, R>
|
|
findOr(const QVariant &id, const QList<Column> &columns,
|
|
const std::function<R()> &callback);
|
|
/*! Execute a query for a single record by ID or call a callback. */
|
|
template<typename R>
|
|
std::pair<std::optional<Model>, R>
|
|
findOr(const QVariant &id, const std::function<R()> &callback);
|
|
|
|
/*! Execute the query and get the first result. */
|
|
std::optional<Model> first(const QList<Column> &columns = {ASTERISK});
|
|
/*! Get the first record matching the attributes or instantiate it. */
|
|
Model firstOrNew(const QList<WhereItem> &attributes = {},
|
|
const QList<AttributeItem> &values = {});
|
|
/*! Get the first record matching the attributes or create it. */
|
|
Model firstOrCreate(const QList<WhereItem> &attributes = {},
|
|
const QList<AttributeItem> &values = {});
|
|
/*! Execute the query and get the first result or throw an exception. */
|
|
Model firstOrFail(const QList<Column> &columns = {ASTERISK});
|
|
|
|
/*! Execute the query and get the first result or call a callback. */
|
|
std::optional<Model>
|
|
firstOr(const QList<Column> &columns,
|
|
const std::function<void()> &callback = nullptr);
|
|
/*! Execute the query and get the first result or call a callback. */
|
|
std::optional<Model>
|
|
firstOr(const std::function<void()> &callback = nullptr);
|
|
|
|
/*! Execute the query and get the first result or call a callback. */
|
|
template<typename R>
|
|
std::pair<std::optional<Model>, R>
|
|
firstOr(const QList<Column> &columns, const std::function<R()> &callback);
|
|
/*! Execute the query and get the first result or call a callback. */
|
|
template<typename R>
|
|
std::pair<std::optional<Model>, R>
|
|
firstOr(const std::function<R()> &callback);
|
|
|
|
/*! Add a basic where clause to the query, and return the first result. */
|
|
std::optional<Model>
|
|
firstWhere(const Column &column, const QString &comparison,
|
|
const QVariant &value, const QString &condition = AND);
|
|
/*! Add a basic equal where clause to the query, and return the first result. */
|
|
std::optional<Model>
|
|
firstWhereEq(const Column &column, const QVariant &value,
|
|
const QString &condition = AND);
|
|
|
|
/*! Add a where clause on the primary key to the query. */
|
|
Builder &whereKey(const QVariant &id);
|
|
/*! Add a where clause on the primary key to the query. */
|
|
Builder &whereKey(const QList<QVariant> &ids);
|
|
/*! Add a where clause on the primary key to the query. */
|
|
Builder &whereKeyNot(const QVariant &id);
|
|
/*! Add a where clause on the primary key to the query. */
|
|
Builder &whereKeyNot(const QList<QVariant> &ids);
|
|
|
|
/*! Set the relationships that should be eager loaded. */
|
|
template<typename = void>
|
|
Builder &with(const QList<WithItem> &relations);
|
|
/*! Set the relationships that should be eager loaded. */
|
|
template<typename = void>
|
|
Builder &with(QString relation);
|
|
/*! Set the relationships that should be eager loaded. */
|
|
inline Builder &with(const QList<QString> &relations);
|
|
/*! Set the relationships that should be eager loaded. */
|
|
inline Builder &with(QList<QString> &&relations);
|
|
|
|
/*! Prevent the specified relations from being eager loaded. */
|
|
Builder &without(const QList<QString> &relations);
|
|
/*! Prevent the specified relations from being eager loaded. */
|
|
inline Builder &without(QString relation);
|
|
|
|
/*! Set the relationships that should be eager loaded while removing
|
|
any previously added eager loading specifications. */
|
|
template<typename = void>
|
|
Builder &withOnly(const QList<WithItem> &relations);
|
|
/*! Set the relationship that should be eager loaded while removing
|
|
any previously added eager loading specifications. */
|
|
template<typename = void>
|
|
Builder &withOnly(QString relation);
|
|
/*! Set the relationships that should be eager loaded while removing
|
|
any previously added eager loading specifications. */
|
|
inline Builder &withOnly(const QList<QString> &relations);
|
|
/*! Set the relationships that should be eager loaded while removing
|
|
any previously added eager loading specifications. */
|
|
inline Builder &withOnly(QList<QString> &&relations);
|
|
|
|
/* Insert, Update, Delete */
|
|
/*! Save a new model and return the instance. */
|
|
Model create(const QList<AttributeItem> &attributes = {});
|
|
/*! Save a new model and return the instance. */
|
|
Model create(QList<AttributeItem> &&attributes = {});
|
|
|
|
/*! Create or update a record matching the attributes, and fill it with
|
|
values. */
|
|
Model updateOrCreate(const QList<WhereItem> &attributes,
|
|
const QList<AttributeItem> &values = {});
|
|
|
|
/*! Update the column's update timestamp. */
|
|
std::tuple<int, std::optional<TSqlQuery>>
|
|
touch(const QString &column = "");
|
|
|
|
/* QueryBuilder proxy methods that need modifications */
|
|
/*! Update records in the database. */
|
|
std::tuple<int, TSqlQuery>
|
|
update(const QList<UpdateItem> &values);
|
|
|
|
/*! Delete records from the database. */
|
|
std::tuple<int, TSqlQuery> remove();
|
|
/*! Delete records from the database, alias. */
|
|
inline std::tuple<int, TSqlQuery> deleteModels();
|
|
|
|
/*! Insert new records or update the existing ones. */
|
|
std::tuple<int, std::optional<TSqlQuery>>
|
|
upsert(const QList<QVariantMap> &values, const QStringList &uniqueBy,
|
|
const QStringList &update);
|
|
/*! Insert new records or update the existing ones (update all columns). */
|
|
std::tuple<int, std::optional<TSqlQuery>>
|
|
upsert(const QList<QVariantMap> &values, const QStringList &uniqueBy);
|
|
|
|
/* Casting Attributes */
|
|
/*! Apply query-time casts to the model instance. */
|
|
inline Builder &withCasts(const std::unordered_map<QString, CastItem> &casts);
|
|
/*! Apply query-time casts to the model instance. */
|
|
inline Builder &withCasts(std::unordered_map<QString, CastItem> &casts);
|
|
/*! Apply query-time casts to the model instance. */
|
|
inline Builder &withCasts(std::unordered_map<QString, CastItem> &&casts);
|
|
/*! Apply query-time cast to the model instance. */
|
|
inline Builder &withCast(std::pair<QString, CastItem> cast);
|
|
|
|
/* TinyBuilder methods */
|
|
/*! Clone the Tiny query. */
|
|
inline Builder clone() const;
|
|
/*! Create a new instance of the model being queried. */
|
|
Model newModelInstance(const QList<AttributeItem> &attributes) const;
|
|
/*! Create a new instance of the model being queried. */
|
|
Model newModelInstance(QList<AttributeItem> &&attributes = {}) const;
|
|
|
|
/*! Get the hydrated models without eager loading. */
|
|
ModelsCollection<Model> getModels(const QList<Column> &columns = {ASTERISK});
|
|
|
|
/*! Eager load the relationships for the models. */
|
|
template<SameDerivedCollectionModel<Model> CollectionModel>
|
|
void eagerLoadRelations(ModelsCollection<CollectionModel> &models) const;
|
|
/*! Eager load the relationships on the model. */
|
|
void eagerLoadRelations(Model &model) const;
|
|
|
|
/*! Eagerly load the relationship on a set of models. */
|
|
template<typename Relation, SameDerivedCollectionModel<Model> CollectionModel>
|
|
void eagerLoadRelationVisited(
|
|
Relation &relation, ModelsCollection<CollectionModel> &models,
|
|
const WithItem &relationItem) const;
|
|
|
|
/*! Create a vector of models from the SqlQuery. */
|
|
ModelsCollection<Model> hydrate(SqlQuery &&result) const; // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
|
|
|
|
/*! Get the model instance being queried. */
|
|
inline Model &getModel() noexcept;
|
|
/*! Get the underlying query builder instance. */
|
|
inline QueryBuilder &getQuery() const noexcept;
|
|
/*! Get the underlying query builder instance as a std::shared_ptr. */
|
|
inline const std::shared_ptr<QueryBuilder> &getQueryShared() const noexcept;
|
|
|
|
/*! Get a database connection. */
|
|
inline DatabaseConnection &getConnection();
|
|
/*! Get the query grammar instance. */
|
|
inline const QueryGrammar &getGrammar();
|
|
|
|
/*! Get a base query builder instance. */
|
|
QueryBuilder &toBase();
|
|
|
|
/*! Qualify the given column name by the model's table. */
|
|
inline QString qualifyColumn(const QString &column) const;
|
|
|
|
/*! Register a replacement for the default delete function. */
|
|
inline void
|
|
onDelete(std::function<std::tuple<int, TSqlQuery>(Builder<Model> &)> &&callback);
|
|
|
|
/* BuildsSoftDeletes */
|
|
/*! Apply the SoftDeletes where null condition for the deleted_at column. */
|
|
Builder<Model> &applySoftDeletes();
|
|
/*! Determine whether the Model the TinyBuilder manages extends SoftDeletes. */
|
|
constexpr static bool extendsSoftDeletes() noexcept;
|
|
|
|
protected:
|
|
/*! Alias for the Expression. */
|
|
using Expression = Orm::Query::Expression;
|
|
|
|
/*! Get the default key name of the table. */
|
|
inline const QString &defaultKeyName() const;
|
|
|
|
/*! Parse a list of relations into individuals. */
|
|
QList<WithItem> parseWithRelations(const QList<WithItem> &relations);
|
|
/*! Create a constraint to select the given columns for the relation. */
|
|
WithItem createSelectWithConstraint(const QString &name);
|
|
/*! Parse the nested relationships in a relation. */
|
|
void addNestedWiths(const QString &name, QList<WithItem> &results) const;
|
|
|
|
/*! Guess number of relations for the reserve (including nested relations). */
|
|
static QList<WithItem>::size_type
|
|
guessParseWithRelationsSize(const QList<WithItem> &relations);
|
|
|
|
/*! Get the deeply nested relations for a given top-level relation. */
|
|
QList<WithItem>
|
|
relationsNestedUnder(const QString &topRelationName) const;
|
|
/*! Determine if the relationship is nested. */
|
|
bool isNestedUnder(const QString &topRelation,
|
|
const QString &nestedRelation) const;
|
|
|
|
/*! Add the "updated at" column to the vector of values. */
|
|
QList<UpdateItem>
|
|
addUpdatedAtColumn(QList<UpdateItem> values) const;
|
|
|
|
/*! Add timestamps to the inserted values. */
|
|
QList<QVariantMap>
|
|
addTimestampsToUpsertValues(const QList<QVariantMap> &values) const;
|
|
/*! Add the "updated at" column to the updated columns. */
|
|
QStringList addUpdatedAtToUpsertColumns(const QStringList &update) const;
|
|
|
|
/*! Get the name of the "created at" column. */
|
|
Column getCreatedAtColumnForLatestOldest(Column column) const;
|
|
|
|
/*! Add a generic "order by" clause if the query doesn't already have one. */
|
|
void enforceOrderBy();
|
|
|
|
/*! Apply the given scope on the current builder instance. */
|
|
// template<typename ...Args>
|
|
// Builder &callScope(const std::function<void(Builder &, Args ...)> &scope,
|
|
// Args &&...parameters);
|
|
|
|
/*! The base query builder instance. */
|
|
/*const*/ std::shared_ptr<QueryBuilder> m_query;
|
|
/* This can't be a reference because the model is created on the stack
|
|
in Model::query(), then copied here and the original is destroyed
|
|
immediately. */
|
|
/*! The model being queried. */
|
|
Model m_model;
|
|
/*! The relationships that should be eager loaded. */
|
|
QList<WithItem> m_eagerLoad;
|
|
|
|
/*! A replacement for the typical delete function. */
|
|
std::function<std::tuple<int, TSqlQuery>(Builder<Model> &)> m_onDelete = nullptr;
|
|
|
|
/* BuildsSoftDeletes */
|
|
/*! Determine whether the Model the TinyBuilder manages extends SoftDeletes. */
|
|
constexpr static bool m_extendsSoftDeletes = Model::extendsSoftDeletes();
|
|
};
|
|
|
|
/* public */
|
|
|
|
template<typename Model>
|
|
Builder<Model>::Builder(std::shared_ptr<QueryBuilder> &&query, const Model &model)
|
|
: m_query(std::move(query))
|
|
, m_model(model)
|
|
{
|
|
m_query->from(m_model.getTable());
|
|
|
|
// Initialize the BuildsSoftDeletes concern (registers onDelete callback)
|
|
if constexpr (m_extendsSoftDeletes)
|
|
this->initializeBuildsSoftDeletes();
|
|
}
|
|
|
|
template<typename Model>
|
|
QString Builder<Model>::toSql()
|
|
{
|
|
return toBase().toSql();
|
|
}
|
|
|
|
template<typename Model>
|
|
QList<QVariant> Builder<Model>::getBindings() const
|
|
{
|
|
// toBase() not needed as the BuildsSoftDeletes is not adding any new bindings
|
|
return getQuery().getBindings();
|
|
}
|
|
|
|
/* Retrieving results */
|
|
|
|
template<typename Model>
|
|
ModelsCollection<Model>
|
|
Builder<Model>::get(const QList<Column> &columns)
|
|
{
|
|
applySoftDeletes();
|
|
|
|
ModelsCollection<Model> models = getModels(columns);
|
|
|
|
/* If we actually found models we will also eager load any relationships that
|
|
have been specified as needing to be eager loaded, which will solve the
|
|
n+1 query issue for the developers to avoid running a lot of queries. */
|
|
if (!models.isEmpty())
|
|
/* 'models' are passed down as the reference and relations are set on models
|
|
at the end of the call tree, no need to return models. */
|
|
eagerLoadRelations(models);
|
|
|
|
return models;
|
|
// FUTURE if I will implement custom container for the Models, this is right place to do it silverqx
|
|
// return getModel().newCollection(models);
|
|
}
|
|
|
|
template<typename Model>
|
|
QVariant Builder<Model>::value(const Column &column)
|
|
{
|
|
auto model = first({column});
|
|
|
|
if (!model)
|
|
return {};
|
|
|
|
// Expression support
|
|
QString column_;
|
|
|
|
if (std::holds_alternative<Expression>(column))
|
|
column_ = std::get<Expression>(column).getValue().value<QString>();
|
|
else
|
|
column_ = std::get<QString>(column);
|
|
|
|
if (column_.contains(DOT))
|
|
column_ = column_.sliced(column_.lastIndexOf(DOT) + 1);
|
|
|
|
return model->getAttribute(column_);
|
|
}
|
|
|
|
template<typename Model>
|
|
QVariant Builder<Model>::soleValue(const Column &column)
|
|
{
|
|
auto model = this->sole({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);
|
|
|
|
if (column_.contains(DOT))
|
|
column_ = column_.sliced(column_.lastIndexOf(DOT) + 1);
|
|
|
|
return model.getAttribute(column_);
|
|
}
|
|
|
|
template<typename Model>
|
|
QList<QVariant> Builder<Model>::pluck(const Column &column)
|
|
{
|
|
auto result = toBase().pluck(column);
|
|
|
|
// Nothing to pluck-ing 😎
|
|
if (result.empty())
|
|
return result;
|
|
|
|
/* If the column is qualified with a table or have an alias, we cannot use
|
|
those directly in the "pluck" operations, we have to strip the table out or
|
|
use the alias name instead. */
|
|
const auto unqualifiedColumn = getQuery().stripTableForPluck(column);
|
|
|
|
/* If the model has a mutator for the requested column or it's a datetime column,
|
|
we will spin through the results and mutate the values so that the mutated
|
|
version of these columns are returned as you would expect from these TinyORM
|
|
models. */
|
|
if (!m_model.getDates().contains(unqualifiedColumn))
|
|
return result;
|
|
|
|
return result |= ranges::actions::transform([this, &unqualifiedColumn]
|
|
(auto &&value)
|
|
{
|
|
return m_model.newFromBuilder({{unqualifiedColumn,
|
|
std::forward<decltype (value)>(value)}})
|
|
.getAttribute(unqualifiedColumn);
|
|
});
|
|
}
|
|
|
|
template<typename Model>
|
|
template<typename T>
|
|
std::map<T, QVariant>
|
|
Builder<Model>::pluck(const Column &column, const Column &key)
|
|
{
|
|
auto result = toBase().template pluck<T>(column, key);
|
|
|
|
// Nothing to pluck-ing 😎
|
|
if (result.empty())
|
|
return result;
|
|
|
|
/* If the column is qualified with a table or have an alias, we cannot use
|
|
those directly in the "pluck" operations, we have to strip the table out or
|
|
use the alias name instead. */
|
|
const auto unqualifiedColumn = getQuery().stripTableForPluck(column);
|
|
|
|
/* If the model has a mutator for the requested column or it's a datetime column,
|
|
we will spin through the results and mutate the values so that the mutated
|
|
version of these columns are returned as you would expect from these TinyORM
|
|
models. */
|
|
if (!m_model.getDates().contains(unqualifiedColumn))
|
|
return result;
|
|
|
|
for (auto &&[_, value] : result)
|
|
value = m_model.newFromBuilder({{unqualifiedColumn, std::move(value)}})
|
|
.getAttribute(unqualifiedColumn);
|
|
|
|
return result;
|
|
}
|
|
|
|
// FEATURE dilemma primarykey, Model::KeyType for id silverqx
|
|
template<typename Model>
|
|
std::optional<Model>
|
|
Builder<Model>::find(const QVariant &id, const QList<Column> &columns)
|
|
{
|
|
return whereKey(id).first(columns);
|
|
}
|
|
|
|
template<typename Model>
|
|
Model Builder<Model>::findOrNew(const QVariant &id, const QList<Column> &columns)
|
|
{
|
|
auto model = find(id, columns);
|
|
|
|
// Found
|
|
if (model)
|
|
return std::move(*model);
|
|
|
|
return newModelInstance();
|
|
}
|
|
|
|
template<typename Model>
|
|
Model Builder<Model>::findOrFail(const QVariant &id, const QList<Column> &columns)
|
|
{
|
|
auto model = find(id, columns);
|
|
|
|
// Found
|
|
if (model)
|
|
return std::move(*model);
|
|
|
|
throw Exceptions::ModelNotFoundError(
|
|
TypeUtils::classPureBasename<Model>(), {id});
|
|
}
|
|
|
|
template<typename Model>
|
|
ModelsCollection<Model>
|
|
Builder<Model>::findMany(const QList<QVariant> &ids,
|
|
const QList<Column> &columns)
|
|
{
|
|
if (ids.isEmpty())
|
|
return {};
|
|
|
|
return whereKey(ids).get(columns);
|
|
}
|
|
|
|
template<typename Model>
|
|
std::optional<Model>
|
|
Builder<Model>::findOr(const QVariant &id, const QList<Column> &columns,
|
|
const std::function<void()> &callback)
|
|
{
|
|
auto model = find(id, columns);
|
|
|
|
if (model)
|
|
return model;
|
|
|
|
// Optionally invoke the callback
|
|
if (callback)
|
|
std::invoke(callback);
|
|
|
|
// Return an original model from the find() method
|
|
return model;
|
|
}
|
|
|
|
template<typename Model>
|
|
std::optional<Model>
|
|
Builder<Model>::findOr(const QVariant &id, const std::function<void()> &callback)
|
|
{
|
|
return findOr(id, {ASTERISK}, callback);
|
|
}
|
|
|
|
template<typename Model>
|
|
template<typename R>
|
|
std::pair<std::optional<Model>, R>
|
|
Builder<Model>::findOr(const QVariant &id, const QList<Column> &columns,
|
|
const std::function<R()> &callback)
|
|
{
|
|
auto model = find(id, columns);
|
|
|
|
if (model)
|
|
return {std::move(model), R {}};
|
|
|
|
// Return an original model from the find() method instead of the default Model{}
|
|
|
|
// Optionally invoke the callback
|
|
if (callback)
|
|
return {std::move(model), std::invoke(callback)};
|
|
|
|
return {std::move(model), R {}};
|
|
}
|
|
|
|
template<typename Model>
|
|
template<typename R>
|
|
std::pair<std::optional<Model>, R>
|
|
Builder<Model>::findOr(const QVariant &id, const std::function<R()> &callback)
|
|
{
|
|
return findOr<R>(id, {ASTERISK}, callback);
|
|
}
|
|
|
|
template<typename Model>
|
|
std::optional<Model>
|
|
Builder<Model>::first(const QList<Column> &columns)
|
|
{
|
|
auto models = this->take(1).get(columns);
|
|
|
|
if (models.isEmpty())
|
|
return std::nullopt;
|
|
|
|
return std::move(models.first());
|
|
}
|
|
|
|
template<typename Model>
|
|
Model
|
|
Builder<Model>::firstOrNew(const QList<WhereItem> &attributes,
|
|
const QList<AttributeItem> &values)
|
|
{
|
|
auto instance = this->where(attributes).first();
|
|
|
|
// Model found in db
|
|
if (instance)
|
|
return std::move(*instance);
|
|
|
|
return newModelInstance(AttributeUtils::joinAttributesForFirstOr(
|
|
attributes, values, m_model.getKeyName()));
|
|
}
|
|
|
|
template<typename Model>
|
|
Model
|
|
Builder<Model>::firstOrCreate(const QList<WhereItem> &attributes,
|
|
const QList<AttributeItem> &values)
|
|
{
|
|
// Model found in db
|
|
if (auto instance = this->where(attributes).first(); instance)
|
|
return std::move(*instance);
|
|
|
|
return Helpers::tap<Model>(
|
|
newModelInstance(AttributeUtils::joinAttributesForFirstOr(
|
|
attributes, values, m_model.getKeyName())),
|
|
[](auto &newInstance)
|
|
{
|
|
newInstance.save();
|
|
});
|
|
}
|
|
|
|
template<typename Model>
|
|
Model Builder<Model>::firstOrFail(const QList<Column> &columns)
|
|
{
|
|
auto model = first(columns);
|
|
|
|
// Found
|
|
if (model)
|
|
return std::move(*model);
|
|
|
|
throw Exceptions::ModelNotFoundError(TypeUtils::classPureBasename<Model>());
|
|
}
|
|
|
|
template<typename Model>
|
|
std::optional<Model>
|
|
Builder<Model>::firstOr(const QList<Column> &columns,
|
|
const std::function<void()> &callback)
|
|
{
|
|
auto model = first(columns);
|
|
|
|
if (model)
|
|
return model;
|
|
|
|
// Optionally invoke the callback
|
|
if (callback)
|
|
std::invoke(callback);
|
|
|
|
// Return an original model from the find() method
|
|
return model;
|
|
}
|
|
|
|
template<typename Model>
|
|
std::optional<Model>
|
|
Builder<Model>::firstOr(const std::function<void()> &callback)
|
|
{
|
|
return firstOr({ASTERISK}, callback);
|
|
}
|
|
|
|
template<typename Model>
|
|
template<typename R>
|
|
std::pair<std::optional<Model>, R>
|
|
Builder<Model>::firstOr(const QList<Column> &columns,
|
|
const std::function<R()> &callback)
|
|
{
|
|
auto model = first(columns);
|
|
|
|
if (model)
|
|
return {std::move(model), R {}};
|
|
|
|
// Return an original model from the find() method instead of the default Model{}
|
|
|
|
// Optionally invoke the callback
|
|
if (callback)
|
|
return {std::move(model), std::invoke(callback)};
|
|
|
|
return {std::move(model), R {}};
|
|
}
|
|
|
|
template<typename Model>
|
|
template<typename R>
|
|
std::pair<std::optional<Model>, R>
|
|
Builder<Model>::firstOr(const std::function<R()> &callback)
|
|
{
|
|
return firstOr<R>({ASTERISK}, callback);
|
|
}
|
|
|
|
template<typename Model>
|
|
std::optional<Model>
|
|
Builder<Model>::firstWhere(const Column &column, const QString &comparison,
|
|
const QVariant &value, const QString &condition)
|
|
{
|
|
return this->where(column, comparison, value, condition).first();
|
|
}
|
|
|
|
template<typename Model>
|
|
std::optional<Model>
|
|
Builder<Model>::firstWhereEq(const Column &column, const QVariant &value,
|
|
const QString &condition)
|
|
{
|
|
return this->where(column, EQ, value, condition).first();
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::whereKey(const QVariant &id)
|
|
{
|
|
return this->where(m_model.getQualifiedKeyName(), EQ, id);
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::whereKey(const QList<QVariant> &ids)
|
|
{
|
|
m_query->whereIn(m_model.getQualifiedKeyName(), ids);
|
|
|
|
return *this;
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::whereKeyNot(const QVariant &id)
|
|
{
|
|
return this->where(m_model.getQualifiedKeyName(), NE, id);
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::whereKeyNot(const QList<QVariant> &ids)
|
|
{
|
|
m_query->whereNotIn(m_model.getQualifiedKeyName(), ids);
|
|
|
|
return *this;
|
|
}
|
|
|
|
template<typename Model>
|
|
template<typename>
|
|
Builder<Model> &
|
|
Builder<Model>::with(const QList<WithItem> &relations)
|
|
{
|
|
/* Don't make the rvalue variant or pass relations by value, I have tested it and
|
|
it's ~0.4ms slower, very interesting. 😮 Talking about the with() and
|
|
the parseWithRelations() methods. */
|
|
auto eagerLoad = parseWithRelations(relations);
|
|
|
|
m_eagerLoad.reserve(eagerLoad.size());
|
|
|
|
std::ranges::move(eagerLoad, std::back_inserter(m_eagerLoad));
|
|
|
|
return *this;
|
|
}
|
|
|
|
template<typename Model>
|
|
template<typename>
|
|
Builder<Model> &
|
|
Builder<Model>::with(QString relation)
|
|
{
|
|
return with(QList<WithItem> {{std::move(relation)}});
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::with(const QList<QString> &relations)
|
|
{
|
|
return with(WithItem::fromStringVector(relations));
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::with(QList<QString> &&relations)
|
|
{
|
|
return with(WithItem::fromStringVector(std::move(relations)));
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::without(const QList<QString> &relations)
|
|
{
|
|
// Remove relations in the "relations" vector from the m_eagerLoad vector
|
|
m_eagerLoad = m_eagerLoad
|
|
| ranges::views::remove_if([&relations](const WithItem &with)
|
|
{
|
|
return relations.contains(with.name);
|
|
})
|
|
| ranges::to<QList<WithItem>>();
|
|
|
|
return *this;
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::without(QString relation)
|
|
{
|
|
return without(QList<QString> {std::move(relation)});
|
|
}
|
|
|
|
template<typename Model>
|
|
template<typename>
|
|
Builder<Model> &
|
|
Builder<Model>::withOnly(const QList<WithItem> &relations)
|
|
{
|
|
m_eagerLoad.clear();
|
|
|
|
return with(relations);
|
|
}
|
|
|
|
template<typename Model>
|
|
template<typename>
|
|
Builder<Model> &
|
|
Builder<Model>::withOnly(QString relation)
|
|
{
|
|
return withOnly(QList<WithItem> {{std::move(relation)}});
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::withOnly(const QList<QString> &relations)
|
|
{
|
|
return withOnly(WithItem::fromStringVector(relations));
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::withOnly(QList<QString> &&relations)
|
|
{
|
|
return withOnly(WithItem::fromStringVector(std::move(relations)));
|
|
}
|
|
|
|
/* Insert, Update, Delete */
|
|
|
|
template<typename Model>
|
|
Model Builder<Model>::create(const QList<AttributeItem> &attributes)
|
|
{
|
|
auto model = newModelInstance(attributes);
|
|
|
|
model.save();
|
|
|
|
return model;
|
|
}
|
|
|
|
template<typename Model>
|
|
Model Builder<Model>::create(QList<AttributeItem> &&attributes)
|
|
{
|
|
auto model = newModelInstance(std::move(attributes));
|
|
|
|
model.save();
|
|
|
|
return model;
|
|
}
|
|
|
|
template<typename Model>
|
|
Model Builder<Model>::updateOrCreate(const QList<WhereItem> &attributes,
|
|
const QList<AttributeItem> &values)
|
|
{
|
|
auto instance = firstOrNew(attributes);
|
|
|
|
instance.fill(values).save();
|
|
|
|
return instance;
|
|
}
|
|
|
|
template<typename Model>
|
|
std::tuple<int, std::optional<TSqlQuery>>
|
|
Builder<Model>::touch(const QString &column)
|
|
{
|
|
auto time = m_model.freshTimestamp();
|
|
|
|
if (!column.isEmpty())
|
|
return toBase().update({{column, std::move(time)}});
|
|
|
|
const auto &updatedAtColumn = m_model.getUpdatedAtColumn();
|
|
|
|
if (!m_model.usesTimestamps() || updatedAtColumn.isEmpty())
|
|
return {0, std::nullopt};
|
|
|
|
return toBase().update({{updatedAtColumn, std::move(time)}});
|
|
}
|
|
|
|
/* QueryBuilder proxy methods that need modifications */
|
|
|
|
template<typename Model>
|
|
std::tuple<int, TSqlQuery>
|
|
Builder<Model>::update(const QList<UpdateItem> &values)
|
|
{
|
|
return toBase().update(addUpdatedAtColumn(values));
|
|
}
|
|
|
|
template<typename Model>
|
|
std::tuple<int, TSqlQuery> Builder<Model>::remove()
|
|
{
|
|
// Custom onDelete callback registered
|
|
if (m_onDelete)
|
|
return std::invoke(m_onDelete, *this);
|
|
|
|
return toBase().deleteRow();
|
|
}
|
|
|
|
template<typename Model>
|
|
std::tuple<int, TSqlQuery> Builder<Model>::deleteModels()
|
|
{
|
|
return remove();
|
|
}
|
|
|
|
template<typename Model>
|
|
std::tuple<int, std::optional<TSqlQuery>>
|
|
Builder<Model>::upsert(
|
|
const QList<QVariantMap> &values, const QStringList &uniqueBy,
|
|
const QStringList &update)
|
|
{
|
|
// Nothing to do, no values to insert or update
|
|
if (values.isEmpty())
|
|
return {0, std::nullopt};
|
|
|
|
// NOTE api different, don't call insert, it's useless silverqx
|
|
// If the update is an empty vector then throw and don't insert
|
|
if (update.isEmpty())
|
|
throw Orm::Exceptions::InvalidArgumentError(
|
|
"The upsert method doesn't support an empty update argument, please "
|
|
"use the insert method instead.");
|
|
|
|
return toBase().upsert(addTimestampsToUpsertValues(values), uniqueBy,
|
|
addUpdatedAtToUpsertColumns(update));
|
|
}
|
|
|
|
template<typename Model>
|
|
std::tuple<int, std::optional<TSqlQuery>>
|
|
Builder<Model>::upsert(
|
|
const QList<QVariantMap> &values, const QStringList &uniqueBy)
|
|
{
|
|
// Update all columns
|
|
// Columns are obtained only from a first QMap
|
|
const auto update = values.constFirst().keys();
|
|
|
|
return upsert(values, uniqueBy, update);
|
|
}
|
|
|
|
/* Casting Attributes */
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::withCasts(const std::unordered_map<QString, CastItem> &casts)
|
|
{
|
|
m_model.mergeCasts(casts);
|
|
|
|
return *this;
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::withCasts(std::unordered_map<QString, CastItem> &casts)
|
|
{
|
|
m_model.mergeCasts(casts);
|
|
|
|
return *this;
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::withCasts(std::unordered_map<QString, CastItem> &&casts)
|
|
{
|
|
m_model.mergeCasts(std::move(casts));
|
|
|
|
return *this;
|
|
}
|
|
|
|
template<typename Model>
|
|
Builder<Model> &
|
|
Builder<Model>::withCast(std::pair<QString, CastItem> cast)
|
|
{
|
|
m_model.mergeCasts({std::move(cast)});
|
|
|
|
return *this;
|
|
}
|
|
|
|
/* TinyBuilder methods */
|
|
|
|
template<typename Model>
|
|
Builder<Model> Builder<Model>::clone() const
|
|
{
|
|
return *this;
|
|
}
|
|
|
|
template<typename Model>
|
|
Model Builder<Model>::newModelInstance(const QList<AttributeItem> &attributes) const
|
|
{
|
|
return m_model.newInstance(attributes)
|
|
.setConnection(m_query->getConnection().getName());
|
|
}
|
|
|
|
template<typename Model>
|
|
Model Builder<Model>::newModelInstance(QList<AttributeItem> &&attributes) const
|
|
{
|
|
return m_model.newInstance(std::move(attributes))
|
|
.setConnection(m_query->getConnection().getName());
|
|
}
|
|
|
|
template<typename Model>
|
|
ModelsCollection<Model>
|
|
Builder<Model>::getModels(const QList<Column> &columns)
|
|
{
|
|
return hydrate(m_query->get(columns));
|
|
}
|
|
|
|
// TODO docs add similar note for lazy load silverqx
|
|
/* Look also at EagerRelationStore::visited(), where the whole flow begins.
|
|
How this relation flow works:
|
|
EagerRelationStore::visited() obtains a reference by the relation name
|
|
to the relation method, these relation methods are defined on models
|
|
as member functions.
|
|
References to the relation methods are defined in the Model::u_relations
|
|
hash as lambda expressions. These lambda expressions will be visited/invoked
|
|
by the EagerRelationStore::visited() to obtain references to the relation
|
|
methods.
|
|
Relation constraints will be disabled for eager relations by
|
|
the Relation::noConstraints() method, these default constraints are only used
|
|
for lazy loading, for eager constraints are used constraints, which are
|
|
defined by the Relation::addEagerConstraints() virtual methods.
|
|
To the Relation::noConstraints() method is passed a lambda, which invokes
|
|
obtained reference to the relation method defined on the model and invokes it
|
|
on the 'new' model instance refered as the 'dummyModel'.
|
|
The Relation instance is created by this relation method, this relation
|
|
method calls a factory method, which creates this Relation instance.
|
|
Every Relation has it's own Relation::create() factory method, to which
|
|
the following parameters are passed, a newly created Related model instance,
|
|
current/parent model instance, database column names of the relationship, and
|
|
for BelongsTo relations also the name of a relation.
|
|
The Relation instance saves a non-const reference to the current/parent model
|
|
instance, a copy of the related model instance because it is created
|
|
on the stack.
|
|
The Relation instance creates a new TinyBuilder instance from the Related
|
|
model instance using the TinyBuilder::newQuery() and saves a ownership as
|
|
the shared pointer (because this class is copyable).
|
|
Then eager constraints are applied to this newly created TinyBuilder and
|
|
the result is returned back to the initial model.
|
|
The result is transformed into models and these models are hydrated.
|
|
Hydrated models are saved to the templated Model::m_relations data member
|
|
hash.
|
|
Also, look the NOTES.txt for eagerLoadRelations() and Model::load() history. */
|
|
|
|
template<typename Model>
|
|
template<SameDerivedCollectionModel<Model> CollectionModel>
|
|
void
|
|
Builder<Model>::eagerLoadRelations(ModelsCollection<CollectionModel> &models) const
|
|
{
|
|
// Nothing to load
|
|
if (m_eagerLoad.isEmpty())
|
|
return;
|
|
|
|
for (const auto &relation : m_eagerLoad)
|
|
/* For nested eager loads we'll skip loading them here and they will be
|
|
loaded later using the nested query which retrieves this nested relations,
|
|
because that is where they get hydrated as models. */
|
|
if (!relation.name.contains(DOT))
|
|
/* Get the relation instance for the given relation name, have to be done
|
|
through the visitor pattern which obtains also the Related type.
|
|
After the visitation the eagerLoadRelationVisited() will be called. */
|
|
m_model.eagerLoadRelationWithVisitor(relation, *this, models);
|
|
}
|
|
|
|
template<typename Model>
|
|
void Builder<Model>::eagerLoadRelations(Model &model) const
|
|
{
|
|
// Nothing to load
|
|
if (m_eagerLoad.isEmpty())
|
|
return;
|
|
|
|
/* The eagerLoadRelations() methods chain can only operate on a ModelsCollection
|
|
it can't accept a single Model instance, would be possible to create
|
|
a templated EagerRelationStore that would accept a single Model but the effort
|
|
is not worth it.
|
|
Also, I checked now what would be required to implement this and it would be
|
|
really sketchy as all methods like addEagerConstraints(), initRelation(),
|
|
match(), getKeys() would have to be able to operate on a single Model. */
|
|
ModelsCollection<Model *> models {&model};
|
|
|
|
eagerLoadRelations(models);
|
|
}
|
|
|
|
template<typename Model>
|
|
template<typename Relation, SameDerivedCollectionModel<Model> CollectionModel>
|
|
void Builder<Model>::eagerLoadRelationVisited(
|
|
Relation &relation, ModelsCollection<CollectionModel> &models,
|
|
const WithItem &relationItem) const
|
|
{
|
|
/* First we will "back up" the existing where conditions (Relation::noConstraints)
|
|
on the query so we can add our eager constraints, this is done
|
|
in the EagerRelationStore::visited() using the Relation::noConstraints().
|
|
Following is true for the relationItem.constraints (Constraining Eager Loads):
|
|
Then we will merge the user defined constraints that were on the query
|
|
back to it, this ensures that a user can specify any where constraints or
|
|
ordering (where, orderBy, and maybe more). */
|
|
auto nested = relationsNestedUnder(relationItem.name);
|
|
|
|
/* If there are nested relationships set on this query, we will put those onto
|
|
the relation's query instance so they can be handled after this relationship
|
|
is loaded. In this way they will all trickle down as they are loaded. */
|
|
if (nested.size() > 0)
|
|
relation->getQuery().with(std::move(nested));
|
|
|
|
relation->addEagerConstraints(models);
|
|
|
|
// Add relation constraints defined in the user callback
|
|
// NOTE api different, Eloquent is passing the Relation reference into the lambda, it would be almost impossible to do it silverqx
|
|
if (relationItem.constraints)
|
|
std::invoke(relationItem.constraints, relation->getBaseQuery());
|
|
|
|
/* Once we have the results, we just match those back up to their parent models
|
|
using the relationship instance. Then we just return the finished vector
|
|
of models which have been eagerly hydrated and are readied for return. */
|
|
relation->match(relation->initRelation(models, relationItem.name),
|
|
relation->getEager(), relationItem.name);
|
|
}
|
|
|
|
template<typename Model>
|
|
ModelsCollection<Model>
|
|
Builder<Model>::hydrate(SqlQuery &&result) const // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
|
|
{
|
|
auto instance = newModelInstance();
|
|
|
|
ModelsCollection<Model> models;
|
|
models.reserve(QueryUtils::queryResultSize(result));
|
|
|
|
while (result.next()) {
|
|
const auto record = result.record();
|
|
const auto fieldsCount = record.count();
|
|
|
|
QList<AttributeItem> row;
|
|
row.reserve(fieldsCount);
|
|
|
|
// Populate model attributes with data from the database (one table row)
|
|
for (int i = 0; i < fieldsCount; ++i)
|
|
row.append({record.fieldName(i), result.value(i)});
|
|
|
|
// Create a new model instance from the table row
|
|
models << instance.newFromBuilder(std::move(row));
|
|
}
|
|
|
|
return models;
|
|
}
|
|
|
|
template<typename Model>
|
|
Model &Builder<Model>::getModel() noexcept
|
|
{
|
|
return m_model;
|
|
}
|
|
|
|
template<typename Model>
|
|
QueryBuilder &Builder<Model>::getQuery() const noexcept
|
|
{
|
|
return *m_query;
|
|
}
|
|
|
|
template<typename Model>
|
|
const std::shared_ptr<QueryBuilder> &
|
|
Builder<Model>::getQueryShared() const noexcept
|
|
{
|
|
return m_query;
|
|
}
|
|
|
|
template<typename Model>
|
|
DatabaseConnection &
|
|
Builder<Model>::getConnection()
|
|
{
|
|
return toBase().getConnection();
|
|
}
|
|
|
|
template<typename Model>
|
|
const QueryGrammar &
|
|
Builder<Model>::getGrammar()
|
|
{
|
|
return toBase().getGrammar();
|
|
}
|
|
|
|
template<typename Model>
|
|
QueryBuilder &Builder<Model>::toBase()
|
|
{
|
|
// FUTURE add Query Scopes feature silverqx
|
|
// retutn applyScopes().getQuery();
|
|
|
|
return applySoftDeletes().getQuery();
|
|
}
|
|
|
|
template<typename Model>
|
|
QString Builder<Model>::qualifyColumn(const QString &column) const
|
|
{
|
|
return m_model.qualifyColumn(column);
|
|
}
|
|
|
|
template<typename Model>
|
|
void Builder<Model>::onDelete(
|
|
std::function<std::tuple<int, TSqlQuery>(Builder<Model> &)> &&callback)
|
|
{
|
|
m_onDelete = std::move(callback);
|
|
}
|
|
|
|
/* BuildsSoftDeletes */
|
|
|
|
template<typename Model>
|
|
Builder<Model> &Builder<Model>::applySoftDeletes()
|
|
{
|
|
if constexpr (m_extendsSoftDeletes)
|
|
return Concerns::BuildsSoftDeletes<Model, true>::applySoftDeletes();
|
|
else
|
|
return *this;
|
|
}
|
|
|
|
template<typename Model>
|
|
constexpr bool Builder<Model>::extendsSoftDeletes() noexcept
|
|
{
|
|
return m_extendsSoftDeletes;
|
|
}
|
|
|
|
/* protected */
|
|
|
|
template<typename Model>
|
|
const QString &Builder<Model>::defaultKeyName() const
|
|
{
|
|
return m_model.getKeyName();
|
|
}
|
|
|
|
template<typename Model>
|
|
QList<WithItem>
|
|
Builder<Model>::parseWithRelations(const QList<WithItem> &relations)
|
|
{
|
|
// Guess number of relations (including nested relations)
|
|
const auto relationsSize = guessParseWithRelationsSize(relations);
|
|
|
|
// Nothing to prepare (no nested relations)
|
|
if (relationsSize == 0)
|
|
return {};
|
|
|
|
QList<WithItem> results;
|
|
results.reserve(relationsSize);
|
|
|
|
for (auto relation : relations) {
|
|
const auto isSelectConstraint = relation.name.contains(COLON);
|
|
|
|
if (isSelectConstraint && relation.constraints)
|
|
throw Orm::Exceptions::InvalidArgumentError(
|
|
"Passing both 'Select constraint' and 'Lambda expression "
|
|
"constraint' to the Model::with() method is not allowed, use "
|
|
"only one of them.");
|
|
|
|
/* I have to write a note here, this !relation.name.contains(DOT) bugfix
|
|
was unbelievable, it took me a whole day of debugging, writing unit tests,
|
|
and figuring out how to solve this problem. I wrote dozen of lines to make
|
|
it work but at the end I started removing what was not needed and ended
|
|
with this one condition. 😮🙃
|
|
If the relation name is a nested relation, then the select constraints
|
|
lambda will not be generated and will be nullptr, so will be skipped here,
|
|
the problem was that the getRelatedTableForBelongsToManyWithVisitor()
|
|
could not be invoked correctly because it's a nested relation.
|
|
The getRelatedTableForBelongsToManyWithVisitor() will be visited or
|
|
resolved later, right before it will be needed and it will be done during
|
|
relation->getQuery().with(std::move(nested));
|
|
in the eagerLoadRelationVisited(), the magic is done
|
|
in the relationsNestedUnder() when the nested relation is unwrapped.
|
|
The super paradox is that this was the first think I wrote but I still got
|
|
crash and then a whole day of fixing started. 🐛 */
|
|
if (isSelectConstraint && !relation.name.contains(DOT))
|
|
relation = createSelectWithConstraint(relation.name);
|
|
|
|
/* We need to separate out any nested includes, which allows the developers
|
|
to load deep relationships using "dots" without stating each level of
|
|
the relationship with its own key in the vector of eager-load names. */
|
|
addNestedWiths(relation.name, results);
|
|
|
|
results << std::move(relation);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
template<typename Model>
|
|
WithItem Builder<Model>::createSelectWithConstraint(const QString &name)
|
|
{
|
|
auto splitted = name.split(COLON);
|
|
auto relation = splitted.constFirst().trimmed();
|
|
auto &columns = splitted[1];
|
|
|
|
/* Get the Related model table name if the relation is BelongsToMany, otherwise
|
|
return an empty std::optional. */
|
|
auto belongsToManyRelatedTable =
|
|
m_model.getRelatedTableForBelongsToManyWithVisitor(relation);
|
|
|
|
// Move the relation and also values to the lambda, to avoid dangling references
|
|
return {
|
|
std::move(relation),
|
|
[columns = std::move(columns),
|
|
belongsToManyRelatedTable = std::move(belongsToManyRelatedTable)]
|
|
(QueryBuilder &query)
|
|
{
|
|
const auto columnsSplitted = QStringView(columns)
|
|
.split(COMMA_C, Qt::SkipEmptyParts);
|
|
|
|
// Nothing to do
|
|
if (columnsSplitted.isEmpty())
|
|
return;
|
|
|
|
QList<Column> columnsList;
|
|
columnsList.reserve(columnsSplitted.size());
|
|
|
|
// Avoid 'clazy might detach' warning
|
|
for (const auto column_ : columnsSplitted)
|
|
{
|
|
const auto column = column_.trimmed();
|
|
|
|
/* Nothing to process, already a fully qualified column name or
|
|
it's not a column for the BelongsToMany relation, in this case,
|
|
an unqualified column name is ok. */
|
|
if (column.contains(DOT) || !belongsToManyRelatedTable) {
|
|
columnsList << column.toString();
|
|
continue;
|
|
}
|
|
|
|
/* Generate the fully qualified column name for the BelongsToMany
|
|
relation. */
|
|
columnsList << DOT_IN.arg(*belongsToManyRelatedTable, column);
|
|
}
|
|
|
|
query.select(std::move(columnsList));
|
|
}
|
|
};
|
|
}
|
|
|
|
template<typename Model>
|
|
void Builder<Model>::addNestedWiths(const QString &name,
|
|
QList<WithItem> &results) const
|
|
{
|
|
/* If the relation has already been set on the result vector, we will not set it
|
|
again, since that would override any constraints that were already placed
|
|
on the relationships. We will only set the ones that are not specified. */
|
|
auto names = name.split(DOT, Qt::SkipEmptyParts, Qt::CaseSensitive);
|
|
|
|
// Nothing to do (no nested relations)
|
|
if (names.isEmpty())
|
|
return;
|
|
|
|
QStringList progress;
|
|
progress.reserve(names.size());
|
|
|
|
for (auto &&segment : names) {
|
|
progress << std::move(segment);
|
|
|
|
auto last = progress.join(DOT);
|
|
const auto containsRelation = [&last](const auto &relation)
|
|
{
|
|
return relation.name == last;
|
|
};
|
|
const auto contains = ranges::contains(results, true, containsRelation);
|
|
|
|
// Don't add a relation in the 'name' variable
|
|
if (!contains && last != name)
|
|
results.append({std::move(last)});
|
|
}
|
|
}
|
|
|
|
template<typename Model>
|
|
QList<WithItem>::size_type
|
|
Builder<Model>::guessParseWithRelationsSize(const QList<WithItem> &relations)
|
|
{
|
|
QList<WithItem>::size_type size = 0;
|
|
|
|
for (const auto &[relation, _] : relations)
|
|
// Nested relations (x.y.z == 3 relations)
|
|
if (relation.contains(DOT))
|
|
size += relation.count(DOT) + 1;
|
|
|
|
// All others, with the ':' (Select Constraints) or only the relation name
|
|
else
|
|
++size;
|
|
|
|
return size;
|
|
}
|
|
|
|
template<typename Model>
|
|
QList<WithItem>
|
|
Builder<Model>::relationsNestedUnder(const QString &topRelationName) const
|
|
{
|
|
/*! Count the number of nested relations, it always returns the qint64 difference
|
|
type. */
|
|
const auto nestedSize = std::ranges::count_if(
|
|
m_eagerLoad, [this, &topRelationName]
|
|
(const auto &relation)
|
|
{
|
|
return isNestedUnder(topRelationName, relation.name);
|
|
});
|
|
|
|
// Nothing to prepare (no nested relations)
|
|
if (nestedSize == 0)
|
|
return {};
|
|
|
|
QList<WithItem> nested;
|
|
nested.reserve(nestedSize);
|
|
|
|
/* We are basically looking for any relationships that are nested deeper than
|
|
the given top-level relationship. We will just check for any relations
|
|
that start with the given top relations and add them to our vector. */
|
|
for (const auto &[relationName, constraints] : m_eagerLoad)
|
|
if (isNestedUnder(topRelationName, relationName))
|
|
#if defined(__clang__) && __clang_major__ < 16
|
|
nested.append({relationName.mid(topRelationName.size() + 1),
|
|
constraints});
|
|
#else
|
|
nested.emplaceBack(relationName.sliced(topRelationName.size() + 1),
|
|
constraints);
|
|
#endif
|
|
|
|
return nested;
|
|
}
|
|
|
|
template<typename Model>
|
|
bool Builder<Model>::isNestedUnder(const QString &topRelation,
|
|
const QString &nestedRelation) const
|
|
{
|
|
return nestedRelation.contains(DOT) &&
|
|
nestedRelation.startsWith(QStringLiteral("%1.").arg(topRelation));
|
|
}
|
|
|
|
template<typename Model>
|
|
QList<UpdateItem>
|
|
Builder<Model>::addUpdatedAtColumn(QList<UpdateItem> values) const
|
|
{
|
|
const auto &updatedAtColumn = m_model.getUpdatedAtColumn();
|
|
|
|
// Nothing to do (model doesn't use timestamps)
|
|
if (!m_model.usesTimestamps() || updatedAtColumn.isEmpty())
|
|
return values;
|
|
|
|
/*! Find updated_at column. */
|
|
const auto valuesUpdatedAtColumn =
|
|
std::ranges::find_if(values,
|
|
[&updatedAtColumn](const auto &updateItem)
|
|
{
|
|
return updateItem.column == updatedAtColumn;
|
|
});
|
|
|
|
auto qualifiedUpdatedAtColumn = m_model.getQualifiedUpdatedAtColumn();
|
|
|
|
// Not found, append a fresh timestamp
|
|
if (valuesUpdatedAtColumn == std::ranges::cend(values))
|
|
values.append({std::move(qualifiedUpdatedAtColumn),
|
|
m_model.freshTimestampString()});
|
|
|
|
// Found, rename the updated_at column to the qualified column
|
|
else
|
|
valuesUpdatedAtColumn->column = std::move(qualifiedUpdatedAtColumn);
|
|
|
|
return values;
|
|
}
|
|
|
|
template<typename Model>
|
|
QList<QVariantMap>
|
|
Builder<Model>::addTimestampsToUpsertValues(const QList<QVariantMap> &values) const
|
|
{
|
|
// Nothing to do (model doesn't use timestamps)
|
|
if (!m_model.usesTimestamps())
|
|
return values;
|
|
|
|
// Prepare timestamp columns to add
|
|
std::vector<QString> columns;
|
|
columns.reserve(2);
|
|
|
|
// Don't use qualified columns here, they are not needed
|
|
if (const auto &createdAtColumn = m_model.getCreatedAtColumn();
|
|
!createdAtColumn.isEmpty()
|
|
)
|
|
columns.push_back(createdAtColumn);
|
|
if (const auto &updatedAtColumn = m_model.getUpdatedAtColumn();
|
|
!updatedAtColumn.isEmpty()
|
|
)
|
|
columns.push_back(updatedAtColumn);
|
|
|
|
// Nothing to insert, both timestamp column names are empty 🙃
|
|
if (columns.empty())
|
|
return values;
|
|
|
|
const auto timestamp = m_model.freshTimestampString();
|
|
auto valuesCopy = values;
|
|
|
|
// Insert timestamp columns if a row doesn't already contain one
|
|
for (const auto &column : columns)
|
|
for (auto &row : valuesCopy)
|
|
if (!row.contains(column))
|
|
row.insert(column, timestamp);
|
|
|
|
return valuesCopy;
|
|
}
|
|
|
|
template<typename Model>
|
|
QStringList
|
|
Builder<Model>::addUpdatedAtToUpsertColumns(const QStringList &update) const
|
|
{
|
|
// Nothing to do (model doesn't use timestamps)
|
|
if (!m_model.usesTimestamps())
|
|
return update;
|
|
|
|
const auto &updatedAtColumn = m_model.getUpdatedAtColumn();
|
|
|
|
// Nothing to do
|
|
if (updatedAtColumn.isEmpty() || update.contains(updatedAtColumn))
|
|
return update;
|
|
|
|
auto updateCopy = update;
|
|
|
|
// Don't use qualified column here, it's not needed
|
|
return updateCopy << updatedAtColumn;
|
|
}
|
|
|
|
template<typename Model>
|
|
Column
|
|
Builder<Model>::getCreatedAtColumnForLatestOldest(Column column) const
|
|
{
|
|
/* Don't initialize column when user passed column expression, only when it
|
|
holds the QString type. */
|
|
if (std::holds_alternative<QString>(column) &&
|
|
std::get<QString>(column).isEmpty()
|
|
) {
|
|
if (const auto &createdAtColumn = m_model.getCreatedAtColumn();
|
|
createdAtColumn.isEmpty()
|
|
)
|
|
column = CREATED_AT;
|
|
else
|
|
column = createdAtColumn;
|
|
}
|
|
|
|
return column;
|
|
}
|
|
|
|
template<typename Model>
|
|
void Builder<Model>::enforceOrderBy()
|
|
{
|
|
if (!m_query->getOrders().isEmpty())
|
|
return;
|
|
|
|
this->orderBy(m_model.getQualifiedKeyName(), ASC);
|
|
}
|
|
|
|
// FEATURE scopes, anyway std::apply() do the same, will have to investigate it silverqx
|
|
// template<typename Model>
|
|
// template<typename ...Args>
|
|
// Builder<Model> &
|
|
// Builder<Model>::callScope(
|
|
// const std::function<void(Builder &, Args ...)> &scope,
|
|
// Args &&...parameters)
|
|
// {
|
|
// std::invoke(scope, *this, std::forward<Args>(parameters)...);
|
|
|
|
// return *this;
|
|
// }
|
|
|
|
} // namespace Orm::Tiny
|
|
|
|
TINYORM_END_COMMON_NAMESPACE
|
|
|
|
#endif // ORM_TINY_TINYBUILDER_HPP
|