From 8568a833629f9b6fe9a987683d823c64818dd408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Thu, 20 Nov 2025 21:20:12 +0100 Subject: [PATCH] Added support for DuckDB (#90) --- .github/workflows/windows-cxx20-vcpkg.yaml | 7 - CMakeLists.txt | 16 + README.md | 1 + docs/README.md | 1 + docs/duckdb.md | 142 +++ docs/dynamic.md | 54 + docs/mysql.md | 2 +- docs/postgres.md | 2 +- docs/sqlite.md | 2 +- include/sqlgen/duckdb.hpp | 8 + include/sqlgen/duckdb/ColumnData.hpp | 33 + include/sqlgen/duckdb/Connection.hpp | 209 ++++ include/sqlgen/duckdb/DuckDBAppender.hpp | 84 ++ include/sqlgen/duckdb/DuckDBConnection.hpp | 52 + include/sqlgen/duckdb/DuckDBResult.hpp | 69 ++ include/sqlgen/duckdb/Iterator.hpp | 113 +++ include/sqlgen/duckdb/cast_duckdb_type.hpp | 103 ++ include/sqlgen/duckdb/check_duckdb_type.hpp | 68 ++ include/sqlgen/duckdb/chunk_ptrs_t.hpp | 32 + include/sqlgen/duckdb/connect.hpp | 16 + include/sqlgen/duckdb/from_chunk_ptrs.hpp | 49 + include/sqlgen/duckdb/make_chunk_ptrs.hpp | 90 ++ include/sqlgen/duckdb/parsing/Parser.hpp | 15 + include/sqlgen/duckdb/parsing/Parser_base.hpp | 11 + include/sqlgen/duckdb/parsing/Parser_date.hpp | 42 + .../sqlgen/duckdb/parsing/Parser_default.hpp | 94 ++ include/sqlgen/duckdb/parsing/Parser_enum.hpp | 44 + include/sqlgen/duckdb/parsing/Parser_json.hpp | 39 + .../sqlgen/duckdb/parsing/Parser_optional.hpp | 43 + .../duckdb/parsing/Parser_reflection_type.hpp | 36 + .../duckdb/parsing/Parser_smart_ptr.hpp | 46 + .../sqlgen/duckdb/parsing/Parser_string.hpp | 40 + .../duckdb/parsing/Parser_timestamp.hpp | 40 + include/sqlgen/duckdb/to_sql.hpp | 27 + include/sqlgen/internal/to_container.hpp | 5 +- include/sqlgen/postgres/Connection.hpp | 1 + src/sqlgen/duckdb/Connection.cpp | 40 + src/sqlgen/duckdb/DuckDBConnection.cpp | 43 + src/sqlgen/duckdb/exec.cpp | 27 + src/sqlgen/duckdb/to_sql.cpp | 960 ++++++++++++++++++ src/sqlgen_duckdb.cpp | 5 + tests/CMakeLists.txt | 4 + tests/duckdb/CMakeLists.txt | 19 + tests/duckdb/test_aggregations.cpp | 64 ++ .../test_aggregations_with_nullable.cpp | 63 ++ tests/duckdb/test_alpha_numeric.cpp | 39 + tests/duckdb/test_alpha_numeric_query.cpp | 47 + tests/duckdb/test_auto_incr_primary_key.cpp | 45 + tests/duckdb/test_boolean.cpp | 52 + tests/duckdb/test_boolean_conditions.cpp | 54 + tests/duckdb/test_boolean_update.cpp | 59 ++ tests/duckdb/test_cache.cpp | 45 + tests/duckdb/test_create_index.cpp | 34 + tests/duckdb/test_create_table.cpp | 31 + tests/duckdb/test_create_table_as.cpp | 60 ++ tests/duckdb/test_create_view_as.cpp | 65 ++ tests/duckdb/test_delete_from.cpp | 48 + tests/duckdb/test_drop.cpp | 41 + tests/duckdb/test_dynamic_type.cpp | 129 +++ tests/duckdb/test_enum_crosstable.cpp | 100 ++ tests/duckdb/test_enum_lookup.cpp | 84 ++ tests/duckdb/test_enum_namespace.cpp | 59 ++ tests/duckdb/test_flatten.cpp | 45 + tests/duckdb/test_foreign_key.cpp | 58 ++ tests/duckdb/test_full_join.cpp | 69 ++ tests/duckdb/test_group_by.cpp | 67 ++ .../duckdb/test_group_by_with_operations.cpp | 63 ++ tests/duckdb/test_hello_world.cpp | 32 + tests/duckdb/test_in.cpp | 48 + tests/duckdb/test_in_vec.cpp | 49 + tests/duckdb/test_insert_and_read.cpp | 44 + tests/duckdb/test_insert_by_ref_and_read.cpp | 44 + tests/duckdb/test_insert_fail.cpp | 57 ++ tests/duckdb/test_insert_or_replace.cpp | 70 ++ tests/duckdb/test_is_null.cpp | 55 + tests/duckdb/test_join.cpp | 51 + tests/duckdb/test_joins_from.cpp | 86 ++ tests/duckdb/test_joins_nested.cpp | 82 ++ tests/duckdb/test_joins_nested_grouped.cpp | 81 ++ tests/duckdb/test_joins_two_tables.cpp | 77 ++ .../duckdb/test_joins_two_tables_grouped.cpp | 78 ++ tests/duckdb/test_json.cpp | 45 + tests/duckdb/test_left_join.cpp | 69 ++ tests/duckdb/test_like.cpp | 65 ++ tests/duckdb/test_limit.cpp | 47 + tests/duckdb/test_not_in.cpp | 48 + tests/duckdb/test_not_in_vec.cpp | 49 + tests/duckdb/test_operations.cpp | 76 ++ .../duckdb/test_operations_with_nullable.cpp | 63 ++ tests/duckdb/test_order_by.cpp | 47 + tests/duckdb/test_range.cpp | 47 + tests/duckdb/test_range_select_from.cpp | 52 + .../duckdb/test_range_select_from_with_to.cpp | 51 + tests/duckdb/test_right_join.cpp | 69 ++ .../test_select_from_with_timestamps.cpp | 79 ++ tests/duckdb/test_single_read.cpp | 43 + tests/duckdb/test_timestamp.cpp | 49 + tests/duckdb/test_to_create_table.cpp | 25 + tests/duckdb/test_to_insert.cpp | 27 + tests/duckdb/test_to_insert_or_replace.cpp | 33 + tests/duckdb/test_to_select_from.cpp | 24 + .../test_to_select_from_with_schema.cpp | 27 + tests/duckdb/test_transaction.cpp | 55 + tests/duckdb/test_unique.cpp | 46 + tests/duckdb/test_update.cpp | 50 + tests/duckdb/test_varchar.cpp | 39 + tests/duckdb/test_where.cpp | 48 + tests/duckdb/test_where_with_nullable.cpp | 48 + .../test_where_with_nullable_operations.cpp | 49 + tests/duckdb/test_where_with_operations.cpp | 48 + tests/duckdb/test_where_with_timestamps.cpp | 60 ++ tests/duckdb/test_write_and_read.cpp | 39 + tests/duckdb/test_write_and_read_curried.cpp | 42 + tests/duckdb/test_write_and_read_to_file.cpp | 44 + vcpkg.json | 9 + 115 files changed, 6609 insertions(+), 12 deletions(-) create mode 100644 docs/duckdb.md create mode 100644 include/sqlgen/duckdb.hpp create mode 100644 include/sqlgen/duckdb/ColumnData.hpp create mode 100644 include/sqlgen/duckdb/Connection.hpp create mode 100644 include/sqlgen/duckdb/DuckDBAppender.hpp create mode 100644 include/sqlgen/duckdb/DuckDBConnection.hpp create mode 100644 include/sqlgen/duckdb/DuckDBResult.hpp create mode 100644 include/sqlgen/duckdb/Iterator.hpp create mode 100644 include/sqlgen/duckdb/cast_duckdb_type.hpp create mode 100644 include/sqlgen/duckdb/check_duckdb_type.hpp create mode 100644 include/sqlgen/duckdb/chunk_ptrs_t.hpp create mode 100644 include/sqlgen/duckdb/connect.hpp create mode 100644 include/sqlgen/duckdb/from_chunk_ptrs.hpp create mode 100644 include/sqlgen/duckdb/make_chunk_ptrs.hpp create mode 100644 include/sqlgen/duckdb/parsing/Parser.hpp create mode 100644 include/sqlgen/duckdb/parsing/Parser_base.hpp create mode 100644 include/sqlgen/duckdb/parsing/Parser_date.hpp create mode 100644 include/sqlgen/duckdb/parsing/Parser_default.hpp create mode 100644 include/sqlgen/duckdb/parsing/Parser_enum.hpp create mode 100644 include/sqlgen/duckdb/parsing/Parser_json.hpp create mode 100644 include/sqlgen/duckdb/parsing/Parser_optional.hpp create mode 100644 include/sqlgen/duckdb/parsing/Parser_reflection_type.hpp create mode 100644 include/sqlgen/duckdb/parsing/Parser_smart_ptr.hpp create mode 100644 include/sqlgen/duckdb/parsing/Parser_string.hpp create mode 100644 include/sqlgen/duckdb/parsing/Parser_timestamp.hpp create mode 100644 include/sqlgen/duckdb/to_sql.hpp create mode 100644 src/sqlgen/duckdb/Connection.cpp create mode 100644 src/sqlgen/duckdb/DuckDBConnection.cpp create mode 100644 src/sqlgen/duckdb/exec.cpp create mode 100644 src/sqlgen/duckdb/to_sql.cpp create mode 100644 src/sqlgen_duckdb.cpp create mode 100644 tests/duckdb/CMakeLists.txt create mode 100644 tests/duckdb/test_aggregations.cpp create mode 100644 tests/duckdb/test_aggregations_with_nullable.cpp create mode 100644 tests/duckdb/test_alpha_numeric.cpp create mode 100644 tests/duckdb/test_alpha_numeric_query.cpp create mode 100644 tests/duckdb/test_auto_incr_primary_key.cpp create mode 100644 tests/duckdb/test_boolean.cpp create mode 100644 tests/duckdb/test_boolean_conditions.cpp create mode 100644 tests/duckdb/test_boolean_update.cpp create mode 100644 tests/duckdb/test_cache.cpp create mode 100644 tests/duckdb/test_create_index.cpp create mode 100644 tests/duckdb/test_create_table.cpp create mode 100644 tests/duckdb/test_create_table_as.cpp create mode 100644 tests/duckdb/test_create_view_as.cpp create mode 100644 tests/duckdb/test_delete_from.cpp create mode 100644 tests/duckdb/test_drop.cpp create mode 100644 tests/duckdb/test_dynamic_type.cpp create mode 100644 tests/duckdb/test_enum_crosstable.cpp create mode 100644 tests/duckdb/test_enum_lookup.cpp create mode 100644 tests/duckdb/test_enum_namespace.cpp create mode 100644 tests/duckdb/test_flatten.cpp create mode 100644 tests/duckdb/test_foreign_key.cpp create mode 100644 tests/duckdb/test_full_join.cpp create mode 100644 tests/duckdb/test_group_by.cpp create mode 100644 tests/duckdb/test_group_by_with_operations.cpp create mode 100644 tests/duckdb/test_hello_world.cpp create mode 100644 tests/duckdb/test_in.cpp create mode 100644 tests/duckdb/test_in_vec.cpp create mode 100644 tests/duckdb/test_insert_and_read.cpp create mode 100644 tests/duckdb/test_insert_by_ref_and_read.cpp create mode 100644 tests/duckdb/test_insert_fail.cpp create mode 100644 tests/duckdb/test_insert_or_replace.cpp create mode 100644 tests/duckdb/test_is_null.cpp create mode 100644 tests/duckdb/test_join.cpp create mode 100644 tests/duckdb/test_joins_from.cpp create mode 100644 tests/duckdb/test_joins_nested.cpp create mode 100644 tests/duckdb/test_joins_nested_grouped.cpp create mode 100644 tests/duckdb/test_joins_two_tables.cpp create mode 100644 tests/duckdb/test_joins_two_tables_grouped.cpp create mode 100644 tests/duckdb/test_json.cpp create mode 100644 tests/duckdb/test_left_join.cpp create mode 100644 tests/duckdb/test_like.cpp create mode 100644 tests/duckdb/test_limit.cpp create mode 100644 tests/duckdb/test_not_in.cpp create mode 100644 tests/duckdb/test_not_in_vec.cpp create mode 100644 tests/duckdb/test_operations.cpp create mode 100644 tests/duckdb/test_operations_with_nullable.cpp create mode 100644 tests/duckdb/test_order_by.cpp create mode 100644 tests/duckdb/test_range.cpp create mode 100644 tests/duckdb/test_range_select_from.cpp create mode 100644 tests/duckdb/test_range_select_from_with_to.cpp create mode 100644 tests/duckdb/test_right_join.cpp create mode 100644 tests/duckdb/test_select_from_with_timestamps.cpp create mode 100644 tests/duckdb/test_single_read.cpp create mode 100644 tests/duckdb/test_timestamp.cpp create mode 100644 tests/duckdb/test_to_create_table.cpp create mode 100644 tests/duckdb/test_to_insert.cpp create mode 100644 tests/duckdb/test_to_insert_or_replace.cpp create mode 100644 tests/duckdb/test_to_select_from.cpp create mode 100644 tests/duckdb/test_to_select_from_with_schema.cpp create mode 100644 tests/duckdb/test_transaction.cpp create mode 100644 tests/duckdb/test_unique.cpp create mode 100644 tests/duckdb/test_update.cpp create mode 100644 tests/duckdb/test_varchar.cpp create mode 100644 tests/duckdb/test_where.cpp create mode 100644 tests/duckdb/test_where_with_nullable.cpp create mode 100644 tests/duckdb/test_where_with_nullable_operations.cpp create mode 100644 tests/duckdb/test_where_with_operations.cpp create mode 100644 tests/duckdb/test_where_with_timestamps.cpp create mode 100644 tests/duckdb/test_write_and_read.cpp create mode 100644 tests/duckdb/test_write_and_read_curried.cpp create mode 100644 tests/duckdb/test_write_and_read_to_file.cpp diff --git a/.github/workflows/windows-cxx20-vcpkg.yaml b/.github/workflows/windows-cxx20-vcpkg.yaml index f8a2f71..ef97f4a 100644 --- a/.github/workflows/windows-cxx20-vcpkg.yaml +++ b/.github/workflows/windows-cxx20-vcpkg.yaml @@ -14,7 +14,6 @@ jobs: - db: postgres - db: sqlite - db: mysql - - db: headers name: "(windows-${{ matrix.db }})" concurrency: group: ci-${{ github.ref }}-windows-${{ matrix.db }} @@ -34,11 +33,6 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - uses: ilammy/msvc-dev-cmd@v1 - uses: lukka/run-vcpkg@v11 - - name: Compile - if: matrix.db == 'headers' - run: | - cmake -S . -B build -DCMAKE_CXX_STANDARD=20 -DSQLGEN_CHECK_HEADERS=ON - cmake --build build --config Release -j4 - name: Compile if: matrix.db == 'postgres' run: | @@ -55,6 +49,5 @@ jobs: cmake -S . -B build -DCMAKE_CXX_STANDARD=20 -DCMAKE_BUILD_TYPE=Release -DSQLGEN_BUILD_TESTS=ON -DSQLGEN_MYSQL=ON -DSQLGEN_POSTGRES=OFF -DSQLGEN_SQLITE3=OFF -DSQLGEN_BUILD_DRY_TESTS_ONLY=ON -DBUILD_SHARED_LIBS=ON -DVCPKG_TARGET_TRIPLET=x64-windows-release cmake --build build --config Release -j4 - name: Run tests - if: matrix.db != 'headers' run: | ctest --test-dir build --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a2237d..0c114d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,8 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) option(SQLGEN_BUILD_SHARED "Build shared library" ${BUILD_SHARED_LIBS}) +option(SQLGEN_DUCKDB "Enable DuckDB support" OFF) + option(SQLGEN_MYSQL "Enable MySQL support" OFF) option(SQLGEN_POSTGRES "Enable PostgreSQL support" ON) # enabled by default @@ -30,6 +32,10 @@ if (SQLGEN_USE_VCPKG) list(APPEND VCPKG_MANIFEST_FEATURES "tests") endif() + if (SQLGEN_DUCKDB OR SQLGEN_CHECK_HEADERS) + list(APPEND VCPKG_MANIFEST_FEATURES "duckdb") + endif() + if (SQLGEN_MYSQL OR SQLGEN_CHECK_HEADERS) list(APPEND VCPKG_MANIFEST_FEATURES "mysql") endif() @@ -89,6 +95,16 @@ target_include_directories( $ $) + +if (SQLGEN_DUCKDB OR SQLGEN_CHECK_HEADERS) + list(APPEND SQLGEN_SOURCES src/sqlgen_duckdb.cpp) + if (NOT TARGET DuckDB) + find_package(DuckDB REQUIRED) + endif() + target_link_libraries(sqlgen PUBLIC $,duckdb,duckdb_static>) +endif() + + if(SQLGEN_MYSQL OR SQLGEN_CHECK_HEADERS) list(APPEND SQLGEN_SOURCES src/sqlgen_mysql.cpp) if (SQLGEN_USE_VCPKG) diff --git a/README.md b/README.md index 9d6aad3..2b9d192 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ The following table lists the databases currently supported by sqlgen and the un | Database | Library | Version | License | Remarks | |---------------|--------------------------------------------------------------------------|--------------|---------------| -----------------------------------------------------| +| DuckDB | [duckdb](https://github.com/duckdb/duckdb) | >= 1.4.1 | MIT | | | 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 | | diff --git a/docs/README.md b/docs/README.md index 8c98144..e1e079a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -59,6 +59,7 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab ## Supported Databases +- [DuckDB](duckdb.md) - How to interact with DuckDB - [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 diff --git a/docs/duckdb.md b/docs/duckdb.md new file mode 100644 index 0000000..09ba808 --- /dev/null +++ b/docs/duckdb.md @@ -0,0 +1,142 @@ +∂# `sqlgen::duckdb` + +The `sqlgen::duckdb` module provides a type-safe and efficient interface for interacting with DuckDB 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 DuckDB database: + +```cpp +// Connect to an in-memory database +const auto conn = sqlgen::duckdb::connect(); + +// Connect to a file-based database +const auto conn = sqlgen::duckdb::connect("database.db"); +``` + +The type of `conn` is `sqlgen::Result>`, which is useful for error handling: + +```cpp +// Handle connection errors +const auto conn = sqlgen::duckdb::connect("database.db"); +if (!conn) { + // Handle error... + return; +} + +using namespace sqlgen; +using namespace sqlgen::literals; + +const auto query = sqlgen::read> | + 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 id; + std::string first_name; + std::string last_name; + int age; +}; + +const auto people = std::vector{ + 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> | + order_by("age"_c); + +// Read minors only +const auto minors = sqlgen::read> | + 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 | + where("first_name"_c == "Hugo"); + +const auto update_homer = update("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(); +``` + +### Update Operations + +Update data in a table: + +```cpp +using namespace sqlgen; +using namespace sqlgen::literals; + +// Update multiple columns +const auto query = update("first_name"_c.set("last_name"_c), "age"_c.set(100)) | + where("first_name"_c == "Hugo"); + +query(conn).value(); +``` + +## Notes + +- The module provides a type-safe interface for DuckDB operations +- All operations return `sqlgen::Result` for error handling +- Prepared statements are used for efficient query execution +- The iterator interface supports batch processing of results +- SQL generation adapts to DuckDB's dialect +- The module supports: + - In-memory and file-based databases + - Transactions (begin, commit, rollback) + - Efficient batch operations + - Type-safe SQL generation + - Error handling through `Result` + - Resource management through `Ref` + - 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 + - JSON data types + - Foreign keys and referential integrity + - Unique constraints + - Views and materialized views + - Indexes +``` \ No newline at end of file diff --git a/docs/dynamic.md b/docs/dynamic.md index 03e51af..86b2594 100644 --- a/docs/dynamic.md +++ b/docs/dynamic.md @@ -45,6 +45,50 @@ struct Parser { } // namespace sqlgen::parsing ``` +### DuckDB parser specialization + +**Important:** If you're using DuckDB, you must also implement a separate parser specialization in the `sqlgen::duckdb::parsing` namespace. This is required for performance reasons, as DuckDB uses its own native types and appender interface. + +The DuckDB parser has a different interface than the generic parser: + +```cpp +#include +#include +#include +#include +#include + +namespace sqlgen::duckdb::parsing { + +template <> +struct Parser { + using ResultingType = duckdb_string_t; + + static Result read(const ResultingType* _r) noexcept { + return Parser::read(_r).and_then( + [&](const std::string& _str) -> Result { + try { + return boost::lexical_cast(_str); + } catch (const std::exception& e) { + return error(e.what()); + } + }); + } + + static Result write(const boost::uuids::uuid& _u, + duckdb_appender _appender) noexcept { + return Parser::write(boost::uuids::to_string(_u), _appender); + } +}; + +} // namespace sqlgen::duckdb::parsing +``` + +Key differences from the generic parser: +- `read` takes `const ResultingType*` (where `ResultingType = duckdb_string_t`) instead of `const std::optional&` +- `write` takes a `duckdb_appender` parameter and returns `Result` instead of `std::optional` +- No `to_type()` method is required (the generic parser's `to_type()` is used for schema generation) + The second step is to specialize `sqlgen::transpilation::ToValue` for `boost::uuids::uuid` and implement `operator()`: ```cpp @@ -157,6 +201,15 @@ static dynamic::Type to_type() noexcept { } ``` +- DuckDB: +```cpp +static dynamic::Type to_type() noexcept { + return sqlgen::dynamic::types::Dynamic{"TEXT"}; +} +``` + +Note: For DuckDB, you must also implement the `sqlgen::duckdb::parsing::Parser` specialization as shown in the DuckDB parser specialization section above. + ## Parser specialization requirements Specializing `sqlgen::parsing::Parser` requires three methods. These guidelines help ensure correctness and portability: @@ -200,4 +253,5 @@ Additional best practices: - Works with all operations: `create_table`, `insert`, `select`, `update`, `delete` - The type name is passed directly to the database; ensure it is valid for the target dialect - Keep specializations in the `sqlgen::parsing` namespace +- **DuckDB users:** You must implement both `sqlgen::parsing::Parser` and `sqlgen::duckdb::parsing::Parser` specializations for your custom type diff --git a/docs/mysql.md b/docs/mysql.md index 85e1c47..87edb17 100644 --- a/docs/mysql.md +++ b/docs/mysql.md @@ -23,7 +23,7 @@ const auto creds = sqlgen::mysql::Credentials{ const auto conn = sqlgen::mysql::connect(creds); ``` -The connection is wrapped in a `sqlgen::Result>` for error handling: +The type of `conn` is `sqlgen::Result>`, which is useful for error handling: ```cpp // Handle connection errors diff --git a/docs/postgres.md b/docs/postgres.md index 9cea693..f8fde58 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -22,7 +22,7 @@ const auto creds = sqlgen::postgres::Credentials{ const auto conn = sqlgen::postgres::connect(creds); ``` -The connection is wrapped in a `sqlgen::Result>` for error handling: +The type of `conn` is `sqlgen::Result>`, which is useful for error handling: ```cpp // Handle connection errors diff --git a/docs/sqlite.md b/docs/sqlite.md index 7ebfeb8..833d787 100644 --- a/docs/sqlite.md +++ b/docs/sqlite.md @@ -16,7 +16,7 @@ const auto conn = sqlgen::sqlite::connect(); const auto conn = sqlgen::sqlite::connect("database.db"); ``` -The connection is wrapped in a `sqlgen::Result>` for error handling: +The type of `conn` is `sqlgen::Result>`, which is useful for error handling: ```cpp // Handle connection errors diff --git a/include/sqlgen/duckdb.hpp b/include/sqlgen/duckdb.hpp new file mode 100644 index 0000000..02b22f1 --- /dev/null +++ b/include/sqlgen/duckdb.hpp @@ -0,0 +1,8 @@ +#ifndef SQLGEN_DUCKDB_HPP_ +#define SQLGEN_DUCKDB_HPP_ + +#include "../sqlgen.hpp" +#include "duckdb/connect.hpp" +#include "duckdb/to_sql.hpp" + +#endif diff --git a/include/sqlgen/duckdb/ColumnData.hpp b/include/sqlgen/duckdb/ColumnData.hpp new file mode 100644 index 0000000..b7ee203 --- /dev/null +++ b/include/sqlgen/duckdb/ColumnData.hpp @@ -0,0 +1,33 @@ +#ifndef SQLGEN_DUCKDB_COLUMNDATA_HPP_ +#define SQLGEN_DUCKDB_COLUMNDATA_HPP_ + +#include + +#include +#include +#include +#include + +namespace sqlgen::duckdb { + +template +struct ColumnData { + using ColName = _ColName; + + duckdb_vector vec; + T *data; + uint64_t *validity; + + // This is only needed if the data returned by DuckDB is not of the + // same type as T, but can be converted to T. In this case, + // data actually points to ptr->data(). Otherwise, ptr is a nullptr. + std::shared_ptr> ptr; + + bool is_not_null(idx_t _i) const { + return (validity == nullptr) || duckdb_validity_row_is_valid(validity, _i); + } +}; + +} // namespace sqlgen::duckdb + +#endif diff --git a/include/sqlgen/duckdb/Connection.hpp b/include/sqlgen/duckdb/Connection.hpp new file mode 100644 index 0000000..95eff62 --- /dev/null +++ b/include/sqlgen/duckdb/Connection.hpp @@ -0,0 +1,209 @@ +#ifndef SQLGEN_DUCKDB_CONNECTION_HPP_ +#define SQLGEN_DUCKDB_CONNECTION_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../Range.hpp" +#include "../Ref.hpp" +#include "../Result.hpp" +#include "../Transaction.hpp" +#include "../dynamic/Operation.hpp" +#include "../dynamic/SelectFrom.hpp" +#include "../dynamic/Write.hpp" +#include "../internal/iterator_t.hpp" +#include "../internal/remove_auto_incr_primary_t.hpp" +#include "../internal/to_container.hpp" +#include "../is_connection.hpp" +#include "../sqlgen_api.hpp" +#include "./parsing/Parser_default.hpp" +#include "DuckDBAppender.hpp" +#include "DuckDBConnection.hpp" +#include "DuckDBResult.hpp" +#include "Iterator.hpp" +#include "to_sql.hpp" + +namespace sqlgen::duckdb { + +class SQLGEN_API Connection { + using ConnPtr = Ref; + + public: + Connection(const ConnPtr &_conn) : appender_(nullptr), conn_(_conn) {} + + static rfl::Result> make( + const std::optional &_fname) noexcept; + + ~Connection() = default; + + Result begin_transaction() noexcept; + + Result commit() noexcept; + + Result execute(const std::string &_sql) noexcept; + + template + Result insert(const dynamic::Insert &_insert_stmt, ItBegin _begin, + ItEnd _end) noexcept { + using namespace std::ranges::views; + + const auto sql = to_sql(_insert_stmt); + + auto columns = internal::collect::vector( + _insert_stmt.columns | + transform([](const auto &_str) { return _str.c_str(); })); + + return get_duckdb_logical_types(_insert_stmt.table, _insert_stmt.columns) + .and_then([&](const auto &_types) { + return DuckDBAppender::make(sql, conn_, columns, _types); + }) + .and_then([&](auto _appender) { + return write_to_appender(_begin, _end, _appender->appender()) + .and_then([&](const auto &) { return _appender->close(); }); + }); + } + + template + auto read(const dynamic::SelectFrom &_query) { + using ValueType = transpilation::value_t; + return internal::to_container>( + Iterator(to_sql(_query), conn_)); + } + + Result rollback() noexcept; + + std::string to_sql(const dynamic::Statement &_stmt) noexcept { + return duckdb::to_sql_impl(_stmt); + } + + Result start_write(const dynamic::Write &_write_stmt) { + if (appender_) { + return error( + "Write operation already in progress - you cannot start another."); + } + + using namespace std::ranges::views; + + auto columns = internal::collect::vector( + _write_stmt.columns | + transform([](const auto &_str) { return _str.c_str(); })); + + const auto sql = to_sql(_write_stmt); + + return get_duckdb_logical_types(_write_stmt.table, _write_stmt.columns) + .and_then([&](auto _types) { + return DuckDBAppender::make(sql, conn_, columns, _types); + }) + .transform([&](auto &&_appender) { + appender_ = _appender.ptr(); + return Nothing{}; + }); + } + + Result end_write() { + if (!appender_) { + return error("No write operation in progress - nothing to end."); + } + appender_ = nullptr; + return Nothing{}; + } + + template + Result write(ItBegin _begin, ItEnd _end) { + if (!appender_) { + return error("No write operation in progress - nothing to write."); + } + return write_to_appender(_begin, _end, appender_->appender()); + } + + private: + Result> get_duckdb_logical_types( + const dynamic::Table &_table, const std::vector &_columns) { + using namespace std::ranges::views; + + const auto fields = internal::collect::vector( + _columns | transform([](const auto &_name) { + return dynamic::SelectFrom::Field{ + .val = dynamic::Operation{dynamic::Column{.alias = std::nullopt, + .name = _name}}, + .as = std::nullopt}; + })); + + const auto select_from = dynamic::SelectFrom{ + .table_or_query = _table, .fields = fields, .limit = dynamic::Limit{0}}; + + return DuckDBResult::make(to_sql(select_from), conn_) + .transform([&](const auto &_res) { + return internal::collect::vector( + iota(static_cast(0), static_cast(fields.size())) | + transform( + std::bind_front(duckdb_column_logical_type, &_res->res()))); + }); + } + + template + Result write_to_appender(ItBegin _begin, ItEnd _end, + duckdb_appender _appender) { + for (auto it = _begin; it < _end; ++it) { + const auto res = write_row(*it, _appender); + if (!res) { + return res; + } + const auto state = duckdb_appender_end_row(_appender); + if (state == DuckDBError) { + return error(duckdb_appender_error(_appender)); + } + } + return Nothing{}; + } + + template + Result write_row(const StructT &_struct, + duckdb_appender _appender) noexcept { + using ViewType = + internal::remove_auto_incr_primary_t>; + try { + ViewType(rfl::to_view(_struct)).apply([&](const auto &_field) { + using ValueType = std::remove_cvref_t::Type>>; + duckdb::parsing::Parser::write(*_field.value(), _appender) + .value(); + }); + } catch (const std::exception &e) { + return error(e.what()); + } + return Nothing{}; + } + + private: + /// The appender to be used for the write statements + std::shared_ptr appender_; + + /// The underlying duckdb3 connection. + ConnPtr conn_; +}; + +static_assert(is_connection, + "Must fulfill the is_connection concept."); +static_assert(is_connection>, + "Must fulfill the is_connection concept."); + +} // namespace sqlgen::duckdb + +namespace sqlgen::internal { +template +struct IteratorType { + using Type = duckdb::Iterator; +}; + +} // namespace sqlgen::internal + +#endif diff --git a/include/sqlgen/duckdb/DuckDBAppender.hpp b/include/sqlgen/duckdb/DuckDBAppender.hpp new file mode 100644 index 0000000..3f5aa91 --- /dev/null +++ b/include/sqlgen/duckdb/DuckDBAppender.hpp @@ -0,0 +1,84 @@ +#ifndef SQLGEN_DUCKDB_DUCKDBAPPENDER_HPP_ +#define SQLGEN_DUCKDB_DUCKDBAPPENDER_HPP_ + +#include + +#include + +#include "../sqlgen_api.hpp" +#include "DuckDBConnection.hpp" + +namespace sqlgen::duckdb { + +class SQLGEN_API DuckDBAppender { + using ConnPtr = Ref; + + public: + static Result> make( + const std::string& _sql, const ConnPtr& _conn, + const std::vector& _columns, + const std::vector& _types) { + try { + return Ref::make(_sql, _conn, _columns, _types); + } catch (const std::exception& e) { + return error(e.what()); + } + } + + DuckDBAppender(const std::string& _sql, const ConnPtr& _conn, + std::vector _columns, + std::vector _types) + : destroy_(false) { + if (duckdb_appender_create_query( + _conn->conn(), _sql.c_str(), static_cast(_columns.size()), + _types.data(), "sqlgen_appended_data", _columns.data(), + &appender_) == DuckDBError) { + throw std::runtime_error("Could not create appender."); + } + destroy_ = true; + } + + ~DuckDBAppender() { + if (destroy_) { + duckdb_appender_destroy(&appender_); + } + } + + DuckDBAppender(const DuckDBAppender& _other) = delete; + + DuckDBAppender(DuckDBAppender&& _other) + : destroy_(_other.destroy_), appender_(_other.appender_) { + _other.destroy_ = false; + } + + DuckDBAppender& operator=(const DuckDBAppender& _other) = delete; + + DuckDBAppender& operator=(DuckDBAppender&& _other) { + if (this == &_other) { + return *this; + } + destroy_ = _other.destroy_; + appender_ = _other.appender_; + _other.destroy_ = false; + return *this; + } + + duckdb_appender& appender() { return appender_; } + + Result close() { + const auto state = duckdb_appender_close(appender_); + if (state == DuckDBError) { + return error(duckdb_appender_error(appender_)); + } + return Nothing{}; + } + + private: + bool destroy_; + + duckdb_appender appender_; +}; + +} // namespace sqlgen::duckdb + +#endif diff --git a/include/sqlgen/duckdb/DuckDBConnection.hpp b/include/sqlgen/duckdb/DuckDBConnection.hpp new file mode 100644 index 0000000..be9c0d6 --- /dev/null +++ b/include/sqlgen/duckdb/DuckDBConnection.hpp @@ -0,0 +1,52 @@ +#ifndef SQLGEN_DUCKDB_DUCKDBCONNECTION_HPP_ +#define SQLGEN_DUCKDB_DUCKDBCONNECTION_HPP_ + +#include + +#include +#include + +#include "../Ref.hpp" +#include "../Result.hpp" +#include "../sqlgen_api.hpp" + +namespace sqlgen::duckdb { + +class SQLGEN_API DuckDBConnection { + public: + static Result> make( + const std::optional& _fname); + + DuckDBConnection(duckdb_connection _conn, duckdb_database _db) + : conn_(_conn), db_(_db) {} + + ~DuckDBConnection() { + duckdb_disconnect(&conn_); + duckdb_close(&db_); + } + + DuckDBConnection(const DuckDBConnection& _other) = delete; + + DuckDBConnection(DuckDBConnection&& _other) + : conn_(_other.conn_), db_(_other.db_) { + _other.conn_ = NULL; + _other.db_ = NULL; + } + + DuckDBConnection& operator=(const DuckDBConnection& _other) = delete; + + DuckDBConnection& operator=(DuckDBConnection&& _other); + + duckdb_connection conn() { return conn_; } + + duckdb_database db() { return db_; } + + private: + duckdb_connection conn_; + + duckdb_database db_; +}; + +} // namespace sqlgen::duckdb + +#endif diff --git a/include/sqlgen/duckdb/DuckDBResult.hpp b/include/sqlgen/duckdb/DuckDBResult.hpp new file mode 100644 index 0000000..6479e3a --- /dev/null +++ b/include/sqlgen/duckdb/DuckDBResult.hpp @@ -0,0 +1,69 @@ +#ifndef SQLGEN_DUCKDB_DUCKDBRESULT_HPP_ +#define SQLGEN_DUCKDB_DUCKDBRESULT_HPP_ + +#include + +#include + +#include "../sqlgen_api.hpp" +#include "DuckDBConnection.hpp" + +namespace sqlgen::duckdb { + +class SQLGEN_API DuckDBResult { + using ConnPtr = Ref; + + public: + static Result> make(const std::string& _query, + const ConnPtr& _conn) { + try { + return Ref::make(_query, _conn); + } catch (const std::exception& e) { + return error(e.what()); + } + } + + DuckDBResult(const std::string& _query, const ConnPtr& _conn) + : destroy_(false) { + if (duckdb_query(_conn->conn(), _query.c_str(), &res_) == DuckDBError) { + throw std::runtime_error(duckdb_result_error(&res_)); + } + destroy_ = true; + } + + ~DuckDBResult() { + if (destroy_) { + duckdb_destroy_result(&res_); + } + } + + DuckDBResult(const DuckDBResult& _other) = delete; + + DuckDBResult(DuckDBResult&& _other) + : destroy_(_other.destroy_), res_(_other.res_) { + _other.destroy_ = false; + } + + DuckDBResult& operator=(const DuckDBResult& _other) = delete; + + DuckDBResult& operator=(DuckDBResult&& _other) { + if (this == &_other) { + return *this; + } + destroy_ = _other.destroy_; + res_ = _other.res_; + _other.destroy_ = false; + return *this; + } + + duckdb_result& res() { return res_; } + + private: + bool destroy_; + + duckdb_result res_; +}; + +} // namespace sqlgen::duckdb + +#endif diff --git a/include/sqlgen/duckdb/Iterator.hpp b/include/sqlgen/duckdb/Iterator.hpp new file mode 100644 index 0000000..3b3e733 --- /dev/null +++ b/include/sqlgen/duckdb/Iterator.hpp @@ -0,0 +1,113 @@ +#ifndef SQLGEN_DUCKDB_ITERATOR_HPP_ +#define SQLGEN_DUCKDB_ITERATOR_HPP_ + +#include + +#include +#include +#include + +#include "../Ref.hpp" +#include "../Result.hpp" +#include "DuckDBConnection.hpp" +#include "DuckDBResult.hpp" +#include "from_chunk_ptrs.hpp" +#include "make_chunk_ptrs.hpp" + +namespace sqlgen::duckdb { + +template +class Iterator { + using ConnPtr = Ref; + using ResultPtr = Ref; + + public: + struct End { + bool operator==(const Iterator& _it) const noexcept { + return _it == *this; + } + + bool operator!=(const Iterator& _it) const noexcept { + return _it != *this; + } + }; + + public: + using difference_type = std::ptrdiff_t; + using value_type = Result; + + Iterator(const std::string& _query, const ConnPtr& _conn) + : res_(DuckDBResult::make(_query, _conn)), + conn_(_conn), + current_batch_(get_next_batch(res_, _conn)), + ix_(0) {} + + ~Iterator() = default; + + Result& operator*() const noexcept { return (*current_batch_)[ix_]; } + + Result* operator->() const noexcept { return &(*current_batch_)[ix_]; } + + bool operator==(const End&) const noexcept { + return current_batch_->size() == 0; + } + + bool operator!=(const End& _end) const noexcept { return !(*this == _end); } + + Iterator& operator++() noexcept { + ++ix_; + if (ix_ >= current_batch_->size()) { + current_batch_ = get_next_batch(res_, conn_); + ix_ = 0; + } + return *this; + } + + void operator++(int) noexcept { ++*this; } + + private: + static Ref>> get_next_batch( + const Result& _result_ptr, const ConnPtr& _conn) noexcept { + return _result_ptr + .and_then([&](const auto& _res) -> Result>>> { + duckdb_data_chunk chunk = duckdb_fetch_chunk(_res->res()); + if (!chunk) { + return Ref>>::make(); + } + const idx_t row_count = duckdb_data_chunk_get_size(chunk); + auto res = + make_chunk_ptrs(_res, chunk) + .transform([&](auto&& _chunk_ptrs) { + auto batch = Ref>>::make(); + for (idx_t i = 0; i < row_count; ++i) { + batch->emplace_back(from_chunk_ptrs(_chunk_ptrs, i)); + } + return batch; + }); + duckdb_destroy_data_chunk(&chunk); + return res; + }) + .or_else([](auto _err) { + return Ref>>::make( + std::vector>({Result(_err)})); + }) + .value(); + } + + private: + /// The underlying DuckDB result. + Result res_; + + /// The underlying connection. + ConnPtr conn_; + + /// The current batch of results. + Ref>> current_batch_; + + /// The index on the current_chunk + idx_t ix_; +}; + +} // namespace sqlgen::duckdb + +#endif diff --git a/include/sqlgen/duckdb/cast_duckdb_type.hpp b/include/sqlgen/duckdb/cast_duckdb_type.hpp new file mode 100644 index 0000000..53dd863 --- /dev/null +++ b/include/sqlgen/duckdb/cast_duckdb_type.hpp @@ -0,0 +1,103 @@ +#ifndef SQLGEN_DUCKDB_CASTDUCKDBTYPE_HPP_ +#define SQLGEN_DUCKDB_CASTDUCKDBTYPE_HPP_ + +#include + +#include +#include + +#include "../Ref.hpp" +#include "../Result.hpp" + +namespace sqlgen::duckdb { + +template +Ref> cast_as_vector(const size_t _size, U* _ptr) { + constexpr int64_t microseconds_per_day = + static_cast(24 * 60 * 60) * static_cast(1000000); + + auto vec = Ref>::make(_size); + for (size_t i = 0; i < _size; ++i) { + if constexpr (std::is_same_v) { + (*vec)[i] = static_cast(duckdb_hugeint_to_double(_ptr[i])); + + } else if constexpr (std::is_same_v && + std::is_same_v) { + (*vec)[i] = duckdb_timestamp{ + .micros = static_cast(_ptr[i].days) * microseconds_per_day}; + + } else if constexpr (std::is_same_v && + std::is_same_v) { + (*vec)[i] = duckdb_date{ + .days = static_cast(_ptr[i].micros / microseconds_per_day)}; + + } else { + (*vec)[i] = static_cast(_ptr[i]); + } + } + return vec; +} + +template +Result>> cast_duckdb_type(const duckdb_type _type, + const size_t _size, + void* _raw_ptr) { + if constexpr (std::is_same_v) { + if (_type == DUCKDB_TYPE_DATE) { + return cast_as_vector(_size, static_cast(_raw_ptr)); + } + return error("Could not cast"); + + } else if constexpr (std::is_same_v) { + if (_type == DUCKDB_TYPE_TIMESTAMP) { + return cast_as_vector(_size, static_cast(_raw_ptr)); + } + return error("Could not cast"); + + } else if constexpr (!std::is_floating_point_v && !std::is_integral_v) { + return error("Could not cast"); + + } else { + switch (_type) { + case DUCKDB_TYPE_TINYINT: + return cast_as_vector(_size, static_cast(_raw_ptr)); + + case DUCKDB_TYPE_UTINYINT: + return cast_as_vector(_size, static_cast(_raw_ptr)); + + case DUCKDB_TYPE_SMALLINT: + return cast_as_vector(_size, static_cast(_raw_ptr)); + + case DUCKDB_TYPE_USMALLINT: + return cast_as_vector(_size, static_cast(_raw_ptr)); + + case DUCKDB_TYPE_INTEGER: + return cast_as_vector(_size, static_cast(_raw_ptr)); + + case DUCKDB_TYPE_UINTEGER: + return cast_as_vector(_size, static_cast(_raw_ptr)); + + case DUCKDB_TYPE_BIGINT: + return cast_as_vector(_size, static_cast(_raw_ptr)); + + case DUCKDB_TYPE_UBIGINT: + return cast_as_vector(_size, static_cast(_raw_ptr)); + + case DUCKDB_TYPE_FLOAT: + return cast_as_vector(_size, static_cast(_raw_ptr)); + + case DUCKDB_TYPE_DOUBLE: + return cast_as_vector(_size, static_cast(_raw_ptr)); + + case DUCKDB_TYPE_HUGEINT: + return cast_as_vector(_size, static_cast(_raw_ptr)); + + default: + return error("Could not cast"); + } + } +} + +} // namespace sqlgen::duckdb + +#endif diff --git a/include/sqlgen/duckdb/check_duckdb_type.hpp b/include/sqlgen/duckdb/check_duckdb_type.hpp new file mode 100644 index 0000000..4f7a86f --- /dev/null +++ b/include/sqlgen/duckdb/check_duckdb_type.hpp @@ -0,0 +1,68 @@ +#ifndef SQLGEN_DUCKDB_CHECKDUCKDBTYPE_HPP_ +#define SQLGEN_DUCKDB_CHECKDUCKDBTYPE_HPP_ + +#include + +#include +#include + +#include "../Result.hpp" + +namespace sqlgen::duckdb { + +template +bool check_duckdb_type(duckdb_type _t) { + using Type = std::remove_cvref_t; + + switch (_t) { + case DUCKDB_TYPE_BOOLEAN: + return std::is_same_v; + + case DUCKDB_TYPE_TINYINT: + return std::is_same_v || std::is_same_v; + + case DUCKDB_TYPE_ENUM: + case DUCKDB_TYPE_UTINYINT: + return std::is_same_v; + + case DUCKDB_TYPE_SMALLINT: + return std::is_same_v; + + case DUCKDB_TYPE_USMALLINT: + return std::is_same_v; + + case DUCKDB_TYPE_INTEGER: + return std::is_same_v; + + case DUCKDB_TYPE_UINTEGER: + return std::is_same_v; + + case DUCKDB_TYPE_BIGINT: + return std::is_same_v; + + case DUCKDB_TYPE_UBIGINT: + return std::is_same_v; + + case DUCKDB_TYPE_FLOAT: + return std::is_same_v; + + case DUCKDB_TYPE_DOUBLE: + return std::is_same_v; + + case DUCKDB_TYPE_DATE: + return std::is_same_v; + + case DUCKDB_TYPE_VARCHAR: + return std::is_same_v; + + case DUCKDB_TYPE_TIMESTAMP: + return std::is_same_v; + + default: + return false; + } +} + +} // namespace sqlgen::duckdb + +#endif diff --git a/include/sqlgen/duckdb/chunk_ptrs_t.hpp b/include/sqlgen/duckdb/chunk_ptrs_t.hpp new file mode 100644 index 0000000..afe732a --- /dev/null +++ b/include/sqlgen/duckdb/chunk_ptrs_t.hpp @@ -0,0 +1,32 @@ +#ifndef SQLGEN_DUCKDB_CHUNKPTRST_HPP_ +#define SQLGEN_DUCKDB_CHUNKPTRST_HPP_ + +#include +#include + +#include "./parsing/Parser.hpp" +#include "ColumnData.hpp" + +namespace sqlgen::duckdb { + +template +struct ChunkPtrsType; + +template +struct ChunkPtrsType> { + using Type = rfl::Tuple::ResultingType, + typename FieldTs::Name>...>; +}; + +template +struct ChunkPtrsType { + using Type = typename ChunkPtrsType>::Type; +}; + +template +using chunk_ptrs_t = typename ChunkPtrsType>::Type; + +} // namespace sqlgen::duckdb + +#endif diff --git a/include/sqlgen/duckdb/connect.hpp b/include/sqlgen/duckdb/connect.hpp new file mode 100644 index 0000000..6cc6a0f --- /dev/null +++ b/include/sqlgen/duckdb/connect.hpp @@ -0,0 +1,16 @@ +#ifndef SQLGEN_DUCKDB_CONNECT_HPP_ +#define SQLGEN_DUCKDB_CONNECT_HPP_ + +#include + +#include "Connection.hpp" + +namespace sqlgen::duckdb { + +inline auto connect(const std::string& _fname = ":memory:") { + return Connection::make(_fname); +} + +} // namespace sqlgen::duckdb + +#endif diff --git a/include/sqlgen/duckdb/from_chunk_ptrs.hpp b/include/sqlgen/duckdb/from_chunk_ptrs.hpp new file mode 100644 index 0000000..87f8824 --- /dev/null +++ b/include/sqlgen/duckdb/from_chunk_ptrs.hpp @@ -0,0 +1,49 @@ +#ifndef SQLGEN_DUCKDB_FROMCHUNKPTRS_HPP_ +#define SQLGEN_DUCKDB_FROMCHUNKPTRS_HPP_ + +#include + +#include +#include +#include +#include +#include + +#include "../Result.hpp" +#include "./parsing/Parser_default.hpp" +#include "ColumnData.hpp" +#include "chunk_ptrs_t.hpp" + +namespace sqlgen::duckdb { + +template +struct FromChunkPtrs; + +template +struct FromChunkPtrs, + rfl::Tuple...>> { + Result operator()( + const rfl::Tuple...>& _chunk_ptrs, + idx_t _i) noexcept { + return [&](std::integer_sequence) -> Result { + try { + return rfl::from_named_tuple(rfl::named_tuple_t( + duckdb::parsing::Parser::read( + rfl::get<_is>(_chunk_ptrs).is_not_null(_i) + ? rfl::get<_is>(_chunk_ptrs).data + _i + : nullptr) + .value()...)); + } catch (const std::exception& e) { + return error(e.what()); + } + }(std::make_integer_sequence()); + } +}; + +template +auto from_chunk_ptrs = FromChunkPtrs, + rfl::named_tuple_t, chunk_ptrs_t>{}; + +} // namespace sqlgen::duckdb + +#endif diff --git a/include/sqlgen/duckdb/make_chunk_ptrs.hpp b/include/sqlgen/duckdb/make_chunk_ptrs.hpp new file mode 100644 index 0000000..6a9c6b1 --- /dev/null +++ b/include/sqlgen/duckdb/make_chunk_ptrs.hpp @@ -0,0 +1,90 @@ +#ifndef SQLGEN_DUCKDB_MAKECHUNKPTRS_HPP_ +#define SQLGEN_DUCKDB_MAKECHUNKPTRS_HPP_ + +#include + +#include +#include +#include +#include +#include + +#include "../Ref.hpp" +#include "ColumnData.hpp" +#include "DuckDBResult.hpp" +#include "cast_duckdb_type.hpp" +#include "check_duckdb_type.hpp" +#include "chunk_ptrs_t.hpp" + +namespace sqlgen::duckdb { + +template +struct MakeChunkPtrs; + +template +struct MakeChunkPtrs...>> { + Result...>> operator()( + const Ref& _res, duckdb_data_chunk _chunk) { + try { + return [&](std::integer_sequence) { + return rfl::Tuple...>( + make_column_data(_res, _chunk)...); + }(std::make_integer_sequence()); + } catch (const std::exception& e) { + return error(e.what()); + } + } + + template + static auto make_column_data(const Ref& _res, + duckdb_data_chunk _chunk) { + const auto actual_duckdb_type = duckdb_column_type(&_res->res(), _i); + + auto vec = duckdb_data_chunk_get_vector(_chunk, _i); + + if (check_duckdb_type(actual_duckdb_type)) { + return ColumnData{ + .vec = vec, + .data = static_cast(duckdb_vector_get_data(vec)), + .validity = duckdb_vector_get_validity(vec)}; + } + + if constexpr (std::is_same_v) { + throw std::runtime_error("Wrong type in field '" + ColName().str() + + "'. " + rfl::enum_to_string(actual_duckdb_type) + + " could not be cast to " + + rfl::type_name_t().str() + "."); + + } else { + const auto ptr_res = cast_duckdb_type( + actual_duckdb_type, duckdb_data_chunk_get_size(_chunk), + duckdb_vector_get_data(vec)); + + if (!ptr_res) { + throw std::runtime_error( + "Wrong type in field '" + ColName().str() + "'. " + + rfl::enum_to_string(actual_duckdb_type) + " could not be cast to " + + rfl::type_name_t().str() + "."); + } + + return ColumnData{.vec = vec, + .data = (*ptr_res)->data(), + .validity = duckdb_vector_get_validity(vec), + .ptr = ptr_res->ptr()}; + } + } +}; + +template +struct MakeChunkPtrs { + auto operator()(const Ref& _res, duckdb_data_chunk _chunk) { + return MakeChunkPtrs>{}(_res, _chunk); + } +}; + +template +auto make_chunk_ptrs = MakeChunkPtrs>{}; + +} // namespace sqlgen::duckdb + +#endif diff --git a/include/sqlgen/duckdb/parsing/Parser.hpp b/include/sqlgen/duckdb/parsing/Parser.hpp new file mode 100644 index 0000000..453eb4b --- /dev/null +++ b/include/sqlgen/duckdb/parsing/Parser.hpp @@ -0,0 +1,15 @@ +#ifndef SQLGEN_DUCKDB_PARSING_PARSER_HPP_ +#define SQLGEN_DUCKDB_PARSING_PARSER_HPP_ + +#include "Parser_base.hpp" +#include "Parser_date.hpp" +#include "Parser_default.hpp" +#include "Parser_enum.hpp" +#include "Parser_json.hpp" +#include "Parser_optional.hpp" +#include "Parser_reflection_type.hpp" +#include "Parser_smart_ptr.hpp" +#include "Parser_string.hpp" +#include "Parser_timestamp.hpp" + +#endif diff --git a/include/sqlgen/duckdb/parsing/Parser_base.hpp b/include/sqlgen/duckdb/parsing/Parser_base.hpp new file mode 100644 index 0000000..dd39974 --- /dev/null +++ b/include/sqlgen/duckdb/parsing/Parser_base.hpp @@ -0,0 +1,11 @@ +#ifndef SQLGEN_DUCKDB_PARSING_PARSER_BASE_HPP_ +#define SQLGEN_DUCKDB_PARSING_PARSER_BASE_HPP_ + +namespace sqlgen::duckdb::parsing { + +template +struct Parser; + +} + +#endif diff --git a/include/sqlgen/duckdb/parsing/Parser_date.hpp b/include/sqlgen/duckdb/parsing/Parser_date.hpp new file mode 100644 index 0000000..615c32e --- /dev/null +++ b/include/sqlgen/duckdb/parsing/Parser_date.hpp @@ -0,0 +1,42 @@ +#ifndef SQLGEN_DUCKDB_PARSING_PARSER_DATE_HPP_ +#define SQLGEN_DUCKDB_PARSING_PARSER_DATE_HPP_ + +#include + +#include +#include +#include + +#include "../../Result.hpp" +#include "../../Timestamp.hpp" +#include "Parser_base.hpp" + +namespace sqlgen::duckdb::parsing { + +template <> +struct Parser { + using ResultingType = duckdb_date; + + static constexpr time_t seconds_per_day = 24 * 60 * 60; + + static Result read(const ResultingType* _r) noexcept { + if (!_r) { + return error("Date value cannot be NULL."); + } + return Date(static_cast(_r->days) * seconds_per_day); + } + + static Result write(const Date& _t, + duckdb_appender _appender) noexcept { + return duckdb_append_date( + _appender, duckdb_date{.days = static_cast( + _t.to_time_t() / seconds_per_day)}) != + DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append date value.")); + } +}; + +} // namespace sqlgen::duckdb::parsing + +#endif diff --git a/include/sqlgen/duckdb/parsing/Parser_default.hpp b/include/sqlgen/duckdb/parsing/Parser_default.hpp new file mode 100644 index 0000000..400cf0b --- /dev/null +++ b/include/sqlgen/duckdb/parsing/Parser_default.hpp @@ -0,0 +1,94 @@ +#ifndef SQLGEN_DUCKDB_PARSING_PARSER_DEFAULT_HPP_ +#define SQLGEN_DUCKDB_PARSING_PARSER_DEFAULT_HPP_ + +#include + +#include +#include +#include + +#include "../../Result.hpp" +#include "Parser_base.hpp" + +namespace sqlgen::duckdb::parsing { + +template +struct Parser { + using Type = std::remove_cvref_t; + using ResultingType = Type; + + static Result read(const ResultingType* _r) noexcept { + if (!_r) { + return error("Numeric or boolean value cannot be NULL."); + } + return Type(*_r); + } + + static Result write(const T& _t, + duckdb_appender _appender) noexcept { + if constexpr (std::is_same_v) { + return duckdb_append_bool(_appender, _t) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append boolean value.")); + + } else if constexpr (std::is_same_v || + std::is_same_v) { + return duckdb_append_int8(_appender, _t) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append int8 value.")); + + } else if constexpr (std::is_same_v) { + return duckdb_append_uint8(_appender, _t) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append uint8 value.")); + + } else if constexpr (std::is_same_v) { + return duckdb_append_int16(_appender, _t) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append int16 value.")); + + } else if constexpr (std::is_same_v) { + return duckdb_append_uint16(_appender, _t) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append uint16 value.")); + + } else if constexpr (std::is_same_v) { + return duckdb_append_int32(_appender, _t) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append int32 value.")); + + } else if constexpr (std::is_same_v) { + return duckdb_append_uint32(_appender, _t) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append uint32 value.")); + + } else if constexpr (std::is_same_v) { + return duckdb_append_int64(_appender, _t) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append int64 value.")); + + } else if constexpr (std::is_same_v) { + return duckdb_append_uint64(_appender, _t) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append uint64 value.")); + + } else if constexpr (std::is_same_v) { + return duckdb_append_float(_appender, _t) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append float value.")); + + } else if constexpr (std::is_same_v) { + return duckdb_append_double(_appender, _t) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append double value.")); + + } else { + static_assert(rfl::always_false_v, "Unsupported type."); + return error("Unsupported type."); + } + } +}; + +} // namespace sqlgen::duckdb::parsing + +#endif diff --git a/include/sqlgen/duckdb/parsing/Parser_enum.hpp b/include/sqlgen/duckdb/parsing/Parser_enum.hpp new file mode 100644 index 0000000..566251e --- /dev/null +++ b/include/sqlgen/duckdb/parsing/Parser_enum.hpp @@ -0,0 +1,44 @@ +#ifndef SQLGEN_DUCKDB_PARSING_PARSER_ENUM_HPP_ +#define SQLGEN_DUCKDB_PARSING_PARSER_ENUM_HPP_ + +#include + +#include +#include +#include +#include + +#include "../../Result.hpp" +#include "Parser_base.hpp" + +namespace sqlgen::duckdb::parsing { + +template + requires std::is_enum_v +struct Parser { + using ResultingType = uint8_t; + + static_assert(enchantum::ScopedEnum, "The enum must be scoped."); + static constexpr auto arr = rfl::get_enumerator_array(); + static_assert(arr.size() < 255, "Enum size cannot exceed 255."); + + static Result read(const ResultingType* _r) noexcept { + if (!_r) { + return error("Enum value cannot be NULL."); + } + return static_cast(*_r); + } + + static Result write(const EnumT& _t, + duckdb_appender _appender) noexcept { + const auto str = rfl::enum_to_string(_t); + return duckdb_append_varchar_length(_appender, str.c_str(), str.length()) != + DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append string value.")); + } +}; + +} // namespace sqlgen::duckdb::parsing + +#endif diff --git a/include/sqlgen/duckdb/parsing/Parser_json.hpp b/include/sqlgen/duckdb/parsing/Parser_json.hpp new file mode 100644 index 0000000..03cdd44 --- /dev/null +++ b/include/sqlgen/duckdb/parsing/Parser_json.hpp @@ -0,0 +1,39 @@ +#ifndef SQLGEN_DUCKDB_PARSING_PARSER_JSON_HPP_ +#define SQLGEN_DUCKDB_PARSING_PARSER_JSON_HPP_ + +#include + +#include +#include +#include + +#include "../../JSON.hpp" +#include "../../Result.hpp" +#include "Parser_base.hpp" +#include "Parser_string.hpp" + +namespace sqlgen::duckdb::parsing { + +template +struct Parser> { + using ResultingType = duckdb_string_t; + + static Result> read(const ResultingType* _r) noexcept { + return Parser::read(_r).and_then( + [&](const auto& _str) { return rfl::json::read(_str); }); + } + + static Result write(const JSON& _t, + duckdb_appender _appender) noexcept { + try { + return Parser::write(rfl::json::write(_t.value()), + _appender); + } catch (const std::exception& e) { + return error(e.what()); + } + } +}; + +} // namespace sqlgen::duckdb::parsing + +#endif diff --git a/include/sqlgen/duckdb/parsing/Parser_optional.hpp b/include/sqlgen/duckdb/parsing/Parser_optional.hpp new file mode 100644 index 0000000..e5b373d --- /dev/null +++ b/include/sqlgen/duckdb/parsing/Parser_optional.hpp @@ -0,0 +1,43 @@ +#ifndef SQLGEN_DUCKDB_PARSING_PARSER_OPTIONAL_HPP_ +#define SQLGEN_DUCKDB_PARSING_PARSER_OPTIONAL_HPP_ + +#include + +#include +#include +#include + +#include "../../Result.hpp" +#include "Parser_base.hpp" + +namespace sqlgen::duckdb::parsing { + +template +struct Parser> { + using Type = std::remove_cvref_t; + using ResultingType = typename Parser::ResultingType; + + static Result> read(const ResultingType* _r) noexcept { + if (!_r) { + return std::optional(); + } + return Parser>::read(_r).transform( + [](auto&& _t) -> std::optional { + return std::make_optional(std::move(_t)); + }); + } + + static Result write(const std::optional& _o, + duckdb_appender _appender) noexcept { + if (!_o) { + return duckdb_append_null(_appender) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append null value.")); + } + return Parser>::write(*_o, _appender); + } +}; + +} // namespace sqlgen::duckdb::parsing + +#endif diff --git a/include/sqlgen/duckdb/parsing/Parser_reflection_type.hpp b/include/sqlgen/duckdb/parsing/Parser_reflection_type.hpp new file mode 100644 index 0000000..8f3a0f1 --- /dev/null +++ b/include/sqlgen/duckdb/parsing/Parser_reflection_type.hpp @@ -0,0 +1,36 @@ +#ifndef SQLGEN_DUCKDB_PARSING_PARSER_FOREIGN_KEY_HPP_ +#define SQLGEN_DUCKDB_PARSING_PARSER_FOREIGN_KEY_HPP_ + +#include + +#include +#include + +#include "../../Result.hpp" +#include "../../transpilation/has_reflection_method.hpp" +#include "Parser_base.hpp" + +namespace sqlgen::duckdb::parsing { + +template + requires transpilation::has_reflection_method> +struct Parser { + using Type = std::remove_cvref_t; + using ResultingType = typename Parser< + std::remove_cvref_t>::ResultingType; + + static Result read(const ResultingType* _r) noexcept { + return Parser>::read(_r) + .transform([](auto&& _t) { return T(std::move(_t)); }); + } + + static Result write(const T& _t, + duckdb_appender _appender) noexcept { + return Parser>::write( + _t.reflection(), _appender); + } +}; + +} // namespace sqlgen::duckdb::parsing + +#endif diff --git a/include/sqlgen/duckdb/parsing/Parser_smart_ptr.hpp b/include/sqlgen/duckdb/parsing/Parser_smart_ptr.hpp new file mode 100644 index 0000000..dcea151 --- /dev/null +++ b/include/sqlgen/duckdb/parsing/Parser_smart_ptr.hpp @@ -0,0 +1,46 @@ +#ifndef SQLGEN_DUCKDB_PARSING_PARSER_SMART_PTR_HPP_ +#define SQLGEN_DUCKDB_PARSING_PARSER_SMART_PTR_HPP_ + +#include + +#include +#include + +#include "../../Result.hpp" +#include "../../transpilation/is_nullable.hpp" +#include "Parser_base.hpp" + +namespace sqlgen::duckdb::parsing { + +template + requires transpilation::is_nullable_v> +struct Parser { + using Type = std::remove_cvref_t; + using ResultingType = + typename Parser::ResultingType; + + static Result read(const ResultingType* _r) noexcept { + if (!_r) { + return T(); + } + return Parser::read(_r).transform( + [](auto&& _u) -> T { + using U = std::remove_cvref; + return T(new U(std::move(_u))); + }); + } + + static Result write(const T& _ptr, + duckdb_appender _appender) noexcept { + if (!_ptr) { + return duckdb_append_null(_appender) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append null value.")); + } + return Parser>::write(*_ptr, _appender); + } +}; + +} // namespace sqlgen::duckdb::parsing + +#endif diff --git a/include/sqlgen/duckdb/parsing/Parser_string.hpp b/include/sqlgen/duckdb/parsing/Parser_string.hpp new file mode 100644 index 0000000..27cb341 --- /dev/null +++ b/include/sqlgen/duckdb/parsing/Parser_string.hpp @@ -0,0 +1,40 @@ +#ifndef SQLGEN_DUCKDB_PARSING_PARSER_STRING_HPP_ +#define SQLGEN_DUCKDB_PARSING_PARSER_STRING_HPP_ + +#include + +#include +#include + +#include "../../Result.hpp" +#include "Parser_base.hpp" + +namespace sqlgen::duckdb::parsing { + +template <> +struct Parser { + using ResultingType = duckdb_string_t; + + static Result read(const ResultingType* _r) noexcept { + if (!_r) { + return error("String value cannot be NULL."); + } + if (duckdb_string_is_inlined(*_r)) { + return std::string(_r->value.inlined.inlined, _r->value.inlined.length); + } else { + return std::string(_r->value.pointer.ptr, _r->value.pointer.length); + } + } + + static Result write(const std::string& _t, + duckdb_appender _appender) noexcept { + return duckdb_append_varchar_length(_appender, _t.c_str(), _t.length()) != + DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append string value.")); + } +}; + +} // namespace sqlgen::duckdb::parsing + +#endif diff --git a/include/sqlgen/duckdb/parsing/Parser_timestamp.hpp b/include/sqlgen/duckdb/parsing/Parser_timestamp.hpp new file mode 100644 index 0000000..2d9f12a --- /dev/null +++ b/include/sqlgen/duckdb/parsing/Parser_timestamp.hpp @@ -0,0 +1,40 @@ +#ifndef SQLGEN_DUCKDB_PARSING_PARSER_TIMESTAMP_HPP_ +#define SQLGEN_DUCKDB_PARSING_PARSER_TIMESTAMP_HPP_ + +#include + +#include +#include +#include + +#include "../../Result.hpp" +#include "Parser_base.hpp" + +namespace sqlgen::duckdb::parsing { + +template +struct Parser> { + using ResultingType = duckdb_timestamp; + + static Result> read( + const ResultingType* _r) noexcept { + if (!_r) { + return error("Timestamp value cannot be NULL."); + } + return rfl::Timestamp<_format>(static_cast(_r->micros / 1000000)); + } + + static Result write(const rfl::Timestamp<_format>& _t, + duckdb_appender _appender) noexcept { + return duckdb_append_timestamp( + _appender, + duckdb_timestamp{.micros = static_cast(_t.to_time_t()) * + 1000000}) != DuckDBError + ? Result(Nothing{}) + : Result(error("Could not append timestamp value.")); + } +}; + +} // namespace sqlgen::duckdb::parsing + +#endif diff --git a/include/sqlgen/duckdb/to_sql.hpp b/include/sqlgen/duckdb/to_sql.hpp new file mode 100644 index 0000000..88656ca --- /dev/null +++ b/include/sqlgen/duckdb/to_sql.hpp @@ -0,0 +1,27 @@ +#ifndef SQLGEN_DUCKDB_TO_SQL_HPP_ +#define SQLGEN_DUCKDB_TO_SQL_HPP_ + +#include + +#include "../dynamic/Statement.hpp" +#include "../sqlgen_api.hpp" +#include "../transpilation/to_sql.hpp" + +namespace sqlgen::duckdb { + +/// Transpiles a dynamic general SQL statement to the duckdb dialect. +SQLGEN_API std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept; + +/// Transpiles any SQL statement to the duckdb dialect. +template +std::string to_sql(const T& _t) noexcept { + if constexpr (std::is_same_v, dynamic::Statement>) { + return to_sql_impl(_t); + } else { + return to_sql_impl(transpilation::to_sql(_t)); + } +} + +} // namespace sqlgen::duckdb + +#endif diff --git a/include/sqlgen/internal/to_container.hpp b/include/sqlgen/internal/to_container.hpp index 852ef6f..ceaacbc 100644 --- a/include/sqlgen/internal/to_container.hpp +++ b/include/sqlgen/internal/to_container.hpp @@ -12,11 +12,12 @@ namespace sqlgen::internal { template auto to_container(const Result& _res) { if constexpr (internal::is_range_v) { - return _res.transform([](auto&& _it) { return Range(_it); }); + return _res.transform( + [](auto&& _it) { return Range(std::move(_it)); }); } else { return to_container>(_res).and_then( - [](auto range) -> Result { + [](const auto& range) -> Result { ContainerType container; for (auto& res : range) { if (res) { diff --git a/include/sqlgen/postgres/Connection.hpp b/include/sqlgen/postgres/Connection.hpp index af50b10..94e743c 100644 --- a/include/sqlgen/postgres/Connection.hpp +++ b/include/sqlgen/postgres/Connection.hpp @@ -15,6 +15,7 @@ #include "../dynamic/Column.hpp" #include "../dynamic/Statement.hpp" #include "../dynamic/Write.hpp" +#include "../internal/iterator_t.hpp" #include "../internal/to_container.hpp" #include "../internal/write_or_insert.hpp" #include "../is_connection.hpp" diff --git a/src/sqlgen/duckdb/Connection.cpp b/src/sqlgen/duckdb/Connection.cpp new file mode 100644 index 0000000..b78222e --- /dev/null +++ b/src/sqlgen/duckdb/Connection.cpp @@ -0,0 +1,40 @@ +#include "sqlgen/duckdb/Connection.hpp" + +#include +#include +#include +#include + +// #include "sqlgen/duckdb/Iterator.hpp" +#include "sqlgen/internal/collect/vector.hpp" +#include "sqlgen/internal/strings/strings.hpp" + +namespace sqlgen::duckdb { + +Result Connection::begin_transaction() noexcept { + return execute("BEGIN TRANSACTION;"); +} + +Result Connection::commit() noexcept { return execute("COMMIT;"); } + +Result Connection::execute(const std::string& _sql) noexcept { + duckdb_result res{}; + const auto state = duckdb_query(conn_->conn(), _sql.c_str(), &res); + if (state == DuckDBError) { + const auto err = error(duckdb_result_error(&res)); + duckdb_destroy_result(&res); + return err; + } + duckdb_destroy_result(&res); + return Nothing{}; +} + +rfl::Result> Connection::make( + const std::optional& _fname) noexcept { + return DuckDBConnection::make(_fname).transform( + [](auto&& _conn) { return Ref::make(std::move(_conn)); }); +} + +Result Connection::rollback() noexcept { return execute("ROLLBACK;"); } + +} // namespace sqlgen::duckdb diff --git a/src/sqlgen/duckdb/DuckDBConnection.cpp b/src/sqlgen/duckdb/DuckDBConnection.cpp new file mode 100644 index 0000000..55c0b46 --- /dev/null +++ b/src/sqlgen/duckdb/DuckDBConnection.cpp @@ -0,0 +1,43 @@ +#include "sqlgen/duckdb/DuckDBConnection.hpp" + +namespace sqlgen::duckdb { + +Result> DuckDBConnection::make( + const std::optional& _fname) { + duckdb_database db = NULL; + + const auto res_db = + _fname ? duckdb_open(_fname->c_str(), &db) : duckdb_open(NULL, &db); + + if (res_db == DuckDBError) { + duckdb_close(&db); + return error("Could not open database."); + } + + duckdb_connection conn = NULL; + + const auto res_conn = duckdb_connect(db, &conn); + + if (res_conn == DuckDBError) { + duckdb_disconnect(&conn); + duckdb_close(&db); + return error("Could not connect to database."); + } + + return Ref::make(conn, db); +} + +DuckDBConnection& DuckDBConnection::operator=(DuckDBConnection&& _other) { + if (this == &_other) { + return *this; + } + duckdb_disconnect(&conn_); + duckdb_close(&db_); + conn_ = _other.conn_; + db_ = _other.db_; + _other.conn_ = NULL; + _other.db_ = NULL; + return *this; +} + +} // namespace sqlgen::duckdb diff --git a/src/sqlgen/duckdb/exec.cpp b/src/sqlgen/duckdb/exec.cpp new file mode 100644 index 0000000..19220f6 --- /dev/null +++ b/src/sqlgen/duckdb/exec.cpp @@ -0,0 +1,27 @@ +#include "sqlgen/postgres/exec.hpp" + +#include +#include +#include +#include + +namespace sqlgen::postgres { + +Result> exec(const Ref& _conn, + const std::string& _sql) noexcept { + const auto res = PQexec(_conn.get(), _sql.c_str()); + + const auto status = PQresultStatus(res); + + if (status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK && + status != PGRES_COPY_IN) { + const auto err = + error("Executing '" + _sql + "' failed: " + PQresultErrorMessage(res)); + PQclear(res); + return err; + } + + return Ref::make(std::shared_ptr(res, PQclear)); +} + +} // namespace sqlgen::postgres diff --git a/src/sqlgen/duckdb/to_sql.cpp b/src/sqlgen/duckdb/to_sql.cpp new file mode 100644 index 0000000..498e913 --- /dev/null +++ b/src/sqlgen/duckdb/to_sql.cpp @@ -0,0 +1,960 @@ +#include "sqlgen/duckdb/to_sql.hpp" + +#include +#include +#include +#include +#include +#include + +#include "sqlgen/dynamic/Column.hpp" +#include "sqlgen/dynamic/Join.hpp" +#include "sqlgen/dynamic/Operation.hpp" +#include "sqlgen/internal/collect/vector.hpp" +#include "sqlgen/internal/strings/strings.hpp" + +namespace sqlgen::duckdb { + +std::string aggregation_to_sql( + const dynamic::Aggregation& _aggregation) noexcept; + +std::string column_or_value_to_sql(const dynamic::ColumnOrValue& _col) noexcept; + +std::string condition_to_sql(const dynamic::Condition& _cond) noexcept; + +template +std::string condition_to_sql_impl(const ConditionType& _condition) noexcept; + +std::string column_to_sql_definition(const dynamic::Table& _table, + const dynamic::Column& _col) noexcept; + +std::string create_enums(const dynamic::CreateTable& _stmt) noexcept; + +std::string create_index_to_sql(const dynamic::CreateIndex& _stmt) noexcept; + +std::string create_sequences_for_auto_incr( + const dynamic::CreateTable& _stmt) noexcept; + +std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept; + +std::string create_as_to_sql(const dynamic::CreateAs& _stmt) 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 get_primary_keys( + const dynamic::CreateTable& _stmt) noexcept; + +std::vector>> get_enum_types( + const dynamic::CreateTable& _stmt) noexcept; + +std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept; + +std::string join_to_sql(const dynamic::Join& _stmt) noexcept; + +std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept; + +std::string make_sequence_name(const dynamic::Table& _table, + const dynamic::Column& _col) noexcept; + +std::string properties_to_sql(const dynamic::Table& _table, + const dynamic::Column& _col) noexcept; + +std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept; + +std::string table_or_query_to_sql( + const dynamic::SelectFrom::TableOrQueryType& _table_or_query) noexcept; + +std::string type_to_sql(const dynamic::Type& _type) noexcept; + +std::string update_to_sql(const dynamic::Update& _stmt) noexcept; + +std::string write_to_sql(const dynamic::Write& _stmt) noexcept; + +// ---------------------------------------------------------------------------- + +inline std::string get_name(const dynamic::Column& _col) { return _col.name; } + +inline std::pair> get_enum_mapping( + const dynamic::Column& _col) { + return _col.type.visit( + [&](const auto& _t) -> std::pair> { + using T = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return {type_to_sql(_t), _t.values}; + } + return {}; + }); +} + +inline std::string wrap_in_quotes(const std::string& _name) noexcept { + return "\"" + _name + "\""; +} + +inline std::string wrap_in_single_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; + std::stringstream stream; + if constexpr (std::is_same_v) { + stream << "AVG(" << operation_to_sql(*_agg.val) << ")"; + + } else if constexpr (std::is_same_v) { + 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) { + stream << "MAX(" << operation_to_sql(*_agg.val) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "MIN(" << operation_to_sql(*_agg.val) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "SUM(" << operation_to_sql(*_agg.val) << ")"; + + } else { + static_assert(rfl::always_false_v, "Not all cases were covered."); + } + return stream.str(); + }); +} + +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; + if constexpr (std::is_same_v) { + return "'" + escape_single_quote(_v.val) + "'"; + + } else if constexpr (std::is_same_v) { + return "INTERVAL '" + std::to_string(_v.val) + " " + + rfl::enum_to_string(_v.unit) + "'"; + + } else if constexpr (std::is_same_v) { + return "to_timestamp(" + std::to_string(_v.seconds_since_unix) + ")"; + + } else if constexpr (std::is_same_v) { + return _v.val ? "TRUE" : "FALSE"; + + } else { + return std::to_string(_v.val); + } + }; + + return _col.visit([&](const auto& _c) -> std::string { + using Type = std::remove_cvref_t; + if constexpr (std::is_same_v) { + 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 +std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { + using namespace std::ranges::views; + + using C = std::remove_cvref_t; + + std::stringstream stream; + + if constexpr (std::is_same_v) { + stream << "(" << condition_to_sql(*_condition.cond1) << ") AND (" + << condition_to_sql(*_condition.cond2) << ")"; + + } else if constexpr (std::is_same_v< + C, dynamic::Condition::BooleanColumnOrValue>) { + stream << column_or_value_to_sql(_condition.col_or_val); + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(_condition.op1) << " = " + << operation_to_sql(_condition.op2); + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(_condition.op1) + << " >= " << operation_to_sql(_condition.op2); + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(_condition.op1) << " > " + << operation_to_sql(_condition.op2); + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(_condition.op) << " IN (" + << internal::strings::join( + ", ", + internal::collect::vector(_condition.patterns | + transform(column_or_value_to_sql))) + << ")"; + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(_condition.op) << " IS NULL"; + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(_condition.op) << " IS NOT NULL"; + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(_condition.op1) + << " <= " << operation_to_sql(_condition.op2); + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(_condition.op1) << " < " + << operation_to_sql(_condition.op2); + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(_condition.op) << " LIKE " + << column_or_value_to_sql(_condition.pattern); + + } else if constexpr (std::is_same_v) { + stream << "NOT (" << condition_to_sql(*_condition.cond) << ")"; + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(_condition.op1) + << " != " << operation_to_sql(_condition.op2); + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(_condition.op) << " NOT LIKE " + << column_or_value_to_sql(_condition.pattern); + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(_condition.op) << " NOT IN (" + << internal::strings::join( + ", ", + internal::collect::vector(_condition.patterns | + transform(column_or_value_to_sql))) + << ")"; + + } else if constexpr (std::is_same_v) { + stream << "(" << condition_to_sql(*_condition.cond1) << ") OR (" + << condition_to_sql(*_condition.cond2) << ")"; + + } else { + static_assert(rfl::always_false_v, "Not all cases were covered."); + } + return stream.str(); +} + +std::string column_to_sql_definition(const dynamic::Table& _table, + const dynamic::Column& _col) noexcept { + return wrap_in_quotes(_col.name) + " " + type_to_sql(_col.type) + + properties_to_sql(_table, _col); +} + +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 << "\"" << _stmt.name << "\" "; + + stream << "ON "; + + stream << table_or_query_to_sql(_stmt.table); + + 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 make_sequence_name(const dynamic::Table& _table, + const dynamic::Column& _col) noexcept { + return "sqlgen_seq_" + (_table.alias ? *_table.alias + "_" : std::string()) + + _table.name + "_" + _col.name; +} + +std::string create_sequences_for_auto_incr( + const dynamic::CreateTable& _stmt) noexcept { + using namespace std::ranges::views; + + const auto is_auto_incr = [](const auto& _col) { + return _col.type.visit( + [](const auto& _t) { return _t.properties.auto_incr; }); + }; + + const auto create_one_sequence = + [&](const dynamic::Column& _col) -> std::string { + std::stringstream stream; + stream << "CREATE SEQUENCE "; + if (_stmt.if_not_exists) { + stream << "IF NOT EXISTS "; + } + stream << wrap_in_quotes(make_sequence_name(_stmt.table, _col)) << ";"; + return stream.str(); + }; + + return internal::strings::join( + " ", internal::collect::vector(_stmt.columns | filter(is_auto_incr) | + transform(create_one_sequence))); +} + +std::string create_enums(const dynamic::CreateTable& _stmt) noexcept { + using namespace std::ranges::views; + + std::stringstream stream; + + for (const auto& [enum_name, enum_values] : get_enum_types(_stmt)) { + stream << "CREATE TYPE "; + if (_stmt.if_not_exists) { + stream << "IF NOT EXISTS "; + } + stream << enum_name << " AS ENUM (" + << internal::strings::join( + ", ", internal::collect::vector( + enum_values | transform(wrap_in_single_quotes))) + << "); "; + } + + return stream.str(); +} + +std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept { + using namespace std::ranges::views; + + std::stringstream stream; + + stream << create_enums(_stmt); + + stream << create_sequences_for_auto_incr(_stmt); + + stream << "CREATE TABLE "; + + if (_stmt.if_not_exists) { + stream << "IF NOT EXISTS "; + } + + stream << table_or_query_to_sql(_stmt.table); + + stream << "("; + stream << internal::strings::join( + ", ", internal::collect::vector( + _stmt.columns | transform(std::bind_front( + column_to_sql_definition, _stmt.table)))); + + 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 create_as_to_sql(const dynamic::CreateAs& _stmt) noexcept { + std::stringstream stream; + + stream << "CREATE "; + + if (_stmt.or_replace) { + stream << "OR REPLACE "; + } + + stream << internal::strings::replace_all( + internal::strings::to_upper(rfl::enum_to_string(_stmt.what)), + "_", " ") + << " "; + + if (_stmt.if_not_exists) { + stream << "IF NOT EXISTS "; + } + + if (_stmt.table_or_view.schema) { + stream << wrap_in_quotes(*_stmt.table_or_view.schema) << "."; + } + stream << wrap_in_quotes(_stmt.table_or_view.name) << " AS "; + + stream << select_from_to_sql(_stmt.query); + + 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 " + << internal::strings::replace_all( + internal::strings::to_upper(rfl::enum_to_string(_stmt.what)), + "_", " ") + << " "; + + 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); + + if (_stmt.cascade) { + stream << " CASCADE"; + } + + 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 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)); +} + +std::vector>> get_enum_types( + const dynamic::CreateTable& _stmt) noexcept { + using namespace std::ranges::views; + + const auto is_enum = [](const dynamic::Column& _col) -> bool { + return _col.type.visit([&](const auto& _t) -> bool { + using T = std::remove_cvref_t; + return std::is_same_v; + }); + }; + return internal::collect::vector(_stmt.columns | filter(is_enum) | + transform(get_enum_mapping)); +} + +std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept { + using namespace std::ranges::views; + + std::stringstream stream; + + stream << "INSERT "; + + if (_stmt.or_replace) { + stream << "OR REPLACE "; + } + + stream << "INTO "; + + if (_stmt.table.schema) { + stream << wrap_in_quotes(*_stmt.table.schema) << "."; + } + + stream << wrap_in_quotes(_stmt.table.name); + + stream << " BY NAME ( SELECT "; + stream << internal::strings::join( + ", ", internal::collect::vector( + _stmt.columns | transform([&](const auto _name) { + return wrap_in_quotes(_name) + " AS " + wrap_in_quotes(_name); + }))); + stream << " FROM sqlgen_appended_data)"; + + 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), "_", " ")) + << " " << table_or_query_to_sql(_stmt.table_or_query) << " " + << _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; + + std::stringstream stream; + + if constexpr (std::is_same_v) { + stream << "abs(" << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << aggregation_to_sql(_s); + + } else if constexpr (std::is_same_v) { + stream << "cast(" << operation_to_sql(*_s.op1) << " as " + << type_to_sql(_s.target_type) << ")"; + + } else if constexpr (std::is_same_v) { + 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) { + stream << "ceil(" << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << column_or_value_to_sql(_s); + + } else if constexpr (std::is_same_v) { + stream << "(" + << internal::strings::join( + " || ", internal::collect::vector( + _s.ops | transform([](const auto& _op) { + return operation_to_sql(*_op); + }))) + << ")"; + + } else if constexpr (std::is_same_v) { + stream << "cos(" << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(*_s.date) << " + " + << internal::strings::join( + " + ", + internal::collect::vector( + _s.durations | transform([](const auto& _d) { + return column_or_value_to_sql(dynamic::Value{_d}); + }))); + + } else if constexpr (std::is_same_v) { + stream << "extract(DAY from " << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "cast(" << operation_to_sql(*_s.op2) << " as DATE) - cast(" + << operation_to_sql(*_s.op1) << " as DATE)"; + + } else if constexpr (std::is_same_v) { + stream << "(" << operation_to_sql(*_s.op1) << ") / (" + << operation_to_sql(*_s.op2) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "exp(" << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "floor(" << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "extract(HOUR from " << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "length(" << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "ln(" << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "log(2.0, " << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "lower(" << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "ltrim(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "(" << operation_to_sql(*_s.op1) << ") - (" + << operation_to_sql(*_s.op2) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "extract(MINUTE from " << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "mod(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "extract(MONTH from " << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "(" << operation_to_sql(*_s.op1) << ") * (" + << operation_to_sql(*_s.op2) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "(" << operation_to_sql(*_s.op1) << ") + (" + << operation_to_sql(*_s.op2) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "replace(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ", " << operation_to_sql(*_s.op3) + << ")"; + + } else if constexpr (std::is_same_v) { + stream << "round(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "rtrim(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "extract(SECOND from " << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "sin(" << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "sqrt(" << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "tan(" << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "trim(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "extract(EPOCH FROM " << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "upper(" << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << column_or_value_to_sql(_s); + + } else if constexpr (std::is_same_v) { + stream << "extract(DOW from " << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "extract(YEAR from " << operation_to_sql(*_s.op1) << ")"; + + } else { + static_assert(rfl::always_false_v, "Unsupported type."); + } + return stream.str(); + }); +} + +std::string properties_to_sql(const dynamic::Table& _table, + const dynamic::Column& _col) noexcept { + const auto properties = + _col.type.visit([](const auto& _t) { return _t.properties; }); + + return [&]() -> std::string { + return std::string(properties.nullable ? "" : " NOT NULL") + + std::string(properties.auto_incr + ? " DEFAULT nextval('" + + make_sequence_name(_table, _col) + "')" + : "") + + std::string(properties.unique ? " UNIQUE" : ""); + }() + [&]() -> std::string { + if (!properties.foreign_key_reference) { + return ""; + } + const auto& ref = *properties.foreign_key_reference; + return " REFERENCES " + wrap_in_quotes(ref.table) + "(" + + wrap_in_quotes(ref.column) + ")"; + }(); +} + +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 " << table_or_query_to_sql(_stmt.table_or_query); + + 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 table_or_query_to_sql( + const dynamic::SelectFrom::TableOrQueryType& _table_or_query) noexcept { + return _table_or_query.visit([](const auto& _t) -> std::string { + using Type = std::remove_cvref_t; + if constexpr (std::is_same_v) { + if (_t.schema) { + return wrap_in_quotes(*_t.schema) + "." + wrap_in_quotes(_t.name); + } + return wrap_in_quotes(_t.name); + } else { + return "(" + select_from_to_sql(*_t) + ")"; + } + }); +} + +std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept { + return _stmt.visit([&](const auto& _s) -> std::string { + using S = std::remove_cvref_t; + + if constexpr (std::is_same_v) { + return create_index_to_sql(_s); + + } else if constexpr (std::is_same_v) { + return create_table_to_sql(_s); + + } else if constexpr (std::is_same_v) { + return create_as_to_sql(_s); + + } else if constexpr (std::is_same_v) { + return delete_from_to_sql(_s); + + } else if constexpr (std::is_same_v) { + return drop_to_sql(_s); + + } else if constexpr (std::is_same_v) { + return insert_to_sql(_s); + + } else if constexpr (std::is_same_v) { + return select_from_to_sql(_s); + + } else if constexpr (std::is_same_v) { + return update_to_sql(_s); + + } else if constexpr (std::is_same_v) { + return write_to_sql(_s); + + } else { + static_assert(rfl::always_false_v, "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; + if constexpr (std::is_same_v) { + return "BOOLEAN"; + + } else if constexpr (std::is_same_v) { + return _t.type_name; + + } else if constexpr (std::is_same_v) { + return "TINYINT"; + + } else if constexpr (std::is_same_v) { + return "UTINYINT"; + + } else if constexpr (std::is_same_v) { + return "SMALLINT"; + + } else if constexpr (std::is_same_v) { + return "USMALLINT"; + + } else if constexpr (std::is_same_v) { + return "INTEGER"; + + } else if constexpr (std::is_same_v) { + return "UINTEGER"; + + } else if constexpr (std::is_same_v) { + return "BIGINT"; + + } else if constexpr (std::is_same_v) { + return "UBIGINT"; + + } else if constexpr (std::is_same_v) { + return _t.name; + + } else if constexpr (std::is_same_v) { + return "FLOAT"; + + } else if constexpr (std::is_same_v) { + return "DOUBLE"; + + } else if constexpr (std::is_same_v) { + return "TEXT"; + + } else if constexpr (std::is_same_v) { + return "VARCHAR(" + std::to_string(_t.length) + ")"; + + } else if constexpr (std::is_same_v) { + return "JSON"; + + } else if constexpr (std::is_same_v) { + return "DATE"; + + } else if constexpr (std::is_same_v) { + return "TIMESTAMP"; + + } else if constexpr (std::is_same_v) { + return "TIMESTAMP WITH TIME ZONE"; + + } else if constexpr (std::is_same_v) { + return "TEXT"; + } else { + static_assert(rfl::always_false_v, "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 "; + + stream << table_or_query_to_sql(_stmt.table); + + 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(); +} + +std::string write_to_sql(const dynamic::Write& _stmt) noexcept { + using namespace std::ranges::views; + + std::stringstream stream; + stream << "INSERT INTO "; + stream << table_or_query_to_sql(_stmt.table); + + stream << " BY NAME ( SELECT "; + stream << internal::strings::join( + ", ", internal::collect::vector( + _stmt.columns | transform([&](const auto _name) { + return wrap_in_quotes(_name) + " AS " + wrap_in_quotes(_name); + }))); + stream << " FROM sqlgen_appended_data)"; + + stream << ";"; + + return stream.str(); +} + +} // namespace sqlgen::duckdb diff --git a/src/sqlgen_duckdb.cpp b/src/sqlgen_duckdb.cpp new file mode 100644 index 0000000..1e254aa --- /dev/null +++ b/src/sqlgen_duckdb.cpp @@ -0,0 +1,5 @@ +#include "sqlgen/duckdb/Connection.cpp" +#include "sqlgen/duckdb/DuckDBConnection.cpp" +// #include "sqlgen/duckdb/Iterator.cpp" +// #include "sqlgen/duckdb/exec.cpp" +#include "sqlgen/duckdb/to_sql.cpp" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f938e71..26b718a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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_DUCKDB) + add_subdirectory(duckdb) +endif() + if(SQLGEN_MYSQL) add_subdirectory(mysql) endif() diff --git a/tests/duckdb/CMakeLists.txt b/tests/duckdb/CMakeLists.txt new file mode 100644 index 0000000..956f073 --- /dev/null +++ b/tests/duckdb/CMakeLists.txt @@ -0,0 +1,19 @@ +project(sqlgen-duckdb-tests) + +file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "*.cpp") + +add_executable( + sqlgen-duckdb-tests + ${SOURCES} +) +target_precompile_headers(sqlgen-duckdb-tests PRIVATE [["sqlgen.hpp"]] ) + +target_link_libraries(sqlgen-duckdb-tests PRIVATE sqlgen_tests_crt) + +add_custom_command(TARGET sqlgen-duckdb-tests POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy -t $ $ + COMMAND_EXPAND_LISTS +) + +find_package(GTest) +gtest_discover_tests(sqlgen-duckdb-tests) diff --git a/tests/duckdb/test_aggregations.cpp b/tests/duckdb/test_aggregations.cpp new file mode 100644 index 0000000..a1beb0d --- /dev/null +++ b/tests/duckdb/test_aggregations.cpp @@ -0,0 +1,64 @@ + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_aggregations { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_aggregations) { + const auto people1 = std::vector( + {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; + 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( + 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; + + const auto children = duckdb::connect() + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(get_children) + .value(); + + 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 + diff --git a/tests/duckdb/test_aggregations_with_nullable.cpp b/tests/duckdb/test_aggregations_with_nullable.cpp new file mode 100644 index 0000000..dd7b81a --- /dev/null +++ b/tests/duckdb/test_aggregations_with_nullable.cpp @@ -0,0 +1,63 @@ + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_aggregations_with_nullable { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(duckdb, test_aggregations_with_nullable) { + const auto people1 = std::vector( + {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"}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + struct Children { + int64_t num_children; + uint64_t num_last_names; + std::optional avg_age; + std::optional max_age; + std::optional min_age; + std::optional sum_age; + }; + + const auto get_children = + select_from( + 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; + + const auto children = duckdb::connect() + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(get_children) + .value(); + + 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 + diff --git a/tests/duckdb/test_alpha_numeric.cpp b/tests/duckdb/test_alpha_numeric.cpp new file mode 100644 index 0000000..2e81d7e --- /dev/null +++ b/tests/duckdb/test_alpha_numeric.cpp @@ -0,0 +1,39 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_alpha_numeric { + +struct Person { + sqlgen::PrimaryKey id; + sqlgen::AlphaNumeric first_name; + sqlgen::AlphaNumeric last_name; + int age; +}; + +TEST(duckdb, test_alpha_numeric) { + const auto people1 = std::vector( + {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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + const auto people2 = sqlgen::read>(conn).value(); + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_alpha_numeric diff --git a/tests/duckdb/test_alpha_numeric_query.cpp b/tests/duckdb/test_alpha_numeric_query.cpp new file mode 100644 index 0000000..3910441 --- /dev/null +++ b/tests/duckdb/test_alpha_numeric_query.cpp @@ -0,0 +1,47 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_alpha_numeric_query { + +struct Person { + sqlgen::PrimaryKey id; + sqlgen::AlphaNumeric first_name; + sqlgen::AlphaNumeric last_name; + int age; +}; + +sqlgen::Result> get_people( + const auto& _conn, const sqlgen::AlphaNumeric& _first_name) { + using namespace sqlgen; + using namespace sqlgen::literals; + const auto query = + sqlgen::read> | where("first_name"_c == _first_name); + return query(_conn); +} + +TEST(duckdb, test_alpha_numeric_query) { + const auto people1 = std::vector({Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}}); + + const auto conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + const auto people2 = sqlgen::AlphaNumeric::from_value("Homer") + .and_then([&](const auto& _first_name) { + return get_people(conn, _first_name); + }) + .value(); + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_alpha_numeric_query diff --git a/tests/duckdb/test_auto_incr_primary_key.cpp b/tests/duckdb/test_auto_incr_primary_key.cpp new file mode 100644 index 0000000..2d3b5f0 --- /dev/null +++ b/tests/duckdb/test_auto_incr_primary_key.cpp @@ -0,0 +1,45 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_auto_incr_primary_key { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_auto_incr_primary_key) { + auto people1 = std::vector( + {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}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people2 = duckdb::connect() + .and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + 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 diff --git a/tests/duckdb/test_boolean.cpp b/tests/duckdb/test_boolean.cpp new file mode 100644 index 0000000..ff75499 --- /dev/null +++ b/tests/duckdb/test_boolean.cpp @@ -0,0 +1,52 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_boolean { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + bool has_children; +}; + +TEST(duckdb, test_boolean) { + const auto people1 = std::vector({Person{.id = 0, + .first_name = "Homer", + .last_name = "Simpson", + .has_children = true}, + Person{.id = 1, + .first_name = "Bart", + .last_name = "Simpson", + .has_children = false}, + Person{.id = 2, + .first_name = "Lisa", + .last_name = "Simpson", + .has_children = false}, + Person{.id = 3, + .first_name = "Maggie", + .last_name = "Simpson", + .has_children = false}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto conn = duckdb::connect(); + + const auto people2 = sqlgen::write(conn, people1) + .and_then(sqlgen::read>) + .value(); + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_boolean + diff --git a/tests/duckdb/test_boolean_conditions.cpp b/tests/duckdb/test_boolean_conditions.cpp new file mode 100644 index 0000000..8fe076a --- /dev/null +++ b/tests/duckdb/test_boolean_conditions.cpp @@ -0,0 +1,54 @@ + +#include + +#include +#include +#include +#include +#include + +namespace test_boolean_conditions { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + bool has_children; +}; + +TEST(duckdb, test_boolean_conditions) { + const auto people1 = std::vector({Person{.id = 0, + .first_name = "Homer", + .last_name = "Simpson", + .has_children = true}, + Person{.id = 1, + .first_name = "Bart", + .last_name = "Simpson", + .has_children = false}, + Person{.id = 2, + .first_name = "Lisa", + .last_name = "Simpson", + .has_children = false}, + Person{.id = 3, + .first_name = "Maggie", + .last_name = "Simpson", + .has_children = false}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto homer = + duckdb::connect() + .and_then(sqlgen::write(people1)) + .and_then(sqlgen::read | where("has_children"_c == true) | + order_by("id"_c)) + .value(); + + const auto json1 = rfl::json::write(people1.at(0)); + const auto json2 = rfl::json::write(homer); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_boolean_conditions + diff --git a/tests/duckdb/test_boolean_update.cpp b/tests/duckdb/test_boolean_update.cpp new file mode 100644 index 0000000..a202cff --- /dev/null +++ b/tests/duckdb/test_boolean_update.cpp @@ -0,0 +1,59 @@ + +#include + +#include +#include +#include +#include +#include + +namespace test_boolean_update { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + bool has_children; +}; + +TEST(duckdb, test_boolean_update) { + auto people1 = std::vector({Person{.id = 0, + .first_name = "Homer", + .last_name = "Simpson", + .has_children = true}, + Person{.id = 1, + .first_name = "Bart", + .last_name = "Simpson", + .has_children = false}, + Person{.id = 2, + .first_name = "Lisa", + .last_name = "Simpson", + .has_children = false}, + Person{.id = 3, + .first_name = "Maggie", + .last_name = "Simpson", + .has_children = false}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto conn = duckdb::connect(); + + const auto people2 = + sqlgen::write(conn, people1) + .and_then(update("has_children"_c.set(false)) | + where("has_children"_c == true)) + .and_then(sqlgen::read> | + where("has_children"_c == false) | order_by("id"_c)) + .value(); + + people1.at(0).has_children = false; + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_boolean_update + diff --git a/tests/duckdb/test_cache.cpp b/tests/duckdb/test_cache.cpp new file mode 100644 index 0000000..322442b --- /dev/null +++ b/tests/duckdb/test_cache.cpp @@ -0,0 +1,45 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_cache { + +struct User { + std::string name; + int age; +}; + +TEST(duckdb, test_cache) { + const auto conn = sqlgen::duckdb::connect(); + + const auto user = User{.name = "John", .age = 30}; + sqlgen::write(conn, user); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query = sqlgen::read | where("name"_c == "John"); + + const auto cached_query = sqlgen::cache<100>(query); + + const auto user1 = conn.and_then(cache<100>(query)).value(); + + EXPECT_EQ(cached_query.cache(conn).size(), 1); + + const auto user2 = cached_query(conn).value(); + const auto user3 = cached_query(conn).value(); + + EXPECT_EQ(user1.name, "John"); + EXPECT_EQ(user1.age, 30); + EXPECT_EQ(user2.name, "John"); + EXPECT_EQ(user2.age, 30); + EXPECT_EQ(cached_query.cache(conn).size(), 1); + EXPECT_EQ(user3.name, "John"); + EXPECT_EQ(user3.age, 30); +} + +} // namespace test_cache diff --git a/tests/duckdb/test_create_index.cpp b/tests/duckdb/test_create_index.cpp new file mode 100644 index 0000000..cbce32d --- /dev/null +++ b/tests/duckdb/test_create_index.cpp @@ -0,0 +1,34 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_create_index { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_create_index) { + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people = sqlgen::duckdb::connect() + .and_then(create_table | if_not_exists) + .and_then(create_index<"person_ix", Person>( + "first_name"_c, "last_name"_c) | + if_not_exists) + .and_then(sqlgen::read>); + + const std::string expected = R"([])"; + + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_create_index diff --git a/tests/duckdb/test_create_table.cpp b/tests/duckdb/test_create_table.cpp new file mode 100644 index 0000000..cc2174c --- /dev/null +++ b/tests/duckdb/test_create_table.cpp @@ -0,0 +1,31 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_create_table { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_create_table) { + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people = sqlgen::duckdb::connect() + .and_then(create_table | if_not_exists) + .and_then(sqlgen::read>); + + const std::string expected = R"([])"; + + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_create_table diff --git a/tests/duckdb/test_create_table_as.cpp b/tests/duckdb/test_create_table_as.cpp new file mode 100644 index 0000000..f10e11e --- /dev/null +++ b/tests/duckdb/test_create_table_as.cpp @@ -0,0 +1,60 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_create_table_as { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +struct Name { + std::string first_name; + std::string last_name; +}; + +TEST(duckdb, test_create_table_as) { + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people = std::vector( + {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 names_query = select_from("first_name"_c, "last_name"_c); + + const auto get_names = create_as(names_query) | if_not_exists; + + const auto names = sqlgen::duckdb::connect() + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(write(std::ref(people))) + .and_then(get_names) + .and_then(sqlgen::read>) + .value(); + + const std::string expected_query = + R"(CREATE TABLE IF NOT EXISTS "Name" AS SELECT "first_name", "last_name" FROM "Person")"; + + const std::string expected = + R"([{"first_name":"Homer","last_name":"Simpson"},{"first_name":"Marge","last_name":"Simpson"},{"first_name":"Bart","last_name":"Simpson"},{"first_name":"Lisa","last_name":"Simpson"},{"first_name":"Maggie","last_name":"Simpson"}])"; + + EXPECT_EQ(duckdb::to_sql(get_names), expected_query); + EXPECT_EQ(rfl::json::write(names), expected); +} + +} // namespace test_create_table_as + diff --git a/tests/duckdb/test_create_view_as.cpp b/tests/duckdb/test_create_view_as.cpp new file mode 100644 index 0000000..bfb2b5a --- /dev/null +++ b/tests/duckdb/test_create_view_as.cpp @@ -0,0 +1,65 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_create_view_as { + +struct Person { + static constexpr const char* tablename = "PEOPLE"; + + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +struct Name { + static constexpr const char* viewname = "NAMES"; + static constexpr bool is_view = true; + + std::string first_name; + std::string last_name; +}; + +TEST(duckdb, test_create_view_as) { + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people = std::vector( + {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 names_query = select_from("first_name"_c, "last_name"_c); + + const auto get_names = create_as(names_query) | if_not_exists; + + const auto names = duckdb::connect() + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(write(std::ref(people))) + .and_then(get_names) + .and_then(sqlgen::read>) + .value(); + + const std::string expected_query = + R"(CREATE VIEW IF NOT EXISTS "NAMES" AS SELECT "first_name", "last_name" FROM "PEOPLE")"; + + const std::string expected = + R"([{"first_name":"Homer","last_name":"Simpson"},{"first_name":"Marge","last_name":"Simpson"},{"first_name":"Bart","last_name":"Simpson"},{"first_name":"Lisa","last_name":"Simpson"},{"first_name":"Maggie","last_name":"Simpson"}])"; + + EXPECT_EQ(duckdb::to_sql(get_names), expected_query); + EXPECT_EQ(rfl::json::write(names), expected); +} + +} // namespace test_create_view_as + diff --git a/tests/duckdb/test_delete_from.cpp b/tests/duckdb/test_delete_from.cpp new file mode 100644 index 0000000..642345e --- /dev/null +++ b/tests/duckdb/test_delete_from.cpp @@ -0,0 +1,48 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_delete_from { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_delete_from) { + const auto people1 = std::vector( + {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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query = delete_from | where("first_name"_c == "Hugo"); + + query(conn).value(); + + const auto people2 = sqlgen::read>(conn).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 diff --git a/tests/duckdb/test_drop.cpp b/tests/duckdb/test_drop.cpp new file mode 100644 index 0000000..3d0e4fa --- /dev/null +++ b/tests/duckdb/test_drop.cpp @@ -0,0 +1,41 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_drop { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_drop) { + const auto people1 = std::vector( + {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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query = drop | if_exists; + + query(conn).value(); +} + +} // namespace test_drop diff --git a/tests/duckdb/test_dynamic_type.cpp b/tests/duckdb/test_dynamic_type.cpp new file mode 100644 index 0000000..6ab8954 --- /dev/null +++ b/tests/duckdb/test_dynamic_type.cpp @@ -0,0 +1,129 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sqlgen::parsing { + +template <> +struct Parser { + using Type = boost::uuids::uuid; + + static Result read( + const std::optional& _str) noexcept { + if (!_str) { + return error("boost::uuids::uuid cannot be NULL."); + } + return boost::lexical_cast(*_str); + } + + static std::optional write( + const boost::uuids::uuid& _u) noexcept { + return boost::uuids::to_string(_u); + } + + static dynamic::Type to_type() noexcept { + return sqlgen::dynamic::types::Dynamic{"TEXT"}; + } +}; + +} // namespace sqlgen::parsing + +namespace sqlgen::duckdb::parsing { + +template <> +struct Parser { + using ResultingType = duckdb_string_t; + + static Result read(const ResultingType* _r) noexcept { + return Parser::read(_r).and_then( + [&](const std::string& _str) -> Result { + try { + return boost::lexical_cast(_str); + } catch (const std::exception& e) { + return error(e.what()); + } + }); + } + + static Result write(const boost::uuids::uuid& _u, + duckdb_appender _appender) noexcept { + return Parser::write(boost::uuids::to_string(_u), _appender); + } +}; + +} // namespace sqlgen::duckdb::parsing + +namespace sqlgen::transpilation { + +template <> +struct ToValue { + dynamic::Value operator()(const boost::uuids::uuid& _u) const { + return dynamic::Value{dynamic::String{.val = boost::uuids::to_string(_u)}}; + } +}; + +} // namespace sqlgen::transpilation + +/// For the JSON serialization - not needed for +/// the actual DB operations. +namespace rfl { + +template <> +struct Reflector { + using ReflType = std::string; + + static boost::uuids::uuid to(const std::string& _str) { + return boost::lexical_cast(_str); + } + + static std::string from(const boost::uuids::uuid& _u) { + return boost::uuids::to_string(_u); + } +}; + +} // namespace rfl + +namespace test_dynamic_type { + +struct Person { + sqlgen::PrimaryKey id = + boost::uuids::uuid(boost::uuids::random_generator()()); + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_dynamic_type) { + const auto people1 = std::vector( + {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}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto conn = duckdb::connect().and_then(drop | if_exists); + + const auto people2 = sqlgen::write(conn, people1) + .and_then(sqlgen::read> | + where("id"_c == people1.front().id())) + .value(); + + const auto json1 = rfl::json::write(std::vector({people1.front()})); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_dynamic_type + diff --git a/tests/duckdb/test_enum_crosstable.cpp b/tests/duckdb/test_enum_crosstable.cpp new file mode 100644 index 0000000..c11f1d2 --- /dev/null +++ b/tests/duckdb/test_enum_crosstable.cpp @@ -0,0 +1,100 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_enum_cross_table { +enum class AccessRestriction { PUBLIC, INTERNAL, CONFIDENTIAL }; +struct Employee { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + AccessRestriction access_level; +}; + +struct Document { + sqlgen::PrimaryKey id; + AccessRestriction min_access_level; + std::string name; + std::string path; +}; + +TEST(duckdb, test_enum_cross_table) { + using namespace sqlgen; + using namespace sqlgen::literals; + + auto employees = std::vector({ + Employee{.first_name = "Homer", + .last_name = "Simpson", + .access_level = AccessRestriction::PUBLIC}, + Employee{.first_name = "Waylon", + .last_name = "Smithers", + .access_level = AccessRestriction::INTERNAL}, + Employee{.first_name = "Montgomery", + .last_name = "Burns", + .access_level = AccessRestriction::CONFIDENTIAL}, + }); + auto documents = std::vector({ + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Power Plant Safety Manual", + .path = "/documents/powerplant/safety_manual.txt"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Staff Memo", + .path = "/documents/powerplant/staff_memo.txt"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Operations Report", + .path = "/documents/powerplant/operations_report.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Project Plan", + .path = "/documents/powerplant/project_plan.md"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Budget Q1", + .path = "/documents/powerplant/budget_q1.pdf"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "HR Policies", + .path = "/documents/powerplant/hr_policies.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Team Photo", + .path = "/documents/powerplant/team.jpg"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Executive Summary", + .path = "/documents/powerplant/executive_summary.docx"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Release Notes", + .path = "/documents/powerplant/release_notes.txt"}, + }); + + const auto conn = duckdb::connect() + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(write(employees)) + .and_then(write(documents)); + + const auto smithers = conn.and_then(sqlgen::read | + where("last_name"_c == "Smithers" and + "first_name"_c == "Waylon")) + .value(); + + const auto smithers_level = smithers.access_level; + + const auto smithers_documents = + conn.and_then(sqlgen::read> | + where("min_access_level"_c == smithers_level || + "min_access_level"_c == AccessRestriction::PUBLIC) | + order_by("name"_c)) + .value(); + + const auto expected_ids = std::set{1, 2, 4, 5, 7, 9}; + std::set actual_ids; + for (const auto &d : smithers_documents) { + actual_ids.emplace(d.id()); + } + + EXPECT_EQ(expected_ids, actual_ids); +} + +} // namespace test_enum_cross_table + diff --git a/tests/duckdb/test_enum_lookup.cpp b/tests/duckdb/test_enum_lookup.cpp new file mode 100644 index 0000000..b4f8895 --- /dev/null +++ b/tests/duckdb/test_enum_lookup.cpp @@ -0,0 +1,84 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_enum_lookup { + +enum class AccessRestriction { PUBLIC, INTERNAL, CONFIDENTIAL }; + +struct Document { + sqlgen::PrimaryKey id; + AccessRestriction min_access_level; + std::string name; + std::string path; +}; + +TEST(duckdb, test_enum_lookup) { + auto documents = std::vector({ + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Power Plant Safety Manual", + .path = "/documents/powerplant/safety_manual.txt"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Staff Memo", + .path = "/documents/powerplant/staff_memo.txt"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Operations Report", + .path = "/documents/powerplant/operations_report.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Project Plan", + .path = "/documents/powerplant/project_plan.md"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Budget Q1", + .path = "/documents/powerplant/budget_q1.pdf"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "HR Policies", + .path = "/documents/powerplant/hr_policies.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Team Photo", + .path = "/documents/powerplant/team.jpg"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Executive Summary", + .path = "/documents/powerplant/executive_summary.docx"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Release Notes", + .path = "/documents/powerplant/release_notes.txt"}, + }); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto public_documents = + duckdb::connect() + .and_then(drop | if_exists) + .and_then(write(std::ref(documents))) + .and_then(sqlgen::read> | + where("min_access_level"_c == AccessRestriction::PUBLIC) | + order_by("name"_c.desc())) + .value(); + + const auto expected = std::vector({ + Document{.id = 7, + .min_access_level = AccessRestriction::PUBLIC, + .name = "Team Photo", + .path = "/documents/powerplant/team.jpg"}, + Document{.id = 4, + .min_access_level = AccessRestriction::PUBLIC, + .name = "Project Plan", + .path = "/documents/powerplant/project_plan.md"}, + Document{.id = 1, + .min_access_level = AccessRestriction::PUBLIC, + .name = "Power Plant Safety Manual", + .path = "/documents/powerplant/safety_manual.txt"}, + }); + + const auto json1 = rfl::json::write(expected); + const auto json2 = rfl::json::write(public_documents); + EXPECT_EQ(json1, json2); +} + +} // namespace test_enum_lookup + diff --git a/tests/duckdb/test_enum_namespace.cpp b/tests/duckdb/test_enum_namespace.cpp new file mode 100644 index 0000000..920582a --- /dev/null +++ b/tests/duckdb/test_enum_namespace.cpp @@ -0,0 +1,59 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_enum_namespace { +namespace first { +enum class IdenticallyNamed { VALUE0, VALUE1, VALUE2 }; + +} +namespace second { +enum class IdenticallyNamed { VALUE3, VALUE4, VALUE5 }; +} + +struct MultiStruct { + sqlgen::PrimaryKey id; + first::IdenticallyNamed enum_one; + second::IdenticallyNamed enum_two; +}; + +TEST(duckdb, test_enum_namespace) { + using namespace sqlgen; + using namespace sqlgen::literals; + + auto objects = std::vector({ + MultiStruct{.enum_one = first::IdenticallyNamed::VALUE0, + .enum_two = second::IdenticallyNamed::VALUE3}, + MultiStruct{.enum_one = first::IdenticallyNamed::VALUE1, + .enum_two = second::IdenticallyNamed::VALUE4}, + MultiStruct{.enum_one = first::IdenticallyNamed::VALUE2, + .enum_two = second::IdenticallyNamed::VALUE5}, + }); + + const auto conn = duckdb::connect(); + conn.and_then(drop | if_exists); + + write(conn, objects); + + const auto read_objects = + sqlgen::read>(conn).value(); + std::vector actual_ids; + for (const auto& obj : read_objects) { + if (obj.enum_one == first::IdenticallyNamed::VALUE0) { + EXPECT_EQ(obj.enum_two, second::IdenticallyNamed::VALUE3); + } else if (obj.enum_one == first::IdenticallyNamed::VALUE1) { + EXPECT_EQ(obj.enum_two, second::IdenticallyNamed::VALUE4); + } else if (obj.enum_one == first::IdenticallyNamed::VALUE2) { + EXPECT_EQ(obj.enum_two, second::IdenticallyNamed::VALUE5); + } else { + FAIL() << "Unexpected enum value"; + } + } +} + +} // namespace test_enum_namespace + diff --git a/tests/duckdb/test_flatten.cpp b/tests/duckdb/test_flatten.cpp new file mode 100644 index 0000000..b3856b8 --- /dev/null +++ b/tests/duckdb/test_flatten.cpp @@ -0,0 +1,45 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_write_and_read { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +struct Employee { + static constexpr const char* tablename = "EMPLOYEES"; + + sqlgen::Flatten person; + float salary; +}; + +TEST(duckdb, test_flatten) { + const auto people1 = + std::vector({Employee{.person = Person{.id = 0, + .first_name = "Homer", + .last_name = "Simpson", + .age = 45}, + .salary = 60000.0}}); + + const auto conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + const auto people2 = sqlgen::read>(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 diff --git a/tests/duckdb/test_foreign_key.cpp b/tests/duckdb/test_foreign_key.cpp new file mode 100644 index 0000000..2224fa0 --- /dev/null +++ b/tests/duckdb/test_foreign_key.cpp @@ -0,0 +1,58 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_foreign_key { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +struct Relationship { + sqlgen::ForeignKey parent_id; + uint32_t child_id; +}; + +TEST(duckdb, test_foreign_key) { + auto people1 = std::vector( + {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 relationships = + std::vector({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}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people = duckdb::connect() + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(begin_transaction) + .and_then(create_table) + .and_then(create_table) + .and_then(insert(std::ref(people1))) + .and_then(insert(std::ref(relationships))) + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(commit) + .value(); +} + +} // namespace test_foreign_key + diff --git a/tests/duckdb/test_full_join.cpp b/tests/duckdb/test_full_join.cpp new file mode 100644 index 0000000..27a0111 --- /dev/null +++ b/tests/duckdb/test_full_join.cpp @@ -0,0 +1,69 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_full_join { + +TEST(duckdb, test_full_join) { + struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + }; + + struct Pet { + sqlgen::PrimaryKey id; + std::string name; + uint32_t owner_id; + }; + + const auto people = std::vector({ + Person{.id = 1, .first_name = "Homer", .last_name = "Simpson"}, + Person{.id = 2, .first_name = "Marge", .last_name = "Simpson"}, + Person{.id = 3, .first_name = "Bart", .last_name = "Simpson"}, + Person{.id = 4, .first_name = "Lisa", .last_name = "Simpson"}, + }); + + const auto pets = std::vector({ + Pet{.id = 1, .name = "Santa's Little Helper", .owner_id = 1}, + Pet{.id = 2, .name = "Snowball", .owner_id = 4}, + Pet{.id = 3, .name = "Mr. Teeny", .owner_id = 99}, + }); + + using namespace sqlgen; + using namespace sqlgen::literals; + + struct PersonAndPet { + std::optional first_name; + std::optional last_name; + std::optional pet_name; + }; + + const auto get_all = + select_from("first_name"_t1 | as<"first_name">, + "last_name"_t1 | as<"last_name">, + "name"_t2 | as<"pet_name">) | + full_join("id"_t1 == "owner_id"_t2) | + order_by("id"_t1, "id"_t2) | to>; + + const auto result = duckdb::connect() + .and_then(write(std::ref(people))) + .and_then(write(std::ref(pets))) + .and_then(get_all) + .value(); + + const std::string expected_query = + R"(SELECT t1."first_name" AS "first_name", t1."last_name" AS "last_name", t2."name" AS "pet_name" FROM "Person" t1 FULL JOIN "Pet" t2 ON t1."id" = t2."owner_id" ORDER BY t1."id", t2."id")"; + const std::string expected_json = + R"([{"first_name":"Homer","last_name":"Simpson","pet_name":"Santa's Little Helper"},{"first_name":"Marge","last_name":"Simpson"},{"first_name":"Bart","last_name":"Simpson"},{"first_name":"Lisa","last_name":"Simpson","pet_name":"Snowball"},{"pet_name":"Mr. Teeny"}])"; + + EXPECT_EQ(duckdb::to_sql(get_all), expected_query); + EXPECT_EQ(rfl::json::write(result), expected_json); +} + +} // namespace test_full_join diff --git a/tests/duckdb/test_group_by.cpp b/tests/duckdb/test_group_by.cpp new file mode 100644 index 0000000..a04e841 --- /dev/null +++ b/tests/duckdb/test_group_by.cpp @@ -0,0 +1,67 @@ + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_group_by { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_group_by) { + const auto people1 = std::vector( + {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; + 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( + "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>; + + const auto children = duckdb::connect() + .and_then(drop | 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 + diff --git a/tests/duckdb/test_group_by_with_operations.cpp b/tests/duckdb/test_group_by_with_operations.cpp new file mode 100644 index 0000000..c3da456 --- /dev/null +++ b/tests/duckdb/test_group_by_with_operations.cpp @@ -0,0 +1,63 @@ + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_group_by_with_operations { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_group_by_with_operations) { + const auto people1 = std::vector( + {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; + 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( + "last_name"_c, trim("last_name"_c).as<"last_name_trimmed">(), + max(cast("age"_c) + 1.0).as<"max_age_plus_one">(), + (min(cast("age"_c)) + 1.0).as<"min_age_plus_one">(), + round(avg(cast("age"_c))).as<"avg_age">()) | + where("age"_c < 18) | group_by("last_name"_c) | to>; + + const auto conn = duckdb::connect() + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))); + + const auto children = get_children(conn).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 + diff --git a/tests/duckdb/test_hello_world.cpp b/tests/duckdb/test_hello_world.cpp new file mode 100644 index 0000000..cf69eb0 --- /dev/null +++ b/tests/duckdb/test_hello_world.cpp @@ -0,0 +1,32 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_hello_world { + +struct User { + std::string name; + int age; +}; + +TEST(duckdb, test_hello_world) { + // Connect to duckdb database + const auto conn = sqlgen::duckdb::connect("test.db"); + + // Create and insert a user + const auto user = User{.name = "John", .age = 30}; + sqlgen::write(conn, user); + + // Read all users + const auto users = sqlgen::read>(conn).value(); + + EXPECT_EQ(users.size(), 1); + EXPECT_EQ(users.at(0).name, "John"); + EXPECT_EQ(users.at(0).age, 30); +} + +} // namespace test_hello_world diff --git a/tests/duckdb/test_in.cpp b/tests/duckdb/test_in.cpp new file mode 100644 index 0000000..dc1923e --- /dev/null +++ b/tests/duckdb/test_in.cpp @@ -0,0 +1,48 @@ + +#include + +#include +#include +#include +#include +#include + +namespace test_in { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_in) { + const auto people1 = std::vector( + {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}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people2 = + duckdb::connect() + .and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + where("first_name"_c.in("Bart", "Lisa", "Maggie")) | + order_by("age"_c)) + .value(); + + const std::string expected1 = + 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); +} + +} // namespace test_in + diff --git a/tests/duckdb/test_in_vec.cpp b/tests/duckdb/test_in_vec.cpp new file mode 100644 index 0000000..2160aba --- /dev/null +++ b/tests/duckdb/test_in_vec.cpp @@ -0,0 +1,49 @@ + +#include + +#include +#include +#include +#include +#include + +namespace test_in_vec { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_in_vec) { + const auto people1 = std::vector( + {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}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people2 = + duckdb::connect() + .and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + where("first_name"_c.in( + std::vector({"Bart", "Lisa", "Maggie"}))) | + order_by("age"_c)) + .value(); + + const std::string expected1 = + 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); +} + +} // namespace test_in_vec + diff --git a/tests/duckdb/test_insert_and_read.cpp b/tests/duckdb/test_insert_and_read.cpp new file mode 100644 index 0000000..8efb523 --- /dev/null +++ b/tests/duckdb/test_insert_and_read.cpp @@ -0,0 +1,44 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_insert_and_read { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_insert_and_read) { + const auto people1 = std::vector( + {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; + using namespace sqlgen::literals; + + const auto people2 = duckdb::connect() + .and_then(begin_transaction) + .and_then(create_table | if_not_exists) + .and_then(insert(people1)) + .and_then(commit) + .and_then(sqlgen::read>) + .value(); + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_insert_and_read diff --git a/tests/duckdb/test_insert_by_ref_and_read.cpp b/tests/duckdb/test_insert_by_ref_and_read.cpp new file mode 100644 index 0000000..ffa866e --- /dev/null +++ b/tests/duckdb/test_insert_by_ref_and_read.cpp @@ -0,0 +1,44 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_insert_by_ref_and_read { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_insert_by_ref_and_read) { + const auto people1 = std::vector( + {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; + using namespace sqlgen::literals; + + const auto people2 = duckdb::connect() + .and_then(begin_transaction) + .and_then(create_table | if_not_exists) + .and_then(insert(std::ref(people1))) + .and_then(commit) + .and_then(sqlgen::read>) + .value(); + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_insert_by_ref_and_read diff --git a/tests/duckdb/test_insert_fail.cpp b/tests/duckdb/test_insert_fail.cpp new file mode 100644 index 0000000..cfb62d1 --- /dev/null +++ b/tests/duckdb/test_insert_fail.cpp @@ -0,0 +1,57 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_insert_fail { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_insert_fail) { + const auto people1 = std::vector( + {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; + using namespace sqlgen::literals; + + const auto conn = duckdb::connect() + .and_then(drop | if_exists) + .and_then(write(people1.at(0))); + + const auto res = conn.and_then(insert(people1.at(0))); + + // Should fail - duplicate key violation. + EXPECT_FALSE(res && true); + + const auto people2 = + conn.and_then(begin_transaction) + .and_then(insert(people1 | std::ranges::views::drop(1))) + .and_then(commit) + .and_then(sqlgen::read>) + .value(); + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_insert_fail + +#endif diff --git a/tests/duckdb/test_insert_or_replace.cpp b/tests/duckdb/test_insert_or_replace.cpp new file mode 100644 index 0000000..e2f1c28 --- /dev/null +++ b/tests/duckdb/test_insert_or_replace.cpp @@ -0,0 +1,70 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_insert_or_replace { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_insert_or_replace) { + const auto people1 = std::vector( + {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 people2 = std::vector({Person{.id = 1, + .first_name = "Bartholomew", + .last_name = "Simpson", + .age = 10}, + Person{.id = 3, + .first_name = "Margaret", + .last_name = "Simpson", + .age = 1}}); + + const auto people3 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, + .first_name = "Bartholomew", + .last_name = "Simpson", + .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{.id = 3, + .first_name = "Margaret", + .last_name = "Simpson", + .age = 1}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people4 = + duckdb::connect() + .and_then(begin_transaction) + .and_then(create_table | if_not_exists) + .and_then(insert(std::ref(people1))) + .and_then(commit) + .and_then(begin_transaction) + .and_then(insert_or_replace(std::ref(people2))) + .and_then(commit) + .and_then(sqlgen::read> | order_by("id"_c)) + .value(); + + const auto json3 = rfl::json::write(people3); + const auto json4 = rfl::json::write(people4); + + EXPECT_EQ(json3, json4); +} + +} // namespace test_insert_or_replace diff --git a/tests/duckdb/test_is_null.cpp b/tests/duckdb/test_is_null.cpp new file mode 100644 index 0000000..3910f48 --- /dev/null +++ b/tests/duckdb/test_is_null.cpp @@ -0,0 +1,55 @@ + +#include + +#include +#include +#include +#include +#include + +namespace test_is_null { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(duckdb, test_is_null) { + const auto people1 = std::vector( + {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"}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto conn = duckdb::connect(); + + const auto people2 = + conn.and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + where("age"_c.is_null()) | order_by("first_name"_c.desc())) + .value(); + + const auto people3 = + conn.and_then(sqlgen::read> | + 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 + diff --git a/tests/duckdb/test_join.cpp b/tests/duckdb/test_join.cpp new file mode 100644 index 0000000..7e69ee1 --- /dev/null +++ b/tests/duckdb/test_join.cpp @@ -0,0 +1,51 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_join { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +TEST(duckdb, test_join) { + const auto people1 = std::vector( + {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; + using namespace sqlgen::literals; + + const auto get_people = + select_from( + "id"_t1 | as<"id">, "first_name"_t1 | as<"first_name">, + "last_name"_t2 | as<"last_name">, "age"_t2 | as<"age">) | + inner_join("id"_t1 == "id"_t2) | order_by("id"_t1) | + to>; + + const auto people = duckdb::connect() + .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 INNER 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(duckdb::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_join diff --git a/tests/duckdb/test_joins_from.cpp b/tests/duckdb/test_joins_from.cpp new file mode 100644 index 0000000..30aba80 --- /dev/null +++ b/tests/duckdb/test_joins_from.cpp @@ -0,0 +1,86 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_joins_from { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + uint32_t parent_id; + uint32_t child_id; +}; + +TEST(duckdb, test_joins_from) { + const auto people1 = std::vector( + {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{.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}}); + + 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_parents = + select_from( + "child_id"_t2 | as<"id">, "first_name"_t1 | as<"first_name">, + "last_name"_t1 | as<"last_name">, "age"_t1 | as<"age">) | + inner_join("id"_t1 == "parent_id"_t2); + + const auto get_people = + select_from<"t1">(get_parents, "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("id"_t1 == "id"_t2) | + order_by("id"_t2, "id"_t1) | to>; + + const auto people = duckdb::connect() + .and_then(drop | if_exists) + .and_then(drop | 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 (SELECT t2."child_id" AS "id", t1."first_name" AS "first_name", t1."last_name" AS "last_name", t1."age" AS "age" FROM "Person" t1 INNER JOIN "Relationship" t2 ON t1."id" = t2."parent_id") t1 INNER JOIN "Person" t2 ON t1."id" = t2."id" ORDER BY t2."id", t1."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":"Marge","first_name_child":"Bart","parent_age_at_birth":30.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Lisa","parent_age_at_birth":37.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Lisa","parent_age_at_birth":32.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":"Maggie","parent_age_at_birth":40.0}])"; + + EXPECT_EQ(duckdb::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_from + diff --git a/tests/duckdb/test_joins_nested.cpp b/tests/duckdb/test_joins_nested.cpp new file mode 100644 index 0000000..abdfc4b --- /dev/null +++ b/tests/duckdb/test_joins_nested.cpp @@ -0,0 +1,82 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_joins_nested { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + uint32_t parent_id; + uint32_t child_id; +}; + +TEST(duckdb, test_joins_nested) { + const auto people1 = std::vector( + {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{.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}}); + + 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("parent_id"_t1 | as<"id">, + "first_name"_t2 | as<"first_name">, + "age"_t2 | as<"age">) | + inner_join("id"_t2 == "child_id"_t1); + + const auto get_people = + select_from( + "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>; + + const auto people = duckdb::connect() + .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 INNER 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":"Maggie","parent_age_at_birth":45.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":"Bart","parent_age_at_birth":35.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Maggie","parent_age_at_birth":40.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":"Bart","parent_age_at_birth":30.0}])"; + + EXPECT_EQ(duckdb::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_nested diff --git a/tests/duckdb/test_joins_nested_grouped.cpp b/tests/duckdb/test_joins_nested_grouped.cpp new file mode 100644 index 0000000..990e4d5 --- /dev/null +++ b/tests/duckdb/test_joins_nested_grouped.cpp @@ -0,0 +1,81 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_joins_nested_grouped { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + uint32_t parent_id; + uint32_t child_id; +}; + +TEST(duckdb, test_joins_nested_grouped) { + const auto people1 = std::vector( + {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{.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}}); + + 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("parent_id"_t1 | as<"id">, + "first_name"_t2 | as<"first_name">, + "age"_t2 | as<"age">) | + inner_join("id"_t2 == "child_id"_t1); + + const auto get_people = + select_from( + "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>; + + const auto people = duckdb::connect() + .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 INNER 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(duckdb::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_nested_grouped diff --git a/tests/duckdb/test_joins_two_tables.cpp b/tests/duckdb/test_joins_two_tables.cpp new file mode 100644 index 0000000..2caa027 --- /dev/null +++ b/tests/duckdb/test_joins_two_tables.cpp @@ -0,0 +1,77 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_joins_two_tables { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + uint32_t parent_id; + uint32_t child_id; +}; + +TEST(duckdb, test_joins_two_tables) { + const auto people1 = std::vector( + {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{.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}}); + + 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( + "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("id"_t1 == "parent_id"_t2) | + inner_join("id"_t3 == "child_id"_t2) | + order_by("id"_t1, "id"_t3) | to>; + + const auto people = duckdb::connect() + .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" INNER 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(duckdb::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_two_tables diff --git a/tests/duckdb/test_joins_two_tables_grouped.cpp b/tests/duckdb/test_joins_two_tables_grouped.cpp new file mode 100644 index 0000000..9ec6167 --- /dev/null +++ b/tests/duckdb/test_joins_two_tables_grouped.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_joins_two_tables_grouped { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + uint32_t parent_id; + uint32_t child_id; +}; + +TEST(duckdb, test_joins_two_tables_grouped) { + const auto people1 = std::vector( + {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{.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}}); + + 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( + "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("id"_t1 == "parent_id"_t2) | + inner_join("id"_t3 == "child_id"_t2) | + group_by("last_name"_t1, "first_name"_t3) | + order_by("last_name"_t1, "first_name"_t3) | + to>; + + const auto people = duckdb::connect() + .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" INNER 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(duckdb::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_two_tables_grouped diff --git a/tests/duckdb/test_json.cpp b/tests/duckdb/test_json.cpp new file mode 100644 index 0000000..b970301 --- /dev/null +++ b/tests/duckdb/test_json.cpp @@ -0,0 +1,45 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_json { + +struct Person { + std::string first_name; + std::string last_name; + int age; + sqlgen::JSON>> children; +}; + +TEST(duckdb, test_json) { + const auto children = std::vector( + {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 homer = Person{.first_name = "Homer", + .last_name = "Simpson", + .age = 45, + .children = children}; + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people2 = sqlgen::duckdb::connect() + .and_then(drop | if_exists) + .and_then(write(std::ref(homer))) + .and_then(sqlgen::read>) + .value(); + + const std::string expected = + R"([{"first_name":"Homer","last_name":"Simpson","age":45,"children":[{"first_name":"Bart","last_name":"Simpson","age":10},{"first_name":"Lisa","last_name":"Simpson","age":8},{"first_name":"Maggie","last_name":"Simpson","age":0}]}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_json + diff --git a/tests/duckdb/test_left_join.cpp b/tests/duckdb/test_left_join.cpp new file mode 100644 index 0000000..4e7cb37 --- /dev/null +++ b/tests/duckdb/test_left_join.cpp @@ -0,0 +1,69 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_left_join { + +TEST(duckdb, test_left_join) { + struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + }; + + struct Pet { + sqlgen::PrimaryKey id; + std::string name; + uint32_t owner_id; + }; + + const auto people = std::vector({ + Person{.id = 1, .first_name = "Homer", .last_name = "Simpson"}, + Person{.id = 2, .first_name = "Marge", .last_name = "Simpson"}, + Person{.id = 3, .first_name = "Bart", .last_name = "Simpson"}, + Person{.id = 4, .first_name = "Lisa", .last_name = "Simpson"}, + }); + + const auto pets = std::vector({ + Pet{.id = 1, .name = "Santa's Little Helper", .owner_id = 1}, + Pet{.id = 2, .name = "Snowball", .owner_id = 4}, + Pet{.id = 3, .name = "Mr. Teeny", .owner_id = 99}, + }); + + using namespace sqlgen; + using namespace sqlgen::literals; + + struct PersonWithPet { + std::string first_name; + std::string last_name; + std::optional pet_name; + }; + + const auto get_people_with_pets = + select_from("first_name"_t1 | as<"first_name">, + "last_name"_t1 | as<"last_name">, + "name"_t2 | as<"pet_name">) | + left_join("id"_t1 == "owner_id"_t2) | order_by("id"_t1) | + to>; + + const auto result = duckdb::connect() + .and_then(write(std::ref(people))) + .and_then(write(std::ref(pets))) + .and_then(get_people_with_pets) + .value(); + + const std::string expected_query = + R"(SELECT t1."first_name" AS "first_name", t1."last_name" AS "last_name", t2."name" AS "pet_name" FROM "Person" t1 LEFT JOIN "Pet" t2 ON t1."id" = t2."owner_id" ORDER BY t1."id")"; + const std::string expected_json = + R"([{"first_name":"Homer","last_name":"Simpson","pet_name":"Santa's Little Helper"},{"first_name":"Marge","last_name":"Simpson"},{"first_name":"Bart","last_name":"Simpson"},{"first_name":"Lisa","last_name":"Simpson","pet_name":"Snowball"}])"; + + EXPECT_EQ(duckdb::to_sql(get_people_with_pets), expected_query); + EXPECT_EQ(rfl::json::write(result), expected_json); +} + +} // namespace test_left_join diff --git a/tests/duckdb/test_like.cpp b/tests/duckdb/test_like.cpp new file mode 100644 index 0000000..a968121 --- /dev/null +++ b/tests/duckdb/test_like.cpp @@ -0,0 +1,65 @@ + +#include + +#include +#include +#include +#include +#include + +namespace test_like { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_like) { + const auto people1 = std::vector( + {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}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto conn = duckdb::connect(); + + const auto people2 = + conn.and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + where("first_name"_c.like("H%")) | order_by("age"_c)) + .value(); + + const auto people3 = + conn.and_then(sqlgen::read> | + where("first_name"_c.not_like("H%")) | order_by("age"_c)) + .value(); + + const auto people4 = + conn.and_then(sqlgen::read> | + 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 + diff --git a/tests/duckdb/test_limit.cpp b/tests/duckdb/test_limit.cpp new file mode 100644 index 0000000..e3cf2d4 --- /dev/null +++ b/tests/duckdb/test_limit.cpp @@ -0,0 +1,47 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_limit { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_limit) { + const auto people1 = std::vector( + {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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query = + sqlgen::read> | 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 diff --git a/tests/duckdb/test_not_in.cpp b/tests/duckdb/test_not_in.cpp new file mode 100644 index 0000000..44dcca7 --- /dev/null +++ b/tests/duckdb/test_not_in.cpp @@ -0,0 +1,48 @@ + +#include + +#include +#include +#include +#include +#include + +namespace test_not_in { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_not_in) { + const auto people1 = std::vector( + {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}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people2 = + duckdb::connect() + .and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + where("first_name"_c.not_in("Homer", "Hugo")) | + order_by("age"_c)) + .value(); + + const std::string expected1 = + 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); +} + +} // namespace test_not_in + diff --git a/tests/duckdb/test_not_in_vec.cpp b/tests/duckdb/test_not_in_vec.cpp new file mode 100644 index 0000000..3996b03 --- /dev/null +++ b/tests/duckdb/test_not_in_vec.cpp @@ -0,0 +1,49 @@ + +#include + +#include +#include +#include +#include +#include + +namespace test_not_in_vec { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_not_in_vec) { + const auto people1 = std::vector( + {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}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people2 = + duckdb::connect() + .and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + where("first_name"_c.not_in( + std::vector({"Homer", "Hugo"}))) | + order_by("age"_c)) + .value(); + + const std::string expected1 = + 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); +} + +} // namespace test_not_in_vec + diff --git a/tests/duckdb/test_operations.cpp b/tests/duckdb/test_operations.cpp new file mode 100644 index 0000000..42bc6c4 --- /dev/null +++ b/tests/duckdb/test_operations.cpp @@ -0,0 +1,76 @@ + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_operations { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_operations) { + const auto people1 = std::vector( + {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; + 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( + ("id"_c + "age"_c) | as<"id_plus_age">, + ("age"_c * 2) | as<"age_times_2">, ("age"_c % 3) | as<"age_mod_3">, + abs("age"_c * (-1)) | as<"abs_age">, + round(exp(cast("age"_c)), 2) | as<"exp_age">, + round(sqrt(cast("age"_c)), 2) | as<"sqrt_age">, + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_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>; + + const auto children = duckdb::connect() + .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 + diff --git a/tests/duckdb/test_operations_with_nullable.cpp b/tests/duckdb/test_operations_with_nullable.cpp new file mode 100644 index 0000000..c59d969 --- /dev/null +++ b/tests/duckdb/test_operations_with_nullable.cpp @@ -0,0 +1,63 @@ + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace test_operations_with_nullable { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::optional last_name; + std::optional age; +}; + +TEST(duckdb, test_operations_with_nullable) { + const auto people1 = std::vector( + {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}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + struct Children { + std::optional id_plus_age; + std::optional age_times_2; + std::optional id_plus_2_minus_age; + std::optional full_name; + std::string last_name_or_none; + }; + + const auto get_children = + select_from( + ("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(upper("last_name"_c), "none") | as<"last_name_or_none">) | + where("age"_c < 18) | to>; + + const auto children = duckdb::connect() + .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 + diff --git a/tests/duckdb/test_order_by.cpp b/tests/duckdb/test_order_by.cpp new file mode 100644 index 0000000..ea6615e --- /dev/null +++ b/tests/duckdb/test_order_by.cpp @@ -0,0 +1,47 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_order_by { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_order_by) { + const auto people1 = std::vector( + {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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query = sqlgen::read> | + order_by("age"_c, "first_name"_c.desc()); + + 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":4,"first_name":"Hugo","last_name":"Simpson","age":10},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10},{"id":0,"first_name":"Homer","last_name":"Simpson","age":45}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_order_by diff --git a/tests/duckdb/test_range.cpp b/tests/duckdb/test_range.cpp new file mode 100644 index 0000000..dfd32bd --- /dev/null +++ b/tests/duckdb/test_range.cpp @@ -0,0 +1,47 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_range { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_range) { + using namespace std::ranges::views; + + const auto people1 = std::vector( + {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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + const auto people2 = sqlgen::read>(conn).value(); + + const auto first_names = + sqlgen::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 diff --git a/tests/duckdb/test_range_select_from.cpp b/tests/duckdb/test_range_select_from.cpp new file mode 100644 index 0000000..2905c09 --- /dev/null +++ b/tests/duckdb/test_range_select_from.cpp @@ -0,0 +1,52 @@ + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_range_select_from { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_range_select_from) { + const auto people1 = std::vector( + {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; + using namespace sqlgen::literals; + + const auto people2 = + sqlgen::duckdb::connect() + .and_then(sqlgen::write(std::ref(people1))) + .and_then(select_from("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 + diff --git a/tests/duckdb/test_range_select_from_with_to.cpp b/tests/duckdb/test_range_select_from_with_to.cpp new file mode 100644 index 0000000..fce1ceb --- /dev/null +++ b/tests/duckdb/test_range_select_from_with_to.cpp @@ -0,0 +1,51 @@ + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_range_select_from_with_to { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_range_select_from_with_to) { + const auto people1 = std::vector( + {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; + using namespace sqlgen::literals; + + struct FirstName { + std::string first_name; + }; + + const auto people2 = + duckdb::connect() + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(select_from("first_name"_c) | order_by("id"_c) | + to>) + .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 + diff --git a/tests/duckdb/test_right_join.cpp b/tests/duckdb/test_right_join.cpp new file mode 100644 index 0000000..dea2b4f --- /dev/null +++ b/tests/duckdb/test_right_join.cpp @@ -0,0 +1,69 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_right_join { + +TEST(duckdb, test_right_join) { + struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + }; + + struct Pet { + sqlgen::PrimaryKey id; + std::string name; + uint32_t owner_id; + }; + + const auto people = std::vector({ + Person{.id = 1, .first_name = "Homer", .last_name = "Simpson"}, + Person{.id = 2, .first_name = "Marge", .last_name = "Simpson"}, + Person{.id = 3, .first_name = "Bart", .last_name = "Simpson"}, + Person{.id = 4, .first_name = "Lisa", .last_name = "Simpson"}, + }); + + const auto pets = std::vector({ + Pet{.id = 1, .name = "Santa's Little Helper", .owner_id = 1}, + Pet{.id = 2, .name = "Snowball", .owner_id = 4}, + Pet{.id = 3, .name = "Mr. Teeny", .owner_id = 99}, + }); + + using namespace sqlgen; + using namespace sqlgen::literals; + + struct PetWithOwner { + std::string pet_name; + std::optional owner_first_name; + std::optional owner_last_name; + }; + + const auto get_pets_with_owners = + select_from("name"_t2 | as<"pet_name">, + "first_name"_t1 | as<"owner_first_name">, + "last_name"_t1 | as<"owner_last_name">) | + right_join("id"_t1 == "owner_id"_t2) | order_by("id"_t2) | + to>; + + const auto result = duckdb::connect() + .and_then(write(std::ref(people))) + .and_then(write(std::ref(pets))) + .and_then(get_pets_with_owners) + .value(); + + const std::string expected_query = + R"(SELECT t2."name" AS "pet_name", t1."first_name" AS "owner_first_name", t1."last_name" AS "owner_last_name" FROM "Person" t1 RIGHT JOIN "Pet" t2 ON t1."id" = t2."owner_id" ORDER BY t2."id")"; + const std::string expected_json = + R"([{"pet_name":"Santa's Little Helper","owner_first_name":"Homer","owner_last_name":"Simpson"},{"pet_name":"Snowball","owner_first_name":"Lisa","owner_last_name":"Simpson"},{"pet_name":"Mr. Teeny"}])"; + + EXPECT_EQ(duckdb::to_sql(get_pets_with_owners), expected_query); + EXPECT_EQ(rfl::json::write(result), expected_json); +} + +} // namespace test_right_join diff --git a/tests/duckdb/test_select_from_with_timestamps.cpp b/tests/duckdb/test_select_from_with_timestamps.cpp new file mode 100644 index 0000000..f11d7b2 --- /dev/null +++ b/tests/duckdb/test_select_from_with_timestamps.cpp @@ -0,0 +1,79 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace test_range_select_from_with_timestamps { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + sqlgen::Date birthday; +}; + +TEST(duckdb, test_range_select_from_with_timestamps) { + const auto people1 = + std::vector({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")}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + struct Birthday { + Date birthday; + Date birthday_recreated; + time_t birthday_unixepoch; + double age_in_days; + int hour; + int minute; + int second; + int weekday; + }; + + const auto get_birthdays = + select_from( + ("birthday"_c + std::chrono::days(10)) | as<"birthday">, + ((cast(concat(cast(year("birthday"_c)), "-", + cast(month("birthday"_c)), "-", + cast(day("birthday"_c)))))) | + as<"birthday_recreated">, + 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">, + weekday("birthday"_c) | as<"weekday">) | + order_by("id"_c) | to>; + + const auto birthdays = duckdb::connect() + .and_then(write(std::ref(people1))) + .and_then(get_birthdays) + .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 DATE) 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")"; + const std::string expected = + R"([{"birthday":"1970-01-11","birthday_recreated":"1970-01-01","birthday_unixepoch":864000,"age_in_days":14975.0,"hour":0,"minute":0,"second":0,"weekday":4},{"birthday":"2000-01-11","birthday_recreated":"2000-01-01","birthday_unixepoch":947548800,"age_in_days":4018.0,"hour":0,"minute":0,"second":0,"weekday":6},{"birthday":"2002-01-11","birthday_recreated":"2002-01-01","birthday_unixepoch":1010707200,"age_in_days":3287.0,"hour":0,"minute":0,"second":0,"weekday":2},{"birthday":"2010-01-11","birthday_recreated":"2010-01-01","birthday_unixepoch":1263168000,"age_in_days":365.0,"hour":0,"minute":0,"second":0,"weekday":5}])"; + + EXPECT_EQ(duckdb::to_sql(get_birthdays), expected_query); + EXPECT_EQ(rfl::json::write(birthdays), expected); +} + +} // namespace test_range_select_from_with_timestamps + diff --git a/tests/duckdb/test_single_read.cpp b/tests/duckdb/test_single_read.cpp new file mode 100644 index 0000000..ce3ecdd --- /dev/null +++ b/tests/duckdb/test_single_read.cpp @@ -0,0 +1,43 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_single_read { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_single_read) { + const auto people1 = std::vector( + {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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people2 = + (sqlgen::read | where("id"_c == 0))(conn).value(); + + const auto json1 = rfl::json::write(people1.at(0)); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_single_read diff --git a/tests/duckdb/test_timestamp.cpp b/tests/duckdb/test_timestamp.cpp new file mode 100644 index 0000000..0e5fc1c --- /dev/null +++ b/tests/duckdb/test_timestamp.cpp @@ -0,0 +1,49 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_timestamp { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + sqlgen::Timestamp<"%Y-%m-%d %H:%M:%S"> birthdate; +}; + +TEST(duckdb, test_timestamp) { + const auto people1 = + std::vector({Person{.id = 0, + .first_name = "Homer", + .last_name = "Simpson", + .birthdate = "1989-12-17 12:00:00"}, + Person{.id = 1, + .first_name = "Bart", + .last_name = "Simpson", + .birthdate = "1989-12-17 12:00:00"}, + Person{.id = 2, + .first_name = "Lisa", + .last_name = "Simpson", + .birthdate = "1989-12-17 12:00:00"}, + Person{.id = 3, + .first_name = "Maggie", + .last_name = "Simpson", + .birthdate = "1989-12-17 12:00:00"}}); + + const auto conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + const auto people2 = sqlgen::read>(conn).value(); + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_timestamp diff --git a/tests/duckdb/test_to_create_table.cpp b/tests/duckdb/test_to_create_table.cpp new file mode 100644 index 0000000..326343a --- /dev/null +++ b/tests/duckdb/test_to_create_table.cpp @@ -0,0 +1,25 @@ +#include + +#include +#include +#include + +namespace test_to_create_table { + +struct TestTable { + std::string field1; + int32_t field2; + sqlgen::PrimaryKey id; + std::optional nullable; +}; + +TEST(duckdb, test_to_create_table) { + const auto create_table_stmt = + sqlgen::transpilation::to_create_table(); + const auto conn = sqlgen::duckdb::connect().value(); + const auto expected = + R"(CREATE TABLE IF NOT EXISTS "TestTable"("field1" TEXT NOT NULL, "field2" INTEGER NOT NULL, "id" UINTEGER NOT NULL, "nullable" TEXT, PRIMARY KEY ("id"));)"; + + EXPECT_EQ(conn->to_sql(create_table_stmt), expected); +} +} // namespace test_to_create_table diff --git a/tests/duckdb/test_to_insert.cpp b/tests/duckdb/test_to_insert.cpp new file mode 100644 index 0000000..90fd641 --- /dev/null +++ b/tests/duckdb/test_to_insert.cpp @@ -0,0 +1,27 @@ +#include + +#include +#include +#include +#include + +namespace test_to_insert { + +struct TestTable { + std::string field1; + int32_t field2; + sqlgen::PrimaryKey id; + std::optional nullable; +}; + +TEST(duckdb, test_to_insert) { + const auto insert_stmt = + sqlgen::transpilation::to_insert_or_write(); + const auto conn = sqlgen::duckdb::connect().value(); + const auto expected = + R"(INSERT INTO "TestTable" BY NAME ( SELECT "field1" AS "field1", "field2" AS "field2", "id" AS "id", "nullable" AS "nullable" FROM sqlgen_appended_data);)"; + + EXPECT_EQ(conn->to_sql(insert_stmt), expected); +} +} // namespace test_to_insert diff --git a/tests/duckdb/test_to_insert_or_replace.cpp b/tests/duckdb/test_to_insert_or_replace.cpp new file mode 100644 index 0000000..81e68be --- /dev/null +++ b/tests/duckdb/test_to_insert_or_replace.cpp @@ -0,0 +1,33 @@ +#include + +#include +#include +#include +#include + +namespace test_to_insert_or_replace { + +struct TestTable { + std::string field1; + int32_t field2; + sqlgen::Unique field3; + sqlgen::PrimaryKey id; + std::optional nullable; +}; + +TEST(duckdb, test_to_insert_or_replace) { + static_assert(sqlgen::internal::has_constraint_v, + "The table must have a primary key or unique column for " + "insert_or_replace(...) to work."); + + const auto insert_stmt = + sqlgen::transpilation::to_insert_or_write(true); + const auto conn = sqlgen::duckdb::connect().value(); + + const auto expected = + R"(INSERT OR REPLACE INTO "TestTable" BY NAME ( SELECT "field1" AS "field1", "field2" AS "field2", "field3" AS "field3", "id" AS "id", "nullable" AS "nullable" FROM sqlgen_appended_data);)"; + + EXPECT_EQ(conn->to_sql(insert_stmt), expected); +} +} // namespace test_to_insert_or_replace diff --git a/tests/duckdb/test_to_select_from.cpp b/tests/duckdb/test_to_select_from.cpp new file mode 100644 index 0000000..ff64e1e --- /dev/null +++ b/tests/duckdb/test_to_select_from.cpp @@ -0,0 +1,24 @@ +#include + +#include +#include + +namespace test_to_select_from { + +struct TestTable { + std::string field1; + int32_t field2; + sqlgen::PrimaryKey id; + std::optional nullable; +}; + +TEST(duckdb, test_to_select_from) { + const auto select_from_stmt = + sqlgen::transpilation::read_to_select_from(); + const auto conn = sqlgen::duckdb::connect().value(); + const auto expected = + R"(SELECT "field1", "field2", "id", "nullable" FROM "TestTable")"; + + EXPECT_EQ(conn->to_sql(select_from_stmt), expected); +} +} // namespace test_to_select_from diff --git a/tests/duckdb/test_to_select_from_with_schema.cpp b/tests/duckdb/test_to_select_from_with_schema.cpp new file mode 100644 index 0000000..0711a48 --- /dev/null +++ b/tests/duckdb/test_to_select_from_with_schema.cpp @@ -0,0 +1,27 @@ +#include + +#include +#include + +namespace test_to_select_from_with_schema { + +struct TestTable { + constexpr static const char* tablename = "test_table"; + constexpr static const char* schema = "my_schema"; + + std::string field1; + int32_t field2; + sqlgen::PrimaryKey id; + std::optional nullable; +}; + +TEST(duckdb, test_to_select_from_with_schema) { + const auto select_from_stmt = + sqlgen::transpilation::read_to_select_from(); + const auto conn = sqlgen::duckdb::connect().value(); + const auto expected = + R"(SELECT "field1", "field2", "id", "nullable" FROM "my_schema"."test_table")"; + + EXPECT_EQ(conn->to_sql(select_from_stmt), expected); +} +} // namespace test_to_select_from_with_schema diff --git a/tests/duckdb/test_transaction.cpp b/tests/duckdb/test_transaction.cpp new file mode 100644 index 0000000..fbf7f61 --- /dev/null +++ b/tests/duckdb/test_transaction.cpp @@ -0,0 +1,55 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_update { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_transaction) { + const auto people1 = std::vector( + {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}}); + + auto conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto delete_hugo = + delete_from | where("first_name"_c == "Hugo"); + + const auto update_homers_age = + update("age"_c.set(46)) | where("first_name"_c == "Homer"); + + conn = sqlgen::begin_transaction(conn) + .and_then(delete_hugo) + .and_then(update_homers_age) + .and_then(sqlgen::commit); + + const auto people2 = sqlgen::read>(conn).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_update diff --git a/tests/duckdb/test_unique.cpp b/tests/duckdb/test_unique.cpp new file mode 100644 index 0000000..a9a7c30 --- /dev/null +++ b/tests/duckdb/test_unique.cpp @@ -0,0 +1,46 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_unique { + +struct Person { + sqlgen::PrimaryKey id; + sqlgen::Unique first_name; + std::string last_name; + double age; +}; + +TEST(duckdb, test_unique) { + const auto people = std::vector( + {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}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + duckdb::connect() + .and_then(drop | if_exists) + .and_then(create_table) + .and_then(insert(std::ref(people))) + .value(); + + const std::string expected_query = + R"(CREATE TABLE IF NOT EXISTS "Person"("id" UINTEGER NOT NULL, "first_name" TEXT NOT NULL UNIQUE, "last_name" TEXT NOT NULL, "age" DOUBLE NOT NULL, PRIMARY KEY ("id"));)"; + + EXPECT_EQ(duckdb::to_sql(create_table), expected_query); +} + +} // namespace test_unique + diff --git a/tests/duckdb/test_update.cpp b/tests/duckdb/test_update.cpp new file mode 100644 index 0000000..aef5b9f --- /dev/null +++ b/tests/duckdb/test_update.cpp @@ -0,0 +1,50 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_update { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_update) { + const auto people1 = std::vector( + {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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query = + update("first_name"_c.set("last_name"_c), "age"_c.set(100)) | + where("first_name"_c == "Hugo"); + + query(conn).value(); + + const auto people2 = sqlgen::read>(conn).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 diff --git a/tests/duckdb/test_varchar.cpp b/tests/duckdb/test_varchar.cpp new file mode 100644 index 0000000..db6ec28 --- /dev/null +++ b/tests/duckdb/test_varchar.cpp @@ -0,0 +1,39 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_varchar { + +struct Person { + sqlgen::PrimaryKey id; + sqlgen::Varchar<6> first_name; + sqlgen::Varchar<7> last_name; + int age; +}; + +TEST(duckdb, test_varchar) { + const auto people1 = std::vector( + {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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + const auto people2 = sqlgen::read>(conn).value(); + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_varchar diff --git a/tests/duckdb/test_where.cpp b/tests/duckdb/test_where.cpp new file mode 100644 index 0000000..6c69afa --- /dev/null +++ b/tests/duckdb/test_where.cpp @@ -0,0 +1,48 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_where { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_where) { + const auto people1 = std::vector( + {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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query = sqlgen::read> | + 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 diff --git a/tests/duckdb/test_where_with_nullable.cpp b/tests/duckdb/test_where_with_nullable.cpp new file mode 100644 index 0000000..307a2e0 --- /dev/null +++ b/tests/duckdb/test_where_with_nullable.cpp @@ -0,0 +1,48 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_where_with_nullable { + +struct Person { + sqlgen::PrimaryKey id; + std::optional first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_where_with_nullable) { + const auto people1 = std::vector( + {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, .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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query = sqlgen::read> | + where("age"_c < 18 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":1,"first_name":"Bart","last_name":"Simpson","age":10}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_where_with_nullable diff --git a/tests/duckdb/test_where_with_nullable_operations.cpp b/tests/duckdb/test_where_with_nullable_operations.cpp new file mode 100644 index 0000000..3f73bf7 --- /dev/null +++ b/tests/duckdb/test_where_with_nullable_operations.cpp @@ -0,0 +1,49 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_where_with_nullable_operations { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(duckdb, test_where_with_nullable_operations) { + const auto people1 = std::vector( + {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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query = sqlgen::read> | + 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_nullable_operations diff --git a/tests/duckdb/test_where_with_operations.cpp b/tests/duckdb/test_where_with_operations.cpp new file mode 100644 index 0000000..7480b8d --- /dev/null +++ b/tests/duckdb/test_where_with_operations.cpp @@ -0,0 +1,48 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_where_with_operations { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_where_with_operations) { + const auto people1 = std::vector( + {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 conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query = sqlgen::read> | + 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 diff --git a/tests/duckdb/test_where_with_timestamps.cpp b/tests/duckdb/test_where_with_timestamps.cpp new file mode 100644 index 0000000..2e077d9 --- /dev/null +++ b/tests/duckdb/test_where_with_timestamps.cpp @@ -0,0 +1,60 @@ + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_where_with_timestamps { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + sqlgen::Date birthday; +}; + +TEST(duckdb, test_where_with_timestamps) { + const auto people1 = + std::vector({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")}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1).value(); + + const auto query = + sqlgen::read> | + where("birthday"_c + std::chrono::years(11) - std::chrono::weeks(10) + + std::chrono::milliseconds(4000005) > + Date("2010-01-01")) | + order_by("id"_c); + + const auto people2 = query(conn).value(); + + const std::string expected_query = + R"(SELECT "id", "first_name", "last_name", "birthday" FROM "Person" WHERE datetime("birthday", '+11 years', '-70 days', '+01:06:40.005') > '2010-01-01' ORDER BY "id";)"; + 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 + diff --git a/tests/duckdb/test_write_and_read.cpp b/tests/duckdb/test_write_and_read.cpp new file mode 100644 index 0000000..e4c7e59 --- /dev/null +++ b/tests/duckdb/test_write_and_read.cpp @@ -0,0 +1,39 @@ +#include + +#include +#include +#include +#include + +namespace test_write_and_read { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_write_and_read) { + const auto people1 = std::vector( + {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 people2 = duckdb::connect() + .and_then(write(std::ref(people1))) + .and_then(sqlgen::read>) + .value(); + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_write_and_read diff --git a/tests/duckdb/test_write_and_read_curried.cpp b/tests/duckdb/test_write_and_read_curried.cpp new file mode 100644 index 0000000..4b5c1c6 --- /dev/null +++ b/tests/duckdb/test_write_and_read_curried.cpp @@ -0,0 +1,42 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_write_and_read_curried { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_write_and_read_curried) { + const auto people1 = std::vector( + {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; + using namespace sqlgen::literals; + + const auto people2 = duckdb::connect() + .and_then(write(std::ref(people1))) + .and_then(sqlgen::read>) + .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 diff --git a/tests/duckdb/test_write_and_read_to_file.cpp b/tests/duckdb/test_write_and_read_to_file.cpp new file mode 100644 index 0000000..47e358e --- /dev/null +++ b/tests/duckdb/test_write_and_read_to_file.cpp @@ -0,0 +1,44 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_write_and_read_to_file { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_write_and_read_to_file) { + const auto people1 = std::vector( + {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}}); + + sqlgen::duckdb::connect("test.db") + .and_then([&](auto&& _conn) { return sqlgen::write(_conn, people1); }) + .value(); + + const auto people2 = sqlgen::duckdb::connect("test.db") + .and_then(sqlgen::read>) + .value(); + + std::remove("test.db"); + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_write_and_read_to_file diff --git a/vcpkg.json b/vcpkg.json index 425e953..4217130 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -6,6 +6,15 @@ } ], "features": { + "duckdb": { + "description": "Enable DuckDB support", + "dependencies": [ + { + "name": "duckdb", + "version>=": "1.4.1" + } + ] + }, "mysql": { "description": "Enable MySQL/MariaDB support", "dependencies": [