From c0d82f5a8dbb68dfe8df325794b10412da83b43a Mon Sep 17 00:00:00 2001 From: Christian Parpart Date: Thu, 24 Oct 2024 23:06:59 +0200 Subject: [PATCH] Initial work to spport building it as shared lib :) Signed-off-by: Christian Parpart --- .github/workflows/build.yml | 15 +- .vimspector.json | 1 - CMakeLists.txt | 12 + CMakePresets.json | 16 + src/Lightweight/Api.hpp | 25 + src/Lightweight/CMakeLists.txt | 78 +- .../DataBinder/BasicStringBinder.hpp | 4 +- src/Lightweight/DataBinder/Core.hpp | 3 +- src/Lightweight/DataBinder/MFCStringLike.hpp | 2 +- src/Lightweight/DataBinder/Primitives.hpp | 2 +- src/Lightweight/DataBinder/SqlDate.hpp | 4 +- src/Lightweight/DataBinder/SqlDateTime.hpp | 6 +- src/Lightweight/DataBinder/SqlFixedString.hpp | 8 +- .../DataBinder/UnicodeConverter.hpp | 14 +- src/Lightweight/Model/AbstractField.hpp | 102 --- src/Lightweight/Model/AbstractRecord.cpp | 64 -- src/Lightweight/Model/AbstractRecord.hpp | 110 --- src/Lightweight/Model/All.hpp | 12 - .../Model/Associations/BelongsTo.hpp | 283 -------- .../Model/Associations/HasMany.hpp | 133 ---- .../Model/Associations/HasManyThrough.hpp | 191 ----- src/Lightweight/Model/Associations/HasOne.hpp | 103 --- .../Model/Associations/HasOneThrough.hpp | 104 --- src/Lightweight/Model/ColumnType.hpp | 44 -- src/Lightweight/Model/Detail.hpp | 42 -- src/Lightweight/Model/Field.hpp | 186 ----- src/Lightweight/Model/Logger.cpp | 83 --- src/Lightweight/Model/Logger.hpp | 67 -- src/Lightweight/Model/README.md | 81 --- src/Lightweight/Model/Record.hpp | 671 ------------------ src/Lightweight/Model/RecordId.hpp | 73 -- src/Lightweight/Model/StringLiteral.hpp | 25 - src/Lightweight/Model/Utils.hpp | 27 - src/Lightweight/SqlConnectInfo.hpp | 4 +- src/Lightweight/SqlConnection.cpp | 176 ++--- src/Lightweight/SqlConnection.hpp | 58 +- src/Lightweight/SqlError.hpp | 8 +- src/Lightweight/SqlLogger.cpp | 13 - src/Lightweight/SqlLogger.hpp | 13 +- src/Lightweight/SqlQuery.hpp | 9 +- src/Lightweight/SqlQuery/Core.hpp | 118 +-- src/Lightweight/SqlQuery/Delete.hpp | 5 +- src/Lightweight/SqlQuery/Insert.hpp | 7 +- src/Lightweight/SqlQuery/Select.hpp | 3 +- src/Lightweight/SqlQuery/Update.hpp | 3 +- src/Lightweight/SqlQueryFormatter.hpp | 3 +- src/Lightweight/SqlSchema.hpp | 11 +- src/Lightweight/SqlScopedTraceLogger.hpp | 3 +- src/Lightweight/SqlStatement.cpp | 57 +- src/Lightweight/SqlStatement.hpp | 55 +- src/Lightweight/SqlTraits.hpp | 4 +- src/Lightweight/SqlTransaction.hpp | 2 +- src/Lightweight/SqlUtils.hpp | 121 +++- src/tests/CMakeLists.txt | 16 +- src/tests/CoreTests.cpp | 33 - src/tests/ModelAssociationsTests.cpp | 428 ----------- src/tests/ModelTests.cpp | 355 --------- src/tests/Utils.hpp | 21 +- src/tools/CMakeLists.txt | 1 + src/tools/ddl2cpp.cpp | 2 - 60 files changed, 519 insertions(+), 3601 deletions(-) create mode 100644 src/Lightweight/Api.hpp delete mode 100644 src/Lightweight/Model/AbstractField.hpp delete mode 100644 src/Lightweight/Model/AbstractRecord.cpp delete mode 100644 src/Lightweight/Model/AbstractRecord.hpp delete mode 100644 src/Lightweight/Model/All.hpp delete mode 100644 src/Lightweight/Model/Associations/BelongsTo.hpp delete mode 100644 src/Lightweight/Model/Associations/HasMany.hpp delete mode 100644 src/Lightweight/Model/Associations/HasManyThrough.hpp delete mode 100644 src/Lightweight/Model/Associations/HasOne.hpp delete mode 100644 src/Lightweight/Model/Associations/HasOneThrough.hpp delete mode 100644 src/Lightweight/Model/ColumnType.hpp delete mode 100644 src/Lightweight/Model/Detail.hpp delete mode 100644 src/Lightweight/Model/Field.hpp delete mode 100644 src/Lightweight/Model/Logger.cpp delete mode 100644 src/Lightweight/Model/Logger.hpp delete mode 100644 src/Lightweight/Model/README.md delete mode 100644 src/Lightweight/Model/Record.hpp delete mode 100644 src/Lightweight/Model/RecordId.hpp delete mode 100644 src/Lightweight/Model/StringLiteral.hpp delete mode 100644 src/Lightweight/Model/Utils.hpp delete mode 100644 src/tests/ModelAssociationsTests.cpp delete mode 100644 src/tests/ModelTests.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7fc70415..78d8ee57 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -163,13 +163,17 @@ jobs: -DCMAKE_CXX_COMPILER="${CC_EXE}-${CC_VER}" \ -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS}" \ -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" \ - -DCMAKE_INSTALL_PREFIX="/usr" \ + -DCMAKE_INSTALL_PREFIX="/usr/local" \ -DPEDANTIC_COMPILER_WERROR=OFF \ --preset linux-gcc-release - name: "build" run: cmake --build --preset linux-gcc-release -- -j3 - name: "tests" run: ctest --preset linux-gcc-release + - name: "package tar.gz" + run: | + cpack --preset linux-gcc-release + cp out/package/linux-gcc-debug/Lightweight-*-Linux.tar.gz ./Lightweight-Linux-CI.tar.gz - name: "Run tests through valgrind (without model tests)" run: | valgrind \ @@ -186,7 +190,7 @@ jobs: with: name: ubuntu2404-tests path: | - LightweightTest + Lightweight-Linux-CI.tar.gz retention-days: 1 dbms_test_matrix: @@ -213,15 +217,16 @@ jobs: uses: actions/download-artifact@v4 with: name: ubuntu2404-tests - - name: "Mark unit tests as executable" - run: chmod 0755 LightweightTest + - name: "Extract package" + run: | + sudo tar -xvf Lightweight-Linux-CI.tar.gz --strip-components=1 -C /usr/local - name: "Setup ${{ matrix.database }}" id: setup run: bash ./.github/prepare-test-run.sh "${{ matrix.database }}" - name: "Dump SQL connection string" run: echo "ODBC_CONNECTION_STRING=${{ steps.setup.outputs.ODBC_CONNECTION_STRING }}" - name: "Run SQL Core tests" - run: ./LightweightTest --trace-sql --trace-model -s # --odbc-connection-string="${{ steps.setup.outputs.ODBC_CONNECTION_STRING }}" + run: LightweightTest --trace-sql --trace-model -s # --odbc-connection-string="${{ steps.setup.outputs.ODBC_CONNECTION_STRING }}" env: ODBC_CONNECTION_STRING: "${{ steps.setup.outputs.ODBC_CONNECTION_STRING }}" diff --git a/.vimspector.json b/.vimspector.json index baceeeae..97cf20ef 100644 --- a/.vimspector.json +++ b/.vimspector.json @@ -7,7 +7,6 @@ "request": "launch", "program": "${workspaceRoot}/out/build/linux-gcc-debug/src/tests/LightweightTest", "args": [ - "[Unicode]" ], "cwd": "${workspaceRoot}", "externalConsole": true, diff --git a/CMakeLists.txt b/CMakeLists.txt index 845fd491..5f2379ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,18 @@ if(DEFINED MSVC) add_compile_options(/nologo) endif() +if(NOT(CPACK_GENERATOR)) + set(CPACK_GENERATOR TGZ) +endif() +set(CPACK_PACKAGE_NAME "Lightweight") +set(CPACK_PACKAGE_VENDOR "https://github.com/christianparpart/Lightwweight/") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Lightweight SQL C++ library on top of ODBC") +set(CPACK_PACKAGE_CONTACT "Christian Parpart ") +set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md") +#set(CPACK_PACKAGE_EXECUTABLES LightweightTest "Lightweight Test Suite") +#set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE.txt") +#set(CPACK_PACKAGE_INSTALL_DIRECTORY "Lightweight SQL ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}") + add_subdirectory(src/Lightweight) add_subdirectory(src/tools) diff --git a/CMakePresets.json b/CMakePresets.json index a87b5ea9..d7f628c3 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -84,5 +84,21 @@ { "name": "linux-gcc-release", "configurePreset": "linux-gcc-release", "output": {"outputOnFailure": true}, "execution": { "noTestsAction": "error", "stopOnFailure": true } }, { "name": "linux-clang-debug", "configurePreset": "linux-clang-debug", "output": {"outputOnFailure": true}, "execution": { "noTestsAction": "error", "stopOnFailure": true } }, { "name": "linux-clang-release", "configurePreset": "linux-clang-release", "output": {"outputOnFailure": true}, "execution": { "noTestsAction": "error", "stopOnFailure": true } } + ], + "packagePresets": [ + { + "name": "linux-gcc-debug", + "packageName": "Lightweight", + "packageDirectory": "${sourceDir}/out/package/linux-gcc-debug", + "configurePreset": "linux-gcc-debug", + "generators": [ "TGZ" ] + }, + { + "name": "linux-gcc-release", + "packageName": "Lightweight", + "packageDirectory": "${sourceDir}/out/package/linux-gcc-debug", + "configurePreset": "linux-gcc-release", + "generators": [ "TGZ" ] + } ] } diff --git a/src/Lightweight/Api.hpp b/src/Lightweight/Api.hpp new file mode 100644 index 00000000..247d70e7 --- /dev/null +++ b/src/Lightweight/Api.hpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#if defined(__GNUC__) + #define LIGHTWEIGHT_NO_EXPORT __attribute__((visibility("hidden"))) + #define LIGHTWEIGHT_EXPORT __attribute__((visibility("default"))) + #define LIGHTWEIGHT_IMPORT /*!*/ + #define LIGHTWEIGHT_FORCE_INLINE __attribute__((always_inline)) +#elif defined(_MSC_VER) + #define LIGHTWEIGHT_NO_EXPORT /*!*/ + #define LIGHTWEIGHT_EXPORT __declspec(dllexport) + #define LIGHTWEIGHT_IMPORT __declspec(dllimport) + #define LIGHTWEIGHT_FORCE_INLINE __forceinline +#endif + +#if defined(LIGHTWEIGHT_SHARED) + #if defined(BUILD_LIGHTWEIGHT) + #define LIGHTWEIGHT_API LIGHTWEIGHT_EXPORT + #else + #define LIGHTWEIGHT_API LIGHTWEIGHT_IMPORT + #endif +#else + #define LIGHTWEIGHT_API /*!*/ +#endif diff --git a/src/Lightweight/CMakeLists.txt b/src/Lightweight/CMakeLists.txt index a9deceed..67c176a9 100644 --- a/src/Lightweight/CMakeLists.txt +++ b/src/Lightweight/CMakeLists.txt @@ -18,17 +18,30 @@ if(NOT WIN32) endif() set(HEADER_FILES + DataBinder/BasicStringBinder.hpp + DataBinder/Core.hpp + DataBinder/MFCStringLike.hpp + DataBinder/Primitives.hpp + DataBinder/SqlDate.hpp + DataBinder/SqlDateTime.hpp + DataBinder/SqlFixedString.hpp + DataBinder/SqlNullValue.hpp + DataBinder/SqlText.hpp + DataBinder/SqlTime.hpp + DataBinder/SqlTrimmedString.hpp + DataBinder/SqlVariant.hpp + DataBinder/StdOptional.hpp + DataBinder/StdString.hpp + DataBinder/StdStringView.hpp + DataBinder/StringLiteral.hpp DataBinder/UnicodeConverter.hpp - Model/Associations/BelongsTo.hpp - Model/Associations/HasMany.hpp - Model/Associations/HasOne.hpp - Model/ColumnType.hpp - Model/Detail.hpp - Model/Logger.hpp - Model/Record.hpp - Model/Relation.hpp - Model/StringLiteral.hpp - Model/Utils.hpp + + SqlQuery/Core.hpp + SqlQuery/Delete.hpp + SqlQuery/Insert.hpp + SqlQuery/Select.hpp + SqlQuery/Update.hpp + SqlConnectInfo.hpp SqlConnection.hpp SqlError.hpp @@ -47,8 +60,6 @@ set(HEADER_FILES set(SOURCE_FILES DataBinder/UnicodeConverter.cpp - Model/AbstractRecord.cpp - Model/Logger.cpp SqlConnectInfo.cpp SqlConnection.cpp SqlError.cpp @@ -61,11 +72,31 @@ set(SOURCE_FILES SqlTransaction.cpp ) -add_library(Lightweight STATIC) +set(LIGHTWEIGHT_BUILD_SHARED_DEFAULT OFF) +if(WIN32) + set(LIGHTWEIGHT_BUILD_SHARED_DEFAULT ON) +endif() + +option(LIGHTWEIGHT_BUILD_SHARED "Build Lightweight as a shared library" ${LIGHTWEIGHT_BUILD_SHARED_DEFAULT}) + +if(LIGHTWEIGHT_BUILD_SHARED) + set(LIGHTWEIGHT_LIBRARY_TYPE SHARED) +else() + set(LIGHTWEIGHT_LIBRARY_TYPE STATIC) +endif() + +add_library(Lightweight ${LIGHTWEIGHT_LIBRARY_TYPE}) + add_library(Lightweight::Lightweight ALIAS Lightweight) target_compile_features(Lightweight PUBLIC cxx_std_23) target_sources(Lightweight PRIVATE ${SOURCE_FILES}) -#target_sources(Lightweight PUBLIC ${HEADER_FILES}) +target_sources(Lightweight PUBLIC ${HEADER_FILES}) + +if(LIGHTWEIGHT_BUILD_SHARED) + target_compile_definitions(Lightweight PRIVATE BUILD_LIGHTWEIGHT=1) + target_compile_definitions(Lightweight PUBLIC LIGHTWEIGHT_SHARED=1) + set_target_properties(Lightweight PROPERTIES CXX_VISIBILITY_PRESET hidden) +endif() if(CLANG_TIDY_EXE) set_target_properties(Lightweight PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") @@ -90,3 +121,22 @@ else() target_link_libraries(Lightweight PUBLIC stdc++exp) # GCC >= 14 endif() endif() + +# ================================================================================================== + + +if(NOT WIN32) + include(GNUInstallDirs) +endif() + +#install(TARGETS LightweightTest DESTINATION bin) +install(TARGETS Lightweight DESTINATION lib) + +# install header files recursively +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/ + DESTINATION include/Lightweight + FILES_MATCHING PATTERN "*.hpp" +) + +include(CPack) diff --git a/src/Lightweight/DataBinder/BasicStringBinder.hpp b/src/Lightweight/DataBinder/BasicStringBinder.hpp index fddc9162..c59ce1b3 100644 --- a/src/Lightweight/DataBinder/BasicStringBinder.hpp +++ b/src/Lightweight/DataBinder/BasicStringBinder.hpp @@ -9,7 +9,7 @@ #include template -struct SqlDataBinder +struct LIGHTWEIGHT_API SqlDataBinder { using ValueType = StringType; using StringTraits = SqlCommonStringBinder; @@ -121,7 +121,7 @@ struct SqlDataBinder }; template -struct SqlDataBinder +struct LIGHTWEIGHT_API SqlDataBinder { using ValueType = StringType; using StringTraits = SqlCommonStringBinder; diff --git a/src/Lightweight/DataBinder/Core.hpp b/src/Lightweight/DataBinder/Core.hpp index 3206977e..d87521e2 100644 --- a/src/Lightweight/DataBinder/Core.hpp +++ b/src/Lightweight/DataBinder/Core.hpp @@ -6,6 +6,7 @@ #include #endif +#include "../Api.hpp" #include "../SqlTraits.hpp" #include @@ -20,7 +21,7 @@ // This is needed because the SQLBindCol() function does not allow to specify a callback function to be called // after the data has been fetched from the database. This is needed to trim strings to the correct size, for // example. -class SqlDataBinderCallback +class LIGHTWEIGHT_API SqlDataBinderCallback { public: virtual ~SqlDataBinderCallback() = default; diff --git a/src/Lightweight/DataBinder/MFCStringLike.hpp b/src/Lightweight/DataBinder/MFCStringLike.hpp index 43094e71..406e6e00 100644 --- a/src/Lightweight/DataBinder/MFCStringLike.hpp +++ b/src/Lightweight/DataBinder/MFCStringLike.hpp @@ -14,7 +14,7 @@ concept MFCStringLike = requires(T const& t) { template requires(MFCStringLike) -struct SqlDataBinder +struct LIGHTWEIGHT_API SqlDataBinder { static SQLRETURN InputParameter(SQLHSTMT stmt, SQLUSMALLINT column, T const& value) noexcept { diff --git a/src/Lightweight/DataBinder/Primitives.hpp b/src/Lightweight/DataBinder/Primitives.hpp index 4626da55..3ab93f04 100644 --- a/src/Lightweight/DataBinder/Primitives.hpp +++ b/src/Lightweight/DataBinder/Primitives.hpp @@ -6,7 +6,7 @@ // clang-format off template -struct SqlSimpleDataBinder +struct LIGHTWEIGHT_API SqlSimpleDataBinder { static constexpr inline SqlColumnType ColumnType = TheColumnType; diff --git a/src/Lightweight/DataBinder/SqlDate.hpp b/src/Lightweight/DataBinder/SqlDate.hpp index 7b17a92f..70e6deaa 100644 --- a/src/Lightweight/DataBinder/SqlDate.hpp +++ b/src/Lightweight/DataBinder/SqlDate.hpp @@ -7,7 +7,7 @@ #include // Helper struct to store a date (without time of the day) to write to or read from a database. -struct SqlDate +struct LIGHTWEIGHT_API SqlDate { SQL_DATE_STRUCT sqlValue {}; @@ -69,7 +69,7 @@ struct SqlDate }; template <> -struct SqlDataBinder +struct LIGHTWEIGHT_API SqlDataBinder { static SQLRETURN InputParameter(SQLHSTMT stmt, SQLUSMALLINT column, SqlDate const& value) noexcept { diff --git a/src/Lightweight/DataBinder/SqlDateTime.hpp b/src/Lightweight/DataBinder/SqlDateTime.hpp index c5f6284a..c2cc7e1f 100644 --- a/src/Lightweight/DataBinder/SqlDateTime.hpp +++ b/src/Lightweight/DataBinder/SqlDateTime.hpp @@ -6,7 +6,7 @@ #include -struct SqlDateTime +struct LIGHTWEIGHT_API SqlDateTime { using native_type = std::chrono::time_point; @@ -110,7 +110,7 @@ struct SqlDateTime }; template <> -struct SqlDataBinder +struct LIGHTWEIGHT_API SqlDataBinder { static SQLRETURN GetColumn(SQLHSTMT stmt, SQLUSMALLINT column, @@ -126,7 +126,7 @@ struct SqlDataBinder }; template <> -struct SqlDataBinder +struct LIGHTWEIGHT_API SqlDataBinder { static SQLRETURN InputParameter(SQLHSTMT stmt, SQLUSMALLINT column, SqlDateTime const& value) noexcept { diff --git a/src/Lightweight/DataBinder/SqlFixedString.hpp b/src/Lightweight/DataBinder/SqlFixedString.hpp index 0efb3133..ac614ddd 100644 --- a/src/Lightweight/DataBinder/SqlFixedString.hpp +++ b/src/Lightweight/DataBinder/SqlFixedString.hpp @@ -22,7 +22,7 @@ enum class SqlStringPostRetrieveOperation : uint8_t template -class SqlFixedString +class LIGHTWEIGHT_API SqlFixedString { private: T _data[N + 1] {}; @@ -192,7 +192,7 @@ template using SqlTrimmedFixedString = SqlFixedString; template -struct SqlDataBinder> +struct LIGHTWEIGHT_API SqlDataBinder> { using ValueType = SqlFixedString; using StringTraits = SqlCommonStringBinder; @@ -269,7 +269,7 @@ struct SqlDataBinder> }; template -struct SqlCommonStringBinder> +struct LIGHTWEIGHT_API SqlCommonStringBinder> { using ValueType = SqlFixedString; // clang-format off @@ -283,7 +283,7 @@ struct SqlCommonStringBinder> }; template -struct SqlDataTraits> +struct LIGHTWEIGHT_API SqlDataTraits> { static constexpr inline unsigned Size = N; static constexpr inline SqlColumnType Type = SqlColumnType::CHAR; diff --git a/src/Lightweight/DataBinder/UnicodeConverter.hpp b/src/Lightweight/DataBinder/UnicodeConverter.hpp index 53fc9a43..35043fcf 100644 --- a/src/Lightweight/DataBinder/UnicodeConverter.hpp +++ b/src/Lightweight/DataBinder/UnicodeConverter.hpp @@ -2,6 +2,8 @@ #pragma once +#include "../Api.hpp" + #include #include #include @@ -14,7 +16,7 @@ template struct UnicodeConverter; template <> -struct UnicodeConverter +struct LIGHTWEIGHT_API UnicodeConverter { // Converts a UTF-32 code point to one to four UTF-8 code units. template @@ -47,7 +49,7 @@ struct UnicodeConverter }; template <> -struct UnicodeConverter +struct LIGHTWEIGHT_API UnicodeConverter { // Converts a UTF-32 code point to one or two UTF-16 code units. template @@ -81,15 +83,15 @@ struct UnicodeConverter } // namespace detail // Converts from UTF-32 to UTF-8. -std::u8string ToUtf8(std::u32string_view u32InputString); +LIGHTWEIGHT_API std::u8string ToUtf8(std::u32string_view u32InputString); // Converts from UTF-16 to UTF-8. -std::u8string ToUtf8(std::u16string_view u16InputString); +LIGHTWEIGHT_API std::u8string ToUtf8(std::u16string_view u16InputString); // Converts from UTF-32 to UTF-16. template requires std::is_same_v || (std::is_same_v && sizeof(wchar_t) == 4) -std::u16string ToUtf16(const std::basic_string_view u32InputString) +LIGHTWEIGHT_API std::u16string ToUtf16(const std::basic_string_view u32InputString) { std::u16string u16OutputString; u16OutputString.reserve(u32InputString.size()); @@ -100,4 +102,4 @@ std::u16string ToUtf16(const std::basic_string_view u32InputString) } // Converts from UTF-8 to UTF-16. -std::u16string ToUtf16(std::u8string_view u8InputString); +LIGHTWEIGHT_API std::u16string ToUtf16(std::u8string_view u8InputString); diff --git a/src/Lightweight/Model/AbstractField.hpp b/src/Lightweight/Model/AbstractField.hpp deleted file mode 100644 index 70be9809..00000000 --- a/src/Lightweight/Model/AbstractField.hpp +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "../SqlError.hpp" -#include "ColumnType.hpp" -#include "Detail.hpp" - -#include -#include - -class SqlStatement; - -namespace Model -{ -struct SqlColumnNameView -{ - std::string_view name; -}; -} // namespace Model - -template <> -struct std::formatter: std::formatter -{ - auto format(Model::SqlColumnNameView const& column, format_context& ctx) const -> format_context::iterator - { - return std::formatter::format(std::format("\"{}\"", column.name), ctx); - } -}; - -namespace Model -{ - -enum class FieldValueRequirement : uint8_t -{ - NULLABLE, - NOT_NULL, -}; - -constexpr inline FieldValueRequirement SqlNullable = FieldValueRequirement::NULLABLE; -constexpr inline FieldValueRequirement SqlNotNullable = FieldValueRequirement::NULLABLE; - -struct AbstractRecord; - -// Base class for all fields in a table row (Record). -class AbstractField -{ - public: - AbstractField(AbstractRecord& record, - SQLSMALLINT index, - std::string_view name, - SqlColumnType type, - FieldValueRequirement requirement): - m_record { &record }, - m_index { index }, - m_name { name }, - m_type { type }, - m_requirement { requirement } - { - } - - AbstractField() = delete; - AbstractField(AbstractField&&) = default; - AbstractField& operator=(AbstractField&&) = default; - AbstractField(AbstractField const&) = delete; - AbstractField& operator=(AbstractField const&) = delete; - virtual ~AbstractField() = default; - - // Returns the syntax for the SQL constraint specification for this field, if any, otherwise an empty string. - [[nodiscard]] virtual std::string SqlConstraintSpecifier() const - { - return ""; - } - - [[nodiscard]] virtual std::string InspectValue() const = 0; - virtual void BindInputParameter(SQLSMALLINT parameterIndex, SqlStatement& stmt) const = 0; - virtual void BindOutputColumn(SqlStatement& stmt) = 0; - virtual void BindOutputColumn(SQLSMALLINT outputIndex, SqlStatement& stmt) = 0; - virtual void LoadValueFrom(AbstractField& other) = 0; - - // clang-format off - AbstractRecord& GetRecord() noexcept { return *m_record; } - [[nodiscard]] AbstractRecord const& GetRecord() const noexcept { return *m_record; } - void SetRecord(AbstractRecord& record) noexcept { m_record = &record; } - [[nodiscard]] bool IsModified() const noexcept { return m_modified; } - void SetModified(bool value) noexcept { m_modified = value; } - [[nodiscard]] SQLSMALLINT Index() const noexcept { return m_index; } - [[nodiscard]] SqlColumnNameView Name() const noexcept { return m_name; } - [[nodiscard]] SqlColumnType Type() const noexcept { return m_type; } - [[nodiscard]] bool IsNullable() const noexcept { return m_requirement == FieldValueRequirement::NULLABLE; } - [[nodiscard]] bool IsRequired() const noexcept { return m_requirement == FieldValueRequirement::NOT_NULL; } - // clang-format on - - private: - AbstractRecord* m_record; - SQLSMALLINT m_index; - SqlColumnNameView m_name; - SqlColumnType m_type; - FieldValueRequirement m_requirement; - bool m_modified = false; -}; - -} // namespace Model diff --git a/src/Lightweight/Model/AbstractRecord.cpp b/src/Lightweight/Model/AbstractRecord.cpp deleted file mode 100644 index 23d44fd7..00000000 --- a/src/Lightweight/Model/AbstractRecord.cpp +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "AbstractField.hpp" -#include "AbstractRecord.hpp" - -#include -#include -#include -#include - -namespace Model -{ - -std::string AbstractRecord::Inspect() const noexcept -{ - if (!m_data) - return "UNAVAILABLE"; - - detail::StringBuilder result; - - // Reserve enough space for the output string (This is merely a guess, but it's better than nothing) - result.output.reserve(TableName().size() + AllFields().size() * 32); - - result << "#<" << TableName() << ": id=" << Id().value; - for (auto const* field: AllFields()) - result << ", " << field->Name() << "=" << field->InspectValue(); - result << ">"; - - return *result; -} - -void AbstractRecord::SetModified(bool value) noexcept -{ - for (AbstractField* field: m_data->fields) - field->SetModified(value); -} - -bool AbstractRecord::IsModified() const noexcept -{ - return std::ranges::any_of(m_data->fields, [](AbstractField* field) { return field->IsModified(); }); -} - -AbstractRecord::FieldList AbstractRecord::GetModifiedFields() const noexcept -{ - FieldList result; - std::ranges::copy_if(m_data->fields, std::back_inserter(result), [](auto* field) { return field->IsModified(); }); - return result; -} - -void AbstractRecord::SortFieldsByIndex() noexcept -{ - std::sort(m_data->fields.begin(), m_data->fields.end(), [](auto a, auto b) { return a->Index() < b->Index(); }); -} - -std::vector AbstractRecord::AllFieldNames() const -{ - std::vector columnNames; - columnNames.resize(1 + m_data->fields.size()); - columnNames[0] = PrimaryKeyName(); - for (auto const* field: AllFields()) - columnNames[field->Index() - 1] = field->Name().name; - return columnNames; -} - -} // namespace Model diff --git a/src/Lightweight/Model/AbstractRecord.hpp b/src/Lightweight/Model/AbstractRecord.hpp deleted file mode 100644 index d786ebbc..00000000 --- a/src/Lightweight/Model/AbstractRecord.hpp +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "RecordId.hpp" - -#include -#include -#include -#include - -namespace Model -{ - -class AbstractField; - -struct SqlColumnIndex -{ - size_t value; -}; - -// Base class for every SqlModel. -struct AbstractRecord -{ - public: - AbstractRecord(std::string_view tableName, std::string_view primaryKey, RecordId id): - m_data { std::make_unique(tableName, primaryKey, id) } - { - } - - AbstractRecord() = delete; - AbstractRecord(AbstractRecord const&) = delete; - AbstractRecord(AbstractRecord&& other) noexcept: - AbstractRecord { other.m_data->tableName, other.m_data->primaryKeyName, other.m_data->id } - { - } - - AbstractRecord& operator=(AbstractRecord const&) = delete; - AbstractRecord& operator=(AbstractRecord&&) = delete; - virtual ~AbstractRecord() = default; - - // Returns a human readable string representation of this model. - [[nodiscard]] std::string Inspect() const noexcept; - - // clang-format off - [[nodiscard]] std::string_view TableName() const noexcept { return m_data->tableName; } // TODO: make this statically accessible from Record<> as well - [[nodiscard]] std::string_view PrimaryKeyName() const noexcept { return m_data->primaryKeyName; } // TODO: make this statically accessible from Record<> as well - [[nodiscard]] RecordId Id() const noexcept { return m_data->id; } - - RecordId& MutableId() noexcept { return m_data->id; } - - void RegisterField(AbstractField& field) noexcept { m_data->fields.push_back(&field); } - - void UnregisterField(AbstractField const& field) noexcept - { - if (!m_data) - return; - // remove field by rotating it to the end and then popping it - auto it = std::ranges::find(m_data->fields, &field); - if (it != m_data->fields.end()) - { - std::rotate(it, std::next(it), m_data->fields.end()); - m_data->fields.pop_back(); - } - } - - [[nodiscard]] AbstractField const& GetField(SqlColumnIndex index) const noexcept { return *m_data->fields[index.value]; } - AbstractField& GetField(SqlColumnIndex index) noexcept { return *m_data->fields[index.value]; } - // clang-format on - - void SetModified(bool value) noexcept; - - [[nodiscard]] bool IsModified() const noexcept; - - void SortFieldsByIndex() noexcept; - - using FieldList = std::vector; - - [[nodiscard]] FieldList GetModifiedFields() const noexcept; - - [[nodiscard]] FieldList const& AllFields() const noexcept - { - return m_data->fields; - } - - [[nodiscard]] std::vector AllFieldNames() const; - - protected: - struct Data - { - std::string_view tableName; // Should be const, but we want to allow move semantics - std::string_view primaryKeyName; // Should be const, but we want to allow move semantics - RecordId id {}; - - bool modified = false; - FieldList fields; - }; - std::unique_ptr m_data; -}; - -} // namespace Model - -template - requires std::derived_from -struct std::formatter: std::formatter -{ - auto format(D const& record, format_context& ctx) const -> format_context::iterator - { - return formatter::format(record.Inspect(), ctx); - } -}; diff --git a/src/Lightweight/Model/All.hpp b/src/Lightweight/Model/All.hpp deleted file mode 100644 index c99551d7..00000000 --- a/src/Lightweight/Model/All.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include "Associations/BelongsTo.hpp" -#include "Associations/HasMany.hpp" -#include "Associations/HasManyThrough.hpp" -#include "Associations/HasOne.hpp" -#include "Associations/HasOneThrough.hpp" -#include "Field.hpp" -#include "Logger.hpp" -#include "Record.hpp" -#include "RecordId.hpp" -#include "Utils.hpp" diff --git a/src/Lightweight/Model/Associations/BelongsTo.hpp b/src/Lightweight/Model/Associations/BelongsTo.hpp deleted file mode 100644 index 3123c9c6..00000000 --- a/src/Lightweight/Model/Associations/BelongsTo.hpp +++ /dev/null @@ -1,283 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "../../SqlStatement.hpp" -#include "../AbstractField.hpp" -#include "../AbstractRecord.hpp" -#include "../ColumnType.hpp" -#include "../RecordId.hpp" -#include "../StringLiteral.hpp" - -#include -#include -#include - -namespace Model -{ - -template -struct Record; - -// Represents a column in a table that is a foreign key to another table. -template -class BelongsTo final: public AbstractField -{ - public: - constexpr static inline SQLSMALLINT ColumnIndex { TheColumnIndex }; - constexpr static inline std::string_view ColumnName { TheForeignKeyName.value }; - - explicit BelongsTo(AbstractRecord& record); - BelongsTo(BelongsTo const& other); - explicit BelongsTo(AbstractRecord& record, BelongsTo&& other); - BelongsTo& operator=(RecordId modelId); - BelongsTo& operator=(OtherRecord const& model); - ~BelongsTo() override = default; - - OtherRecord* operator->(); - OtherRecord& operator*(); - - [[nodiscard]] std::string SqlConstraintSpecifier() const override; - - [[nodiscard]] std::string InspectValue() const override; - void BindInputParameter(SQLSMALLINT parameterIndex, SqlStatement& stmt) const override; - void BindOutputColumn(SqlStatement& stmt) override; - void BindOutputColumn(SQLSMALLINT index, SqlStatement& stmt) override; - void LoadValueFrom(AbstractField& other) override; - - auto operator<=>(BelongsTo const& other) const noexcept; - - template - bool operator==(BelongsTo const& other) const noexcept; - - template - bool operator!=(BelongsTo const& other) const noexcept; - - void Load() noexcept; - - private: - void RequireLoaded(); - - RecordId m_value {}; - std::shared_ptr m_otherRecord; - - // We decided to use shared_ptr here, because we do not want to require to know the size of the OtherRecord - // at declaration time. -}; - -// {{{ BelongsTo<> implementation - -template -BelongsTo::BelongsTo(AbstractRecord& record): - AbstractField { - record, TheColumnIndex, TheForeignKeyName.value, ColumnTypeOf, TheRequirement, - } -{ - record.RegisterField(*this); -} - -template -BelongsTo::BelongsTo(BelongsTo const& other): - AbstractField { - const_cast(other).GetRecord(), - TheColumnIndex, - TheForeignKeyName.value, - ColumnTypeOf, - TheRequirement, - }, - m_value { other.m_value } -{ - GetRecord().RegisterField(*this); -} - -template -BelongsTo::BelongsTo(AbstractRecord& record, - BelongsTo&& other): - AbstractField { std::move(static_cast(other)) }, - m_value { std::move(other.m_value) } -{ - record.RegisterField(*this); -} - -template -BelongsTo& -BelongsTo::operator=(RecordId modelId) -{ - SetModified(true); - m_value = modelId; - return *this; -} - -template -BelongsTo& -BelongsTo::operator=(OtherRecord const& model) -{ - SetModified(true); - m_value = model.Id(); - return *this; -} - -template -inline OtherRecord* BelongsTo::operator->() -{ - RequireLoaded(); - return &*m_otherRecord; -} - -template -inline OtherRecord& BelongsTo::operator*() -{ - RequireLoaded(); - return *m_otherRecord; -} - -template -std::string BelongsTo::SqlConstraintSpecifier() const -{ - auto const otherRecord = OtherRecord {}; - // TODO: Move the syntax into SqlTraits, as a parametrized member function - return std::format("FOREIGN KEY ({}) REFERENCES {}({}) ON DELETE CASCADE", - ColumnName, - otherRecord.TableName(), - otherRecord.PrimaryKeyName()); -} - -template -inline std::string BelongsTo::InspectValue() const -{ - return std::to_string(m_value.value); -} - -template -inline void BelongsTo::BindInputParameter( - SQLSMALLINT parameterIndex, SqlStatement& stmt) const -{ - return stmt.BindInputParameter(parameterIndex, m_value.value); -} - -template -inline void BelongsTo::BindOutputColumn( - SqlStatement& stmt) -{ - return stmt.BindOutputColumn(TheColumnIndex, &m_value.value); -} - -template -inline void BelongsTo::BindOutputColumn( - SQLSMALLINT outputIndex, SqlStatement& stmt) -{ - return stmt.BindOutputColumn(outputIndex, &m_value.value); -} - -template -void BelongsTo::LoadValueFrom(AbstractField& other) -{ - assert(Type() == other.Type()); - m_value = std::move(static_cast(other).m_value); - m_otherRecord.reset(); -} - -template -inline auto BelongsTo::operator<=>( - BelongsTo const& other) const noexcept -{ - return m_value <=> other.m_value; -} - -template -template -inline bool BelongsTo::operator==( - BelongsTo const& other) const noexcept -{ - return m_value == other.m_value; -} - -template -template -inline bool BelongsTo::operator!=( - BelongsTo const& other) const noexcept -{ - return m_value == other.m_value; -} - -template -void BelongsTo::Load() noexcept -{ - if (m_otherRecord) - return; - - auto otherRecord = OtherRecord::Find(m_value); - if (otherRecord.has_value()) - m_otherRecord = std::make_shared(std::move(otherRecord.value())); -} - -template -inline void BelongsTo::RequireLoaded() -{ - if (!m_otherRecord) - { - Load(); - if (!m_otherRecord) - throw std::runtime_error("BelongsTo::RequireLoaded(): Record not found"); - } -} - -// }}} - -} // namespace Model diff --git a/src/Lightweight/Model/Associations/HasMany.hpp b/src/Lightweight/Model/Associations/HasMany.hpp deleted file mode 100644 index 503e8e35..00000000 --- a/src/Lightweight/Model/Associations/HasMany.hpp +++ /dev/null @@ -1,133 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "../../SqlError.hpp" -#include "../AbstractRecord.hpp" -#include "../Logger.hpp" -#include "../StringLiteral.hpp" - -#include - -namespace Model -{ - -template -class HasMany -{ - public: - explicit HasMany(AbstractRecord& parent); - HasMany(AbstractRecord& record, HasMany&& other) noexcept; - - [[nodiscard]] bool IsEmpty() const; - [[nodiscard]] size_t Count() const; - - std::vector& All(); - - OtherRecord& At(size_t index); - OtherRecord& operator[](size_t index); - - [[nodiscard]] bool IsLoaded() const noexcept; - void Load(); - void Reload(); - - private: - bool RequireLoaded(); - - bool m_loaded = false; - AbstractRecord* m_record; - std::vector m_models; -}; - -// {{{ HasMany<> implementation - -template -HasMany::HasMany(AbstractRecord& parent): - m_record { &parent } -{ -} - -template -HasMany::HasMany(AbstractRecord& record, HasMany&& other) noexcept: - m_loaded { other.m_loaded }, - m_record { &record }, - m_models { std::move(other.m_models) } -{ -} - -template -void HasMany::Load() -{ - if (m_loaded) - return; - - m_models = OtherRecord::Where(*ForeignKeyName, *m_record->Id()).All(); - m_loaded = true; -} - -template -void HasMany::Reload() -{ - m_loaded = false; - m_models.clear(); - return Load(); -} - -template -bool HasMany::IsEmpty() const -{ - return Count() == 0; -} - -template -size_t HasMany::Count() const -{ - if (m_loaded) - return m_models.size(); - - SqlStatement stmt; - - auto const sqlQueryString = std::format( - "SELECT COUNT(*) FROM {} WHERE {} = {}", OtherRecord().TableName(), *ForeignKeyName, *m_record->Id()); - auto const scopedModelSqlLogger = detail::SqlScopedModelQueryLogger(sqlQueryString, {}); - return stmt.ExecuteDirectSingle(sqlQueryString).value(); -} - -template -inline std::vector& HasMany::All() -{ - RequireLoaded(); - return m_models; -} - -template -inline OtherRecord& HasMany::At(size_t index) -{ - RequireLoaded(); - return m_models.at(index); -} - -template -inline OtherRecord& HasMany::operator[](size_t index) -{ - RequireLoaded(); - return m_models[index]; -} - -template -inline bool HasMany::IsLoaded() const noexcept -{ - return m_loaded; -} - -template -inline bool HasMany::RequireLoaded() -{ - if (!m_loaded) - Load(); - - return m_loaded; -} - -// }}} - -} // namespace Model diff --git a/src/Lightweight/Model/Associations/HasManyThrough.hpp b/src/Lightweight/Model/Associations/HasManyThrough.hpp deleted file mode 100644 index 8e314704..00000000 --- a/src/Lightweight/Model/Associations/HasManyThrough.hpp +++ /dev/null @@ -1,191 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "../../SqlQuery.hpp" -#include "../AbstractRecord.hpp" -#include "../StringLiteral.hpp" - -namespace Model -{ - -template -class HasManyThrough -{ - public: - explicit HasManyThrough(AbstractRecord& record); - explicit HasManyThrough(AbstractRecord& record, HasManyThrough&& other) noexcept; - - [[nodiscard]] bool IsEmpty() const noexcept; - [[nodiscard]] size_t Count() const; - - std::vector& All(); - - template - void Each(Callback&& callback); - - TargetRecord& At(size_t index); - TargetRecord const& At(size_t index) const; - TargetRecord& operator[](size_t index); - TargetRecord const& operator[](size_t index) const; - - [[nodiscard]] bool IsLoaded() const noexcept; - void Load(); - void Reload(); - - private: - void RequireLoaded() const; - - AbstractRecord* m_record; - bool m_loaded = false; - std::vector m_models; -}; - -// {{{ inlines - -template -HasManyThrough::HasManyThrough(AbstractRecord& record): - m_record { &record } -{ -} - -template -HasManyThrough::HasManyThrough(AbstractRecord& record, - HasManyThrough&& other) noexcept: - m_record { &record }, - m_loaded { other.m_loaded }, - m_models { std::move(other.m_models) } -{ -} - -template -bool HasManyThrough::IsEmpty() const noexcept -{ - return Count() == 0; -} - -template -size_t HasManyThrough::Count() const -{ - if (IsLoaded()) - return m_models.size(); - - auto const targetRecord = TargetRecord(); - auto const throughRecordMeta = ThroughRecord(); // TODO: eliminate instances, allowing direct access to meta info - - return TargetRecord::Join(throughRecordMeta.TableName(), - LeftKeyName.value, - SqlQualifiedTableColumnName(targetRecord.TableName(), targetRecord.PrimaryKeyName())) - .Join(m_record->TableName(), - m_record->PrimaryKeyName(), - SqlQualifiedTableColumnName(throughRecordMeta.TableName(), RightKeyName.value)) - .Where(SqlQualifiedTableColumnName(m_record->TableName(), m_record->PrimaryKeyName()), *m_record->Id()) - .Count(); -} - -template -inline std::vector& HasManyThrough::All() -{ - RequireLoaded(); - return m_models; -} - -template -TargetRecord& HasManyThrough::At(size_t index) -{ - RequireLoaded(); - return m_models.at(index); -} - -template -TargetRecord const& HasManyThrough::At(size_t index) const -{ - RequireLoaded(); - return m_models.at(index); -} - -template -TargetRecord& HasManyThrough::operator[](size_t index) -{ - RequireLoaded(); - return m_models[index]; -} - -template -TargetRecord const& HasManyThrough::operator[]( - size_t index) const -{ - RequireLoaded(); - return m_models[index]; -} - -template -template -inline void HasManyThrough::Each(Callback&& callback) -{ - if (IsLoaded()) - { - for (auto& model: m_models) - callback(model); - } - else - { - auto const targetRecord = TargetRecord(); - auto const throughRecordMeta = ThroughRecord(); - - TargetRecord::Join(throughRecordMeta.TableName(), - LeftKeyName.value, - SqlQualifiedTableColumnName(targetRecord.TableName(), targetRecord.PrimaryKeyName())) - .Join(m_record->TableName(), - m_record->PrimaryKeyName(), - SqlQualifiedTableColumnName(throughRecordMeta.TableName(), RightKeyName.value)) - .Where(SqlQualifiedTableColumnName(m_record->TableName(), m_record->PrimaryKeyName()), *m_record->Id()) - .Each(callback); - } -} - -template -inline bool HasManyThrough::IsLoaded() const noexcept -{ - return m_loaded; -} - -template -void HasManyThrough::Load() -{ - if (m_loaded) - return; - - auto const targetRecord = TargetRecord(); - auto const throughRecordMeta = ThroughRecord(); - - m_models = - TargetRecord::Join(throughRecordMeta.TableName(), - LeftKeyName.value, - SqlQualifiedTableColumnName(targetRecord.TableName(), targetRecord.PrimaryKeyName())) - .Join(m_record->TableName(), - m_record->PrimaryKeyName(), - SqlQualifiedTableColumnName(throughRecordMeta.TableName(), RightKeyName.value)) - .Where(SqlQualifiedTableColumnName(m_record->TableName(), m_record->PrimaryKeyName()), *m_record->Id()) - .All(); - - m_loaded = true; -} - -template -void HasManyThrough::Reload() -{ - m_loaded = false; - m_models.clear(); - Load(); -} - -template -void HasManyThrough::RequireLoaded() const -{ - if (!IsLoaded()) - const_cast(this)->Load(); -} - -// }}} - -} // namespace Model diff --git a/src/Lightweight/Model/Associations/HasOne.hpp b/src/Lightweight/Model/Associations/HasOne.hpp deleted file mode 100644 index 302eaf61..00000000 --- a/src/Lightweight/Model/Associations/HasOne.hpp +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "../../SqlError.hpp" -#include "../AbstractRecord.hpp" -#include "../StringLiteral.hpp" - -#include - -namespace Model -{ - -// Represents a column in a another table that refers to this record. -template -class HasOne final -{ - public: - explicit HasOne(AbstractRecord& record); - explicit HasOne(AbstractRecord& record, HasOne&& other); - - OtherRecord& operator*(); - OtherRecord* operator->(); - [[nodiscard]] bool IsLoaded() const; - - bool Load(); - void Reload(); - - private: - void RequireLoaded(); - - AbstractRecord* m_record; - std::shared_ptr m_otherRecord; - - // We decided to use shared_ptr here, because we do not want to require to know the size of the OtherRecord - // at declaration time. -}; - -// {{{ HasOne<> implementation - -template -HasOne::HasOne(AbstractRecord& record): - m_record { &record } -{ -} - -template -HasOne::HasOne(AbstractRecord& record, HasOne&& other): - m_record { &record }, - m_otherRecord { std::move(other.m_otherRecord) } -{ -} - -template -OtherRecord& HasOne::operator*() -{ - RequireLoaded(); - return *m_otherRecord; -} - -template -OtherRecord* HasOne::operator->() -{ - RequireLoaded(); - return &*m_otherRecord; -} - -template -bool HasOne::IsLoaded() const -{ - return m_otherRecord.get() != nullptr; -} - -template -bool HasOne::Load() -{ - if (m_otherRecord) - return true; - - auto foundRecord = OtherRecord::FindBy(TheForeignKeyName.value, m_record->Id()); - if (!foundRecord) - return false; - - m_otherRecord = std::make_shared(std::move(foundRecord.value())); - return true; -} - -template -void HasOne::Reload() -{ - m_otherRecord.reset(); - Load(); -} - -template -void HasOne::RequireLoaded() -{ - if (!m_otherRecord) - Load(); -} - -// }}} - -} // namespace Model diff --git a/src/Lightweight/Model/Associations/HasOneThrough.hpp b/src/Lightweight/Model/Associations/HasOneThrough.hpp deleted file mode 100644 index eac754a8..00000000 --- a/src/Lightweight/Model/Associations/HasOneThrough.hpp +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "../../SqlQuery.hpp" -#include "../AbstractRecord.hpp" -#include "../StringLiteral.hpp" - -#include - -namespace Model -{ - -template -class HasOneThrough -{ - public: - explicit HasOneThrough(AbstractRecord& record); - HasOneThrough(AbstractRecord& record, HasOneThrough&& other) noexcept; - - OtherRecord& operator*(); - OtherRecord* operator->(); - - [[nodiscard]] bool IsLoaded() const noexcept; - void Load(); - void Reload(); - - private: - AbstractRecord* m_record; - std::shared_ptr m_otherRecord; -}; - -// {{{ inlines - -template -HasOneThrough::HasOneThrough(AbstractRecord& record): - m_record { &record } -{ -} - -template -HasOneThrough::HasOneThrough(AbstractRecord& record, - HasOneThrough&& other) noexcept: - m_record { &record }, - m_otherRecord { std::move(other.m_otherRecord) } -{ -} - -template -OtherRecord& HasOneThrough::operator*() -{ - if (!m_otherRecord) - Load(); - - return *m_otherRecord; -} - -template -OtherRecord* HasOneThrough::operator->() -{ - if (!m_otherRecord) - Load(); - - return &*m_otherRecord; -} - -template -bool HasOneThrough::IsLoaded() const noexcept -{ - return m_otherRecord.get(); -} - -template -void HasOneThrough::Load() -{ - if (IsLoaded()) - return; - - auto result = - OtherRecord::template Join() - .Where(SqlQualifiedTableColumnName(OtherRecord().TableName(), ForeignKeyName.value), m_record->Id().value) - .First(); - - if (!result.has_value()) - { - SqlLogger::GetLogger().OnWarning(std::format("No data found on table {} for {} = {}", - OtherRecord().TableName(), - ForeignKeyName.value, - m_record->Id().value)); - return; - } - - m_otherRecord = std::make_shared(std::move(result.value())); -} - -template -void HasOneThrough::Reload() -{ - m_otherRecord.reset(); - return Load(); -} - -// }}} - -} // namespace Model diff --git a/src/Lightweight/Model/ColumnType.hpp b/src/Lightweight/Model/ColumnType.hpp deleted file mode 100644 index d17550d5..00000000 --- a/src/Lightweight/Model/ColumnType.hpp +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "../SqlDataBinder.hpp" -#include "../SqlTraits.hpp" -#include "RecordId.hpp" - -#include -#include - -namespace Model -{ - -namespace detail -{ - template - struct ColumnTypeOf; - - // clang-format off -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::CHAR; }; -template struct ColumnTypeOf> { static constexpr SqlColumnType value = SqlColumnType::STRING; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::STRING; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::STRING; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::TEXT; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::BOOLEAN; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::INTEGER; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::INTEGER; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::INTEGER; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::INTEGER; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::INTEGER; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::INTEGER; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::REAL; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::REAL; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::DATE; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::TIME; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::DATETIME; }; -template <> struct ColumnTypeOf { static constexpr SqlColumnType value = SqlColumnType::INTEGER; }; - // clang-format on -} // namespace detail - -template -constexpr SqlColumnType ColumnTypeOf = detail::ColumnTypeOf::value; - -} // namespace Model diff --git a/src/Lightweight/Model/Detail.hpp b/src/Lightweight/Model/Detail.hpp deleted file mode 100644 index 22164971..00000000 --- a/src/Lightweight/Model/Detail.hpp +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include -#include - -namespace Model::detail -{ - -struct StringBuilder -{ - std::string output; - - std::string operator*() const& noexcept - { - return output; - } - - std::string operator*() && noexcept - { - return std::move(output); - } - - [[nodiscard]] bool empty() const noexcept - { - return output.empty(); - } - - template - StringBuilder& operator<<(T&& value) - { - if constexpr (std::is_same_v || std::is_same_v - || std::is_same_v) - output += value; - else - output += std::format("{}", std::forward(value)); - return *this; - } -}; - -} // namespace Model::detail diff --git a/src/Lightweight/Model/Field.hpp b/src/Lightweight/Model/Field.hpp deleted file mode 100644 index 014ec531..00000000 --- a/src/Lightweight/Model/Field.hpp +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "../SqlStatement.hpp" -#include "AbstractField.hpp" -#include "AbstractRecord.hpp" -#include "ColumnType.hpp" -#include "StringLiteral.hpp" - -#include - -namespace Model -{ - -template -struct Record; - -// Represents a single column in a table. -// -// The column name, index, and type are known at compile time. -// If either name or index are not known at compile time, leave them at their default values, -// but at least one of them msut be known. -template -class Field: public AbstractField -{ - public: - explicit Field(AbstractRecord& record): - AbstractField { - record, TheTableColumnIndex, TheColumnName.value, ColumnTypeOf, TheRequirement, - } - { - record.RegisterField(*this); - } - - Field(Field const& other): - AbstractField { - const_cast(other).GetRecord(), - TheTableColumnIndex, - TheColumnName.value, - ColumnTypeOf, - TheRequirement, - }, - m_value { other.m_value } - { - GetRecord().RegisterField(*this); - } - - Field(AbstractRecord& record, Field&& other): - AbstractField { std::move(static_cast(other)) }, - m_value { std::move(other.m_value) } - { - record.RegisterField(*this); - } - - Field() = delete; - Field(Field&& other) = delete; - Field& operator=(Field&& other) = delete; - Field& operator=(Field const& other) = delete; - ~Field() override = default; - - // clang-format off - - template - auto operator<=>(Field const& other) const noexcept { return m_value <=> other.m_value; } - - // We also define the equality and inequality operators explicitly, because <=> from above does not seem to work in MSVC VS 2022. - template - auto operator==(Field const& other) const noexcept { return m_value == other.m_value; } - - template - auto operator!=(Field const& other) const noexcept { return m_value != other.m_value; } - - bool operator==(T const& other) const noexcept { return m_value == other; } - bool operator!=(T const& other) const noexcept { return m_value != other; } - - T const& Value() const noexcept { return m_value; } - void SetData(T&& value) { SetModified(true); m_value = std::move(value); } - void SetNull() { SetModified(true); m_value = T {}; } - - Field& operator=(T&& value) noexcept; - - T& operator*() noexcept { return m_value; } - T const& operator*() const noexcept { return m_value; } - - // clang-format on - - [[nodiscard]] std::string InspectValue() const override; - void BindInputParameter(SQLSMALLINT parameterIndex, SqlStatement& stmt) const override; - void BindOutputColumn(SqlStatement& stmt) override; - void BindOutputColumn(SQLSMALLINT index, SqlStatement& stmt) override; - - void LoadValueFrom(AbstractField& other) override - { - assert(Type() == other.Type()); - m_value = std::move(static_cast(other).m_value); - } - - private: - T m_value {}; -}; - -// {{{ Field<> implementation - -template -Field& Field::operator=(T&& value) noexcept -{ - SetModified(true); - m_value = std::move(value); - return *this; -} - -template -std::string Field::InspectValue() const -{ - if constexpr (std::is_same_v) - { - std::stringstream result; - result << std::quoted(m_value, '\''); - return result.str(); - } - else if constexpr (std::is_same_v) - { - std::stringstream result; - result << std::quoted(m_value.value, '\''); - return result.str(); - } - else if constexpr (std::is_same_v) - { - std::stringstream result; - result << std::quoted(m_value.value, '\''); - return result.str(); - } - else if constexpr (std::is_same_v) - return std::format("\'{}\'", m_value.value); - else if constexpr (std::is_same_v) - return std::format("\'{}\'", m_value.value); - else if constexpr (std::is_same_v) - return std::format("\'{}\'", m_value.value()); - else - return std::format("{}", m_value); -} - -template -void Field::BindInputParameter(SQLSMALLINT parameterIndex, - SqlStatement& stmt) const -{ - return stmt.BindInputParameter(parameterIndex, m_value); -} - -template -void Field::BindOutputColumn(SqlStatement& stmt) -{ - stmt.BindOutputColumn(TheTableColumnIndex, &m_value); -} - -template -void Field::BindOutputColumn(SQLSMALLINT outputIndex, - SqlStatement& stmt) -{ - stmt.BindOutputColumn(outputIndex, &m_value); -} - -// }}} - -} // namespace Model diff --git a/src/Lightweight/Model/Logger.cpp b/src/Lightweight/Model/Logger.cpp deleted file mode 100644 index 68105f02..00000000 --- a/src/Lightweight/Model/Logger.cpp +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "Detail.hpp" -#include "Field.hpp" -#include "Logger.hpp" - -#include -#include -#include -#include - -namespace Model -{ - -class StandardQueryLogger: public QueryLogger -{ - private: - std::chrono::steady_clock::time_point m_startedAt; - std::string m_query; - std::vector m_output; - size_t m_rowCount {}; - - public: - void QueryStart(std::string_view query, std::vector const& output) override - { - m_startedAt = std::chrono::steady_clock::now(); - m_query = query; - m_output = output; - m_rowCount = 0; - } - - void QueryNextRow(AbstractRecord const& /*record*/) override - { - ++m_rowCount; - } - - void QueryEnd() override - { - auto const stoppedAt = std::chrono::steady_clock::now(); - auto const duration = std::chrono::duration_cast(stoppedAt - m_startedAt); - auto const seconds = std::chrono::duration_cast(duration); - auto const microseconds = std::chrono::duration_cast(duration - seconds); - auto const durationStr = std::format("{}.{:06}", seconds.count(), microseconds.count()); - - auto const rowCountStr = m_rowCount == 0 ? "" - : m_rowCount == 1 ? " [1 row]" - : std::format(" [{} rows]", m_rowCount); - - if (m_output.empty()) - { - std::println("[{}]{} {}", durationStr, rowCountStr, m_query); - return; - } - - detail::StringBuilder output; - - for (AbstractField const* field: m_output) - { - if (!output.empty()) - output << ", "; - output << field->Name().name << '=' << field->InspectValue(); - } - - std::println("[{}]{} {} WITH [{}]", durationStr, rowCountStr, m_query, *output); - } -}; - -static QueryLogger theNullLogger; - -QueryLogger* QueryLogger::NullLogger() noexcept -{ - return &theNullLogger; -} - -static StandardQueryLogger theStandardLogger; - -QueryLogger* QueryLogger::StandardLogger() noexcept -{ - return &theStandardLogger; -} - -QueryLogger* QueryLogger::m_instance = QueryLogger::NullLogger(); - -} // namespace Model diff --git a/src/Lightweight/Model/Logger.hpp b/src/Lightweight/Model/Logger.hpp deleted file mode 100644 index b3c20faa..00000000 --- a/src/Lightweight/Model/Logger.hpp +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -namespace Model -{ - -class AbstractField; -struct AbstractRecord; - -class QueryLogger -{ - public: - virtual ~QueryLogger() = default; - - using FieldList = std::vector; - - virtual void QueryStart(std::string_view /*query*/, FieldList const& /*output*/) {} - virtual void QueryNextRow(AbstractRecord const& /*model*/) {} - virtual void QueryEnd() {} - - static void Set(QueryLogger* next) noexcept - { - m_instance = next; - } - - static QueryLogger& Get() noexcept - { - return *m_instance; - } - - static QueryLogger* NullLogger() noexcept; - static QueryLogger* StandardLogger() noexcept; - - private: - static QueryLogger* m_instance; -}; - -namespace detail -{ - - struct SqlScopedModelQueryLogger - { - using FieldList = QueryLogger::FieldList; - - SqlScopedModelQueryLogger(std::string_view query, FieldList const& output) - { - QueryLogger::Get().QueryStart(query, output); - } - - SqlScopedModelQueryLogger& operator+=(AbstractRecord const& model) - { - QueryLogger::Get().QueryNextRow(model); - return *this; - } - - ~SqlScopedModelQueryLogger() - { - QueryLogger::Get().QueryEnd(); - } - }; - -} // namespace detail - -} // namespace Model diff --git a/src/Lightweight/Model/README.md b/src/Lightweight/Model/README.md deleted file mode 100644 index da7b0860..00000000 --- a/src/Lightweight/Model/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Lightweight Model API - -The Lightweight Model API is a lightweight ORM that allows you to interact with your database using a simple and intuitive API. -It is designed to be easy to use while being as efficient as possible. - -This API was inspired by **Active Record** pattern and API from Ruby on Rails. - -## Features - -- **Simple & Intuitive API**: The API is designed to be as simple as possible. -- **Efficient**: The API is designed to be as efficient as possible. - -## Example - -```cpp -#include -#include - -struct Book; - -struct Author: Model::Record -{ - Model::Field name; - Model::HasMany books; -}; - -struct Book: Model::Record -{ - Model::Field title; - Model::Field isbn; - Model::BelongsTo author; -}; - -void demo() -{ - Model::CreateTables(); - - Author author; - author.name = "Bjarne Stroustrup"; - author.Save().or_else(std::abort); - - Book book; - book.title = "The C++ Programming Language"; - book.isbn = "978-0-321-56384-2"; - book.author = author; - book.Save().or_else(std::abort); - - auto books = Book::All().or_else(std::abort); - for (auto book: books) - std::println("{}", book); - - std::println("{} has {} books", author.name, author.books.Count()); - - author.Destroy(); - book.Destroy(); -} -``` - -## TODO: Open Refactors - -- [ ] Consider reintroducing `Model::Record` -- [x] Support (join) Associations (https://guides.rubyonrails.org/association_basics.html) (e.g. `Author::All().Join().Where() == 42; v.As() == "42";`) - -## Open TODOs - -- [x] [Lightweight] Add custom type `SqlText` for `TEXT` fields -- [x] Add std::formatter for `Record` -- [x] Add test for `BelongsTo<>` (including eager & lazy-loading check) -- [x] Add test for `HasOne<>` (including eager & lazy-loading check) -- [x] Add test for `HasMany<>` (including eager & lazy-loading check) -- [x] Differenciate between VARCHAR (`std::string`) and TEXT (maybe `SqlText`?) -- [x] Make logging more useful, adding payload data -- [x] remove debug prints -- [x] add proper trace logging -- [x] Add `HasManyThrough<>` association -- [x] Add `HasOneThrough<>` association -- [ ] Add `HasAndBelongsToMany<>` association -- [ ] Add SQL query caching -- [x] Add lazy loading constraints (e.g. something similar to `Book::All().Where`) -- [ ] Add ability to configure PK's auto-increment to be server-side (default) vs client-side. this must be a compile-time option (via template parameter) diff --git a/src/Lightweight/Model/Record.hpp b/src/Lightweight/Model/Record.hpp deleted file mode 100644 index d8438f48..00000000 --- a/src/Lightweight/Model/Record.hpp +++ /dev/null @@ -1,671 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "../SqlQuery.hpp" -#include "AbstractRecord.hpp" -#include "Detail.hpp" -#include "Field.hpp" -#include "Logger.hpp" - -#include -#include -#include -#include -#include - -namespace Model -{ - -enum class SqlWhereOperator : uint8_t -{ - EQUAL, - NOT_EQUAL, - LESS_THAN, - LESS_OR_EQUAL, - GREATER_THAN, - GREATER_OR_EQUAL -}; - -constexpr std::string_view sqlOperatorString(SqlWhereOperator value) noexcept -{ - using namespace std::string_view_literals; - - auto constexpr mappings = std::array { - "="sv, "!="sv, "<"sv, "<="sv, ">"sv, ">="sv, - }; - - std::string_view result; - - if (std::to_underlying(value) < mappings.size()) - result = mappings[std::to_underlying(value)]; - - return result; -} - -// API to load records with more complex constraints. -// -// @see Record -// @see Record::Join() -template -class RecordQueryBuilder -{ - private: - explicit RecordQueryBuilder(SqlSelectQueryBuilder queryBuilder): - m_queryBuilder { std::move(queryBuilder) } - { - } - - public: - explicit RecordQueryBuilder(): - m_queryBuilder { - SqlQueryBuilder(SqlConnection().QueryFormatter()).FromTable(std::string(TargetModel().TableName())).Select() - } - { - } - - template - [[nodiscard]] RecordQueryBuilder Join() && - { - auto const joinModel = JoinModel(); - (void) m_queryBuilder.InnerJoin(joinModel.TableName(), joinModel.PrimaryKeyName(), foreignKeyColumn.value); - return RecordQueryBuilder { std::move(m_queryBuilder) }; - } - - [[nodiscard]] RecordQueryBuilder Join(std::string_view joinTableName, - std::string_view joinTablePrimaryKey, - SqlQualifiedTableColumnName onComparisonColumn) && - { - (void) m_queryBuilder.InnerJoin(joinTableName, joinTablePrimaryKey, onComparisonColumn); - return RecordQueryBuilder { std::move(m_queryBuilder) }; - } - - [[nodiscard]] RecordQueryBuilder Join(std::string_view joinTableName, - std::string_view joinTablePrimaryKey, - std::string_view onComparisonColumn) && - { - (void) m_queryBuilder.InnerJoin(joinTableName, joinTablePrimaryKey, onComparisonColumn); - return RecordQueryBuilder { std::move(m_queryBuilder) }; - } - - template - [[nodiscard]] RecordQueryBuilder Where(ColumnName const& columnName, - SqlWhereOperator whereOperator, - T const& value) && - { - (void) m_queryBuilder.Where(columnName, sqlOperatorString(whereOperator), value); - return RecordQueryBuilder { std::move(m_queryBuilder) }; - } - - template - [[nodiscard]] RecordQueryBuilder Where(ColumnName const& columnName, T const& value) && - { - (void) m_queryBuilder.Where(columnName, value); - return *this; - } - - [[nodiscard]] RecordQueryBuilder OrderBy(std::string_view columnName, - SqlResultOrdering ordering = SqlResultOrdering::ASCENDING) && - { - (void) m_queryBuilder.OrderBy(columnName, ordering); - return *this; - } - - [[nodiscard]] std::size_t Count() - { - auto stmt = SqlStatement(); - auto const sqlQueryString = m_queryBuilder.Count().ToSql(); - auto const scopedModelSqlLogger = detail::SqlScopedModelQueryLogger(sqlQueryString, {}); - return stmt.ExecuteDirectSingle(sqlQueryString).value(); - } - - [[nodiscard]] std::optional First(size_t count = 1) - { - TargetModel targetRecord; - - auto stmt = SqlStatement {}; - - auto const sqlQueryString = - m_queryBuilder.Fields(targetRecord.AllFieldNames(), targetRecord.TableName()).First(count).ToSql(); - - auto const _ = detail::SqlScopedModelQueryLogger(sqlQueryString, {}); - - stmt.Prepare(sqlQueryString); - stmt.Execute(); - - stmt.BindOutputColumn(1, &targetRecord.MutableId().value); - for (AbstractField* field: targetRecord.AllFields()) - field->BindOutputColumn(stmt); - - if (!stmt.FetchRow()) - return std::nullopt; - - return { std::move(targetRecord) }; - } - - [[nodiscard]] std::vector Range(std::size_t offset, std::size_t limit) - { - auto const targetRecord = TargetModel(); - auto const sqlQueryString = - m_queryBuilder.Field(targetRecord.AllFieldNames(), targetRecord.TableName()).Range(offset, limit).ToSql(); - return TargetModel::Query(sqlQueryString).value_or(std::vector {}); - } - - template - void Each(Callback&& callback) - { - auto const targetRecord = TargetModel(); - auto const sqlQueryString = - m_queryBuilder.Fields(targetRecord.AllFieldNames(), targetRecord.TableName()).All().ToSql(); - TargetModel::Each(std::forward(callback), sqlQueryString); - } - - [[nodiscard]] std::vector All() - { - auto const targetRecord = TargetModel(); - auto const sqlQueryString = - m_queryBuilder.Fields(targetRecord.AllFieldNames(), targetRecord.TableName()).All().ToSql(); - return TargetModel::Query(sqlQueryString); - } - - private: - SqlSelectQueryBuilder m_queryBuilder; -}; - -template -struct Record: AbstractRecord -{ - public: - static constexpr auto Nullable = Model::SqlNullable; - static constexpr auto NotNullable = Model::SqlNotNullable; - - Record() = delete; - Record(Record const&) = default; - Record& operator=(Record const&) = delete; - Record& operator=(Record&&) noexcept = default; - ~Record() override = default; - - Record(Record&& other) noexcept: - AbstractRecord(std::move(other)) - { - } - - // Creates (or recreates a copy of) the model in the database. - RecordId Create(); - - // Reads the model from the database by given model ID. - bool Load(RecordId id); - - // Re-reads the model from the database. - void Reload(); - - // Reads the model from the database by given column name and value. - template - bool Load(std::string_view const& columnName, T const& value); - - // Updates the model in the database. - void Update(); - - // Creates or updates the model in the database, depending on whether it already exists. - void Save(); - - // Deletes the model from the database. - void Destroy(); - - // Updates all models with the given changes in the modelChanges model. - static void UpdateAll(Derived const& modelChanges) noexcept; - - // Retrieves the first model from the database (ordered by ID ASC). - static std::optional First(size_t count = 1); - - // Retrieves the last model from the database (ordered by ID ASC). - static std::optional Last(); - - // Retrieves the model with the given ID from the database. - static std::optional Find(RecordId id); - - template - static std::optional FindBy(ColumnName const& columnName, T const& value); - - // Retrieves all models of this kind from the database. - static std::vector All() noexcept; - - // Retrieves the number of models of this kind from the database. - static size_t Count() noexcept; - - static RecordQueryBuilder Build(); - - // Joins the model with this record's model and returns a proxy for further joins and actions on this join. - template - static RecordQueryBuilder Join(); - - static RecordQueryBuilder Join(std::string_view joinTable, - std::string_view joinColumnName, - SqlQualifiedTableColumnName onComparisonColumn); - - template - static RecordQueryBuilder Where(std::string_view columnName, - SqlWhereOperator whereOperator, - Value const& value); - - template - static RecordQueryBuilder Where(std::string_view columnName, Value const& value); - - // Invokes a callback for each model that matches the given query string. - template - static void Each(Callback&& callback, std::string_view sqlQueryString, InputParameters&&... inputParameters); - - template - static std::vector Query(std::string_view sqlQueryString, InputParameters&&... inputParameters); - - // Returns the SQL string to create the table for this model. - static std::string CreateTableString(SqlServerType serverType); - - // Creates the table for this model from the database. - static void CreateTable(); - - // Drops the table for this model from the database. - static void DropTable(); - - protected: - explicit Record(std::string_view tableName, std::string_view primaryKey = "id"); -}; - -// {{{ Record<> implementation - -template -Record::Record(std::string_view tableName, std::string_view primaryKey): - AbstractRecord { tableName, primaryKey, RecordId {} } -{ -} - -template -size_t Record::Count() noexcept -{ - SqlStatement stmt; - return stmt.ExecuteDirectSingle(std::format("SELECT COUNT(*) FROM {}", Derived().TableName())).value(); -} - -template -std::optional Record::Find(RecordId id) -{ - static_assert(std::is_move_constructible_v, - "The model `Derived` must be move constructible for Find() to return the model."); - Derived model; - if (!model.Load(id)) - return std::nullopt; - return { std::move(model) }; -} - -template -template -std::optional Record::FindBy(ColumnName const& columnName, T const& value) -{ - static_assert(std::is_move_constructible_v, - "The model `Derived` must be move constructible for Find() to return the model."); - Derived model; - if (!model.Load(columnName, value)) - return std::nullopt; - return { std::move(model) }; -} - -template -std::vector Record::All() noexcept -{ - // Require that the model is copy constructible. Simply add a default move constructor to the model if it is not. - static_assert(std::is_move_constructible_v, - "The model `Derived` must be move constructible for All() to copy elements into the result."); - - std::vector allModels; - - Derived const modelSchema; - - detail::StringBuilder sqlColumnsString; - sqlColumnsString << modelSchema.PrimaryKeyName(); - for (AbstractField const* field: modelSchema.AllFields()) - sqlColumnsString << ", " << field->Name(); - - SqlStatement stmt; - - auto const sqlQueryString = std::format("SELECT {} FROM {}", *sqlColumnsString, modelSchema.TableName()); - - auto scopedModelSqlLogger = detail::SqlScopedModelQueryLogger(sqlQueryString, {}); - - stmt.Prepare(sqlQueryString); - stmt.Execute(); - - while (true) - { - Derived record; - - stmt.BindOutputColumn(1, &record.m_data->id.value); - for (AbstractField* field: record.AllFields()) - field->BindOutputColumn(stmt); - - if (!stmt.FetchRow()) - break; - - scopedModelSqlLogger += record; - - allModels.emplace_back(std::move(record)); - } - - return allModels; -} - -template -std::string Record::CreateTableString(SqlServerType serverType) -{ - SqlTraits const& traits = GetSqlTraits(serverType); // TODO: take server type from connection - detail::StringBuilder sql; - auto model = Derived(); - model.SortFieldsByIndex(); - - // TODO: verify that field indices are unique, contiguous, and start at 1 - // TODO: verify that the primary key is the first field - // TODO: verify that the primary key is not nullable - - sql << "CREATE TABLE " << model.TableName() << " (\n"; - - sql << " " << model.PrimaryKeyName() << " " << traits.PrimaryKeyAutoIncrement << ",\n"; - - std::vector sqlConstraints; - - for (auto const* field: model.AllFields()) - { - sql << " " << field->Name() << " " << traits.ColumnTypeName(field->Type()); - - if (field->IsNullable()) - sql << " NULL"; - else - sql << " NOT NULL"; - - if (auto constraint = field->SqlConstraintSpecifier(); !constraint.empty()) - sqlConstraints.emplace_back(std::move(constraint)); - - if (field != model.AllFields().back() || !sqlConstraints.empty()) - sql << ","; - sql << "\n"; - } - - for (auto const& constraint: sqlConstraints) - { - sql << " " << constraint; - if (&constraint != &sqlConstraints.back()) - sql << ","; - sql << "\n"; - } - - sql << ");\n"; - - return *sql; -} - -template -void Record::CreateTable() -{ - auto stmt = SqlStatement {}; - auto const sqlQueryString = CreateTableString(stmt.Connection().ServerType()); - auto const scopedModelSqlLogger = detail::SqlScopedModelQueryLogger(sqlQueryString, {}); - stmt.ExecuteDirect(sqlQueryString); -} - -template -void Record::DropTable() -{ - auto const sqlQueryString = std::format("DROP TABLE \"{}\"", Derived().TableName()); - auto const scopedModelSqlLogger = detail::SqlScopedModelQueryLogger(sqlQueryString, {}); - SqlStatement().ExecuteDirect(sqlQueryString); -} - -template -RecordId Record::Create() -{ - auto stmt = SqlStatement {}; - - auto const modifiedFields = GetModifiedFields(); - - detail::StringBuilder sqlColumnsString; - detail::StringBuilder sqlValuesString; - for (auto const* field: modifiedFields) - { - if (!field->IsModified()) - { - // if (field->IsNull() && field->IsRequired()) - // { - // SqlLogger::GetLogger().OnWarning( // TODO - // std::format("Model required field not given: {}.{}", TableName(), field->Name())); - // return std::unexpected { SqlError::FAILURE }; // TODO: return - // SqlError::MODEL_REQUIRED_FIELD_NOT_GIVEN; - // } - continue; - } - - if (!sqlColumnsString.empty()) - { - sqlColumnsString << ", "; - sqlValuesString << ", "; - } - - sqlColumnsString << field->Name(); - sqlValuesString << "?"; - } - - auto const sqlInsertStmtString = - std::format("INSERT INTO {} ({}) VALUES ({})", TableName(), *sqlColumnsString, *sqlValuesString); - - auto const scopedModelSqlLogger = detail::SqlScopedModelQueryLogger(sqlInsertStmtString, modifiedFields); - - stmt.Prepare(sqlInsertStmtString); - - for (auto const&& [parameterIndex, field]: modifiedFields | std::views::enumerate) - field->BindInputParameter(parameterIndex + 1, stmt); - - stmt.Execute(); - - for (auto* field: AllFields()) - field->SetModified(false); - - // Update the model's ID with the last insert ID - m_data->id = RecordId { .value = stmt.LastInsertId() }; - return m_data->id; -} - -template -bool Record::Load(RecordId id) -{ - return Load(PrimaryKeyName(), id.value); -} - -template -void Record::Reload() -{ - Load(PrimaryKeyName(), Id()); -} - -template -template -bool Record::Load(std::string_view const& columnName, T const& value) -{ - SqlStatement stmt; - - auto const sqlQueryString = SqlQueryBuilder(stmt.Connection().QueryFormatter()) - .FromTable(std::string(TableName())) - .Select() - .Fields(AllFieldNames()) - .Where(columnName, SqlWildcard) - .First() - .ToSql(); - - auto const scopedModelSqlLogger = detail::SqlScopedModelQueryLogger(sqlQueryString, AllFields()); - - stmt.Prepare(sqlQueryString); - stmt.BindInputParameter(1, value); - stmt.BindOutputColumn(1, &m_data->id.value); - for (AbstractField* field: AllFields()) - field->BindOutputColumn(stmt); - stmt.Execute(); - return stmt.FetchRow(); -} - -template -void Record::Update() -{ - auto sqlColumnsString = detail::StringBuilder {}; - auto modifiedFields = GetModifiedFields(); - - for (AbstractField* field: modifiedFields) - { - if (!field->IsModified()) - continue; - - if (!sqlColumnsString.empty()) - sqlColumnsString << ", "; - - sqlColumnsString << field->Name() << " = ?"; - } - - auto stmt = SqlStatement {}; - - auto const sqlQueryString = - std::format("UPDATE {} SET {} WHERE {} = {}", TableName(), *sqlColumnsString, PrimaryKeyName(), Id()); - - auto const scopedModelSqlLogger = detail::SqlScopedModelQueryLogger(sqlQueryString, modifiedFields); - - stmt.Prepare(sqlQueryString); - - for (auto const&& [index, field]: modifiedFields | std::views::enumerate) - field->BindInputParameter(index + 1, stmt); - - stmt.Execute(); - - for (auto* field: modifiedFields) - field->SetModified(false); -} - -template -void Record::Save() -{ - if (Id().value != 0) - return Update(); - - Create(); -} - -template -void Record::Destroy() -{ - auto const sqlQueryString = std::format("DELETE FROM {} WHERE {} = {}", TableName(), PrimaryKeyName(), *Id()); - auto const scopedModelSqlLogger = detail::SqlScopedModelQueryLogger(sqlQueryString, {}); - auto stmt = SqlStatement {}; - auto const& sqlTraits = stmt.Connection().Traits(); - stmt.ExecuteDirect(sqlTraits.EnforceForeignKeyConstraint); - stmt.ExecuteDirect(sqlQueryString); -} - -template -RecordQueryBuilder Record::Build() -{ - return RecordQueryBuilder(); -} - -template -template -RecordQueryBuilder Record::Join() -{ - return RecordQueryBuilder().template Join(); -} - -template -RecordQueryBuilder Record::Join(std::string_view joinTable, - std::string_view joinColumnName, - SqlQualifiedTableColumnName onComparisonColumn) -{ - return RecordQueryBuilder().Join(joinTable, joinColumnName, onComparisonColumn); -} - -template -template -RecordQueryBuilder Record::Where(std::string_view columnName, Value const& value) -{ - return Where(columnName, SqlWhereOperator::EQUAL, value); -} - -template -template -RecordQueryBuilder Record::Where(std::string_view columnName, - SqlWhereOperator whereOperator, - Value const& value) -{ - static_assert(std::is_move_constructible_v, - "The model `Derived` must be move constructible for Where() to return the models."); - -#if 1 - return RecordQueryBuilder().Where(columnName, whereOperator, value); -#else - std::vector allModels; - - Derived modelSchema; - - detail::StringBuilder sqlColumnsString; - sqlColumnsString << modelSchema.PrimaryKeyName(); - for (AbstractField const* field: modelSchema.AllFields()) - sqlColumnsString << ", " << field->Name(); - - auto const sqlQueryString = std::format("SELECT {} FROM {} WHERE \"{}\" {} ?", - *sqlColumnsString, - modelSchema.TableName(), - columnName, - sqlOperatorString(whereOperator)); - return Query(sqlQueryString, value); -#endif -} - -template -template -std::vector Record::Query(std::string_view sqlQueryString, InputParameters&&... inputParameters) -{ - static_assert(std::is_move_constructible_v, - "The model `Derived` must be move constructible for Where() to return the models."); - - std::vector output; - Each([&output](Derived& model) { output.push_back(std::move(model)); }, - sqlQueryString, - std::forward(inputParameters)...); - return { std::move(output) }; -} - -template -template -void Record::Each(Callback&& callback, std::string_view sqlQueryString, InputParameters&&... inputParameters) -{ - SqlStatement stmt; - - auto scopedModelSqlLogger = detail::SqlScopedModelQueryLogger(sqlQueryString, {}); - - stmt.Prepare(sqlQueryString); - - SQLSMALLINT inputParameterPosition = 0; - (stmt.BindInputParameter(++inputParameterPosition, std::forward(inputParameters)), ...); - - stmt.Execute(); - - while (true) - { - Derived record; - - stmt.BindOutputColumn(1, &record.m_data->id.value); - for (AbstractField* field: record.AllFields()) - field->BindOutputColumn(stmt); - - if (!stmt.FetchRow()) - break; - - scopedModelSqlLogger += record; - - callback(record); - } -} - -// }}} - -} // namespace Model diff --git a/src/Lightweight/Model/RecordId.hpp b/src/Lightweight/Model/RecordId.hpp deleted file mode 100644 index c7575136..00000000 --- a/src/Lightweight/Model/RecordId.hpp +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "../../Lightweight/SqlDataBinder.hpp" - -#include - -namespace Model -{ - -// Represents a unique identifier of a specific record in a table. -struct RecordId -{ - using InnerType = size_t; - InnerType value; - - constexpr InnerType operator*() const noexcept - { - return value; - } - - constexpr std::weak_ordering operator<=>(RecordId const& other) const noexcept = default; - - constexpr bool operator==(RecordId other) const noexcept - { - return value == other.value; - } - - constexpr bool operator==(InnerType other) const noexcept - { - return value == other; - } -}; - -} // namespace Model - -template -struct WhereConditionLiteralType; - -template <> -struct WhereConditionLiteralType -{ - constexpr static bool needsQuotes = false; -}; - -template <> -struct SqlDataBinder -{ - static SQLRETURN InputParameter(SQLHSTMT stmt, SQLSMALLINT column, Model::RecordId const& value) - { - return SqlDataBinder::InputParameter(stmt, column, value.value); - } - - static SQLRETURN OutputColumn( - SQLHSTMT stmt, SQLSMALLINT column, Model::RecordId* result, SQLLEN* indicator, SqlDataBinderCallback& cb) - { - return SqlDataBindervalue)>::OutputColumn(stmt, column, &result->value, indicator, cb); - } - - static SQLRETURN GetColumn(SQLHSTMT stmt, SQLSMALLINT column, Model::RecordId* result, SQLLEN* indicator) - { - return SqlDataBindervalue)>::GetColumn(stmt, column, &result->value, indicator); - } -}; - -template <> -struct std::formatter: std::formatter -{ - auto format(Model::RecordId id, format_context& ctx) const -> format_context::iterator - { - return formatter::format(id.value, ctx); - } -}; diff --git a/src/Lightweight/Model/StringLiteral.hpp b/src/Lightweight/Model/StringLiteral.hpp deleted file mode 100644 index 9575a4d6..00000000 --- a/src/Lightweight/Model/StringLiteral.hpp +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include - -namespace Model -{ - -template -struct StringLiteral -{ - constexpr StringLiteral(const char (&str)[N]) noexcept - { - std::copy_n(str, N, value); - } - - constexpr std::string_view operator*() const noexcept - { - return value; - } - - char value[N]; -}; - -} // namespace Model diff --git a/src/Lightweight/Model/Utils.hpp b/src/Lightweight/Model/Utils.hpp deleted file mode 100644 index 87a1b36b..00000000 --- a/src/Lightweight/Model/Utils.hpp +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "../SqlConnection.hpp" -#include "./Detail.hpp" -#include "./Record.hpp" - -#include - -namespace Model -{ - -template -std::string CreateSqlTablesString(SqlServerType serverType) -{ - detail::StringBuilder result; - result << ((Models::CreateTableString(serverType) << "\n") << ...); - return *result; -} - -template -void CreateSqlTables() -{ - (Models::CreateTable(), ...); -} - -} // namespace Model diff --git a/src/Lightweight/SqlConnectInfo.hpp b/src/Lightweight/SqlConnectInfo.hpp index cc926ada..0b040d33 100644 --- a/src/Lightweight/SqlConnectInfo.hpp +++ b/src/Lightweight/SqlConnectInfo.hpp @@ -2,6 +2,8 @@ #pragma once +#include "Api.hpp" + #include #include #include @@ -33,7 +35,7 @@ struct SqlConnectionDataSource using SqlConnectInfo = std::variant; template <> -struct std::formatter: std::formatter +struct LIGHTWEIGHT_API std::formatter: std::formatter { auto format(SqlConnectInfo const& info, format_context& ctx) const -> format_context::iterator { diff --git a/src/Lightweight/SqlConnection.cpp b/src/Lightweight/SqlConnection.cpp index 4bc8cb31..a10e33fe 100644 --- a/src/Lightweight/SqlConnection.cpp +++ b/src/Lightweight/SqlConnection.cpp @@ -4,108 +4,38 @@ #include "SqlQuery.hpp" #include "SqlQueryFormatter.hpp" -#include -#include -#include - #include using namespace std::chrono_literals; using namespace std::string_view_literals; -static std::list g_unusedConnections; - -class SqlConnectionPool -{ - public: - ~SqlConnectionPool() - { - KillAllIdleConnections(); - } - - void KillAllIdleConnections() - { - auto const _ = std::lock_guard { m_unusedConnectionsMutex }; - for (auto& connection: m_unusedConnections) - connection.Kill(); - m_unusedConnections.clear(); - } - - SqlConnection AcquireDirect() - { - auto const _ = std::lock_guard { m_unusedConnectionsMutex }; - - // Close idle connections - auto const now = std::chrono::steady_clock::now(); - while (!m_unusedConnections.empty() && now - m_unusedConnections.front().LastUsed() > m_connectionTimeout) - { - ++m_stats.timedout; - SqlLogger::GetLogger().OnConnectionIdle(m_unusedConnections.front()); - m_unusedConnections.front().Kill(); - m_unusedConnections.pop_front(); - } - - // Reuse an existing connection - if (!m_unusedConnections.empty()) - { - ++m_stats.reused; - auto connection = std::move(m_unusedConnections.front()); - m_unusedConnections.pop_front(); - SqlLogger::GetLogger().OnConnectionReuse(connection); - return connection; - } - - // Create a new connection - ++m_stats.created; - auto connection = SqlConnection { SqlConnection::DefaultConnectInfo() }; - return connection; - } - - void Release(SqlConnection&& connection) - { - auto const _ = std::lock_guard { m_unusedConnectionsMutex }; - ++m_stats.released; - if (m_unusedConnections.size() < m_maxIdleConnections) - { - connection.SetLastUsed(std::chrono::steady_clock::now()); - SqlLogger::GetLogger().OnConnectionReuse(connection); - m_unusedConnections.emplace_back(std::move(connection)); - } - else - { - SqlLogger::GetLogger().OnConnectionIdle(connection); - connection.Kill(); - } - } - - void SetMaxIdleConnections(size_t maxIdleConnections) noexcept - { - m_maxIdleConnections = maxIdleConnections; - } +static std::optional gDefaultConnectInfo {}; +static std::atomic gNextConnectionId { 1 }; +static std::function gPostConnectedHook {}; - [[nodiscard]] SqlConnectionStats Stats() const noexcept - { - return m_stats; - } +// ===================================================================================================================== - private: - std::list m_unusedConnections; - std::mutex m_unusedConnectionsMutex; - size_t m_maxIdleConnections = 10; - std::chrono::seconds m_connectionTimeout = std::chrono::seconds { 120 }; - SqlConnectionStats m_stats; +struct SqlConnection::Data +{ + std::chrono::steady_clock::time_point lastUsed; // Last time the connection was used (mostly interesting for + // idle connections in the connection pool). + SqlConnectInfo connectInfo; }; -static SqlConnectionPool g_connectionPool; - -// ===================================================================================================================== - -SqlConnection::SqlConnection() noexcept: - SqlConnection(g_connectionPool.AcquireDirect()) +SqlConnection::SqlConnection(): + m_connectionId { gNextConnectionId++ }, + m_data { new Data() } { + SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &m_hEnv); + SQLSetEnvAttr(m_hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER) SQL_OV_ODBC3, 0); + SQLAllocHandle(SQL_HANDLE_DBC, m_hEnv, &m_hDbc); + + Connect(DefaultConnectInfo()); } -SqlConnection::SqlConnection(std::optional connectInfo) noexcept +SqlConnection::SqlConnection(std::optional connectInfo): + m_connectionId { gNextConnectionId++ }, + m_data { new Data() } { SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &m_hEnv); SQLSetEnvAttr(m_hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER) SQL_OV_ODBC3, 0); @@ -119,13 +49,13 @@ SqlConnection::SqlConnection(SqlConnection&& other) noexcept: m_hEnv { other.m_hEnv }, m_hDbc { other.m_hDbc }, m_connectionId { other.m_connectionId }, - m_connectInfo { std::move(other.m_connectInfo) }, - m_lastUsed { other.m_lastUsed }, m_serverType { other.m_serverType }, - m_queryFormatter { other.m_queryFormatter } + m_queryFormatter { other.m_queryFormatter }, + m_data { other.m_data } { other.m_hEnv = {}; other.m_hDbc = {}; + other.m_data = nullptr; } SqlConnection& SqlConnection::operator=(SqlConnection&& other) noexcept @@ -138,11 +68,11 @@ SqlConnection& SqlConnection::operator=(SqlConnection&& other) noexcept m_hEnv = other.m_hEnv; m_hDbc = other.m_hDbc; m_connectionId = other.m_connectionId; - m_connectInfo = std::move(other.m_connectInfo); - m_lastUsed = other.m_lastUsed; + m_data = other.m_data; other.m_hEnv = {}; other.m_hDbc = {}; + other.m_data = nullptr; return *this; } @@ -150,31 +80,42 @@ SqlConnection& SqlConnection::operator=(SqlConnection&& other) noexcept SqlConnection::~SqlConnection() noexcept { Close(); + delete m_data; } -void SqlConnection::SetMaxIdleConnections(size_t maxIdleConnections) noexcept +SqlConnectInfo const& SqlConnection::DefaultConnectInfo() noexcept { - g_connectionPool.SetMaxIdleConnections(maxIdleConnections); + return gDefaultConnectInfo.value(); } -void SqlConnection::KillAllIdle() +void SqlConnection::SetDefaultConnectInfo(SqlConnectInfo connectInfo) noexcept { - g_connectionPool.KillAllIdleConnections(); + gDefaultConnectInfo = std::move(connectInfo); } -void SqlConnection::SetPostConnectedHook(std::function hook) +SqlConnectInfo const& SqlConnection::ConnectionInfo() const noexcept { - m_gPostConnectedHook = std::move(hook); + return m_data->connectInfo; } -void SqlConnection::ResetPostConnectedHook() +void SqlConnection::SetLastUsed(std::chrono::steady_clock::time_point lastUsed) noexcept { - m_gPostConnectedHook = {}; + m_data->lastUsed = lastUsed; } -SqlConnectionStats SqlConnection::Stats() noexcept +std::chrono::steady_clock::time_point SqlConnection::LastUsed() const noexcept { - return g_connectionPool.Stats(); + return m_data->lastUsed; +} + +void SqlConnection::SetPostConnectedHook(std::function hook) +{ + gPostConnectedHook = std::move(hook); +} + +void SqlConnection::ResetPostConnectedHook() +{ + gPostConnectedHook = {}; } bool SqlConnection::Connect(std::string_view datasource, std::string_view username, std::string_view password) noexcept @@ -218,9 +159,9 @@ bool SqlConnection::Connect(SqlConnectInfo connectInfo) noexcept if (m_hDbc) SQLDisconnect(m_hDbc); - m_connectInfo = std::move(connectInfo); + m_data->connectInfo = std::move(connectInfo); - if (auto const* info = std::get_if(&m_connectInfo)) + if (auto const* info = std::get_if(&m_data->connectInfo)) { SQLRETURN sqlReturn = SQLSetConnectAttrA(m_hDbc, SQL_LOGIN_TIMEOUT, (SQLPOINTER) info->timeout.count(), 0); if (!SQL_SUCCEEDED(sqlReturn)) @@ -253,13 +194,13 @@ bool SqlConnection::Connect(SqlConnectInfo connectInfo) noexcept SqlLogger::GetLogger().OnConnectionOpened(*this); - if (m_gPostConnectedHook) - m_gPostConnectedHook(*this); + if (gPostConnectedHook) + gPostConnectedHook(*this); return true; } - auto const& connectionString = std::get(m_connectInfo).value; + auto const& connectionString = std::get(m_data->connectInfo).value; SQLRETURN sqlResult = SQLDriverConnectA(m_hDbc, (SQLHWND) nullptr, @@ -279,24 +220,13 @@ bool SqlConnection::Connect(SqlConnectInfo connectInfo) noexcept PostConnect(); SqlLogger::GetLogger().OnConnectionOpened(*this); - if (m_gPostConnectedHook) - m_gPostConnectedHook(*this); + if (gPostConnectedHook) + gPostConnectedHook(*this); return true; } void SqlConnection::Close() noexcept -{ - if (!m_hDbc) - return; - - if (m_connectInfo == DefaultConnectInfo()) - g_connectionPool.Release(std::move(*this)); - else - Kill(); -} - -void SqlConnection::Kill() noexcept { if (!m_hDbc) return; diff --git a/src/Lightweight/SqlConnection.hpp b/src/Lightweight/SqlConnection.hpp index bb29ba5e..26a0a4fd 100644 --- a/src/Lightweight/SqlConnection.hpp +++ b/src/Lightweight/SqlConnection.hpp @@ -6,16 +6,19 @@ #include #endif +#include "Api.hpp" #include "SqlConnectInfo.hpp" #include "SqlError.hpp" #include "SqlLogger.hpp" #include "SqlTraits.hpp" +#include "SqlUtils.hpp" #include #include #include #include #include +#include #include #include #include @@ -30,7 +33,7 @@ class SqlQueryBuilder; class SqlQueryFormatter; // @brief Represents a connection to a SQL database. -class SqlConnection final +class LIGHTWEIGHT_API SqlConnection final { public: // Constructs a new SQL connection to the default connection. @@ -38,10 +41,10 @@ class SqlConnection final // The default connection is set via SetDefaultConnectInfo. // In case the default connection is not set, the connection will fail. // And in case the connection fails, the last error will be set. - SqlConnection() noexcept; + SqlConnection(); // Constructs a new SQL connection to the given connect informaton. - explicit SqlConnection(std::optional connectInfo) noexcept; + explicit SqlConnection(std::optional connectInfo); SqlConnection(SqlConnection&&) noexcept; SqlConnection& operator=(SqlConnection&&) noexcept; @@ -52,28 +55,14 @@ class SqlConnection final ~SqlConnection() noexcept; // Retrieves the default connection information. - static SqlConnectInfo const& DefaultConnectInfo() noexcept - { - return m_gDefaultConnectInfo.value(); - } + static SqlConnectInfo const& DefaultConnectInfo() noexcept; // Sets the default connection information. - static void SetDefaultConnectInfo(SqlConnectInfo connectInfo) noexcept - { - m_gDefaultConnectInfo = std::move(connectInfo); - } - - // Sets the maximum number of idle connections in the connection pool. - static void SetMaxIdleConnections(size_t maxIdleConnections) noexcept; - - // Kills all idle connections in the connection pool. - static void KillAllIdle(); + static void SetDefaultConnectInfo(SqlConnectInfo connectInfo) noexcept; static void SetPostConnectedHook(std::function hook); static void ResetPostConnectedHook(); - static SqlConnectionStats Stats() noexcept; - // Retrieves the connection ID. // // This is a unique identifier for the connection, which is useful for debugging purposes. @@ -86,9 +75,6 @@ class SqlConnection final // Closes the connection (attempting to put it back into the connection pool). void Close() noexcept; - // Kills the connection. - void Kill() noexcept; - // Connects to the given database with the given username and password. bool Connect(std::string_view datasource, std::string_view username, std::string_view password) noexcept; @@ -138,10 +124,7 @@ class SqlConnection final [[nodiscard]] bool IsAlive() const noexcept; // Retrieves the connection information. - [[nodiscard]] SqlConnectInfo const& ConnectionInfo() const noexcept - { - return m_connectInfo; - } + [[nodiscard]] SqlConnectInfo const& ConnectionInfo() const noexcept; // Retrieves the native handle. [[nodiscard]] SQLHDBC NativeHandle() const noexcept @@ -150,16 +133,10 @@ class SqlConnection final } // Retrieves the last time the connection was used. - [[nodiscard]] std::chrono::steady_clock::time_point LastUsed() const noexcept - { - return m_lastUsed; - } + [[nodiscard]] std::chrono::steady_clock::time_point LastUsed() const noexcept; // Sets the last time the connection was used. - void SetLastUsed(std::chrono::steady_clock::time_point lastUsed) noexcept - { - m_lastUsed = lastUsed; - } + void SetLastUsed(std::chrono::steady_clock::time_point lastUsed) noexcept; private: void PostConnect(); @@ -167,19 +144,14 @@ class SqlConnection final void RequireSuccess(SQLRETURN error, std::source_location sourceLocation = std::source_location::current()) const; // Private data members - - static inline std::optional m_gDefaultConnectInfo; - static inline std::atomic m_gNextConnectionId { 1 }; - static inline std::function m_gPostConnectedHook {}; - SQLHENV m_hEnv {}; SQLHDBC m_hDbc {}; - uint64_t m_connectionId { m_gNextConnectionId++ }; - SqlConnectInfo m_connectInfo; - std::chrono::steady_clock::time_point m_lastUsed; // Last time the connection was used (mostly interesting for - // idle connections in the connection pool). + uint64_t m_connectionId; SqlServerType m_serverType = SqlServerType::UNKNOWN; SqlQueryFormatter const* m_queryFormatter {}; + + struct Data; + Data* m_data {}; }; inline SqlServerType SqlConnection::ServerType() const noexcept diff --git a/src/Lightweight/SqlError.hpp b/src/Lightweight/SqlError.hpp index ebdbee8c..89e624b8 100644 --- a/src/Lightweight/SqlError.hpp +++ b/src/Lightweight/SqlError.hpp @@ -6,6 +6,8 @@ #include #endif +#include "Api.hpp" + #include #include #include @@ -61,7 +63,7 @@ struct SqlErrorInfo class SqlException: public std::runtime_error { public: - explicit SqlException(SqlErrorInfo info); + LIGHTWEIGHT_API explicit SqlException(SqlErrorInfo info); [[nodiscard]] SqlErrorInfo const& info() const noexcept { @@ -142,7 +144,7 @@ inline std::error_code make_error_code(SqlError e) } template <> -struct std::formatter: formatter +struct LIGHTWEIGHT_API std::formatter: formatter { auto format(SqlError value, format_context& ctx) const -> format_context::iterator { @@ -152,7 +154,7 @@ struct std::formatter: formatter }; template <> -struct std::formatter: formatter +struct LIGHTWEIGHT_API std::formatter: formatter { auto format(SqlErrorInfo const& info, format_context& ctx) const -> format_context::iterator { diff --git a/src/Lightweight/SqlLogger.cpp b/src/Lightweight/SqlLogger.cpp index 45849d9f..35fa832a 100644 --- a/src/Lightweight/SqlLogger.cpp +++ b/src/Lightweight/SqlLogger.cpp @@ -75,7 +75,6 @@ class SqlStandardLogger: public SqlLogger void OnExecute(std::string_view const&) override {} void OnExecuteBatch() override {} void OnFetchedRow() override {} - void OnStats(SqlConnectionStats const&) override {} }; class SqlTraceLogger: public SqlStandardLogger @@ -148,18 +147,6 @@ class SqlTraceLogger: public SqlStandardLogger WriteMessage("Fetched row"); } - void OnStats(SqlConnectionStats const& stats) override - { - Tick(); - WriteMessage("[SqlConnectionPool stats] " - "created: {}, reused: {}, closed: {}, timedout: {}, released: {}", - stats.created, - stats.reused, - stats.closed, - stats.timedout, - stats.released); - } - private: void WriteDetails(std::source_location sourceLocation) { diff --git a/src/Lightweight/SqlLogger.hpp b/src/Lightweight/SqlLogger.hpp index f1e396fb..dae57468 100644 --- a/src/Lightweight/SqlLogger.hpp +++ b/src/Lightweight/SqlLogger.hpp @@ -2,6 +2,7 @@ #pragma once +#include "Api.hpp" #include "SqlError.hpp" #include @@ -9,17 +10,8 @@ class SqlConnection; -struct SqlConnectionStats -{ - size_t created {}; - size_t reused {}; - size_t closed {}; - size_t timedout {}; - size_t released {}; -}; - // Represents a logger for SQL operations. -class SqlLogger +class LIGHTWEIGHT_API SqlLogger { public: virtual ~SqlLogger() = default; @@ -41,7 +33,6 @@ class SqlLogger virtual void OnExecute(std::string_view const& query) = 0; virtual void OnExecuteBatch() = 0; virtual void OnFetchedRow() = 0; - virtual void OnStats(SqlConnectionStats const& stats) = 0; // Logs the most important events to standard output in a human-readable format. static SqlLogger& StandardLogger(); diff --git a/src/Lightweight/SqlQuery.hpp b/src/Lightweight/SqlQuery.hpp index 6881425c..9d330326 100644 --- a/src/Lightweight/SqlQuery.hpp +++ b/src/Lightweight/SqlQuery.hpp @@ -1,13 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include "Api.hpp" #include "SqlQuery/Delete.hpp" #include "SqlQuery/Insert.hpp" #include "SqlQuery/Select.hpp" #include "SqlQuery/Update.hpp" // API Entry point for building SQL queries. -class [[nodiscard]] SqlQueryBuilder final +class [[nodiscard]] LIGHTWEIGHT_API SqlQueryBuilder final { public: // Constructs a new query builder for the given table. @@ -47,9 +48,9 @@ class [[nodiscard]] SqlQueryBuilder final std::string m_tableAlias; }; -inline SqlQueryBuilder::SqlQueryBuilder(SqlQueryFormatter const& formatter, - std::string&& table, - std::string&& alias) noexcept: +inline LIGHTWEIGHT_FORCE_INLINE SqlQueryBuilder::SqlQueryBuilder(SqlQueryFormatter const& formatter, + std::string&& table, + std::string&& alias) noexcept: m_formatter { formatter }, m_table { std::move(table) }, m_tableAlias { std::move(alias) } diff --git a/src/Lightweight/SqlQuery/Core.hpp b/src/Lightweight/SqlQuery/Core.hpp index 466de06e..11568e04 100644 --- a/src/Lightweight/SqlQuery/Core.hpp +++ b/src/Lightweight/SqlQuery/Core.hpp @@ -2,6 +2,7 @@ #pragma once +#include "../Api.hpp" #include "../SqlDataBinder.hpp" #include "../SqlQueryFormatter.hpp" @@ -36,7 +37,7 @@ namespace detail { template -std::string MakeSqlColumnName(ColumnName const& columnName) +LIGHTWEIGHT_API std::string MakeSqlColumnName(ColumnName const& columnName) { using namespace std::string_view_literals; std::string output; @@ -77,7 +78,7 @@ namespace detail // // This class is inherited by the SqlSelectQueryBuilder, SqlUpdateQueryBuilder, and SqlDeleteQueryBuilder template -class [[nodiscard]] SqlWhereClauseBuilder +class [[nodiscard]] LIGHTWEIGHT_API SqlWhereClauseBuilder { public: // Indicates, that the next WHERE clause should be AND-ed (default). @@ -221,21 +222,21 @@ class [[nodiscard]] SqlWhereClauseBuilder }; template -Derived& SqlWhereClauseBuilder::And() noexcept +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::And() noexcept { m_nextWhereJunctor = WhereJunctor::And; return static_cast(*this); } template -Derived& SqlWhereClauseBuilder::Or() noexcept +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::Or() noexcept { m_nextWhereJunctor = WhereJunctor::Or; return static_cast(*this); } template -Derived& SqlWhereClauseBuilder::Not() noexcept +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::Not() noexcept { m_nextIsNot = !m_nextIsNot; return static_cast(*this); @@ -243,7 +244,8 @@ Derived& SqlWhereClauseBuilder::Not() noexcept template template -Derived& SqlWhereClauseBuilder::Where(ColumnName const& columnName, T const& value) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::Where(ColumnName const& columnName, + T const& value) { return Where(columnName, "=", value); } @@ -251,7 +253,7 @@ Derived& SqlWhereClauseBuilder::Where(ColumnName const& columnName, T c template template requires std::invocable&> -Derived& SqlWhereClauseBuilder::OrWhere(Callable&& callable) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::OrWhere(Callable&& callable) { return Or().Where(std::forward(callable)); } @@ -259,7 +261,7 @@ Derived& SqlWhereClauseBuilder::OrWhere(Callable&& callable) template template requires std::invocable&> -Derived& SqlWhereClauseBuilder::Where(Callable&& callable) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::Where(Callable&& callable) { auto& condition = SearchCondition().condition; @@ -281,7 +283,7 @@ Derived& SqlWhereClauseBuilder::Where(Callable&& callable) return static_cast(*this); } -RawSqlCondition PopulateSqlSetExpression(auto const& values) +inline LIGHTWEIGHT_FORCE_INLINE RawSqlCondition PopulateSqlSetExpression(auto const& values) { using namespace std::string_view_literals; std::ostringstream fragment; @@ -298,51 +300,53 @@ RawSqlCondition PopulateSqlSetExpression(auto const& values) template template -Derived& SqlWhereClauseBuilder::WhereIn(ColumnName const& columnName, InputRange const& values) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::WhereIn(ColumnName const& columnName, + InputRange const& values) { return Where(columnName, "IN", detail::PopulateSqlSetExpression(values)); } template template -Derived& SqlWhereClauseBuilder::WhereIn(ColumnName const& columnName, std::initializer_list&& values) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::WhereIn(ColumnName const& columnName, + std::initializer_list&& values) { return Where(columnName, "IN", detail::PopulateSqlSetExpression(std::forward>(values))); } template template -Derived& SqlWhereClauseBuilder::WhereNotNull(ColumnName const& columnName) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::WhereNotNull(ColumnName const& columnName) { return Where(columnName, "!=", "NULL"); } template template -Derived& SqlWhereClauseBuilder::WhereNull(ColumnName const& columnName) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::WhereNull(ColumnName const& columnName) { return Where(columnName, "=", "NULL"); } template template -Derived& SqlWhereClauseBuilder::WhereTrue(ColumnName const& columnName) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::WhereTrue(ColumnName const& columnName) { return Where(columnName, "=", true); } template template -Derived& SqlWhereClauseBuilder::WhereFalse(ColumnName const& columnName) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::WhereFalse(ColumnName const& columnName) { return Where(columnName, "=", false); } template template -Derived& SqlWhereClauseBuilder::WhereColumn(LeftColumn const& left, - std::string_view binaryOp, - RightColumn const& right) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::WhereColumn(LeftColumn const& left, + std::string_view binaryOp, + RightColumn const& right) { AppendWhereJunctor(); @@ -363,16 +367,18 @@ struct WhereConditionLiteralType template template -Derived& SqlWhereClauseBuilder::Where(ColumnName const& columnName, - std::string_view binaryOp, - char const (&value)[N]) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::Where(ColumnName const& columnName, + std::string_view binaryOp, + char const (&value)[N]) { return Where(columnName, binaryOp, std::string_view { value, N - 1 }); } template template -Derived& SqlWhereClauseBuilder::Where(ColumnName const& columnName, std::string_view binaryOp, T const& value) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::Where(ColumnName const& columnName, + std::string_view binaryOp, + T const& value) { auto& searchCondition = SearchCondition(); @@ -415,71 +421,65 @@ Derived& SqlWhereClauseBuilder::Where(ColumnName const& columnName, std } template -Derived& SqlWhereClauseBuilder::InnerJoin(std::string_view joinTable, - std::string_view joinColumnName, - SqlQualifiedTableColumnName onOtherColumn) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::InnerJoin( + std::string_view joinTable, std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn) { return Join(JoinType::INNER, joinTable, joinColumnName, onOtherColumn); } template -Derived& SqlWhereClauseBuilder::InnerJoin(std::string_view joinTable, - std::string_view joinColumnName, - std::string_view onMainTableColumn) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::InnerJoin(std::string_view joinTable, + std::string_view joinColumnName, + std::string_view onMainTableColumn) { return Join(JoinType::INNER, joinTable, joinColumnName, onMainTableColumn); } template -Derived& SqlWhereClauseBuilder::LeftOuterJoin(std::string_view joinTable, - std::string_view joinColumnName, - SqlQualifiedTableColumnName onOtherColumn) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::LeftOuterJoin( + std::string_view joinTable, std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn) { return Join(JoinType::LEFT, joinTable, joinColumnName, onOtherColumn); } template -Derived& SqlWhereClauseBuilder::LeftOuterJoin(std::string_view joinTable, - std::string_view joinColumnName, - std::string_view onMainTableColumn) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::LeftOuterJoin( + std::string_view joinTable, std::string_view joinColumnName, std::string_view onMainTableColumn) { return Join(JoinType::LEFT, joinTable, joinColumnName, onMainTableColumn); } template -Derived& SqlWhereClauseBuilder::RightOuterJoin(std::string_view joinTable, - std::string_view joinColumnName, - SqlQualifiedTableColumnName onOtherColumn) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::RightOuterJoin( + std::string_view joinTable, std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn) { return Join(JoinType::RIGHT, joinTable, joinColumnName, onOtherColumn); } template -Derived& SqlWhereClauseBuilder::RightOuterJoin(std::string_view joinTable, - std::string_view joinColumnName, - std::string_view onMainTableColumn) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::RightOuterJoin( + std::string_view joinTable, std::string_view joinColumnName, std::string_view onMainTableColumn) { return Join(JoinType::RIGHT, joinTable, joinColumnName, onMainTableColumn); } template -Derived& SqlWhereClauseBuilder::FullOuterJoin(std::string_view joinTable, - std::string_view joinColumnName, - SqlQualifiedTableColumnName onOtherColumn) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::FullOuterJoin( + std::string_view joinTable, std::string_view joinColumnName, SqlQualifiedTableColumnName onOtherColumn) { return Join(JoinType::FULL, joinTable, joinColumnName, onOtherColumn); } template -Derived& SqlWhereClauseBuilder::FullOuterJoin(std::string_view joinTable, - std::string_view joinColumnName, - std::string_view onMainTableColumn) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::FullOuterJoin( + std::string_view joinTable, std::string_view joinColumnName, std::string_view onMainTableColumn) { return Join(JoinType::FULL, joinTable, joinColumnName, onMainTableColumn); } template -Derived& SqlWhereClauseBuilder::WhereRaw(std::string_view sqlConditionExpression) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::WhereRaw( + std::string_view sqlConditionExpression) { AppendWhereJunctor(); @@ -490,19 +490,19 @@ Derived& SqlWhereClauseBuilder::WhereRaw(std::string_view sqlConditionE } template -inline SqlSearchCondition& SqlWhereClauseBuilder::SearchCondition() noexcept +inline LIGHTWEIGHT_FORCE_INLINE SqlSearchCondition& SqlWhereClauseBuilder::SearchCondition() noexcept { return static_cast(this)->SearchCondition(); } template -SqlQueryFormatter const& SqlWhereClauseBuilder::Formatter() const noexcept +inline LIGHTWEIGHT_FORCE_INLINE SqlQueryFormatter const& SqlWhereClauseBuilder::Formatter() const noexcept { return static_cast(this)->Formatter(); } template -void SqlWhereClauseBuilder::AppendWhereJunctor() +inline LIGHTWEIGHT_FORCE_INLINE void SqlWhereClauseBuilder::AppendWhereJunctor() { using namespace std::string_view_literals; @@ -536,16 +536,16 @@ template template requires(std::is_same_v || std::is_convertible_v || std::is_convertible_v) -void SqlWhereClauseBuilder::AppendColumnName(ColumnName const& columnName) +inline LIGHTWEIGHT_FORCE_INLINE void SqlWhereClauseBuilder::AppendColumnName(ColumnName const& columnName) { SearchCondition().condition += detail::MakeSqlColumnName(columnName); } template -Derived& SqlWhereClauseBuilder::Join(JoinType joinType, - std::string_view joinTable, - std::string_view joinColumnName, - SqlQualifiedTableColumnName onOtherColumn) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::Join(JoinType joinType, + std::string_view joinTable, + std::string_view joinColumnName, + SqlQualifiedTableColumnName onOtherColumn) { static constexpr std::array JoinTypeStrings = { "INNER", @@ -565,10 +565,10 @@ Derived& SqlWhereClauseBuilder::Join(JoinType joinType, } template -Derived& SqlWhereClauseBuilder::Join(JoinType joinType, - std::string_view joinTable, - std::string_view joinColumnName, - std::string_view onMainTableColumn) +inline LIGHTWEIGHT_FORCE_INLINE Derived& SqlWhereClauseBuilder::Join(JoinType joinType, + std::string_view joinTable, + std::string_view joinColumnName, + std::string_view onMainTableColumn) { return Join( joinType, diff --git a/src/Lightweight/SqlQuery/Delete.hpp b/src/Lightweight/SqlQuery/Delete.hpp index 107a29c9..2ad7c703 100644 --- a/src/Lightweight/SqlQuery/Delete.hpp +++ b/src/Lightweight/SqlQuery/Delete.hpp @@ -6,7 +6,8 @@ #include -class [[nodiscard]] SqlDeleteQueryBuilder final: public detail::SqlWhereClauseBuilder +class [[nodiscard]] LIGHTWEIGHT_API SqlDeleteQueryBuilder final: + public detail::SqlWhereClauseBuilder { public: explicit SqlDeleteQueryBuilder(SqlQueryFormatter const& formatter, @@ -37,7 +38,7 @@ class [[nodiscard]] SqlDeleteQueryBuilder final: public detail::SqlWhereClauseBu SqlSearchCondition m_searchCondition; }; -inline std::string SqlDeleteQueryBuilder::ToSql() const +inline LIGHTWEIGHT_FORCE_INLINE std::string SqlDeleteQueryBuilder::ToSql() const { return m_formatter.Delete(m_searchCondition.tableName, m_searchCondition.tableAlias, diff --git a/src/Lightweight/SqlQuery/Insert.hpp b/src/Lightweight/SqlQuery/Insert.hpp index febdfac0..6fdf2536 100644 --- a/src/Lightweight/SqlQuery/Insert.hpp +++ b/src/Lightweight/SqlQuery/Insert.hpp @@ -9,7 +9,7 @@ #include #include -class [[nodiscard]] SqlInsertQueryBuilder final +class [[nodiscard]] LIGHTWEIGHT_API SqlInsertQueryBuilder final { public: explicit SqlInsertQueryBuilder(SqlQueryFormatter const& formatter, @@ -38,9 +38,8 @@ class [[nodiscard]] SqlInsertQueryBuilder final std::vector* m_inputBindings; }; -inline SqlInsertQueryBuilder::SqlInsertQueryBuilder(SqlQueryFormatter const& formatter, - std::string tableName, - std::vector* inputBindings) noexcept: +inline LIGHTWEIGHT_FORCE_INLINE SqlInsertQueryBuilder::SqlInsertQueryBuilder( + SqlQueryFormatter const& formatter, std::string tableName, std::vector* inputBindings) noexcept: m_formatter { formatter }, m_tableName { std::move(tableName) }, m_inputBindings { inputBindings } diff --git a/src/Lightweight/SqlQuery/Select.hpp b/src/Lightweight/SqlQuery/Select.hpp index 45501e6c..435c7d18 100644 --- a/src/Lightweight/SqlQuery/Select.hpp +++ b/src/Lightweight/SqlQuery/Select.hpp @@ -10,7 +10,8 @@ enum class SqlResultOrdering : uint8_t DESCENDING }; -class [[nodiscard]] SqlSelectQueryBuilder final: public detail::SqlWhereClauseBuilder +class [[nodiscard]] LIGHTWEIGHT_API SqlSelectQueryBuilder final: + public detail::SqlWhereClauseBuilder { public: enum class SelectType diff --git a/src/Lightweight/SqlQuery/Update.hpp b/src/Lightweight/SqlQuery/Update.hpp index d663eac0..e334c563 100644 --- a/src/Lightweight/SqlQuery/Update.hpp +++ b/src/Lightweight/SqlQuery/Update.hpp @@ -8,7 +8,8 @@ #include #include -class [[nodiscard]] SqlUpdateQueryBuilder final: public detail::SqlWhereClauseBuilder +class [[nodiscard]] LIGHTWEIGHT_API SqlUpdateQueryBuilder final: + public detail::SqlWhereClauseBuilder { public: SqlUpdateQueryBuilder(SqlQueryFormatter const& formatter, diff --git a/src/Lightweight/SqlQueryFormatter.hpp b/src/Lightweight/SqlQueryFormatter.hpp index 037402cb..a71a133f 100644 --- a/src/Lightweight/SqlQueryFormatter.hpp +++ b/src/Lightweight/SqlQueryFormatter.hpp @@ -2,6 +2,7 @@ #pragma once +#include "Api.hpp" #include "SqlConnection.hpp" #include "SqlDataBinder.hpp" @@ -11,7 +12,7 @@ struct SqlQualifiedTableColumnName; // API to format SQL queries for different SQL dialects. -class [[nodiscard]] SqlQueryFormatter +class [[nodiscard]] LIGHTWEIGHT_API SqlQueryFormatter { public: virtual ~SqlQueryFormatter() = default; diff --git a/src/Lightweight/SqlSchema.hpp b/src/Lightweight/SqlSchema.hpp index 2f5490e3..7083964d 100644 --- a/src/Lightweight/SqlSchema.hpp +++ b/src/Lightweight/SqlSchema.hpp @@ -6,6 +6,7 @@ #include #endif +#include "Api.hpp" #include "SqlTraits.hpp" #include @@ -110,7 +111,7 @@ class EventHandler virtual void OnTableEnd() = 0; }; -void ReadAllTables(std::string_view database, std::string_view schema, EventHandler& eventHandler); +LIGHTWEIGHT_API void ReadAllTables(std::string_view database, std::string_view schema, EventHandler& eventHandler); struct Table { @@ -124,12 +125,12 @@ struct Table using TableList = std::vector; -TableList ReadAllTables(std::string_view database, std::string_view schema = {}); +LIGHTWEIGHT_API TableList ReadAllTables(std::string_view database, std::string_view schema = {}); } // namespace SqlSchema template <> -struct std::formatter: std::formatter +struct LIGHTWEIGHT_API std::formatter: std::formatter { auto format(SqlSchema::FullyQualifiedTableName const& value, format_context& ctx) const -> format_context::iterator { @@ -146,7 +147,7 @@ struct std::formatter: std::formatter -struct std::formatter: std::formatter +struct LIGHTWEIGHT_API std::formatter: std::formatter { auto format(SqlSchema::FullyQualifiedTableColumn const& value, format_context& ctx) const -> format_context::iterator @@ -160,7 +161,7 @@ struct std::formatter: std::formatter -struct std::formatter: std::formatter +struct LIGHTWEIGHT_API std::formatter: std::formatter { auto format(SqlSchema::FullyQualifiedTableColumnSequence const& value, format_context& ctx) const -> format_context::iterator diff --git a/src/Lightweight/SqlScopedTraceLogger.hpp b/src/Lightweight/SqlScopedTraceLogger.hpp index 7d97c7f7..be5543aa 100644 --- a/src/Lightweight/SqlScopedTraceLogger.hpp +++ b/src/Lightweight/SqlScopedTraceLogger.hpp @@ -2,13 +2,14 @@ #pragma once +#include "Api.hpp" #include "SqlConnection.hpp" #include "SqlStatement.hpp" #include // TODO: move to public API -class SqlScopedTraceLogger +class LIGHTWEIGHT_API SqlScopedTraceLogger { SQLHDBC m_nativeConnection; diff --git a/src/Lightweight/SqlStatement.cpp b/src/Lightweight/SqlStatement.cpp index 8b29c7fb..384b9f20 100644 --- a/src/Lightweight/SqlStatement.cpp +++ b/src/Lightweight/SqlStatement.cpp @@ -3,16 +3,56 @@ #include "SqlQuery.hpp" #include "SqlStatement.hpp" -SqlStatement::SqlStatement() noexcept: - m_ownedConnection { SqlConnection() }, - m_connection { &m_ownedConnection.value() } +struct SqlStatement::Data { + std::optional ownedConnection; // The connection object (if owned) + std::vector indicators; // Holds the indicators for the bound output columns + std::vector> postExecuteCallbacks; + std::vector> postProcessOutputColumnCallbacks; +}; + +void SqlStatement::RequireIndicators() +{ + auto const count = NumColumnsAffected() + 1; + if (m_data->indicators.size() <= count) + m_data->indicators.resize(count + 1); +} + +SQLLEN* SqlStatement::GetIndicatorForColumn(SQLUSMALLINT column) noexcept +{ + return &m_data->indicators[column]; +} + +void SqlStatement::PlanPostExecuteCallback(std::function&& cb) +{ + m_data->postExecuteCallbacks.emplace_back(std::move(cb)); +} + +void SqlStatement::ProcessPostExecuteCallbacks() +{ + for (auto& cb: m_data->postExecuteCallbacks) + cb(); + m_data->postExecuteCallbacks.clear(); +} + +void SqlStatement::PlanPostProcessOutputColumn(std::function&& cb) +{ + m_data->postProcessOutputColumnCallbacks.emplace_back(std::move(cb)); +} + +SqlStatement::SqlStatement(): + m_data { new Data() } +{ + m_data->ownedConnection = SqlConnection(); + m_connection = &*m_data->ownedConnection; + if (m_connection->NativeHandle()) RequireSuccess(SQLAllocHandle(SQL_HANDLE_STMT, m_connection->NativeHandle(), &m_hStmt)); } // Construct a new SqlStatement object, using the given connection. SqlStatement::SqlStatement(SqlConnection& relatedConnection): + m_data { new Data() }, m_connection { &relatedConnection } { RequireSuccess(SQLAllocHandle(SQL_HANDLE_STMT, m_connection->NativeHandle(), &m_hStmt)); @@ -21,6 +61,7 @@ SqlStatement::SqlStatement(SqlConnection& relatedConnection): SqlStatement::~SqlStatement() noexcept { SQLFreeHandle(SQL_HANDLE_STMT, m_hStmt); + delete m_data; } void SqlStatement::Prepare(std::string_view query) @@ -29,8 +70,8 @@ void SqlStatement::Prepare(std::string_view query) m_preparedQuery = std::string(query); - m_postExecuteCallbacks.clear(); - m_postProcessOutputColumnCallbacks.clear(); + m_data->postExecuteCallbacks.clear(); + m_data->postProcessOutputColumnCallbacks.clear(); // Closes the cursor if it is open RequireSuccess(SQLFreeStmt(m_hStmt, SQL_CLOSE)); @@ -38,7 +79,7 @@ void SqlStatement::Prepare(std::string_view query) // Prepares the statement RequireSuccess(SQLPrepareA(m_hStmt, (SQLCHAR*) query.data(), (SQLINTEGER) query.size())); RequireSuccess(SQLNumParams(m_hStmt, &m_expectedParameterCount)); - m_indicators.resize(m_expectedParameterCount + 1); + m_data->indicators.resize(m_expectedParameterCount + 1); } void SqlStatement::ExecuteDirect(const std::string_view& query, std::source_location location) @@ -105,9 +146,9 @@ bool SqlStatement::FetchRow() default: RequireSuccess(sqlResult); // post-process the output columns, if needed - for (auto const& postProcess: m_postProcessOutputColumnCallbacks) + for (auto const& postProcess: m_data->postProcessOutputColumnCallbacks) postProcess(); - m_postProcessOutputColumnCallbacks.clear(); + m_data->postProcessOutputColumnCallbacks.clear(); return true; } } diff --git a/src/Lightweight/SqlStatement.hpp b/src/Lightweight/SqlStatement.hpp index f090107b..4ddb20cb 100644 --- a/src/Lightweight/SqlStatement.hpp +++ b/src/Lightweight/SqlStatement.hpp @@ -7,7 +7,9 @@ #include #endif +#include "Api.hpp" #include "SqlConnection.hpp" +#include "SqlQuery.hpp" #include "SqlDataBinder.hpp" #include @@ -38,11 +40,11 @@ concept SqlQueryObject = requires(QueryObject const& queryObject) // 3. Execute the statement (optionally with input parameters) // 4. Fetch rows (if any) // 5. Repeat steps 3 and 4 as needed -class SqlStatement final: public SqlDataBinderCallback +class LIGHTWEIGHT_API SqlStatement final: public SqlDataBinderCallback { public: // Construct a new SqlStatement object, using a new connection, and connect to the default database. - SqlStatement() noexcept; + SqlStatement(); SqlStatement(SqlStatement&&) noexcept = default; SqlStatement& operator=(SqlStatement&&) noexcept = default; @@ -59,7 +61,7 @@ class SqlStatement final: public SqlDataBinderCallback [[nodiscard]] SqlConnection const& Connection() const noexcept; // Creates a new query builder for the given table, compatible with the SQL server being connected. - [[nodiscard]] SqlQueryBuilder Query(std::string_view const& table = {}) const; + SqlQueryBuilder Query(std::string_view const& table = {}) const; // Creates a new query builder for the given table with an alias, compatible with the SQL server being connected. [[nodiscard]] SqlQueryBuilder QueryAs(std::string_view const& table, std::string_view const& tableAlias) const; @@ -161,16 +163,16 @@ class SqlStatement final: public SqlDataBinderCallback void PlanPostProcessOutputColumn(std::function&& cb) override; void ProcessPostExecuteCallbacks(); - // private data members + void RequireIndicators(); + SQLLEN* GetIndicatorForColumn(SQLUSMALLINT column) noexcept; - std::optional m_ownedConnection; // The connection object (if owned) + // private data members + struct Data; + Data* m_data; SqlConnection* m_connection {}; // Pointer to the connection object SQLHSTMT m_hStmt {}; // The native oDBC statement handle std::string m_preparedQuery; // The last prepared query SQLSMALLINT m_expectedParameterCount {}; // The number of parameters expected by the query - std::vector m_indicators; // Holds the indicators for the bound output columns - std::vector> m_postExecuteCallbacks; - std::vector> m_postProcessOutputColumnCallbacks; }; // {{{ inline implementation @@ -197,20 +199,18 @@ inline void SqlStatement::Prepare(SqlQueryObject auto const& queryObject) template void SqlStatement::BindOutputColumns(Args*... args) { - auto const numColumns = NumColumnsAffected(); - m_indicators.resize(numColumns + 1); + RequireIndicators(); SQLUSMALLINT i = 0; - ((++i, SqlDataBinder::OutputColumn(m_hStmt, i, args, &m_indicators[i], *this)), ...); + ((++i, SqlDataBinder::OutputColumn(m_hStmt, i, args, GetIndicatorForColumn(i), *this)), ...); } template void SqlStatement::BindOutputColumn(SQLUSMALLINT columnIndex, T* arg) { - if (m_indicators.size() <= columnIndex) - m_indicators.resize(NumColumnsAffected() + 1); + RequireIndicators(); - SqlDataBinder::OutputColumn(m_hStmt, columnIndex, arg, &m_indicators[columnIndex], *this); + SqlDataBinder::OutputColumn(m_hStmt, columnIndex, arg, GetIndicatorForColumn(columnIndex), *this); } template @@ -362,30 +362,15 @@ template return { std::move(result) }; } -inline void SqlStatement::PlanPostExecuteCallback(std::function&& cb) -{ - m_postExecuteCallbacks.emplace_back(std::move(cb)); -} - -inline void SqlStatement::ProcessPostExecuteCallbacks() -{ - for (auto& cb: m_postExecuteCallbacks) - cb(); - m_postExecuteCallbacks.clear(); -} - -inline void SqlStatement::PlanPostProcessOutputColumn(std::function&& cb) -{ - m_postProcessOutputColumnCallbacks.emplace_back(std::move(cb)); -} - -inline void SqlStatement::ExecuteDirect(SqlQueryObject auto const& query, std::source_location location) +inline LIGHTWEIGHT_FORCE_INLINE void SqlStatement::ExecuteDirect(SqlQueryObject auto const& query, + std::source_location location) { return ExecuteDirect(query.ToSql(), location); } template -std::optional SqlStatement::ExecuteDirectSingle(const std::string_view& query, std::source_location location) +inline LIGHTWEIGHT_FORCE_INLINE std::optional SqlStatement::ExecuteDirectSingle(const std::string_view& query, + std::source_location location) { ExecuteDirect(query, location); if (FetchRow()) @@ -394,8 +379,8 @@ std::optional SqlStatement::ExecuteDirectSingle(const std::string_view& query } template -inline std::optional SqlStatement::ExecuteDirectSingle(SqlQueryObject auto const& query, - std::source_location location) +inline LIGHTWEIGHT_FORCE_INLINE std::optional SqlStatement::ExecuteDirectSingle(SqlQueryObject auto const& query, + std::source_location location) { return ExecuteDirectSingle(query.ToSql(), location); } diff --git a/src/Lightweight/SqlTraits.hpp b/src/Lightweight/SqlTraits.hpp index 0b6fa396..e325f528 100644 --- a/src/Lightweight/SqlTraits.hpp +++ b/src/Lightweight/SqlTraits.hpp @@ -2,6 +2,8 @@ #pragma once +#include "Api.hpp" + #include #include #include @@ -159,7 +161,7 @@ inline SqlTraits const& GetSqlTraits(SqlServerType serverType) noexcept } template <> -struct std::formatter: std::formatter +struct LIGHTWEIGHT_API std::formatter: std::formatter { auto format(SqlServerType type, format_context& ctx) const -> format_context::iterator { diff --git a/src/Lightweight/SqlTransaction.hpp b/src/Lightweight/SqlTransaction.hpp index 0160ca25..6d76ef2d 100644 --- a/src/Lightweight/SqlTransaction.hpp +++ b/src/Lightweight/SqlTransaction.hpp @@ -30,7 +30,7 @@ enum class SqlTransactionMode // // This class is designed with RAII in mind, so that the transaction is automatically committed or rolled back // when the object goes out of scope. -class SqlTransaction +class LIGHTWEIGHT_API SqlTransaction { public: // Construct a new SqlTransaction object, and disable the auto-commit mode, so that the transaction can be diff --git a/src/Lightweight/SqlUtils.hpp b/src/Lightweight/SqlUtils.hpp index 2cb3c7a3..c3031b64 100644 --- a/src/Lightweight/SqlUtils.hpp +++ b/src/Lightweight/SqlUtils.hpp @@ -6,20 +6,119 @@ #include #endif -#include "SqlConnection.hpp" -#include "SqlError.hpp" -#include "SqlStatement.hpp" +#include "Api.hpp" +#include -#include -#include -#include -#include +namespace detail +{ + +template +struct LIGHTWEIGHT_API DefaultDeleter +{ + void operator()(T* object) + { + delete object; + } +}; -namespace SqlUtils +// We cannot use std::unique_ptr in Windows DLLs as struct/class members, +// because of how Windows handles DLLs and the potential of incompatibilities (See C4251) +template > +class LIGHTWEIGHT_API UniquePtr { + public: + explicit UniquePtr() noexcept = default; + + explicit UniquePtr(std::nullptr_t) noexcept: + _value { nullptr } + { + } + + explicit UniquePtr(T* value, Deleter deleter): + _value { value }, + _deleter { deleter } + { + } + + UniquePtr(UniquePtr const&) = delete; + UniquePtr& operator=(UniquePtr const&) = delete; + + UniquePtr(UniquePtr&& source) noexcept: + _value { source._value }, + _deleter { std::move(source._deleter) } + { + } + + UniquePtr& operator=(UniquePtr&& source) noexcept + { + _value = source._value; + _deleter = std::move(source._deleter); + return *this; + } + + ~UniquePtr() + { + _deleter(_value); + } + + T* release() noexcept + { + T* oldValue = _value; + _value = nullptr; + return oldValue; + } -std::vector TableNames(std::string_view database, std::string_view schema = {}); + void reset(T* newValue) noexcept + { + if (_value) + delete _value; + _value = newValue; + } -std::vector ColumnNames(std::string_view tableName, std::string_view schema = {}); + T* get() noexcept + { + return _value; + } + + T const* get() const noexcept + { + return _value; + } + + T& operator*() noexcept + { + return *_value; + } + + T const& operator*() const noexcept + { + return *_value; + } + + T& operator->() noexcept + { + return *_value; + } + + T const& operator->() const noexcept + { + return *_value; + } + + explicit operator bool() const noexcept + { + return _value != nullptr; + } + + private: + T* _value = nullptr; + Deleter _deleter = Deleter {}; +}; + +template +UniquePtr MakeUnique(Args&&... args) +{ + return UniquePtr(new T(std::forward(args)...)); +} -} // namespace SqlUtils +} // namespace detail diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index b41f776a..7a6c5c43 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -12,12 +12,16 @@ endif() target_link_libraries(LightweightTest PRIVATE ${TEST_LIBRARIES}) -target_sources(LightweightTest PRIVATE - CoreTests.cpp - ModelTests.cpp - ModelAssociationsTests.cpp - UnicodeConverterTests.cpp -) +set(SOURCE_FILES CoreTests.cpp UnicodeConverterTests.cpp) + +if(LIGHTWEIGHT_ORM) + target_compile_definitions(LightweightTest PRIVATE LIGHTWEIGHT_ORM) + list(APPEND SOURCE_FILES ModelTests.cpp ModelAssociationsTests.cpp) +endif() + +target_sources(LightweightTest PRIVATE ${SOURCE_FILES}) enable_testing() add_test(NAME LightweightTest COMMAND LightweightTest) + +install(TARGETS LightweightTest DESTINATION bin) diff --git a/src/tests/CoreTests.cpp b/src/tests/CoreTests.cpp index b58f3ad2..6637a74a 100644 --- a/src/tests/CoreTests.cpp +++ b/src/tests/CoreTests.cpp @@ -37,14 +37,6 @@ int main(int argc, char** argv) std::tie(argc, argv) = std::get(result); - struct finally - { - ~finally() - { - SqlLogger::GetLogger().OnStats(SqlConnection::Stats()); - } - } _; - return Catch::Session().run(argc, argv); } @@ -401,31 +393,6 @@ TEST_CASE_METHOD(SqlTestFixture, "SqlStatement.ExecuteBatchNative") REQUIRE(!stmt.FetchRow()); } -TEST_CASE_METHOD(SqlTestFixture, "connection pool reusage", "[sql]") -{ - // auto-instanciating an SqlConnection - auto const id1 = [] { - auto connection = SqlConnection {}; - return connection.ConnectionId(); - }(); - - // Explicitly passing a borrowed SqlConnection - auto const id2 = [] { - auto conn = SqlConnection {}; - auto stmt = SqlStatement { conn }; - return stmt.Connection().ConnectionId(); - }(); - CHECK(id1 == id2); - - // &&-created SqlConnections are reused - auto const id3 = SqlConnection().ConnectionId(); - CHECK(id1 == id3); - - // Explicit constructor passing SqlConnectInfo always creates a new SqlConnection - auto const id4 = SqlConnection(SqlConnection::DefaultConnectInfo()).ConnectionId(); - CHECK(id1 != id4); -} - TEST_CASE_METHOD(SqlTestFixture, "SqlConnection: manual connect") { auto conn = SqlConnection { std::nullopt }; diff --git a/src/tests/ModelAssociationsTests.cpp b/src/tests/ModelAssociationsTests.cpp deleted file mode 100644 index 14841023..00000000 --- a/src/tests/ModelAssociationsTests.cpp +++ /dev/null @@ -1,428 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include "../Lightweight/Model/All.hpp" -#include "../Lightweight/SqlConnection.hpp" -#include "Utils.hpp" - -#include -#include - -struct Artist; -struct Track; -struct Publisher; - -struct Artist: Model::Record -{ - Model::Field name; - Model::HasMany tracks; - - Artist(): - Record { "artists" }, - name { *this }, - tracks { *this } - { - } - - Artist(Artist&& other) noexcept: - Record { std::move(other) }, - name { *this, std::move(other.name) }, - tracks { *this, std::move(other.tracks) } - { - } -}; - -struct Track: Model::Record -{ - Model::Field title; - Model::BelongsTo artist; - - Track(): - Record { "tracks" }, - title { *this }, - artist { *this } - { - } - - Track(Track&& other) noexcept: - Record { std::move(other) }, - title { *this, std::move(other.title) }, - artist { *this, std::move(other.artist) } - { - } -}; - -TEST_CASE_METHOD(SqlTestFixture, "Model.BelongsTo", "[model]") -{ - CreateModelTable(); - CreateModelTable(); - - Artist artist; - artist.name = "Snoop Dog"; - artist.Save(); - REQUIRE(artist.Id().value); - - Track track1; - track1.title = "Wuff"; - track1.artist = artist; // track1 "BelongsTo" artist - track1.Save(); - REQUIRE(track1.Id().value); - - CHECK(track1.artist->Inspect() == artist.Inspect()); - - artist.Destroy(); - CHECK(Artist::Count() == 0); - CHECK(Track::Count() == 0); - // Destroying the artist must also destroy the track, due to the foreign key constraint. -} - -TEST_CASE_METHOD(SqlTestFixture, "Model.HasMany", "[model]") -{ - CreateModelTable(); - CreateModelTable(); - - Artist artist; - artist.name = "Snoop Dog"; - artist.Save(); - - Track track1; - track1.title = "Wuff"; - track1.artist = artist; - track1.Save(); - - Track track2; - track2.title = "Paff Dog"; - track2.artist = artist; - track2.Save(); - - REQUIRE(artist.tracks.IsLoaded() == false); - REQUIRE(artist.tracks.IsEmpty() == false); - REQUIRE(artist.tracks.Count() == 2); - artist.tracks.Load(); - REQUIRE(artist.tracks.IsLoaded() == true); - REQUIRE(artist.tracks.Count() == 2); // Using cached value - REQUIRE(artist.tracks[0].Inspect() == track1.Inspect()); - REQUIRE(artist.tracks[1].Inspect() == track2.Inspect()); -} - -TEST_CASE_METHOD(SqlTestFixture, "Model.HasOne", "[model]") -{ - struct Suppliers; - struct Account; - - struct Suppliers: Model::Record - { - Model::Field name; - Model::HasOne account; - - Suppliers(): - Record { "suppliers" }, - name { *this }, - account { *this } - { - } - - Suppliers(Suppliers&& other) noexcept: - Record { std::move(other) }, - name { *this, std::move(other.name) }, - account { *this, std::move(other.account) } - { - } - }; - - struct Account: Model::Record - { - Model::Field iban; - Model::BelongsTo supplier; - - Account(): - Record { "accounts" }, - iban { *this }, - supplier { *this } - { - } - - Account(Account&& other) noexcept: - Record { std::move(other) }, - iban { *this, std::move(other.iban) }, - supplier { *this, std::move(other.supplier) } - { - } - }; - - CreateModelTable(); - CreateModelTable(); - - Suppliers supplier; - supplier.name = "Supplier"; - supplier.Save(); - - Account account; - account.iban = "DE123456789"; - account.supplier = supplier; - account.Save(); - - REQUIRE(supplier.account.IsLoaded() == false); - supplier.account.Load(); - REQUIRE(supplier.account.IsLoaded() == true); - REQUIRE(supplier.account->Inspect() == account.Inspect()); -} - -TEST_CASE_METHOD(SqlTestFixture, "Model.HasOneThrough", "[model]") -{ - // {{{ models - struct Suppliers; - struct Account; - struct AccountHistory; - - struct Suppliers: Model::Record - { - Model::HasOne account; - Model::HasOneThrough accountHistory; - Model::Field name; - - // {{{ ctors - Suppliers(): - Record { "suppliers" }, - account { *this }, - accountHistory { *this }, - name { *this } - { - } - - Suppliers(Suppliers&& other) noexcept: - Record { std::move(other) }, - account { *this, std::move(other.account) }, - accountHistory { *this, std::move(other.accountHistory) }, - name { *this, std::move(other.name) } - { - } - // }}} - }; - - struct Account: Model::Record - { - Model::Field iban; - Model::BelongsTo supplier; - Model::HasOne accountHistory; - - // {{{ ctors - Account(): - Record { "accounts" }, - iban { *this }, - supplier { *this }, - accountHistory { *this } - { - } - - Account(Account&& other) noexcept: - Record { std::move(other) }, - iban { *this, std::move(other.iban) }, - supplier { *this, std::move(other.supplier) }, - accountHistory { *this, std::move(other.accountHistory) } - { - } - // }}} - }; - - struct AccountHistory: Model::Record - { - Model::BelongsTo account; - Model::Field description; - - // {{{ ctors - AccountHistory(): - Record { "account_histories" }, - account { *this }, - description { *this } - { - } - - AccountHistory(AccountHistory&& other) noexcept: - Record { std::move(other) }, - account { *this, std::move(other.account) }, - description { *this, std::move(other.description) } - { - } - // }}} - }; - // }}} - - CreateModelTable(); - CreateModelTable(); - CreateModelTable(); - - Suppliers supplier; - supplier.name = "The Supplier"; - supplier.Save(); - - Account account; - account.supplier = supplier; - account.iban = "DE123456789"; - account.Save(); - - AccountHistory accountHistory; - accountHistory.account = account; - accountHistory.description = "Initial deposit"; - accountHistory.Save(); - - REQUIRE(supplier.accountHistory.IsLoaded() == false); - REQUIRE(supplier.accountHistory->Inspect() == accountHistory.Inspect()); // auto-loads the accountHistory - REQUIRE(supplier.accountHistory.IsLoaded() == true); -} - -TEST_CASE_METHOD(SqlTestFixture, "Model.HasManyThrough", "[model]") -{ - // {{{ Models - struct Physician; - struct Appointment; - struct Patient; - - struct Physician: Model::Record - { - Model::Field name; - Model::HasMany appointments; - Model::HasManyThrough patients; - - Physician(): - Record { "physicians" }, - name { *this }, - appointments { *this }, - patients { *this } - { - } - - Physician(Physician&& other) noexcept: - Record { std::move(other) }, - name { *this, std::move(other.name) }, - appointments { *this, std::move(other.appointments) }, - patients { *this, std::move(other.patients) } - { - } - }; - - struct Appointment: Model::Record - { - Model::Field date; - Model::Field comment; - Model::BelongsTo physician; - Model::BelongsTo patient; - - Appointment(): - Record { "appointments" }, - date { *this }, - comment { *this }, - physician { *this }, - patient { *this } - { - } - - Appointment(Appointment&& other) noexcept: - Record { std::move(other) }, - date { *this, std::move(other.date) }, - comment { *this, std::move(other.comment) }, - physician { *this, std::move(other.physician) }, - patient { *this, std::move(other.patient) } - { - } - }; - - struct Patient: Model::Record - { - Model::Field name; - Model::Field comment; - Model::HasMany appointments; - Model::HasManyThrough physicians; - - Patient(): - Record { "patients" }, - name { *this }, - comment { *this }, - appointments { *this }, - physicians { *this } - { - } - - Patient(Patient&& other) noexcept: - Record { std::move(other) }, - name { *this, std::move(other.name) }, - comment { *this, std::move(other.comment) }, - appointments { *this, std::move(other.appointments) }, - physicians { *this, std::move(other.physicians) } - { - } - }; - // }}} - - CreateModelTable(); - CreateModelTable(); - CreateModelTable(); - - Physician physician1; - physician1.name = "Dr. House"; - physician1.Save(); - - Physician physician2; - physician2.name = "Granny"; - physician2.Save(); - - Patient patient1; - patient1.name = "Blooper"; - patient1.comment = "Prefers morning times"; - patient1.Save(); - - Patient patient2; - patient2.name = "Valentine"; - patient2.comment = "always friendly"; - patient2.Save(); - - Appointment patient1Apointment1; - patient1Apointment1.date = SqlDateTime::Now(); - patient1Apointment1.patient = patient1; - patient1Apointment1.physician = physician2; - patient1Apointment1.comment = "Patient is a bit nervous"; - patient1Apointment1.Save(); - - Appointment patient1Apointment2; - patient1Apointment2.date = SqlDateTime::Now(); - patient1Apointment2.patient = patient1; - patient1Apointment2.physician = physician1; - patient1Apointment2.comment = "Patient is a bit nervous, again"; - patient1Apointment2.Save(); - - Appointment patient2Apointment1; - patient2Apointment1.date = SqlDateTime::Now(); - patient2Apointment1.patient = patient2; - patient2Apointment1.physician = physician1; - patient2Apointment1.comment = "Patient is funny"; - patient2Apointment1.Save(); - - auto const queriedCount = physician1.patients.Count(); - CHECK(queriedCount == 2); - - auto const& physician1Patients = physician1.patients.All(); - REQUIRE(physician1Patients.size() == 2); - CHECK(physician1Patients.at(0).Inspect() == patient1.Inspect()); - CHECK(physician1Patients.at(1).Inspect() == patient2.Inspect()); - - CHECK(patient1.physicians.Count() == 2); - CHECK(patient2.physicians.Count() == 1); - - // Test Each() method - size_t numPatientsIterated = 0; - std::vector retrievedPatients; - physician2.patients.Each([&](Patient& patient) { - // NB: Mind, SQLite does not seem to like issuing another query on the memory database while - // we are currently fetching the results via the Each() call. So we moved the results to a - // vector and then inspect them. - REQUIRE(numPatientsIterated == 0); - ++numPatientsIterated; - retrievedPatients.emplace_back(std::move(patient)); - }); - Patient const& patient = retrievedPatients.at(0); - CHECK(patient.Inspect() == patient1.Inspect()); // Blooper - CHECK(patient.comment.Value() == "Prefers morning times"); - CHECK(patient.physicians.Count() == 2); - CHECK(patient.physicians.IsLoaded() == false); - CHECK(patient.physicians[0].name.Value() == "Granny"); - CHECK(patient.physicians[0].Inspect() == physician2.Inspect()); -} diff --git a/src/tests/ModelTests.cpp b/src/tests/ModelTests.cpp deleted file mode 100644 index 8accbb44..00000000 --- a/src/tests/ModelTests.cpp +++ /dev/null @@ -1,355 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include "Utils.hpp" - -#include -#include - -#include -#include - -using namespace std::string_view_literals; - -struct Author; -struct Book; -struct Company; -struct Job; -struct Person; -struct Phone; - -TEST_CASE_METHOD(SqlTestFixture, "Model.Move", "[model]") -{ - struct MovableRecord: public Model::Record - { - Model::Field name; - - MovableRecord(): - Record { "movables" }, - name { *this } - { - } - - MovableRecord(MovableRecord&& other) noexcept: - Record { std::move(other) }, - name { *this, std::move(other.name) } - { - } - }; - - // Ensure move constructor is working as expected. - // Inspect() touches the most internal data structures, so we use this call to verify. - - CreateModelTable(); - - MovableRecord record; - record.name = "Foxy Fox"; - record.Save(); - auto const originalText = record.Inspect(); - INFO("Original: " << originalText); - - MovableRecord movedRecord(std::move(record)); - auto const movedText = movedRecord.Inspect(); - REQUIRE(movedText == originalText); -} - -TEST_CASE_METHOD(SqlTestFixture, "Model.Field: SqlTrimmedString", "[model]") -{ - struct TrimmedStringRecord: Model::Record - { - Model::Field name; - - TrimmedStringRecord(): - Record { "trimmed_strings" }, - name { *this } - { - } - - TrimmedStringRecord(TrimmedStringRecord&& other) noexcept: - Record { std::move(other) }, - name { *this, std::move(other.name) } - { - } - }; - - CreateModelTable(); - - TrimmedStringRecord record; - record.name = SqlTrimmedString { " Hello, World! " }; - record.Save(); - record.Reload(); // Ensure we fetch name from the database and got trimmed on fetch. - - CHECK(record.name == SqlTrimmedString { " Hello, World!" }); -} - -struct Author: Model::Record -{ - Model::Field name; - Model::HasMany books; - - Author(): - Record { "authors" }, - name { *this }, - books { *this } - { - } - - Author(Author&& other) noexcept: - Record { std::move(other) }, - name { *this, std::move(other.name) }, - books { *this, std::move(other.books) } - { - } -}; - -struct Book: Model::Record -{ - Model::Field, 2, "title"> title; - Model::Field isbn; - Model::BelongsTo author; - - Book(): - Record { "books", "id" }, - title { *this }, - isbn { *this }, - author { *this } - { - } - - Book(Book&& other) noexcept: - Record { std::move(other) }, - title { *this, std::move(other.title) }, - isbn { *this, std::move(other.isbn) }, - author { *this, std::move(other.author) } - { - } -}; - -TEST_CASE_METHOD(SqlTestFixture, "Model.Create", "[model]") -{ - CreateModelTable(); - CreateModelTable(); - - Author author; - author.name = "Bjarne Stroustrup"; - author.Save(); - REQUIRE(author.Id() == 1); - REQUIRE(author.books.Count() == 0); - - Book book1; - book1.title = "The C++ Programming Language"; - book1.isbn = "978-0-321-56384-2"; - book1.author = author; - book1.Save(); - REQUIRE(book1.Id() == 1); - REQUIRE(Book::Count() == 1); - REQUIRE(author.books.Count() == 1); - - Book book2; - book2.title = "A Tour of C++"; - book2.isbn = "978-0-321-958310"; - book2.author = author; - book2.Save(); - REQUIRE(book2.Id() == 2); - REQUIRE(Book::Count() == 2); - REQUIRE(author.books.Count() == 2); - - // Also take the chance to ensure the formatter works. - REQUIRE(std::format("{}", author) == author.Inspect()); -} - -TEST_CASE_METHOD(SqlTestFixture, "Model.Load", "[model]") -{ - Model::CreateSqlTables(); - - Author author; - author.name = "Bjarne Stroustrup"; - author.Save(); - - Book book; - book.title = "The C++ Programming Language"; - book.isbn = "978-0-321-56384-2"; - book.author = author; - book.Save(); - - Book bookLoaded; - bookLoaded.Load(book.Id()); - INFO("Book: " << book); - CHECK(bookLoaded.Id() == book.Id()); - CHECK(bookLoaded.title == book.title); - CHECK(bookLoaded.isbn == book.isbn); - CHECK(bookLoaded.author == book.author); -} - -TEST_CASE_METHOD(SqlTestFixture, "Model.Find", "[model]") -{ - Model::CreateSqlTables(); - - Author author; - author.name = "Bjarne Stroustrup"; - author.Save(); - - Book book; - book.title = "The C++ Programming Language"; - book.isbn = "978-0-321-56384-2"; - book.author = author; - book.Save(); - - Book bookLoaded = Book::Find(book.Id()).value(); - INFO("Book: " << book); - CHECK(bookLoaded.Id() == book.Id()); // primary key - CHECK(bookLoaded.title == book.title); // Field<> - CHECK(bookLoaded.isbn == book.isbn); // Field<> - CHECK(bookLoaded.author == book.author); // BelongsTo<> -} - -TEST_CASE_METHOD(SqlTestFixture, "Model.Update", "[model]") -{ - Model::CreateSqlTables(); - - Author author; - author.name = "Bjarne Stroustrup"; - author.Save(); - - Book book; - book.title = "The C++ Programming Language"; - book.isbn = "978-0-321-56384-2"; - book.author = author; - book.Save(); - - book.isbn = "978-0-321-958310"; - book.Save(); - - Book bookRead = Book::Find(book.Id()).value(); - CHECK(bookRead.Id() == book.Id()); - CHECK(bookRead.title == book.title); - CHECK(bookRead.isbn == book.isbn); -} - -TEST_CASE_METHOD(SqlTestFixture, "Model.Destroy", "[model]") -{ - CreateModelTable(); - - Author author1; - author1.name = "Bjarne Stroustrup"; - author1.Save(); - REQUIRE(Author::Count() == 1); - - Author author2; - author2.name = "John Doe"; - author2.Save(); - REQUIRE(Author::Count() == 2); - - author1.Destroy(); - REQUIRE(Author::Count() == 1); -} - -TEST_CASE_METHOD(SqlTestFixture, "Model.All", "[model]") -{ - CreateModelTable(); - - Author author1; - author1.name = "Bjarne Stroustrup"; - author1.Save(); - - Author author2; - author2.name = "John Doe"; - author2.Save(); - - Author author3; - author3.name = "Some very long name"; - author3.Save(); - - Author author4; - author4.name = "Shorty"; - author4.Save(); - - auto authors = Author::All(); - REQUIRE(authors.size() == 4); - CHECK(authors[0].name == author1.name); - CHECK(authors[1].name == author2.name); - CHECK(authors[2].name == author3.name); - CHECK(authors[3].name == author4.name); -} - -struct ColumnTypesRecord: Model::Record -{ - Model::Field stringColumn; - Model::Field textColumn; - - ColumnTypesRecord(): - Record { "column_types" }, - stringColumn { *this }, - textColumn { *this } - { - } - - ColumnTypesRecord(ColumnTypesRecord&& other) noexcept: - Record { std::move(other) }, - stringColumn { *this, std::move(other.stringColumn) }, - textColumn { *this, std::move(other.textColumn) } - { - } -}; - -TEST_CASE_METHOD(SqlTestFixture, "Model.ColumnTypes", "[model]") -{ - CreateModelTable(); - - ColumnTypesRecord record; - record.stringColumn = "Hello"; - record.textColumn = SqlText { ", World!" }; - record.Save(); - - ColumnTypesRecord record2 = ColumnTypesRecord::Find(record.Id()).value(); - CHECK(record2.stringColumn == record.stringColumn); - CHECK(record2.textColumn == record.textColumn); -} - -struct Employee: Model::Record -{ - Model::Field name; - Model::Field isSenior; - - Employee(): - Record { "employees" }, - name { *this }, - isSenior { *this } - { - } - - Employee(Employee&& other) noexcept: - Record { std::move(other) }, - name { *this, std::move(other.name) }, - isSenior { *this, std::move(other.isSenior) } - { - } -}; - -TEST_CASE_METHOD(SqlTestFixture, "Model.Where", "[model]") -{ - CreateModelTable(); - - Employee employee1; - employee1.name = "John Doe"; - employee1.isSenior = false; - employee1.Save(); - - Employee employee2; - employee2.name = "Jane Doe"; - employee2.isSenior = true; - employee2.Save(); - - Employee employee3; - employee3.name = "John Smith"; - employee3.isSenior = true; - employee3.Save(); - - auto employees = Employee::Where("is_senior"sv, true).All(); - for (const auto& employee: employees) - INFO("Employee: {}" << employee); // FIXME: breaks due to field name being NULL - REQUIRE(employees.size() == 2); - CHECK(employees[0].Id() == employee2.Id()); - CHECK(employees[0].name == employee2.name); - CHECK(employees[1].Id() == employee3.Id()); - CHECK(employees[1].name == employee3.name); -} diff --git a/src/tests/Utils.hpp b/src/tests/Utils.hpp index baca40bd..70d6a642 100644 --- a/src/tests/Utils.hpp +++ b/src/tests/Utils.hpp @@ -6,7 +6,10 @@ #include #endif -#include "../Lightweight/Model/All.hpp" +#if defined(LIGHTWEIGHT_ORM) + #include "../Lightweight/Model/All.hpp" +#endif + #include "../Lightweight/SqlConnectInfo.hpp" #include "../Lightweight/SqlConnection.hpp" #include "../Lightweight/SqlDataBinder.hpp" @@ -76,7 +79,6 @@ class ScopedSqlNullLogger: public SqlLogger void OnExecute(std::string_view const&) override {} void OnExecuteBatch() override {} void OnFetchedRow() override {} - void OnStats(SqlConnectionStats const&) override {} }; class SqlTestFixture @@ -94,8 +96,10 @@ class SqlTestFixture { if (argv[i] == "--trace-sql"sv) SqlLogger::SetLogger(SqlLogger::TraceLogger()); +#if defined(LIGHTWEIGHT_ORM) else if (argv[i] == "--trace-model"sv) Model::QueryLogger::Set(Model::QueryLogger::StandardLogger()); +#endif else if (argv[i] == "--help"sv || argv[i] == "-h"sv) { std::println("{} [--trace-sql] [--trace-model] [[--] [Catch2 flags ...]]", argv[0]); @@ -167,14 +171,11 @@ class SqlTestFixture { REQUIRE(SqlConnection().IsAlive()); DropAllTablesInDatabase(); - SqlConnection::KillAllIdle(); } - virtual ~SqlTestFixture() - { - SqlConnection::KillAllIdle(); - } + virtual ~SqlTestFixture() = default; +#if defined(LIGHTWEIGHT_ORM) template void CreateModelTable() { @@ -182,6 +183,7 @@ class SqlTestFixture m_createdTables.emplace_back(tableName); T::CreateTable(); } +#endif private: static std::string SanitizePwd(std::string_view input) @@ -226,7 +228,6 @@ class SqlTestFixture switch (stmt.Connection().ServerType()) { case SqlServerType::MICROSOFT_SQL: - SqlConnection::KillAllIdle(); (void) stmt.ExecuteDirect(std::format("USE {}", "master")); (void) stmt.ExecuteDirect(std::format("DROP DATABASE IF EXISTS \"{}\"", testDatabaseName)); (void) stmt.ExecuteDirect(std::format("CREATE DATABASE \"{}\"", testDatabaseName)); @@ -250,6 +251,7 @@ class SqlTestFixture }; // {{{ ostream support for Lightweight, for debugging purposes +#if defined(LIGHTWEIGHT_ORM) inline std::ostream& operator<<(std::ostream& os, Model::RecordId value) { return os << "ModelId { " << value.value << " }"; @@ -259,6 +261,7 @@ inline std::ostream& operator<<(std::ostream& os, Model::AbstractRecord const& v { return os << std::format("{}", value); } +#endif inline std::ostream& operator<<(std::ostream& os, SqlTrimmedString const& value) { @@ -307,6 +310,7 @@ inline std::ostream& operator<<(std::ostream& os, SqlFixedString c return os << std::format("SqlTrimmedFixedString<{}> {{ '{}' }}", N, value.data()); } +#if defined(LIGHTWEIGHT_ORM) template ", TheTableColumnIndex, TheColumnName.value, field.Value()); } +#endif // }}} diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index 0651df4b..55b8d59e 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -1,3 +1,4 @@ add_executable(ddl2cpp ddl2cpp.cpp) target_link_libraries(ddl2cpp PRIVATE Lightweight::Lightweight) target_compile_features(ddl2cpp PUBLIC cxx_std_23) +install(TARGETS ddl2cpp DESTINATION bin) diff --git a/src/tools/ddl2cpp.cpp b/src/tools/ddl2cpp.cpp index aca95fc0..a21fd4b0 100644 --- a/src/tools/ddl2cpp.cpp +++ b/src/tools/ddl2cpp.cpp @@ -373,8 +373,6 @@ int main(int argc, char const* argv[]) SqlConnection::SetDefaultConnectInfo(SqlConnectionString { std::string(config.connectionString) }); SqlConnection::SetPostConnectedHook(&PostConnectedHook); - auto const _ = finally([] { SqlConnection::KillAllIdle(); }); - if (config.createTestTables) CreateTestTables();