Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5e0f254
Implement insert_or_replace(...) for the SQLite backend
demianmnave Aug 29, 2025
d21b9d9
dynamic::Write should not be saddled with .or_replace
demianmnave Aug 29, 2025
3ce6309
Added compile-time checks for primary keys
liuzicheng1987 Aug 29, 2025
ef47a2d
Added field non_primary_keys to dynamic::Insert
liuzicheng1987 Aug 29, 2025
e8ed7d2
Default to a static runtime for static MSVC libs
demianmnave Sep 6, 2025
3e555b8
Record constraints for insert_or_replace
demianmnave Sep 6, 2025
1b3189b
insert_or_replace(...) requires a primary key or unique column
demianmnave Sep 6, 2025
3253ecb
Implement "ON CONFLICT() DO UPDATE" for SQLite insert_or_replace
demianmnave Sep 6, 2025
9283378
Test generating ON CONFLICT for Postgres insert_or_replace
demianmnave Sep 7, 2025
f51eea2
Fix typos
demianmnave Sep 7, 2025
c5e40c1
Enforce a repeatable order on the rows returned after a replace
demianmnave Sep 7, 2025
ee80f16
Test Postgres insert_or_replace(...)
demianmnave Sep 7, 2025
ba4101d
Fix typos
demianmnave Sep 7, 2025
8c5e7f6
Test generating "ON DUPLICATE KEY UPDATE" for MySQL
demianmnave Sep 7, 2025
7efb39c
Test MySQL insert_or_replace(...)
demianmnave Sep 7, 2025
b52657f
Update insert_or_replace() documentation
demianmnave Sep 7, 2025
3f833c2
Reformatted the code
liuzicheng1987 Sep 7, 2025
4c65899
Fixed typo in the test
liuzicheng1987 Sep 7, 2025
147c950
Make sure tests run for every PR
liuzicheng1987 Sep 7, 2025
84c7aed
Fixed typo
liuzicheng1987 Sep 7, 2025
ac9654a
Removed lines in CMakeLists that led to build conflicts on Windows
liuzicheng1987 Sep 7, 2025
d294189
Added SQLGEN_BUILD_DRY_TESTS_ONLY
liuzicheng1987 Sep 7, 2025
fd205a6
Added change necessary to make things work on macOS
liuzicheng1987 Sep 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/linux-cxx20-conan.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: linux-cxx20-conan

on: [push]
on: [push, pull_request]

jobs:
linux:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/linux-cxx20-vcpkg.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: linux-cxx20-vcpkg

on: [push]
on: [push, pull_request]

env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/macos-cxx20-conan.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: macos-cxx20-conan

on: [push]
on: [push, pull_request]

jobs:
macos-clang:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/macos-cxx20-vcpkg.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: macos-cxx20-vcpkg

on: [push]
on: [push, pull_request]

env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
Expand Down Expand Up @@ -57,6 +57,7 @@ jobs:
if [[ "${{ matrix.os == 'macos-latest' }}" == "true" ]]; then
export VCPKG_FORCE_SYSTEM_BINARIES=arm
export CMAKE_GENERATOR=Ninja
export MACOSX_DEPLOYMENT_TARGET="$(sw_vers -productVersion)"
fi
$CXX --version
cmake -S . -B build -G Ninja -DCMAKE_CXX_STANDARD=20 -DSQLGEN_BUILD_TESTS=ON -DSQLGEN_SQLITE3=OFF -DSQLGEN_BUILD_DRY_TESTS_ONLY=ON
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/windows-cxx20-vcpkg.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: windows-cxx20-vcpkg

on: [push]
on: [push, pull_request]

env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
Expand Down
55 changes: 53 additions & 2 deletions docs/insert.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,58 @@ sqlgen::sqlite::connect("database.db")
.value();
```

### With Replacement

Replace existing rows:

```cpp
const auto people1 = std::vector<Person>({
Person{.id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45},
Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}
});

const auto people2 = std::vector<Person>({
Person{.id = 1, .first_name = "Bartholomew", .last_name = "Simpson", .age = 10}
});

using namespace sqlgen;

const auto result = sqlite::connect()
.and_then(create_table<Person> | if_not_exists)
.and_then(insert(std::ref(people1)))
.and_then(insert_or_replace(std::ref(people2)))
.value();
```

This generates the following SQL:

```sql
CREATE TABLE IF NOT EXISTS "Person" (
"id" INTEGER PRIMARY KEY,
"first_name" TEXT NOT NULL,
"last_name" TEXT NOT NULL,
"age" INTEGER NOT NULL
);
INSERT INTO "Person" ("id", "first_name", "last_name", "age") VALUES (?, ?, ?, ?);
INSERT INTO "Person" ("id", "first_name", "last_name", "age") VALUES (?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
id=excluded.id,
first_name=excluded.first_name,
last_name=excluded.last_name,
age=excluded.age;
```

The SQL generated by the PostgreSQL backend for `insert_or_replace(...)` is similar. The MySQL backend generates `ON DUPLICATE KEY UPDATE` instead:
```sql
...
INSERT INTO `Person` (`id`, `first_name`, `last_name`, `age`) VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
id=VALUES(id),
first_name=VALUES(first_name),
last_name=VALUES(last_name),
age=VALUES(age);
```

## Example: Full Transaction Usage

Here's a complete example showing how to use `insert` within a transaction:
Expand Down Expand Up @@ -195,5 +247,4 @@ While both `insert` and `write` can be used to add data to a database, they serv
4. Takes a connection and a reference wrapper to a container
- Unlike `write`, `insert` does not create tables automatically - you must create tables separately using `create_table`
- The insert operation is atomic within a transaction
- When using reference wrappers (`std::ref`), the data is not copied, which can be more efficient for large datasets

- When using reference wrappers (`std::ref`), the data is not copied, which can be more efficient for large datasets
5 changes: 5 additions & 0 deletions include/sqlgen/dynamic/Insert.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ namespace sqlgen::dynamic {
struct Insert {
Table table;
std::vector<std::string> columns;
bool or_replace;
std::vector<std::string> non_primary_keys;

/// Holds primary keys and unique columns when or_replace is true.
std::vector<std::string> constraints;
};

} // namespace sqlgen::dynamic
Expand Down
79 changes: 62 additions & 17 deletions include/sqlgen/insert.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
#include <rfl.hpp>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>

#include "internal/batch_size.hpp"
#include "internal/has_constraint.hpp"
#include "internal/to_str_vec.hpp"
#include "is_connection.hpp"
#include "transpilation/to_insert_or_write.hpp"
Expand All @@ -18,13 +20,14 @@ namespace sqlgen {

template <class ItBegin, class ItEnd, class Connection>
requires is_connection<Connection>
Result<Ref<Connection>> insert(const Ref<Connection>& _conn, ItBegin _begin,
ItEnd _end) {
Result<Ref<Connection>> insert_impl(const Ref<Connection>& _conn,
ItBegin _begin, ItEnd _end,
bool _or_replace) {
using T =
std::remove_cvref_t<typename std::iterator_traits<ItBegin>::value_type>;

const auto insert_stmt =
transpilation::to_insert_or_write<T, dynamic::Insert>();
transpilation::to_insert_or_write<T, dynamic::Insert>(_or_replace);

std::vector<std::vector<std::optional<std::string>>> data;

Expand All @@ -51,40 +54,82 @@ Result<Ref<Connection>> insert(const Ref<Connection>& _conn, ItBegin _begin,

template <class ItBegin, class ItEnd, class Connection>
requires is_connection<Connection>
Result<Ref<Connection>> insert(const Result<Ref<Connection>>& _res,
ItBegin _begin, ItEnd _end) {
return _res.and_then(
[&](const auto& _conn) { return insert(_conn, _begin, _end); });
Result<Ref<Connection>> insert_impl(const Result<Ref<Connection>>& _res,
ItBegin _begin, ItEnd _end,
bool _or_replace) {
return _res.and_then([&](const auto& _conn) {
return insert_impl(_conn, _begin, _end, _or_replace);
});
}

template <class ContainerType>
auto insert(const auto& _conn, const ContainerType& _data) {
auto insert_impl(const auto& _conn, const ContainerType& _data,
bool _or_replace) {
if constexpr (std::ranges::input_range<std::remove_cvref_t<ContainerType>>) {
return insert(_conn, _data.begin(), _data.end());
return insert_impl(_conn, _data.begin(), _data.end(), _or_replace);
} else {
return insert(_conn, &_data, &_data + 1);
return insert_impl(_conn, &_data, &_data + 1, _or_replace);
}
}

template <class ContainerType>
auto insert(const auto& _conn,
const std::reference_wrapper<ContainerType>& _data) {
return insert(_conn, _data.get());
auto insert_impl(const auto& _conn,
const std::reference_wrapper<ContainerType>& _data,
bool _or_replace) {
return insert_impl(_conn, _data.get(), _or_replace);
}

template <class ContainerType>
struct Insert {
auto operator()(const auto& _conn) const { return insert(_conn, data_); }
auto operator()(const auto& _conn) const {
return insert_impl(_conn, data_, or_replace_);
}

ContainerType data_;
bool or_replace_;
};

template <class ContainerType>
Insert<ContainerType> insert(const ContainerType& _data) {
return Insert<ContainerType>{.data_ = _data};
Insert<ContainerType> insert_impl(const ContainerType& _data,
bool _or_replace) {
return Insert<ContainerType>{.data_ = _data, .or_replace_ = _or_replace};
}

template <class... Args>
auto insert(const Args&... args) {
return insert_impl(args..., false);
}

template <class ContainerType>
auto insert_or_replace(const auto& _conn, const ContainerType& _data) {
if constexpr (std::ranges::input_range<std::remove_cvref_t<ContainerType>>) {
static_assert(
internal::has_constraint_v<typename ContainerType::value_type>,
"The table must have a primary key or unique column for "
"insert_or_replace(...) to work.");
} else {
static_assert(internal::has_constraint_v<ContainerType>,
"The table must have a primary key or unique column for "
"insert_or_replace(...) to work.");
}
return insert_impl(_conn, _data, true);
}

template <class ContainerType>
auto insert_or_replace(const ContainerType& _data) {
if constexpr (std::ranges::input_range<std::remove_cvref_t<ContainerType>>) {
static_assert(
internal::has_constraint_v<typename ContainerType::value_type>,
"The table must have a primary key or unique column for "
"insert_or_replace(...) to work.");
} else {
static_assert(internal::has_constraint_v<ContainerType>,
"The table must have a primary key or unique column for "
"insert_or_replace(...) to work.");
}
return insert_impl(_data, true);
}

}; // namespace sqlgen

#endif

24 changes: 24 additions & 0 deletions include/sqlgen/internal/has_constraint.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#ifndef SQLGEN_INTERNAL_HAS_CONSTRAINT_HPP_
#define SQLGEN_INTERNAL_HAS_CONSTRAINT_HPP_

#include <type_traits>

#include "is_constraint.hpp"

namespace sqlgen::internal {

template <class T>
struct has_constraint;

template <class... FieldTypes>
struct has_constraint<rfl::NamedTuple<FieldTypes...>> {
constexpr static bool value =
(false || ... || is_constraint_v<typename FieldTypes::Type>);
};

template <class T>
constexpr bool has_constraint_v = has_constraint<rfl::named_tuple_t<T>>::value;

} // namespace sqlgen::internal

#endif
25 changes: 25 additions & 0 deletions include/sqlgen/internal/has_primary_key.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#ifndef SQLGEN_INTERNAL_HAS_PRIMARY_KEY_HPP_
#define SQLGEN_INTERNAL_HAS_PRIMARY_KEY_HPP_

#include <rfl.hpp>

#include "is_primary_key.hpp"

namespace sqlgen::internal {

template <class T>
struct has_primary_key;

template <class... FieldTypes>
struct has_primary_key<rfl::NamedTuple<FieldTypes...>> {
constexpr static bool value =
(false || ... || is_primary_key_v<typename FieldTypes::Type>);
};

template <class T>
constexpr bool has_primary_key_v =
has_primary_key<rfl::named_tuple_t<T>>::value;

} // namespace sqlgen::internal

#endif
25 changes: 25 additions & 0 deletions include/sqlgen/internal/has_unique_column.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#ifndef SQLGEN_INTERNAL_HAS_UNIQUE_COLUMN_HPP_
#define SQLGEN_INTERNAL_HAS_UNIQUE_COLUMN_HPP_

#include <rfl.hpp>

#include "is_unique_column.hpp"

namespace sqlgen::internal {

template <class T>
struct has_unique_column;

template <class... FieldTypes>
struct has_unique_column<rfl::NamedTuple<FieldTypes...>> {
constexpr static bool value =
(false || ... || is_unique_column_v<typename FieldTypes::Type>);
};

template <class T>
constexpr bool has_unique_column_v =
has_unique_column<rfl::named_tuple_t<T>>::value;

} // namespace sqlgen::internal

#endif
19 changes: 19 additions & 0 deletions include/sqlgen/internal/is_constraint.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef SQLGEN_INTERNAL_IS_CONSTRAINT_HPP_
#define SQLGEN_INTERNAL_IS_CONSTRAINT_HPP_

#include <type_traits>

#include "is_primary_key.hpp"
#include "is_unique_column.hpp"

namespace sqlgen::internal {

template <class T>
using is_constraint = std::disjunction<is_primary_key<T>, is_unique_column<T>>;

template <class T>
constexpr bool is_constraint_v = is_constraint<std::remove_cvref_t<T>>();

} // namespace sqlgen::internal

#endif
24 changes: 24 additions & 0 deletions include/sqlgen/internal/is_primary_key.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#ifndef SQLGEN_INTERNAL_IS_PRIMARY_KEY_HPP_
#define SQLGEN_INTERNAL_IS_PRIMARY_KEY_HPP_

#include <type_traits>

#include "../PrimaryKey.hpp"

namespace sqlgen::internal {

template <class T>
class is_primary_key;

template <class T>
class is_primary_key : public std::false_type {};

template <class T, bool _auto_incr>
class is_primary_key<PrimaryKey<T, _auto_incr>> : public std::true_type {};

template <class T>
constexpr bool is_primary_key_v = is_primary_key<std::remove_cvref_t<T>>();

} // namespace sqlgen::internal

#endif
Loading
Loading