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