Skip to content

Commit eb9fb3f

Browse files
Pierre-Luc Gagnéclaude
andcommitted
feat: add sql_default() sentinel, positional and column-specific insert APIs
Introduce sql_default_t/sql_default() for emitting DEFAULT in INSERT statements. Columns with auto_increment or default_value attributes can be constructed from or assigned sql_default(). Add two new insert paths: positional field-based (.values(sql_default(), col{"val"}, ...)) and column-specific (.columns(col1{}, col2{}).values(v1, v2)). Both support on_duplicate_key_update. Generalize upsert builder to work with all insert paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e1f9d50 commit eb9fb3f

9 files changed

Lines changed: 529 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1010

1111
### Added
1212

13+
- `sql_default()` sentinel and `sql_default_t` type — represents the SQL `DEFAULT` keyword in INSERT statements
14+
- Positional field-based insert — `insert_into(t{}).values(sql_default(), col2{"val"}, col3{"val"})` allows specifying all struct fields in order with `sql_default()` as a placeholder for columns that should use their database default
15+
- Column-specific insert — `insert_into(t{}).columns(col1{}, col2{}).values(val1, val2)` allows inserting a subset of columns; column list is instance-based (no template arguments needed)
16+
- Column field `sql_default()` assignability — columns with `auto_increment` or `default_value` attributes can be constructed from or assigned `sql_default()`: `row.id_ = sql_default()`, then the struct-based `insert_into(t{}).values(row)` emits `DEFAULT` for those columns
1317
- `prepared_statement` — RAII wrapper for MySQL prepared statements (`MYSQL_STMT*`); created via `mysql_connection::prepare(sql)` or `prepare(builder)`; supports type-safe parameter binding via `execute(params...)` and typed result fetching via `query<RowType>(params...)`
1418
- `transaction_guard` — RAII scoped transaction helper; `transaction_guard::begin(conn)` disables autocommit, destructor auto-rolls-back unless `commit()` is called; also provides explicit `rollback()`
1519
- 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

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ Style: Google-based, 120-char line limit, 4-space indent (`.clang-format` at roo
4949
- `ds_mysql.hpp` — umbrella include; users include only this
5050
- `mysql_connection.hpp``mysql_config`, `connect_options`, and `mysql_connection` classes; `.query()` returns `std::expected<ResultType, std::string>`, `.execute()` returns `std::expected<uint64_t, std::string>` (affected row count; errors include the failing SQL statement); also provides `last_insert_id()`, `ping()`, `commit()`, `rollback()`, `autocommit()`, `select_db()`, `reset_connection()`, `escape_string()`, `warning_count()`, `info()`, `server_version()`, `server_info()`, `stat()`, `thread_id()`, `character_set()`, `set_character_set()`
5151
- `sql_ddl.hpp``create_table(T{})`, `drop_table(T{})`, `create_database(DB{})`, `create_all_tables(DB{})`
52-
- `sql_dml.hpp``insert_into(T{})`, `update(T{})`, `delete_from(T{})`, `truncate_table(T{})`
52+
- `sql_dml.hpp``insert_into(T{})` (struct-based, positional field-based with `sql_default()`, column-specific via `.columns().values()`), `update(T{})`, `delete_from(T{})`, `truncate_table(T{})`
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 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("...")`
56+
- `column_field.hpp` / `column_field_base_*.hpp``column_field<"name", Type, Attrs...>` descriptors, `COLUMN_FIELD(name, type, attrs...)` macro, 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("...")`; `sql_default_t` sentinel and `sql_default()` factory — columns with `auto_increment` or `default_value` support construction/assignment from `sql_default()`
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: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ struct attr_type_compat<ColumnType, First, Rest...> {
102102
template <fixed_string Name, typename T, auto... Attrs>
103103
struct column_field : column_field_detail::base<T> {
104104
using column_field_detail::base<T>::base;
105-
using column_field_detail::base<T>::operator=;
106105

107106
static constexpr bool ddl_primary_key = (std::is_same_v<decltype(Attrs), column_attr::primary_key> || ...);
108107
static constexpr bool ddl_auto_increment = (std::is_same_v<decltype(Attrs), column_attr::auto_increment> || ...);
@@ -111,9 +110,47 @@ struct column_field : column_field_detail::base<T> {
111110
static constexpr bool ddl_has_default = (column_field_detail::is_default_value_attr_v<decltype(Attrs)> || ...);
112111
static constexpr bool ddl_has_on_update = (column_field_detail::is_on_update_attr_v<decltype(Attrs)> || ...);
113112

113+
static constexpr bool supports_sql_default = ddl_auto_increment || ddl_has_default;
114+
114115
static_assert(column_field_detail::attr_type_compat<T, Attrs...>::value,
115116
"default_value / on_update type must match column value type");
116117

118+
// --- sql_default support ---------------------------------------------------
119+
bool is_sql_default_ = false;
120+
121+
constexpr column_field(sql_default_t) noexcept
122+
requires supports_sql_default
123+
: is_sql_default_{true} {
124+
}
125+
126+
constexpr column_field& operator=(sql_default_t) noexcept
127+
requires supports_sql_default
128+
{
129+
is_sql_default_ = true;
130+
return *this;
131+
}
132+
133+
// Generic forwarding operator= that resets the sql_default flag.
134+
template <typename U>
135+
constexpr auto operator=(U&& v) -> column_field&
136+
requires(!std::same_as<std::decay_t<U>, sql_default_t> && !std::same_as<std::decay_t<U>, column_field>) &&
137+
requires(column_field_detail::base<T>& b) { b = std::forward<U>(std::declval<U>()); }
138+
{
139+
static_cast<column_field_detail::base<T>&>(*this) = std::forward<U>(v);
140+
is_sql_default_ = false;
141+
return *this;
142+
}
143+
144+
constexpr column_field() = default;
145+
constexpr column_field(column_field const&) = default;
146+
constexpr column_field(column_field&&) = default;
147+
constexpr column_field& operator=(column_field const&) = default;
148+
constexpr column_field& operator=(column_field&&) = default;
149+
150+
[[nodiscard]] constexpr bool is_sql_default() const noexcept {
151+
return is_sql_default_;
152+
}
153+
117154
[[nodiscard]] static std::string ddl_default_sql() {
118155
return column_field_detail::default_value_sql<Attrs...>::get();
119156
}

lib/include/ds_mysql/column_field_base_core.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ inline constexpr bool is_fixed_string_v = is_fixed_string<T>::value;
3030
struct current_timestamp_t {};
3131
inline constexpr current_timestamp_t current_timestamp{};
3232

33+
/// Sentinel type for SQL DEFAULT keyword in INSERT statements.
34+
/// Use with the field-based insert API or assign to auto_increment / default_value columns:
35+
/// insert_into(table{}).values(sql_default(), col2{"val"}, ...).build_sql()
36+
/// table::id field{sql_default()};
37+
struct sql_default_t {};
38+
[[nodiscard]] inline constexpr sql_default_t sql_default() noexcept {
39+
return {};
40+
}
41+
3342
namespace column_attr {
3443

3544
struct primary_key {};

lib/include/ds_mysql/sql_core.hpp

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,14 @@ namespace sql_detail {
206206
*/
207207
template <typename T>
208208
[[nodiscard]] std::string to_sql_value(T const& v) {
209-
if constexpr (ColumnFieldType<T>) {
209+
if constexpr (std::same_as<T, sql_default_t>) {
210+
return "DEFAULT";
211+
} else if constexpr (ColumnFieldType<T>) {
212+
if constexpr (requires(T const& x) { x.is_sql_default_; }) {
213+
if (v.is_sql_default_) {
214+
return "DEFAULT";
215+
}
216+
}
210217
return to_sql_value(v.value);
211218
} else if constexpr (is_optional_v<T>) {
212219
if (!v.has_value()) {
@@ -267,10 +274,10 @@ template <typename T>
267274
// ===================================================================
268275
template <typename T>
269276
concept SqlValue =
270-
ColumnFieldType<T> || is_optional_v<T> || is_datetime_type_v<T> || is_timestamp_type_v<T> || is_date_type_v<T> ||
271-
std::same_as<T, std::chrono::system_clock::time_point> || is_time_type_v<T> || std::same_as<T, bool> ||
272-
std::integral<T> || std::floating_point<T> || is_formatted_numeric_type_v<T> || is_varchar_type_v<T> ||
273-
is_text_type_v<T> || std::same_as<T, std::string>;
277+
std::same_as<T, sql_default_t> || ColumnFieldType<T> || is_optional_v<T> || is_datetime_type_v<T> ||
278+
is_timestamp_type_v<T> || is_date_type_v<T> || std::same_as<T, std::chrono::system_clock::time_point> ||
279+
is_time_type_v<T> || std::same_as<T, bool> || std::integral<T> || std::floating_point<T> ||
280+
is_formatted_numeric_type_v<T> || is_varchar_type_v<T> || is_text_type_v<T> || std::same_as<T, std::string>;
274281

275282
// ===================================================================
276283
// check_id<"name"> — compile-time CHECK constraint name type.

lib/include/ds_mysql/sql_dml.hpp

Lines changed: 130 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ namespace ds_mysql {
1212
// describe<T>()
1313
// → build_sql() — DESCRIBE T
1414
//
15-
// insert_into<T>()
15+
// insert_into(T{})
1616
// .values(row)
1717
// → build_sql() — INSERT INTO T (...) VALUES (...)
1818
// → .on_duplicate_key_update(col1, ...) — ... ON DUPLICATE KEY UPDATE ...
1919
// .values(rows) where rows is std::ranges::input_range<T>
2020
// → build_sql() — INSERT INTO T (...) VALUES (...), (...), ...
21+
// .values(sql_default(), col2{"v"}, ...) — positional, all fields
22+
// → build_sql() — INSERT INTO T (...) VALUES (DEFAULT, ...)
23+
// .columns(col1{}, col2{}).values(v1, v2) — column-specific, subset
24+
// → build_sql() — INSERT INTO T (col1, col2) VALUES (...)
2125
//
2226
// update<T>()
2327
// .set(col1, col2, ...) — all columns in one call
@@ -71,15 +75,15 @@ class insert_into_builder;
7175
template <typename... Cols>
7276
[[nodiscard]] std::string build_assignment_sql_from_tuple(std::tuple<Cols...> const& assignments);
7377

74-
template <typename T, typename... Cols>
78+
template <typename T, typename InnerBuilder, typename... Cols>
7579
class insert_into_upsert_builder {
7680
public:
77-
insert_into_upsert_builder(insert_into_values_builder<T> values_builder, std::tuple<Cols...> assignments)
78-
: values_builder_(std::move(values_builder)), assignments_(std::move(assignments)) {
81+
insert_into_upsert_builder(InnerBuilder inner, std::tuple<Cols...> assignments)
82+
: inner_(std::move(inner)), assignments_(std::move(assignments)) {
7983
}
8084

8185
[[nodiscard]] std::string build_sql() const {
82-
auto insert_sql = values_builder_.build_sql();
86+
auto insert_sql = inner_.build_sql();
8387
auto update_clause = build_assignment_sql_from_tuple(assignments_);
8488
std::string sql;
8589
sql.reserve(insert_sql.size() + 26 + update_clause.size());
@@ -90,7 +94,7 @@ class insert_into_upsert_builder {
9094
}
9195

9296
private:
93-
insert_into_values_builder<T> values_builder_;
97+
InnerBuilder inner_;
9498
std::tuple<Cols...> assignments_;
9599
};
96100

@@ -108,8 +112,9 @@ class insert_into_values_builder {
108112
// Template form: .on_duplicate_key_update<T::col>("val") (mirrors update().set<>())
109113
template <FieldOf<T>... Cols>
110114
requires(sizeof...(Cols) > 0)
111-
[[nodiscard]] insert_into_upsert_builder<T, Cols...> on_duplicate_key_update(Cols const&... assignments) const {
112-
return insert_into_upsert_builder<T, Cols...>{*this, std::tuple<Cols...>{assignments...}};
115+
[[nodiscard]] insert_into_upsert_builder<T, insert_into_values_builder<T>, Cols...> on_duplicate_key_update(
116+
Cols const&... assignments) const {
117+
return {*this, std::tuple<Cols...>{assignments...}};
113118
}
114119

115120
[[nodiscard]] std::string build_sql() const {
@@ -155,13 +160,105 @@ class insert_into_values_builder {
155160
bool bulk_ = false;
156161
};
157162

163+
// ---------------------------------------------------------------
164+
// insert_into_fields_builder — positional field-based insert
165+
// (all struct fields, in order, each either column_field or sql_default)
166+
// ---------------------------------------------------------------
167+
template <typename T, typename... Args>
168+
class insert_into_fields_builder {
169+
public:
170+
explicit insert_into_fields_builder(std::tuple<Args...> args) : args_(std::move(args)) {
171+
}
172+
173+
template <FieldOf<T>... Cols>
174+
requires(sizeof...(Cols) > 0)
175+
[[nodiscard]] insert_into_upsert_builder<T, insert_into_fields_builder<T, Args...>, Cols...>
176+
on_duplicate_key_update(Cols const&... assignments) const {
177+
return {*this, std::tuple<Cols...>{assignments...}};
178+
}
179+
180+
[[nodiscard]] std::string build_sql() const {
181+
const auto table_name = table_name_for<T>::value().to_string_view();
182+
const auto& column_list = dml_detail::generate_column_list<T>();
183+
auto values = dml_detail::generate_field_values_impl(args_, std::make_index_sequence<sizeof...(Args)>{});
184+
std::string s;
185+
s.reserve(12 + table_name.size() + 2 + column_list.size() + 10 + values.size() + 1);
186+
s += "INSERT INTO ";
187+
s += table_name;
188+
s += " (";
189+
s += column_list;
190+
s += ") VALUES (";
191+
s += values;
192+
s += ')';
193+
return s;
194+
}
195+
196+
private:
197+
std::tuple<Args...> args_;
198+
};
199+
200+
// ---------------------------------------------------------------
201+
// insert_into_column_values_builder — column-specific insert
202+
// (subset of columns, raw values wrapped into column fields)
203+
// ---------------------------------------------------------------
204+
template <typename T, typename StoredTuple, typename... Cols>
205+
class insert_into_column_values_builder {
206+
public:
207+
explicit insert_into_column_values_builder(StoredTuple stored) : stored_(std::move(stored)) {
208+
}
209+
210+
template <FieldOf<T>... UpdateCols>
211+
requires(sizeof...(UpdateCols) > 0)
212+
[[nodiscard]] insert_into_upsert_builder<T, insert_into_column_values_builder, UpdateCols...>
213+
on_duplicate_key_update(UpdateCols const&... assignments) const {
214+
return {*this, std::tuple<UpdateCols...>{assignments...}};
215+
}
216+
217+
[[nodiscard]] std::string build_sql() const {
218+
const auto table_name = table_name_for<T>::value().to_string_view();
219+
auto column_list = dml_detail::generate_column_names<Cols...>();
220+
auto values = dml_detail::generate_field_values_impl(stored_, std::make_index_sequence<sizeof...(Cols)>{});
221+
std::string s;
222+
s.reserve(12 + table_name.size() + 2 + column_list.size() + 10 + values.size() + 1);
223+
s += "INSERT INTO ";
224+
s += table_name;
225+
s += " (";
226+
s += column_list;
227+
s += ") VALUES (";
228+
s += values;
229+
s += ')';
230+
return s;
231+
}
232+
233+
private:
234+
StoredTuple stored_;
235+
};
236+
237+
template <typename T, typename... Cols>
238+
class insert_into_columns_builder {
239+
public:
240+
template <typename... Vals>
241+
requires(sizeof...(Vals) == sizeof...(Cols) &&
242+
dml_detail::valid_column_values_impl<std::tuple<Cols...>, Vals...>(
243+
std::make_index_sequence<sizeof...(Cols)>{}))
244+
[[nodiscard]] auto values(Vals const&... vals) const {
245+
auto stored = std::tuple{dml_detail::make_insert_value<Cols>(vals)...};
246+
return insert_into_column_values_builder<T, decltype(stored), Cols...>{std::move(stored)};
247+
}
248+
};
249+
250+
// ---------------------------------------------------------------
251+
// insert_into_builder
252+
// ---------------------------------------------------------------
158253
template <typename T>
159254
class insert_into_builder {
160255
public:
256+
// Struct-based insert: .values(row)
161257
[[nodiscard]] insert_into_values_builder<T> values(T const& row) const {
162258
return insert_into_values_builder<T>{row};
163259
}
164260

261+
// Bulk insert: .values(range_of_rows)
165262
template <std::ranges::input_range Rows>
166263
requires std::same_as<std::remove_cvref_t<std::ranges::range_value_t<Rows>>, T>
167264
[[nodiscard]] insert_into_values_builder<T> values(Rows&& rows) const {
@@ -174,6 +271,23 @@ class insert_into_builder {
174271
}
175272
return {std::move(collected_rows), /*bulk=*/true};
176273
}
274+
275+
// Positional field-based insert: .values(sql_default(), col2{"val"}, ...)
276+
// All struct fields must be provided in order. Each is either sql_default()
277+
// or the matching column field type at that position.
278+
template <typename... Args>
279+
requires ValidFieldArgs<T, std::decay_t<Args>...>
280+
[[nodiscard]] insert_into_fields_builder<T, std::decay_t<Args>...> values(Args const&... args) const {
281+
return insert_into_fields_builder<T, std::decay_t<Args>...>{std::tuple<std::decay_t<Args>...>{args...}};
282+
}
283+
284+
// Column-specific insert: .columns(col1{}, col2{}).values(val1, val2)
285+
// Specify a subset of columns; values are provided in a subsequent .values() call.
286+
template <FieldOf<T>... Cols>
287+
requires(sizeof...(Cols) > 0)
288+
[[nodiscard]] insert_into_columns_builder<T, Cols...> columns(Cols const&...) const {
289+
return {};
290+
}
177291
};
178292

179293
// ---------------------------------------------------------------
@@ -410,9 +524,16 @@ template <ValidTable T>
410524
* Bulk insert (multiple rows in one statement):
411525
* insert_into(symbol{}).values(rows).build_sql() // rows is std::ranges::input_range<T>
412526
*
527+
* Positional field-based insert (all fields, sql_default() for auto-increment):
528+
* insert_into(symbol{}).values(sql_default(), symbol::ticker{"AAPL"}, symbol::instrument{"Stock"}).build_sql()
529+
* // → INSERT INTO symbol (id, ticker, instrument) VALUES (DEFAULT, 'AAPL', 'Stock')
530+
*
531+
* Column-specific insert (subset of columns):
532+
* insert_into(symbol{}).columns(symbol::ticker{}, symbol::instrument{}).values("AAPL", "Stock").build_sql()
533+
* // → INSERT INTO symbol (ticker, instrument) VALUES ('AAPL', 'Stock')
534+
*
413535
* Upsert (INSERT … ON DUPLICATE KEY UPDATE):
414536
* insert_into(symbol{}).values(row).on_duplicate_key_update(symbol::ticker{"AAPL"}, ...)
415-
* insert_into(symbol{}).values(row).on_duplicate_key_update<symbol::ticker>("AAPL") // template form
416537
*
417538
* Example:
418539
* symbol row;

0 commit comments

Comments
 (0)