Skip to content

Commit 8311463

Browse files
author
Perdixky
committed
Orthogonal returning(ids) + strong-typed conflict policy tags for sqlgen::insert
1 parent a414397 commit 8311463

38 files changed

Lines changed: 1425 additions & 186 deletions

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab
2727
- [sqlgen::exec](exec.md) - How to execute raw SQL statements
2828
- [sqlgen::group_by and Aggregations](group_by_and_aggregations.md) - How generate GROUP BY queries and aggregate data
2929
- [sqlgen::inner_join, sqlgen::left_join, sqlgen::right_join, sqlgen::full_join](joins.md) - How to join different tables
30-
- [sqlgen::insert, sqlgen::insert_or_replace](insert.md) - How to insert data within transactions
30+
- [sqlgen::insert, sqlgen::insert_or_replace, sqlgen::returning](insert.md) - How to insert data within transactions
3131
- [sqlgen::select_from](select_from.md) - How to read data from a database using more complex queries
3232
- [sqlgen::unite and sqlgen::unite_all](unite.md) - How to combine results from multiple SELECT statements
3333
- [sqlgen::update](update.md) - How to update data in a table

docs/insert.md

Lines changed: 43 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# `sqlgen::insert`, `sqlgen::insert_or_replace`
1+
# `sqlgen::insert`, `sqlgen::insert_or_replace`, `sqlgen::returning`
22

33
The `sqlgen::insert` interface provides a type-safe way to insert data from C++ containers or ranges into a SQL database. Unlike `sqlgen::write`, it does not create tables automatically and is designed to be used within transactions. It's particularly useful when you need fine-grained control over table creation and transaction boundaries.
44

@@ -68,76 +68,67 @@ sqlgen::sqlite::connect("database.db")
6868
.value();
6969
```
7070

71-
### With Replacement (`insert_or_replace`)
71+
### Conflict Policies (`or_replace`, `or_ignore`)
7272

73-
The `insert_or_replace` helper inserts rows and updates existing rows when a primary key or unique constraint would be violated by the insert. It is a thin wrapper over the same insertion paths used by `insert`, but it sets the internal `or_replace` flag so the transpiler emits backend-specific "upsert" SQL.
74-
75-
Function signatures (examples):
73+
`insert(...)` supports typed conflict-policy tags:
7674

7775
```cpp
78-
// Use with an explicit connection (or a Result<Ref<Connection>>)
79-
template <class ContainerType>
80-
auto insert_or_replace(const auto& conn, const ContainerType& data);
76+
using namespace sqlgen;
77+
78+
insert(people, or_replace);
79+
insert(people, or_ignore);
8180

82-
// Use as a pipeline element (returns a callable that accepts a connection)
83-
template <class ContainerType>
84-
auto insert_or_replace(const ContainerType& data);
81+
// Pipeline style is also supported (suggest):
82+
insert(people) | or_replace;
83+
insert(people) | or_ignore;
8584
```
8685
87-
Compile-time requirement
86+
Behavior by backend:
8887
89-
- The table type must have a primary key or at least one unique constraint. This is enforced at compile time via a static_assert:
88+
- SQLite: `OR REPLACE`, `OR IGNORE`
89+
- PostgreSQL: `ON CONFLICT (...) DO UPDATE ...`, `ON CONFLICT DO NOTHING`
90+
- DuckDB: `OR REPLACE`, `OR IGNORE`
91+
- MySQL: `ON DUPLICATE KEY UPDATE`, `INSERT IGNORE`
9092
91-
"The table must have a primary key or unique column for insert_or_replace(...) to work."
93+
Compile-time rules:
9294
93-
Behavior notes
95+
- You can set at most one conflict policy (`or_replace` or `or_ignore`).
96+
- `or_replace` requires at least one primary key or unique constraint.
9497
95-
- SQLite, PostgreSQL and DuckDB backends emit `ON CONFLICT (...) DO UPDATE ...` (using `excluded.*` to reference the incoming values).
96-
- MySQL backend emits `ON DUPLICATE KEY UPDATE` and uses `VALUES(...)` to reference incoming values.
97-
- The transpilation helper `to_insert_or_write<..., dynamic::Insert>(true)` is used internally to produce the correct SQL.
98+
### Returning Auto-generated IDs (`returning(ids)`)
9899
99-
Example:
100+
Use `returning(ids)` to collect auto-generated primary keys during `insert`:
100101
101102
```cpp
102-
const auto people1 = std::vector<Person>({
103-
Person{.id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45},
104-
Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}
105-
});
106-
107-
const auto people2 = std::vector<Person>({
108-
Person{.id = 1, .first_name = "Bartholomew", .last_name = "Simpson", .age = 10}
109-
});
103+
struct Person {
104+
sqlgen::PrimaryKey<uint32_t, sqlgen::auto_incr> id;
105+
std::string first_name;
106+
int age;
107+
};
110108
111-
using namespace sqlgen;
109+
auto ids = std::vector<uint32_t>{};
112110
113-
const auto result = sqlite::connect()
111+
sqlite::connect()
114112
.and_then(create_table<Person> | if_not_exists)
115-
.and_then(insert(std::ref(people1)))
116-
.and_then(insert_or_replace(std::ref(people2)))
113+
.and_then(insert(people, returning(ids)))
117114
.value();
118115
```
119116

120-
Generated SQL (SQLite/Postgres/DuckDB style):
117+
Compile-time rules:
121118

122-
```sql
123-
INSERT INTO "Person" ("id", "first_name", "last_name", "age") VALUES (?, ?, ?, ?)
124-
ON CONFLICT (id) DO UPDATE SET
125-
id=excluded.id,
126-
first_name=excluded.first_name,
127-
last_name=excluded.last_name,
128-
age=excluded.age;
129-
```
119+
- The target type must contain an auto-incrementing primary key.
120+
- `returning(ids)` cannot be combined with `or_ignore`.
121+
- The `ids` container must support `clear()` and `push_back(value_type)`.
122+
- On MySQL, `returning(ids)` is supported for single-object inserts only.
130123

131-
Generated SQL (MySQL style):
124+
Backend behavior:
132125

133-
```sql
134-
INSERT INTO `Person` (`id`, `first_name`, `last_name`, `age`) VALUES (?, ?, ?, ?)
135-
ON DUPLICATE KEY UPDATE
136-
id=VALUES(id),
137-
first_name=VALUES(first_name),
138-
last_name=VALUES(last_name),
139-
age=VALUES(age);
140-
```
126+
- SQLite/PostgreSQL/DuckDB: generated SQL uses `RETURNING`.
127+
- MySQL: no `RETURNING` SQL is emitted; IDs are read via the MySQL C API.
128+
129+
### Backward Compatibility (`insert_or_replace`)
130+
131+
`insert_or_replace(...)` is still available and works like before. Internally it is now a thin wrapper over `insert(..., or_replace)`.
141132

142133
## Example: Full Transaction Usage
143134

@@ -259,11 +250,9 @@ While both `insert` and `write` can be used to add data to a database, they serv
259250
## Notes
260251

261252
- The `Result<Ref<Connection>>` type provides error handling; use `.value()` to extract the result (will throw an exception if there's an error) or handle errors as needed
262-
- The function has several overloads:
263-
1. Takes a connection reference and iterators
264-
2. Takes a `Result<Ref<Connection>>` and iterators
265-
3. Takes a connection and a container directly
266-
4. Takes a connection and a reference wrapper to a container
253+
- `insert(...)` accepts optional modifiers: `or_replace`, `or_ignore`, `returning(ids)`
254+
- Modifiers can be passed directly (`insert(data, or_replace)`) or in pipeline style (`insert(data) | or_replace`)
267255
- Unlike `write`, `insert` does not create tables automatically - you must create tables separately using `create_table`
268256
- The insert operation is atomic within a transaction
269257
- When using reference wrappers (`std::ref`), the data is not copied, which can be more efficient for large datasets
258+
- On MySQL, `returning(ids)` is limited to single-object inserts

include/sqlgen/Session.hpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ class Session {
2222
using Connection = _Connection;
2323
using ConnPtr = Ref<Connection>;
2424

25+
static constexpr bool supports_returning_ids =
26+
Connection::supports_returning_ids;
27+
static constexpr bool supports_multirow_returning_ids =
28+
Connection::supports_multirow_returning_ids;
29+
2530
Session(const Ref<Connection>& _conn, const Ref<std::atomic_flag>& _flag)
2631
: conn_(_conn), flag_(_flag.ptr()) {}
2732

@@ -47,9 +52,10 @@ class Session {
4752
}
4853

4954
template <class ItBegin, class ItEnd>
50-
Result<Nothing> insert(const dynamic::Insert& _stmt, ItBegin _begin,
51-
ItEnd _end) {
52-
return conn_->insert(_stmt, _begin, _end);
55+
Result<Nothing> insert(
56+
const dynamic::Insert& _stmt, ItBegin _begin, ItEnd _end,
57+
std::vector<std::optional<std::string>>* _returned_ids = nullptr) {
58+
return conn_->insert(_stmt, _begin, _end, _returned_ids);
5359
}
5460

5561
Session& operator=(const Session& _other) = delete;

include/sqlgen/Transaction.hpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#ifndef SQLGEN_TRANSACTION_HPP_
22
#define SQLGEN_TRANSACTION_HPP_
33

4+
#include <optional>
5+
#include <vector>
6+
47
#include "Ref.hpp"
58
#include "internal/iterator_t.hpp"
69
#include "is_connection.hpp"
@@ -13,6 +16,11 @@ class Transaction {
1316
public:
1417
using ConnType = _ConnType;
1518

19+
static constexpr bool supports_returning_ids =
20+
ConnType::supports_returning_ids;
21+
static constexpr bool supports_multirow_returning_ids =
22+
ConnType::supports_multirow_returning_ids;
23+
1624
Transaction(const Ref<ConnType>& _conn)
1725
: conn_(_conn), transaction_ended_(false) {}
1826

@@ -57,9 +65,10 @@ class Transaction {
5765
}
5866

5967
template <class ItBegin, class ItEnd>
60-
Result<Nothing> insert(const dynamic::Insert& _stmt, ItBegin _begin,
61-
ItEnd _end) {
62-
return conn_->insert(_stmt, _begin, _end);
68+
Result<Nothing> insert(
69+
const dynamic::Insert& _stmt, ItBegin _begin, ItEnd _end,
70+
std::vector<std::optional<std::string>>* _returned_ids = nullptr) {
71+
return conn_->insert(_stmt, _begin, _end, _returned_ids);
6372
}
6473

6574
Transaction& operator=(const Transaction& _other) = delete;

0 commit comments

Comments
 (0)