diff --git a/.clang-format b/.clang-format index 4d064552e..2c2d646a1 100644 --- a/.clang-format +++ b/.clang-format @@ -124,6 +124,7 @@ StatementMacros: - _Pragma - Q_UNUSED - QT_REQUIRE_VERSION +WhitespaceSensitiveMacros: [SQLITE_ORM_HAS_INCLUDE] TabWidth: 4 UseTab: Never LineEnding: LF diff --git a/.clang-format-ignore b/.clang-format-ignore new file mode 100644 index 000000000..9468717b1 --- /dev/null +++ b/.clang-format-ignore @@ -0,0 +1,2 @@ +# exclude until clang-format understands C++ reflection syntax +./dev/functional/meta_util.h diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fca085636..a029d0b8d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -130,6 +130,15 @@ jobs: compiler_package: g++-11 experimental: true + - name: "gcc-16, C++26" + os: ubuntu-24.04 + cc: gcc-16 + cxx: g++-16 + cxx_standard: "-DSQLITE_ORM_ENABLE_CXX_26=ON" + install_compiler: true + compiler_package: g++-16 + compiler_ppa: ppa:ubuntu-toolchain-r/test + name: Linux - ${{ matrix.name }} env: @@ -143,6 +152,9 @@ jobs: - name: Install compiler if: matrix.install_compiler run: | + if [ -n "${{ matrix.compiler_ppa }}" ]; then + sudo add-apt-repository -y ${{ matrix.compiler_ppa }} + fi sudo apt-get update sudo apt-get install -y ${{ matrix.compiler_package }} diff --git a/TODO.md b/TODO.md index 44da03cb2..1235976c6 100644 --- a/TODO.md +++ b/TODO.md @@ -6,7 +6,6 @@ * rest of core functions(https://sqlite.org/lang_corefunc.html) * `ATTACH` * blob incremental I/O https://sqlite.org/c3ref/blob_open.html -* CREATE VIEW and other view operations https://sqlite.org/lang_createview.html * query static check for correct order (e.g. `GROUP BY` after `WHERE`) * `SAVEPOINT` https://www.sqlite.org/lang_savepoint.html * add `static_assert` in crud `get*` functions in case user passes `where_t` instead of id to make compilation error more clear (example https://github.com/fnc12/sqlite_orm/issues/485) diff --git a/dev/ast/rank.h b/dev/ast/rank.h index 9b101f4e9..2b2fe1c10 100644 --- a/dev/ast/rank.h +++ b/dev/ast/rank.h @@ -17,7 +17,7 @@ namespace sqlite_orm::internal { SQLITE_ORM_EXPORT namespace sqlite_orm { /** - * RANK() window function / FTS5 rank keyword. + * RANK() window function * https://sqlite.org/windowfunctions.html#built-in_window_functions */ inline internal::rank_t rank() { diff --git a/dev/conditions.h b/dev/conditions.h index 81f7ded90..fce081091 100644 --- a/dev/conditions.h +++ b/dev/conditions.h @@ -27,6 +27,7 @@ #include "type_printer.h" #include "literal.h" #include "ast/cross_join.h" +#include "ast/rank.h" namespace sqlite_orm::internal { /** @@ -1016,6 +1017,14 @@ SQLITE_ORM_EXPORT namespace sqlite_orm { return {std::move(o)}; } + /** + * [Deprecation notice] This expression factory function is deprecated and will be removed in v1.11. + */ + [[deprecated("Use the hidden FTS5 rank column instead")]] + inline internal::order_by_t order_by(internal::rank_t expression) { + return {std::move(expression)}; + } + /** * ORDER BY positional ordinal * diff --git a/dev/cte_storage.h b/dev/cte_storage.h index a2344e3f2..090e244e9 100644 --- a/dev/cte_storage.h +++ b/dev/cte_storage.h @@ -84,7 +84,7 @@ namespace sqlite_orm::internal { auto make_cte_column(std::string name, const ColRef& finalColRef) { using column_type = column_t; - return column_type{std::move(name), finalColRef, empty_setter{}}; + return column_type{std::move(name), finalColRef, empty_setter{}, std::tuple<>{}}; } #ifdef SQLITE_ORM_STRUCTURED_BINDING_PACK_SUPPORTED diff --git a/dev/functional/config.h b/dev/functional/config.h index e2a3d4973..552e76638 100644 --- a/dev/functional/config.h +++ b/dev/functional/config.h @@ -90,6 +90,10 @@ #define SQLITE_ORM_WITH_CTE +#if defined(SQLITE_ORM_REFLECTION_SUPPORTED) +#define SQLITE_ORM_WITH_VIEW +#endif + // define the inline namespace "literals" so that it is available even if it was not introduced by a feature namespace sqlite_orm { inline namespace literals {} diff --git a/dev/functional/cxx_core_features.h b/dev/functional/cxx_core_features.h index 1647fbcc7..d43ca8e71 100644 --- a/dev/functional/cxx_core_features.h +++ b/dev/functional/cxx_core_features.h @@ -43,10 +43,6 @@ #define SQLITE_ORM_CLASSTYPE_TEMPLATE_ARGS_SUPPORTED #endif -#if __cpp_nontype_template_args >= 201911L -#define SQLITE_ORM_CLASSTYPE_TEMPLATE_ARGS_SUPPORTED -#endif - #if __cpp_explicit_this_parameter >= 202110L #define SQLITE_ORM_DEDUCING_THIS_SUPPORTED #endif @@ -67,6 +63,10 @@ #define SQLITE_ORM_CONTRACTS_SUPPORTED #endif +#if __cpp_impl_reflection >= 202506L +#define SQLITE_ORM_REFLECTION_SUPPORTED +#endif + #if __cplusplus >= 202002L #define SQLITE_ORM_DEFAULT_COMPARISONS_SUPPORTED #define SQLITE_ORM_INITSTMT_RANGE_BASED_FOR_SUPPORTED diff --git a/dev/functional/meta_util.h b/dev/functional/meta_util.h new file mode 100644 index 000000000..f04ae3ee4 --- /dev/null +++ b/dev/functional/meta_util.h @@ -0,0 +1,87 @@ +#pragma once + +#ifndef SQLITE_ORM_IMPORT_STD_MODULE +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED +#include // std::array +#include // std::define_static_array, std::meta::access_context, std::meta::nonstatic_data_members_of, std::meta::identifier_of, std::meta::annotations_of +#include // std::tuple +#include // std::index_sequence, std::make_index_sequence +#endif +#endif + +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED +namespace sqlite_orm::internal { + /** + * Reflects the non-static data members of `T` and its base classes + * and returns them as a fixed-size span of `std::meta::info` reflections. + */ + template + consteval auto extract_members() { + constexpr auto ctx = std::meta::access_context::current(); + + constexpr auto collect = [](this const auto& self) -> std::vector { + std::vector result; + + // Recurse into direct base classes first (preserves layout order) + template for (constexpr std::meta::info base : std::define_static_array(bases_of(^^U, ctx))) { + using base_type = typename[:type_of(base):]; + result.append_range(self.template operator()()); + } + + // Then this class's own non-static data members + result.append_range(nonstatic_data_members_of(^^U, ctx)); + + return result; + }; + + return std::define_static_array(collect.template operator()()); + } + + /** + * Returns the identifier of `T`. + */ + template + consteval auto extract_type_identifier() { + return std::meta::identifier_of(^^T); + } + + /** + * Splices a non-static data member reflection into a member-pointer expression. + * Encapsulated here so the splice operator does not leak into consumer headers. + */ + template + consteval auto splice_member_pointer() { + return &[:member:]; + } + + /** + * Splices a reflection's annotations into a tuple of values. The reflection may be + * a type or a non-static data member. + * Encapsulated here so the splice operator does not leak into consumer headers. + * + * Two P3394 details inform this implementation: + * - Annotation reflections returned by `annotations_of` are not directly spliceable; + * they must first be routed through `std::meta::constant_of`, which returns a + * splice-able constant reflection. + * - `std::meta::annotations_of` returns a `std::vector`, whose heap + * allocation is transient under C++20 constexpr rules and cannot be bound to a + * `constexpr` variable. The size and per-index lookups therefore re-call + * `annotations_of` inline so each transient vector dies within its own constant + * expression. + */ + template + consteval auto splice_annotations() { + return [](std::index_sequence) consteval { + return std::tuple{[:std::meta::constant_of(std::meta::annotations_of(refl)[I]):]...}; + }(std::make_index_sequence{}); + } + + /** + * Returns the class-scope annotations of `T` as a tuple. + */ + template + consteval auto extract_type_annotations() { + return splice_annotations<^^T>(); + } +} +#endif diff --git a/dev/functional/mpl.h b/dev/functional/mpl.h index 3d6665d05..f53a2c076 100644 --- a/dev/functional/mpl.h +++ b/dev/functional/mpl.h @@ -471,6 +471,12 @@ namespace sqlite_orm::internal { template class Op> using check_if_lacks = mpl::not_>; + /* + * Quoted metafunction that finds the index of the element having the specified trait in a tuple. + */ + template class TraitFn> + using finds_if_has = mpl::finds>; + /* * Quoted metafunction that finds the index of the given type in a tuple. */ diff --git a/dev/mapped_type_proxy.h b/dev/mapped_type_proxy.h index c59ffa4e7..8718fe5e9 100644 --- a/dev/mapped_type_proxy.h +++ b/dev/mapped_type_proxy.h @@ -6,7 +6,6 @@ #include "functional/cxx_type_traits_polyfill.h" #include "type_traits.h" -#include "table_reference.h" #include "alias_traits.h" namespace sqlite_orm::internal { diff --git a/dev/object_from_column_builder.h b/dev/object_from_column_builder.h index bcfb0c015..9b73e1892 100644 --- a/dev/object_from_column_builder.h +++ b/dev/object_from_column_builder.h @@ -8,6 +8,7 @@ #include "functional/gsl.h" #include "member_traits/member_traits.h" +#include "type_traits.h" #include "table_reference.h" #include "row_extractor.h" #include "schema/column.h" @@ -46,7 +47,7 @@ namespace sqlite_orm::internal { /** * Specialization for a table reference. * - * This plays together with `column_result_of_t`, which returns `object_t` as `table_referenece` + * This plays together with `column_result_of_t`, which returns `object_t` as `table_reference` */ template struct struct_extractor, DBOs> { diff --git a/dev/schema/dbo_name.h b/dev/schema/dbo_name.h new file mode 100644 index 000000000..5f409d6be --- /dev/null +++ b/dev/schema/dbo_name.h @@ -0,0 +1,97 @@ +#pragma once + +#ifndef SQLITE_ORM_IMPORT_STD_MODULE +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED +#include // std::string_view +#include // std::tuple +#include // std::bool_constant +#include // std::forward +#endif +#endif + +#include "../functional/cstring_literal.h" +#include "../functional/meta_util.h" +#include "../functional/mpl.h" +#include "../tuple_helper/tuple_filter.h" +#include "../tuple_helper/tuple_traits.h" + +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED +namespace sqlite_orm::internal { + /** + * Class-scope annotation that overrides the database object name (table or view). + * When absent, the name falls back to `std::meta::identifier_of(^^T)`. + * + * The string is embedded in the type's bytes via `cstring_literal` rather than + * carried by pointer + size: pointers to string literals are not accepted as + * annotation values by current reflection implementations (the underlying object + * has no linkage), so a self-contained fixed-size byte array is required. + */ + template + struct dbo_name_literal : cstring_literal { + constexpr dbo_name_literal(const char (&cstr)[N]) : cstring_literal{cstr} {} + + constexpr auto name() const noexcept { + return this->cstr; + } + }; + + template + constexpr bool is_dbo_name_v = false; + + template + constexpr bool is_dbo_name_v> = true; + + template + using is_dbo_name = std::bool_constant>; + + /** + * Returns the database object name carried by the `dbo_name_literal<…>` element of `annotations`, + * or the type's reflected identifier when no such element is present. + */ + template + constexpr std::string_view resolve_dbo_name(const Tuple& annotations) { + using name_index = find_tuple_element; + + if constexpr (name_index::value < std::tuple_size_v) { + return std::get(annotations).name(); + } else { + return extract_type_identifier(); + } + } + + /** + * Returns a copy of `tuple` with all `dbo_name_literal<…>` elements removed. + */ + template + constexpr auto filter_out_dbo_name(Tuple&& tuple) { + using constraints_index_sequence = filter_tuple_sequence_t::template fn>; + return create_from_tuple(std::forward(tuple), constraints_index_sequence{}); + } +} + +SQLITE_ORM_EXPORT namespace sqlite_orm { + inline namespace literals { + /** + * Database object name annotation factory. + * Use as a class-scope annotation: + * `struct [[="users"_dbo_name]] User { ... };` + * `struct [[= sqlite_orm::operator""_dbo_name<"users">()]] User { ... };` + * `make_view()` consumes this annotation. + */ + template + [[nodiscard]] consteval auto operator""_dbo_name() { + return dboName; + } + } + + /** + * Database object name annotation factory. + * Use as a class-scope annotation: `struct [[=dbo_name("users")]] User { ... };`. + * `make_view()` consumes this annotation. + */ + template + consteval internal::dbo_name_literal dbo_name(const char (&dboName)[N]) { + return {dboName}; + } +} +#endif diff --git a/dev/schema/table_base.h b/dev/schema/table_base.h index fb2721797..f46dc9420 100644 --- a/dev/schema/table_base.h +++ b/dev/schema/table_base.h @@ -225,7 +225,7 @@ namespace sqlite_orm::internal { } /** - * Mixin for a base table, providing methods used to access a mapped object's members. + * Mixin for a base table, providing methods used to access a mapped object's member variables for insertion. * * Implementation note: it is provided as a mixin to reduce the number of involved template parameters, * which is possible in C++23 mode for 'getters'. diff --git a/dev/schema/view.h b/dev/schema/view.h new file mode 100644 index 000000000..2a5ab5905 --- /dev/null +++ b/dev/schema/view.h @@ -0,0 +1,103 @@ +#pragma once + +#ifndef SQLITE_ORM_IMPORT_STD_MODULE +#ifdef SQLITE_ORM_WITH_VIEW +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED +#include // std::forward, std::move, std::index_sequence, std::make_index_sequence +#include // std::meta::info, std::meta::identifier_of +#endif +#endif +#endif + +#include "../functional/cxx_type_traits_polyfill.h" +#include "../functional/meta_util.h" +#include "../column_pointer.h" +#include "../select_constraints.h" +#include "column.h" +#include "table_base.h" +#include "dbo_name.h" + +namespace sqlite_orm::internal { +#ifdef SQLITE_ORM_WITH_VIEW + /** + * View definition, mapping an aggregate object type to a corresponding select statement. + */ + template + struct query_view : table_identifier, table_definition { + using definition_base_type = table_definition; + using object_type = O; + using elements_type = typename definition_base_type::elements_type; + using select_type = Select; + + select_type select; + }; + + template + constexpr bool is_view_v = polyfill::is_specialization_of_v; +#else + template + constexpr bool is_view_v = false; +#endif + + template + using is_view = polyfill::bool_constant>; +} + +#ifdef SQLITE_ORM_WITH_VIEW +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED +namespace sqlite_orm::internal { + template + auto make_reflected_view(Select select) { + std::string viewName{resolve_dbo_name(extract_type_annotations())}; + static /*gcc*/ constexpr auto members = extract_members(); + + auto columns = [](std::index_sequence) static { + return std::tuple { + []() static { + return sqlite_orm::make_column(std::string(std::meta::identifier_of(member)), + splice_member_pointer()); + }.template operator()()... + }; + }(std::make_index_sequence{}); + + return [&viewName, &select](std::tuple&& cols) { + return query_view{std::move(viewName), std::move(cols), std::move(select)}; + }(std::move(columns)); + } +} + +SQLITE_ORM_EXPORT namespace sqlite_orm { + /** + * Factory function for a view definition. + * + * The mapped object type is explicitly specified, columns and their names are deferred from the object type. + * The object type must be an aggregate. The optional `[[="…"_dbo_name]]` class-scope annotation overrides + * the view name (otherwise the type's reflected identifier is used). + */ + template + requires (internal::is_select_expression_v) { + select.highest_level = true; + } + return make_reflected_view(std::move(select)); + } + +#ifdef SQLITE_ORM_WITH_CPP20_ALIASES + /** + * Factory function for a view definition. + * + * The mapped object type is explicitly specified, columns and their names are deferred from the object type. + * The object type must be an aggregate. The optional `[[="…"_dbo_name]]` class-scope annotation overrides + * the view name (otherwise the type's reflected identifier is used). + */ + template + auto make_view(Select select) { + return make_view>(std::forward) + auto make_view(Select select) { + using namespace ::sqlite_orm::internal; - /** - * Factory function for a base table. - * - * The mapped object type is explicitly specified. - */ - template - internal::base_table make_table(std::string name, Cs... definition) { - internal::validate_base_table_definition(); - return {std::move(name), std::tuple{std::forward(definition)...}}; + if constexpr (is_select_v(select)); } #endif } +#endif +#endif // #include "storage_lookup.h" @@ -13745,6 +14071,11 @@ namespace sqlite_orm::internal { template using tables_index_sequence = filter_tuple_sequence_t; +#ifdef SQLITE_ORM_WITH_VIEW + template + using views_index_sequence = filter_tuple_sequence_t; +#endif + template = true> constexpr int foreign_keys_count() { int res = 0; @@ -14623,6 +14954,8 @@ SQLITE_ORM_EXPORT namespace sqlite_orm { // #include "member_traits/member_traits.h" +// #include "type_traits.h" + // #include "table_reference.h" // #include "row_extractor.h" @@ -14664,7 +14997,7 @@ namespace sqlite_orm::internal { /** * Specialization for a table reference. * - * This plays together with `column_result_of_t`, which returns `object_t` as `table_referenece` + * This plays together with `column_result_of_t`, which returns `object_t` as `table_reference` */ template struct struct_extractor, DBOs> { @@ -19592,15 +19925,6 @@ namespace sqlite_orm::internal { this->drop_trigger_internal(triggerName, true); } - /** - * `VACUUM` query. - * More info: https://www.sqlite.org/lang_vacuum.html - */ - void vacuum() { - auto connection = this->get_connection(); - this->executor.perform_void_exec(connection.get(), "VACUUM"); - } - /** * Drops table with given name. * Calls `DROP TABLE tableName`. @@ -19621,6 +19945,26 @@ namespace sqlite_orm::internal { this->drop_table_internal(connection.get(), tableName, true); } + /** + * Drops the view with the specified name. + * Calls `DROP VIEW "viewName"`. + * More info: https://www.sqlite.org/lang_droptable.html + */ + void drop_view(const std::string& viewName) { + auto connection = this->get_connection(); + this->drop_view_internal(connection.get(), viewName, false); + } + + /** + * Drops the view with the specified name if it exists. + * Calls `DROP VIEW IF EXISTS "viewName"`. + * More info: https://www.sqlite.org/lang_droptable.html + */ + void drop_view_if_exists(const std::string& viewName) { + auto connection = this->get_connection(); + this->drop_view_internal(connection.get(), viewName, true); + } + /** * Rename table named `from` to `to`. */ @@ -19629,6 +19973,15 @@ namespace sqlite_orm::internal { this->rename_table(connection.get(), from, to); } + /** + * `VACUUM` query. + * More info: https://www.sqlite.org/lang_vacuum.html + */ + void vacuum() { + auto connection = this->get_connection(); + this->executor.perform_void_exec(connection.get(), "VACUUM"); + } + protected: void rename_table(sqlite3* db, const std::string& oldName, const std::string& newName) const { std::string sql; @@ -19648,28 +20001,24 @@ namespace sqlite_orm::internal { */ bool table_exists(const std::string& tableName) { auto connection = this->get_connection(); - return this->table_exists(connection.get(), tableName); + return this->object_exists(connection.get(), "table", tableName); } bool table_exists(sqlite3* db, const std::string& tableName) const { - bool result = false; - std::string sql; - { - std::stringstream ss; - ss << "SELECT COUNT(*) FROM sqlite_master WHERE type = " << quote_string_literal("table") - << " AND name = " << quote_string_literal(tableName) << std::flush; - sql = ss.str(); - } - this->executor.perform_exec( - db, - sql, - [](void* userData, int /*argc*/, orm_gsl::zstring* argv, orm_gsl::zstring* /*azColName*/) -> int { - auto& res = *(bool*)userData; - res = !!atoi(argv[0]); - return 0; - }, - &result); - return result; + return this->object_exists(db, "table", tableName); + } + + /** + * Directly checks the actual database whether the specified view exists, bypassing the library's 'storage' mapping. + * @return true if view with the specified name exists in the database, false otherwise. + */ + bool view_exists(const std::string& viewName) { + auto connection = this->get_connection(); + return this->object_exists(connection.get(), "view", viewName); + } + + bool view_exists(sqlite3* db, const std::string& viewName) const { + return this->object_exists(db, "view", viewName); } void add_generated_cols(std::vector& columnsToAdd, @@ -19753,18 +20102,27 @@ namespace sqlite_orm::internal { #endif /** - * Returns existing permanent table names in database. Doesn't check storage itself - works only with + * Returns the names of existing permanent views in the database. Doesn't check storage itself - works only with + * actual database. + * @return Returns a list of views in the database. + */ + std::vector view_names() { + return this->object_names("view"); + } + + /** + * Returns the names of existing permanent tables in the database. Doesn't check storage itself - works only with * actual database. - * @return Returns list of tables in database. + * @return Returns a list of tables in the database. */ std::vector table_names() { return this->object_names("table"); } /** - * Returns existing permanent trigger names in database. Doesn't check storage itself - works only with + * Returns the names of existing permanent triggers in the database. Doesn't check storage itself - works only with * actual database. - * @return Returns list of triggers in database. + * @return Returns a list of triggers in the database. */ std::vector trigger_names() { return this->object_names("trigger"); @@ -19805,7 +20163,7 @@ namespace sqlite_orm::internal { * double operator()(double arg) const { * return std::sqrt(arg); * } - * + * * static const char* name() { * return "SQRT"; * } @@ -20264,7 +20622,7 @@ namespace sqlite_orm::internal { auto connection = this->get_connection(); data_t objectNames; std::stringstream ss; - ss << "SELECT name FROM sqlite_master WHERE type=" << quote_string_literal(std::string(type)); + ss << "SELECT name FROM sqlite_master WHERE type=" << quote_string_literal(std::string(type)) << std::flush; this->executor.perform_exec( connection.get(), ss.str(), @@ -20506,6 +20864,20 @@ namespace sqlite_orm::internal { this->executor.perform_void_exec(db, sql.c_str()); } + void drop_view_internal(sqlite3* db, const std::string& viewName, bool ifExists) { + std::string sql; + { + std::stringstream ss; + ss << "DROP VIEW"; + if (ifExists) { + ss << " IF EXISTS"; + } + ss << ' ' << streaming_identifier(viewName) << std::flush; + sql = ss.str(); + } + this->executor.perform_void_exec(db, sql.c_str()); + } + void drop_index_internal(const std::string& indexName, bool ifExists) { std::string sql; { @@ -20523,10 +20895,10 @@ namespace sqlite_orm::internal { void drop_trigger_internal(const std::string& triggerName, bool ifExists) { auto connection = this->get_connection(); - this->drop_trigger_internal(triggerName, ifExists, connection.get()); + this->drop_trigger_internal(connection.get(), triggerName, ifExists); } - void drop_trigger_internal(const std::string& triggerName, bool ifExists, sqlite3* db) { + void drop_trigger_internal(sqlite3* db, const std::string& triggerName, bool ifExists) { std::stringstream ss; ss << "DROP TRIGGER"; if (ifExists) { @@ -20536,18 +20908,33 @@ namespace sqlite_orm::internal { this->executor.perform_void_exec(db, ss.str().c_str()); } + bool object_exists(sqlite3* db, const std::string& type, const std::string& name) const { + bool result = false; + std::stringstream ss; + ss << "SELECT COUNT(*) FROM sqlite_master WHERE type = " << quote_string_literal(type) + << " AND name = " << quote_string_literal(name) << std::flush; + this->executor.perform_exec( + db, + ss.str(), + [](void* userData, int /*argc*/, orm_gsl::zstring* argv, orm_gsl::zstring* /*azColName*/) -> int { + auto& res = *(bool*)userData; + res = !!atoi(argv[0]); + return 0; + }, + &result); + return result; + } + std::string retrieve_object_sql(sqlite3* db, const std::string& type, const std::string& name) const { std::string result; std::stringstream ss; ss << "SELECT sql FROM sqlite_master WHERE type = " << quote_string_literal(type) - << " AND name = " << quote_string_literal(name); + << " AND name = " << quote_string_literal(name) << std::flush; this->executor.perform_exec( db, ss.str(), [](void* userData, int /*argc*/, orm_gsl::zstring* argv, orm_gsl::zstring* /*columnName*/) -> int { - if (argv[0]) { - *static_cast(userData) = argv[0]; - } + *static_cast(userData) = argv[0]; return 0; }, &result); @@ -21574,6 +21961,8 @@ SQLITE_ORM_EXPORT namespace sqlite_orm { // #include "schema/table.h" +// #include "schema/view.h" + // #include "schema/virtual_table.h" #ifndef SQLITE_ORM_IMPORT_STD_MODULE @@ -21844,6 +22233,26 @@ namespace sqlite_orm::internal { } }; +#ifdef SQLITE_ORM_WITH_VIEW + template + struct statement_serializer>> { + using statement_type = View; + + template + std::string operator()(const statement_type& statement, const Ctx& context) { + auto subContext = context; + subContext.omit_column_type = true; + std::stringstream ss; + ss << "CREATE VIEW " << streaming_identifier(statement.name) << " (" + << streaming_expressions_tuple(statement.elements, subContext) + << ")" + " AS " + << serialize(statement.select, context); + return ss.str(); + } + }; +#endif + // Eponymous virtual tables serialize only table values. Their definition is built-in, fixed and implicit template = true> std::string serialize_virtual_table_definition(const Elements& elements, const Ctx& context) { @@ -24517,6 +24926,8 @@ namespace sqlite_orm::internal { // #include "schema/table.h" +// #include "schema/view.h" + // #include "schema/virtual_table.h" // #include "schema/column.h" @@ -24730,7 +25141,7 @@ namespace sqlite_orm::internal { auto make_cte_column(std::string name, const ColRef& finalColRef) { using column_type = column_t; - return column_type{std::move(name), finalColRef, empty_setter{}}; + return column_type{std::move(name), finalColRef, empty_setter{}, std::tuple<>{}}; } #ifdef SQLITE_ORM_STRUCTURED_BINDING_PACK_SUPPORTED @@ -25109,7 +25520,11 @@ namespace sqlite_orm::internal { storage_opt_or_default(options), storage_opt_or_default(options), foreign_keys_count()}, - db_objects{std::move(dbObjects)} {} + db_objects{std::move(dbObjects)} { +#ifdef SQLITE_ORM_WITH_VIEW + this->validate_dbos(); +#endif + } storage_t(const storage_t&) = default; @@ -25132,6 +25547,25 @@ namespace sqlite_orm::internal { return storage.db_objects; } +#ifdef SQLITE_ORM_WITH_VIEW + void validate_dbos() const { + // validate views: a view cannot select sub-objects, and column results must be convertible to view's object type + iterate_tuple(views_index_sequence{}, [this](const auto* view) { + using DrivingSelect = polyfill::remove_cvref_tselect))>; + using ExprDBOs = + polyfill::remove_cvref_tdb_objects, view->select))>; + using ColResult = column_result_of_t; + using elements_type = elements_type_t>; + using field_types = transform_tuple_t, field_type_t>; + + static_assert(std::is_same, ColResult>::value, + "A view cannot select sub-objects"); + static_assert(std::is_convertible, field_types>::value, + "Column results must be convertible to view's object type"); + }); + } +#endif + template void create_table(sqlite3* db, const std::string& tableName, const Table& table) { using context_t = serializer_context; @@ -25935,7 +26369,7 @@ namespace sqlite_orm::internal { * @return The ID of the last inserted record for a rowid table, otherwise a meaningless value. * Attention: `sqlite3_last_insert_rowid()` is used to retrieve the last inserted ID, therefore the ID is only useful in single-threaded contexts. * Attention: While SQLite returns a 64-bit integer as rowid, this function returns an `int` that most likely has less precision. - * If you need the full 64-bit rowid value, use `storage_t<>::execute()` instead, or call `storage_t<>::last_insert_rowid()` after inserting. + * If you need the full 64-bit rowid value, use `storage_t<>::execute()` instead, or call `storage_t<>::last_insert_rowid()` after inserting. */ template int insert(const O& o, columns_t cols) { @@ -25960,7 +26394,7 @@ namespace sqlite_orm::internal { * @return The ID of the last inserted record for a rowid table, otherwise a meaningless value. * Attention: `sqlite3_last_insert_rowid()` is used to retrieve the last inserted ID, therefore the ID is only useful in single-threaded contexts. * Attention: While SQLite returns a 64-bit integer as rowid, this function returns an `int` that most likely has less precision. - * If you need the full 64-bit rowid value, use `storage_t<>::execute()` instead, or call `storage_t<>::last_insert_rowid()` after inserting. + * If you need the full 64-bit rowid value, use `storage_t<>::execute()` instead, or call `storage_t<>::last_insert_rowid()` after inserting. */ template int insert(const O& o) { @@ -26131,6 +26565,27 @@ namespace sqlite_orm::internal { return sync_schema_result::already_in_sync; } +#ifdef SQLITE_ORM_WITH_VIEW + template = true> + sync_schema_result schema_status(const View& queryView, sqlite3* db, bool, bool*) { + auto dbViewSql = this->retrieve_object_sql(db, "view", queryView.name); + if (dbViewSql.empty()) { + return sync_schema_result::new_table_created; + } + + const auto& exprDBOs = db_objects_for_expression(this->db_objects, queryView.select); + + using context_t = serializer_context>; + const context_t context{exprDBOs}; + auto storageSql = serialize(queryView, context); + + if (dbViewSql == storageSql) { + return sync_schema_result::already_in_sync; + } + return sync_schema_result::dropped_and_recreated; + } +#endif + template = true> sync_schema_result schema_status(const Table& table, sqlite3* db, bool preserve, bool* attempt_to_preserve) { if (attempt_to_preserve) { @@ -26261,7 +26716,7 @@ namespace sqlite_orm::internal { auto res = this->schema_status(trigger, db, preserve, nullptr); if (res != sync_schema_result::already_in_sync) { if (res == sync_schema_result::dropped_and_recreated) { - this->drop_trigger_internal(trigger.name, true, db); + this->drop_trigger_internal(db, trigger.name, true); } const serializer_context context{this->db_objects}; const auto sql = serialize(trigger, context); @@ -26270,6 +26725,26 @@ namespace sqlite_orm::internal { return res; } +#ifdef SQLITE_ORM_WITH_VIEW + template = true> + sync_schema_result sync_dbo(const View& queryView, sqlite3* db, bool preserve) { + auto res = this->schema_status(queryView, db, preserve, nullptr); + if (res != sync_schema_result::already_in_sync) { + if (res == sync_schema_result::dropped_and_recreated) { + this->drop_view_internal(db, queryView.name, true); + } + + const auto& exprDBOs = db_objects_for_expression(this->db_objects, queryView.select); + + using context_t = serializer_context>; + const context_t context{exprDBOs}; + const auto sql = serialize(queryView, context); + this->executor.perform_void_exec(db, sql.c_str()); + } + return res; + } +#endif + template = true> sync_schema_result sync_dbo(const Table& table, sqlite3* db, bool preserve); @@ -26314,7 +26789,6 @@ namespace sqlite_orm::internal { const auto& exprDBOs = db_objects_for_expression(this->db_objects, expression); using context_t = serializer_context>; - context_t context{exprDBOs}; context.replace_bindable_with_question = parametrized; // just like prepare_impl() @@ -26334,7 +26808,6 @@ namespace sqlite_orm::internal { const auto& exprDBOs = db_objects_for_expression(this->db_objects, statement); using context_t = serializer_context>; - context_t context{exprDBOs}; context.omit_table_name = false; context.replace_bindable_with_question = true; @@ -26362,12 +26835,12 @@ namespace sqlite_orm::internal { * file at all it will be created and all tables also will be created with exact tables and columns you * specified in `make_storage`, `make_table` and `make_column` calls. The best practice is to call this * function right after storage creation. - * @param preserve affects function's behaviour in case it is needed to remove a column. If it is `false` + * @param preserve affects function's behaviour in case it is needed to remove a column. If it is `false` * so table will be dropped if there is column to remove if SQLite version is < 3.35.0 and remove column if SQLite version >= 3.35.0, * if `true` - table is being copied into another table, dropped and copied table is renamed with source table name. * Warning: sync_schema doesn't check foreign keys cause it is unable to do so in sqlite3. If you know how to get foreign key info please * submit an issue https://github.com/fnc12/sqlite_orm/issues - * @return std::map with std::string key equal table name and `sync_schema_result` as value. + * @return std::map with std::string key equal table name and `sync_schema_result` as value. * `sync_schema_result` is a enum value that stores table state after syncing a schema. `sync_schema_result` * can be printed out on std::ostream with `operator<<`. */ @@ -26396,7 +26869,8 @@ namespace sqlite_orm::internal { return result; } - using storage_base::table_exists; // now that it is in storage_base make it into overload set + using storage_base::table_exists; + using storage_base::view_exists; template, bool> = true> prepared_statement_t prepare(DML statement) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 82d0e5367..a215835df 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -83,11 +83,18 @@ elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") target_compile_options(unit_tests PUBLIC # disabled due to unit tests testing deprecated features -Wno-deprecated-declarations + # disabled due to unit tests testing overaligned structs + -Wno-interference-size # perfectly valid to have missing field initializers in library's test code -Wno-missing-field-initializers # suppress warnings about unused variables in test code -Wno-unused-variable) endif() + +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_CXX_COMPILER_VERSION GREATER_EQUAL 16) + target_compile_options(unit_tests PUBLIC + -freflection) +endif() target_precompile_headers(unit_tests PRIVATE diff --git a/tests/logger_tests.cpp b/tests/logger_tests.cpp index 490f64afa..0b7508145 100644 --- a/tests/logger_tests.cpp +++ b/tests/logger_tests.cpp @@ -3,26 +3,36 @@ using namespace sqlite_orm; -struct WillLogsCollector { - static std::vector logs; +namespace { + struct WillLogsCollector { + static std::vector logs; - void operator()(const std::string_view log) { - this->logs.push_back(std::string(log)); - } -}; + void operator()(const std::string_view log) { + this->logs.push_back(std::string(log)); + } + }; -std::vector WillLogsCollector::logs; + std::vector WillLogsCollector::logs; -struct DidLogsCollector { - static std::vector logs; + struct DidLogsCollector { + static std::vector logs; - void operator()(const std::string_view log) { - this->logs.push_back(std::string(log)); - } -}; + void operator()(const std::string_view log) { + this->logs.push_back(std::string(log)); + } + }; -std::vector DidLogsCollector::logs; + std::vector DidLogsCollector::logs; +#ifdef SQLITE_ORM_WITH_VIEW +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED + struct[[= "users_view"_dbo_name]] UserViewLoggerTests { + int id = 0; + std::string name; + }; +#endif +#endif +} TEST_CASE("logger") { using Logs = std::vector; using Callback = std::function; @@ -62,7 +72,7 @@ TEST_CASE("logger") { } }; - auto requireLogsAreEmpty = [] { + constexpr auto requireLogsAreEmpty = [] { REQUIRE(WillLogsCollector::logs.empty()); REQUIRE(DidLogsCollector::logs.empty()); }; @@ -89,6 +99,11 @@ TEST_CASE("logger") { make_table("visits_log", make_column("id", &VisitLog::id, primary_key()), make_column("message", &VisitLog::message)), +#ifdef SQLITE_ORM_WITH_VIEW +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED + make_view(select(asterisk())), +#endif +#endif will_run_query(willRunQuery), did_run_query(didRunQuery)); storage.sync_schema(); @@ -150,10 +165,6 @@ TEST_CASE("logger") { storage.drop_trigger_if_exists(value); pushExpected(expected); } - SECTION("vacuum") { - storage.vacuum(); - pushExpected("VACUUM"); - } SECTION("drop_table") { const auto [value, expected] = GENERATE(table({ {"users", R"(DROP TABLE "users")"}, @@ -170,6 +181,26 @@ TEST_CASE("logger") { storage.drop_table_if_exists(value); pushExpected(expected); } +#ifdef SQLITE_ORM_WITH_VIEW +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED + SECTION("drop_view") { + storage.drop_view("users_view"); + pushExpected(R"(DROP VIEW "users_view")"); + } + SECTION("drop_view_if_exists") { + const auto [value, expected] = GENERATE(table({ + {"users_view", R"(DROP VIEW IF EXISTS "users_view")"}, + {"xyz_view", R"(DROP VIEW IF EXISTS "xyz_view")"}, + })); + storage.drop_view_if_exists(value); + pushExpected(expected); + } +#endif +#endif + SECTION("vacuum") { + storage.vacuum(); + pushExpected("VACUUM"); + } SECTION("changes") { std::ignore = storage.changes(); } @@ -223,6 +254,14 @@ TEST_CASE("logger") { std::ignore = storage.table_names(); pushExpected("SELECT name FROM sqlite_master WHERE type='table'"); } + SECTION("view_names") { + std::ignore = storage.view_names(); + pushExpected("SELECT name FROM sqlite_master WHERE type='view'"); + } + SECTION("trigger_names") { + std::ignore = storage.trigger_names(); + pushExpected("SELECT name FROM sqlite_master WHERE type='trigger'"); + } SECTION("open_forever") { storage.open_forever(); } diff --git a/tests/prepared_statement_tests/update.cpp b/tests/prepared_statement_tests/update.cpp index acbea120a..5aa8c3209 100644 --- a/tests/prepared_statement_tests/update.cpp +++ b/tests/prepared_statement_tests/update.cpp @@ -8,7 +8,6 @@ using namespace sqlite_orm; TEST_CASE("Prepared update") { using namespace PreparedStatementTests; - using Catch::Matchers::UnorderedEquals; const int defaultVisitTime = 50; diff --git a/tests/schema/table_tests.cpp b/tests/schema/table_tests.cpp index 4304f9449..6695292d1 100644 --- a/tests/schema/table_tests.cpp +++ b/tests/schema/table_tests.cpp @@ -26,6 +26,7 @@ TEST_CASE("table::find_column_name") { make_column("country_code", &Contact::countryCode), make_column("phone_number", &Contact::phoneNumber), make_column("visits_count", &Contact::visitsCount)); + STATIC_REQUIRE(table.count_of() == 6); REQUIRE(*table.find_column_name(&Contact::id) == "contact_id"); REQUIRE(*table.find_column_name(&Contact::firstName) == "first_name"); REQUIRE(*table.find_column_name(&Contact::lastName) == "last_name"); diff --git a/tests/schema/view_tests.cpp b/tests/schema/view_tests.cpp new file mode 100644 index 000000000..ec56414dc --- /dev/null +++ b/tests/schema/view_tests.cpp @@ -0,0 +1,72 @@ +#include +#include + +#ifdef SQLITE_ORM_WITH_VIEW +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED +using namespace sqlite_orm; + +namespace { + struct[[= "user_view"_dbo_name]] UserViewSchemaTests { + int64 id = 0; + std::string name; + }; + + struct[[= dbo_name("user_view")]] UserViewSchemaTests2 { + int64 id = 0; + std::string name; + }; + + struct UserViewSchemaTestsDefaultName { + int64 id = 0; + std::string name; + }; +} + +TEST_CASE("view make_view name resolution") { + struct User { + int64 id = 0; + std::string name; + }; + + SECTION("annotation supplies the view name") { + auto view = make_view(select(asterisk())); + REQUIRE(view.name == "user_view"); + } + + SECTION("annotation supplies the view name 2") { + auto view = make_view(select(asterisk())); + REQUIRE(view.name == "user_view"); + } + + SECTION("fallback to type identifier") { + auto view = make_view(select(asterisk())); + REQUIRE(view.name == "UserViewSchemaTestsDefaultName"); + } +} + +TEST_CASE("view::find_column_name") { + struct User { + int64 id = 0; + std::string name; + }; + + SECTION("fields, direct") { + auto view = make_view(select(asterisk())); + + REQUIRE((view.find_column_name(&UserViewSchemaTests::id) && + *view.find_column_name(&UserViewSchemaTests::id) == "id")); + REQUIRE((view.find_column_name(&UserViewSchemaTests::name) && + *view.find_column_name(&UserViewSchemaTests::name) == "name")); + } + SECTION("fields, derived") { + struct DerivedUserView : UserViewSchemaTests {}; + auto view = make_view(select(asterisk())); + + REQUIRE((view.find_column_name(&UserViewSchemaTests::id) && + *view.find_column_name(&UserViewSchemaTests::id) == "id")); + REQUIRE((view.find_column_name(&UserViewSchemaTests::name) && + *view.find_column_name(&UserViewSchemaTests::name) == "name")); + } +} +#endif +#endif diff --git a/tests/statement_serializer_tests/schema/view.cpp b/tests/statement_serializer_tests/schema/view.cpp new file mode 100644 index 000000000..2d2509629 --- /dev/null +++ b/tests/statement_serializer_tests/schema/view.cpp @@ -0,0 +1,47 @@ +#include +#include + +#ifdef SQLITE_ORM_WITH_VIEW +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED +using namespace sqlite_orm; +using internal::serialize; + +namespace { + struct[[= "user_view"_dbo_name]] UserViewSerializerTests { + int id = 0; + std::string name; + }; +} + +TEST_CASE("view statement_serializer") { + struct User { + int id = 0; + std::string name; + }; + + auto table = make_table("user", make_column("id", &User::id), make_column("name", &User::name)); + auto view = make_view(select(asterisk(true))); + using db_objects_t = internal::db_objects_tuple; + const db_objects_t dbObjects{table, view}; + using context_t = internal::serializer_context; + const context_t context{dbObjects}; + + SECTION("create") { + std::string value = serialize(view, context); + REQUIRE(value == R"(CREATE VIEW "user_view" ("id", "name") AS SELECT "user"."id", "user"."name" FROM "user")"); + } + SECTION("as object") { + auto expression = select(object()); + expression.highest_level = true; + std::string value = serialize(expression, context); + REQUIRE(value == R"(SELECT "user_view".* FROM "user_view")"); + } + SECTION("asterisk") { + auto expression = select(asterisk()); + expression.highest_level = true; + std::string value = serialize(expression, context); + REQUIRE(value == R"(SELECT "user_view".* FROM "user_view")"); + } +} +#endif +#endif diff --git a/tests/static_tests/view_static_tests.cpp b/tests/static_tests/view_static_tests.cpp new file mode 100644 index 000000000..91a1ad8e7 --- /dev/null +++ b/tests/static_tests/view_static_tests.cpp @@ -0,0 +1,56 @@ +#include +#include + +#ifdef SQLITE_ORM_WITH_VIEW +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED +using namespace sqlite_orm; +using internal::col_index_sequence_of, internal::col_index_sequence_with_field_type; +using internal::is_column; + +namespace { + struct[[= "user_view"_dbo_name]] UserViewStaticTests { + int64 id = 0; + std::string name; + + private: + std::string _privateDummy; + }; +#ifdef SQLITE_ORM_WITH_CPP20_ALIASES + constexpr orm_table_reference auto user_view = c(); +#endif +} + +TEST_CASE("view static count_of()") { + struct User { + int64 id = 0; + std::string name; + }; + + SECTION("traditional") { + auto view = make_view(select(asterisk())); + using elements_type = decltype(view.elements); + STATIC_REQUIRE(view.count_of() == 2); + STATIC_REQUIRE(col_index_sequence_of::size() == 2); + STATIC_REQUIRE(col_index_sequence_with_field_type::size() == 1); + } + SECTION("derived") { + struct DerivedUserView : UserViewStaticTests {}; + + auto view = make_view(select(asterisk())); + using elements_type = decltype(view.elements); + STATIC_REQUIRE(view.count_of() == 2); + STATIC_REQUIRE(col_index_sequence_of::size() == 2); + STATIC_REQUIRE(col_index_sequence_with_field_type::size() == 1); + } +#ifdef SQLITE_ORM_WITH_CPP20_ALIASES + SECTION("table reference") { + auto view = make_view(select(asterisk())); + using elements_type = decltype(view.elements); + STATIC_REQUIRE(view.count_of() == 2); + STATIC_REQUIRE(col_index_sequence_of::size() == 2); + STATIC_REQUIRE(col_index_sequence_with_field_type::size() == 1); + } +#endif +} +#endif +#endif diff --git a/tests/storage_tests.cpp b/tests/storage_tests.cpp index 50ff865ae..7e93edc75 100644 --- a/tests/storage_tests.cpp +++ b/tests/storage_tests.cpp @@ -265,6 +265,40 @@ TEST_CASE("drop table") { REQUIRE_NOTHROW(storage.drop_table_if_exists(visitsTableName)); } +#ifdef SQLITE_ORM_WITH_VIEW +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED +namespace { + constexpr char usersViewName[] = "users_view"; + + struct[[= dbo_name(usersViewName)]] UserViewDropViewTests { + int64 id = 0; + std::string name; + }; +} +TEST_CASE("drop view") { + struct User { + int id = 0; + std::string name; + }; + + auto storage = + make_storage({}, + make_table("users", make_column("id", &User::id, primary_key()), make_column("name", &User::name)), + make_view(select(asterisk()))); + REQUIRE_FALSE(storage.view_exists(usersViewName)); + + storage.sync_schema(); + REQUIRE(storage.view_exists(usersViewName)); + + storage.drop_view(usersViewName); + REQUIRE_FALSE(storage.view_exists(usersViewName)); + + REQUIRE_THROWS(storage.drop_view(usersViewName)); + REQUIRE_NOTHROW(storage.drop_view_if_exists(usersViewName)); +} +#endif +#endif + TEST_CASE("drop index") { struct User { int id = 0; diff --git a/tests/table_name_collector.cpp b/tests/table_name_collector.cpp index c229a0b12..8ea1cbba9 100644 --- a/tests/table_name_collector.cpp +++ b/tests/table_name_collector.cpp @@ -50,6 +50,22 @@ TEST_CASE("table name collector") { expected.emplace(tableName, ""); iterate_ast(expression, collector); } + SECTION("asterisk") { + auto expression = asterisk(); + expected.emplace(tableName, ""); + iterate_ast(expression, collector); + } + SECTION("object") { + auto expression = object(); + expected.emplace(tableName, ""); + iterate_ast(expression, collector); + } + SECTION("aliased asterisk") { + using als = alias_z; + auto expression = asterisk(); + expected.emplace(tableName, "z"); + iterate_ast(expression, collector); + } REQUIRE(collector.table_names == expected); } #ifdef SQLITE_ENABLE_DBSTAT_VTAB diff --git a/tests/trigger_tests.cpp b/tests/trigger_tests.cpp index 8c556b13d..c05da0834 100644 --- a/tests/trigger_tests.cpp +++ b/tests/trigger_tests.cpp @@ -1,5 +1,6 @@ #include #include +#include // std::remove using namespace sqlite_orm; diff --git a/tests/view_tests.cpp b/tests/view_tests.cpp new file mode 100644 index 000000000..b01dcf77c --- /dev/null +++ b/tests/view_tests.cpp @@ -0,0 +1,143 @@ +#include +#include +#include // std::remove + +#ifdef SQLITE_ORM_WITH_VIEW +#ifdef SQLITE_ORM_REFLECTION_SUPPORTED +using namespace sqlite_orm; + +namespace { + struct[[= "user_view"_dbo_name]] UserViewTests { + int64 id = 0; + std::string name; + +#ifdef SQLITE_ORM_DEFAULT_COMPARISONS_SUPPORTED + bool operator==(const UserViewTests&) const = default; +#else + bool operator==(const UserViewTests& right) const { + return id == right.id && name == right.name; + } +#endif + }; + + struct[[= "user_view"_dbo_name]] UserView2Tests { + std::string name; + +#ifdef SQLITE_ORM_DEFAULT_COMPARISONS_SUPPORTED + bool operator==(const UserView2Tests&) const = default; +#else + bool operator==(const UserView2Tests& right) const { + return name == right.name; + } +#endif + }; +} + +TEST_CASE("view") { + using Catch::Matchers::UnorderedEquals; + + struct User { + int64 id = 0; + std::string name; + }; + + SECTION("normal") { + auto storage = make_storage( + "", + make_table("user", make_column("id", &User::id, primary_key()), make_column("name", &User::name)), + make_view(select(asterisk()))); + + storage.sync_schema(); + + storage.transaction([&storage] { + storage.insert({0, "name"}); + return true; + }); + + SECTION("created view") { + auto viewNames = storage.view_names(); + REQUIRE_THAT(viewNames, UnorderedEquals({"user_view"})); + } + SECTION("view select") { + auto users = storage.select(object()); + REQUIRE_THAT(users, UnorderedEquals({{1, "name"}})); + } + } + SECTION("view with CTE") { +#if (SQLITE_VERSION_NUMBER >= 3008003) && defined(SQLITE_ORM_WITH_CTE) +#ifdef SQLITE_ORM_WITH_CPP20_ALIASES + constexpr orm_cte_moniker auto users_cte = "users"_cte; + auto storage = make_storage( + "", + make_table("user", make_column("id", &User::id, primary_key()), make_column("name", &User::name)), + make_view(with(users_cte().as(select(asterisk())), select(asterisk())))); + + storage.sync_schema(); + + storage.transaction([&storage] { + storage.insert({0, "name"}); + return true; + }); + + SECTION("created view") { + auto viewNames = storage.view_names(); + REQUIRE_THAT(viewNames, UnorderedEquals({"user_view"})); + } + SECTION("view select") { + auto users = storage.select(object()); + REQUIRE_THAT(users, UnorderedEquals({{1, "name"}})); + } +#endif +#endif + } +} + +TEST_CASE("view sync") { + struct User { + int64 id = 0; + std::string name; + }; + + auto storagePath = "sync_sql_view.sqlite"; + std::remove(storagePath); + + // first: create storage with trigger checking "name" column + { + auto storage = make_storage( + storagePath, + make_table("user", make_column("id", &User::id, primary_key()), make_column("name", &User::name)), + make_view(select(&User::name))); + auto syncResult = storage.sync_schema(); + REQUIRE(syncResult.at("user_view") == sync_schema_result::new_table_created); + + // second sync should report already_in_sync + syncResult = storage.sync_schema(); + REQUIRE(syncResult.at("user_view") == sync_schema_result::already_in_sync); + } + // second: create storage with a different view on User instead + { + auto storage = make_storage( + storagePath, + make_table("user", make_column("id", &User::id, primary_key()), make_column("name", &User::name)), + make_view(select(asterisk()))); + + // simulate should detect the change + auto simulateResult = storage.sync_schema_simulate(); + REQUIRE(simulateResult.at("user_view") == sync_schema_result::dropped_and_recreated); + + // sync should update the view + auto syncResult = storage.sync_schema(); + REQUIRE(syncResult.at("user_view") == sync_schema_result::dropped_and_recreated); + + // verify view was updated + REQUIRE_NOTHROW(storage.iterate()); + + // after update, second sync should be already_in_sync + syncResult = storage.sync_schema(); + REQUIRE(syncResult.at("user_view") == sync_schema_result::already_in_sync); + } + + std::remove(storagePath); +} +#endif +#endif