Skip to content

Commit 1f99266

Browse files
Pierre-Luc Gagnéclaude
andcommitted
refactor!: unify SqlBuilder concept and type-safe DDL builder chaining
Replace three identical concepts (BuildsSql, AnySelectQuery, SqlStatement) with a single SqlBuilder concept in sql_core.hpp. DDL builders now use a typed Prior template parameter instead of std::string prior_sql_, making raw-string construction impossible at the type level. - Add no_prior sentinel and sql_string_builder bridge types for DDL chains - Template ddl_continuation<Prior> and all DDL builders on SqlBuilder Prior - Template union_query on Left/Right builders instead of storing pre-built SQL - Template case_when_expr on Builder instead of storing pre-built SQL - lateral_join methods now take SqlBuilder subquery + sql_alias (was string) - Rename column_alias to sql_alias (it's used for table aliases too) - TypedSelectQuery now refines SqlBuilder - explain/explain_analyze use SqlBuilder constraint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 82898b0 commit 1f99266

9 files changed

Lines changed: 438 additions & 407 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5151
- `grant<privilege::select, ...>(grant_target, grant_user)` — compile-time privilege set overload of `grant`
5252
- `revoke(privilege_list, grant_target, grant_user)``REVOKE ... ON ... FROM ...`
5353
- `revoke<privilege::select, ...>(grant_target, grant_user)` — compile-time privilege set overload of `revoke`
54-
- `column_alias` — strongly-typed SQL alias identifier (wraps `std::string_view`); required as the second argument to `.with_alias()` in place of a plain `std::string`
54+
- `SqlBuilder` concept — unified concept for any type that can produce SQL via `build_sql() const → std::string`; replaces the previous `BuildsSql`, `AnySelectQuery`, and `SqlStatement` concepts which were structurally identical
55+
- `sql_alias` — strongly-typed SQL alias identifier (wraps `std::string_view`); used as the alias argument to `.with_alias()`, `.lateral_join()`, etc.
56+
- `no_prior` / `sql_string_builder` — DDL-internal sentinel and bridge types for the typed prior chain
5557

5658
### Changed
5759

@@ -70,7 +72,7 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7072
| `.order_by<Col, sort_order::desc>()` | `.order_by(desc(Col{}))` |
7173
| `.order_by_alias<Proj>()` | `.order_by_alias(Proj{})` |
7274
| `.order_by_alias<Proj, sort_order::desc>()` | `.order_by_alias(desc(Proj{}))` |
73-
| `.with_alias<Proj>("name")` | `.with_alias(Proj{}, column_alias{"name"})` |
75+
| `.with_alias<Proj>("name")` | `.with_alias(Proj{}, sql_alias{"name"})` |
7476
| `.inner_join<T, L, R>()` | `.inner_join(T{}, L{}, R{})` |
7577
| `.inner_join_on<T>(pred)` | `.inner_join(T{}, pred)` |
7678
| `.left_join<T, L, R>()` | `.left_join(T{}, L{}, R{})` |
@@ -106,7 +108,15 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
106108
- Simple column-ref predicate factories (`equal`, `greater_than`, `like`, `between`, `in`, etc.) now return `check_expr` instead of `sql_predicate`; `check_expr` implicitly converts to `sql_predicate` so existing WHERE/HAVING/JOIN ON usage is unaffected
107109
- Subquery predicate factories (`in_subquery`, `not_in_subquery`, `exists`, `not_exists`) and `match_against` return `sql_predicate` directly (not check-safe)
108110

109-
- **Breaking:** `.with_alias()` now requires a `column_alias` value as its second argument instead of a plain `std::string`; replace `.with_alias(Proj{}, "name")` with `.with_alias(Proj{}, column_alias{"name"})`
111+
- **Breaking:** `column_alias` renamed to `sql_alias`; replace `sql_alias{"name"}` with `sql_alias{"name"}`
112+
- **Breaking:** `.with_alias()` now requires a `sql_alias` value as its second argument instead of a plain `std::string`; replace `.with_alias(Proj{}, "name")` with `.with_alias(Proj{}, sql_alias{"name"})`
113+
- **Breaking:** DDL builders now use a typed `Prior` template parameter instead of `std::string prior_sql_`; builder constructors no longer accept raw strings — chaining is enforced at the type level via `ddl_continuation<Prior>`
114+
- **Breaking:** `lateral_join` / `left_lateral_join` / `lateral_join_on` / `left_lateral_join_on` now take a `SqlBuilder`-constrained subquery and `sql_alias` instead of `std::string` and `std::string_view`; replace `.lateral_join(q.build_sql(), "alias")` with `.lateral_join(q, sql_alias{"alias"})`
115+
- **Breaking:** `union_query` now stores the two query builders instead of a pre-built SQL string; `union_()`, `union_all()`, `intersect_()`, `except_()` return deduced types
116+
- **Breaking:** `case_when_expr` now stores the builder instead of a pre-built SQL string
117+
- **Breaking:** `AnySelectQuery` and `SqlStatement` concepts removed; use `SqlBuilder` instead
118+
- `TypedSelectQuery` now refines `SqlBuilder`: `SqlBuilder<T> && requires { typename T::result_row_type; }`
119+
- `explain()` and `explain_analyze()` now use `SqlBuilder` concept constraint instead of inline requires clause
110120
- **Breaking:** `grant` and `revoke` now require strongly-typed `privilege_list`, `grant_target`, and `grant_user` arguments instead of plain strings; the old string-based overloads have been removed
111121

112122
- `natural_join<T>()`, `natural_left_join<T>()`, `natural_right_join<T>()``NATURAL [LEFT|RIGHT] JOIN` with no ON/USING clause

lib/include/ds_mysql/schema_generator.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,8 @@ template <typename T, std::size_t... Is>
485485
sql += table_name;
486486
sql += " (\n";
487487
for (std::size_t i = 0; i < sizeof...(Is); ++i) {
488-
if (i > 0) sql += ",\n";
488+
if (i > 0)
489+
sql += ",\n";
489490
sql += col_defs[i];
490491
}
491492
sql += "\n)";

lib/include/ds_mysql/sql_core.hpp

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ namespace sql_detail {
161161
return result;
162162
}
163163

164-
[[nodiscard]] inline std::string format_datetime(std::chrono::system_clock::time_point tp, uint32_t fractional_second_precision = 0) {
164+
[[nodiscard]] inline std::string format_datetime(std::chrono::system_clock::time_point tp,
165+
uint32_t fractional_second_precision = 0) {
165166
auto const precision = normalize_fractional_second_precision(fractional_second_precision);
166167
auto const micros = std::chrono::floor<std::chrono::microseconds>(tp);
167168
auto const secs = std::chrono::floor<std::chrono::seconds>(micros);
@@ -550,12 +551,11 @@ template <ColumnFieldType Col>
550551
return or_(std::move(a), std::move(b));
551552
}
552553

553-
// Concept for any query that can produce SQL — used for subquery predicates.
554-
// Forward-declared here so subquery predicates can reference it before the full
555-
// SELECT builder is defined.
556-
template <typename Q>
557-
concept AnySelectQuery = requires(Q const& q) {
558-
{ q.build_sql() } -> std::convertible_to<std::string>;
554+
// SqlBuilder — any type that can produce a SQL string via build_sql().
555+
// Defined early so subquery predicates can reference it before the full SELECT builder is defined.
556+
template <typename T>
557+
concept SqlBuilder = requires(T const& t) {
558+
{ t.build_sql() } -> std::convertible_to<std::string>;
559559
};
560560

561561
// check-safe: literal value list
@@ -715,7 +715,7 @@ template <ColumnFieldType... Cols>
715715
return {{}, {}, std::move(s)};
716716
}
717717

718-
template <ColumnFieldType Col, AnySelectQuery Query>
718+
template <ColumnFieldType Col, SqlBuilder Query>
719719
[[nodiscard]] sql_predicate in_subquery(Query const& subquery) {
720720
auto sub = subquery.build_sql();
721721
std::string rhs;
@@ -726,7 +726,7 @@ template <ColumnFieldType Col, AnySelectQuery Query>
726726
return {column_traits<Col>::column_name(), " IN ", std::move(rhs)};
727727
}
728728

729-
template <ColumnFieldType Col, AnySelectQuery Query>
729+
template <ColumnFieldType Col, SqlBuilder Query>
730730
[[nodiscard]] sql_predicate not_in_subquery(Query const& subquery) {
731731
auto sub = subquery.build_sql();
732732
std::string rhs;
@@ -737,7 +737,7 @@ template <ColumnFieldType Col, AnySelectQuery Query>
737737
return {column_traits<Col>::column_name(), " NOT IN ", std::move(rhs)};
738738
}
739739

740-
template <AnySelectQuery Query>
740+
template <SqlBuilder Query>
741741
[[nodiscard]] sql_predicate exists(Query const& subquery) {
742742
auto sub = subquery.build_sql();
743743
std::string s;
@@ -748,7 +748,7 @@ template <AnySelectQuery Query>
748748
return {{}, {}, std::move(s)};
749749
}
750750

751-
template <AnySelectQuery Query>
751+
template <SqlBuilder Query>
752752
[[nodiscard]] sql_predicate not_exists(Query const& subquery) {
753753
auto sub = subquery.build_sql();
754754
std::string s;
@@ -871,17 +871,17 @@ struct col_expr {
871871
return ds_mysql::null_safe_equal<Col>(Col{val});
872872
}
873873

874-
template <AnySelectQuery Query>
874+
template <SqlBuilder Query>
875875
[[nodiscard]] sql_predicate in_subquery(Query const& subquery) const {
876876
return ds_mysql::in_subquery<Col>(subquery);
877877
}
878878

879-
template <AnySelectQuery Query>
879+
template <SqlBuilder Query>
880880
[[nodiscard]] sql_predicate not_in_subquery(Query const& subquery) const {
881881
return ds_mysql::not_in_subquery<Col>(subquery);
882882
}
883883

884-
template <AnySelectQuery Query>
884+
template <SqlBuilder Query>
885885
[[nodiscard]] sql_predicate eq_subquery(Query const& subquery) const {
886886
auto sub = subquery.build_sql();
887887
std::string rhs;
@@ -892,7 +892,7 @@ struct col_expr {
892892
return {column_traits<Col>::column_name(), " = ", std::move(rhs)};
893893
}
894894

895-
template <AnySelectQuery Query>
895+
template <SqlBuilder Query>
896896
[[nodiscard]] sql_predicate ne_subquery(Query const& subquery) const {
897897
auto sub = subquery.build_sql();
898898
std::string rhs;
@@ -903,7 +903,7 @@ struct col_expr {
903903
return {column_traits<Col>::column_name(), " != ", std::move(rhs)};
904904
}
905905

906-
template <AnySelectQuery Query>
906+
template <SqlBuilder Query>
907907
[[nodiscard]] sql_predicate lt_subquery(Query const& subquery) const {
908908
auto sub = subquery.build_sql();
909909
std::string rhs;
@@ -914,7 +914,7 @@ struct col_expr {
914914
return {column_traits<Col>::column_name(), " < ", std::move(rhs)};
915915
}
916916

917-
template <AnySelectQuery Query>
917+
template <SqlBuilder Query>
918918
[[nodiscard]] sql_predicate gt_subquery(Query const& subquery) const {
919919
auto sub = subquery.build_sql();
920920
std::string rhs;
@@ -925,7 +925,7 @@ struct col_expr {
925925
return {column_traits<Col>::column_name(), " > ", std::move(rhs)};
926926
}
927927

928-
template <AnySelectQuery Query>
928+
template <SqlBuilder Query>
929929
[[nodiscard]] sql_predicate le_subquery(Query const& subquery) const {
930930
auto sub = subquery.build_sql();
931931
std::string rhs;
@@ -936,7 +936,7 @@ struct col_expr {
936936
return {column_traits<Col>::column_name(), " <= ", std::move(rhs)};
937937
}
938938

939-
template <AnySelectQuery Query>
939+
template <SqlBuilder Query>
940940
[[nodiscard]] sql_predicate ge_subquery(Query const& subquery) const {
941941
auto sub = subquery.build_sql();
942942
std::string rhs;
@@ -952,14 +952,4 @@ struct col_expr {
952952
template <ColumnFieldType Col>
953953
inline constexpr col_expr<Col> col_ref{};
954954

955-
// ===================================================================
956-
// SqlStatement — unified concept for all SQL builder types.
957-
// Satisfied by any builder stage that exposes build_sql() -> std::string.
958-
// ===================================================================
959-
960-
template <typename T>
961-
concept SqlStatement = requires(T const& t) {
962-
{ t.build_sql() } -> std::convertible_to<std::string>;
963-
};
964-
965955
} // namespace ds_mysql

0 commit comments

Comments
 (0)