Added support for MySQL (#33); resolves #18

This commit is contained in:
Dr. Patrick Urbanke (劉自成)
2025-07-27 12:25:12 +02:00
committed by GitHub
parent 1424d29c5d
commit 3f8c5556d2
81 changed files with 4338 additions and 23 deletions

View File

@@ -10,16 +10,37 @@ jobs:
include:
- compiler: llvm
compiler-version: 16
link: "static"
- compiler: llvm
compiler-version: 18
link: "static"
- compiler: gcc
compiler-version: 11
additional-dep: "g++-11"
link: "static"
- compiler: gcc
compiler-version: 12
link: "static"
- compiler: gcc
compiler-version: 14
name: "${{ github.job }} (${{ matrix.compiler }}-${{ matrix.compiler-version }})"
link: "static"
- compiler: llvm
compiler-version: 16
link: "shared"
- compiler: llvm
compiler-version: 18
link: "shared"
- compiler: gcc
compiler-version: 11
additional-dep: "g++-11"
link: "shared"
- compiler: gcc
compiler-version: 12
link: "shared"
- compiler: gcc
compiler-version: 14
link: "shared"
name: "${{ github.job }} (${{ matrix.compiler }}-${{ matrix.compiler-version }}-${{ matrix.link }})"
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -47,4 +68,8 @@ jobs:
sudo ln -s $(which ccache) /usr/local/bin/$CC
sudo ln -s $(which ccache) /usr/local/bin/$CXX
$CXX --version
conan build . --build=missing -s compiler.cppstd=gnu20
if [[ "${{ matrix.link }}" == "static" ]]; then
conan build . --build=missing -s compiler.cppstd=gnu20
else
conan build . --build=missing -s compiler.cppstd=gnu20 -o sqlgen/*:with_mysql=True -o */*:shared=True
fi

View File

@@ -43,6 +43,22 @@ jobs:
- compiler: gcc
compiler-version: 14
db: sqlite
- compiler: llvm
compiler-version: 16
db: mysql
- compiler: llvm
compiler-version: 18
db: mysql
- compiler: gcc
compiler-version: 11
additional-dep: "g++-11"
db: mysql
- compiler: gcc
compiler-version: 12
db: mysql
- compiler: gcc
compiler-version: 14
db: mysql
name: "${{ github.job }} (${{ matrix.compiler }}-${{ matrix.compiler-version }}-${{ matrix.db }})"
runs-on: ubuntu-latest
steps:
@@ -98,11 +114,33 @@ jobs:
$CXX --version
cmake -S . -B build -G Ninja -DCMAKE_CXX_STANDARD=20 -DSQLGEN_BUILD_TESTS=ON -DSQLGEN_POSTGRES=OFF
cmake --build build
- name: Compile
if: matrix.db == 'mysql'
run: |
if [[ "${{ matrix.compiler }}" == "llvm" ]]; then
export CC=clang-${{ matrix.compiler-version }}
export CXX=clang++-${{ matrix.compiler-version }}
elif [[ "${{ matrix.compiler }}" == "gcc" ]]; then
export CC=gcc-${{ matrix.compiler-version }}
export CXX=g++-${{ matrix.compiler-version }}
fi
sudo ln -s $(which ccache) /usr/local/bin/$CC
sudo ln -s $(which ccache) /usr/local/bin/$CXX
$CXX --version
cmake -S . -B build -G Ninja -DCMAKE_CXX_STANDARD=20 -DSQLGEN_BUILD_TESTS=ON -DSQLGEN_MYSQL=ON -DSQLGEN_POSTGRES=OFF -DSQLGEN_SQLITE3=OFF -DBUILD_SHARED_LIBS=ON -DVCPKG_TARGET_TRIPLET=x64-linux-dynamic
cmake --build build
- name: Set up postgres
if: matrix.db == 'postgres'
run: |
sudo systemctl start postgresql.service
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'password';"
- name: Set up mysql
if: matrix.db == 'mysql'
run: |
sudo systemctl start mysql
sudo mysql -uroot -proot -e "CREATE USER sqlgen IDENTIFIED WITH mysql_native_password BY 'password';"
sudo mysql -uroot -proot -e "GRANT ALL PRIVILEGES ON *.* TO 'sqlgen';"
sudo mysql -uroot -proot -e "SET GLOBAL time_zone = '+0:00';"
- name: Run tests
run: |
ctest --test-dir build --output-on-failure

View File

@@ -9,8 +9,14 @@ jobs:
matrix:
include:
- os: "macos-latest"
link: "static"
- os: "macos-13"
name: "${{ github.job }} (${{ matrix.os }})"
link: "static"
- os: "macos-latest"
link: "shared"
- os: "macos-13"
link: "shared"
name: "${{ github.job }} (${{ matrix.os }}-${{ matrix.link }})"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
@@ -30,4 +36,8 @@ jobs:
CXX: clang++
run: |
$CXX --version
conan build . --build=missing -s compiler.cppstd=gnu20
if [[ "${{ matrix.link }}" == "static" ]]; then
conan build . --build=missing -s compiler.cppstd=gnu20
else
conan build . --build=missing -s compiler.cppstd=gnu20 -o sqlgen/*:with_mysql=True -o */*:shared=True
fi

View File

@@ -15,10 +15,14 @@ jobs:
db: postgres
- os: "macos-latest"
db: sqlite
- os: "macos-latest"
db: mysql
- os: "macos-13"
db: postgres
- os: "macos-13"
db: sqlite
- os: "macos-13"
db: mysql
name: "${{ github.job }} (${{ matrix.os }}-${{ matrix.db }})"
runs-on: ${{ matrix.os }}
steps:
@@ -70,6 +74,23 @@ jobs:
$CXX --version
cmake -S . -B build -G Ninja -DCMAKE_CXX_STANDARD=20 -DSQLGEN_BUILD_TESTS=ON -DSQLGEN_POSTGRES=OFF
cmake --build build -j 4
- name: Compile
if: matrix.db == 'mysql'
env:
CC: clang
CXX: clang++
run: |
if [[ "${{ matrix.os == 'macos-latest' }}" == "true" ]]; then
export VCPKG_FORCE_SYSTEM_BINARIES=arm
export CMAKE_GENERATOR=Ninja
fi
$CXX --version
if [[ "${{ matrix.os == 'macos-latest' }}" == "true" ]]; then
cmake -S . -B build -G Ninja -DCMAKE_CXX_STANDARD=20 -DSQLGEN_BUILD_TESTS=ON -DSQLGEN_MYSQL=ON -DSQLGEN_POSTGRES=OFF -DSQLGEN_SQLITE3=OFF -DBUILD_SHARED_LIBS=ON -DVCPKG_TARGET_TRIPLET=arm64-osx-dynamic -DSQLGEN_BUILD_DRY_TESTS_ONLY=ON
else
cmake -S . -B build -G Ninja -DCMAKE_CXX_STANDARD=20 -DSQLGEN_BUILD_TESTS=ON -DSQLGEN_MYSQL=ON -DSQLGEN_POSTGRES=OFF -DSQLGEN_SQLITE3=OFF -DBUILD_SHARED_LIBS=ON -DVCPKG_TARGET_TRIPLET=x64-osx-dynamic -DSQLGEN_BUILD_DRY_TESTS_ONLY=ON
fi
cmake --build build -j 4
- name: Run tests
run: |
ctest --test-dir build --output-on-failure

View File

@@ -13,6 +13,7 @@ jobs:
include:
- db: postgres
- db: sqlite
- db: mysql
name: "(windows-${{ matrix.db }})"
runs-on: windows-latest
steps:
@@ -39,6 +40,11 @@ jobs:
run: |
cmake -S . -B build -G Ninja -DCMAKE_CXX_STANDARD=20 -DSQLGEN_BUILD_TESTS=ON -DSQLGEN_POSTGRES=OFF
cmake --build build --config Release -j4
- name: Compile
if: matrix.db == 'mysql'
run: |
cmake -S . -B build -G Ninja -DCMAKE_CXX_STANDARD=20 -DSQLGEN_BUILD_TESTS=ON -DSQLGEN_MYSQL=ON -DSQLGEN_POSTGRES=OFF -DSQLGEN_SQLITE3=OFF -DSQLGEN_BUILD_DRY_TESTS_ONLY=ON
cmake --build build --config Release -j4
- name: Run tests
run: |
ctest --test-dir build --output-on-failure

View File

@@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.23)
option(SQLGEN_BUILD_SHARED "Build shared library" ${BUILD_SHARED_LIBS})
option(SQLGEN_MYSQL "Enable MySQL support" OFF)
option(SQLGEN_POSTGRES "Enable PostgreSQL support" ON) # enabled by default
option(SQLGEN_SQLITE3 "Enable SQLite3 support" ON) # enabled by default
@@ -15,6 +17,7 @@ if (NOT DEFINED CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 20)
endif()
set(SQLGEN_USE_VCPKG_DEFAULT ON)
option(SQLGEN_USE_VCPKG "Use VCPKG to download and build dependencies" ${SQLGEN_USE_VCPKG_DEFAULT})
@@ -24,6 +27,10 @@ if (SQLGEN_USE_VCPKG)
list(APPEND VCPKG_MANIFEST_FEATURES "tests")
endif()
if (SQLGEN_MYSQL)
list(APPEND VCPKG_MANIFEST_FEATURES "mysql")
endif()
if (SQLGEN_POSTGRES)
list(APPEND VCPKG_MANIFEST_FEATURES "postgres")
endif()
@@ -31,11 +38,11 @@ if (SQLGEN_USE_VCPKG)
if (SQLGEN_SQLITE3)
list(APPEND VCPKG_MANIFEST_FEATURES "sqlite3")
endif()
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake CACHE STRING "Vcpkg toolchain file")
endif ()
project(sqlgen VERSION 0.0.1 LANGUAGES CXX)
project(sqlgen VERSION 0.1.0 LANGUAGES CXX)
if (SQLGEN_BUILD_SHARED)
add_library(sqlgen SHARED)
@@ -59,6 +66,21 @@ target_include_directories(
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
if(SQLGEN_MYSQL)
list(APPEND SQLGEN_SOURCES src/sqlgen_mysql.cpp)
if (SQLGEN_USE_VCPKG)
if (NOT TARGET unofficial-libmariadb)
find_package(unofficial-libmariadb CONFIG REQUIRED)
endif()
target_link_libraries(sqlgen PUBLIC unofficial::libmariadb)
else()
if (NOT TARGET mariadb-connector-c)
find_package(mariadb-connector-c REQUIRED)
endif()
target_link_libraries(sqlgen PUBLIC mariadb-connector-c::mariadb-connector-c)
endif()
endif()
if (SQLGEN_POSTGRES)
list(APPEND SQLGEN_SOURCES src/sqlgen_postgres.cpp)
if (NOT TARGET PostgreSQL)

View File

@@ -23,6 +23,16 @@ Together, reflect-cpp and sqlgen enable reliable and efficient ETL pipelines.
- 🔌 **Multiple Backends**: Support for PostgreSQL and SQLite
- 🔍 **Reflection Integration**: Seamless integration with [reflect-cpp](https://github.com/getml/reflect-cpp)
## Supported databases
The following table lists the databases currently supported by sqlgen and the underlying libraries used:
| Database | Library | Version | License | Remarks |
|---------------|--------------------------------------------------------------------------|--------------|---------------| -----------------------------------------------------|
| MySQL/MariaDB | [libmariadb](https://github.com/mariadb-corporation/mariadb-connector-c) | >= 3.4.5 | LGPL | |
| PostgreSQL | [libpq](https://github.com/postgres/postgres) | >= 16.4 | PostgreSQL | Will work for all libpq-compatible databases |
| sqlite | [sqlite](https://sqlite.org/index.html) | >= 3.49.1 | Public Domain | |
## Quick Start
### Installation using vcpkg
@@ -47,6 +57,14 @@ cmake --build build -j 4 # gcc, clang
cmake --build build --config Release -j 4 # MSVC
```
This will build the static library. To build the shared library
add `-DBUILD_SHARED_LIBS=ON -DVCPKG_TARGET_TRIPLET=...` to the first line.
Run `./vcpkg/vcpkg help triplets` to view all supported triplets.
Common triplets for shared libraries are `x64-linux-dynamic`,
`arm64-osx-dynamic` or `x64-osx-dynamic`.
Add `-DSQLGEN_BUILD_MYSQL=ON` to support MySQL/MariaDB.
4. Include in your CMake project:
```cmake
find_package(sqlgen REQUIRED)
@@ -70,6 +88,17 @@ For older versions of pip, you can also use `pip` instead of `pipx`.
conan build . --build=missing -s compiler.cppstd=gnu20
```
This will build the static library. To build the shared library,
add `-o */*:shared=True`.
Add `-o sqlgen/*:with_mysql=True` to support MySQL/MariaDB.
3. Include in your CMake project:
```cmake
find_package(sqlgen REQUIRED)
target_link_libraries(your_target PRIVATE sqlgen::sqlgen)
```
You can call `conan inspect .` to get an overview of the supported options.
## Usage Examples

View File

@@ -26,12 +26,14 @@ class SQLGenConan(ConanFile):
options = {
"shared": [True, False],
"fPIC": [True, False],
"with_mysql": [True, False],
"with_postgres": [True, False],
"with_sqlite3": [True, False],
}
default_options = {
"shared": False,
"fPIC": True,
"with_mysql": False,
"with_postgres": True,
"with_sqlite3": True,
}
@@ -46,6 +48,8 @@ class SQLGenConan(ConanFile):
def requirements(self):
self.requires("reflect-cpp/0.19.0", transitive_headers=True)
if self.options.with_mysql:
self.requires("mariadb-connector-c/3.4.3", transitive_headers=True)
if self.options.with_postgres:
self.requires("libpq/17.5", transitive_headers=True)
if self.options.with_sqlite3:
@@ -71,6 +75,7 @@ class SQLGenConan(ConanFile):
deps.generate()
tc = CMakeToolchain(self)
tc.cache_variables["SQLGEN_BUILD_SHARED"] = self.options.shared
tc.cache_variables["SQLGEN_MYSQL"] = self.options.with_mysql
tc.cache_variables["SQLGEN_POSTGRES"] = self.options.with_postgres
tc.cache_variables["SQLGEN_SQLITE3"] = self.options.with_sqlite3
tc.cache_variables["SQLGEN_USE_VCPKG"] = False

View File

@@ -50,6 +50,7 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab
## Supported Databases
- [MySQL](mysql.md) - How to interact with MariaDB and MySQL
- [PostgreSQL](postgres.md) - How to interact with PostgreSQL and compatible databases (Redshift, Aurora, Greenplum, CockroachDB, ...)
- [SQLite](sqlite.md) - How to interact with SQLite3

176
docs/mysql.md Normal file
View File

@@ -0,0 +1,176 @@
# `sqlgen::mysql`
The `sqlgen::mysql` module provides a type-safe and efficient interface for interacting with MySQL and MariaDB databases. It implements the core database operations through a connection-based API with support for prepared statements, transactions, and efficient data iteration.
## Usage
### Basic Connection
Create a connection to a MySQL/MariaDB database using credentials:
```cpp
// Create credentials for the database connection
const auto creds = sqlgen::mysql::Credentials{
.host = "localhost",
.user = "myuser",
.password = "mypassword",
.dbname = "mydatabase",
.port = 3306, // Optional, defaults to 3306
.unix_socket = "/var/run/mysqld/mysqld.sock" // Optional, defaults to "/var/run/mysqld/mysqld.sock"
};
// Connect to the database
const auto conn = sqlgen::mysql::connect(creds);
```
The connection is wrapped in a `sqlgen::Result<Ref<Connection>>` for error handling:
```cpp
// Handle connection errors
const auto conn = sqlgen::mysql::connect(creds);
if (!conn) {
// Handle error...
return;
}
using namespace sqlgen;
using namespace sqlgen::literals;
const auto query = sqlgen::read<std::vector<Person>> |
where("age"_c < 18 and "first_name"_c != "Hugo");
// Use the connection
const auto minors = query(conn);
```
### Basic Operations
Write data to the database:
```cpp
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
const auto people = 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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}
};
// Write data to database
const auto result = sqlgen::write(conn, people);
```
Read data with filtering and ordering:
```cpp
using namespace sqlgen;
using namespace sqlgen::literals;
// Read all people ordered by age
const auto all_people = sqlgen::read<std::vector<Person>> |
order_by("age"_c);
// Read minors only
const auto minors = sqlgen::read<std::vector<Person>> |
where("age"_c < 18) |
order_by("age"_c);
// Use the queries
const auto result1 = all_people(conn);
const auto result2 = minors(conn);
```
### Transactions
Perform operations within transactions:
```cpp
using namespace sqlgen;
using namespace sqlgen::literals;
// Delete a person and update another in a transaction
const auto delete_hugo = delete_from<Person> |
where("first_name"_c == "Hugo");
const auto update_homer = update<Person>("age"_c.set(46)) |
where("first_name"_c == "Homer");
const auto result = begin_transaction(conn)
.and_then(delete_hugo)
.and_then(update_homer)
.and_then(commit)
.value();
```
### Advanced Queries
Use complex queries with joins and projections:
```cpp
using namespace sqlgen;
using namespace sqlgen::literals;
// Select specific columns with aliases
struct FirstName {
std::string first_name;
};
const auto names = select_from<Person>("first_name"_c) |
order_by("id"_c) |
to<std::vector<FirstName>>;
// Join operations
const auto joined_data = select_from<Person, "t1">(
"id"_t1 | as<"id">,
"first_name"_t1 | as<"first_name">,
"last_name"_t2 | as<"last_name">,
"age"_t2 | as<"age">) |
left_join<Person, "t2">("id"_t1 == "id"_t2) |
order_by("id"_t1) |
to<std::vector<Person>>;
```
### Connection Pools
Use connection pools for efficient resource management:
```cpp
const auto pool_config = sqlgen::ConnectionPoolConfig{.size = 2};
const auto pool = sqlgen::make_connection_pool<sqlgen::mysql::Connection>(
pool_config, credentials);
// Use the pool in a session
const auto result = session(pool)
.and_then(write(std::ref(people)))
.and_then(sqlgen::read<std::vector<Person>>)
.value();
```
## Notes
- The module provides a type-safe interface for MySQL/MariaDB operations
- All operations return `sqlgen::Result<T>` for error handling
- Prepared statements are used for efficient query execution
- The iterator interface supports batch processing of results
- SQL generation adapts to MySQL's dialect
- The module supports:
- Connection management with credentials (host, port, database name, unix socket)
- Transactions (begin, commit, rollback)
- Prepared statements for efficient execution
- Efficient batch operations
- Type-safe SQL generation
- Error handling through `Result<T>`
- Resource management through `Ref<T>`
- Connection pooling for high-performance applications
- Auto-incrementing primary keys
- Various data types including VARCHAR, TIMESTAMP, DATE
- Complex queries with WHERE clauses, ORDER BY, LIMIT, JOINs
- LIKE and pattern matching operations
- Mathematical operations and string functions

View File

@@ -12,7 +12,7 @@ using Type =
rfl::TaggedUnion<"type", types::Unknown, types::Boolean, types::Float32,
types::Float64, types::Int8, types::Int16, types::Int32,
types::Int64, types::UInt8, types::UInt16, types::UInt32,
types::UInt64, types::Text, types::Timestamp,
types::UInt64, types::Text, types::Date, types::Timestamp,
types::TimestampWithTZ, types::VarChar>;
} // namespace sqlgen::dynamic

View File

@@ -65,6 +65,11 @@ struct Text {
Properties properties;
};
struct Date {
std::string tz;
Properties properties;
};
struct Timestamp {
std::string tz;
Properties properties;

View File

@@ -23,6 +23,15 @@ std::string replace_all(const std::string& _str, const std::string& _from,
std::vector<std::string> split(const std::string& _str,
const std::string& _delimiter);
std::string ltrim(const std::string& _str, const std::string& _chars = " ");
std::string rtrim(const std::string& _str, const std::string& _chars = " ");
inline std::string trim(const std::string& _str,
const std::string& _chars = " ") {
return ltrim(rtrim(_str, _chars), _chars);
}
} // namespace sqlgen::internal::strings
#endif

9
include/sqlgen/mysql.hpp Normal file
View File

@@ -0,0 +1,9 @@
#ifndef SQLGEN_MYSQL_HPP_
#define SQLGEN_MYSQL_HPP_
#include "../sqlgen.hpp"
#include "mysql/Credentials.hpp"
#include "mysql/connect.hpp"
#include "mysql/to_sql.hpp"
#endif

View File

@@ -0,0 +1,97 @@
#ifndef SQLGEN_MYSQL_CONNECTION_HPP_
#define SQLGEN_MYSQL_CONNECTION_HPP_
#include <mysql.h>
#include <memory>
#include <rfl.hpp>
#include <stdexcept>
#include <string>
#include "../IteratorBase.hpp"
#include "../Ref.hpp"
#include "../Result.hpp"
#include "../Transaction.hpp"
#include "../dynamic/Column.hpp"
#include "../dynamic/Statement.hpp"
#include "../dynamic/Write.hpp"
#include "../is_connection.hpp"
#include "Credentials.hpp"
#include "exec.hpp"
#include "to_sql.hpp"
namespace sqlgen::mysql {
class Connection {
using ConnPtr = Ref<MYSQL>;
using StmtPtr = std::shared_ptr<MYSQL_STMT>;
public:
Connection(const Credentials& _credentials)
: conn_(make_conn(_credentials)) {}
static rfl::Result<Ref<Connection>> make(
const Credentials& _credentials) noexcept;
~Connection() = default;
Result<Nothing> begin_transaction() noexcept {
return execute("START TRANSACTION;");
}
Result<Nothing> commit() noexcept { return execute("COMMIT;"); }
Result<Nothing> execute(const std::string& _sql) noexcept {
return exec(conn_, _sql);
}
Result<Nothing> insert(
const dynamic::Insert& _stmt,
const std::vector<std::vector<std::optional<std::string>>>&
_data) noexcept;
Result<Ref<IteratorBase>> read(const dynamic::SelectFrom& _query);
Result<Nothing> rollback() noexcept { return execute("ROLLBACK;"); }
std::string to_sql(const dynamic::Statement& _stmt) noexcept {
return to_sql_impl(_stmt);
}
Result<Nothing> start_write(const dynamic::Write& _stmt);
Result<Nothing> write(
const std::vector<std::vector<std::optional<std::string>>>& _data);
Result<Nothing> end_write();
private:
/// Actually inserts data based on a prepared statement -
/// used by both .insert(...) and .write(...).
Result<Nothing> actual_insert(
const std::vector<std::vector<std::optional<std::string>>>& _data,
MYSQL_STMT* _stmt) const noexcept;
static ConnPtr make_conn(const Credentials& _credentials);
Result<StmtPtr> prepare_statement(
const std::variant<dynamic::Insert, dynamic::Write>& _stmt)
const noexcept;
private:
/// A prepared statement - needed for the read and write operations. Note that
/// we have declared it before conn_, meaning it will be destroyed first.
StmtPtr stmt_;
/// The underlying connection.
ConnPtr conn_;
};
static_assert(is_connection<Connection>,
"Must fulfill the is_connection concept.");
static_assert(is_connection<Transaction<Connection>>,
"Must fulfill the is_connection concept.");
} // namespace sqlgen::mysql
#endif

View File

@@ -0,0 +1,19 @@
#ifndef SQLGEN_MYSQL_CREDENTIALS_HPP_
#define SQLGEN_MYSQL_CREDENTIALS_HPP_
#include <string>
namespace sqlgen::mysql {
struct Credentials {
std::string host;
std::string user;
std::string password;
std::string dbname = "mysql";
int port = 3306;
std::string unix_socket = "/var/run/mysqld/mysqld.sock";
};
} // namespace sqlgen::mysql
#endif

View File

@@ -0,0 +1,50 @@
#ifndef SQLGEN_MYSQL_ITERATOR_HPP_
#define SQLGEN_MYSQL_ITERATOR_HPP_
#include <mysql.h>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "../IteratorBase.hpp"
#include "../Ref.hpp"
#include "../Result.hpp"
#include "Connection.hpp"
namespace sqlgen::mysql {
class Iterator : public sqlgen::IteratorBase {
using ConnPtr = Ref<MYSQL>;
using ResPtr = Ref<MYSQL_RES>;
public:
Iterator(const ResPtr& _res, const ConnPtr& _conn);
~Iterator();
/// Whether the end of the available data has been reached.
bool end() const final { return end_; }
/// Returns the next batch of rows.
/// If _batch_size is greater than the number of rows left, returns all
/// of the rows left.
Result<std::vector<std::vector<std::optional<std::string>>>> next(
const size_t _batch_size) final;
private:
/// The underlying mysql result.
ResPtr res_;
/// The underlying mysql connection. We have this in here to prevent its
/// destruction for the lifetime of the iterator.
ConnPtr conn_;
/// Whether the end is reached.
bool end_;
};
} // namespace sqlgen::mysql
#endif

View File

@@ -0,0 +1,17 @@
#ifndef SQLGEN_MYSQL_CONNECT_HPP_
#define SQLGEN_MYSQL_CONNECT_HPP_
#include <string>
#include "Connection.hpp"
#include "Credentials.hpp"
namespace sqlgen::mysql {
inline auto connect(const Credentials& _credentials) {
return Connection::make(_credentials);
}
} // namespace sqlgen::mysql
#endif

View File

@@ -0,0 +1,17 @@
#ifndef SQLGEN_MYSQL_EXEC_HPP_
#define SQLGEN_MYSQL_EXEC_HPP_
#include <mysql.h>
#include <string>
#include "../Ref.hpp"
#include "../Result.hpp"
namespace sqlgen::mysql {
Result<Nothing> exec(const Ref<MYSQL>& _conn, const std::string& _sql) noexcept;
} // namespace sqlgen::mysql
#endif

View File

@@ -0,0 +1,22 @@
#ifndef SQLGEN_MYSQL_MAKE_ERROR_HPP_
#define SQLGEN_MYSQL_MAKE_ERROR_HPP_
#include <mysql.h>
#include <rfl.hpp>
#include <string>
#include "../Ref.hpp"
#include "../Result.hpp"
namespace sqlgen::mysql {
inline rfl::Unexpected<Error> make_error(const Ref<MYSQL>& _conn) noexcept {
return error("MySQL error (" + std::to_string(mysql_errno(_conn.get())) +
") [" + mysql_sqlstate(_conn.get()) + "] " +
mysql_error(_conn.get()));
}
} // namespace sqlgen::mysql
#endif

View File

@@ -0,0 +1,27 @@
#ifndef SQLGEN_MYSQL_TO_SQL_HPP_
#define SQLGEN_MYSQL_TO_SQL_HPP_
#include <string>
#include <type_traits>
#include "../dynamic/Statement.hpp"
#include "../transpilation/to_sql.hpp"
namespace sqlgen::mysql {
/// Transpiles a dynamic general SQL statement to the mysql dialect.
std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept;
/// Transpiles any SQL statement to the mysql dialect.
template <class T>
std::string to_sql(const T& _t) noexcept {
if constexpr (std::is_same_v<std::remove_cvref_t<T>, dynamic::Statement>) {
return to_sql_impl(_t);
} else {
return to_sql_impl(transpilation::to_sql(_t));
}
}
} // namespace sqlgen::mysql
#endif

View File

@@ -29,10 +29,16 @@ struct Parser<Timestamp<_format>> {
static dynamic::Type to_type() noexcept {
const std::string format = typename TSType::Format().str();
if (format.find("%z") == std::string::npos) {
if (format.find("%z") != std::string::npos) {
return dynamic::types::TimestampWithTZ{};
} else if (format.find("%H") != std::string::npos ||
format.find("%M") != std::string::npos ||
format.find("%S") != std::string::npos ||
format.find("%R") != std::string::npos ||
format.find("%T") != std::string::npos) {
return dynamic::types::Timestamp{};
} else {
return dynamic::types::TimestampWithTZ{};
return dynamic::types::Date{};
}
}
};

View File

@@ -1,5 +1,7 @@
#include "sqlgen/internal/strings/strings.hpp"
#include <algorithm>
namespace sqlgen::internal::strings {
char to_lower(const char ch) {
@@ -71,4 +73,26 @@ std::vector<std::string> split(const std::string& _str,
return result;
}
std::string ltrim(const std::string& _str, const std::string& _chars) {
auto str = _str;
str.erase(str.begin(), std::find_if(str.begin(), str.end(), [&](char ch1) {
return std::none_of(_chars.begin(), _chars.end(),
[&](char ch2) { return ch1 == ch2; });
}));
return str;
}
std::string rtrim(const std::string& _str, const std::string& _chars) {
auto str = _str;
str.erase(std::find_if(str.rbegin(), str.rend(),
[&](char ch1) {
return std::none_of(
_chars.begin(), _chars.end(),
[&](char ch2) { return ch1 == ch2; });
})
.base(),
str.end());
return str;
}
} // namespace sqlgen::internal::strings

View File

@@ -0,0 +1,178 @@
#include "sqlgen/mysql/Connection.hpp"
#include <cstring>
#include <ranges>
#include <rfl.hpp>
#include <sstream>
#include <stdexcept>
#include <vector>
#include "sqlgen/internal/collect/vector.hpp"
#include "sqlgen/internal/strings/strings.hpp"
#include "sqlgen/mysql/Iterator.hpp"
#include "sqlgen/mysql/make_error.hpp"
namespace sqlgen::mysql {
Result<Nothing> Connection::actual_insert(
const std::vector<std::vector<std::optional<std::string>>>& _data,
MYSQL_STMT* _stmt) const noexcept {
const auto num_params = static_cast<size_t>(mysql_stmt_param_count(_stmt));
std::vector<MYSQL_BIND> bind(num_params);
std::vector<long unsigned int> lengths(num_params);
std::vector<my_bool> is_null(num_params);
for (const auto& row : _data) {
memset(bind.data(), 0, sizeof(MYSQL_BIND) * num_params);
if (row.size() != num_params) {
return error("Expected " + std::to_string(num_params) + " fields, got " +
std::to_string(row.size()) + ".");
}
auto buffer = row;
for (size_t i = 0; i < num_params; ++i) {
if (buffer[i]) {
lengths[i] = static_cast<long unsigned int>(row[i]->size());
is_null[i] = 0;
bind[i].buffer_type = MYSQL_TYPE_STRING;
bind[i].buffer = &((*buffer[i])[0]);
bind[i].buffer_length = lengths[i];
bind[i].is_null = &(is_null[i]);
bind[i].length = &(lengths[i]);
} else {
lengths[i] = 0;
is_null[i] = 1;
bind[i].buffer_type = MYSQL_TYPE_NULL;
bind[i].buffer = nullptr;
bind[i].buffer_length = 0;
bind[i].is_null = &(is_null[i]);
bind[i].length = 0;
}
}
auto err = mysql_stmt_bind_param(_stmt, bind.data());
if (err) {
return make_error(conn_);
}
err = mysql_stmt_execute(_stmt);
if (err) {
return make_error(conn_);
}
}
return Nothing{};
}
Result<Nothing> Connection::insert(
const dynamic::Insert& _stmt,
const std::vector<std::vector<std::optional<std::string>>>&
_data) noexcept {
if (_data.size() == 0) {
return Nothing{};
}
return prepare_statement(_stmt).and_then(
[&](auto&& _stmt_ptr) { return actual_insert(_data, _stmt_ptr.get()); });
}
rfl::Result<Ref<Connection>> Connection::make(
const Credentials& _credentials) noexcept {
try {
return Ref<Connection>::make(_credentials);
} catch (std::exception& e) {
return error(e.what());
}
}
typename Connection::ConnPtr Connection::make_conn(
const Credentials& _credentials) {
const auto raw_ptr = mysql_init(nullptr);
const auto shared_ptr = std::shared_ptr<MYSQL>(raw_ptr, mysql_close);
const auto res = mysql_real_connect(
shared_ptr.get(), _credentials.host.c_str(), _credentials.user.c_str(),
_credentials.password.c_str(), _credentials.dbname.c_str(),
_credentials.port, _credentials.unix_socket.c_str(),
CLIENT_MULTI_STATEMENTS);
if (!res) {
throw std::runtime_error(
make_error(ConnPtr::make(shared_ptr).value()).error().what());
}
return ConnPtr::make(shared_ptr).value();
}
Result<Connection::StmtPtr> Connection::prepare_statement(
const std::variant<dynamic::Insert, dynamic::Write>& _stmt) const noexcept {
const auto sql = std::visit(to_sql_impl, _stmt);
const auto stmt_ptr = StmtPtr(mysql_stmt_init(conn_.get()), mysql_stmt_close);
const auto err = mysql_stmt_prepare(stmt_ptr.get(), sql.c_str(),
static_cast<unsigned long>(sql.size()));
if (err) {
return make_error(conn_);
}
return stmt_ptr;
}
Result<Ref<IteratorBase>> Connection::read(const dynamic::SelectFrom& _query) {
const auto sql = mysql::to_sql_impl(_query);
const auto err =
mysql_real_query(conn_.get(), sql.c_str(), static_cast<int>(sql.size()));
if (err) {
return make_error(conn_);
}
const auto raw_ptr = mysql_use_result(conn_.get());
if (!raw_ptr) {
return make_error(conn_);
}
return Ref<MYSQL_RES>::make(
std::shared_ptr<MYSQL_RES>(raw_ptr, mysql_free_result))
.transform([&](auto&& _res) { return Ref<Iterator>::make(_res, conn_); });
}
Result<Nothing> Connection::start_write(const dynamic::Write& _write_stmt) {
if (stmt_) {
return error(
"A write operation has already been launched. You need to call "
".end_write() before you can start another.");
}
return begin_transaction()
.and_then([&](auto&&) { return prepare_statement(_write_stmt); })
.transform([&](auto&& _stmt) {
stmt_ = _stmt;
return Nothing{};
})
.or_else([&](auto&& _err) {
rollback();
return error(_err.what());
});
}
Result<Nothing> Connection::write(
const std::vector<std::vector<std::optional<std::string>>>& _data) {
if (!stmt_) {
return error(
" You need to call .start_write(...) before you can call "
".write(...).");
}
return actual_insert(_data, stmt_.get()).or_else([&](const auto& _err) {
rollback();
stmt_ = nullptr;
return error(_err.what());
});
}
Result<Nothing> Connection::end_write() {
stmt_ = nullptr;
return commit();
}
} // namespace sqlgen::mysql

View File

@@ -0,0 +1,44 @@
#include "sqlgen/mysql/Iterator.hpp"
namespace sqlgen::mysql {
Iterator::Iterator(const ResPtr& _res, const ConnPtr& _conn)
: res_(_res), conn_(_conn), end_(false) {}
Iterator::~Iterator() = default;
Result<std::vector<std::vector<std::optional<std::string>>>> Iterator::next(
const size_t _batch_size) {
std::vector<std::vector<std::optional<std::string>>> vec;
const unsigned int num_fields = mysql_num_fields(res_.get());
for (size_t i = 0; i < _batch_size; ++i) {
MYSQL_ROW row = mysql_fetch_row(res_.get());
if (!row) {
const auto err = mysql_error(conn_.get());
if (*err) {
return error(err);
}
end_ = true;
return vec;
}
std::vector<std::optional<std::string>> res_row(num_fields);
for (unsigned int j = 0; j < num_fields; ++j) {
if (row[j]) {
res_row[j] = std::string(row[j]);
} else {
res_row[j] = std::nullopt;
}
}
vec.emplace_back(std::move(res_row));
}
return vec;
}
} // namespace sqlgen::mysql

30
src/sqlgen/mysql/exec.cpp Normal file
View File

@@ -0,0 +1,30 @@
#include "sqlgen/mysql/exec.hpp"
#include <ranges>
#include <rfl.hpp>
#include <sstream>
#include <stdexcept>
#include "sqlgen/mysql/make_error.hpp"
namespace sqlgen::mysql {
Result<Nothing> exec(const Ref<MYSQL>& _conn,
const std::string& _sql) noexcept {
const auto err = mysql_real_query(_conn.get(), _sql.c_str(),
static_cast<int>(_sql.size()));
if (err) {
return make_error(_conn);
}
const auto raw_ptr = mysql_store_result(_conn.get());
if (raw_ptr) {
mysql_free_result(raw_ptr);
}
return Nothing{};
}
} // namespace sqlgen::mysql

809
src/sqlgen/mysql/to_sql.cpp Normal file
View File

@@ -0,0 +1,809 @@
#include "sqlgen/mysql/to_sql.hpp"
#include <ranges>
#include <rfl.hpp>
#include <sstream>
#include <stdexcept>
#include <type_traits>
#include "sqlgen/dynamic/Join.hpp"
#include "sqlgen/dynamic/Operation.hpp"
#include "sqlgen/internal/collect/vector.hpp"
#include "sqlgen/internal/strings/strings.hpp"
namespace sqlgen::mysql {
std::string aggregation_to_sql(
const dynamic::Aggregation& _aggregation) noexcept;
std::string cast_type_to_sql(const dynamic::Type& _type) noexcept;
std::string column_or_value_to_sql(const dynamic::ColumnOrValue& _col) noexcept;
std::string condition_to_sql(const dynamic::Condition& _cond) noexcept;
template <class ConditionType>
std::string condition_to_sql_impl(const ConditionType& _condition) noexcept;
std::string column_to_sql_definition(const dynamic::Column& _col) noexcept;
std::string create_index_to_sql(const dynamic::CreateIndex& _stmt) noexcept;
std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept;
std::string date_plus_duration_to_sql(
const dynamic::Operation::DatePlusDuration& _stmt,
const size_t _ix = 0) noexcept;
std::string delete_from_to_sql(const dynamic::DeleteFrom& _stmt) noexcept;
std::string drop_to_sql(const dynamic::Drop& _stmt) noexcept;
std::string escape_single_quote(const std::string& _str) noexcept;
std::string field_to_str(const dynamic::SelectFrom::Field& _field) noexcept;
std::vector<std::string> get_primary_keys(
const dynamic::CreateTable& _stmt) noexcept;
template <class InsertOrWrite>
std::string insert_or_write_to_sql(const InsertOrWrite& _stmt) noexcept;
std::string join_to_sql(const dynamic::Join& _stmt) noexcept;
std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept;
std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept;
std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept;
std::string type_to_sql(const dynamic::Type& _type) noexcept;
std::string update_to_sql(const dynamic::Update& _stmt) noexcept;
// ----------------------------------------------------------------------------
inline std::string get_name(const dynamic::Column& _col) { return _col.name; }
inline std::string wrap_in_quotes(const std::string& _name) noexcept {
return "`" + _name + "`";
}
// ----------------------------------------------------------------------------
std::string aggregation_to_sql(
const dynamic::Aggregation& _aggregation) noexcept {
return _aggregation.val.visit([](const auto& _agg) -> std::string {
using Type = std::remove_cvref_t<decltype(_agg)>;
std::stringstream stream;
if constexpr (std::is_same_v<Type, dynamic::Aggregation::Avg>) {
stream << "AVG(" << operation_to_sql(*_agg.val) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Aggregation::Count>) {
const auto val =
std::string(_agg.val && _agg.distinct ? "DISTINCT " : "") +
(_agg.val ? column_or_value_to_sql(*_agg.val) : std::string("*"));
stream << "COUNT(" << val << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Aggregation::Max>) {
stream << "MAX(" << operation_to_sql(*_agg.val) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Aggregation::Min>) {
stream << "MIN(" << operation_to_sql(*_agg.val) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Aggregation::Sum>) {
stream << "SUM(" << operation_to_sql(*_agg.val) << ")";
} else {
static_assert(rfl::always_false_v<Type>, "Not all cases were covered.");
}
return stream.str();
});
}
std::string cast_type_to_sql(const dynamic::Type& _type) noexcept {
return _type.visit([](const auto _t) -> std::string {
using T = std::remove_cvref_t<decltype(_t)>;
if constexpr (std::is_same_v<T, dynamic::types::Boolean>) {
return "BOOLEAN";
} else if constexpr (std::is_same_v<T, dynamic::types::Int8> ||
std::is_same_v<T, dynamic::types::Int16> ||
std::is_same_v<T, dynamic::types::Int32> ||
std::is_same_v<T, dynamic::types::Int64>) {
return "SIGNED";
} else if constexpr (std::is_same_v<T, dynamic::types::UInt8> ||
std::is_same_v<T, dynamic::types::UInt16> ||
std::is_same_v<T, dynamic::types::UInt32> ||
std::is_same_v<T, dynamic::types::UInt64>) {
return "UNSIGNED";
} else if constexpr (std::is_same_v<T, dynamic::types::Float32> ||
std::is_same_v<T, dynamic::types::Float64>) {
return "DECIMAL";
} else if constexpr (std::is_same_v<T, dynamic::types::Text> ||
std::is_same_v<T, dynamic::types::VarChar>) {
return "CHAR";
} else if constexpr (std::is_same_v<T, dynamic::types::Date>) {
return "DATE";
} else if constexpr (std::is_same_v<T, dynamic::types::Timestamp> ||
std::is_same_v<T, dynamic::types::TimestampWithTZ>) {
return "DATETIME";
} else if constexpr (std::is_same_v<T, dynamic::types::Unknown>) {
return "CHAR";
} else {
static_assert(rfl::always_false_v<T>, "Not all cases were covered.");
}
});
}
std::string column_or_value_to_sql(
const dynamic::ColumnOrValue& _col) noexcept {
const auto handle_value = [](const auto& _v) -> std::string {
using Type = std::remove_cvref_t<decltype(_v)>;
if constexpr (std::is_same_v<Type, dynamic::String>) {
return "'" + escape_single_quote(_v.val) + "'";
} else if constexpr (std::is_same_v<Type, dynamic::Duration>) {
const auto unit =
_v.unit == dynamic::TimeUnit::milliseconds
? std::string("* 1000 microsecond")
: internal::strings::rtrim(rfl::enum_to_string(_v.unit), "s");
return "INTERVAL " + std::to_string(_v.val) + " " + unit;
} else if constexpr (std::is_same_v<Type, dynamic::Timestamp>) {
return "to_timestamp(" + std::to_string(_v.seconds_since_unix) + ")";
} else {
return std::to_string(_v.val);
}
};
return _col.visit([&](const auto& _c) -> std::string {
using Type = std::remove_cvref_t<decltype(_c)>;
if constexpr (std::is_same_v<Type, dynamic::Column>) {
if (_c.alias) {
return *_c.alias + "." + wrap_in_quotes(_c.name);
} else {
return wrap_in_quotes(_c.name);
}
} else {
return _c.val.visit(handle_value);
}
});
}
std::string condition_to_sql(const dynamic::Condition& _cond) noexcept {
return _cond.val.visit(
[&](const auto& _c) { return condition_to_sql_impl(_c); });
}
template <class ConditionType>
std::string condition_to_sql_impl(const ConditionType& _condition) noexcept {
using C = std::remove_cvref_t<ConditionType>;
std::stringstream stream;
if constexpr (std::is_same_v<C, dynamic::Condition::And>) {
stream << "(" << condition_to_sql(*_condition.cond1) << ") AND ("
<< condition_to_sql(*_condition.cond2) << ")";
} else if constexpr (std::is_same_v<C, dynamic::Condition::Equal>) {
stream << operation_to_sql(_condition.op1) << " = "
<< operation_to_sql(_condition.op2);
} else if constexpr (std::is_same_v<C, dynamic::Condition::GreaterEqual>) {
stream << operation_to_sql(_condition.op1)
<< " >= " << operation_to_sql(_condition.op2);
} else if constexpr (std::is_same_v<C, dynamic::Condition::GreaterThan>) {
stream << operation_to_sql(_condition.op1) << " > "
<< operation_to_sql(_condition.op2);
} else if constexpr (std::is_same_v<C, dynamic::Condition::IsNull>) {
stream << operation_to_sql(_condition.op) << " IS NULL";
} else if constexpr (std::is_same_v<C, dynamic::Condition::IsNotNull>) {
stream << operation_to_sql(_condition.op) << " IS NOT NULL";
} else if constexpr (std::is_same_v<C, dynamic::Condition::LesserEqual>) {
stream << operation_to_sql(_condition.op1)
<< " <= " << operation_to_sql(_condition.op2);
} else if constexpr (std::is_same_v<C, dynamic::Condition::LesserThan>) {
stream << operation_to_sql(_condition.op1) << " < "
<< operation_to_sql(_condition.op2);
} else if constexpr (std::is_same_v<C, dynamic::Condition::Like>) {
stream << operation_to_sql(_condition.op) << " LIKE "
<< column_or_value_to_sql(_condition.pattern);
} else if constexpr (std::is_same_v<C, dynamic::Condition::Not>) {
stream << "NOT (" << condition_to_sql(*_condition.cond) << ")";
} else if constexpr (std::is_same_v<C, dynamic::Condition::NotEqual>) {
stream << operation_to_sql(_condition.op1)
<< " != " << operation_to_sql(_condition.op2);
} else if constexpr (std::is_same_v<C, dynamic::Condition::NotLike>) {
stream << operation_to_sql(_condition.op) << " NOT LIKE "
<< column_or_value_to_sql(_condition.pattern);
} else if constexpr (std::is_same_v<C, dynamic::Condition::Or>) {
stream << "(" << condition_to_sql(*_condition.cond1) << ") OR ("
<< condition_to_sql(*_condition.cond2) << ")";
} else {
static_assert(rfl::always_false_v<C>, "Not all cases were covered.");
}
return stream.str();
}
std::string column_to_sql_definition(const dynamic::Column& _col) noexcept {
return wrap_in_quotes(_col.name) + " " + type_to_sql(_col.type) +
properties_to_sql(
_col.type.visit([](const auto& _t) { return _t.properties; }));
}
std::string create_index_to_sql(const dynamic::CreateIndex& _stmt) noexcept {
using namespace std::ranges::views;
std::stringstream stream;
if (_stmt.unique) {
stream << "CREATE UNIQUE INDEX ";
} else {
stream << "CREATE INDEX ";
}
if (_stmt.if_not_exists) {
stream << "IF NOT EXISTS ";
}
stream << wrap_in_quotes(_stmt.name) << " ";
stream << "ON ";
if (_stmt.table.schema) {
stream << wrap_in_quotes(*_stmt.table.schema) << ".";
}
stream << wrap_in_quotes(_stmt.table.name);
stream << "(";
stream << internal::strings::join(
", ",
internal::collect::vector(_stmt.columns | transform(wrap_in_quotes)));
stream << ")";
if (_stmt.where) {
stream << " WHERE " << condition_to_sql(*_stmt.where);
}
stream << ";";
return stream.str();
}
std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept {
using namespace std::ranges::views;
const auto col_to_sql = [&](const auto& _col) {
return column_to_sql_definition(_col);
};
std::stringstream stream;
stream << "CREATE TABLE ";
if (_stmt.if_not_exists) {
stream << "IF NOT EXISTS ";
}
if (_stmt.table.schema) {
stream << wrap_in_quotes(*_stmt.table.schema) << ".";
}
stream << wrap_in_quotes(_stmt.table.name) << " ";
stream << "(";
stream << internal::strings::join(
", ", internal::collect::vector(_stmt.columns | transform(col_to_sql)));
const auto primary_keys = get_primary_keys(_stmt);
if (primary_keys.size() != 0) {
stream << ", PRIMARY KEY (" << internal::strings::join(", ", primary_keys)
<< ")";
}
stream << ");";
return stream.str();
}
std::string date_plus_duration_to_sql(
const dynamic::Operation::DatePlusDuration& _stmt,
const size_t _ix) noexcept {
using namespace std::ranges::views;
std::stringstream stream;
stream << internal::strings::join(
"",
internal::collect::vector(
_stmt.durations | transform([](const auto&) -> std::string {
return "date_add(";
})))
<< operation_to_sql(*_stmt.date) << ", "
<< internal::strings::join(
"), ", internal::collect::vector(
_stmt.durations | transform([](const auto& _d) {
return column_or_value_to_sql(dynamic::Value{_d});
})))
<< ")";
return stream.str();
}
std::string delete_from_to_sql(const dynamic::DeleteFrom& _stmt) noexcept {
std::stringstream stream;
stream << "DELETE FROM ";
if (_stmt.table.schema) {
stream << wrap_in_quotes(*_stmt.table.schema) << ".";
}
stream << wrap_in_quotes(_stmt.table.name);
if (_stmt.where) {
stream << " WHERE " << condition_to_sql(*_stmt.where);
}
stream << ";";
return stream.str();
}
std::string drop_to_sql(const dynamic::Drop& _stmt) noexcept {
std::stringstream stream;
stream << "DROP TABLE ";
if (_stmt.if_exists) {
stream << "IF EXISTS ";
}
if (_stmt.table.schema) {
stream << wrap_in_quotes(*_stmt.table.schema) << ".";
}
stream << wrap_in_quotes(_stmt.table.name);
stream << ";";
return stream.str();
}
std::string escape_single_quote(const std::string& _str) noexcept {
return internal::strings::replace_all(_str, "'", "''");
}
std::string field_to_str(const dynamic::SelectFrom::Field& _field) noexcept {
std::stringstream stream;
stream << operation_to_sql(_field.val);
if (_field.as) {
stream << " AS " << wrap_in_quotes(*_field.as);
}
return stream.str();
}
std::vector<std::string> get_primary_keys(
const dynamic::CreateTable& _stmt) noexcept {
using namespace std::ranges::views;
const auto is_primary_key = [](const auto& _col) -> bool {
return _col.type.visit(
[](const auto& _t) -> bool { return _t.properties.primary; });
};
return internal::collect::vector(_stmt.columns | filter(is_primary_key) |
transform(get_name) |
transform(wrap_in_quotes));
}
template <class InsertOrWrite>
std::string insert_or_write_to_sql(const InsertOrWrite& _stmt) noexcept {
using namespace std::ranges::views;
std::stringstream stream;
stream << "INSERT INTO ";
if (_stmt.table.schema) {
stream << wrap_in_quotes(*_stmt.table.schema) << ".";
}
stream << wrap_in_quotes(_stmt.table.name);
stream << " (";
stream << internal::strings::join(
", ",
internal::collect::vector(_stmt.columns | transform(wrap_in_quotes)));
stream << ")";
stream << " VALUES (";
stream << internal::strings::join(
", ", internal::collect::vector(
_stmt.columns |
transform([](const auto&) -> std::string { return "?"; })));
stream << ");";
return stream.str();
}
std::string join_to_sql(const dynamic::Join& _stmt) noexcept {
std::stringstream stream;
stream << internal::strings::to_upper(internal::strings::replace_all(
rfl::enum_to_string(_stmt.how), "_", " "))
<< " ";
stream << _stmt.table_or_query.visit(
[](const auto& _table_or_query) -> std::string {
using Type = std::remove_cvref_t<decltype(_table_or_query)>;
if constexpr (std::is_same_v<Type, dynamic::Table>) {
return wrap_in_quotes(_table_or_query.name);
} else {
return "(" + select_from_to_sql(*_table_or_query) + ")";
}
})
<< " ";
stream << _stmt.alias << " ";
if (_stmt.on) {
stream << "ON " << condition_to_sql(*_stmt.on);
} else {
stream << "ON 1 = 1";
}
return stream.str();
}
std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept {
using namespace std::ranges::views;
return _stmt.val.visit([](const auto& _s) -> std::string {
using Type = std::remove_cvref_t<decltype(_s)>;
std::stringstream stream;
if constexpr (std::is_same_v<Type, dynamic::Operation::Abs>) {
stream << "abs(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Aggregation>) {
stream << aggregation_to_sql(_s);
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Cast>) {
stream << "cast(" << operation_to_sql(*_s.op1) << " as "
<< cast_type_to_sql(_s.target_type) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Coalesce>) {
stream << "coalesce("
<< internal::strings::join(
", ", internal::collect::vector(
_s.ops | transform([](const auto& _op) {
return operation_to_sql(*_op);
})))
<< ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Ceil>) {
stream << "ceil(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Column>) {
stream << column_or_value_to_sql(_s);
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Concat>) {
stream << "concat("
<< internal::strings::join(
", ", internal::collect::vector(
_s.ops | transform([](const auto& _op) {
return operation_to_sql(*_op);
})))
<< ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Cos>) {
stream << "cos(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type,
dynamic::Operation::DatePlusDuration>) {
return date_plus_duration_to_sql(_s);
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Day>) {
stream << "extract(DAY from " << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type,
dynamic::Operation::DaysBetween>) {
stream << "datediff(" << operation_to_sql(*_s.op2) << ", "
<< operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Divides>) {
stream << "(" << operation_to_sql(*_s.op1) << ") / ("
<< operation_to_sql(*_s.op2) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Exp>) {
stream << "exp(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Floor>) {
stream << "floor(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Hour>) {
stream << "extract(HOUR from " << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Length>) {
stream << "length(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Ln>) {
stream << "ln(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Log2>) {
stream << "log2( " << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Lower>) {
stream << "lower(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::LTrim>) {
stream << "trim(leading " << operation_to_sql(*_s.op2) << " FROM "
<< operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Minus>) {
stream << "(" << operation_to_sql(*_s.op1) << ") - ("
<< operation_to_sql(*_s.op2) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Minute>) {
stream << "extract(MINUTE from " << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Mod>) {
stream << "mod(" << operation_to_sql(*_s.op1) << ", "
<< operation_to_sql(*_s.op2) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Month>) {
stream << "extract(MONTH from " << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Multiplies>) {
stream << "(" << operation_to_sql(*_s.op1) << ") * ("
<< operation_to_sql(*_s.op2) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Plus>) {
stream << "(" << operation_to_sql(*_s.op1) << ") + ("
<< operation_to_sql(*_s.op2) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Replace>) {
stream << "replace(" << operation_to_sql(*_s.op1) << ", "
<< operation_to_sql(*_s.op2) << ", " << operation_to_sql(*_s.op3)
<< ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Round>) {
stream << "round(" << operation_to_sql(*_s.op1) << ", "
<< operation_to_sql(*_s.op2) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::RTrim>) {
stream << "trim(trailing " << operation_to_sql(*_s.op2) << " FROM "
<< operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Second>) {
stream << "extract(SECOND from " << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Sin>) {
stream << "sin(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Sqrt>) {
stream << "sqrt(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Tan>) {
stream << "tan(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Trim>) {
stream << "trim(both " << operation_to_sql(*_s.op2) << " FROM "
<< operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Unixepoch>) {
stream << "unix_timestamp(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Upper>) {
stream << "upper(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Value>) {
stream << column_or_value_to_sql(_s);
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Weekday>) {
stream << "dayofweek(" << operation_to_sql(*_s.op1) << ")";
} else if constexpr (std::is_same_v<Type, dynamic::Operation::Year>) {
stream << "extract(YEAR from " << operation_to_sql(*_s.op1) << ")";
} else {
static_assert(rfl::always_false_v<Type>, "Unsupported type.");
}
return stream.str();
});
}
std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept {
if (_p.auto_incr) {
return " AUTO_INCREMENT";
} else if (!_p.nullable) {
return " NOT NULL";
} else {
return "";
}
}
std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept {
using namespace std::ranges::views;
const auto order_by_to_str = [](const auto& _w) -> std::string {
return column_or_value_to_sql(_w.column) + (_w.desc ? " DESC" : "");
};
std::stringstream stream;
stream << "SELECT ";
stream << internal::strings::join(
", ", internal::collect::vector(_stmt.fields | transform(field_to_str)));
stream << " FROM ";
if (_stmt.table.schema) {
stream << wrap_in_quotes(*_stmt.table.schema) << ".";
}
stream << wrap_in_quotes(_stmt.table.name);
if (_stmt.alias) {
stream << " " << *_stmt.alias;
}
if (_stmt.joins) {
stream << " "
<< internal::strings::join(
" ", internal::collect::vector(*_stmt.joins |
transform(join_to_sql)));
}
if (_stmt.where) {
stream << " WHERE " << condition_to_sql(*_stmt.where);
}
if (_stmt.group_by) {
stream << " GROUP BY "
<< internal::strings::join(
", ",
internal::collect::vector(_stmt.group_by->columns |
transform(column_or_value_to_sql)));
}
if (_stmt.order_by) {
stream << " ORDER BY "
<< internal::strings::join(
", ", internal::collect::vector(_stmt.order_by->columns |
transform(order_by_to_str)));
}
if (_stmt.limit) {
stream << " LIMIT " << _stmt.limit->val;
}
return stream.str();
}
std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept {
return _stmt.visit([&](const auto& _s) -> std::string {
using S = std::remove_cvref_t<decltype(_s)>;
if constexpr (std::is_same_v<S, dynamic::CreateIndex>) {
return create_index_to_sql(_s);
} else if constexpr (std::is_same_v<S, dynamic::CreateTable>) {
return create_table_to_sql(_s);
} else if constexpr (std::is_same_v<S, dynamic::DeleteFrom>) {
return delete_from_to_sql(_s);
} else if constexpr (std::is_same_v<S, dynamic::Drop>) {
return drop_to_sql(_s);
} else if constexpr (std::is_same_v<S, dynamic::Insert> ||
std::is_same_v<S, dynamic::Write>) {
return insert_or_write_to_sql(_s);
} else if constexpr (std::is_same_v<S, dynamic::SelectFrom>) {
return select_from_to_sql(_s);
} else if constexpr (std::is_same_v<S, dynamic::Update>) {
return update_to_sql(_s);
} else {
static_assert(rfl::always_false_v<S>, "Unsupported type.");
}
});
}
std::string type_to_sql(const dynamic::Type& _type) noexcept {
return _type.visit([](const auto _t) -> std::string {
using T = std::remove_cvref_t<decltype(_t)>;
if constexpr (std::is_same_v<T, dynamic::types::Boolean>) {
return "BOOLEAN";
} else if constexpr (std::is_same_v<T, dynamic::types::Int8>) {
return "TINYINT";
} else if constexpr (std::is_same_v<T, dynamic::types::UInt8> ||
std::is_same_v<T, dynamic::types::Int16>) {
return "SMALLINT";
} else if constexpr (std::is_same_v<T, dynamic::types::UInt16> ||
std::is_same_v<T, dynamic::types::Int32>) {
return "INT";
} else if constexpr (std::is_same_v<T, dynamic::types::UInt32> ||
std::is_same_v<T, dynamic::types::Int64> ||
std::is_same_v<T, dynamic::types::UInt64>) {
return "BIGINT";
} else if constexpr (std::is_same_v<T, dynamic::types::Float32> ||
std::is_same_v<T, dynamic::types::Float64>) {
return "DECIMAL";
} else if constexpr (std::is_same_v<T, dynamic::types::Text>) {
return "TEXT";
} else if constexpr (std::is_same_v<T, dynamic::types::VarChar>) {
return "VARCHAR(" + std::to_string(_t.length) + ")";
} else if constexpr (std::is_same_v<T, dynamic::types::Date>) {
return "DATE";
} else if constexpr (std::is_same_v<T, dynamic::types::Timestamp> ||
std::is_same_v<T, dynamic::types::TimestampWithTZ>) {
return "DATETIME";
} else if constexpr (std::is_same_v<T, dynamic::types::Unknown>) {
return "TEXT";
} else {
static_assert(rfl::always_false_v<T>, "Not all cases were covered.");
}
});
}
std::string update_to_sql(const dynamic::Update& _stmt) noexcept {
using namespace std::ranges::views;
const auto to_str = [](const auto& _set) -> std::string {
return wrap_in_quotes(_set.col.name) + " = " +
column_or_value_to_sql(_set.to);
};
std::stringstream stream;
stream << "UPDATE ";
if (_stmt.table.schema) {
stream << wrap_in_quotes(*_stmt.table.schema) << ".";
}
stream << wrap_in_quotes(_stmt.table.name);
stream << " SET ";
stream << internal::strings::join(
", ", internal::collect::vector(_stmt.sets | transform(to_str)));
if (_stmt.where) {
stream << " WHERE " << condition_to_sql(*_stmt.where);
}
stream << ";";
return stream.str();
}
} // namespace sqlgen::mysql

View File

@@ -58,19 +58,15 @@ Result<std::vector<std::vector<std::optional<std::string>>>> Iterator::next(
return vec;
};
const auto shutdown_if_necessary =
[this](std::vector<std::vector<std::optional<std::string>>>&& _vec)
-> std::vector<std::vector<std::optional<std::string>>> {
if (_vec.size() == 0) {
shutdown();
}
return std::move(_vec);
};
return exec(conn_, "FETCH FORWARD " + std::to_string(_batch_size) + " FROM " +
cursor_name_ + ";")
.transform(to_vector)
.transform(shutdown_if_necessary);
.transform([this](auto&& _vec) {
if (_vec.size() == 0) {
shutdown();
}
return std::move(_vec);
});
}
Iterator& Iterator::operator=(Iterator&& _other) noexcept {

View File

@@ -671,30 +671,43 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept {
using T = std::remove_cvref_t<decltype(_t)>;
if constexpr (std::is_same_v<T, dynamic::types::Boolean>) {
return "BOOLEAN";
} else if constexpr (std::is_same_v<T, dynamic::types::Int8> ||
std::is_same_v<T, dynamic::types::Int16> ||
std::is_same_v<T, dynamic::types::UInt8> ||
std::is_same_v<T, dynamic::types::UInt16>) {
return "SMALLINT";
} else if constexpr (std::is_same_v<T, dynamic::types::Int32> ||
std::is_same_v<T, dynamic::types::UInt32>) {
return "INTEGER";
} else if constexpr (std::is_same_v<T, dynamic::types::Int64> ||
std::is_same_v<T, dynamic::types::UInt64>) {
return "BIGINT";
} else if constexpr (std::is_same_v<T, dynamic::types::Float32> ||
std::is_same_v<T, dynamic::types::Float64>) {
return "NUMERIC";
} else if constexpr (std::is_same_v<T, dynamic::types::Text>) {
return "TEXT";
} else if constexpr (std::is_same_v<T, dynamic::types::VarChar>) {
return "VARCHAR(" + std::to_string(_t.length) + ")";
} else if constexpr (std::is_same_v<T, dynamic::types::Date>) {
return "DATE";
} else if constexpr (std::is_same_v<T, dynamic::types::Timestamp>) {
return "TIMESTAMP";
} else if constexpr (std::is_same_v<T, dynamic::types::TimestampWithTZ>) {
return "TIMESTAMP WITH TIME ZONE";
} else if constexpr (std::is_same_v<T, dynamic::types::Unknown>) {
return "TEXT";
} else {
static_assert(rfl::always_false_v<T>, "Not all cases were covered.");
}

View File

@@ -692,6 +692,7 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept {
} else if constexpr (std::is_same_v<T, dynamic::types::Unknown> ||
std::is_same_v<T, dynamic::types::Text> ||
std::is_same_v<T, dynamic::types::VarChar> ||
std::is_same_v<T, dynamic::types::Date> ||
std::is_same_v<T, dynamic::types::Timestamp> ||
std::is_same_v<T, dynamic::types::TimestampWithTZ>) {
return "TEXT";

4
src/sqlgen_mysql.cpp Normal file
View File

@@ -0,0 +1,4 @@
#include "sqlgen/mysql/Connection.cpp"
#include "sqlgen/mysql/Iterator.cpp"
#include "sqlgen/mysql/exec.cpp"
#include "sqlgen/mysql/to_sql.cpp"

View File

@@ -8,6 +8,10 @@ if (SQLGEN_BUILD_DRY_TESTS_ONLY)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSQLGEN_BUILD_DRY_TESTS_ONLY")
endif()
if(SQLGEN_MYSQL)
add_subdirectory(mysql)
endif()
if(SQLGEN_POSTGRES)
add_subdirectory(postgres)
endif()

View File

@@ -0,0 +1,19 @@
project(sqlgen-mysql-tests)
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "*.cpp")
add_executable(
sqlgen-mysql-tests
${SOURCES}
)
target_precompile_headers(sqlgen-mysql-tests PRIVATE [["sqlgen.hpp"]] <iostream> <string> <functional> <gtest/gtest.h>)
target_link_libraries(
sqlgen-mysql-tests
PRIVATE
"${SQLGEN_GTEST_LIB}"
)
find_package(GTest)
gtest_discover_tests(sqlgen-mysql-tests)

View File

@@ -0,0 +1,73 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_aggregations {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
double age;
};
TEST(mysql, test_aggregations) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
struct Children {
int num_children;
int num_last_names;
double avg_age;
double max_age;
double min_age;
double sum_age;
};
const auto get_children =
select_from<Person>(
avg("age"_c).as<"avg_age">(), count().as<"num_children">(),
max("age"_c).as<"max_age">(), min("age"_c).as<"min_age">(),
sum("age"_c).as<"sum_age">(),
count_distinct("last_name"_c).as<"num_last_names">()) |
where("age"_c < 18) | to<Children>;
const auto children = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(get_children)
.value();
using namespace std::ranges::views;
EXPECT_EQ(children.num_children, 3);
EXPECT_EQ(children.num_last_names, 1);
EXPECT_EQ(children.avg_age, 6.0);
EXPECT_EQ(children.max_age, 10.0);
EXPECT_EQ(children.min_age, 0.0);
EXPECT_EQ(children.sum_age, 18.0);
}
} // namespace test_aggregations
#endif

View File

@@ -0,0 +1,72 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_aggregations_with_nullable {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
std::optional<double> age;
};
TEST(mysql, test_aggregations_with_nullable) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{.id = 3, .first_name = "Maggie", .last_name = "Simpson"}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
struct Children {
int num_children;
int num_last_names;
std::optional<double> avg_age;
std::optional<double> max_age;
std::optional<double> min_age;
std::optional<double> sum_age;
};
const auto get_children =
select_from<Person>(
avg("age"_c).as<"avg_age">(), count().as<"num_children">(),
max("age"_c).as<"max_age">(), min("age"_c).as<"min_age">(),
sum("age"_c).as<"sum_age">(),
count_distinct("last_name"_c).as<"num_last_names">()) |
where("age"_c < 18 or "age"_c.is_null()) | to<Children>;
const auto children = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(get_children)
.value();
using namespace std::ranges::views;
EXPECT_EQ(children.num_children, 3);
EXPECT_EQ(children.num_last_names, 1);
EXPECT_EQ(children.avg_age, 9.0);
EXPECT_EQ(children.max_age, 10.0);
EXPECT_EQ(children.min_age, 8.0);
EXPECT_EQ(children.sum_age, 18.0);
}
} // namespace test_aggregations_with_nullable
#endif

View File

@@ -0,0 +1,55 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_auto_incr_primary_key {
struct Person {
sqlgen::PrimaryKey<uint32_t, sqlgen::auto_incr> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_auto_incr_primary_key) {
auto people1 = std::vector<Person>(
{Person{.first_name = "Homer", .last_name = "Simpson", .age = 45},
Person{.first_name = "Bart", .last_name = "Simpson", .age = 10},
Person{.first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{.first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto people2 = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(sqlgen::read<std::vector<Person>> |
order_by("age"_c.desc()))
.value();
people1.at(0).id = 1;
people1.at(1).id = 2;
people1.at(2).id = 3;
people1.at(3).id = 4;
const auto json1 = rfl::json::write(people1);
const auto json2 = rfl::json::write(people2);
EXPECT_EQ(json1, json2);
}
} // namespace test_auto_incr_primary_key
#endif

View File

@@ -0,0 +1,44 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_create_index {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
sqlgen::Varchar<100> first_name;
sqlgen::Varchar<100> last_name;
int age;
};
TEST(mysql, test_create_index) {
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto people = sqlgen::mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(create_table<Person> | if_not_exists)
.and_then(create_index<"test_table_ix", Person>(
"first_name"_c, "last_name"_c))
.and_then(sqlgen::read<std::vector<Person>>)
.value();
const std::string expected = R"([])";
EXPECT_EQ(rfl::json::write(people), expected);
}
} // namespace test_create_index
#endif

View File

@@ -0,0 +1,27 @@
#include <gtest/gtest.h>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
namespace test_create_index_dry {
struct TestTable {
std::string field1;
int32_t field2;
sqlgen::PrimaryKey<uint32_t> id;
std::optional<std::string> nullable;
};
TEST(mysql, test_create_index_dry) {
using namespace sqlgen;
using namespace sqlgen::literals;
const auto query =
create_index<"test_table_ix", TestTable>("field1"_c, "field2"_c);
const auto expected =
R"(CREATE INDEX `test_table_ix` ON `TestTable`(`field1`, `field2`);)";
EXPECT_EQ(sqlgen::mysql::to_sql(query), expected);
}
} // namespace test_create_index_dry

View File

@@ -0,0 +1,42 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_create_table {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_create_table) {
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto people = sqlgen::mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(create_table<Person> | if_not_exists)
.and_then(sqlgen::read<std::vector<Person>>)
.value();
const std::string expected = R"([])";
EXPECT_EQ(rfl::json::write(people), expected);
}
} // namespace test_create_table
#endif

View File

@@ -0,0 +1,26 @@
#include <gtest/gtest.h>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
namespace test_create_table_dry {
struct TestTable {
std::string field1;
int32_t field2;
sqlgen::PrimaryKey<uint32_t> id;
std::optional<std::string> nullable;
};
TEST(mysql, test_create_table_dry) {
using namespace sqlgen;
using namespace sqlgen::literals;
const auto query = create_table<TestTable> | if_not_exists;
const auto expected =
R"(CREATE TABLE IF NOT EXISTS `TestTable` (`field1` TEXT NOT NULL, `field2` INT NOT NULL, `id` BIGINT NOT NULL, `nullable` TEXT, PRIMARY KEY (`id`));)";
EXPECT_EQ(sqlgen::mysql::to_sql(query), expected);
}
} // namespace test_create_table_dry

View File

@@ -0,0 +1,60 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_delete_from {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_delete_from) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0},
Person{
.id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto conn =
sqlgen::mysql::connect(credentials).and_then(drop<Person> | if_exists);
sqlgen::write(conn, people1).value();
const auto delete_hugo =
delete_from<Person> | where("first_name"_c == "Hugo");
const auto people2 =
conn.and_then(delete_hugo)
.and_then(sqlgen::read<std::vector<Person>> | order_by("id"_c))
.value();
const std::string expected =
R"([{"id":0,"first_name":"Homer","last_name":"Simpson","age":45},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0}])";
EXPECT_EQ(rfl::json::write(people2), expected);
}
} // namespace test_delete_from
#endif

View File

@@ -0,0 +1,25 @@
#include <gtest/gtest.h>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
namespace test_delete_from_dry {
struct TestTable {
std::string field1;
int32_t field2;
sqlgen::PrimaryKey<uint32_t> id;
std::optional<std::string> nullable;
};
TEST(mysql, test_delete_from_dry) {
using namespace sqlgen;
using namespace sqlgen::literals;
const auto query = delete_from<TestTable> | where("field2"_c > 0);
const auto expected = R"(DELETE FROM `TestTable` WHERE `field2` > 0;)";
EXPECT_EQ(sqlgen::mysql::to_sql(query), expected);
}
} // namespace test_delete_from_dry

View File

@@ -0,0 +1,25 @@
#include <gtest/gtest.h>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
namespace test_drop_dry {
struct TestTable {
std::string field1;
int32_t field2;
sqlgen::PrimaryKey<uint32_t> id;
std::optional<std::string> nullable;
};
TEST(mysql, test_drop_dry) {
using namespace sqlgen;
using namespace sqlgen::literals;
const auto query = drop<TestTable> | if_exists;
const auto expected = R"(DROP TABLE IF EXISTS `TestTable`;)";
EXPECT_EQ(sqlgen::mysql::to_sql(query), expected);
}
} // namespace test_drop_dry

View File

@@ -0,0 +1,77 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_group_by {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
double age;
};
TEST(mysql, test_group_by) {
static_assert(std::ranges::input_range<sqlgen::Range<Person>>,
"Must be an input range.");
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
struct Children {
std::string last_name;
int num_children;
int num_last_names;
double avg_age;
double max_age;
double min_age;
double sum_age;
};
const auto get_children =
select_from<Person>(
"last_name"_c, avg("age"_c).as<"avg_age">(),
count().as<"num_children">(), max("age"_c).as<"max_age">(),
min("age"_c).as<"min_age">(), sum("age"_c).as<"sum_age">(),
count_distinct("last_name"_c).as<"num_last_names">()) |
where("age"_c < 18) | group_by("last_name"_c) | to<std::vector<Children>>;
const auto children = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(get_children)
.value();
EXPECT_EQ(children.size(), 1);
EXPECT_EQ(children.at(0).last_name, "Simpson");
EXPECT_EQ(children.at(0).num_children, 3);
EXPECT_EQ(children.at(0).num_last_names, 1);
EXPECT_EQ(children.at(0).avg_age, 6.0);
EXPECT_EQ(children.at(0).max_age, 10.0);
EXPECT_EQ(children.at(0).min_age, 0.0);
EXPECT_EQ(children.at(0).sum_age, 18.0);
}
} // namespace test_group_by
#endif

View File

@@ -0,0 +1,73 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_group_by_with_operations {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_group_by_with_operations) {
static_assert(std::ranges::input_range<sqlgen::Range<Person>>,
"Must be an input range.");
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
struct Children {
std::string last_name;
std::string last_name_trimmed;
double avg_age;
double max_age_plus_one;
double min_age_plus_one;
};
const auto get_children =
select_from<Person>(
"last_name"_c, trim("last_name"_c).as<"last_name_trimmed">(),
max(cast<double>("age"_c) + 1.0).as<"max_age_plus_one">(),
(min(cast<double>("age"_c)) + 1.0).as<"min_age_plus_one">(),
round(avg(cast<double>("age"_c))).as<"avg_age">()) |
where("age"_c < 18) | group_by("last_name"_c) | to<std::vector<Children>>;
const auto children = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(get_children)
.value();
EXPECT_EQ(children.size(), 1);
EXPECT_EQ(children.at(0).last_name, "Simpson");
EXPECT_EQ(children.at(0).last_name_trimmed, "Simpson");
EXPECT_EQ(children.at(0).avg_age, 6.0);
EXPECT_EQ(children.at(0).max_age_plus_one, 11.0);
EXPECT_EQ(children.at(0).min_age_plus_one, 1.0);
}
} // namespace test_group_by_with_operations
#endif

View File

@@ -0,0 +1,54 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_insert_and_read {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_insert_and_read) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto people2 = sqlgen::mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(begin_transaction)
.and_then(create_table<Person> | if_not_exists)
.and_then(insert(std::ref(people1)))
.and_then(commit)
.and_then(sqlgen::read<std::vector<Person>>)
.value();
const auto json1 = rfl::json::write(people1);
const auto json2 = rfl::json::write(people2);
EXPECT_EQ(json1, json2);
}
} // namespace test_insert_and_read
#endif

View File

@@ -0,0 +1,67 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_insert_and_read_two_tables {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
struct Children {
uint32_t id_parent;
sqlgen::PrimaryKey<uint32_t> id_child;
};
TEST(mysql, test_insert_and_read_two_tables) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto children =
std::vector<Children>({Children{.id_parent = 0, .id_child = 1},
Children{.id_parent = 0, .id_child = 2},
Children{.id_parent = 0, .id_child = 3}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto people2 = sqlgen::mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(drop<Children> | if_exists)
.and_then(begin_transaction)
.and_then(create_table<Person> | if_not_exists)
.and_then(create_table<Children> | if_not_exists)
.and_then(insert(std::ref(people1)))
.and_then(insert(std::ref(children)))
.and_then(commit)
.and_then(sqlgen::read<std::vector<Person>>)
.value();
const auto json1 = rfl::json::write(people1);
const auto json2 = rfl::json::write(people2);
EXPECT_EQ(json1, json2);
}
} // namespace test_insert_and_read_two_tables
#endif

View File

@@ -0,0 +1,23 @@
#include <gtest/gtest.h>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
namespace test_insert_dry {
struct TestTable {
std::string field1;
int32_t field2;
sqlgen::PrimaryKey<uint32_t> id;
std::optional<std::string> nullable;
};
TEST(mysql, test_insert_dry) {
const auto query = sqlgen::Insert<TestTable>{};
const auto expected =
R"(INSERT INTO `TestTable` (`field1`, `field2`, `id`, `nullable`) VALUES (?, ?, ?, ?);)";
EXPECT_EQ(sqlgen::mysql::to_sql(query), expected);
}
} // namespace test_insert_dry

View File

@@ -0,0 +1,63 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_is_null {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
std::optional<int> age;
};
TEST(mysql, test_is_null) {
const auto people1 = std::vector<Person>(
{Person{.id = 0, .first_name = "Homer", .last_name = "Simpson"},
Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0},
Person{.id = 4, .first_name = "Hugo", .last_name = "Simpson"}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto conn = mysql::connect(credentials);
const auto people2 =
conn.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(sqlgen::read<std::vector<Person>> |
where("age"_c.is_null()) | order_by("first_name"_c.desc()))
.value();
const auto people3 =
conn.and_then(sqlgen::read<std::vector<Person>> |
where("age"_c.is_not_null()) | order_by("age"_c))
.value();
const std::string expected1 =
R"([{"id":4,"first_name":"Hugo","last_name":"Simpson"},{"id":0,"first_name":"Homer","last_name":"Simpson"}])";
const std::string expected2 =
R"([{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10}])";
EXPECT_EQ(rfl::json::write(people2), expected1);
EXPECT_EQ(rfl::json::write(people3), expected2);
}
} // namespace test_is_null
#endif

View File

@@ -0,0 +1,32 @@
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_is_null_dry {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
std::optional<int> age;
};
TEST(mysql, test_is_null_dry) {
using namespace sqlgen;
using namespace sqlgen::literals;
const auto sql =
mysql::to_sql(sqlgen::read<std::vector<Person>> |
where("age"_c.is_null()) | order_by("first_name"_c.desc()));
const std::string expected =
R"(SELECT `id`, `first_name`, `last_name`, `age` FROM `Person` WHERE `age` IS NULL ORDER BY `first_name` DESC)";
EXPECT_EQ(sql, expected);
}
} // namespace test_is_null_dry

65
tests/mysql/test_join.cpp Normal file
View File

@@ -0,0 +1,65 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_join {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
double age;
};
TEST(mysql, test_join) {
static_assert(std::ranges::input_range<sqlgen::Range<Person>>,
"Must be an input range.");
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto get_people =
select_from<Person, "t1">(
"id"_t1 | as<"id">, "first_name"_t1 | as<"first_name">,
"last_name"_t2 | as<"last_name">, "age"_t2 | as<"age">) |
left_join<Person, "t2">("id"_t1 == "id"_t2) | order_by("id"_t1) |
to<std::vector<Person>>;
const auto people = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(get_people)
.value();
const std::string expected_query =
R"(SELECT t1.`id` AS `id`, t1.`first_name` AS `first_name`, t2.`last_name` AS `last_name`, t2.`age` AS `age` FROM `Person` t1 LEFT JOIN `Person` t2 ON t1.`id` = t2.`id` ORDER BY t1.`id`)";
const std::string expected =
R"([{"id":0,"first_name":"Homer","last_name":"Simpson","age":45.0},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10.0},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8.0},{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0.0}])";
EXPECT_EQ(mysql::to_sql(get_people), expected_query);
EXPECT_EQ(rfl::json::write(people), expected);
}
} // namespace test_join
#endif

View File

@@ -0,0 +1,95 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_joins_nested {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
double age;
};
struct Relationship {
sqlgen::PrimaryKey<uint32_t> parent_id;
sqlgen::PrimaryKey<uint32_t> child_id;
};
TEST(mysql, test_joins_nested) {
const auto people1 = std::vector<Person>(
{Person{
.id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45},
Person{
.id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40},
Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10},
Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 4, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto relationships =
std::vector<Relationship>({Relationship{.parent_id = 0, .child_id = 2},
Relationship{.parent_id = 0, .child_id = 3},
Relationship{.parent_id = 0, .child_id = 4},
Relationship{.parent_id = 1, .child_id = 2},
Relationship{.parent_id = 1, .child_id = 3},
Relationship{.parent_id = 1, .child_id = 4}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
struct ParentAndChild {
std::string last_name;
std::string first_name_parent;
std::string first_name_child;
double parent_age_at_birth;
};
const auto get_children =
select_from<Relationship, "t1">("parent_id"_t1 | as<"id">,
"first_name"_t2 | as<"first_name">,
"age"_t2 | as<"age">) |
left_join<Person, "t2">("id"_t2 == "child_id"_t1);
const auto get_people =
select_from<Person, "t1">(
"last_name"_t1 | as<"last_name">,
"first_name"_t1 | as<"first_name_parent">,
"first_name"_t2 | as<"first_name_child">,
("age"_t1 - "age"_t2) | as<"parent_age_at_birth">) |
inner_join<"t2">(get_children, "id"_t1 == "id"_t2) |
order_by("id"_t1, "id"_t2) | to<std::vector<ParentAndChild>>;
const auto people = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(drop<Relationship> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(write(std::ref(relationships)))
.and_then(get_people)
.value();
const std::string expected_query =
R"(SELECT t1.`last_name` AS `last_name`, t1.`first_name` AS `first_name_parent`, t2.`first_name` AS `first_name_child`, (t1.`age`) - (t2.`age`) AS `parent_age_at_birth` FROM `Person` t1 INNER JOIN (SELECT t1.`parent_id` AS `id`, t2.`first_name` AS `first_name`, t2.`age` AS `age` FROM `Relationship` t1 LEFT JOIN `Person` t2 ON t2.`id` = t1.`child_id`) t2 ON t1.`id` = t2.`id` ORDER BY t1.`id`, t2.`id`)";
const std::string expected =
R"([{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Bart","parent_age_at_birth":35.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Lisa","parent_age_at_birth":37.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Maggie","parent_age_at_birth":45.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Bart","parent_age_at_birth":30.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Lisa","parent_age_at_birth":32.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Maggie","parent_age_at_birth":40.0}])";
EXPECT_EQ(mysql::to_sql(get_people), expected_query);
EXPECT_EQ(rfl::json::write(people), expected);
}
} // namespace test_joins_nested
#endif

View File

@@ -0,0 +1,94 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_joins_nested_grouped {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
double age;
};
struct Relationship {
sqlgen::PrimaryKey<uint32_t> parent_id;
sqlgen::PrimaryKey<uint32_t> child_id;
};
TEST(mysql, test_joins_nested_grouped) {
const auto people1 = std::vector<Person>(
{Person{
.id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45},
Person{
.id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40},
Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10},
Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 4, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto relationships =
std::vector<Relationship>({Relationship{.parent_id = 0, .child_id = 2},
Relationship{.parent_id = 0, .child_id = 3},
Relationship{.parent_id = 0, .child_id = 4},
Relationship{.parent_id = 1, .child_id = 2},
Relationship{.parent_id = 1, .child_id = 3},
Relationship{.parent_id = 1, .child_id = 4}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
struct ParentAndChild {
std::string last_name;
std::string first_name_child;
double avg_parent_age_at_birth;
};
const auto get_children =
select_from<Relationship, "t1">("parent_id"_t1 | as<"id">,
"first_name"_t2 | as<"first_name">,
"age"_t2 | as<"age">) |
left_join<Person, "t2">("id"_t2 == "child_id"_t1);
const auto get_people =
select_from<Person, "t1">(
"last_name"_t1 | as<"last_name">,
"first_name"_t2 | as<"first_name_child">,
avg("age"_t1 - "age"_t2) | as<"avg_parent_age_at_birth">) |
inner_join<"t2">(get_children, "id"_t1 == "id"_t2) |
group_by("last_name"_t1, "first_name"_t2) | order_by("first_name"_t2) |
to<std::vector<ParentAndChild>>;
const auto people = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(drop<Relationship> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(write(std::ref(relationships)))
.and_then(get_people)
.value();
const std::string expected_query =
R"(SELECT t1.`last_name` AS `last_name`, t2.`first_name` AS `first_name_child`, AVG((t1.`age`) - (t2.`age`)) AS `avg_parent_age_at_birth` FROM `Person` t1 INNER JOIN (SELECT t1.`parent_id` AS `id`, t2.`first_name` AS `first_name`, t2.`age` AS `age` FROM `Relationship` t1 LEFT JOIN `Person` t2 ON t2.`id` = t1.`child_id`) t2 ON t1.`id` = t2.`id` GROUP BY t1.`last_name`, t2.`first_name` ORDER BY t2.`first_name`)";
const std::string expected =
R"([{"last_name":"Simpson","first_name_child":"Bart","avg_parent_age_at_birth":32.5},{"last_name":"Simpson","first_name_child":"Lisa","avg_parent_age_at_birth":34.5},{"last_name":"Simpson","first_name_child":"Maggie","avg_parent_age_at_birth":42.5}])";
EXPECT_EQ(mysql::to_sql(get_people), expected_query);
EXPECT_EQ(rfl::json::write(people), expected);
}
} // namespace test_joins_nested_grouped
#endif

View File

@@ -0,0 +1,89 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_joins_two_tables {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
double age;
};
struct Relationship {
sqlgen::PrimaryKey<uint32_t> parent_id;
sqlgen::PrimaryKey<uint32_t> child_id;
};
TEST(mysql, test_joins_two_tables) {
const auto people1 = std::vector<Person>(
{Person{
.id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45},
Person{
.id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40},
Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10},
Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 4, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto relationships =
std::vector<Relationship>({Relationship{.parent_id = 0, .child_id = 2},
Relationship{.parent_id = 0, .child_id = 3},
Relationship{.parent_id = 0, .child_id = 4},
Relationship{.parent_id = 1, .child_id = 2},
Relationship{.parent_id = 1, .child_id = 3},
Relationship{.parent_id = 1, .child_id = 4}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
struct ParentAndChild {
std::string last_name;
std::string first_name_parent;
std::string first_name_child;
double parent_age_at_birth;
};
const auto get_people =
select_from<Person, "t1">(
"last_name"_t1 | as<"last_name">,
"first_name"_t1 | as<"first_name_parent">,
"first_name"_t3 | as<"first_name_child">,
("age"_t1 - "age"_t3) | as<"parent_age_at_birth">) |
inner_join<Relationship, "t2">("id"_t1 == "parent_id"_t2) |
left_join<Person, "t3">("id"_t3 == "child_id"_t2) |
order_by("id"_t1, "id"_t3) | to<std::vector<ParentAndChild>>;
const auto people = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(drop<Relationship> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(write(std::ref(relationships)))
.and_then(get_people)
.value();
const std::string expected_query =
R"(SELECT t1.`last_name` AS `last_name`, t1.`first_name` AS `first_name_parent`, t3.`first_name` AS `first_name_child`, (t1.`age`) - (t3.`age`) AS `parent_age_at_birth` FROM `Person` t1 INNER JOIN `Relationship` t2 ON t1.`id` = t2.`parent_id` LEFT JOIN `Person` t3 ON t3.`id` = t2.`child_id` ORDER BY t1.`id`, t3.`id`)";
const std::string expected =
R"([{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Bart","parent_age_at_birth":35.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Lisa","parent_age_at_birth":37.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Maggie","parent_age_at_birth":45.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Bart","parent_age_at_birth":30.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Lisa","parent_age_at_birth":32.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Maggie","parent_age_at_birth":40.0}])";
EXPECT_EQ(mysql::to_sql(get_people), expected_query);
EXPECT_EQ(rfl::json::write(people), expected);
}
} // namespace test_joins_two_tables
#endif

View File

@@ -0,0 +1,90 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_joins_two_tables_grouped {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
double age;
};
struct Relationship {
sqlgen::PrimaryKey<uint32_t> parent_id;
sqlgen::PrimaryKey<uint32_t> child_id;
};
TEST(mysql, test_joins_two_tables_grouped) {
const auto people1 = std::vector<Person>(
{Person{
.id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45},
Person{
.id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40},
Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10},
Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 4, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto relationships =
std::vector<Relationship>({Relationship{.parent_id = 0, .child_id = 2},
Relationship{.parent_id = 0, .child_id = 3},
Relationship{.parent_id = 0, .child_id = 4},
Relationship{.parent_id = 1, .child_id = 2},
Relationship{.parent_id = 1, .child_id = 3},
Relationship{.parent_id = 1, .child_id = 4}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
struct ParentAndChild {
std::string last_name;
std::string first_name_child;
double avg_parent_age_at_birth;
};
const auto get_people =
select_from<Person, "t1">(
"last_name"_t1 | as<"last_name">,
"first_name"_t3 | as<"first_name_child">,
avg("age"_t1 - "age"_t3) | as<"avg_parent_age_at_birth">) |
inner_join<Relationship, "t2">("id"_t1 == "parent_id"_t2) |
left_join<Person, "t3">("id"_t3 == "child_id"_t2) |
group_by("last_name"_t1, "first_name"_t3) |
order_by("last_name"_t1, "first_name"_t3) |
to<std::vector<ParentAndChild>>;
const auto people = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(drop<Relationship> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(write(std::ref(relationships)))
.and_then(get_people)
.value();
const std::string expected_query =
R"(SELECT t1.`last_name` AS `last_name`, t3.`first_name` AS `first_name_child`, AVG((t1.`age`) - (t3.`age`)) AS `avg_parent_age_at_birth` FROM `Person` t1 INNER JOIN `Relationship` t2 ON t1.`id` = t2.`parent_id` LEFT JOIN `Person` t3 ON t3.`id` = t2.`child_id` GROUP BY t1.`last_name`, t3.`first_name` ORDER BY t1.`last_name`, t3.`first_name`)";
const std::string expected =
R"([{"last_name":"Simpson","first_name_child":"Bart","avg_parent_age_at_birth":32.5},{"last_name":"Simpson","first_name_child":"Lisa","avg_parent_age_at_birth":34.5},{"last_name":"Simpson","first_name_child":"Maggie","avg_parent_age_at_birth":42.5}])";
EXPECT_EQ(mysql::to_sql(get_people), expected_query);
EXPECT_EQ(rfl::json::write(people), expected);
}
} // namespace test_joins_two_tables_grouped
#endif

73
tests/mysql/test_like.cpp Normal file
View File

@@ -0,0 +1,73 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_like {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_like) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0},
Person{
.id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto conn = mysql::connect(credentials);
const auto people2 =
conn.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(sqlgen::read<std::vector<Person>> |
where("first_name"_c.like("H%")) | order_by("age"_c))
.value();
const auto people3 =
conn.and_then(sqlgen::read<std::vector<Person>> |
where("first_name"_c.not_like("H%")) | order_by("age"_c))
.value();
const auto people4 =
conn.and_then(sqlgen::read<std::vector<Person>> |
where("first_name"_c.like("O'Reilly")) | order_by("age"_c))
.value();
const std::string expected1 =
R"([{"id":4,"first_name":"Hugo","last_name":"Simpson","age":10},{"id":0,"first_name":"Homer","last_name":"Simpson","age":45}])";
const std::string expected2 =
R"([{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10}])";
const std::string expected3 = R"([])";
EXPECT_EQ(rfl::json::write(people2), expected1);
EXPECT_EQ(rfl::json::write(people3), expected2);
EXPECT_EQ(rfl::json::write(people4), expected3);
}
} // namespace test_like
#endif

View File

@@ -0,0 +1,32 @@
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_like_dry {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
std::optional<int> age;
};
TEST(mysql, test_like_dry) {
using namespace sqlgen;
using namespace sqlgen::literals;
const auto sql =
mysql::to_sql(sqlgen::read<std::vector<Person>> |
where("first_name"_c.like("H%")) | order_by("age"_c));
const std::string expected =
R"(SELECT `id`, `first_name`, `last_name`, `age` FROM `Person` WHERE `first_name` LIKE 'H%' ORDER BY `age`)";
EXPECT_EQ(sql, expected);
}
} // namespace test_like_dry

View File

@@ -0,0 +1,57 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_limit {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_limit) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0},
Person{
.id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto conn =
sqlgen::mysql::connect(credentials).and_then(drop<Person> | if_exists);
sqlgen::write(conn, people1).value();
const auto query =
sqlgen::read<std::vector<Person>> | order_by("age"_c) | limit(2);
const auto people2 = query(conn).value();
const std::string expected =
R"([{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8}])";
EXPECT_EQ(rfl::json::write(people2), expected);
}
} // namespace test_limit
#endif

View File

@@ -0,0 +1,84 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_operations {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_operations) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
struct Children {
int id_plus_age;
int age_times_2;
int id_plus_2_minus_age;
int age_mod_3;
int abs_age;
double exp_age;
double sqrt_age;
size_t length_first_name;
std::string full_name;
std::string first_name_lower;
std::string first_name_upper;
std::string first_name_replaced;
};
const auto get_children =
select_from<Person>(
("id"_c + "age"_c) | as<"id_plus_age">,
("age"_c * 2) | as<"age_times_2">,
("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">,
("age"_c % 3) | as<"age_mod_3">, abs("age"_c * (-1)) | as<"abs_age">,
round(exp(cast<double>("age"_c)), 2) | as<"exp_age">,
round(sqrt(cast<double>("age"_c)), 2) | as<"sqrt_age">,
length(trim("first_name"_c)) | as<"length_first_name">,
concat(ltrim("first_name"_c), " ", rtrim("last_name"_c)) |
as<"full_name">,
upper(rtrim(concat("first_name"_c, " "))) | as<"first_name_upper">,
lower(ltrim(concat(" ", "first_name"_c))) | as<"first_name_lower">,
replace("first_name"_c, "Bart", "Hugo") | as<"first_name_replaced">) |
where("age"_c < 18) | order_by("age"_c.desc()) |
to<std::vector<Children>>;
const auto children = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(get_children)
.value();
const std::string expected =
R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16,"length_first_name":4,"full_name":"Bart Simpson","first_name_lower":"bart","first_name_upper":"BART","first_name_replaced":"Hugo"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83,"length_first_name":4,"full_name":"Lisa Simpson","first_name_lower":"lisa","first_name_upper":"LISA","first_name_replaced":"Lisa"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0,"length_first_name":6,"full_name":"Maggie Simpson","first_name_lower":"maggie","first_name_upper":"MAGGIE","first_name_replaced":"Maggie"}])";
EXPECT_EQ(rfl::json::write(children), expected);
}
} // namespace test_operations
#endif

View File

@@ -0,0 +1,70 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_operations_with_nullable {
struct Person {
sqlgen::PrimaryKey<int> id;
std::string first_name;
std::optional<std::string> last_name;
std::optional<int> age;
};
TEST(mysql, test_operations_with_nullable) {
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},
Person{.id = 2, .first_name = "Hugo", .age = 10},
Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 4, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
struct Children {
std::optional<int> id_plus_age;
std::optional<int> age_times_2;
std::optional<int> id_plus_2_minus_age;
std::optional<std::string> full_name;
std::string last_name_or_none;
};
const auto get_children =
select_from<Person>(
("id"_c + "age"_c) | as<"id_plus_age">,
("age"_c * 2) | as<"age_times_2">,
("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">,
concat(upper("last_name"_c), ", ", "first_name"_c) | as<"full_name">,
coalesce("last_name"_c, "none") | as<"last_name_or_none">) |
where("age"_c < 18) | to<std::vector<Children>>;
const auto children = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(get_children)
.value();
const std::string expected =
R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"full_name":"SIMPSON, Bart","last_name_or_none":"Simpson"},{"id_plus_age":12,"age_times_2":20,"id_plus_2_minus_age":-6,"last_name_or_none":"none"},{"id_plus_age":11,"age_times_2":16,"id_plus_2_minus_age":-3,"full_name":"SIMPSON, Lisa","last_name_or_none":"Simpson"},{"id_plus_age":4,"age_times_2":0,"id_plus_2_minus_age":6,"full_name":"SIMPSON, Maggie","last_name_or_none":"Simpson"}])";
EXPECT_EQ(rfl::json::write(children), expected);
}
} // namespace test_operations_with_nullable
#endif

View File

@@ -0,0 +1,64 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_range {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_range) {
static_assert(std::ranges::input_range<sqlgen::Range<Person>>,
"Must be an input range.");
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto conn =
sqlgen::mysql::connect(credentials).and_then(drop<Person> | if_exists);
const auto people2 =
sqlgen::write(conn, people1)
.and_then(sqlgen::read<sqlgen::Range<Person>> | order_by("id"_c))
.value();
using namespace std::ranges::views;
const auto first_names =
internal::collect::vector(people2 | transform([](const auto& _r) {
return _r.value().first_name;
}));
EXPECT_EQ(first_names.at(0), "Homer");
EXPECT_EQ(first_names.at(1), "Bart");
EXPECT_EQ(first_names.at(2), "Lisa");
EXPECT_EQ(first_names.at(3), "Maggie");
}
} // namespace test_range
#endif

View File

@@ -0,0 +1,64 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_range_select_from {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_range_select_from) {
static_assert(std::ranges::input_range<sqlgen::Range<Person>>,
"Must be an input range.");
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto conn =
sqlgen::mysql::connect(credentials).and_then(drop<Person> | if_exists);
const auto people2 =
sqlgen::write(conn, people1)
.and_then(select_from<Person>("first_name"_c) | order_by("id"_c))
.value();
using namespace std::ranges::views;
const auto first_names =
internal::collect::vector(people2 | transform([](const auto& _r) {
return rfl::get<"first_name">(_r.value());
}));
EXPECT_EQ(first_names.at(0), "Homer");
EXPECT_EQ(first_names.at(1), "Bart");
EXPECT_EQ(first_names.at(2), "Lisa");
EXPECT_EQ(first_names.at(3), "Maggie");
}
} // namespace test_range_select_from
#endif

View File

@@ -0,0 +1,88 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <chrono>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_select_from_with_timestamps {
struct Person {
sqlgen::PrimaryKey<uint32_t, sqlgen::auto_incr> id;
std::string first_name;
std::string last_name;
sqlgen::Date birthday;
};
TEST(mysql, test_select_from_with_timestamps) {
const auto people1 =
std::vector<Person>({Person{.first_name = "Homer",
.last_name = "Simpson",
.birthday = sqlgen::Date("1970-01-01")},
Person{.first_name = "Bart",
.last_name = "Simpson",
.birthday = sqlgen::Date("2000-01-01")},
Person{.first_name = "Lisa",
.last_name = "Simpson",
.birthday = sqlgen::Date("2002-01-01")},
Person{.first_name = "Maggie",
.last_name = "Simpson",
.birthday = sqlgen::Date("2010-01-01")}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
struct Birthday {
Date birthday;
Date birthday_recreated;
double age_in_days;
int hour;
int minute;
int second;
int weekday;
};
const auto get_birthdays =
select_from<Person>(
("birthday"_c + std::chrono::weeks(2) + std::chrono::days(-4)) |
as<"birthday">,
((cast<Date>(concat(cast<std::string>(year("birthday"_c)), "-",
cast<std::string>(month("birthday"_c)), "-",
cast<std::string>(day("birthday"_c))))) +
std::chrono::days(10)) |
as<"birthday_recreated">,
days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days">,
hour("birthday"_c) | as<"hour">, minute("birthday"_c) | as<"minute">,
second("birthday"_c) | as<"second">,
weekday("birthday"_c) | as<"weekday">) |
order_by("id"_c) | to<std::vector<Birthday>>;
const auto birthdays = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(get_birthdays)
.value();
const std::string expected_query =
R"(SELECT date_add(date_add(`birthday`, INTERVAL 2 week), INTERVAL -4 day) AS `birthday`, date_add(cast(concat(cast(extract(YEAR from `birthday`) as CHAR), '-', cast(extract(MONTH from `birthday`) as CHAR), '-', cast(extract(DAY from `birthday`) as CHAR)) as DATE), INTERVAL 10 day) AS `birthday_recreated`, datediff('2011-01-01', `birthday`) AS `age_in_days`, extract(HOUR from `birthday`) AS `hour`, extract(MINUTE from `birthday`) AS `minute`, extract(SECOND from `birthday`) AS `second`, dayofweek(`birthday`) AS `weekday` FROM `Person` ORDER BY `id`)";
const std::string expected =
R"([{"birthday":"1970-01-11","birthday_recreated":"1970-01-11","age_in_days":14975.0,"hour":0,"minute":0,"second":0,"weekday":5},{"birthday":"2000-01-11","birthday_recreated":"2000-01-11","age_in_days":4018.0,"hour":0,"minute":0,"second":0,"weekday":7},{"birthday":"2002-01-11","birthday_recreated":"2002-01-11","age_in_days":3287.0,"hour":0,"minute":0,"second":0,"weekday":3},{"birthday":"2010-01-11","birthday_recreated":"2010-01-11","age_in_days":365.0,"hour":0,"minute":0,"second":0,"weekday":6}])";
EXPECT_EQ(mysql::to_sql(get_birthdays), expected_query);
EXPECT_EQ(rfl::json::write(birthdays), expected);
}
} // namespace test_select_from_with_timestamps
#endif

View File

@@ -0,0 +1,61 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <ranges>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_range_select_from_with_to {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_range_select_from) {
static_assert(std::ranges::input_range<sqlgen::Range<Person>>,
"Must be an input range.");
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
struct FirstName {
std::string first_name;
};
const auto people2 =
mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(select_from<Person>("first_name"_c) | order_by("id"_c) |
to<std::vector<FirstName>>)
.value();
EXPECT_EQ(people2.at(0).first_name, "Homer");
EXPECT_EQ(people2.at(1).first_name, "Bart");
EXPECT_EQ(people2.at(2).first_name, "Lisa");
EXPECT_EQ(people2.at(3).first_name, "Maggie");
}
} // namespace test_range_select_from_with_to
#endif

View File

@@ -0,0 +1,48 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
namespace test_timestamp {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
sqlgen::Timestamp<"%Y-%m-%d %H:%M:%S"> ts;
};
TEST(mysql, test_timestamp) {
const auto people1 =
std::vector<Person>({Person{.id = 0,
.first_name = "Homer",
.last_name = "Simpson",
.ts = "2000-01-01 01:00:00"}});
using namespace sqlgen;
using namespace sqlgen::literals;
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
const auto conn =
sqlgen::mysql::connect(credentials).and_then(drop<Person> | if_exists);
const auto people2 = sqlgen::write(conn, people1)
.and_then(sqlgen::read<std::vector<Person>>)
.value();
const auto json1 = rfl::json::write(people1);
const auto json2 = rfl::json::write(people2);
EXPECT_EQ(json1, json2);
}
} // namespace test_timestamp
#endif

View File

@@ -0,0 +1,23 @@
#include <gtest/gtest.h>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
namespace test_timestamp_dry {
struct TestTable {
std::string field1;
int32_t field2;
sqlgen::PrimaryKey<uint32_t> id;
sqlgen::Timestamp<"%Y-%m-%d %H:%M:%S"> ts;
};
TEST(mysql, test_timestamp_dry) {
const auto query = sqlgen::CreateTable<TestTable>{};
const auto expected =
R"(CREATE TABLE IF NOT EXISTS `TestTable` (`field1` TEXT NOT NULL, `field2` INT NOT NULL, `id` BIGINT NOT NULL, `ts` DATETIME NOT NULL, PRIMARY KEY (`id`));)";
EXPECT_EQ(sqlgen::mysql::to_sql(query), expected);
}
} // namespace test_timestamp_dry

View File

@@ -0,0 +1,32 @@
#include <gtest/gtest.h>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
namespace test_to_select_from2_dry {
struct TestTable {
std::string field1;
int32_t field2;
sqlgen::PrimaryKey<uint32_t> id;
std::optional<std::string> nullable;
};
TEST(mysql, test_to_select_from2_dry) {
using namespace sqlgen;
using namespace sqlgen::literals;
const auto query =
select_from<TestTable>("field1"_c.as<"field">(),
avg("field2"_c).as<"avg_field2">(),
"nullable"_c | as<"nullable_field">, as<"one">(1),
"hello" | as<"hello">) |
where("id"_c > 0) | group_by("field1"_c, "nullable"_c) |
order_by("field1"_c) | limit(10);
const auto expected =
R"(SELECT `field1` AS `field`, AVG(`field2`) AS `avg_field2`, `nullable` AS `nullable_field`, 1 AS `one`, 'hello' AS `hello` FROM `TestTable` WHERE `id` > 0 GROUP BY `field1`, `nullable` ORDER BY `field1` LIMIT 10)";
EXPECT_EQ(sqlgen::mysql::to_sql(query), expected);
}
} // namespace test_to_select_from2_dry

View File

@@ -0,0 +1,23 @@
#include <gtest/gtest.h>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
namespace test_to_select_from_dry {
struct TestTable {
std::string field1;
int32_t field2;
sqlgen::PrimaryKey<uint32_t> id;
std::optional<std::string> nullable;
};
TEST(mysql, test_to_select_from_dry) {
const auto query = sqlgen::read<std::vector<TestTable>>;
const auto expected =
R"(SELECT `field1`, `field2`, `id`, `nullable` FROM `TestTable`)";
EXPECT_EQ(sqlgen::mysql::to_sql(query), expected);
}
} // namespace test_to_select_from_dry

View File

@@ -0,0 +1,67 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_transaction {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_transaction) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0},
Person{
.id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto conn =
mysql::connect(credentials).and_then(drop<Person> | if_exists);
sqlgen::write(conn, people1).value();
const auto delete_hugo =
delete_from<Person> | where("first_name"_c == "Hugo");
const auto update_homers_age =
update<Person>("age"_c.set(46)) | where("first_name"_c == "Homer");
const auto get_data = sqlgen::read<std::vector<Person>> | order_by("id"_c);
const auto people2 = begin_transaction(conn)
.and_then(delete_hugo)
.and_then(update_homers_age)
.and_then(commit)
.and_then(get_data)
.value();
const std::string expected =
R"([{"id":0,"first_name":"Homer","last_name":"Simpson","age":46},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0}])";
EXPECT_EQ(rfl::json::write(people2), expected);
}
} // namespace test_transaction
#endif

View File

@@ -0,0 +1,61 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_update {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_update) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0},
Person{
.id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto conn =
sqlgen::mysql::connect(credentials).and_then(drop<Person> | if_exists);
sqlgen::write(conn, people1).value();
const auto update_hugos_age =
update<Person>("first_name"_c.set("last_name"_c), "age"_c.set(100)) |
where("first_name"_c == "Hugo");
const auto people2 =
conn.and_then(update_hugos_age)
.and_then(sqlgen::read<std::vector<Person>> | order_by("id"_c))
.value();
const std::string expected =
R"([{"id":0,"first_name":"Homer","last_name":"Simpson","age":45},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":4,"first_name":"Simpson","last_name":"Simpson","age":100}])";
EXPECT_EQ(rfl::json::write(people2), expected);
}
} // namespace test_update
#endif

View File

@@ -0,0 +1,28 @@
#include <gtest/gtest.h>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
namespace test_update_dry {
struct TestTable {
std::string field1;
int32_t field2;
sqlgen::PrimaryKey<uint32_t> id;
std::optional<std::string> nullable;
};
TEST(mysql, test_update_dry) {
using namespace sqlgen;
using namespace sqlgen::literals;
const auto query =
update<TestTable>("field1"_c.set("Hello"), "nullable"_c.set("field1"_c)) |
where("field2"_c > 0);
const auto expected =
R"(UPDATE `TestTable` SET `field1` = 'Hello', `nullable` = `field1` WHERE `field2` > 0;)";
EXPECT_EQ(sqlgen::mysql::to_sql(query), expected);
}
} // namespace test_update_dry

View File

@@ -0,0 +1,53 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_varchar {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
sqlgen::Varchar<6> first_name;
sqlgen::Varchar<7> last_name;
int age;
};
TEST(mysql, test_varchar) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto conn =
sqlgen::mysql::connect(credentials).and_then(drop<Person> | if_exists);
sqlgen::write(conn, people1).value();
const auto people2 = sqlgen::read<std::vector<Person>>(conn).value();
const auto json1 = rfl::json::write(people1);
const auto json2 = rfl::json::write(people2);
EXPECT_EQ(json1, json2);
}
} // namespace test_varchar
#endif

View File

@@ -0,0 +1,58 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_where {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_where) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0},
Person{
.id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto conn =
sqlgen::mysql::connect(credentials).and_then(drop<Person> | if_exists);
sqlgen::write(conn, people1).value();
const auto query = sqlgen::read<std::vector<Person>> |
where("age"_c < 18 and not("first_name"_c == "Hugo")) |
order_by("age"_c);
const auto people2 = query(conn).value();
const std::string expected =
R"([{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10}])";
EXPECT_EQ(rfl::json::write(people2), expected);
}
} // namespace test_where
#endif

View File

@@ -0,0 +1,30 @@
#include <gtest/gtest.h>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_where_dry {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_where_dry) {
using namespace sqlgen;
using namespace sqlgen::literals;
const auto query = sqlgen::read<std::vector<Person>> |
where("age"_c < 18 and "first_name"_c != "Hugo") |
order_by("age"_c);
const auto expected =
R"(SELECT `id`, `first_name`, `last_name`, `age` FROM `Person` WHERE (`age` < 18) AND (`first_name` != 'Hugo') ORDER BY `age`)";
EXPECT_EQ(sqlgen::mysql::to_sql(query), expected);
}
} // namespace test_where_dry

View File

@@ -0,0 +1,58 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_where_with_operations {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_where_with_operations) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0},
Person{
.id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto conn =
sqlgen::mysql::connect(credentials).and_then(drop<Person> | if_exists);
sqlgen::write(conn, people1).value();
const auto query = sqlgen::read<std::vector<Person>> |
where("age"_c * 2 + 4 < 40 and "first_name"_c != "Hugo") |
order_by("age"_c);
const auto people2 = query(conn).value();
const std::string expected =
R"([{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10}])";
EXPECT_EQ(rfl::json::write(people2), expected);
}
} // namespace test_where_with_operations
#endif

View File

@@ -0,0 +1,67 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <chrono>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_where_with_timestamps {
struct Person {
sqlgen::PrimaryKey<uint32_t, sqlgen::auto_incr> id;
std::string first_name;
std::string last_name;
sqlgen::Date birthday;
};
TEST(mysql, test_where_with_timestamps) {
const auto people1 =
std::vector<Person>({Person{.first_name = "Homer",
.last_name = "Simpson",
.birthday = sqlgen::Date("1970-01-01")},
Person{.first_name = "Bart",
.last_name = "Simpson",
.birthday = sqlgen::Date("2000-01-01")},
Person{.first_name = "Lisa",
.last_name = "Simpson",
.birthday = sqlgen::Date("2002-01-01")},
Person{.first_name = "Maggie",
.last_name = "Simpson",
.birthday = sqlgen::Date("2010-01-01")}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
using namespace std::literals::chrono_literals;
const auto conn =
sqlgen::mysql::connect(credentials).and_then(drop<Person> | if_exists);
sqlgen::write(conn, people1).value();
const auto query =
sqlgen::read<std::vector<Person>> |
where("birthday"_c + std::chrono::years(11) - std::chrono::weeks(10) +
std::chrono::milliseconds(4000000) >
Date("2010-01-01")) |
order_by("id"_c);
const auto people2 = query(conn).value();
const std::string expected =
R"([{"id":2,"first_name":"Bart","last_name":"Simpson","birthday":"2000-01-01"},{"id":3,"first_name":"Lisa","last_name":"Simpson","birthday":"2002-01-01"},{"id":4,"first_name":"Maggie","last_name":"Simpson","birthday":"2010-01-01"}])";
EXPECT_EQ(rfl::json::write(people2), expected);
}
} // namespace test_where_with_timestamps
#endif

View File

@@ -0,0 +1,51 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_write_and_read {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_write_and_read) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
using namespace sqlgen;
const auto credentials = mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
const auto conn =
mysql::connect(credentials).and_then(drop<Person> | if_exists);
write(conn, people1).value();
const auto people2 = sqlgen::read<std::vector<Person>>(conn).value();
const auto json1 = rfl::json::write(people1);
const auto json2 = rfl::json::write(people2);
EXPECT_EQ(json1, json2);
}
} // namespace test_write_and_read
#endif

View File

@@ -0,0 +1,51 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_write_and_read_curried {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_write_and_read_curried) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
using namespace sqlgen;
using namespace sqlgen::literals;
const auto people2 = mysql::connect(credentials)
.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(sqlgen::read<std::vector<Person>>)
.value();
const auto json1 = rfl::json::write(people1);
const auto json2 = rfl::json::write(people2);
EXPECT_EQ(json1, json2);
}
} // namespace test_write_and_read_curried
#endif

View File

@@ -0,0 +1,58 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>
namespace test_write_and_read_pool {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(mysql, test_write_and_read_pool) {
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},
Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{
.id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
const auto pool_config = sqlgen::ConnectionPoolConfig{.size = 2};
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
.user = "sqlgen",
.password = "password",
.dbname = "mysql"};
const auto pool = sqlgen::make_connection_pool<sqlgen::mysql::Connection>(
pool_config, credentials);
using namespace sqlgen;
using namespace sqlgen::literals;
const auto people2 = session(pool)
.and_then(drop<Person> | if_exists)
.and_then(write(std::ref(people1)))
.and_then(sqlgen::read<std::vector<Person>>)
.value();
EXPECT_EQ(pool.value().available(), 2);
const auto json1 = rfl::json::write(people1);
const auto json2 = rfl::json::write(people2);
EXPECT_EQ(json1, json2);
}
} // namespace test_write_and_read_pool
#endif

View File

@@ -16,7 +16,7 @@ struct Person {
sqlgen::PrimaryKey<uint32_t, sqlgen::auto_incr> id;
std::string first_name;
std::string last_name;
sqlgen::Timestamp<"%Y-%m-%d"> birthday;
sqlgen::Date birthday;
};
TEST(postgres, test_range_select_from_with_timestamps) {
@@ -64,8 +64,9 @@ TEST(postgres, test_range_select_from_with_timestamps) {
days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days">,
unixepoch("birthday"_c + std::chrono::days(10)) |
as<"birthday_unixepoch">,
hour("birthday"_c) | as<"hour">, minute("birthday"_c) | as<"minute">,
second("birthday"_c) | as<"second">,
hour(cast<DateTime>("birthday"_c)) | as<"hour">,
minute(cast<DateTime>("birthday"_c)) | as<"minute">,
second(cast<DateTime>("birthday"_c)) | as<"second">,
weekday("birthday"_c) | as<"weekday">) |
order_by("id"_c) | to<std::vector<Birthday>>;
@@ -76,7 +77,7 @@ TEST(postgres, test_range_select_from_with_timestamps) {
.value();
const std::string expected_query =
R"(SELECT "birthday" + INTERVAL '10 days' AS "birthday", cast((cast(extract(YEAR from "birthday") as TEXT) || '-' || cast(extract(MONTH from "birthday") as TEXT) || '-' || cast(extract(DAY from "birthday") as TEXT)) as TIMESTAMP) + INTERVAL '10 days' AS "birthday_recreated", cast('2011-01-01' as DATE) - cast("birthday" as DATE) AS "age_in_days", extract(EPOCH FROM "birthday" + INTERVAL '10 days') AS "birthday_unixepoch", extract(HOUR from "birthday") AS "hour", extract(MINUTE from "birthday") AS "minute", extract(SECOND from "birthday") AS "second", extract(DOW from "birthday") AS "weekday" FROM "Person" ORDER BY "id")";
R"(SELECT "birthday" + INTERVAL '10 days' AS "birthday", cast((cast(extract(YEAR from "birthday") as TEXT) || '-' || cast(extract(MONTH from "birthday") as TEXT) || '-' || cast(extract(DAY from "birthday") as TEXT)) as DATE) + INTERVAL '10 days' AS "birthday_recreated", cast('2011-01-01' as DATE) - cast("birthday" as DATE) AS "age_in_days", extract(EPOCH FROM "birthday" + INTERVAL '10 days') AS "birthday_unixepoch", extract(HOUR from cast("birthday" as TIMESTAMP)) AS "hour", extract(MINUTE from cast("birthday" as TIMESTAMP)) AS "minute", extract(SECOND from cast("birthday" as TIMESTAMP)) AS "second", extract(DOW from "birthday") AS "weekday" FROM "Person" ORDER BY "id")";
const std::string expected =
R"([{"birthday":"1970-01-11","birthday_recreated":"1970-01-11","birthday_unixepoch":864000,"age_in_days":14975.0,"hour":0,"minute":0,"second":0,"weekday":4},{"birthday":"2000-01-11","birthday_recreated":"2000-01-11","birthday_unixepoch":947548800,"age_in_days":4018.0,"hour":0,"minute":0,"second":0,"weekday":6},{"birthday":"2002-01-11","birthday_recreated":"2002-01-11","birthday_unixepoch":1010707200,"age_in_days":3287.0,"hour":0,"minute":0,"second":0,"weekday":2},{"birthday":"2010-01-11","birthday_recreated":"2010-01-11","birthday_unixepoch":1263168000,"age_in_days":365.0,"hour":0,"minute":0,"second":0,"weekday":5}])";

View File

@@ -6,6 +6,15 @@
}
],
"features": {
"mysql": {
"description": "Enable MySQL/MariaDB support",
"dependencies": [
{
"name": "libmariadb",
"version>=": "3.4.5"
}
]
},
"postgres": {
"description": "Enable PostgreSQL support",
"dependencies": [