88 auto connect(
const std::string& connectionString) -> void;
93 auto connect(db::BackendType requestedBackend,
const std::string& connectionString) -> void;
108 template <
typename SchemaType,
typename T>
111 constexpr auto model = model::modelView<SchemaType, T>();
112 ensureQuerySupported(model, query.getData());
113 std::vector<T> result;
114 const auto statement = getCommandGenerator().select(model, query.getData());
115 ensureStatementWithinBindLimit(statement.parameters.size(),
"select");
119 soci::values parameterValues;
120 detail::bindStatementParameters(getBackend().runtime(), parameterValues, statement.parameters);
122 auto readRows = [&]<
bool JoinedValues>()
124 soci::rowset<db::binding::BindingPayload<T, SchemaType, JoinedValues>> preparedRowSet =
125 (sql.prepare << statement.sql, soci::use(parameterValues));
127 for (
auto& payload : preparedRowSet)
129 result.push_back(std::move(payload.value));
133 if (query.getData().shouldJoin)
135 readRows.template operator()<
true>();
139 readRows.template operator()<
false>();
142 loadIncludedCollections<SchemaType>(model, query.getData(), result);
144 catch (
const db::binding::ConversionError&)
146 throw DatabaseError{DatabaseErrorCode::Conversion, backendType,
"select",
147 "A database result cannot be represented by the requested model"};
149 catch (
const soci::soci_error& error)
151 throwTranslatedError(error, DatabaseErrorCode::Statement,
"select");
165 template <
typename SchemaType,
typename Source,
typename Result>
168 constexpr auto model = model::modelView<SchemaType, Source>();
169 ensureQuerySupported(model, query.getData());
170 std::vector<Result> result;
171 const auto statement = getCommandGenerator().select(model, query.getData());
172 ensureStatementWithinBindLimit(statement.parameters.size(),
"select projection");
176 soci::values parameterValues;
177 detail::bindStatementParameters(getBackend().runtime(), parameterValues, statement.parameters);
179 soci::rowset<db::binding::ProjectionPayload<Result>> preparedRowSet =
180 (sql.prepare << statement.sql, soci::use(parameterValues));
182 for (
auto& payload : preparedRowSet)
184 result.push_back(std::move(payload.value));
187 catch (
const db::binding::ConversionError&)
189 throw DatabaseError{DatabaseErrorCode::Conversion, backendType,
"select projection",
190 "A database result cannot be represented by the requested projection"};
192 catch (
const soci::soci_error& error)
194 throwTranslatedError(error, DatabaseErrorCode::Statement,
"select projection");
206 template <
typename SchemaType,
typename T>
209 for (
const auto&
object : objects)
221 template <
typename SchemaType,
typename T>
224 constexpr auto model = model::modelView<SchemaType, T>();
225 ensureModelSupported(model,
"insert");
227 const auto command = getCommandGenerator().insert(model);
229 const db::binding::BindingPayload<T, SchemaType> payload{};
231 payload.value = std::move(
object);
235 soci::values serializedModel;
236 auto indicator = soci::i_ok;
237 soci::type_conversion<db::binding::BindingPayload<T, SchemaType>>::to_base(payload, serializedModel,
240 soci::values parameterValues;
241 const auto parameterCount =
242 detail::bindModelParameters(getBackend().runtime(), parameterValues, serializedModel, model);
243 ensureStatementWithinBindLimit(parameterCount,
"insert");
245 if (parameterCount == 0)
251 sql << command, soci::use(parameterValues);
254 catch (
const db::binding::ConversionError&)
256 throw DatabaseError{DatabaseErrorCode::Conversion, backendType,
"insert",
257 "A model value cannot be represented by the selected backend"};
259 catch (
const soci::soci_error& error)
261 throwTranslatedError(error, DatabaseErrorCode::Statement,
"insert");
272 template <
typename SchemaType,
typename T>
275 constexpr auto model = model::modelView<SchemaType, T>();
276 ensureModelSupported(model,
"update");
278 const auto statement = getCommandGenerator().update(model, update.getData());
280 return executeMutation(statement,
"update");
290 template <
typename SchemaType,
typename T>
291 auto removeImpl(
const query::Predicate& predicate) -> std::size_t
293 constexpr auto model = model::modelView<SchemaType, T>();
294 ensureModelSupported(model,
"remove");
296 const auto statement = getCommandGenerator().remove(model, predicate);
298 return executeMutation(statement,
"remove");
306 template <
typename SchemaType,
typename T>
309 constexpr auto model = model::modelView<SchemaType, T>();
310 ensureModelSupported(model,
"create table");
312 "idempotent table creation is not supported");
313 const auto command = getCommandGenerator().createTable(model);
314 executeSql(command,
"create table");
322 template <
typename SchemaType,
typename T>
325 constexpr auto model = model::modelView<SchemaType, T>();
326 ensureModelSupported(model,
"drop table");
328 "idempotent table removal is not supported");
329 const auto command = getCommandGenerator().dropTable(model);
330 executeSql(command,
"drop table");
339 template <
typename SchemaType,
typename T>
342 constexpr auto owner = model::modelView<SchemaType, T>();
343 const auto ownsJunctionTable =
344 std::ranges::any_of(owner->relations,
345 [](
const auto& relation)
347 return relation.kind == model::RelationKind::ManyToMany and
348 relation.junction.isConfigured() and relation.junction.owningSide;
351 if (not ownsJunctionTable)
356 ensureModelSupported(owner,
"create relation tables");
357 ensureRelationTableEndpointsExist(owner);
359 for (
const auto& command : db::relations::createTableStatements(getBackend().dialect(), owner))
361 executeSql(command,
"create relation table");
368 template <
typename SchemaType,
typename T>
371 constexpr auto owner = model::modelView<SchemaType, T>();
372 const auto ownsJunctionTable =
373 std::ranges::any_of(owner->relations,
374 [](
const auto& relation)
376 return relation.kind == model::RelationKind::ManyToMany and
377 relation.junction.isConfigured() and relation.junction.owningSide;
380 if (not ownsJunctionTable)
385 ensureModelSupported(owner,
"drop relation tables");
387 "junction tables are not supported");
389 "idempotent table removal is not supported");
391 for (
const auto& command : db::relations::dropTableStatements(getBackend().dialect(), owner))
393 executeSql(command,
"drop relation table");
401 template <
typename SchemaType,
typename Owner,
typename Target>
402 auto linkImpl(
const Owner& owner, std::string_view relationField,
const Target& target) -> std::size_t
404 constexpr auto ownerDescriptor = model::modelView<SchemaType, Owner>();
405 const auto* relation = ownerDescriptor.findRelation(relationField);
407 if (relation ==
nullptr or relation->kind == model::RelationKind::ToOne)
409 throw std::invalid_argument{
"Unknown collection relation: " + std::string{relationField}};
412 const auto targetDescriptor = ownerDescriptor.resolveTarget(*relation);
413 if (targetDescriptor ==
nullptr or targetDescriptor->type != model::typeId<Target>())
415 throw std::invalid_argument{
"Relation target type does not match mapping: " + std::string{relationField}};
418 ensureModelSupported(ownerDescriptor,
"link relation");
419 ensureModelSupported(*targetDescriptor,
"link relation");
420 const auto ownerKey = db::binding::getPrimaryKey<SchemaType>(owner);
421 const auto targetKey = db::binding::getPrimaryKey<SchemaType>(target);
423 if (relation->kind == model::RelationKind::OneToMany)
426 "one-to-many relations are not supported");
431 "many-to-many relations are not supported");
433 "idempotent relation links are not supported");
435 requireCapability(relation->kind == model::RelationKind::OneToMany ?
getBackendCapabilities().mutations.update :
437 "link relation",
"relation mutations are not supported");
439 if (not relationEndpointExists(ownerDescriptor, ownerKey) or
440 not relationEndpointExists(*targetDescriptor, targetKey))
442 throw std::invalid_argument{
"Cannot link relation endpoints that do not exist"};
445 return executeMutation(
446 db::relations::linkStatement(getBackend().dialect(), ownerDescriptor, *relation, ownerKey, targetKey),
454 template <
typename SchemaType,
typename Owner,
typename Target>
455 auto unlinkImpl(
const Owner& owner, std::string_view relationField,
const Target& target) -> std::size_t
457 constexpr auto ownerDescriptor = model::modelView<SchemaType, Owner>();
458 const auto* relation = ownerDescriptor.findRelation(relationField);
460 if (relation ==
nullptr or relation->kind == model::RelationKind::ToOne)
462 throw std::invalid_argument{
"Unknown collection relation: " + std::string{relationField}};
465 const auto targetDescriptor = ownerDescriptor.resolveTarget(*relation);
466 if (targetDescriptor ==
nullptr or targetDescriptor->type != model::typeId<Target>())
468 throw std::invalid_argument{
"Relation target type does not match mapping: " + std::string{relationField}};
471 ensureModelSupported(ownerDescriptor,
"unlink relation");
472 ensureModelSupported(*targetDescriptor,
"unlink relation");
473 requireCapability(relation->kind == model::RelationKind::OneToMany ?
476 "unlink relation",
"the requested collection relation is not supported");
477 requireCapability(relation->kind == model::RelationKind::OneToMany ?
getBackendCapabilities().mutations.update :
479 "unlink relation",
"relation mutations are not supported");
481 return executeMutation(db::relations::unlinkStatement(getBackend().dialect(), ownerDescriptor, *relation,
482 db::binding::getPrimaryKey<SchemaType>(owner),
483 db::binding::getPrimaryKey<SchemaType>(target)),
522 template <typename SchemaType, typename Owner, typename Target,
bool JoinedValues>
523 auto appendCollectionRows(const db::Statement& statement,
524 std::map<db::binding::PrimaryKey, std::vector<Target>>& groupedTargets) ->
void
526 ensureStatementWithinBindLimit(statement.parameters.size(),
"include collection");
527 soci::values parameterValues;
528 detail::bindStatementParameters(getBackend().runtime(), parameterValues, statement.parameters);
529 soci::rowset<db::binding::CollectionPayload<Owner, Target, SchemaType, JoinedValues>> preparedRowSet =
530 (sql.prepare << statement.sql, soci::use(parameterValues));
532 for (
auto& payload : preparedRowSet)
534 groupedTargets[payload.ownerKey].push_back(std::move(payload.value));
538 template <
typename SchemaType,
typename Owner>
539 auto loadIncludedCollections(model::ModelView ownerDescriptor,
const query::SelectSpec& queryData,
540 std::vector<Owner>& owners) ->
void
542 if (queryData.includes.empty())
547 for (
const auto& includedRelation : queryData.includes)
549 const auto* relation = ownerDescriptor.findRelation(includedRelation);
551 if (relation ==
nullptr or relation->kind == model::RelationKind::ToOne)
553 throw std::invalid_argument{
"Unknown collection relation: " + includedRelation};
562 constexpr auto reflectedFields = reflection::fields<Owner>();
563 auto ownerFields = reflection::fieldPointers(owners.front());
564 auto loadField = [
this, &queryData, &owners, ownerDescriptor, &reflectedFields](
auto fieldIndex,
auto* field)
566 using collection_t = std::decay_t<
decltype(*field)>;
568 if constexpr (orm::is_relation_collection_v<collection_t>)
570 const auto relationName = std::string{reflectedFields[fieldIndex].name};
572 if (std::ranges::find(queryData.includes, relationName) == queryData.includes.end())
577 const auto* relation = ownerDescriptor.findRelation(relationName);
578 assert(relation !=
nullptr);
580 loadCollectionField<SchemaType, decltype(fieldIndex)::value, Owner, collection_t>(
581 ownerDescriptor, queryData, owners, *relation);
585 utils::constexpr_for_tuple(ownerFields, loadField);
588 template <
typename SchemaType, std::
size_t FieldIndex,
typename Owner,
typename Collection>
589 auto loadCollectionField(model::ModelView ownerDescriptor,
const query::SelectSpec& queryData,
590 std::vector<Owner>& owners,
const model::RelationView& relation) ->
void
592 using target_t = orm::relation_target_t<Collection>;
594 const auto targetDescriptor = ownerDescriptor.resolveTarget(relation);
595 if (targetDescriptor ==
nullptr or targetDescriptor->type != model::typeId<target_t>())
597 throw std::invalid_argument{
"Collection wrapper target does not match relation metadata: " +
598 std::string{relation.fieldName}};
601 const auto ownerPrimaryKey = db::binding::getPrimaryKeyColumns(ownerDescriptor);
602 const auto runtimeLimits = getBackendRuntimeLimits();
603 const auto parameterBudget = runtimeLimits.maxBindParameters.value_or(std::numeric_limits<std::size_t>::max());
605 if (parameterBudget < ownerPrimaryKey.size())
607 throw DatabaseError{DatabaseErrorCode::UnsupportedFeature, backendType,
"include collection",
608 "The backend bind-parameter limit is too small for the relation primary key"};
611 const auto batchSize = runtimeLimits.maxBindParameters.has_value() ?
612 std::max<std::size_t>(1, parameterBudget / ownerPrimaryKey.size()) :
614 std::map<db::binding::PrimaryKey, std::vector<target_t>> groupedTargets;
616 orm::Query<target_t> targetQuery;
618 if (not queryData.shouldJoin)
620 targetQuery.disableJoining();
623 const auto baseTargetStatement = getCommandGenerator().select(*targetDescriptor, targetQuery.getData());
625 assert(baseTargetStatement.parameters.empty());
627 for (std::size_t batchStart = 0; batchStart < owners.size(); batchStart += batchSize)
629 const auto batchEnd = std::min(owners.size(), batchStart + batchSize);
630 std::vector<db::binding::PrimaryKey> ownerKeys;
631 ownerKeys.reserve(batchEnd - batchStart);
633 for (
auto ownerIndex = batchStart; ownerIndex < batchEnd; ++ownerIndex)
635 ownerKeys.push_back(db::binding::getPrimaryKey<SchemaType>(owners[ownerIndex]));
638 const auto statement =
639 db::relations::collectionSelectStatement(getBackend().dialect(), ownerDescriptor, relation,
640 baseTargetStatement.sql, ownerKeys, queryData.shouldJoin);
641 if (queryData.shouldJoin)
643 appendCollectionRows<SchemaType, Owner, target_t, true>(statement, groupedTargets);
647 appendCollectionRows<SchemaType, Owner, target_t, false>(statement, groupedTargets);
651 for (
auto& owner : owners)
653 const auto ownerKey = db::binding::getPrimaryKey<SchemaType>(owner);
654 auto ownerFields = reflection::fieldPointers(owner);
655 auto* collection = std::get<FieldIndex>(ownerFields);
656 const auto targets = groupedTargets.find(ownerKey);
658 if (targets == groupedTargets.end())
660 collection->setLoaded({});
664 collection->setLoaded(targets->second);
669 auto executeMutation(
const db::Statement& statement, std::string_view operation) -> std::size_t;
670 auto executeSql(std::string_view statement, std::string_view operation) -> void;
671 auto relationEndpointExists(model::ModelView model,
const db::binding::PrimaryKey& key) -> bool;
672 auto tableExists(std::string_view tableName) -> bool;
673 auto ensureRelationTableEndpointsExist(model::ModelView owner) -> void;
674 [[nodiscard]]
auto getBackend() const -> const db::BackendProvider&;
675 [[nodiscard]] auto getCommandGenerator() const -> const db::CommandGenerator&;
676 [[nodiscard]] auto getBackendRuntimeLimits() -> db::BackendRuntimeLimits;
677 auto ensureStatementWithinBindLimit(std::
size_t parameterCount, std::string_view operation) ->
void;
678 auto ensureModelSupported(model::ModelView model, std::string_view operation) const ->
void;
679 auto ensureQuerySupported(model::ModelView model, const query::SelectSpec& spec) const ->
void;
680 auto ensureAffectedRowsAvailable(std::string_view operation) const ->
void;
681 auto requireCapability(
bool supported, std::string_view operation, std::string_view message) const ->
void;
682 [[noreturn]] auto throwTranslatedError(const soci::soci_error& error, DatabaseErrorCode fallback,
683 std::string_view operation) ->
void;
686 std::unique_ptr<soci::transaction> transaction;
687 bool transactionFailed = false;
688 db::BackendType backendType;
689 db::CommandGeneratorFactory commandGeneratorFactory;
690 const db::BackendProvider* backend =
nullptr;