Lightweight 0.20261105.1
Loading...
Searching...
No Matches
Core.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "../Api.hpp"
6#include "../SqlQueryFormatter.hpp"
7#include "../Utils.hpp"
8
9#include <algorithm>
10#include <concepts>
11#include <cstddef>
12#include <optional>
13#include <ranges>
14#include <span>
15#include <stdexcept>
16#include <string>
17#include <vector>
18
19namespace Lightweight
20{
21
22/// @defgroup QueryBuilder Query Builder
23///
24/// @brief The query builder is a high level API for building SQL queries using high level C++ syntax.
25
26/// @brief SqlWildcardType is a placeholder for an explicit wildcard input parameter in a SQL query.
27///
28/// Use this in the SqlQueryBuilder::Where method to insert a '?' placeholder for a wildcard.
29///
30/// @ingroup QueryBuilder
32{
33};
34
35/// @brief SqlWildcard is a placeholder for an explicit wildcard input parameter in a SQL query.
36constexpr inline auto SqlWildcard = SqlWildcardType {};
37
38/// @brief Name of table in a SQL query, where the table's name is aliased.
40{
41 /// The table name.
42 std::string_view tableName;
43 /// The alias for the table.
44 std::string_view alias;
45
46 /// Three-way comparison operator.
47 std::weak_ordering operator<=>(AliasedTableName const&) const = default;
48};
49
50template <typename T>
51concept TableName =
52 std::convertible_to<T, std::string_view> || std::convertible_to<T, std::string> || std::same_as<T, AliasedTableName>;
53
54class SqlBinary;
55template <std::size_t N>
57
58namespace detail
59{
60
61 struct RawSqlCondition
62 {
63 std::string condition;
64 };
65
66 /// @brief Whether @p T is a binary value, which has no textual SQL literal to be inlined as.
67 template <typename T>
68 inline constexpr bool IsBinaryLiteral = std::same_as<T, SqlBinary>;
69
70 template <std::size_t N>
71 inline constexpr bool IsBinaryLiteral<SqlDynamicBinary<N>> = true;
72
73 /// @brief Writes a literal value into a SQL fragment.
74 ///
75 /// Shared by the WHERE and the ON clause so a value is spelled the same way wherever it lands.
76 ///
77 /// @param value The value to write. Column names, NULL, wildcards and raw conditions are
78 /// written as themselves; everything else is formatted, and quoted when its
79 /// type needs quoting.
80 /// @param target The fragment to append to.
81 /// @param formatter Supplies the dialect's spelling of a boolean literal.
82 template <typename LiteralType, typename TargetType>
83 void AppendLiteralValueInto(LiteralType const& value, TargetType& target, SqlQueryFormatter const& formatter);
84
85 /// @brief Writes the value of a WHERE or HAVING term, as a bound parameter where one is possible.
86 ///
87 /// @param value The value to write. Column names, NULL, wildcards and raw conditions have
88 /// no parameter form and are always written into @p target.
89 /// @param target The fragment to append to.
90 /// @param inputBindings Receives the value if not null, in which case @p target receives a marker.
91 /// @param bindingsBehind How many bindings belong to markers further along in the statement than
92 /// this one; the value is inserted ahead of them.
93 /// @param formatter Supplies the dialect's spelling of a boolean literal.
94 template <typename LiteralType>
95 void AppendConditionValue(LiteralType const& value,
96 std::string& target,
97 std::vector<SqlVariant>* inputBindings,
98 std::size_t bindingsBehind,
99 SqlQueryFormatter const& formatter);
100
101} // namespace detail
102
103/// @brief Helper function to create a SqlQualifiedTableColumnName from string_view
104///
105/// @param column The column name, which must be qualified with a table name.
106/// Example QualifiedColumnName<"Table.Column"> will create a SqlQualifiedTableColumnName with
107/// tableName = "Table" and columnName = "Column".
108template <Reflection::StringLiteral columnLiteral>
109constexpr SqlQualifiedTableColumnName QualifiedColumnName = []() consteval {
110#if !defined(_MSC_VER)
111 // enforce that we do not have symbols \ [ ] " '
112 static_assert(
113 !std::ranges::any_of(columnLiteral,
114 [](char c) consteval { return c == '\\' || c == '[' || c == ']' || c == '"' || c == '\''; }),
115 "QualifiedColumnName should not contain symbols \\ [ ] \" '");
116#endif
117
118 static_assert(std::ranges::count(columnLiteral, '.') == 1,
119 "QualifiedColumnName requires a column name with a single '.' to separate table and column name");
120 constexpr auto column = columnLiteral.sv();
121 auto dotPos = column.find('.');
122 return SqlQualifiedTableColumnName { .tableName = column.substr(0, dotPos), .columnName = column.substr(dotPos + 1) };
123}();
124
125namespace detail
126{
127
128 template <typename ColumnName>
129 std::string MakeSqlColumnName(ColumnName const& columnName)
130 {
131 using namespace std::string_view_literals;
132 std::string output;
133
134 if constexpr (std::is_same_v<ColumnName, SqlQualifiedTableColumnName>)
135 {
136 output.reserve(columnName.tableName.size() + columnName.columnName.size() + 5);
137 output += '"';
138 output += columnName.tableName;
139 output += R"(".")"sv;
140 output += columnName.columnName;
141 output += '"';
142 }
143 else if constexpr (std::is_same_v<ColumnName, SqlWildcardType>)
144 {
145 output += '?';
146 }
147 else
148 {
149 output += '"';
150 output += columnName;
151 output += '"';
152 }
153 return output;
154 }
155
156 template <typename T>
157 std::string MakeEscapedSqlString(T const& value)
158 {
159 std::string escapedValue;
160 escapedValue += '\'';
161
162 for (auto const ch: value)
163 {
164 // In SQL strings, single quotes are escaped by doubling them.
165 if (ch == '\'')
166 escapedValue += '\'';
167 escapedValue += ch;
168 }
169 escapedValue += '\'';
170 return escapedValue;
171 }
172
173} // namespace detail
174
175struct [[nodiscard]] SqlSearchCondition
176{
177 std::string tableName;
178 std::string tableAlias;
179 std::string tableJoins;
180 std::string condition;
181 std::vector<SqlVariant>* inputBindings = nullptr;
182
183 /// How many of @c inputBindings were contributed by ON clauses.
184 ///
185 /// A JOIN precedes the WHERE clause in the statement, so a value bound by an ON clause is
186 /// inserted ahead of the WHERE values rather than appended. Tracking the count is what keeps
187 /// the vector in statement order however the caller interleaves joins and WHERE terms.
188 std::size_t joinBindingCount = 0;
189
190 /// How many of @c inputBindings were contributed by the HAVING clause.
191 ///
192 /// HAVING follows the WHERE clause in the statement, so a WHERE value bound after a HAVING value
193 /// is inserted ahead of it rather than appended, which keeps the vector in statement order.
194 std::size_t havingBindingCount = 0;
195
196 /// The table hint rendered after the FROM table (and its alias) and after each joined table (and
197 /// its alias), such as SQL Server's `" WITH (READUNCOMMITTED)"`; empty for none.
198 ///
199 /// Refers to text owned by the query's formatter (see SqlQueryFormatter::ReadUncommittedTableHint()).
200 /// A join renders its table reference when it is attached, so the hint must be set before any join.
201 std::string_view tableHint;
202};
203
204/// @brief Query builder for building JOIN conditions.
205///
206/// An ON clause can say what a WHERE clause can: compare the joined table's column against another
207/// table's column or against a value, test it for null, and group terms in parentheses. The
208/// distinction matters for an outer join, where moving a term from the ON clause to the WHERE
209/// clause turns the join into an inner one.
210///
211/// @ingroup QueryBuilder
213{
214 public:
215 /// Constructs a new SqlJoinConditionBuilder.
216 ///
217 /// @param referenceTable The table being joined; the left operand of every condition.
218 /// @param searchCondition The query's search condition, whose @c tableJoins is written into and
219 /// whose bindings vector, when present, receives the compared values.
220 /// @param formatter Supplies the dialect's spelling of a boolean literal.
221 explicit SqlJoinConditionBuilder(std::string_view referenceTable,
222 SqlSearchCondition* searchCondition,
223 SqlQueryFormatter const* formatter) noexcept:
224 _referenceTable { referenceTable },
225 _searchCondition { *searchCondition },
226 _formatter { *formatter }
227 {
228 }
229
230 /// Adds an AND join condition.
231 SqlJoinConditionBuilder& On(std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn)
232 {
233 return Operator(joinColumnName, onOtherColumn, "AND");
234 }
235
236 /// Adds an OR join condition.
237 SqlJoinConditionBuilder& OrOn(std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn)
238 {
239 return Operator(joinColumnName, onOtherColumn, "OR");
240 }
241
242 /// Adds a join condition with a custom operator.
243 SqlJoinConditionBuilder& Operator(std::string_view joinColumnName,
244 SqlQualifiedTableColumnName onOtherColumn,
245 std::string_view op)
246 {
247 AppendJunctor(op);
248 AppendReferenceColumn(joinColumnName);
249 Condition() += " = ";
250 detail::AppendLiteralValueInto(onOtherColumn, Condition(), _formatter);
251 return *this;
252 }
253
254 /// Adds an AND join condition testing the joined column against @p value for equality.
255 ///
256 /// The value is bound when the query was given a bindings vector, and written into the
257 /// statement text otherwise -- the same rule the WHERE clause follows. A query whose ON clause
258 /// binds must be run through @c SqlStatement::ExecuteWithVariants, and the vector and anything
259 /// a stored string_view points at have to outlive the execution.
260 template <typename T>
261 SqlJoinConditionBuilder& OnValue(std::string_view joinColumnName, T const& value)
262 {
263 return ValueOperator(joinColumnName, "=", value, "AND");
264 }
265
266 /// Adds an AND join condition testing the joined column against @p value with @p binaryOp.
267 template <typename T>
268 SqlJoinConditionBuilder& OnValue(std::string_view joinColumnName, std::string_view binaryOp, T const& value)
269 {
270 return ValueOperator(joinColumnName, binaryOp, value, "AND");
271 }
272
273 /// Adds an OR join condition testing the joined column against @p value for equality.
274 template <typename T>
275 SqlJoinConditionBuilder& OrOnValue(std::string_view joinColumnName, T const& value)
276 {
277 return ValueOperator(joinColumnName, "=", value, "OR");
278 }
279
280 /// Adds an OR join condition testing the joined column against @p value with @p binaryOp.
281 template <typename T>
282 SqlJoinConditionBuilder& OrOnValue(std::string_view joinColumnName, std::string_view binaryOp, T const& value)
283 {
284 return ValueOperator(joinColumnName, binaryOp, value, "OR");
285 }
286
287 /// Adds an AND join condition testing the joined column for NULL.
288 SqlJoinConditionBuilder& OnNull(std::string_view joinColumnName)
289 {
290 return NullTest(joinColumnName, NullSense::Null, "AND");
291 }
292
293 /// Adds an AND join condition testing the joined column for NOT NULL.
294 SqlJoinConditionBuilder& OnNotNull(std::string_view joinColumnName)
295 {
296 return NullTest(joinColumnName, NullSense::NotNull, "AND");
297 }
298
299 /// Adds an OR join condition testing the joined column for NULL.
300 SqlJoinConditionBuilder& OrOnNull(std::string_view joinColumnName)
301 {
302 return NullTest(joinColumnName, NullSense::Null, "OR");
303 }
304
305 /// Adds an OR join condition testing the joined column for NOT NULL.
306 SqlJoinConditionBuilder& OrOnNotNull(std::string_view joinColumnName)
307 {
308 return NullTest(joinColumnName, NullSense::NotNull, "OR");
309 }
310
311 /// Adds an AND group of join conditions, in parentheses.
312 ///
313 /// The parentheses are what keep `a AND (b OR c)` from becoming `a AND b OR c`, which selects
314 /// strictly more.
315 template <typename Callable>
316 requires std::invocable<Callable, SqlJoinConditionBuilder&>
317 SqlJoinConditionBuilder& OnGroup(Callable const& build)
318 {
319 return Group(build, "AND");
320 }
321
322 /// Adds an OR group of join conditions, in parentheses.
323 template <typename Callable>
324 requires std::invocable<Callable, SqlJoinConditionBuilder&>
325 SqlJoinConditionBuilder& OrOnGroup(Callable const& build)
326 {
327 return Group(build, "OR");
328 }
329
330 private:
331 [[nodiscard]] std::string& Condition() noexcept
332 {
333 return _searchCondition.tableJoins;
334 }
335
336 void AppendJunctor(std::string_view op)
337 {
338 if (_firstCall)
339 _firstCall = false;
340 else
341 Condition() += std::format(" {} ", op);
342 }
343
344 void AppendReferenceColumn(std::string_view joinColumnName)
345 {
346 Condition() += '"';
347 Condition() += _referenceTable;
348 Condition() += "\".\"";
349 Condition() += joinColumnName;
350 Condition() += '"';
351 }
352
353 template <typename T>
354 SqlJoinConditionBuilder& ValueOperator(std::string_view joinColumnName,
355 std::string_view binaryOp,
356 T const& value,
357 std::string_view op)
358 {
359 AppendJunctor(op);
360 AppendReferenceColumn(joinColumnName);
361 Condition() += std::format(" {} ", binaryOp);
362
363 if (_searchCondition.inputBindings != nullptr)
364 {
365 Condition() += '?';
366 // Ahead of the WHERE values, because the JOIN clause precedes the WHERE clause.
367 _searchCondition.inputBindings->insert(_searchCondition.inputBindings->begin()
368 + static_cast<std::ptrdiff_t>(_searchCondition.joinBindingCount),
369 SqlVariant { value });
370 ++_searchCondition.joinBindingCount;
371 }
372 else
373 detail::AppendLiteralValueInto(value, Condition(), _formatter);
374
375 return *this;
376 }
377
378 enum class NullSense : std::uint8_t
379 {
380 Null,
381 NotNull,
382 };
383
384 SqlJoinConditionBuilder& NullTest(std::string_view joinColumnName, NullSense sense, std::string_view op)
385 {
386 AppendJunctor(op);
387 AppendReferenceColumn(joinColumnName);
388 Condition() += sense == NullSense::Null ? " IS NULL" : " IS NOT NULL";
389 return *this;
390 }
391
392 template <typename Callable>
393 SqlJoinConditionBuilder& Group(Callable const& build, std::string_view op)
394 {
395 // Remember the state before the junctor so an empty group can roll the junctor back too --
396 // otherwise a dangling " AND " is left where the group would have been.
397 auto const sizeBeforeJunctor = Condition().size();
398 bool const wasFirstCall = _firstCall;
399
400 AppendJunctor(op);
401
402 auto const sizeBeforeParen = Condition().size();
403 Condition() += '(';
404
405 SqlJoinConditionBuilder nested { _referenceTable, &_searchCondition, &_formatter };
406 build(nested);
407
408 // An empty group would render as "()", which no dialect accepts. Drop the junctor and the
409 // opening parenthesis, and restore the first-call flag so the next condition is not junctored.
410 if (Condition().size() == sizeBeforeParen + 1)
411 {
412 Condition().resize(sizeBeforeJunctor);
413 _firstCall = wasFirstCall;
414 return *this;
415 }
416
417 Condition() += ')';
418 return *this;
419 }
420
421 std::string_view _referenceTable;
422 SqlSearchCondition& _searchCondition;
423 SqlQueryFormatter const& _formatter;
424 bool _firstCall = true;
425};
426
427/// Helper CRTP-based class for building WHERE clauses.
428///
429/// This class is inherited by the SqlSelectQueryBuilder, SqlUpdateQueryBuilder, and SqlDeleteQueryBuilder
430///
431/// @ingroup QueryBuilder
432template <typename Derived>
433class [[nodiscard]] SqlWhereClauseBuilder
434{
435 public:
436 /// Indicates, that the next WHERE clause should be AND-ed (default).
437 [[nodiscard]] Derived& And() noexcept;
438
439 /// Indicates, that the next WHERE clause should be OR-ed.
440 [[nodiscard]] Derived& Or() noexcept;
441
442 /// Indicates, that the next WHERE clause should be negated.
443 [[nodiscard]] Derived& Not() noexcept;
444
445 /// Constructs or extends a raw WHERE clause.
446 [[nodiscard]] Derived& WhereRaw(std::string_view sqlConditionExpression);
447
448 /// @brief Starts a conditional WHERE chain driven by a `std::optional<T>` value.
449 ///
450 /// The returned sub-builder exposes two `ThenWhere` overloads:
451 /// `ThenWhere(column)` appends `WHERE column = *value`, and
452 /// `ThenWhere(column, binaryOp)` appends `WHERE column <binaryOp> *value`
453 /// (e.g. `">="`, `"<"`, `"!="`). Both overloads emit nothing when @p value
454 /// is empty and return the underlying builder for further chaining.
455 ///
456 /// The optional is captured by reference and must outlive the chain.
457 ///
458 /// @tparam T The contained value type held by the optional.
459 /// @param value The optional whose presence gates the conditional WHERE.
460 /// @return A sub-builder exposing `ThenWhere(column)` and `ThenWhere(column, binaryOp)`.
461 ///
462 /// Example:
463 /// @code
464 /// std::optional<int> val { 42 };
465 /// builder.If(val).ThenWhere(FullyQualifiedNameOf<&Table::value>);
466 /// // Appends: WHERE "Table"."value" = 42
467 ///
468 /// std::optional<SqlDateTime> since = ...;
469 /// builder.If(since).ThenWhere(FullyQualifiedNameOf<&Events::createdAt>, ">=");
470 /// // Appends: WHERE "Events"."createdAt" >= '2026-05-18T12:30:45.000' (when since holds a value)
471 /// @endcode
472 template <typename T>
473 [[nodiscard]] auto If(std::optional<T> const& value) noexcept;
474
475 /// Constructs or extends a WHERE clause to test for a binary operation.
476 template <typename ColumnName, typename T>
477 [[nodiscard]] Derived& Where(ColumnName const& columnName, std::string_view binaryOp, T const& value);
478
479 /// Constructs or extends a WHERE clause to test for a binary operation for RHS as sub-select query.
480 template <typename ColumnName, typename SubSelectQuery>
481 requires(std::is_invocable_r_v<std::string, decltype(&SubSelectQuery::ToSql), SubSelectQuery const&>)
482 [[nodiscard]] Derived& Where(ColumnName const& columnName, std::string_view binaryOp, SubSelectQuery const& value);
483
484 /// Constructs or extends a WHERE/OR clause to test for a binary operation.
485 template <typename ColumnName, typename T>
486 [[nodiscard]] Derived& OrWhere(ColumnName const& columnName, std::string_view binaryOp, T const& value);
487
488 /// Constructs or extends a WHERE clause to test for a binary operation for RHS as string literal.
489 template <typename ColumnName, std::size_t N>
490 Derived& Where(ColumnName const& columnName, std::string_view binaryOp, char const (&value)[N]);
491
492 /// Constructs or extends a WHERE clause to test for equality.
493 template <typename ColumnName, typename T>
494 [[nodiscard]] Derived& Where(ColumnName const& columnName, T const& value);
495
496 /// Constructs or extends an WHERE/OR clause to test for equality.
497 template <typename ColumnName, typename T>
498 [[nodiscard]] Derived& OrWhere(ColumnName const& columnName, T const& value);
499
500 /// Constructs or extends a WHERE/AND clause to test for a group of values.
501 template <typename Callable>
502 requires std::invocable<Callable, SqlWhereClauseBuilder<Derived>&>
503 [[nodiscard]] Derived& Where(Callable const& callable);
504
505 /// Constructs or extends an WHERE/OR clause to test for a group of values.
506 template <typename Callable>
507 requires std::invocable<Callable, SqlWhereClauseBuilder<Derived>&>
508 [[nodiscard]] Derived& OrWhere(Callable const& callable);
509
510 /// Constructs or extends an WHERE/OR clause to test for a value, satisfying std::ranges::input_range.
511 template <typename ColumnName, std::ranges::input_range InputRange>
512 [[nodiscard]] Derived& WhereIn(ColumnName const& columnName, InputRange const& values);
513
514 /// Constructs or extends an WHERE/OR clause to test for a value, satisfying std::initializer_list.
515 template <typename ColumnName, typename T>
516 [[nodiscard]] Derived& WhereIn(ColumnName const& columnName, std::initializer_list<T> const& values);
517
518 /// Constructs or extends an WHERE/OR clause to test for a value, satisfying a sub-select query.
519 template <typename ColumnName, typename SubSelectQuery>
520 requires(std::is_invocable_r_v<std::string, decltype(&SubSelectQuery::ToSql), SubSelectQuery const&>)
521 [[nodiscard]] Derived& WhereIn(ColumnName const& columnName, SubSelectQuery const& subSelectQuery);
522
523 /// Constructs or extends an WHERE/OR clause to test for a value to be NULL.
524 template <typename ColumnName>
525 [[nodiscard]] Derived& WhereNull(ColumnName const& columnName);
526
527 /// Constructs or extends a WHERE clause to test for a value being not null.
528 template <typename ColumnName>
529 [[nodiscard]] Derived& WhereNotNull(ColumnName const& columnName);
530
531 /// Constructs or extends a WHERE clause to test for a value being equal to another column.
532 template <typename ColumnName, typename T>
533 [[nodiscard]] Derived& WhereNotEqual(ColumnName const& columnName, T const& value);
534
535 /// Constructs or extends a WHERE clause to test for a value being true.
536 template <typename ColumnName>
537 [[nodiscard]] Derived& WhereTrue(ColumnName const& columnName);
538
539 /// Constructs or extends a WHERE clause to test for a value being false.
540 template <typename ColumnName>
541 [[nodiscard]] Derived& WhereFalse(ColumnName const& columnName);
542
543 /// Constructs an INNER JOIN clause.
544 ///
545 /// @param joinTable The table's name to join with. This can be a string, a string_view, or an AliasedTableName.
546 /// @param joinColumnName The name of the column in the main table to join on.
547 /// @param onOtherColumn The column in the join table to compare against.
548 [[nodiscard]] Derived& InnerJoin(TableName auto joinTable,
549 std::string_view joinColumnName,
550 SqlQualifiedTableColumnName onOtherColumn);
551
552 /// Constructs an INNER JOIN clause.
553 [[nodiscard]] Derived& InnerJoin(TableName auto joinTable,
554 std::string_view joinColumnName,
555 std::string_view onMainTableColumn);
556
557 /// Constructs an INNER JOIN clause with a custom ON clause.
558 ///
559 /// @param joinTable The table to join with: a string, a string_view, or an AliasedTableName.
560 /// An aliased table renders as `JOIN "table" AS "alias"`, and the ON-clause
561 /// builder qualifies the joined table's columns with the alias, so the same table
562 /// can be joined more than once under different aliases.
563 /// @param onClauseBuilder Called with a SqlJoinConditionBuilder to compose the ON clause. If it
564 /// adds no condition, the join is dropped from the query.
565 /// @return The query builder, for chaining.
566 template <typename OnChainCallable>
567 requires std::invocable<OnChainCallable, SqlJoinConditionBuilder>
568 [[nodiscard]] Derived& InnerJoin(TableName auto joinTable, OnChainCallable const& onClauseBuilder);
569
570 /// Constructs an `INNER JOIN` clause given two fields from different records
571 /// using the field name as join column.
572 ///
573 /// @tparam LeftField The field name to join on, such as `JoinTestB::a_id`, which will join on table `JoinTestB` with
574 /// the column `a_id` to be compared against right field's column.
575 /// @tparam RightField The other column to compare and join against.
576 ///
577 /// Example:
578 /// @code
579 /// InnerJoin<&JoinTestB::a_id, &JoinTestA::id>()
580 /// // This will generate a INNER JOIN "JoinTestB" ON "InnerTestB"."a_id" = "JoinTestA"."id"
581 /// @endcode
582 template <auto LeftField, auto RightField>
583 [[nodiscard]] Derived& InnerJoin();
584
585 /// Constructs an LEFT OUTER JOIN clause.
586 [[nodiscard]] Derived& LeftOuterJoin(TableName auto joinTable,
587 std::string_view joinColumnName,
588 SqlQualifiedTableColumnName onOtherColumn);
589
590 /// Constructs an LEFT OUTER JOIN clause.
591 [[nodiscard]] Derived& LeftOuterJoin(TableName auto joinTable,
592 std::string_view joinColumnName,
593 std::string_view onMainTableColumn);
594
595 /// Constructs a LEFT OUTER JOIN clause with a custom ON clause.
596 ///
597 /// @param joinTable The table to join with: a string, a string_view, or an AliasedTableName.
598 /// An aliased table renders as `JOIN "table" AS "alias"`, and the ON-clause
599 /// builder qualifies the joined table's columns with the alias, so the same table
600 /// can be joined more than once under different aliases.
601 /// @param onClauseBuilder Called with a SqlJoinConditionBuilder to compose the ON clause. If it
602 /// adds no condition, the join is dropped from the query.
603 /// @return The query builder, for chaining.
604 template <typename OnChainCallable>
605 requires std::invocable<OnChainCallable, SqlJoinConditionBuilder>
606 [[nodiscard]] Derived& LeftOuterJoin(TableName auto joinTable, OnChainCallable const& onClauseBuilder);
607
608 /// Constructs an RIGHT OUTER JOIN clause.
609 [[nodiscard]] Derived& RightOuterJoin(TableName auto joinTable,
610 std::string_view joinColumnName,
611 SqlQualifiedTableColumnName onOtherColumn);
612
613 /// Constructs an RIGHT OUTER JOIN clause.
614 [[nodiscard]] Derived& RightOuterJoin(TableName auto joinTable,
615 std::string_view joinColumnName,
616 std::string_view onMainTableColumn);
617
618 /// Constructs a RIGHT OUTER JOIN clause with a custom ON clause.
619 ///
620 /// @param joinTable The table to join with: a string, a string_view, or an AliasedTableName.
621 /// An aliased table renders as `JOIN "table" AS "alias"`, and the ON-clause
622 /// builder qualifies the joined table's columns with the alias, so the same table
623 /// can be joined more than once under different aliases.
624 /// @param onClauseBuilder Called with a SqlJoinConditionBuilder to compose the ON clause. If it
625 /// adds no condition, the join is dropped from the query.
626 /// @return The query builder, for chaining.
627 template <typename OnChainCallable>
628 requires std::invocable<OnChainCallable, SqlJoinConditionBuilder>
629 [[nodiscard]] Derived& RightOuterJoin(TableName auto joinTable, OnChainCallable const& onClauseBuilder);
630
631 /// Constructs an FULL OUTER JOIN clause.
632 [[nodiscard]] Derived& FullOuterJoin(TableName auto joinTable,
633 std::string_view joinColumnName,
634 SqlQualifiedTableColumnName onOtherColumn);
635
636 /// Constructs an FULL OUTER JOIN clause.
637 [[nodiscard]] Derived& FullOuterJoin(TableName auto joinTable,
638 std::string_view joinColumnName,
639 std::string_view onMainTableColumn);
640
641 /// Constructs a FULL OUTER JOIN clause with a custom ON clause.
642 ///
643 /// @param joinTable The table to join with: a string, a string_view, or an AliasedTableName.
644 /// An aliased table renders as `JOIN "table" AS "alias"`, and the ON-clause
645 /// builder qualifies the joined table's columns with the alias, so the same table
646 /// can be joined more than once under different aliases.
647 /// @param onClauseBuilder Called with a SqlJoinConditionBuilder to compose the ON clause. If it
648 /// adds no condition, the join is dropped from the query.
649 /// @return The query builder, for chaining.
650 template <typename OnChainCallable>
651 requires std::invocable<OnChainCallable, SqlJoinConditionBuilder>
652 [[nodiscard]] Derived& FullOuterJoin(TableName auto joinTable, OnChainCallable const& onClauseBuilder);
653
654 protected:
655 /// What And(), Or() and Not() asked of the next condition.
657 {
658 /// Whether Or() was called; AND is the default otherwise.
659 bool isOr = false;
660 /// Whether Not() was called.
661 bool isNot = false;
662 };
663
664 /// Hands a pending And(), Or() or Not() over to a term of the HAVING clause.
665 ///
666 /// They are called on the shared builder, so they cannot tell which clause they are for. Leaving
667 /// them behind would make the next Where() begin with AND or OR rather than WHERE.
668 /// @return The connective requested; the WHERE clause continues as though it had not been asked.
669 [[nodiscard]] PendingConnective TakePendingConnective() noexcept;
670
671 /// The formatter of the derived builder, which SqlWhereClauseBuilder does not own itself.
672 [[nodiscard]] SqlQueryFormatter const& DerivedFormatter() const noexcept;
673
674 private:
675 /// The search condition of the derived builder, which SqlWhereClauseBuilder does not own itself.
676 [[nodiscard]] SqlSearchCondition& DerivedSearchCondition() noexcept;
677
678 enum class WhereJunctor : uint8_t
679 {
680 Null,
681 Where,
682 And,
683 Or,
684 };
685
686 WhereJunctor m_nextWhereJunctor = WhereJunctor::Where;
687 bool m_nextIsNot = false;
688
689 void AppendWhereJunctor();
690
691 /// Appends a column name to the WHERE condition.
692 template <typename ColumnName>
693 requires(std::same_as<ColumnName, SqlQualifiedTableColumnName> || std::convertible_to<ColumnName, std::string_view>
694 || std::convertible_to<ColumnName, std::string>)
695 void AppendColumnName(ColumnName const& columnName);
696
697 /// Appends a literal value to the WHERE condition.
698 template <typename LiteralType>
699 void AppendLiteralValue(LiteralType const& value);
700
701 /// Populates a literal value into the target string.
702 template <typename LiteralType, typename TargetType>
703 void PopulateLiteralValueInto(LiteralType const& value, TargetType& target);
704
705 template <typename LiteralType>
706 detail::RawSqlCondition PopulateSqlSetExpression(LiteralType const& values);
707
708 enum class JoinType : uint8_t
709 {
710 INNER,
711 LEFT,
712 RIGHT,
713 FULL
714 };
715
716 /// Constructs a JOIN clause.
717 [[nodiscard]] Derived& Join(JoinType joinType,
718 TableName auto joinTable,
719 std::string_view joinColumnName,
720 SqlQualifiedTableColumnName onOtherColumn);
721
722 /// Constructs a JOIN clause.
723 [[nodiscard]] Derived& Join(JoinType joinType,
724 TableName auto joinTable,
725 std::string_view joinColumnName,
726 std::string_view onMainTableColumn);
727
728 /// Constructs a JOIN clause.
729 template <typename OnChainCallable>
730 [[nodiscard]] Derived& Join(JoinType joinType, TableName auto joinTable, OnChainCallable const& onClauseBuilder);
731};
732
733enum class SqlResultOrdering : uint8_t
734{
735 ASCENDING,
736 DESCENDING
737};
738
739namespace detail
740{
741 enum class SelectType : std::uint8_t
742 {
743 Undefined,
744 Count,
745 All,
746 First,
747 Range
748 };
749
750 struct ComposedQuery
751 {
752 SelectType selectType = SelectType::Undefined;
753 SqlQueryFormatter const* formatter = nullptr;
754
755 bool distinct = false;
756 SqlSearchCondition searchCondition {};
757
758 std::string fields;
759
760 /// @brief The name of each projected column, in result-column order, as spelled by the caller.
761 ///
762 /// Populated by the projection methods of @c SqlSelectQueryBuilder so that result columns can be
763 /// addressed by name without asking the driver for result-set metadata (which cannot report table
764 /// names portably). An entry is empty for a projection that carries no caller-given name, such as
765 /// an un-aliased aggregate; the empty slot keeps the remaining entries aligned with their columns.
766 std::vector<std::string> projectedFieldNames;
767
768 /// @brief Whether the projection contains a wildcard, whose column count is unknown at build time.
769 ///
770 /// A wildcard makes every position after it unpredictable, so named column access is unavailable
771 /// for the whole query. Kept separate from an empty @c projectedFieldNames so the diagnostic can
772 /// name the actual cause.
773 bool projectionHasWildcard = false;
774
775 std::string orderBy;
776
777 /// @brief The GROUP BY clause, followed by the HAVING clause if there is one.
778 ///
779 /// HAVING directly follows GROUP BY in every dialect, so they travel as one fragment and every
780 /// formatter and every finalizer that honors GROUP BY honors HAVING with it.
781 std::string groupBy;
782
783 size_t offset = 0;
784 size_t limit = (std::numeric_limits<size_t>::max)();
785
786 [[nodiscard]] LIGHTWEIGHT_API std::string ToSql() const;
787
788 /// @brief The projected column names, in result-column order, for named column access.
789 /// @return One entry per result column, empty for unnamed projections; an empty span when the
790 /// query carries no usable mapping (e.g. the projection contains a wildcard).
791 [[nodiscard]] std::span<std::string const> ProjectedFieldNames() const noexcept
792 {
793 return projectedFieldNames;
794 }
795
796 /// @copydoc projectionHasWildcard
797 [[nodiscard]] bool ProjectionHasWildcard() const noexcept
798 {
799 return projectionHasWildcard;
800 }
801 };
802} // namespace detail
803
804/// @brief A SQL expression usable as a projection or as the left side of a HAVING term.
805///
806/// The factories in the @c Aggregate namespace create the common ones. The text is written into the
807/// statement verbatim, so it must not contain untrusted input.
808///
809/// @ingroup QueryBuilder
810struct [[nodiscard]] SqlFieldExpression final
811{
812 /// The SQL text of the expression.
813 std::string expression;
814};
815
816template <typename Derived>
817class [[nodiscard]] SqlBasicSelectQueryBuilder: public SqlWhereClauseBuilder<Derived>
818{
819 public:
820 /// Adds a DISTINCT clause to the SELECT query.
821 Derived& Distinct() noexcept;
822
823 /// Constructs or extends a ORDER BY clause.
824 Derived& OrderBy(SqlQualifiedTableColumnName const& columnName,
825 SqlResultOrdering ordering = SqlResultOrdering::ASCENDING);
826
827 /// Constructs or extends a ORDER BY clause.
828 Derived& OrderBy(std::string_view columnName, SqlResultOrdering ordering = SqlResultOrdering::ASCENDING);
829
830 /// Constructs or extends a GROUP BY clause.
831 ///
832 /// @throws std::logic_error if Having() was already called, since the columns must precede it.
833 Derived& GroupBy(std::string_view columnName);
834
835 /// Constructs or extends a GROUP BY clause with a qualified column name.
836 ///
837 /// @throws std::logic_error if Having() was already called, since the columns must precede it.
838 Derived& GroupBy(SqlQualifiedTableColumnName const& columnName);
839
840 /// Constructs or extends a HAVING clause, comparing an aggregate against a value.
841 ///
842 /// Terms are AND-ed unless Or() precedes them; And(), Or() and Not() apply to the next term of
843 /// either clause. Like a Where() value, @p value is bound when the query was given a bindings
844 /// vector and written into the statement text otherwise; the bindings stay in statement order
845 /// (ON, WHERE, HAVING) however the calls are interleaved.
846 ///
847 /// @tparam T The value's type.
848 /// @param aggregate The left side, such as `Aggregate::Count()` or `Aggregate::Sum("amount")`.
849 /// @param binaryOp The SQL comparison operator, written verbatim, as in Where().
850 /// @param value The value to compare against.
851 /// @return The query builder, for chaining.
852 /// @throws std::logic_error if no GroupBy() precedes it.
853 template <typename T>
854 [[nodiscard]] Derived& Having(SqlFieldExpression const& aggregate, std::string_view binaryOp, T const& value);
855
856 /// @copydoc Having(SqlFieldExpression const&, std::string_view, T const&)
857 template <std::size_t N>
858 [[nodiscard]] Derived& Having(SqlFieldExpression const& aggregate, std::string_view binaryOp, char const (&value)[N]);
859
860 /// Constructs or extends a HAVING clause with a raw SQL condition, written verbatim.
861 ///
862 /// @param rawSqlCondition The condition. It must not contain untrusted input.
863 /// @return The query builder, for chaining.
864 /// @throws std::logic_error if no GroupBy() precedes it.
865 [[nodiscard]] Derived& Having(std::string_view rawSqlCondition);
866
867 /// Constructs or extends a HAVING clause with a parenthesized group of terms.
868 ///
869 /// A group that adds no term adds nothing to the statement.
870 ///
871 /// @param group Called with this builder to add the terms of the group.
872 /// @return The query builder, for chaining.
873 /// @throws std::logic_error if no GroupBy() precedes it.
874 template <typename Callable>
875 requires std::invocable<Callable, Derived&>
876 [[nodiscard]] Derived& Having(Callable const& group);
877
878 /// Like Having(SqlFieldExpression const&, std::string_view, T const&), but OR-ed with the previous term.
879 ///
880 /// @tparam T The value's type.
881 /// @param aggregate The left side, such as `Aggregate::Count()` or `Aggregate::Sum("amount")`.
882 /// @param binaryOp The SQL comparison operator, written verbatim, as in Where().
883 /// @param value The value to compare against.
884 /// @return The query builder, for chaining.
885 /// @throws std::logic_error if no GroupBy() precedes it.
886 template <typename T>
887 [[nodiscard]] Derived& OrHaving(SqlFieldExpression const& aggregate, std::string_view binaryOp, T const& value);
888
889 /// Like Having(std::string_view), but OR-ed with the previous term.
890 ///
891 /// @param rawSqlCondition The condition. It must not contain untrusted input.
892 /// @return The query builder, for chaining.
893 /// @throws std::logic_error if no GroupBy() precedes it.
894 [[nodiscard]] Derived& OrHaving(std::string_view rawSqlCondition);
895
896 /// Like Having(Callable const&), but OR-ed with the previous term.
897 ///
898 /// @param group Called with this builder to add the terms of the group.
899 /// @return The query builder, for chaining.
900 /// @throws std::logic_error if no GroupBy() precedes it.
901 template <typename Callable>
902 requires std::invocable<Callable, Derived&>
903 [[nodiscard]] Derived& OrHaving(Callable const& group);
904
905 using ComposedQuery = detail::ComposedQuery;
906
907 protected:
908 /// Whether the query has a HAVING term, for finalizers that cannot honor one.
909 /// @return True once Having() or OrHaving() added a term.
910 [[nodiscard]] bool HasHavingClause() const noexcept
911 {
912 return _nextHavingTerm != NextHavingTerm::OpensClause;
913 }
914
915 // mutable so const finalizers / projection-helpers can delegate to the
916 // non-const implementations via const_cast (idiomatic builder pattern —
917 // the observable const-state is the produced SQL, not the accumulator).
918 mutable ComposedQuery _query {}; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
919
920 private:
921 /// What has to precede the next term of the HAVING clause.
922 enum class NextHavingTerm : std::uint8_t
923 {
924 OpensClause,
925 OpensGroup,
926 Continues,
927 };
928
929 /// Starts a GROUP BY column: the clause keyword or a comma.
930 void AppendGroupBySeparator();
931
932 /// Starts a HAVING term: the clause keyword or a junctor, plus NOT if requested.
933 void AppendHavingJunctor();
934
935 NextHavingTerm _nextHavingTerm = NextHavingTerm::OpensClause;
936};
937
938template <typename Derived>
939inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlBasicSelectQueryBuilder<Derived>::Distinct() noexcept
940{
941 _query.distinct = true;
942 return static_cast<Derived&>(*this);
943}
944
945template <typename Derived>
946inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlBasicSelectQueryBuilder<Derived>::OrderBy(std::string_view columnName,
947 SqlResultOrdering ordering)
948{
949 if (_query.orderBy.empty())
950 _query.orderBy += "\n ORDER BY ";
951 else
952 _query.orderBy += ", ";
953
954 _query.orderBy += '"';
955 _query.orderBy += columnName;
956 _query.orderBy += '"';
957
958 if (ordering == SqlResultOrdering::DESCENDING)
959 _query.orderBy += " DESC";
960 else if (ordering == SqlResultOrdering::ASCENDING)
961 _query.orderBy += " ASC";
962
963 return static_cast<Derived&>(*this);
964}
965
966template <typename Derived>
967inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlBasicSelectQueryBuilder<Derived>::OrderBy(
968 SqlQualifiedTableColumnName const& columnName, SqlResultOrdering ordering)
969{
970 if (_query.orderBy.empty())
971 _query.orderBy += "\n ORDER BY ";
972 else
973 _query.orderBy += ", ";
974
975 _query.orderBy += '"';
976 _query.orderBy += columnName.tableName;
977 _query.orderBy += "\".\"";
978 _query.orderBy += columnName.columnName;
979 _query.orderBy += '"';
980
981 if (ordering == SqlResultOrdering::DESCENDING)
982 _query.orderBy += " DESC";
983 else if (ordering == SqlResultOrdering::ASCENDING)
984 _query.orderBy += " ASC";
985
986 return static_cast<Derived&>(*this);
987}
988
989template <typename Derived>
990inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlBasicSelectQueryBuilder<Derived>::GroupBy(std::string_view columnName)
991{
992 AppendGroupBySeparator();
993
994 _query.groupBy += '"';
995 _query.groupBy += columnName;
996 _query.groupBy += '"';
997
998 return static_cast<Derived&>(*this);
999}
1000
1001template <typename Derived>
1002inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlBasicSelectQueryBuilder<Derived>::GroupBy(
1003 SqlQualifiedTableColumnName const& columnName)
1004{
1005 AppendGroupBySeparator();
1006
1007 _query.groupBy += '"';
1008 _query.groupBy += columnName.tableName;
1009 _query.groupBy += "\".\"";
1010 _query.groupBy += columnName.columnName;
1011 _query.groupBy += '"';
1012
1013 return static_cast<Derived&>(*this);
1014}
1015
1016template <typename Derived>
1017inline void SqlBasicSelectQueryBuilder<Derived>::AppendGroupBySeparator()
1018{
1019 if (HasHavingClause())
1020 throw std::logic_error { "GroupBy() was called after Having(); the GROUP BY columns must come first." };
1021
1022 if (_query.groupBy.empty())
1023 _query.groupBy += "\n GROUP BY ";
1024 else
1025 _query.groupBy += ", ";
1026}
1027
1028template <typename Derived>
1029inline void SqlBasicSelectQueryBuilder<Derived>::AppendHavingJunctor()
1030{
1031 if (_query.groupBy.empty())
1032 throw std::logic_error { "Having() requires a preceding GroupBy()." };
1033
1034 auto const pending = this->TakePendingConnective();
1035
1036 switch (_nextHavingTerm)
1037 {
1038 case NextHavingTerm::OpensClause:
1039 _query.groupBy += "\n HAVING ";
1040 break;
1041 case NextHavingTerm::OpensGroup:
1042 break;
1043 case NextHavingTerm::Continues:
1044 _query.groupBy += pending.isOr ? " OR " : " AND ";
1045 break;
1046 }
1047
1048 if (pending.isNot)
1049 _query.groupBy += "NOT ";
1050
1051 _nextHavingTerm = NextHavingTerm::Continues;
1052}
1053
1054template <typename Derived>
1055template <typename T>
1056inline Derived& SqlBasicSelectQueryBuilder<Derived>::Having(SqlFieldExpression const& aggregate,
1057 std::string_view binaryOp,
1058 T const& value)
1059{
1060 AppendHavingJunctor();
1061
1062 _query.groupBy += aggregate.expression;
1063 _query.groupBy += ' ';
1064 _query.groupBy += binaryOp;
1065 _query.groupBy += ' ';
1066
1067 auto* const inputBindings = _query.searchCondition.inputBindings;
1068 auto const bindingsBefore = inputBindings != nullptr ? inputBindings->size() : 0;
1069 detail::AppendConditionValue(value, _query.groupBy, inputBindings, 0, this->DerivedFormatter());
1070 if (inputBindings != nullptr)
1071 _query.searchCondition.havingBindingCount += inputBindings->size() - bindingsBefore;
1072
1073 return static_cast<Derived&>(*this);
1074}
1075
1076template <typename Derived>
1077template <std::size_t N>
1078inline Derived& SqlBasicSelectQueryBuilder<Derived>::Having(SqlFieldExpression const& aggregate,
1079 std::string_view binaryOp,
1080 char const (&value)[N])
1081{
1082 return Having(aggregate, binaryOp, std::string_view { value, N - 1 });
1083}
1084
1085template <typename Derived>
1086inline Derived& SqlBasicSelectQueryBuilder<Derived>::Having(std::string_view rawSqlCondition)
1087{
1088 AppendHavingJunctor();
1089 _query.groupBy += rawSqlCondition;
1090 return static_cast<Derived&>(*this);
1091}
1092
1093template <typename Derived>
1094template <typename Callable>
1095 requires std::invocable<Callable, Derived&>
1096inline Derived& SqlBasicSelectQueryBuilder<Derived>::Having(Callable const& group)
1097{
1098 auto const originalSize = _query.groupBy.size();
1099 auto const originalNextTerm = _nextHavingTerm;
1100
1101 AppendHavingJunctor();
1102 _nextHavingTerm = NextHavingTerm::OpensGroup;
1103 _query.groupBy += '(';
1104
1105 auto const sizeBeforeGroup = _query.groupBy.size();
1106 (void) group(static_cast<Derived&>(*this));
1107
1108 if (_query.groupBy.size() == sizeBeforeGroup)
1109 {
1110 // "()" is not valid SQL in any dialect, so an empty group leaves no trace.
1111 _query.groupBy.resize(originalSize);
1112 _nextHavingTerm = originalNextTerm;
1113 }
1114 else
1115 _query.groupBy += ')';
1116
1117 return static_cast<Derived&>(*this);
1118}
1119
1120template <typename Derived>
1121template <typename T>
1122inline Derived& SqlBasicSelectQueryBuilder<Derived>::OrHaving(SqlFieldExpression const& aggregate,
1123 std::string_view binaryOp,
1124 T const& value)
1125{
1126 return this->Or().Having(aggregate, binaryOp, value);
1127}
1128
1129template <typename Derived>
1130inline Derived& SqlBasicSelectQueryBuilder<Derived>::OrHaving(std::string_view rawSqlCondition)
1131{
1132 return this->Or().Having(rawSqlCondition);
1133}
1134
1135template <typename Derived>
1136template <typename Callable>
1137 requires std::invocable<Callable, Derived&>
1138inline Derived& SqlBasicSelectQueryBuilder<Derived>::OrHaving(Callable const& group)
1139{
1140 return this->Or().Having(group);
1141}
1142
1143template <typename Derived>
1144inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::And() noexcept
1145{
1146 m_nextWhereJunctor = WhereJunctor::And;
1147 return static_cast<Derived&>(*this);
1148}
1149
1150template <typename Derived>
1151inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::Or() noexcept
1152{
1153 m_nextWhereJunctor = WhereJunctor::Or;
1154 return static_cast<Derived&>(*this);
1155}
1156
1157template <typename Derived>
1158inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::Not() noexcept
1159{
1160 m_nextIsNot = !m_nextIsNot;
1161 return static_cast<Derived&>(*this);
1162}
1163
1164/// Constructs or extends a WHERE clause to test for equality.
1165template <typename Derived>
1166template <typename ColumnName, typename T>
1167inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::Where(ColumnName const& columnName, T const& value)
1168{
1169 if constexpr (detail::OneOf<T, SqlNullType, std::nullopt_t>)
1170 {
1171 if (m_nextIsNot)
1172 {
1173 m_nextIsNot = false;
1174 return Where(columnName, "IS NOT", value);
1175 }
1176 else
1177 return Where(columnName, "IS", value);
1178 }
1179 else
1180 return Where(columnName, "=", value);
1181}
1182
1183/// Constructs or extends a WHERE/OR clause to test for equality.
1184template <typename Derived>
1185template <typename ColumnName, typename T>
1186inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::OrWhere(ColumnName const& columnName,
1187 T const& value)
1188{
1189 return Or().Where(columnName, value);
1190}
1191
1192/// Constructs or extends a WHERE/OR clause to test for a group of values.
1193template <typename Derived>
1194template <typename Callable>
1195 requires std::invocable<Callable, SqlWhereClauseBuilder<Derived>&>
1196inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::OrWhere(Callable const& callable)
1197{
1198 return Or().Where(callable);
1199}
1200
1201/// Constructs or extends a WHERE/AND clause to test for a group of values.
1202template <typename Derived>
1203template <typename Callable>
1204 requires std::invocable<Callable, SqlWhereClauseBuilder<Derived>&>
1205inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::Where(Callable const& callable)
1206{
1207 auto& condition = DerivedSearchCondition().condition;
1208
1209 auto const originalSize = condition.size();
1210
1211 AppendWhereJunctor();
1212 m_nextWhereJunctor = WhereJunctor::Null;
1213 condition += '(';
1214
1215 auto const sizeBeforeCallable = condition.size();
1216
1217 (void) callable(*this);
1218
1219 if (condition.size() == sizeBeforeCallable)
1220 condition.resize(originalSize);
1221 else
1222 condition += ')';
1223
1224 return static_cast<Derived&>(*this);
1225}
1226
1227/// Constructs or extends a WHERE IN clause with an input range.
1228template <typename Derived>
1229template <typename ColumnName, std::ranges::input_range InputRange>
1230inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::WhereIn(ColumnName const& columnName,
1231 InputRange const& values)
1232{
1233 // An empty IN-set means "match nothing"; emitting no condition would mean "match everything".
1234 // `1 = 0` rather than `FALSE` because SQL Server has no boolean literal.
1235 //
1236 // std::ranges::empty rather than values.empty(): the latter requires a member function, which
1237 // excludes ranges such as built-in arrays that this overload otherwise handles fine.
1238 if (std::ranges::empty(values))
1239 return WhereRaw("1 = 0");
1240 return Where(columnName, "IN", PopulateSqlSetExpression(values));
1241}
1242
1243/// Constructs or extends a WHERE IN clause with an initializer list.
1244template <typename Derived>
1245template <typename ColumnName, typename T>
1246inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::WhereIn(ColumnName const& columnName,
1247 std::initializer_list<T> const& values)
1248{
1249 // See the range overload above: an empty IN-set must not silently drop the condition.
1250 if (values.begin() == values.end())
1251 return WhereRaw("1 = 0");
1252 return Where(columnName, "IN", PopulateSqlSetExpression(values));
1253}
1254
1255/// Constructs or extends a WHERE IN clause with a sub-select query.
1256template <typename Derived>
1257template <typename ColumnName, typename SubSelectQuery>
1258 requires(std::is_invocable_r_v<std::string, decltype(&SubSelectQuery::ToSql), SubSelectQuery const&>)
1259inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::WhereIn(ColumnName const& columnName,
1260 SubSelectQuery const& subSelectQuery)
1261{
1262 return Where(columnName, "IN", detail::RawSqlCondition { "(" + subSelectQuery.ToSql() + ")" });
1263}
1264
1265/// Constructs or extends a WHERE clause to test for a value being not null.
1266template <typename Derived>
1267template <typename ColumnName>
1268inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::WhereNotNull(ColumnName const& columnName)
1269{
1270 return Where(columnName, "IS NOT", detail::RawSqlCondition { "NULL" });
1271}
1272
1273/// Constructs or extends a WHERE clause to test for a value being null.
1274template <typename Derived>
1275template <typename ColumnName>
1276inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::WhereNull(ColumnName const& columnName)
1277{
1278 return Where(columnName, "IS", detail::RawSqlCondition { "NULL" });
1279}
1280
1281/// Constructs or extends a WHERE clause to test for inequality.
1282template <typename Derived>
1283template <typename ColumnName, typename T>
1284inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::WhereNotEqual(ColumnName const& columnName,
1285 T const& value)
1286{
1287 if constexpr (detail::OneOf<T, SqlNullType, std::nullopt_t>)
1288 return Where(columnName, "IS NOT", value);
1289 else
1290 return Where(columnName, "!=", value);
1291}
1292
1293/// Constructs or extends a WHERE clause to test for a value being true.
1294template <typename Derived>
1295template <typename ColumnName>
1296inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::WhereTrue(ColumnName const& columnName)
1297{
1298 return Where(columnName, "=", true);
1299}
1300
1301/// Constructs or extends a WHERE clause to test for a value being false.
1302template <typename Derived>
1303template <typename ColumnName>
1304inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::WhereFalse(ColumnName const& columnName)
1305{
1306 return Where(columnName, "=", false);
1307}
1308
1309/// @brief Largest IN-set that WhereIn passes as bound parameters.
1310///
1311/// Beyond this the set is written into the SQL text as literals, which is what WhereIn did before
1312/// it learned to bind. Drivers cap how many parameters one statement may carry -- MS SQL Server
1313/// refuses more than 2100 with "07002 COUNT field incorrect" -- and a caller filtering on a few
1314/// thousand keys would otherwise build a statement no driver accepts. The margin below that cap
1315/// leaves room for the parameters the rest of the statement contributes.
1316///
1317/// @ingroup QueryBuilder
1318constexpr inline std::size_t SqlMaxBoundSetSize = 2000;
1319
1320template <typename T>
1321struct WhereConditionLiteralType
1322{
1323 constexpr static bool needsQuotes = !std::is_integral_v<T> && !std::is_floating_point_v<T> && !std::same_as<T, bool>
1324 && !std::same_as<T, SqlWildcardType>;
1325};
1326
1327/// Constructs or extends a WHERE clause with a string literal value.
1328template <typename Derived>
1329template <typename ColumnName, std::size_t N>
1330inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::Where(ColumnName const& columnName,
1331 std::string_view binaryOp,
1332 char const (&value)[N])
1333{
1334 return Where(columnName, binaryOp, std::string_view { value, N - 1 });
1335}
1336
1337/// Constructs or extends a WHERE clause to test for a binary operation.
1338template <typename Derived>
1339template <typename ColumnName, typename T>
1340inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::Where(ColumnName const& columnName,
1341 std::string_view binaryOp,
1342 T const& value)
1343{
1344 auto& searchCondition = DerivedSearchCondition();
1345
1346 AppendWhereJunctor();
1347 AppendColumnName(columnName);
1348 searchCondition.condition += ' ';
1349 searchCondition.condition += binaryOp;
1350 searchCondition.condition += ' ';
1351 AppendLiteralValue(value);
1352
1353 return static_cast<Derived&>(*this);
1354}
1355
1356/// Constructs or extends a WHERE clause with a sub-select query.
1357template <typename Derived>
1358template <typename ColumnName, typename SubSelectQuery>
1359 requires(std::is_invocable_r_v<std::string, decltype(&SubSelectQuery::ToSql), SubSelectQuery const&>)
1360inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::Where(ColumnName const& columnName,
1361 std::string_view binaryOp,
1362 SubSelectQuery const& value)
1363{
1364 return Where(columnName, binaryOp, detail::RawSqlCondition { "(" + value.ToSql() + ")" });
1365}
1366
1367/// Constructs or extends a WHERE/OR clause with a binary operation.
1368template <typename Derived>
1369template <typename ColumnName, typename T>
1370inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::OrWhere(ColumnName const& columnName,
1371 std::string_view binaryOp,
1372 T const& value)
1373{
1374 return Or().Where(columnName, binaryOp, value);
1375}
1376
1377template <typename Derived>
1378inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::InnerJoin(TableName auto joinTable,
1379 std::string_view joinColumnName,
1380 SqlQualifiedTableColumnName onOtherColumn)
1381{
1382 return Join(JoinType::INNER, joinTable, joinColumnName, onOtherColumn);
1383}
1384
1385template <typename Derived>
1386inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::InnerJoin(TableName auto joinTable,
1387 std::string_view joinColumnName,
1388 std::string_view onMainTableColumn)
1389{
1390 return Join(JoinType::INNER, joinTable, joinColumnName, onMainTableColumn);
1391}
1392
1393template <typename Derived>
1394template <auto LeftField, auto RightField>
1396{
1397#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1398 return Join(JoinType::INNER,
1399 RecordTableName<MemberClassType<LeftField>>,
1400 FieldNameOf<LeftField>,
1401 SqlQualifiedTableColumnName { RecordTableName<MemberClassType<RightField>>, FieldNameOf<RightField> });
1402#else
1403 return Join(
1404 JoinType::INNER,
1405 RecordTableName<Reflection::MemberClassType<LeftField>>,
1406 FieldNameOf<LeftField>,
1407 SqlQualifiedTableColumnName { RecordTableName<Reflection::MemberClassType<RightField>>, FieldNameOf<RightField> });
1408#endif
1409}
1410
1411template <typename Derived>
1412template <typename OnChainCallable>
1413 requires std::invocable<OnChainCallable, SqlJoinConditionBuilder>
1414Derived& SqlWhereClauseBuilder<Derived>::InnerJoin(TableName auto joinTable, OnChainCallable const& onClauseBuilder)
1415{
1416 return Join(JoinType::INNER, joinTable, onClauseBuilder);
1417}
1418
1419template <typename Derived>
1420inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::LeftOuterJoin(
1421 TableName auto joinTable, std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn)
1422{
1423 return Join(JoinType::LEFT, joinTable, joinColumnName, onOtherColumn);
1424}
1425
1426template <typename Derived>
1427inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::LeftOuterJoin(TableName auto joinTable,
1428 std::string_view joinColumnName,
1429 std::string_view onMainTableColumn)
1430{
1431 return Join(JoinType::LEFT, joinTable, joinColumnName, onMainTableColumn);
1432}
1433
1434template <typename Derived>
1435template <typename OnChainCallable>
1436 requires std::invocable<OnChainCallable, SqlJoinConditionBuilder>
1437Derived& SqlWhereClauseBuilder<Derived>::LeftOuterJoin(TableName auto joinTable, OnChainCallable const& onClauseBuilder)
1438{
1439 return Join(JoinType::LEFT, joinTable, onClauseBuilder);
1440}
1441
1442template <typename Derived>
1443inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::RightOuterJoin(
1444 TableName auto joinTable, std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn)
1445{
1446 return Join(JoinType::RIGHT, joinTable, joinColumnName, onOtherColumn);
1447}
1448
1449template <typename Derived>
1450inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::RightOuterJoin(TableName auto joinTable,
1451 std::string_view joinColumnName,
1452 std::string_view onMainTableColumn)
1453{
1454 return Join(JoinType::RIGHT, joinTable, joinColumnName, onMainTableColumn);
1455}
1456
1457template <typename Derived>
1458template <typename OnChainCallable>
1459 requires std::invocable<OnChainCallable, SqlJoinConditionBuilder>
1460Derived& SqlWhereClauseBuilder<Derived>::RightOuterJoin(TableName auto joinTable, OnChainCallable const& onClauseBuilder)
1461{
1462 return Join(JoinType::RIGHT, joinTable, onClauseBuilder);
1463}
1464
1465template <typename Derived>
1466inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::FullOuterJoin(
1467 TableName auto joinTable, std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn)
1468{
1469 return Join(JoinType::FULL, joinTable, joinColumnName, onOtherColumn);
1470}
1471
1472template <typename Derived>
1473inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::FullOuterJoin(TableName auto joinTable,
1474 std::string_view joinColumnName,
1475 std::string_view onMainTableColumn)
1476{
1477 return Join(JoinType::FULL, joinTable, joinColumnName, onMainTableColumn);
1478}
1479
1480template <typename Derived>
1481template <typename OnChainCallable>
1482 requires std::invocable<OnChainCallable, SqlJoinConditionBuilder>
1483Derived& SqlWhereClauseBuilder<Derived>::FullOuterJoin(TableName auto joinTable, OnChainCallable const& onClauseBuilder)
1484{
1485 return Join(JoinType::FULL, joinTable, onClauseBuilder);
1486}
1487
1488namespace detail
1489{
1490
1491 /// @brief Sub-builder returned by `SqlWhereClauseBuilder::If`.
1492 ///
1493 /// Captures the underlying builder and a gating `std::optional`. Calling
1494 /// `ThenWhere(column)` or `ThenWhere(column, binaryOp)` commits the chain:
1495 /// when the optional holds a value it appends `WHERE column = *value` (or
1496 /// `WHERE column <binaryOp> *value` for the explicit-operator overload);
1497 /// either way it returns the underlying builder so further methods can be
1498 /// chained.
1499 template <typename Derived, typename T>
1500 class [[nodiscard]] ConditionalWhereBuilder
1501 {
1502 public:
1503 /// Constructs the sub-builder, binding to the underlying builder and the gating optional.
1504 constexpr ConditionalWhereBuilder(Derived& builder, std::optional<T> const& value) noexcept:
1505 _builder { builder },
1506 _value { value }
1507 {
1508 }
1509
1510 /// Commits the conditional WHERE for @p column. Appends
1511 /// `WHERE column = *value` when the gating optional holds a value;
1512 /// otherwise the builder is left untouched.
1513 /// @param column The column name (string, `SqlQualifiedTableColumnName`, etc.).
1514 /// @return Reference to the underlying query builder.
1515 template <typename ColumnName>
1516 [[nodiscard]] Derived& ThenWhere(ColumnName const& column) const
1517 {
1518 if (_value.has_value())
1519 return _builder.Where(column, *_value);
1520 return _builder;
1521 }
1522
1523 /// Commits the conditional WHERE for @p column using an explicit binary
1524 /// operator. Appends `WHERE column <binaryOp> *value` when the gating
1525 /// optional holds a value; otherwise the builder is left untouched.
1526 /// Mirrors the `Where(column, binaryOp, value)` overload — use it for
1527 /// range-style filters such as `">="`, `"<"`, `"!="`, or `"LIKE"`.
1528 /// @param column The column name (string, `SqlQualifiedTableColumnName`, etc.).
1529 /// @param binaryOp The SQL binary operator (e.g. `">="`, `"<"`, `"!="`).
1530 /// @return Reference to the underlying query builder.
1531 ///
1532 /// Example:
1533 /// @code
1534 /// std::optional<SqlDateTime> since = ...;
1535 /// std::optional<SqlDateTime> until = ...;
1536 /// q.FromTable("Events").Select().Field("id")
1537 /// .If(since).ThenWhere(FullyQualifiedNameOf<&Events::createdAt>, ">=")
1538 /// .If(until).ThenWhere(FullyQualifiedNameOf<&Events::createdAt>, "<")
1539 /// .All();
1540 /// @endcode
1541 template <typename ColumnName>
1542 [[nodiscard]] Derived& ThenWhere(ColumnName const& column, std::string_view binaryOp) const
1543 {
1544 if (_value.has_value())
1545 return _builder.Where(column, binaryOp, *_value);
1546 return _builder;
1547 }
1548
1549 private:
1550 Derived& _builder;
1551 std::optional<T> const& _value;
1552 };
1553
1554} // namespace detail
1555
1556/// Starts a conditional WHERE chain gated by a `std::optional` value.
1557template <typename Derived>
1558template <typename T>
1559inline LIGHTWEIGHT_FORCE_INLINE auto SqlWhereClauseBuilder<Derived>::If(std::optional<T> const& value) noexcept
1560{
1561 return detail::ConditionalWhereBuilder<Derived, T> { static_cast<Derived&>(*this), value };
1562}
1563
1564template <typename Derived>
1565inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::WhereRaw(std::string_view sqlConditionExpression)
1566{
1567 AppendWhereJunctor();
1568
1569 auto& condition = DerivedSearchCondition().condition;
1570 condition += sqlConditionExpression;
1571
1572 return static_cast<Derived&>(*this);
1573}
1574
1575template <typename Derived>
1577{
1578 auto const pending = PendingConnective { .isOr = m_nextWhereJunctor == WhereJunctor::Or, .isNot = m_nextIsNot };
1579
1580 m_nextIsNot = false;
1581 if (m_nextWhereJunctor == WhereJunctor::And || m_nextWhereJunctor == WhereJunctor::Or)
1582 m_nextWhereJunctor = DerivedSearchCondition().condition.empty() ? WhereJunctor::Where : WhereJunctor::And;
1583
1584 return pending;
1585}
1586
1587template <typename Derived>
1588inline LIGHTWEIGHT_FORCE_INLINE SqlSearchCondition& SqlWhereClauseBuilder<Derived>::DerivedSearchCondition() noexcept
1589{
1590 return static_cast<Derived*>(this)->SearchCondition();
1591}
1592
1593template <typename Derived>
1594inline LIGHTWEIGHT_FORCE_INLINE SqlQueryFormatter const& SqlWhereClauseBuilder<Derived>::DerivedFormatter() const noexcept
1595{
1596 return static_cast<Derived const*>(this)->Formatter();
1597}
1598
1599template <typename Derived>
1600inline LIGHTWEIGHT_FORCE_INLINE void SqlWhereClauseBuilder<Derived>::AppendWhereJunctor()
1601{
1602 using namespace std::string_view_literals;
1603
1604 auto& condition = DerivedSearchCondition().condition;
1605
1606 switch (m_nextWhereJunctor)
1607 {
1608 case WhereJunctor::Null:
1609 break;
1610 case WhereJunctor::Where:
1611 condition += "\n WHERE "sv;
1612 break;
1613 case WhereJunctor::And:
1614 condition += " AND "sv;
1615 break;
1616 case WhereJunctor::Or:
1617 condition += " OR "sv;
1618 break;
1619 }
1620
1621 if (m_nextIsNot)
1622 {
1623 condition += "NOT "sv;
1624 m_nextIsNot = false;
1625 }
1626
1627 m_nextWhereJunctor = WhereJunctor::And;
1628}
1629
1630/// Appends a column name to the WHERE condition.
1631template <typename Derived>
1632template <typename ColumnName>
1633 requires(std::same_as<ColumnName, SqlQualifiedTableColumnName> || std::convertible_to<ColumnName, std::string_view>
1634 || std::convertible_to<ColumnName, std::string>)
1635inline LIGHTWEIGHT_FORCE_INLINE void SqlWhereClauseBuilder<Derived>::AppendColumnName(ColumnName const& columnName)
1636{
1637 DerivedSearchCondition().condition += detail::MakeSqlColumnName(columnName);
1638}
1639
1640/// Appends a literal value to the WHERE condition.
1641template <typename Derived>
1642template <typename LiteralType>
1643inline LIGHTWEIGHT_FORCE_INLINE void SqlWhereClauseBuilder<Derived>::AppendLiteralValue(LiteralType const& value)
1644{
1645 auto& searchCondition = DerivedSearchCondition();
1646
1647 detail::AppendConditionValue(value,
1648 searchCondition.condition,
1649 searchCondition.inputBindings,
1650 searchCondition.havingBindingCount,
1651 DerivedFormatter());
1652}
1653
1654namespace detail
1655{
1656
1657 template <typename LiteralType, typename TargetType>
1658 void AppendLiteralValueInto(LiteralType const& value, TargetType& target, SqlQueryFormatter const& formatter)
1659 {
1660 if constexpr (std::is_same_v<LiteralType, SqlQualifiedTableColumnName>)
1661 {
1662 target += '"';
1663 target += value.tableName;
1664 target += "\".\"";
1665 target += value.columnName;
1666 target += '"';
1667 }
1668 else if constexpr (detail::OneOf<LiteralType, SqlNullType, std::nullopt_t>)
1669 {
1670 target += "NULL";
1671 }
1672 else if constexpr (std::is_same_v<LiteralType, SqlWildcardType>)
1673 {
1674 target += '?';
1675 }
1676 else if constexpr (std::is_same_v<LiteralType, detail::RawSqlCondition>)
1677 {
1678 target += value.condition;
1679 }
1680 else if constexpr (std::is_same_v<LiteralType, bool>)
1681 {
1682 target += formatter.BooleanLiteral(value);
1683 }
1684 else if constexpr (!WhereConditionLiteralType<LiteralType>::needsQuotes)
1685 {
1686 target += std::format("{}", value);
1687 }
1688 else
1689 {
1690 target += detail::MakeEscapedSqlString(std::format("{}", value));
1691 }
1692 }
1693
1694 template <typename LiteralType>
1695 void AppendConditionValue(LiteralType const& value,
1696 std::string& target,
1697 std::vector<SqlVariant>* inputBindings,
1698 std::size_t bindingsBehind,
1699 SqlQueryFormatter const& formatter)
1700 {
1701 if constexpr (std::is_same_v<LiteralType, SqlQualifiedTableColumnName>
1702 || detail::OneOf<LiteralType, SqlNullType, std::nullopt_t>
1703 || std::is_same_v<LiteralType, SqlWildcardType>
1704 || std::is_same_v<LiteralType, detail::RawSqlCondition>)
1705 {
1706 AppendLiteralValueInto(value, target, formatter);
1707 }
1708 else if (inputBindings)
1709 {
1710 target += '?';
1711 inputBindings->emplace(inputBindings->end() - static_cast<std::ptrdiff_t>(bindingsBehind), value);
1712 }
1713 else if constexpr (detail::IsBinaryLiteral<LiteralType>)
1714 {
1715 // Formatting it would put a description of the value, not the value, into the SQL text.
1716 throw std::invalid_argument { "A binary value cannot be written into the SQL text as a literal. Compare it "
1717 "through a query that binds its values (e.g. DataMapper::Query()), or bind it "
1718 "to a SqlWildcard placeholder." };
1719 }
1720 else
1721 {
1722 AppendLiteralValueInto(value, target, formatter);
1723 }
1724 }
1725
1726} // namespace detail
1727
1728/// Populates a literal value into the target string.
1729template <typename Derived>
1730template <typename LiteralType, typename TargetType>
1731inline LIGHTWEIGHT_FORCE_INLINE void SqlWhereClauseBuilder<Derived>::PopulateLiteralValueInto(LiteralType const& value,
1732 TargetType& target)
1733{
1734 detail::AppendLiteralValueInto(value, target, DerivedFormatter());
1735}
1736
1737template <typename Derived>
1738template <typename LiteralType>
1739detail::RawSqlCondition SqlWhereClauseBuilder<Derived>::PopulateSqlSetExpression(LiteralType const& values)
1740{
1741 using namespace std::string_view_literals;
1742
1743 using ValueType = std::ranges::range_value_t<LiteralType>;
1744
1745 // Mirrors the dispatch in AppendLiteralValue: these types have no parameter representation and
1746 // must stay inline in the SQL text. Everything else becomes a parameter marker whenever the
1747 // caller supplied a bindings vector, so that the statement stays reusable across differing
1748 // IN-sets and the driver — not this builder — encodes the value.
1749 constexpr bool isBindable =
1750 !(std::is_same_v<ValueType, SqlQualifiedTableColumnName> || detail::OneOf<ValueType, SqlNullType, std::nullopt_t>
1751 || std::is_same_v<ValueType, SqlWildcardType> || std::is_same_v<ValueType, detail::RawSqlCondition>);
1752
1753 auto& searchCondition = DerivedSearchCondition();
1754
1755 std::ostringstream fragment;
1756
1757 // String literals decay to a raw character pointer inside an initializer list (and inside a
1758 // built-in array), and SqlVariant cannot be constructed from one: std::variant's converting
1759 // constructor is ambiguous between std::string and std::string_view. Hand such elements over as
1760 // views, mirroring the dedicated char-array overload of Where().
1761 auto const asBindable = [](auto const& value) -> decltype(auto) {
1762 using Decayed = std::decay_t<decltype(value)>;
1763 if constexpr (detail::OneOf<Decayed, char*, char const*>)
1764 return std::string_view { value };
1765 else if constexpr (detail::OneOf<Decayed, char16_t*, char16_t const*>)
1766 return std::u16string_view { value };
1767 else
1768 return (value);
1769 };
1770
1771 // A set too large to be passed as parameters goes into the SQL text instead. Only a sized range
1772 // can be measured without consuming it; an unsized one keeps binding, as it did before.
1773 bool bindValues = searchCondition.inputBindings != nullptr;
1774 if constexpr (std::ranges::sized_range<LiteralType>)
1775 if (std::ranges::size(values) > SqlMaxBoundSetSize)
1776 bindValues = false;
1777
1778 auto const appendValue = [&](auto const& value) {
1779 if constexpr (isBindable)
1780 {
1781 if (bindValues)
1782 {
1783 fragment << '?';
1784 // Ahead of any HAVING values, because the WHERE clause precedes the HAVING clause.
1785 searchCondition.inputBindings->emplace(searchCondition.inputBindings->end()
1786 - static_cast<std::ptrdiff_t>(searchCondition.havingBindingCount),
1787 asBindable(value));
1788 return;
1789 }
1790 }
1791 std::string valueString;
1792 PopulateLiteralValueInto(value, valueString);
1793 fragment << valueString;
1794 };
1795
1796 fragment << '(';
1797#if !defined(__cpp_lib_ranges_enumerate)
1798 int index { -1 };
1799 for (auto const& value: values)
1800 {
1801 ++index;
1802#else
1803 for (auto const&& [index, value]: values | std::views::enumerate)
1804 {
1805#endif
1806 if (index > 0)
1807 fragment << ", "sv;
1808
1809 appendValue(value);
1810 }
1811 fragment << ')';
1812 return detail::RawSqlCondition { fragment.str() };
1813}
1814
1815template <typename Derived>
1816inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::Join(JoinType joinType,
1817 TableName auto joinTable,
1818 std::string_view joinColumnName,
1819 SqlQualifiedTableColumnName onOtherColumn)
1820{
1821 static constexpr std::array<std::string_view, 4> JoinTypeStrings = {
1822 "INNER",
1823 "LEFT OUTER",
1824 "RIGHT OUTER",
1825 "FULL OUTER",
1826 };
1827
1828 if constexpr (std::is_same_v<std::remove_cvref_t<decltype(joinTable)>, AliasedTableName>)
1829 {
1830 DerivedSearchCondition().tableJoins += std::format("\n"
1831 R"( {0} JOIN "{1}" AS "{2}"{6} ON "{2}"."{3}" = "{4}"."{5}")",
1832 JoinTypeStrings[static_cast<std::size_t>(joinType)],
1833 joinTable.tableName,
1834 joinTable.alias,
1835 joinColumnName,
1836 onOtherColumn.tableName,
1837 onOtherColumn.columnName,
1838 DerivedSearchCondition().tableHint);
1839 }
1840 else
1841 {
1842 DerivedSearchCondition().tableJoins += std::format("\n"
1843 R"( {0} JOIN "{1}"{5} ON "{1}"."{2}" = "{3}"."{4}")",
1844 JoinTypeStrings[static_cast<std::size_t>(joinType)],
1845 joinTable,
1846 joinColumnName,
1847 onOtherColumn.tableName,
1848 onOtherColumn.columnName,
1849 DerivedSearchCondition().tableHint);
1850 }
1851 return static_cast<Derived&>(*this);
1852}
1853
1854template <typename Derived>
1855inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::Join(JoinType joinType,
1856 TableName auto joinTable,
1857 std::string_view joinColumnName,
1858 std::string_view onMainTableColumn)
1859{
1860 return Join(
1861 joinType,
1862 joinTable,
1863 joinColumnName,
1864 SqlQualifiedTableColumnName { .tableName = DerivedSearchCondition().tableName, .columnName = onMainTableColumn });
1865}
1866
1867/// Constructs a JOIN clause with a custom ON clause builder.
1868template <typename Derived>
1869template <typename Callable>
1870inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder<Derived>::Join(JoinType joinType,
1871 TableName auto joinTable,
1872 Callable const& onClauseBuilder)
1873{
1874 static constexpr std::array<std::string_view, 4> JoinTypeStrings = {
1875 "INNER",
1876 "LEFT OUTER",
1877 "RIGHT OUTER",
1878 "FULL OUTER",
1879 };
1880
1881 size_t const originalSize = DerivedSearchCondition().tableJoins.size();
1882 auto const joinTypeString = JoinTypeStrings[static_cast<std::size_t>(joinType)];
1883
1884 // The ON clause qualifies the joined table's columns by the name the statement knows it under.
1885 auto const referenceTable = [&]() -> std::string_view {
1886 if constexpr (std::is_same_v<std::remove_cvref_t<decltype(joinTable)>, AliasedTableName>)
1887 {
1888 DerivedSearchCondition().tableJoins += std::format("\n"
1889 R"( {0} JOIN "{1}" AS "{2}"{3} ON )",
1890 joinTypeString,
1891 joinTable.tableName,
1892 joinTable.alias,
1893 DerivedSearchCondition().tableHint);
1894 return joinTable.alias;
1895 }
1896 else
1897 {
1898 DerivedSearchCondition().tableJoins +=
1899 std::format("\n {0} JOIN \"{1}\"{2} ON ", joinTypeString, joinTable, DerivedSearchCondition().tableHint);
1900 return joinTable;
1901 }
1902 }();
1903
1904 size_t const sizeBefore = DerivedSearchCondition().tableJoins.size();
1905 onClauseBuilder(SqlJoinConditionBuilder { referenceTable, &DerivedSearchCondition(), &DerivedFormatter() });
1906 size_t const sizeAfter = DerivedSearchCondition().tableJoins.size();
1907 if (sizeBefore == sizeAfter)
1908 DerivedSearchCondition().tableJoins.resize(originalSize);
1909
1910 return static_cast<Derived&>(*this);
1911}
1912
1913} // namespace Lightweight
Represents a binary data type.
Definition SqlBinary.hpp:24
Query builder for building JOIN conditions.
Definition Core.hpp:213
SqlJoinConditionBuilder & On(std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn)
Adds an AND join condition.
Definition Core.hpp:231
SqlJoinConditionBuilder & OrOnValue(std::string_view joinColumnName, T const &value)
Adds an OR join condition testing the joined column against value for equality.
Definition Core.hpp:275
SqlJoinConditionBuilder & OnNull(std::string_view joinColumnName)
Adds an AND join condition testing the joined column for NULL.
Definition Core.hpp:288
SqlJoinConditionBuilder & OrOnNull(std::string_view joinColumnName)
Adds an OR join condition testing the joined column for NULL.
Definition Core.hpp:300
SqlJoinConditionBuilder & OnValue(std::string_view joinColumnName, std::string_view binaryOp, T const &value)
Adds an AND join condition testing the joined column against value with binaryOp.
Definition Core.hpp:268
SqlJoinConditionBuilder & OnValue(std::string_view joinColumnName, T const &value)
Definition Core.hpp:261
SqlJoinConditionBuilder & OnNotNull(std::string_view joinColumnName)
Adds an AND join condition testing the joined column for NOT NULL.
Definition Core.hpp:294
SqlJoinConditionBuilder & OrOnGroup(Callable const &build)
Adds an OR group of join conditions, in parentheses.
Definition Core.hpp:325
SqlJoinConditionBuilder(std::string_view referenceTable, SqlSearchCondition *searchCondition, SqlQueryFormatter const *formatter) noexcept
Definition Core.hpp:221
SqlJoinConditionBuilder & Operator(std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn, std::string_view op)
Adds a join condition with a custom operator.
Definition Core.hpp:243
SqlJoinConditionBuilder & OrOnNotNull(std::string_view joinColumnName)
Adds an OR join condition testing the joined column for NOT NULL.
Definition Core.hpp:306
SqlJoinConditionBuilder & OrOnValue(std::string_view joinColumnName, std::string_view binaryOp, T const &value)
Adds an OR join condition testing the joined column against value with binaryOp.
Definition Core.hpp:282
SqlJoinConditionBuilder & OrOn(std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn)
Adds an OR join condition.
Definition Core.hpp:237
SqlJoinConditionBuilder & OnGroup(Callable const &build)
Definition Core.hpp:317
API to format SQL queries for different SQL dialects.
virtual std::string_view BooleanLiteral(bool value) const noexcept=0
Converts a boolean value to a string literal.
Derived & FullOuterJoin(TableName auto joinTable, std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn)
Constructs an FULL OUTER JOIN clause.
Definition Core.hpp:1466
Derived & WhereTrue(ColumnName const &columnName)
Constructs or extends a WHERE clause to test for a value being true.
Derived & WhereIn(ColumnName const &columnName, InputRange const &values)
Constructs or extends an WHERE/OR clause to test for a value, satisfying std::ranges::input_range.
Derived & WhereNotNull(ColumnName const &columnName)
Constructs or extends a WHERE clause to test for a value being not null.
Derived & Where(ColumnName const &columnName, std::string_view binaryOp, T const &value)
Constructs or extends a WHERE clause to test for a binary operation.
PendingConnective TakePendingConnective() noexcept
Definition Core.hpp:1576
Derived & WhereRaw(std::string_view sqlConditionExpression)
Constructs or extends a raw WHERE clause.
Definition Core.hpp:1565
Derived & LeftOuterJoin(TableName auto joinTable, std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn)
Constructs an LEFT OUTER JOIN clause.
Definition Core.hpp:1420
Derived & Not() noexcept
Indicates, that the next WHERE clause should be negated.
Definition Core.hpp:1158
Derived & WhereNull(ColumnName const &columnName)
Constructs or extends an WHERE/OR clause to test for a value to be NULL.
Derived & WhereNotEqual(ColumnName const &columnName, T const &value)
Constructs or extends a WHERE clause to test for a value being equal to another column.
Derived & RightOuterJoin(TableName auto joinTable, std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn)
Constructs an RIGHT OUTER JOIN clause.
Definition Core.hpp:1443
Derived & OrWhere(ColumnName const &columnName, std::string_view binaryOp, T const &value)
Constructs or extends a WHERE/OR clause to test for a binary operation.
auto If(std::optional< T > const &value) noexcept
Starts a conditional WHERE chain driven by a std::optional<T> value.
Derived & And() noexcept
Indicates, that the next WHERE clause should be AND-ed (default).
Definition Core.hpp:1144
Derived & Or() noexcept
Indicates, that the next WHERE clause should be OR-ed.
Definition Core.hpp:1151
Derived & WhereFalse(ColumnName const &columnName)
Constructs or extends a WHERE clause to test for a value being false.
SqlQueryFormatter const & DerivedFormatter() const noexcept
The formatter of the derived builder, which SqlWhereClauseBuilder does not own itself.
Definition Core.hpp:1594
constexpr std::string_view RecordTableName
Holds the SQL tabl ename for the given record type.
Definition Utils.hpp:291
LIGHTWEIGHT_API std::vector< std::string > ToSql(SqlQueryFormatter const &formatter, SqlMigrationPlanElement const &element)
constexpr std::size_t SqlMaxBoundSetSize
Largest IN-set that WhereIn passes as bound parameters.
Definition Core.hpp:1318
Name of table in a SQL query, where the table's name is aliased.
Definition Core.hpp:40
std::string_view alias
The alias for the table.
Definition Core.hpp:44
std::weak_ordering operator<=>(AliasedTableName const &) const =default
Three-way comparison operator.
std::string_view tableName
The table name.
Definition Core.hpp:42
A SQL expression usable as a projection or as the left side of a HAVING term.
Definition Core.hpp:811
std::string expression
The SQL text of the expression.
Definition Core.hpp:813
SqlQualifiedTableColumnName represents a column name qualified with a table name.
Definition Utils.hpp:342
std::string_view tableName
The table name.
Definition Utils.hpp:344
What And(), Or() and Not() asked of the next condition.
Definition Core.hpp:657
bool isOr
Whether Or() was called; AND is the default otherwise.
Definition Core.hpp:659
SqlWildcardType is a placeholder for an explicit wildcard input parameter in a SQL query.
Definition Core.hpp:32