Skip to content

Commit e1f9d50

Browse files
Pierre-Luc Gagnéclaude
andcommitted
feat: instance-based typed column attributes with compile-time validation
Column attributes are now NTTP instances (auto... Attrs) instead of type parameters. default_value and on_update are type-safe: the value type is validated at compile time against the column type. current_timestamp is a sentinel value in the ds_mysql namespace, composed via default_value(current_timestamp) and on_update(current_timestamp). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2306077 commit e1f9d50

8 files changed

Lines changed: 508 additions & 124 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,24 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1414
- `transaction_guard` — RAII scoped transaction helper; `transaction_guard::begin(conn)` disables autocommit, destructor auto-rolls-back unless `commit()` is called; also provides explicit `rollback()`
1515
- CTE fluent API — `with(cte("name", query)).select(...).from(cte_ref{"name"})` and `with(recursive(cte("name", sql))).select(...)` replace the old `with_cte()`/`with_recursive_cte()` builders
1616
- `sql_predicates.hpp` — predicates, operators, and `col_expr`/`col_ref` extracted from `sql_core.hpp` into their own header for readability
17+
- `column_attr::default_value(V)` — typed `DEFAULT` attribute for any column type; value type is validated at compile time against the column type (e.g. `default_value(0)` for `int32_t`, `default_value("active")` for `varchar_type<N>`, `default_value(current_timestamp)` for temporal types)
18+
- `column_attr::on_update(V)` — typed `ON UPDATE` attribute (e.g. `on_update(current_timestamp)`)
19+
- `current_timestamp` — sentinel value in `ds_mysql` namespace, used with `default_value` and `on_update` for temporal columns
1720

1821
### Changed
1922

2023
- `ColumnDescriptor` concept replaced by `ColumnFieldType` everywhere — the dual-concept system is removed
2124
- `column_traits<T>` removed — callers now use `T::column_name()` and `T::value_type` directly
2225
- `qual<Col>` now derives the table name from the tag type via compile-time reflection, replacing the old `col<T,I>` approach
2326
- `sql_core.hpp` reduced from ~1 035 to ~340 lines via the predicate extraction
27+
- Column attributes are now instance-based NTTPs (`auto... Attrs`) instead of type parameters (`typename... Attrs`) — marker attributes use `{}` syntax (e.g. `column_attr::primary_key{}`), parametric attributes use constructor syntax (e.g. `column_attr::comment("...")`, `column_attr::collate("...")`)
2428

2529
### Removed
2630

2731
- `col<Table, Index>` (`col.hpp`) — index-based column descriptor removed; use `tagged_column_field` (or `COLUMN_FIELD` macro) instead
2832
- `col_of<&T::field>` — member-pointer column alias removed alongside `col<T,I>`
2933
- `cte_builder`, `with_cte()`, `with_recursive_cte()` — replaced by the new `with(cte(...))` fluent API
34+
- `column_attr::default_current_timestamp` and `column_attr::on_update_current_timestamp` — replaced by `default_value(current_timestamp)` and `on_update(current_timestamp)`
3035

3136
---
3237

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Style: Google-based, 120-char line limit, 4-space indent (`.clang-format` at roo
5353
- `sql_dql.hpp``select(col1{}, col2{})`, `count(T{})`, `describe(T{})`
5454
- `connect_options.hpp``connect_options` fluent builder and `ssl_mode` enum for pre-connect `mysql_options()` configuration (timeouts, SSL/TLS, charset, compression, etc.)
5555
- `charset_name.hpp``charset_name` strong type for character set names
56-
- `column_field.hpp` / `column_field_base_*.hpp``column_field<"name", Type, Attrs...>` descriptors, `COLUMN_FIELD(name, type, attrs...)` macro, and column attributes (`column_attr::primary_key`, `auto_increment`, `unique`, `default_current_timestamp`, `on_update_current_timestamp`, `comment<"...">`, `collate<"...">`)
56+
- `column_field.hpp` / `column_field_base_*.hpp``column_field<"name", Type, Attrs...>` descriptors, `COLUMN_FIELD(name, type, attrs...)` macro, and column attributes passed as NTTP instances: `column_attr::primary_key{}`, `auto_increment{}`, `unique{}`, `default_value(0)`, `default_value("text")`, `default_value(current_timestamp)`, `on_update(current_timestamp)`, `comment("...")`, `collate("...")`
5757
- `schema_generator.hpp` — derives CREATE TABLE SQL from a C++ struct at compile time using Boost.PFR
5858
- `sql_varchar.hpp`, `sql_numeric.hpp`, `sql_temporal.hpp` — library-specific SQL types (`varchar_type<N>`, `decimal_type<P,S>`, `datetime_type<FSP>`, etc.)
5959
- `metadata.hpp` — types for querying `information_schema`

lib/include/ds_mysql/column_field.hpp

Lines changed: 100 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,69 @@
1515

1616
namespace ds_mysql {
1717

18+
// ===================================================================
19+
// default_value type compatibility
20+
// ===================================================================
21+
22+
namespace column_field_detail {
23+
24+
// Strip std::optional wrapper to get the base column type.
25+
template <typename T>
26+
struct unwrap_optional_for_default {
27+
using type = T;
28+
};
29+
template <typename T>
30+
struct unwrap_optional_for_default<std::optional<T>> {
31+
using type = T;
32+
};
33+
template <typename T>
34+
using unwrap_optional_for_default_t = typename unwrap_optional_for_default<T>::type;
35+
36+
// Check whether ValueType is a valid default / on_update value for ColumnType.
37+
template <typename ColumnType, typename ValueType>
38+
consteval bool is_compatible_column_value() {
39+
using CT = unwrap_optional_for_default_t<ColumnType>;
40+
41+
if constexpr (std::is_same_v<ValueType, current_timestamp_t>) {
42+
return std::is_same_v<CT, std::chrono::system_clock::time_point> || is_datetime_type_v<CT> ||
43+
is_timestamp_type_v<CT>;
44+
} else if constexpr (std::is_integral_v<CT>) {
45+
return std::is_integral_v<ValueType>;
46+
} else if constexpr (std::is_floating_point_v<CT>) {
47+
return std::is_arithmetic_v<ValueType>;
48+
} else if constexpr (requires { typename CT::underlying_type; }) {
49+
// Formatted numeric types (int_type, decimal_type, etc.)
50+
return is_compatible_column_value<typename CT::underlying_type, ValueType>();
51+
} else if constexpr (is_varchar_type_v<CT> || is_text_type_v<CT> || std::is_same_v<CT, std::string>) {
52+
return is_fixed_string_v<ValueType>;
53+
} else {
54+
return false;
55+
}
56+
}
57+
58+
// Validate that all typed attrs in the pack are type-compatible with ColumnType.
59+
template <typename ColumnType, auto... Attrs>
60+
struct attr_type_compat {
61+
static constexpr bool value = true;
62+
};
63+
64+
template <typename ColumnType, auto First, auto... Rest>
65+
struct attr_type_compat<ColumnType, First, Rest...> {
66+
static constexpr bool value = [] {
67+
if constexpr (is_default_value_attr_v<decltype(First)>) {
68+
return is_compatible_column_value<ColumnType, decltype(First.val)>() &&
69+
attr_type_compat<ColumnType, Rest...>::value;
70+
} else if constexpr (is_on_update_attr_v<decltype(First)>) {
71+
return is_compatible_column_value<ColumnType, decltype(First.val)>() &&
72+
attr_type_compat<ColumnType, Rest...>::value;
73+
} else {
74+
return attr_type_compat<ColumnType, Rest...>::value;
75+
}
76+
}();
77+
};
78+
79+
} // namespace column_field_detail
80+
1881
// ===================================================================
1982
// column_field<Name, T> — the only user-facing form
2083
// ===================================================================
@@ -23,9 +86,8 @@ namespace ds_mysql {
2386
* column_field<Name, T, Attrs...> — named column descriptor.
2487
*
2588
* Name is the SQL column name embedded directly as a string literal.
26-
* T is the stored value type. Attrs are optional typed DDL modifiers
27-
* (column_attr::*). All constructors and operators are inherited from the
28-
* internal base type (defined in the adapter headers).
89+
* T is the stored value type. Attrs are optional instance-based DDL
90+
* modifiers (column_attr::*, fk_attr::*) passed as NTTP values.
2991
*
3092
* using id = column_field<"id", uint32_t>;
3193
* using ticker = column_field<"ticker", varchar_type<32>>;
@@ -37,31 +99,44 @@ namespace ds_mysql {
3799
*
38100
* SQL column names: "id", "ticker", "sector"
39101
*/
40-
template <fixed_string Name, typename T, typename... Attrs>
102+
template <fixed_string Name, typename T, auto... Attrs>
41103
struct column_field : column_field_detail::base<T> {
42104
using column_field_detail::base<T>::base;
43105
using column_field_detail::base<T>::operator=;
44106

45-
static constexpr bool ddl_primary_key = (std::same_as<Attrs, column_attr::primary_key> || ...);
46-
static constexpr bool ddl_auto_increment = (std::same_as<Attrs, column_attr::auto_increment> || ...);
47-
static constexpr bool ddl_unique = (std::same_as<Attrs, column_attr::unique> || ...);
48-
static constexpr bool ddl_default_current_timestamp =
49-
(std::same_as<Attrs, column_attr::default_current_timestamp> || ...);
50-
static constexpr bool ddl_on_update_current_timestamp =
51-
(std::same_as<Attrs, column_attr::on_update_current_timestamp> || ...);
52-
static constexpr std::string_view ddl_comment = column_field_detail::comment_attr_value<Attrs...>::value;
53-
static constexpr std::string_view ddl_collate = column_field_detail::collate_attr_value<Attrs...>::value;
107+
static constexpr bool ddl_primary_key = (std::is_same_v<decltype(Attrs), column_attr::primary_key> || ...);
108+
static constexpr bool ddl_auto_increment = (std::is_same_v<decltype(Attrs), column_attr::auto_increment> || ...);
109+
static constexpr bool ddl_unique = (std::is_same_v<decltype(Attrs), column_attr::unique> || ...);
110+
111+
static constexpr bool ddl_has_default = (column_field_detail::is_default_value_attr_v<decltype(Attrs)> || ...);
112+
static constexpr bool ddl_has_on_update = (column_field_detail::is_on_update_attr_v<decltype(Attrs)> || ...);
113+
114+
static_assert(column_field_detail::attr_type_compat<T, Attrs...>::value,
115+
"default_value / on_update type must match column value type");
116+
117+
[[nodiscard]] static std::string ddl_default_sql() {
118+
return column_field_detail::default_value_sql<Attrs...>::get();
119+
}
120+
121+
[[nodiscard]] static std::string ddl_on_update_sql() {
122+
return column_field_detail::on_update_sql<Attrs...>::get();
123+
}
124+
125+
static constexpr std::string_view ddl_comment =
126+
column_field_detail::string_attr_value<column_attr::comment, Attrs...>::value;
127+
static constexpr std::string_view ddl_collate =
128+
column_field_detail::string_attr_value<column_attr::collate, Attrs...>::value;
54129

55130
// Foreign key attributes — populated only when an fk_attr::references<> is present.
56-
static constexpr bool ddl_has_fk = column_field_detail::fk_references_attr<Attrs...>::has_value;
57-
using ddl_fk_ref_table = typename column_field_detail::fk_references_attr<Attrs...>::ref_table;
58-
using ddl_fk_ref_column = typename column_field_detail::fk_references_attr<Attrs...>::ref_column;
59-
static constexpr std::string_view ddl_fk_on_delete = column_field_detail::fk_on_delete_attr<Attrs...>::value;
60-
static constexpr std::string_view ddl_fk_on_update = column_field_detail::fk_on_update_attr<Attrs...>::value;
131+
static constexpr bool ddl_has_fk = column_field_detail::find_fk_ref<Attrs...>::has_value;
132+
using ddl_fk_ref_table = typename column_field_detail::find_fk_ref<Attrs...>::ref_table;
133+
using ddl_fk_ref_column = typename column_field_detail::find_fk_ref<Attrs...>::ref_column;
134+
static constexpr std::string_view ddl_fk_on_delete = column_field_detail::fk_on_delete_value<Attrs...>::value;
135+
static constexpr std::string_view ddl_fk_on_update = column_field_detail::fk_on_update_value<Attrs...>::value;
61136

62137
template <typename Attr>
63138
[[nodiscard]] static consteval bool has_attribute() noexcept {
64-
return (std::same_as<Attr, Attrs> || ...);
139+
return (std::is_same_v<decltype(Attrs), Attr> || ...);
65140
}
66141

67142
[[nodiscard]] static constexpr std::string_view column_name() noexcept {
@@ -79,7 +154,7 @@ struct column_field : column_field_detail::base<T> {
79154
*
80155
* Satisfied by named column descriptors (with or without typed attributes):
81156
* using id = column_field<"id", uint32_t>;
82-
* using id = column_field<"id", uint32_t, column_attr::auto_increment>;
157+
* using id = column_field<"id", uint32_t, column_attr::auto_increment{}>;
83158
*/
84159
template <typename T>
85160
concept ColumnFieldType = std::derived_from<T, column_field_tag> && requires { typename T::value_type; };
@@ -113,7 +188,7 @@ using unwrap_column_field_t = typename unwrap_column_field<T>::type;
113188
// ===================================================================
114189

115190
/**
116-
* tagged_column_field<Tag, T> — tag-struct based column descriptor.
191+
* tagged_column_field<Tag, T, Attrs...> — tag-struct based column descriptor.
117192
*
118193
* The SQL column name is derived at compile time from the tag type name by
119194
* stripping a trailing "_tag" suffix. Unlike a plain type alias, this is a
@@ -144,7 +219,7 @@ using unwrap_column_field_t = typename unwrap_column_field<T>::type;
144219
*
145220
* Both produce the SQL column name "id" (derived from the tag name).
146221
*/
147-
template <typename Tag, typename T, typename... Attrs>
222+
template <typename Tag, typename T, auto... Attrs>
148223
struct tagged_column_field : column_field<detail::column_name_from_tag<Tag>(), T, Attrs...> {
149224
using tag_type = Tag;
150225

@@ -160,7 +235,7 @@ struct tagged_column_field : column_field<detail::column_name_from_tag<Tag>(), T
160235

161236
/**
162237
* COLUMN_FIELD(tag, type[, attrs...]) — one-liner macro to declare a tagged
163-
* column field, optionally with typed DDL attribute tags.
238+
* column field, optionally with instance-based DDL attribute values.
164239
*
165240
* Generates a nested tag struct, a type alias, and a member variable:
166241
*
@@ -172,8 +247,8 @@ struct tagged_column_field : column_field<detail::column_name_from_tag<Tag>(), T
172247
*
173248
* COLUMN_FIELD(created_at,
174249
* ::ds_mysql::timestamp_type,
175-
* ::ds_mysql::column_attr::default_current_timestamp,
176-
* ::ds_mysql::column_attr::on_update_current_timestamp)
250+
* ::ds_mysql::column_attr::default_value(::ds_mysql::current_timestamp),
251+
* ::ds_mysql::column_attr::on_update(::ds_mysql::current_timestamp))
177252
*
178253
* The tag struct is nested inside the enclosing class, guaranteeing
179254
* per-table type uniqueness and satisfying the compile-time membership

0 commit comments

Comments
 (0)