|
1 | | -# `sqlgen::insert`, `sqlgen::insert_or_replace` |
| 1 | +# `sqlgen::insert`, `sqlgen::insert_or_replace`, `sqlgen::returning` |
2 | 2 |
|
3 | 3 | 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. |
4 | 4 |
|
@@ -68,76 +68,67 @@ sqlgen::sqlite::connect("database.db") |
68 | 68 | .value(); |
69 | 69 | ``` |
70 | 70 |
|
71 | | -### With Replacement (`insert_or_replace`) |
| 71 | +### Conflict Policies (`or_replace`, `or_ignore`) |
72 | 72 |
|
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: |
76 | 74 |
|
77 | 75 | ```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); |
81 | 80 |
|
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; |
85 | 84 | ``` |
86 | 85 |
|
87 | | -Compile-time requirement |
| 86 | +Behavior by backend: |
88 | 87 |
|
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` |
90 | 92 |
|
91 | | - "The table must have a primary key or unique column for insert_or_replace(...) to work." |
| 93 | +Compile-time rules: |
92 | 94 |
|
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. |
94 | 97 |
|
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)`) |
98 | 99 |
|
99 | | -Example: |
| 100 | +Use `returning(ids)` to collect auto-generated primary keys during `insert`: |
100 | 101 |
|
101 | 102 | ```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 | +}; |
110 | 108 |
|
111 | | -using namespace sqlgen; |
| 109 | +auto ids = std::vector<uint32_t>{}; |
112 | 110 |
|
113 | | -const auto result = sqlite::connect() |
| 111 | +sqlite::connect() |
114 | 112 | .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))) |
117 | 114 | .value(); |
118 | 115 | ``` |
119 | 116 |
|
120 | | -Generated SQL (SQLite/Postgres/DuckDB style): |
| 117 | +Compile-time rules: |
121 | 118 |
|
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. |
130 | 123 |
|
131 | | -Generated SQL (MySQL style): |
| 124 | +Backend behavior: |
132 | 125 |
|
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)`. |
141 | 132 |
|
142 | 133 | ## Example: Full Transaction Usage |
143 | 134 |
|
@@ -259,11 +250,9 @@ While both `insert` and `write` can be used to add data to a database, they serv |
259 | 250 | ## Notes |
260 | 251 |
|
261 | 252 | - 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`) |
267 | 255 | - Unlike `write`, `insert` does not create tables automatically - you must create tables separately using `create_table` |
268 | 256 | - The insert operation is atomic within a transaction |
269 | 257 | - 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 |
0 commit comments