From 0bcbcd243124b4400a046cd330d29696a58ee10e Mon Sep 17 00:00:00 2001 From: silverqx Date: Mon, 23 May 2022 09:18:15 +0200 Subject: [PATCH] =?UTF-8?q?added=20support=20for=20PostgreSQL=20schema=20b?= =?UTF-8?q?uilder=20=F0=9F=8E=89=F0=9F=91=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - added a new config. option dont_drop, allows to define table that will be excluded during dropAllTables(), has a default value spatial_ref_sys for PostGIS - implemented fluent commands, used only by the PostgreSQL Comment command - added tests for PostgreSQL schema builder - reworked tst_Migrate to be able to run on all supported connections, currently PostgreSQL and MySQL - updated docs Unrelated: - added a new reference class IndexDefinitionReference for a nice API during index definition, contains algorithm and language - unified selectFromWriteConnection() in schema builders --- cmake/Modules/TinySources.cmake | 2 + docs/database/migrations.mdx | 4 +- docs/database/seeding.mdx | 4 + docs/supported-compilers.mdx | 2 +- include/include.pri | 1 + include/orm/constants_extern.hpp | 4 + include/orm/constants_inline.hpp | 4 + include/orm/schema/blueprint.hpp | 48 +- include/orm/schema/columndefinition.hpp | 44 +- .../orm/schema/columndefinitionreference.hpp | 46 + .../schema/foreignkeydefinitionreference.hpp | 9 + .../schema/grammars/mysqlschemagrammar.hpp | 6 +- .../schema/grammars/postgresschemagrammar.hpp | 239 ++- include/orm/schema/grammars/schemagrammar.hpp | 4 +- .../orm/schema/indexdefinitionreference.hpp | 74 + include/orm/schema/postgresschemabuilder.hpp | 20 +- include/orm/schema/schemaconstants_extern.hpp | 3 + include/orm/schema/schemaconstants_inline.hpp | 3 + include/orm/schema/schematypes.hpp | 26 + src/orm/connectors/connectionfactory.cpp | 9 +- src/orm/constants_extern.cpp | 4 + src/orm/query/processors/mysqlprocessor.cpp | 1 + src/orm/schema/blueprint.cpp | 48 +- .../schema/foreignkeydefinitionreference.cpp | 25 + .../schema/grammars/mysqlschemagrammar.cpp | 56 +- .../schema/grammars/postgresschemagrammar.cpp | 877 ++++++++- src/orm/schema/grammars/schemagrammar.cpp | 4 +- src/orm/schema/indexdefinitionreference.cpp | 34 + src/orm/schema/mysqlschemabuilder.cpp | 18 +- src/orm/schema/postgresschemabuilder.cpp | 121 +- src/orm/schema/schemaconstants_extern.cpp | 33 +- src/src.pri | 1 + tests/TinyUtils/src/databases.cpp | 36 +- .../functional/tom/migrate/tst_migrate.cpp | 183 +- tests/auto/unit/orm/schema/CMakeLists.txt | 1 + .../tst_mysql_schemabuilder.cpp | 10 +- .../postgresql_schemabuilder/CMakeLists.txt | 10 +- .../postgresql_schemabuilder.pro | 6 + .../tst_postgresql_schemabuilder.cpp | 1599 +++++++++++++++++ tests/auto/unit/orm/schema/schema.pro | 1 + 40 files changed, 3382 insertions(+), 238 deletions(-) create mode 100644 include/orm/schema/indexdefinitionreference.hpp create mode 100644 src/orm/schema/indexdefinitionreference.cpp create mode 100644 tests/auto/unit/orm/schema/postgresql_schemabuilder/postgresql_schemabuilder.pro create mode 100644 tests/auto/unit/orm/schema/postgresql_schemabuilder/tst_postgresql_schemabuilder.cpp diff --git a/cmake/Modules/TinySources.cmake b/cmake/Modules/TinySources.cmake index b8aaad03f..1b5cbabb5 100644 --- a/cmake/Modules/TinySources.cmake +++ b/cmake/Modules/TinySources.cmake @@ -78,6 +78,7 @@ function(tinyorm_sources out_headers out_sources) schema/grammars/postgresschemagrammar.hpp schema/grammars/schemagrammar.hpp schema/grammars/sqliteschemagrammar.hpp + schema/indexdefinitionreference.hpp schema/mysqlschemabuilder.hpp schema/postgresschemabuilder.hpp schema/schemabuilder.hpp @@ -193,6 +194,7 @@ function(tinyorm_sources out_headers out_sources) schema/grammars/postgresschemagrammar.cpp schema/grammars/schemagrammar.cpp schema/grammars/sqliteschemagrammar.cpp + schema/indexdefinitionreference.cpp schema/mysqlschemabuilder.cpp schema/postgresschemabuilder.cpp schema/schemabuilder.cpp diff --git a/docs/database/migrations.mdx b/docs/database/migrations.mdx index fa02081c8..b9c8f18df 100644 --- a/docs/database/migrations.mdx +++ b/docs/database/migrations.mdx @@ -38,7 +38,7 @@ The TinyORM `Schema` facade provides database agnostic support for creating and `Tom` migrations is a small console application that depends on the `TinyORM` library. All migrations logic is compiled so recompilation is needed after adding a new migration class. :::caution -Currently, TinyORM's schema builder provides first-party support for the MySQL database only. Support for the PostgreSQL and SQLite databases is not implemented 😢. +Currently, TinyORM's schema builder provides first-party support for the MySQL and PostgreSQL database. Support for the SQLite databases is not implemented 😢. ::: ## Generating Migrations @@ -253,7 +253,7 @@ You may check for the existence of a table or column using the `hasTable` and `h If you want to perform a schema operation on a database connection that is not your application's default connection, use the `connection` method or `on` alias: - Schema::connection("sqlite").create("users", [](Blueprint &table) + Schema::connection("postgres").create("users", [](Blueprint &table) { table.id(); }); diff --git a/docs/database/seeding.mdx b/docs/database/seeding.mdx index 00ce9ffe0..98c3355f7 100644 --- a/docs/database/seeding.mdx +++ b/docs/database/seeding.mdx @@ -19,6 +19,10 @@ TinyORM includes the ability to seed your database with data using seed classes. [Mass assignment protection](tinyorm/getting-started.mdx#mass-assignment) is automatically disabled during database seeding. ::: +:::note +Because seeding is independent of the schema builder, it supports all [supported databases](database/getting-started.mdx#introduction) out of the box. +::: + ## Writing Seeders To generate a seeder, execute the `make:seeder` `tom` command. A new seeder will be placed in the `database/seeders` directory relative to the current pwd: diff --git a/docs/supported-compilers.mdx b/docs/supported-compilers.mdx index 92049af2d..97944c214 100644 --- a/docs/supported-compilers.mdx +++ b/docs/supported-compilers.mdx @@ -7,7 +7,7 @@ description: Platform requirements and supported compilers for TinyORM c++ libra # Supported Compilers -Following compilers are backed up by the GitHub Action [workflows](https://github.com/silverqx/TinyORM/tree/main/.github/workflows) (CI pipelines), these workflows also include more then 920 unit tests. +Following compilers are backed up by the GitHub Action [workflows](https://github.com/silverqx/TinyORM/tree/main/.github/workflows) (CI pipelines), these workflows also include more then 973 unit tests 😮. Windows >=10: diff --git a/include/include.pri b/include/include.pri index 22c6e3409..bb0dcaf3a 100644 --- a/include/include.pri +++ b/include/include.pri @@ -73,6 +73,7 @@ headersList += \ $$PWD/orm/schema/grammars/postgresschemagrammar.hpp \ $$PWD/orm/schema/grammars/schemagrammar.hpp \ $$PWD/orm/schema/grammars/sqliteschemagrammar.hpp \ + $$PWD/orm/schema/indexdefinitionreference.hpp \ $$PWD/orm/schema/mysqlschemabuilder.hpp \ $$PWD/orm/schema/postgresschemabuilder.hpp \ $$PWD/orm/schema/schemabuilder.hpp \ diff --git a/include/orm/constants_extern.hpp b/include/orm/constants_extern.hpp index 1f0f73a1c..2d6d87bb8 100644 --- a/include/orm/constants_extern.hpp +++ b/include/orm/constants_extern.hpp @@ -52,6 +52,7 @@ namespace Orm::Constants SHAREDLIB_EXPORT extern const QString SPACE_IN; SHAREDLIB_EXPORT extern const QString NOSPACE; SHAREDLIB_EXPORT extern const QString EMPTY; + SHAREDLIB_EXPORT extern const QString text_; SHAREDLIB_EXPORT extern const QString QMYSQL; SHAREDLIB_EXPORT extern const QString QPSQL; @@ -74,6 +75,7 @@ namespace Orm::Constants SHAREDLIB_EXPORT extern const QString options_; SHAREDLIB_EXPORT extern const QString strict_; SHAREDLIB_EXPORT extern const QString engine_; + SHAREDLIB_EXPORT extern const QString dont_drop; SHAREDLIB_EXPORT extern const QString isolation_level; SHAREDLIB_EXPORT extern const QString foreign_key_constraints; @@ -94,8 +96,10 @@ namespace Orm::Constants SHAREDLIB_EXPORT extern const QString UTF8MB4; SHAREDLIB_EXPORT extern const QString InnoDB; SHAREDLIB_EXPORT extern const QString MyISAM; + SHAREDLIB_EXPORT extern const QString postgres_; SHAREDLIB_EXPORT extern const QString UTF8MB40900aici; + SHAREDLIB_EXPORT extern const QString UcsBasic; SHAREDLIB_EXPORT extern const QString NotImplemented; // Comparison/logical/search operators diff --git a/include/orm/constants_inline.hpp b/include/orm/constants_inline.hpp index 77271ae16..437e7156a 100644 --- a/include/orm/constants_inline.hpp +++ b/include/orm/constants_inline.hpp @@ -52,6 +52,7 @@ namespace Orm::Constants inline const QString SPACE_IN = QStringLiteral("%1 %2"); inline const QString NOSPACE = QStringLiteral("%1%2"); inline const QString EMPTY = QLatin1String(""); + inline const QString text_ = QStringLiteral("text"); inline const QString QMYSQL = QStringLiteral("QMYSQL"); inline const QString QPSQL = QStringLiteral("QPSQL"); @@ -74,6 +75,7 @@ namespace Orm::Constants inline const QString options_ = QStringLiteral("options"); inline const QString strict_ = QStringLiteral("strict"); inline const QString engine_ = QStringLiteral("engine"); + inline const QString dont_drop = QStringLiteral("dont_drop"); inline const QString isolation_level = QStringLiteral("isolation_level"); @@ -98,8 +100,10 @@ namespace Orm::Constants inline const QString UTF8MB4 = QStringLiteral("utf8mb4"); inline const QString InnoDB = QStringLiteral("InnoDB"); inline const QString MyISAM = QStringLiteral("MyISAM"); + inline const QString postgres_ = QStringLiteral("postgres"); inline const QString UTF8MB40900aici = QStringLiteral("utf8mb4_0900_ai_ci"); + inline const QString UcsBasic = QStringLiteral("ucs_basic"); inline const QString NotImplemented = QStringLiteral("Not implemented :/."); // Comparison/logical/search operators diff --git a/include/orm/schema/blueprint.hpp b/include/orm/schema/blueprint.hpp index de9962f27..6c9682e91 100644 --- a/include/orm/schema/blueprint.hpp +++ b/include/orm/schema/blueprint.hpp @@ -11,6 +11,7 @@ TINY_SYSTEM_HEADER #include "orm/ormconcepts.hpp" #include "orm/schema/foreignidcolumndefinitionreference.hpp" +#include "orm/schema/indexdefinitionreference.hpp" #include "orm/schema/schemaconstants.hpp" #ifndef TINYORM_DISABLE_ORM # include "orm/tiny/tinytypes.hpp" @@ -89,50 +90,50 @@ namespace Grammars const RenameCommand &renameColumn(const QString &from, const QString &to); /*! Specify the primary key(s) for the table. */ - const IndexCommand & + IndexDefinitionReference primary(const QVector &columns, const QString &indexName = "", const QString &algorithm = ""); /*! Specify the primary key(s) for the table. */ template - const IndexCommand & + IndexDefinitionReference primary(const QString &column, const QString &indexName = "", const QString &algorithm = ""); /*! Specify a unique index for the table. */ - const IndexCommand & + IndexDefinitionReference unique(const QVector &columns, const QString &indexName = "", const QString &algorithm = ""); /*! Specify a unique index for the table. */ template - const IndexCommand & + IndexDefinitionReference unique(const QString &column, const QString &indexName = "", const QString &algorithm = ""); /*! Specify an index for the table. */ - const IndexCommand & + IndexDefinitionReference index(const QVector &columns, const QString &indexName = "", const QString &algorithm = ""); /*! Specify an index for the table. */ template - const IndexCommand & + IndexDefinitionReference index(const QString &column, const QString &indexName = "", const QString &algorithm = ""); /*! Specify an fulltext for the table. */ - const IndexCommand & + IndexDefinitionReference fullText(const QVector &columns, const QString &indexName = "", - const QString &algorithm = ""); + const QString &algorithm = "", const QString &language = ""); /*! Specify an fulltext for the table. */ template - const IndexCommand & + IndexDefinitionReference fullText(const QString &column, const QString &indexName = "", - const QString &algorithm = ""); + const QString &algorithm = "", const QString &language = ""); /*! Specify a spatial index for the table. */ - const IndexCommand & + IndexDefinitionReference spatialIndex(const QVector &columns, const QString &indexName = ""); /*! Specify a spatial index for the table. */ template - const IndexCommand & + IndexDefinitionReference spatialIndex(const QString &column, const QString &indexName = ""); /*! Specify a raw index for the table. */ - const IndexCommand & + IndexDefinitionReference rawIndex(const Expression &expression, const QString &indexName); /*! Specify a foreign key for the table. */ ForeignKeyDefinitionReference @@ -433,11 +434,14 @@ namespace Grammars void addImpliedCommands(const SchemaGrammar &grammar); /*! Add the index commands fluently specified on columns. */ void addFluentIndexes(); + /*! Add the fluent commands specified on any columns. */ + void addFluentCommands(const SchemaGrammar &grammar); /*! Add a new index command to the blueprint. */ - const IndexCommand & + IndexDefinitionReference indexCommand(const QString &type, const QVector &columns, - const QString &indexName, const QString &algorithm = ""); + const QString &indexName, const QString &algorithm = "", + const QString &language = ""); /*! Create a new drop index command on the blueprint. */ const IndexCommand & dropIndexCommand(const QString &command, const QString &type, @@ -500,7 +504,7 @@ namespace Grammars } template - const IndexCommand & + IndexDefinitionReference Blueprint::primary(const QString &column, const QString &indexName, const QString &algorithm) { @@ -508,7 +512,7 @@ namespace Grammars } template - const IndexCommand & + IndexDefinitionReference Blueprint::unique(const QString &column, const QString &indexName, const QString &algorithm) { @@ -516,7 +520,7 @@ namespace Grammars } template - const IndexCommand & + IndexDefinitionReference Blueprint::index(const QString &column, const QString &indexName, const QString &algorithm) { @@ -524,15 +528,15 @@ namespace Grammars } template - const IndexCommand & + IndexDefinitionReference Blueprint::fullText(const QString &column, const QString &indexName, - const QString &algorithm) + const QString &algorithm, const QString &language) { - return fullText(QVector {column}, indexName, algorithm); + return fullText(QVector {column}, indexName, algorithm, language); } template - const IndexCommand & + IndexDefinitionReference Blueprint::spatialIndex(const QString &column, const QString &indexName) { return spatialIndex(QVector {column}, indexName); diff --git a/include/orm/schema/columndefinition.hpp b/include/orm/schema/columndefinition.hpp index 2c7c34ee3..b3377145e 100644 --- a/include/orm/schema/columndefinition.hpp +++ b/include/orm/schema/columndefinition.hpp @@ -71,6 +71,8 @@ namespace Orm::SchemaNs QVector columns; /*! Algorithm to use during index creation. */ QString algorithm {}; + /*! Dictionary for the to_tsvector function for fulltext search (PostgreSQL). */ + QString language {}; }; /*! Foreign key constraints command. */ @@ -96,6 +98,26 @@ namespace Orm::SchemaNs /*! Specifies ON UPDATE action (cascade/restrict/set null/no action/ set default). */ QString onUpdate {}; + + /*! Set the foreign key as deferrable (PostgreSQL). */ + std::optional deferrable = std::nullopt; + /*! Set the default time to check the constraint (PostgreSQL). */ + std::optional initiallyImmediate = std::nullopt; + /*! Set the skip check that all existing rows in the table satisfy the new + constraint, skip this check on true (PostgreSQL). */ + std::optional notValid = std::nullopt; + }; + + /*! Column comment command for the PostgreSQL. */ + class CommentCommand : public CommandDefinition + { + public: + /*! Command name. */ + QString name {}; + /*! Column name. */ + QString column; + /*! Column comment value. */ + QString comment; }; /*! Database column definition. */ @@ -126,7 +148,7 @@ namespace Orm::SchemaNs should be ok: https://dev.mysql.com/doc/refman/8.0/en/spatial-function-argument-handling.html */ /*! The spatial reference identifier (SRID) of a geometry identifies the SRS - in which the geometry is defined. */ + in which the geometry is defined (MySQL/PostgreSQL). */ std::optional srid = std::nullopt; /*! Number of digits before the decimal point for floating-point types. */ std::optional total = std::nullopt; @@ -154,22 +176,30 @@ namespace Orm::SchemaNs QString comment {}; /*! Specify a "default" value for the column. */ QVariant defaultValue {}; - /*! Set the starting value of an auto-incrementing field (MySQL / PostgreSQL). */ - std::optional from = std::nullopt; - /*! Set the starting value of an auto-incrementing field (MySQL / PostgreSQL). */ - std::optional startingValue = std::nullopt; + /*! Create a SQL compliant identity column (PostgreSQL). */ + QString generatedAs {}; /*! Create a stored generated column (MySQL/PostgreSQL/SQLite). */ - QString storedAs {}; + QString storedAs {}; /*! Create a virtual generated column (MySQL/PostgreSQL/SQLite). */ - QString virtualAs {}; + QString virtualAs {}; + + /*! Set the starting value of an auto-incrementing field (MySQL/PostgreSQL). */ + std::optional from = std::nullopt; + /*! Set the starting value of an auto-incrementing field (MySQL/PostgreSQL). */ + std::optional startingValue = std::nullopt; // Place boolean data members at the end to avoid excessive padding + /*! Used as a modifier for generatedAs() (PostgreSQL). */ + bool always = false; /*! Determine whether the INTEGER column is auto-increment (primary key). */ bool autoIncrement = false; /*! Place the column "first" in the table (MySQL). */ bool first = false; /*! Specify that the column should be invisible to "SELECT *" (MySQL). */ bool invisible = false; + /*! Determine whether to use the geography (default, false) or + geometry type (PostgreSQL). */ + bool isGeometry = false; /*! Determine whether the INTEGER column is UNSIGNED (MySQL). */ bool isUnsigned = false; /*! Allow NULL values to be inserted into the column. */ diff --git a/include/orm/schema/columndefinitionreference.hpp b/include/orm/schema/columndefinitionreference.hpp index d2cd57d88..ffec6f987 100644 --- a/include/orm/schema/columndefinitionreference.hpp +++ b/include/orm/schema/columndefinitionreference.hpp @@ -54,6 +54,8 @@ namespace Orm::SchemaNs /*! Place the column "after" another column (MySQL). */ ColumnReferenceType &after(const QString &column); + /*! Used as a modifier for generatedAs() (PostgreSQL). */ + ColumnReferenceType &always(); /*! Set INTEGER column as auto-increment (primary key). */ ColumnReferenceType &autoIncrement(); /*! Specify a character set for the column (MySQL). */ @@ -68,12 +70,20 @@ namespace Orm::SchemaNs ColumnReferenceType &first(); /*! Set the starting value of an auto-incrementing field (MySQL / PostgreSQL). */ ColumnReferenceType &from(int startingValue); + /*! Create a SQL compliant identity column (PostgreSQL). */ + ColumnReferenceType &generatedAs(const QString &expression); /*! Specify that the column should be invisible to "SELECT *" (MySQL). */ ColumnReferenceType &invisible(); + /*! Determine whether to use the geography (default, false) or + geometry type (PostgreSQL). */ + ColumnReferenceType &isGeometry(); /*! Set the INTEGER column as UNSIGNED (MySQL). */ ColumnReferenceType &isUnsigned(); /*! Allow NULL values to be inserted into the column. */ ColumnReferenceType &nullable(bool value = true); + /*! The spatial reference identifier (SRID) of a geometry identifies the SRS + in which the geometry is defined (MySQL/PostgreSQL). */ + ColumnReferenceType &srid(quint32 value); /*! Set the starting value of an auto-incrementing field (MySQL/PostgreSQL). */ ColumnReferenceType &startingValue(int startingValue); /*! Create a stored generated column (MySQL/PostgreSQL/SQLite). */ @@ -137,6 +147,15 @@ namespace Orm::SchemaNs return columnReference(); } + template + typename ColumnDefinitionReference::ColumnReferenceType & + ColumnDefinitionReference::always() + { + m_columnDefinition.get().always = true; + + return columnReference(); + } + template typename ColumnDefinitionReference::ColumnReferenceType & ColumnDefinitionReference::autoIncrement() @@ -200,6 +219,15 @@ namespace Orm::SchemaNs return columnReference(); } + template + typename ColumnDefinitionReference::ColumnReferenceType & + ColumnDefinitionReference::generatedAs(const QString &expression) + { + m_columnDefinition.get().generatedAs = expression; + + return columnReference(); + } + template typename ColumnDefinitionReference::ColumnReferenceType & ColumnDefinitionReference::invisible() @@ -209,6 +237,15 @@ namespace Orm::SchemaNs return columnReference(); } + template + typename ColumnDefinitionReference::ColumnReferenceType & + ColumnDefinitionReference::isGeometry() + { + m_columnDefinition.get().isGeometry = true; + + return columnReference(); + } + template typename ColumnDefinitionReference::ColumnReferenceType & ColumnDefinitionReference::isUnsigned() @@ -236,6 +273,15 @@ namespace Orm::SchemaNs return columnReference(); } + template + typename ColumnDefinitionReference::ColumnReferenceType & + ColumnDefinitionReference::srid(const quint32 value) + { + m_columnDefinition.get().srid = value; + + return columnReference(); + } + template typename ColumnDefinitionReference::ColumnReferenceType & ColumnDefinitionReference::storedAs(const QString &expression) diff --git a/include/orm/schema/foreignkeydefinitionreference.hpp b/include/orm/schema/foreignkeydefinitionreference.hpp index 881a2dcb1..ee476d06f 100644 --- a/include/orm/schema/foreignkeydefinitionreference.hpp +++ b/include/orm/schema/foreignkeydefinitionreference.hpp @@ -41,6 +41,15 @@ namespace Orm::SchemaNs /*! Add an ON UPDATE action. */ ForeignKeyDefinitionReference &onUpdate(const QString &action); + /*! Set the foreign key as deferrable (PostgreSQL). */ + ForeignKeyDefinitionReference &deferrable(bool value = true); + /*! Set the default time to check the constraint (PostgreSQL). */ + ForeignKeyDefinitionReference &initiallyImmediate(bool value = true); + /*! Set skip check that all existing rows in the table satisfy the new + constraint (PostgreSQL). */ + ForeignKeyDefinitionReference ¬Valid(bool value = true); + + /* Shortcuts */ /*! Indicate that updates should cascade. */ ForeignKeyDefinitionReference &cascadeOnUpdate(); /*! Indicate that updates should be restricted. */ diff --git a/include/orm/schema/grammars/mysqlschemagrammar.hpp b/include/orm/schema/grammars/mysqlschemagrammar.hpp index c96f29d5e..0594c165f 100644 --- a/include/orm/schema/grammars/mysqlschemagrammar.hpp +++ b/include/orm/schema/grammars/mysqlschemagrammar.hpp @@ -45,9 +45,11 @@ namespace Grammars QString compileDropAllViews(const QVector &views) const override; /*! Compile the SQL needed to retrieve all table names. */ - QString compileGetAllTables() const override; + QString compileGetAllTables( + const QVector &databases = {}) const override; /*! Compile the SQL needed to retrieve all view names. */ - QString compileGetAllViews() const override; + QString compileGetAllViews( + const QVector &databases = {}) const override; /*! Compile the command to enable foreign key constraints. */ QString compileEnableForeignKeyConstraints() const override; diff --git a/include/orm/schema/grammars/postgresschemagrammar.hpp b/include/orm/schema/grammars/postgresschemagrammar.hpp index c47b9d78c..8401e161c 100644 --- a/include/orm/schema/grammars/postgresschemagrammar.hpp +++ b/include/orm/schema/grammars/postgresschemagrammar.hpp @@ -9,7 +9,14 @@ TINY_SYSTEM_HEADER TINYORM_BEGIN_COMMON_NAMESPACE -namespace Orm::SchemaNs::Grammars +namespace Orm::SchemaNs +{ + class BasicCommand; + class CommentCommand; + class DropColumnsCommand; + class RenameCommand; + +namespace Grammars { /*! PostgreSql schemma grammar. */ @@ -27,20 +34,110 @@ namespace Orm::SchemaNs::Grammars inline bool supportsSchemaTransactions() const noexcept override; /* Compile methods for the SchemaBuilder */ + /*! Compile a create database command. */ + QString compileCreateDatabase(const QString &name, + DatabaseConnection &connection) const override; + /*! Compile a drop database if exists command. */ + QString compileDropDatabaseIfExists(const QString &name) const override; + + /*! Compile the SQL needed to drop all tables. */ + QString compileDropAllTables(const QVector &tables) const override; + /*! Compile the SQL needed to drop all views. */ + QString compileDropAllViews(const QVector &views) const override; + + /*! Compile the SQL needed to retrieve all table names. */ + QString compileGetAllTables( + const QVector &databases = {}) const override; + /*! Compile the SQL needed to retrieve all view names. */ + QString compileGetAllViews( + const QVector &databases = {}) const override; + /*! Compile the command to enable foreign key constraints. */ QString compileEnableForeignKeyConstraints() const override; /*! Compile the command to disable foreign key constraints. */ QString compileDisableForeignKeyConstraints() const override; + /*! Compile the query to determine the list of tables. */ + QString compileTableExists() const override; /*! Compile the query to determine the list of columns. */ QString compileColumnListing(const QString &table = "") const override; /* Compile methods for commands */ + /*! Compile a create table command. */ + QVector compileCreate(const Blueprint &blueprint) const; + + /*! Compile a drop table command. */ + QVector compileDrop(const Blueprint &blueprint, + const BasicCommand &command) const; + /*! Compile a drop table (if exists) command. */ + QVector compileDropIfExists(const Blueprint &blueprint, + const BasicCommand &command) const; + + /*! Compile a rename table command. */ + QVector compileRename(const Blueprint &blueprint, + const RenameCommand &command) const; + + /*! Compile an add column command. */ + QVector compileAdd(const Blueprint &blueprint, + const BasicCommand &command) const; + /*! Compile a drop column command. */ + QVector compileDropColumn(const Blueprint &blueprint, + const DropColumnsCommand &command) const; + /*! Compile a rename column command. */ + QVector compileRenameColumn(const Blueprint &blueprint, + const RenameCommand &command) const; + + /*! Compile a primary key command. */ + QVector compilePrimary(const Blueprint &blueprint, + const IndexCommand &command) const; + /*! Compile a unique key command. */ + QVector compileUnique(const Blueprint &blueprint, + const IndexCommand &command) const; + /*! Compile a plain index key command. */ + QVector compileIndex(const Blueprint &blueprint, + const IndexCommand &command) const; + /*! Compile a fulltext index key command. */ + QVector compileFullText(const Blueprint &blueprint, + const IndexCommand &command) const override; + /*! Compile a spatial index key command. */ + QVector compileSpatialIndex(const Blueprint &blueprint, + const IndexCommand &command) const; + /*! Compile a foreign key command. */ QVector compileForeign(const Blueprint &blueprint, const ForeignKeyCommand &command) const override; + /*! Compile a drop primary key command. */ + QVector compileDropPrimary(const Blueprint &blueprint, + const IndexCommand &command) const; + /*! Compile a drop unique key command. */ + inline QVector compileDropUnique(const Blueprint &blueprint, + const IndexCommand &command) const; + /*! Compile a drop index command. */ + QVector compileDropIndex(const Blueprint &blueprint, + const IndexCommand &command) const; + /*! Compile a drop fulltext index command. */ + inline QVector + compileDropFullText(const Blueprint &blueprint, + const IndexCommand &command) const override; + /*! Compile a drop spatial index command. */ + inline QVector + compileDropSpatialIndex(const Blueprint &blueprint, + const IndexCommand &command) const; + + /*! Compile a drop foreign key command. */ + inline QVector compileDropForeign(const Blueprint &blueprint, + const IndexCommand &command) const; + + /*! Compile a rename index command. */ + QVector compileRenameIndex(const Blueprint &blueprint, + const RenameCommand &command) const; + + /*! Compile a comment command. */ + QVector compileComment(const Blueprint &blueprint, + const CommentCommand &command) const; + /*! Run command's compile method and return SQL queries. */ QVector invokeCompileMethod(const CommandDefinition &command, @@ -48,12 +145,121 @@ namespace Orm::SchemaNs::Grammars const Blueprint &blueprint) const override; protected: + /*! Create the main create table clause. */ + QString compileCreateTable(const Blueprint &blueprint) const; /*! Add the column modifiers to the definition. */ QString addModifiers(QString &&sql, const ColumnDefinition &column) const override; + /*! Compile the auto-incrementing column starting values. */ + QVector + compileAutoIncrementStartingValues(const Blueprint &blueprint) const; + + /*! Compile a drop unique key command. */ + QVector compileDropConstraint(const Blueprint &blueprint, + const IndexCommand &command) const; + /*! Get the SQL for the column data type. */ QString getType(const ColumnDefinition &column) const override; + + /*! Create the column definition for a generatable column. */ + static QString generatableColumn(QString &&type, const ColumnDefinition &column); + /*! Format the column definition for a PostGIS spatial type. */ + static QString formatPostGisType(QString &&type, const ColumnDefinition &column); + + /*! Create the column definition for a char type. */ + QString typeChar(const ColumnDefinition &column) const; + /*! Create the column definition for a string type. */ + QString typeString(const ColumnDefinition &column) const; + /*! Create the column definition for a tiny text type. */ + QString typeTinyText(const ColumnDefinition &column) const; + /*! Create the column definition for a text type. */ + QString typeText(const ColumnDefinition &column) const; + /*! Create the column definition for a medium text type. */ + QString typeMediumText(const ColumnDefinition &column) const; + /*! Create the column definition for a long text type. */ + QString typeLongText(const ColumnDefinition &column) const; + /*! Create the column definition for a big integer type. */ + QString typeBigInteger(const ColumnDefinition &column) const; + /*! Create the column definition for an integer type. */ + QString typeInteger(const ColumnDefinition &column) const; + /*! Create the column definition for a medium integer type. */ + QString typeMediumInteger(const ColumnDefinition &column) const; + /*! Create the column definition for a tiny integer type. */ + QString typeTinyInteger(const ColumnDefinition &column) const; + /*! Create the column definition for a small integer type. */ + QString typeSmallInteger(const ColumnDefinition &column) const; + /*! Create the column definition for a float type. */ + QString typeFloat(const ColumnDefinition &column) const; + /*! Create the column definition for a double type. */ + QString typeDouble(const ColumnDefinition &column) const; + /*! Create the column definition for a double type. */ + QString typeReal(const ColumnDefinition &column) const; + /*! Create the column definition for a decimal type. */ + QString typeDecimal(const ColumnDefinition &column) const; + /*! Create the column definition for a boolean type. */ + QString typeBoolean(const ColumnDefinition &column) const; + /*! Create the column definition for an enumeration type. */ + QString typeEnum(const ColumnDefinition &column) const; + /*! Create the column definition for a json type. */ + QString typeJson(const ColumnDefinition &column) const; + /*! Create the column definition for a jsonb type. */ + QString typeJsonb(const ColumnDefinition &column) const; + /*! Create the column definition for a date type. */ + QString typeDate(const ColumnDefinition &column) const; + /*! Create the column definition for a date-time type. */ + QString typeDateTime(const ColumnDefinition &column) const; + /*! Create the column definition for a date-time (with time zone) type. */ + QString typeDateTimeTz(const ColumnDefinition &column) const; + /*! Create the column definition for a time type. */ + QString typeTime(const ColumnDefinition &column) const; + /*! Create the column definition for a time (with time zone) type. */ + QString typeTimeTz(const ColumnDefinition &column) const; + /*! Create the column definition for a timestamp type. */ + QString typeTimestamp(const ColumnDefinition &column) const; + /*! Create the column definition for a timestamp (with time zone) type. */ + QString typeTimestampTz(const ColumnDefinition &column) const; + /*! Create the column definition for a year type. */ + QString typeYear(const ColumnDefinition &column) const; + /*! Create the column definition for a binary type. */ + QString typeBinary(const ColumnDefinition &column) const; + /*! Create the column definition for a uuid type. */ + QString typeUuid(const ColumnDefinition &column) const; + /*! Create the column definition for an IP address type. */ + QString typeIpAddress(const ColumnDefinition &column) const; + /*! Create the column definition for a MAC address type. */ + QString typeMacAddress(const ColumnDefinition &column) const; + /*! Create the column definition for a spatial Geometry type. */ + QString typeGeometry(const ColumnDefinition &column) const; + /*! Create the column definition for a spatial Point type. */ + QString typePoint(const ColumnDefinition &column) const; + /*! Create the column definition for a spatial LineString type. */ + QString typeLineString(const ColumnDefinition &column) const; + /*! Create the column definition for a spatial Polygon type. */ + QString typePolygon(const ColumnDefinition &column) const; + /*! Create the column definition for a spatial GeometryCollection type. */ + QString typeGeometryCollection(const ColumnDefinition &column) const; + /*! Create the column definition for a spatial MultiPoint type. */ + QString typeMultiPoint(const ColumnDefinition &column) const; + /*! Create the column definition for a spatial MultiLineString type. */ + QString typeMultiLineString(const ColumnDefinition &column) const; + /*! Create the column definition for a spatial MultiPolygon type. */ + QString typeMultiPolygon(const ColumnDefinition &column) const; + /*! Create the column definition for a spatial MultiPolygonZ type. */ + QString typeMultiPolygonZ(const ColumnDefinition &column) const; + + /*! Get the SQL for a generated virtual column modifier. */ + QString modifyVirtualAs(const ColumnDefinition &column) const; + /*! Get the SQL for a generated stored column modifier. */ + QString modifyStoredAs(const ColumnDefinition &column) const; + /*! Get the SQL for a collation column modifier. */ + QString modifyCollate(const ColumnDefinition &column) const; + /*! Get the SQL for a nullable column modifier. */ + QString modifyNullable(const ColumnDefinition &column) const; + /*! Get the SQL for a default column modifier. */ + QString modifyDefault(const ColumnDefinition &column) const; + /*! Get the SQL for an auto-increment column modifier. */ + QString modifyIncrement(const ColumnDefinition &column) const; }; /* public */ @@ -63,7 +269,36 @@ namespace Orm::SchemaNs::Grammars return true; } -} // namespace Orm::SchemaNs::Grammars + QVector + PostgresSchemaGrammar::compileDropUnique(const Blueprint &blueprint, + const IndexCommand &command) const + { + return compileDropConstraint(blueprint, command); + } + + QVector + PostgresSchemaGrammar::compileDropFullText( + const Blueprint &blueprint, const IndexCommand &command) const + { + return compileDropIndex(blueprint, command); + } + + QVector + PostgresSchemaGrammar::compileDropSpatialIndex( + const Blueprint &blueprint, const IndexCommand &command) const + { + return compileDropIndex(blueprint, command); + } + + QVector + PostgresSchemaGrammar::compileDropForeign( + const Blueprint &blueprint, const IndexCommand &command) const + { + return compileDropConstraint(blueprint, command); + } + +} // namespace Grammars +} // namespace Orm::SchemaNs TINYORM_END_COMMON_NAMESPACE diff --git a/include/orm/schema/grammars/schemagrammar.hpp b/include/orm/schema/grammars/schemagrammar.hpp index 611901971..1123d9f9f 100644 --- a/include/orm/schema/grammars/schemagrammar.hpp +++ b/include/orm/schema/grammars/schemagrammar.hpp @@ -50,9 +50,9 @@ namespace Grammars virtual QString compileDropAllViews(const QVector &views) const; /*! Compile the SQL needed to retrieve all table names. */ - virtual QString compileGetAllTables() const; + virtual QString compileGetAllTables(const QVector &databases = {}) const; /*! Compile the SQL needed to retrieve all view names. */ - virtual QString compileGetAllViews() const; + virtual QString compileGetAllViews(const QVector &databases = {}) const; /*! Compile the command to enable foreign key constraints. */ virtual QString compileEnableForeignKeyConstraints() const = 0; diff --git a/include/orm/schema/indexdefinitionreference.hpp b/include/orm/schema/indexdefinitionreference.hpp new file mode 100644 index 000000000..2dca5a817 --- /dev/null +++ b/include/orm/schema/indexdefinitionreference.hpp @@ -0,0 +1,74 @@ +#pragma once +#ifndef ORM_SCHEMA_INDEXDEFINITIONREFERENCE_HPP +#define ORM_SCHEMA_INDEXDEFINITIONREFERENCE_HPP + +#include "orm/macros/systemheader.hpp" +TINY_SYSTEM_HEADER + +#include + +#include "orm/macros/commonnamespace.hpp" +#include "orm/macros/export.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Orm::SchemaNs +{ + class IndexCommand; + + /*! Reference class to the ColumnDefinition, provides setters with a nice API + for the database index. */ + class SHAREDLIB_EXPORT IndexDefinitionReference + { + public: + /*! Constructor. */ + IndexDefinitionReference(IndexCommand &indexCommand); // NOLINT(google-explicit-constructor) + /*! Default destructor. */ + inline ~IndexDefinitionReference() = default; + + /*! Copy constructor. */ + inline IndexDefinitionReference( + const IndexDefinitionReference &) = default; + /*! Move constructor. */ + inline IndexDefinitionReference( + IndexDefinitionReference &&) noexcept = default; + + /*! Deleted copy assignment operator. */ + IndexDefinitionReference & + operator=(const IndexDefinitionReference &) = delete; + /*! Deleted move assignment operator. */ + IndexDefinitionReference & + operator=(IndexDefinitionReference &&) noexcept = delete; + + /*! Return the reference to underlying index command. */ + constexpr const IndexCommand &get() const noexcept; + /*! Return the reference to underlying index command. */ + constexpr operator const IndexCommand &() const noexcept; // NOLINT(google-explicit-constructor) + + /*! Specify an algorithm for the index (MySQL/PostgreSQL). */ + IndexDefinitionReference &algorithm(const QString &algorithm); + /*! Specify a language for the full text index (PostgreSQL). */ + IndexDefinitionReference &language(const QString &language); + + private: + /*! Reference to an index command definition. */ + std::reference_wrapper m_indexCommand; + }; + + /* public */ + + constexpr const IndexCommand &IndexDefinitionReference::get() const noexcept + { + return m_indexCommand.get(); + } + + constexpr IndexDefinitionReference::operator const IndexCommand &() const noexcept + { + return get(); + } + +} // namespace Orm::SchemaNs + +TINYORM_END_COMMON_NAMESPACE + +#endif // ORM_SCHEMA_INDEXDEFINITIONREFERENCE_HPP diff --git a/include/orm/schema/postgresschemabuilder.hpp b/include/orm/schema/postgresschemabuilder.hpp index 30d4f1887..4114ab58a 100644 --- a/include/orm/schema/postgresschemabuilder.hpp +++ b/include/orm/schema/postgresschemabuilder.hpp @@ -24,12 +24,30 @@ namespace Orm::SchemaNs /*! Virtual destructor. */ inline ~PostgresSchemaBuilder() override = default; + /*! Create a database in the schema. */ + QSqlQuery createDatabase(const QString &name) const override; + /*! Drop a database from the schema if the database exists. */ + QSqlQuery dropDatabaseIfExists(const QString &name) const override; + + /*! Drop all tables from the database. */ + void dropAllTables() const override; + /*! Drop all views from the database. */ + void dropAllViews() const override; + + /*! Get all of the table names for the database. */ + QSqlQuery getAllTables() const override; + /*! Get all of the view names for the database. */ + QSqlQuery getAllViews() const override; + /*! Get the column listing for a given table. */ QStringList getColumnListing(const QString &table) const override; + /*! Determine if the given table exists. */ + bool hasTable(const QString &table) const override; + protected: /*! Parse the table name and extract the schema and table. */ - std::pair + std::tuple parseSchemaAndTable(const QString &table) const; }; diff --git a/include/orm/schema/schemaconstants_extern.hpp b/include/orm/schema/schemaconstants_extern.hpp index bca3f9176..91351b583 100644 --- a/include/orm/schema/schemaconstants_extern.hpp +++ b/include/orm/schema/schemaconstants_extern.hpp @@ -35,6 +35,9 @@ namespace Constants SHAREDLIB_EXPORT extern const QString DropForeign; SHAREDLIB_EXPORT extern const QString RenameIndex; + // PostgreSQL specific command + SHAREDLIB_EXPORT extern const QString Comment; + // Indexes SHAREDLIB_EXPORT extern const QString Primary; SHAREDLIB_EXPORT extern const QString Unique; diff --git a/include/orm/schema/schemaconstants_inline.hpp b/include/orm/schema/schemaconstants_inline.hpp index 348e6b983..87e3c105e 100644 --- a/include/orm/schema/schemaconstants_inline.hpp +++ b/include/orm/schema/schemaconstants_inline.hpp @@ -34,6 +34,9 @@ namespace Constants inline const QString DropForeign = QStringLiteral("dropForeign"); inline const QString RenameIndex = QStringLiteral("renameIndex"); + // PostgreSQL specific command + inline const QString Comment = QStringLiteral("comment"); + // Indexes inline const QString Primary = QStringLiteral("primary"); inline const QString Unique = QStringLiteral("unique"); diff --git a/include/orm/schema/schematypes.hpp b/include/orm/schema/schematypes.hpp index 2836aafd9..ae08afc44 100644 --- a/include/orm/schema/schematypes.hpp +++ b/include/orm/schema/schematypes.hpp @@ -74,6 +74,32 @@ namespace Orm::SchemaNs std::optional value; }; + /* Common for the invokeCompileMethod() related methods */ + + /*! Concept for a member function. */ + template + concept IsMemFun = std::is_member_function_pointer_v>; + + /*! Function signature. */ + template + struct FunctionSignature; + + /*! Function signature, a member function specialization. */ + template + struct FunctionSignature + { + using type = std::tuple; + }; + + /*! Helper function to obtain function types as std::tuple. */ + template + auto argumentTypes(M &&) -> typename FunctionSignature>::type; + + /*! Helper function to obtain function parameter type at I position + from std::tuple. */ + template + auto argumentType(M &&method) -> decltype (std::get(argumentTypes(method))); + } // namespace Orm::SchemaNs TINYORM_END_COMMON_NAMESPACE diff --git a/src/orm/connectors/connectionfactory.cpp b/src/orm/connectors/connectionfactory.cpp index fc2e26bb8..d347ecb71 100644 --- a/src/orm/connectors/connectionfactory.cpp +++ b/src/orm/connectors/connectionfactory.cpp @@ -55,10 +55,10 @@ ConnectionFactory::parseConfig(QVariantHash &config, const QString &name) const normalizeDriverName(config); if (!config.contains(database_)) - config.insert(database_, QString("")); + config.insert(database_, EMPTY); if (!config.contains(prefix_)) - config.insert(prefix_, QString("")); + config.insert(prefix_, EMPTY); if (!config.contains(options_)) config.insert(options_, QVariantHash()); @@ -67,6 +67,9 @@ ConnectionFactory::parseConfig(QVariantHash &config, const QString &name) const config.insert(prefix_indexes, false); // FUTURE connector, this can be enhanced, eg. add default values per driver, eg. engine_ for mysql is missing, can not be added because is driver specific silverqx + if (config[driver_] == QPSQL && !config.contains(dont_drop)) + // spatial_ref_sys table is used by the PostGIS + config.insert(dont_drop, QStringList {QStringLiteral("spatial_ref_sys")}); return config; } @@ -74,7 +77,7 @@ ConnectionFactory::parseConfig(QVariantHash &config, const QString &name) const void ConnectionFactory::normalizeDriverName(QVariantHash &config) const { if (!config.contains(driver_)) - config.insert(driver_, QString("")); + config.insert(driver_, EMPTY); else { auto &driver = config[driver_]; diff --git a/src/orm/constants_extern.cpp b/src/orm/constants_extern.cpp index 58da11b2f..c0e77670b 100644 --- a/src/orm/constants_extern.cpp +++ b/src/orm/constants_extern.cpp @@ -41,6 +41,7 @@ namespace Orm::Constants const QString SPACE_IN = QStringLiteral("%1 %2"); const QString NOSPACE = QStringLiteral("%1%2"); const QString EMPTY = QLatin1String(""); + const QString text_ = QStringLiteral("text"); const QString QMYSQL = QStringLiteral("QMYSQL"); const QString QPSQL = QStringLiteral("QPSQL"); @@ -63,6 +64,7 @@ namespace Orm::Constants const QString options_ = QStringLiteral("options"); const QString strict_ = QStringLiteral("strict"); const QString engine_ = QStringLiteral("engine"); + const QString dont_drop = QStringLiteral("dont_drop"); const QString isolation_level = QStringLiteral("isolation_level"); const QString foreign_key_constraints = QStringLiteral("foreign_key_constraints"); @@ -83,8 +85,10 @@ namespace Orm::Constants const QString UTF8MB4 = QStringLiteral("utf8mb4"); const QString InnoDB = QStringLiteral("InnoDB"); const QString MyISAM = QStringLiteral("MyISAM"); + const QString postgres_ = QStringLiteral("postgres"); const QString UTF8MB40900aici = QStringLiteral("utf8mb4_0900_ai_ci"); + const QString UcsBasic = QStringLiteral("ucs_basic"); const QString NotImplemented = QStringLiteral("Not implemented :/."); // Comparison/logical/search operators diff --git a/src/orm/query/processors/mysqlprocessor.cpp b/src/orm/query/processors/mysqlprocessor.cpp index df0787dd0..de8f31414 100644 --- a/src/orm/query/processors/mysqlprocessor.cpp +++ b/src/orm/query/processors/mysqlprocessor.cpp @@ -8,6 +8,7 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Query::Processors { +// CUR schema, duplicate silverqx QStringList MySqlProcessor::processColumnListing(QSqlQuery &query) const { QStringList columns; diff --git a/src/orm/schema/blueprint.cpp b/src/orm/schema/blueprint.cpp index 421230249..c31ecef4b 100644 --- a/src/orm/schema/blueprint.cpp +++ b/src/orm/schema/blueprint.cpp @@ -5,6 +5,7 @@ #include #include "orm/databaseconnection.hpp" +#include "orm/schema/grammars/postgresschemagrammar.hpp" TINYORM_BEGIN_COMMON_NAMESPACE @@ -34,7 +35,10 @@ void Blueprint::build(DatabaseConnection &connection, const SchemaGrammar &gramm { // TODO clazy, old clazy check range-loop, remove after ugprade to newer clazy 1.11, it was divided to two checks in clazy 1.11 silverqx for (const auto &queryString : toSql(connection, grammar)) // clazy:exclude=range-loop,range-loop-detach - connection.statement(queryString); + /* All compile methods in the SchemaBuilders are unprepared, eg. the PostgreSQL + driver even doesn't allow to send DDL commands as prepared statements, + MySQL driver supports to send DDL commands as prepared statements. */ + connection.unprepared(queryString); } QVector Blueprint::toSql(const DatabaseConnection &connection, @@ -110,42 +114,42 @@ const RenameCommand &Blueprint::renameColumn(const QString &from, const QString return addCommand({{}, RenameColumn, from, to}); } -const IndexCommand & +IndexDefinitionReference Blueprint::primary(const QVector &columns, const QString &indexName, const QString &algorithm) { return indexCommand(Primary, columns, indexName, algorithm); } -const IndexCommand & +IndexDefinitionReference Blueprint::unique(const QVector &columns, const QString &indexName, const QString &algorithm) { return indexCommand(Unique, columns, indexName, algorithm); } -const IndexCommand & +IndexDefinitionReference Blueprint::index(const QVector &columns, const QString &indexName, const QString &algorithm) { return indexCommand(Index, columns, indexName, algorithm); // NOLINT(readability-suspicious-call-argument) } -const IndexCommand & +IndexDefinitionReference Blueprint::fullText(const QVector &columns, const QString &indexName, - const QString &algorithm) + const QString &algorithm, const QString &language) { - return indexCommand(Fulltext, columns, indexName, algorithm); + return indexCommand(Fulltext, columns, indexName, algorithm, language); } // CUR schema, it looks like spatial index can not be created on multiple columns on mysql, if its true remove QVector overloads, error 1070 Too many key parts specified; max 1 parts allowed silverqx -const IndexCommand & +IndexDefinitionReference Blueprint::spatialIndex(const QVector &columns, const QString &indexName) { return indexCommand(SpatialIndex, columns, indexName); } -const IndexCommand & +IndexDefinitionReference Blueprint::rawIndex(const Expression &expression, const QString &indexName) { return addCommand({{}, Index, indexName, {expression}}); @@ -589,7 +593,7 @@ ColumnDefinitionReference<> Blueprint::addColumnDefinition(ColumnDefinition &&de return definitionRef; } -void Blueprint::addImpliedCommands(const SchemaGrammar &/*unused*/) +void Blueprint::addImpliedCommands(const SchemaGrammar &grammar) { if (!getAddedColumns().isEmpty() && !creating()) m_commands.emplace_front(createCommand({{}, Add})); @@ -599,7 +603,7 @@ void Blueprint::addImpliedCommands(const SchemaGrammar &/*unused*/) addFluentIndexes(); -// addFluentCommands(grammar); + addFluentCommands(grammar); } void Blueprint::addFluentIndexes() @@ -659,9 +663,22 @@ void Blueprint::addFluentIndexes() } } -const IndexCommand & +void Blueprint::addFluentCommands(const SchemaGrammar &grammar) +{ + /* The PostgreSQL grammar is only one that has a fluent command, it is + the Comment command. */ + if (dynamic_cast(&grammar) == nullptr) + return; + + for (const auto &column : m_columns) + if (!column.comment.isEmpty()) + addCommand({{}, Comment, column.name, column.comment}); +} + +IndexDefinitionReference Blueprint::indexCommand(const QString &type, const QVector &columns, - const QString &indexName, const QString &algorithm) + const QString &indexName, const QString &algorithm, + const QString &language) { /* If no name was specified for this index, we will create one using a basic convention of the table name, followed by the columns, followed by an @@ -672,13 +689,16 @@ Blueprint::indexCommand(const QString &type, const QVector &columns, indexName.isEmpty() ? createIndexName(type, columns) : indexName, QVector(columns.cbegin(), columns.cend()), - algorithm}); + algorithm, language}); } const IndexCommand & Blueprint::dropIndexCommand(const QString &command, const QString &type, const QVector &columns) { + /* Although it looks weird so these two methods works, I tested them, believe me, + the dropPrimary("") drops the primary key on both MySQL and PostgreSQL. */ + // Used by dropPrimary("") if (columns.isEmpty()) return indexCommand(command, {}, {}); diff --git a/src/orm/schema/foreignkeydefinitionreference.cpp b/src/orm/schema/foreignkeydefinitionreference.cpp index 48b008a59..17fdab8b5 100644 --- a/src/orm/schema/foreignkeydefinitionreference.cpp +++ b/src/orm/schema/foreignkeydefinitionreference.cpp @@ -46,6 +46,31 @@ ForeignKeyDefinitionReference::onUpdate(const QString &action) return *this; } +ForeignKeyDefinitionReference & +ForeignKeyDefinitionReference::deferrable(const bool value) +{ + m_foreignKeyCommandDefinition.get().deferrable = value; + + return *this; +} + +ForeignKeyDefinitionReference & +ForeignKeyDefinitionReference::initiallyImmediate(const bool value) +{ + m_foreignKeyCommandDefinition.get().initiallyImmediate = value; + + return *this; +} + +ForeignKeyDefinitionReference &ForeignKeyDefinitionReference::notValid(const bool value) +{ + m_foreignKeyCommandDefinition.get().notValid = value; + + return *this; +} + +/* Shortcuts */ + ForeignKeyDefinitionReference &ForeignKeyDefinitionReference::cascadeOnUpdate() { return onUpdate(Cascade); diff --git a/src/orm/schema/grammars/mysqlschemagrammar.cpp b/src/orm/schema/grammars/mysqlschemagrammar.cpp index 15b8615b7..dac331316 100644 --- a/src/orm/schema/grammars/mysqlschemagrammar.cpp +++ b/src/orm/schema/grammars/mysqlschemagrammar.cpp @@ -40,12 +40,14 @@ QString MySqlSchemaGrammar::compileDropAllViews(const QVector &views) c return QStringLiteral("drop view %1").arg(columnize(views)); } -QString MySqlSchemaGrammar::compileGetAllTables() const +QString +MySqlSchemaGrammar::compileGetAllTables(const QVector &/*unused*/) const { return QStringLiteral("SHOW FULL TABLES WHERE table_type = 'BASE TABLE';"); } -QString MySqlSchemaGrammar::compileGetAllViews() const +QString +MySqlSchemaGrammar::compileGetAllViews(const QVector &/*unused*/) const { return QStringLiteral("SHOW FULL TABLES WHERE table_type = 'VIEW';"); } @@ -77,8 +79,6 @@ QString MySqlSchemaGrammar::compileColumnListing(const QString &/*unused*/) cons /* Compile methods for commands */ -/* public */ - QVector MySqlSchemaGrammar::compileCreate(const Blueprint &blueprint, const DatabaseConnection &connection) const @@ -97,7 +97,11 @@ MySqlSchemaGrammar::compileCreate(const Blueprint &blueprint, /* Prepare container with all sql queries, autoIncrement for every column uses alter table, so separate SQL queries are provided for every column. */ - QVector sql {sqlCreateTable}; + QVector sql; + sql.reserve(2); + + sql << std::move(sqlCreateTable); + if (!autoIncrementStartingValues.isEmpty()) sql << std::move(autoIncrementStartingValues); @@ -137,7 +141,11 @@ QVector MySqlSchemaGrammar::compileAdd(const Blueprint &blueprint, /* Prepare container with all sql queries, autoIncrement for every column uses alter table, so separate SQL queries are provided for every column. */ - QVector sql {sqlAlterTable}; + QVector sql; + sql.reserve(2); + + sql << std::move(sqlAlterTable); + if (!autoIncrementStartingValues.isEmpty()) sql << std::move(autoIncrementStartingValues); @@ -235,34 +243,6 @@ MySqlSchemaGrammar::compileRenameIndex(const Blueprint &blueprint, BaseGrammar::wrap(command.to))}; } -namespace -{ - /*! Concept for a member function. */ - template - concept IsMemFun = std::is_member_function_pointer_v>; - - /*! Function signature. */ - template - struct FunctionSignature; - - /*! Function signature, a member function specialization. */ - template - struct FunctionSignature - { - using type = std::tuple; - }; - - /*! Helper function to obtain function types as std::tuple. */ - template - auto argumentTypes(M &&) -> typename FunctionSignature>::type; - - /*! Helper function to obtain function parameter type at I position - from std::tuple. */ - template - auto argumentType(M &&method) -> decltype (std::get(argumentTypes(method))); - -} // namespace - QVector MySqlSchemaGrammar::invokeCompileMethod(const CommandDefinition &command, const DatabaseConnection &connection, @@ -331,7 +311,8 @@ MySqlSchemaGrammar::invokeCompileMethod(const CommandDefinition &command, Q_ASSERT_X(cached.contains(name), qUtf8Printable(__tiny_func__), - QStringLiteral("Compile methods map doesn't contain the '%1' key.") + QStringLiteral("Compile methods map doesn't contain the '%1' key " + "(unsupported command).") .arg(name) .toUtf8().constData()); @@ -627,7 +608,7 @@ QString MySqlSchemaGrammar::typeTinyText(const ColumnDefinition &/*unused*/) con QString MySqlSchemaGrammar::typeText(const ColumnDefinition &/*unused*/) const { - return QStringLiteral("text"); + return text_; } QString MySqlSchemaGrammar::typeMediumText(const ColumnDefinition &/*unused*/) const @@ -752,6 +733,9 @@ QString MySqlSchemaGrammar::typeTimeTz(const ColumnDefinition &column) const QString MySqlSchemaGrammar::typeTimestamp(const ColumnDefinition &column) const { + /* The behavior if the precision is omitted (or 0 of course): + >0 is ok so the default will be: timestamp and the MySQL default is 0 if omitted. + The same as in the PostgreSQL grammar. */ auto columnType = column.precision > 0 ? QStringLiteral("timestamp(%1)").arg(column.precision) : QStringLiteral("timestamp"); diff --git a/src/orm/schema/grammars/postgresschemagrammar.cpp b/src/orm/schema/grammars/postgresschemagrammar.cpp index fd3aaf58e..051e7f237 100644 --- a/src/orm/schema/grammars/postgresschemagrammar.cpp +++ b/src/orm/schema/grammars/postgresschemagrammar.cpp @@ -1,6 +1,13 @@ #include "orm/schema/grammars/postgresschemagrammar.hpp" -#include "orm/exceptions/runtimeerror.hpp" +#include + +#include "orm/databaseconnection.hpp" +#include "orm/macros/threadlocal.hpp" +#include "orm/utils/container.hpp" +#include "orm/utils/type.hpp" + +using ContainerUtils = Orm::Utils::Container; TINYORM_BEGIN_COMMON_NAMESPACE @@ -11,14 +18,62 @@ namespace Orm::SchemaNs::Grammars /* Compile methods for the SchemaBuilder */ +QString PostgresSchemaGrammar::compileCreateDatabase( + const QString &name, DatabaseConnection &connection) const +{ + return QStringLiteral("create database %1 encoding %2") + .arg(wrapValue(name), + wrapValue(connection.getConfig(charset_).value())); +} + +QString PostgresSchemaGrammar::compileDropDatabaseIfExists(const QString &name) const +{ + // CUR schema, duplicate silverqx + return QStringLiteral("drop database if exists %1").arg(wrapValue(name)); +} + +QString PostgresSchemaGrammar::compileDropAllTables(const QVector &tables) const +{ + return QStringLiteral("drop table %1 cascade").arg(columnize(tables)); +} + +QString PostgresSchemaGrammar::compileDropAllViews(const QVector &views) const +{ + return QStringLiteral("drop view %1 cascade").arg(columnize(views)); +} + +QString +PostgresSchemaGrammar::compileGetAllTables(const QVector &databases) const +{ + return QStringLiteral("select tablename from pg_catalog.pg_tables " + "where schemaname in (%1)") + .arg(quoteString(std::move(databases))); +} + +QString +PostgresSchemaGrammar::compileGetAllViews(const QVector &databases) const +{ + return QStringLiteral("select viewname from pg_catalog.pg_views " + "where schemaname in (%1)") + .arg(quoteString(std::move(databases))); +} + QString PostgresSchemaGrammar::compileEnableForeignKeyConstraints() const { - throw Exceptions::RuntimeError(NotImplemented); + return QStringLiteral("SET CONSTRAINTS ALL IMMEDIATE;"); } QString PostgresSchemaGrammar::compileDisableForeignKeyConstraints() const { - throw Exceptions::RuntimeError(NotImplemented); + return QStringLiteral("SET CONSTRAINTS ALL DEFERRED;"); +} + +QString PostgresSchemaGrammar::compileTableExists() const +{ + return QStringLiteral("select * " + "from information_schema.tables " + "where table_schema = ? and table_name = ? and " + "table_type = 'BASE TABLE'"); } QString PostgresSchemaGrammar::compileColumnListing(const QString &/*unused*/) const @@ -31,29 +86,823 @@ QString PostgresSchemaGrammar::compileColumnListing(const QString &/*unused*/) c /* Compile methods for commands */ QVector -PostgresSchemaGrammar::compileForeign(const Blueprint &/*unused*/, - const ForeignKeyCommand &/*unused*/) const +PostgresSchemaGrammar::compileCreate(const Blueprint &blueprint) const { - throw Exceptions::RuntimeError(NotImplemented); + // Primary SQL query for create table + auto sqlCreateTable = compileCreateTable(blueprint); + + // Add autoIncrement starting values to the SQL query if have been supplied + auto autoIncrementStartingValues = compileAutoIncrementStartingValues(blueprint); + + /* Prepare container with all sql queries, autoIncrement for every column uses + alter table, so separate SQL queries are provided for every column. */ + QVector sql; + sql.reserve(2); + + sql << std::move(sqlCreateTable); + + if (!autoIncrementStartingValues.isEmpty()) + sql << std::move(autoIncrementStartingValues); + + return sql; } QVector -PostgresSchemaGrammar::invokeCompileMethod(const CommandDefinition &/*unused*/, +PostgresSchemaGrammar::compileDrop(const Blueprint &blueprint, + const BasicCommand &/*unused*/) const +{ + // CUR schema, duplicate silverqx + return {QStringLiteral("drop table %1").arg(wrapTable(blueprint))}; +} + +QVector +PostgresSchemaGrammar::compileDropIfExists(const Blueprint &blueprint, + const BasicCommand &/*unused*/) const +{ + // CUR schema, duplicate silverqx + return {QStringLiteral("drop table if exists %1").arg(wrapTable(blueprint))}; +} + +QVector +PostgresSchemaGrammar::compileRename(const Blueprint &blueprint, + const RenameCommand &command) const +{ + return {QStringLiteral("alter table %1 rename to %2") + .arg(wrapTable(blueprint), BaseGrammar::wrap(command.to))}; +} + +QVector +PostgresSchemaGrammar::compileAdd(const Blueprint &blueprint, + const BasicCommand &/*unused*/) const +{ + // CUR schema, duplicate, difference only in "add column" silverqx + auto columns = prefixArray("add column", getColumns(blueprint)); + + // Add autoIncrement starting values to the SQL query if have been supplied + auto autoIncrementStartingValues = compileAutoIncrementStartingValues(blueprint); + + auto sqlAlterTable = QStringLiteral("alter table %1 %2") + .arg(wrapTable(blueprint), columnizeWithoutWrap(columns)); + + /* Prepare container with all sql queries, autoIncrement for every column uses + alter table, so separate SQL queries are provided for every column. */ + QVector sql; + sql.reserve(2); + + sql << std::move(sqlAlterTable); + + if (!autoIncrementStartingValues.isEmpty()) + sql << std::move(autoIncrementStartingValues); + + return sql; +} + +QVector +PostgresSchemaGrammar::compileDropColumn(const Blueprint &blueprint, + const DropColumnsCommand &command) const +{ + // CUR schema, duplicate, difference only in "drop column" silverqx + return {QStringLiteral("alter table %1 %2") + .arg(wrapTable(blueprint), + columnizeWithoutWrap(prefixArray("drop column", + wrapArray(command.columns))))}; +} + +QVector +PostgresSchemaGrammar::compileRenameColumn(const Blueprint &blueprint, + const RenameCommand &command) const +{ + // CUR schema, duplicate silverqx + return {QStringLiteral("alter table %1 rename column %2 to %3") + .arg(wrapTable(blueprint), BaseGrammar::wrap(command.from), + BaseGrammar::wrap(command.to))}; +} + +QVector +PostgresSchemaGrammar::compilePrimary(const Blueprint &blueprint, + const IndexCommand &command) const +{ + return {QStringLiteral("alter table %1 add primary key (%2)") + .arg(wrapTable(blueprint), columnize(command.columns))}; +} + +QVector +PostgresSchemaGrammar::compileUnique(const Blueprint &blueprint, + const IndexCommand &command) const +{ + return {QStringLiteral("alter table %1 add constraint %2 unique (%3)") + .arg(wrapTable(blueprint), BaseGrammar::wrap(command.index), + columnize(command.columns))}; +} + +QVector +PostgresSchemaGrammar::compileIndex(const Blueprint &blueprint, + const IndexCommand &command) const +{ + auto algorithm = command.algorithm.isEmpty() + ? EMPTY + : QStringLiteral(" using %1").arg(command.algorithm); + + return {QStringLiteral("create index %1 on %2%3 (%4)") + .arg(BaseGrammar::wrap(command.index), wrapTable(blueprint), + std::move(algorithm), columnize(command.columns))}; +} + +QVector +PostgresSchemaGrammar::compileFullText(const Blueprint &blueprint, + const IndexCommand &command) const +{ + static const auto TsVectorTmpl = QStringLiteral("to_tsvector(%1, %2)"); + + const auto language = command.language.isEmpty() ? QStringLiteral("english") + : command.language; + + const auto columns = + ranges::views::transform(command.columns, + [this, &language, &TsVectorTmpl = TsVectorTmpl] + (const auto &column) + { + return TsVectorTmpl.arg(quoteString(language), BaseGrammar::wrap(column)); + }) + | ranges::to>(); + + /* Double (()) described here, simply it's a expression not the column name: + https://www.postgresql.org/docs/10/indexes-expressional.html */ + return {QStringLiteral("create index %1 on %2 using gin ((%3))") + .arg(BaseGrammar::wrap(command.index), + wrapTable(blueprint), + ContainerUtils::join(columns, QStringLiteral(" || ")))}; +} + +QVector +PostgresSchemaGrammar::compileSpatialIndex(const Blueprint &blueprint, + const IndexCommand &command) const +{ + const_cast(command).algorithm = QStringLiteral("gist"); + + return compileIndex(blueprint, command); +} + +QVector +PostgresSchemaGrammar::compileForeign(const Blueprint &blueprint, + const ForeignKeyCommand &command) const +{ + auto sqlCommands = SchemaGrammar::compileForeign(blueprint, command); + + Q_ASSERT(sqlCommands.size() == 1); + + auto &sql = sqlCommands[0]; + + const auto isDeferrable = command.deferrable.has_value(); + + if (isDeferrable) + sql.append(*command.deferrable ? QStringLiteral(" deferrable") + : QStringLiteral(" not deferrable")); + + if (isDeferrable && *command.deferrable && command.initiallyImmediate) + sql.append(*command.initiallyImmediate ? QStringLiteral(" initially immediate") + : QStringLiteral(" initially deferred")); + + if (command.notValid && *command.notValid) + sql.append(QStringLiteral(" not valid")); + + return sqlCommands; +} + +QVector +PostgresSchemaGrammar::compileDropPrimary(const Blueprint &blueprint, + const IndexCommand &/*unused*/) const +{ + auto index = BaseGrammar::wrap(QStringLiteral("%1_pkey").arg(blueprint.getTable())); + + return {QStringLiteral("alter table %1 drop constraint %2") + .arg(wrapTable(blueprint), std::move(index))}; +} + +QVector +PostgresSchemaGrammar::compileDropIndex(const Blueprint &/*unused*/, + const IndexCommand &command) const +{ + return {QStringLiteral("drop index %1").arg(BaseGrammar::wrap(command.index))}; +} + +QVector +PostgresSchemaGrammar::compileRenameIndex(const Blueprint &/*unused*/, + const RenameCommand &command) const +{ + return {QStringLiteral("alter index %1 rename to %2") + .arg(BaseGrammar::wrap(command.from), BaseGrammar::wrap(command.to))}; +} + +QVector +PostgresSchemaGrammar::compileComment(const Blueprint &blueprint, + const CommentCommand &command) const +{ + auto comment = command.comment; + // Escape single quotes + comment.replace(QChar('\''), QStringLiteral("''")); + + return {QStringLiteral("comment on column %1.%2 is %3") + .arg(wrapTable(blueprint), BaseGrammar::wrap(command.column), + quoteString(comment))}; +} + +QVector +PostgresSchemaGrammar::invokeCompileMethod(const CommandDefinition &command, const DatabaseConnection &/*unused*/, - const Blueprint &/*unused*/) const + const Blueprint &blueprint) const { - throw Exceptions::RuntimeError(NotImplemented); + // FUTURE concepts, somehow check that after reinterpret_cast<> is command_.name QString, i have tried but without success, I have added example to NOTES.txt silverqx + const auto &basicCommand = reinterpret_cast(command); + const auto &name = basicCommand.name; + + if (name == Create) + return compileCreate(blueprint); + + /*! Type for the compileXx() methods. */ + using CompileMemFn = + std::function( + const PostgresSchemaGrammar &, const Blueprint &, + const CommandDefinition &)>; + + const auto bind = [](auto &&compileMethod) + { + return [compileMethod = std::forward(compileMethod)] + (const PostgresSchemaGrammar &grammar, const Blueprint &blueprint_, + const CommandDefinition &command_) // clazy:exclude=function-args-by-value + { + /* Get type of a second parameter of compile method and cast to that type. */ + const auto &castedCommand = + reinterpret_cast(compileMethod))>(command_); + + return std::invoke(compileMethod, grammar, blueprint_, castedCommand); + }; + }; + + // CUR schema, use enum, indexCommand() calls createIndexName() that needs also command.name silverqx + /* Pointers to a command's compile member methods by a command name, yes yes c++ 😂. + I have to map by QString instead of enum struct because a command.name is used + to look up, I could use enum struct but I had to map + QString(command.name) -> enum. */ + T_THREAD_LOCAL + static const std::unordered_map cached { + {Add, bind(&PostgresSchemaGrammar::compileAdd)}, + {Rename, bind(&PostgresSchemaGrammar::compileRename)}, + {Drop, bind(&PostgresSchemaGrammar::compileDrop)}, + {DropIfExists, bind(&PostgresSchemaGrammar::compileDropIfExists)}, + + {DropColumn, bind(&PostgresSchemaGrammar::compileDropColumn)}, + {RenameColumn, bind(&PostgresSchemaGrammar::compileRenameColumn)}, + + {Primary, bind(&PostgresSchemaGrammar::compilePrimary)}, + {Unique, bind(&PostgresSchemaGrammar::compileUnique)}, + {Index, bind(&PostgresSchemaGrammar::compileIndex)}, + {Fulltext, bind(&PostgresSchemaGrammar::compileFullText)}, + {SpatialIndex, bind(&PostgresSchemaGrammar::compileSpatialIndex)}, + {Foreign, bind(&PostgresSchemaGrammar::compileForeign)}, + + {DropPrimary, bind(&PostgresSchemaGrammar::compileDropPrimary)}, + {DropUnique, bind(&PostgresSchemaGrammar::compileDropUnique)}, + {DropIndex, bind(&PostgresSchemaGrammar::compileDropIndex)}, + {DropFullText, bind(&PostgresSchemaGrammar::compileDropFullText)}, + {DropSpatialIndex, bind(&PostgresSchemaGrammar::compileDropSpatialIndex)}, + {DropForeign, bind(&PostgresSchemaGrammar::compileDropForeign)}, + + {RenameIndex, bind(&PostgresSchemaGrammar::compileRenameIndex)}, + // PostgreSQL specific + {Comment, bind(&PostgresSchemaGrammar::compileComment)}, + }; + + Q_ASSERT_X(cached.contains(name), + qUtf8Printable(__tiny_func__), + QStringLiteral("Compile methods map doesn't contain the '%1' key " + "(unsupported command).") + .arg(name) + .toUtf8().constData()); + + return std::invoke(cached.at(name), *this, blueprint, command); } -QString PostgresSchemaGrammar::addModifiers(QString &&/*unused*/, - const ColumnDefinition &/*unused*/) const +/* protected */ + +// CUR schema, duplicate silverqx +QString PostgresSchemaGrammar::compileCreateTable(const Blueprint &blueprint) const { - throw Exceptions::RuntimeError(NotImplemented); + return QStringLiteral("%1 table %2 (%3)") + .arg(blueprint.isTemporary() ? QStringLiteral("create temporary") + : Create, + wrapTable(blueprint), + columnizeWithoutWrap(getColumns(blueprint))) + .trimmed(); } -QString PostgresSchemaGrammar::getType(const ColumnDefinition &/*unused*/) const +QString PostgresSchemaGrammar::addModifiers(QString &&sql, + const ColumnDefinition &column) const { - throw Exceptions::RuntimeError(NotImplemented); + // CUR schema, should be T_THREAD_LOCAL? silverqx + constexpr static std::array modifierMethods { + &PostgresSchemaGrammar::modifyCollate, &PostgresSchemaGrammar::modifyIncrement, + &PostgresSchemaGrammar::modifyNullable, &PostgresSchemaGrammar::modifyDefault, + &PostgresSchemaGrammar::modifyVirtualAs, &PostgresSchemaGrammar::modifyStoredAs, + }; + + for (const auto method : modifierMethods) + sql.append(std::invoke(method, this, column)); + + return std::move(sql); +} + +QVector +PostgresSchemaGrammar::compileAutoIncrementStartingValues( + const Blueprint &blueprint) const +{ + const auto autoIncrementStartingValues = blueprint.autoIncrementStartingValues(); + + // Nothing to compile + if (autoIncrementStartingValues.isEmpty()) + return {}; + + return autoIncrementStartingValues + | ranges::views::transform([&blueprint](const auto &startingValue) + -> QString + { + Q_ASSERT(startingValue.value); + + return QStringLiteral(R"(alter sequence "%1_%2_seq" restart with %3)") + .arg(blueprint.getTable()) + .arg(startingValue.columnName) + .arg(*startingValue.value); + }) + | ranges::to>(); +} + +QVector +PostgresSchemaGrammar::compileDropConstraint(const Blueprint &blueprint, + const IndexCommand &command) const +{ + return {QStringLiteral("alter table %1 drop constraint %2") + .arg(wrapTable(blueprint), BaseGrammar::wrap(command.index))}; +} + +QString PostgresSchemaGrammar::getType(const ColumnDefinition &column) const +{ + switch (column.type) { + case ColumnType::Char: + return typeChar(column); + + case ColumnType::String: + return typeString(column); + + case ColumnType::Text: + return typeText(column); + + case ColumnType::TinyText: + return typeTinyText(column); + + case ColumnType::MediumText: + return typeMediumText(column); + + case ColumnType::LongText: + return typeLongText(column); + + case ColumnType::Integer: + return typeInteger(column); + + case ColumnType::TinyInteger: + return typeTinyInteger(column); + + case ColumnType::SmallInteger: + return typeSmallInteger(column); + + case ColumnType::MediumInteger: + return typeMediumInteger(column); + + case ColumnType::BigInteger: + return typeBigInteger(column); + + case ColumnType::Float: + return typeFloat(column); + + case ColumnType::Double: + return typeDouble(column); + + case ColumnType::Decimal: + return typeDecimal(column); + + // PostgreSQL only + case ColumnType::Real: + return typeReal(column); + + case ColumnType::Boolean: + return typeBoolean(column); + + case ColumnType::Enum: + return typeEnum(column); + + // MySQL only +// case ColumnType::Set: +// return typeSet(column); + + case ColumnType::Json: + return typeJson(column); + + case ColumnType::Jsonb: + return typeJsonb(column); + + case ColumnType::Date: + return typeDate(column); + + case ColumnType::DateTime: + return typeDateTime(column); + + case ColumnType::DateTimeTz: + return typeDateTimeTz(column); + + case ColumnType::Time: + return typeTime(column); + + case ColumnType::TimeTz: + return typeTimeTz(column); + + case ColumnType::Timestamp: + return typeTimestamp(column); + + case ColumnType::TimestampTz: + return typeTimestampTz(column); + + case ColumnType::Year: + return typeYear(column); + + case ColumnType::Binary: + return typeBinary(column); + + case ColumnType::Uuid: + return typeUuid(column); + + case ColumnType::IpAddress: + return typeIpAddress(column); + + case ColumnType::MacAddress: + return typeMacAddress(column); + + case ColumnType::Geometry: + return typeGeometry(column); + + case ColumnType::Point: + return typePoint(column); + + case ColumnType::LineString: + return typeLineString(column); + + case ColumnType::Polygon: + return typePolygon(column); + + case ColumnType::GeometryCollection: + return typeGeometryCollection(column); + + case ColumnType::MultiPoint: + return typeMultiPoint(column); + + case ColumnType::MultiLineString: + return typeMultiLineString(column); + + case ColumnType::MultiPolygon: + return typeMultiPolygon(column); + + // PostgreSQL only + case ColumnType::MultiPolygonZ: + return typeMultiPolygonZ(column); + + default: + break; + } + + throw Exceptions::RuntimeError( + QStringLiteral("Unsupported column type in %1().").arg(__tiny_func__)); +} + +QString +PostgresSchemaGrammar::generatableColumn(QString &&type, const ColumnDefinition &column) +{ + if (!column.autoIncrement && column.generatedAs.isEmpty()) + return std::move(type); + + static const std::unordered_map GeneratableMap { + {QStringLiteral("smallint"), QStringLiteral("smallserial")}, + {QStringLiteral("integer"), QStringLiteral("serial")}, + {QStringLiteral("bigint"), QStringLiteral("bigserial")}, + }; + + if (column.autoIncrement && column.generatedAs.isEmpty()) + return GeneratableMap.at(type); + + QString options; + + if (!column.generatedAs.isEmpty()) + options = QStringLiteral(" (%1)").arg(column.generatedAs); + + return QStringLiteral("%1 generated %2 as identity%3") + .arg(std::move(type), + column.always ? QStringLiteral("always") : QStringLiteral("by default"), + std::move(options)); +} + +QString +PostgresSchemaGrammar::formatPostGisType(QString &&type, const ColumnDefinition &column) +{ + if (!column.isGeometry) + return QStringLiteral("geography(%1, %2)") + .arg(std::move(type), column.srid ? QString::number(*column.srid) + : QStringLiteral("4326")); + + // NOTE api different, Eloquent uses the column.projection for this, I'm reusing the column.srid silverqx + if (column.srid) + return QStringLiteral("geometry(%1, %2)") + .arg(std::move(type), QString::number(*column.srid)); + + return QStringLiteral("geometry(%1)").arg(std::move(type)); +} + +QString PostgresSchemaGrammar::typeChar(const ColumnDefinition &column) const +{ + return QStringLiteral("char(%1)").arg(column.length); +} + +QString PostgresSchemaGrammar::typeString(const ColumnDefinition &column) const +{ + return QStringLiteral("varchar(%1)").arg(column.length); +} + +QString PostgresSchemaGrammar::typeTinyText(const ColumnDefinition &/*unused*/) const +{ + return QStringLiteral("varchar(255)"); +} + +QString PostgresSchemaGrammar::typeText(const ColumnDefinition &/*unused*/) const +{ + return text_; +} + +QString PostgresSchemaGrammar::typeMediumText(const ColumnDefinition &/*unused*/) const +{ + return text_; +} + +QString PostgresSchemaGrammar::typeLongText(const ColumnDefinition &/*unused*/) const +{ + return text_; +} + +QString PostgresSchemaGrammar::typeBigInteger(const ColumnDefinition &column) const +{ + return generatableColumn("bigint", column); +} + +QString PostgresSchemaGrammar::typeInteger(const ColumnDefinition &column) const +{ + return generatableColumn("integer", column); +} + +QString PostgresSchemaGrammar::typeMediumInteger(const ColumnDefinition &column) const +{ + return generatableColumn("integer", column); +} + +QString PostgresSchemaGrammar::typeTinyInteger(const ColumnDefinition &column) const +{ + return generatableColumn("smallint", column); +} + +QString PostgresSchemaGrammar::typeSmallInteger(const ColumnDefinition &column) const +{ + return generatableColumn("smallint", column); +} + +QString PostgresSchemaGrammar::typeFloat(const ColumnDefinition &column) const +{ + return typeDouble(column); +} + +QString PostgresSchemaGrammar::typeDouble(const ColumnDefinition &/*unused*/) const +{ + return QStringLiteral("double precision"); +} + +QString PostgresSchemaGrammar::typeReal(const ColumnDefinition &/*unused*/) const +{ + return QStringLiteral("real"); +} + +QString PostgresSchemaGrammar::typeDecimal(const ColumnDefinition &column) const +{ + // CUR schema, duplicate and many others are duplicates silverqx + Q_ASSERT(column.total && column.places); + + return QStringLiteral("decimal(%1, %2)").arg(*column.total).arg(*column.places); +} + +QString PostgresSchemaGrammar::typeBoolean(const ColumnDefinition &/*unused*/) const +{ + return QStringLiteral("boolean"); +} + +QString PostgresSchemaGrammar::typeEnum(const ColumnDefinition &column) const +{ + return QStringLiteral(R"(varchar(255) check ("%1" in (%2)))") + .arg(column.name, quoteString(column.allowed)); +} + +QString PostgresSchemaGrammar::typeJson(const ColumnDefinition &/*unused*/) const +{ + return QStringLiteral("json"); +} + +QString PostgresSchemaGrammar::typeJsonb(const ColumnDefinition &/*unused*/) const +{ + return QStringLiteral("jsonb"); +} + +QString PostgresSchemaGrammar::typeDate(const ColumnDefinition &/*unused*/) const +{ + return QStringLiteral("date"); +} + +QString PostgresSchemaGrammar::typeDateTime(const ColumnDefinition &column) const +{ + return typeTimestamp(column); +} + +QString PostgresSchemaGrammar::typeDateTimeTz(const ColumnDefinition &column) const +{ + return typeTimestampTz(column); +} + +QString PostgresSchemaGrammar::typeTime(const ColumnDefinition &column) const +{ + return QStringLiteral("time%1 without time zone") + .arg(column.precision > 0 ? QStringLiteral("(%1)").arg(column.precision) + : EMPTY); +} + +QString PostgresSchemaGrammar::typeTimeTz(const ColumnDefinition &column) const +{ + return QStringLiteral("time%1 with time zone") + .arg(column.precision > 0 ? QStringLiteral("(%1)").arg(column.precision) + : EMPTY); +} + +QString PostgresSchemaGrammar::typeTimestamp(const ColumnDefinition &column) const +{ + auto columnType = + QStringLiteral("timestamp%1 without time zone") + /* The behavior if the precision is omitted: + >-1 is ok so the default will be timestamp(0), the same as + in the MySQL grammar. */ + .arg(column.precision > -1 ? QStringLiteral("(%1)").arg(column.precision) + : EMPTY); + + return column.useCurrent + ? QStringLiteral("%1 default CURRENT_TIMESTAMP").arg(std::move(columnType)) + : std::move(columnType); +} + +QString PostgresSchemaGrammar::typeTimestampTz(const ColumnDefinition &column) const +{ + auto columnType = + QStringLiteral("timestamp%1 with time zone") + .arg(column.precision > 0 ? QStringLiteral("(%1)").arg(column.precision) + : EMPTY); + + return column.useCurrent + ? QStringLiteral("%1 default CURRENT_TIMESTAMP").arg(std::move(columnType)) + : std::move(columnType); +} + +QString PostgresSchemaGrammar::typeYear(const ColumnDefinition &column) const +{ + return typeInteger(column); +} + +QString PostgresSchemaGrammar::typeBinary(const ColumnDefinition &/*unused*/) const +{ + return QStringLiteral("bytea"); +} + +QString PostgresSchemaGrammar::typeUuid(const ColumnDefinition &/*unused*/) const +{ + return QStringLiteral("uuid"); +} + +QString PostgresSchemaGrammar::typeIpAddress(const ColumnDefinition &/*unused*/) const +{ + return QStringLiteral("inet"); +} + +QString PostgresSchemaGrammar::typeMacAddress(const ColumnDefinition &/*unused*/) const +{ + return QStringLiteral("macaddr"); +} + +QString PostgresSchemaGrammar::typeGeometry(const ColumnDefinition &column) const +{ + return formatPostGisType(QStringLiteral("geometry"), column); +} + +QString PostgresSchemaGrammar::typePoint(const ColumnDefinition &column) const +{ + return formatPostGisType(QStringLiteral("point"), column); +} + +QString PostgresSchemaGrammar::typeLineString(const ColumnDefinition &column) const +{ + return formatPostGisType(QStringLiteral("linestring"), column); +} + +QString PostgresSchemaGrammar::typePolygon(const ColumnDefinition &column) const +{ + return formatPostGisType(QStringLiteral("polygon"), column); +} + +QString +PostgresSchemaGrammar::typeGeometryCollection(const ColumnDefinition &column) const +{ + return formatPostGisType(QStringLiteral("geometrycollection"), column); +} + +QString PostgresSchemaGrammar::typeMultiPoint(const ColumnDefinition &column) const +{ + return formatPostGisType(QStringLiteral("multipoint"), column); +} + +QString PostgresSchemaGrammar::typeMultiLineString(const ColumnDefinition &column) const +{ + return formatPostGisType(QStringLiteral("multilinestring"), column); +} + +QString PostgresSchemaGrammar::typeMultiPolygon(const ColumnDefinition &column) const +{ + return formatPostGisType(QStringLiteral("multipolygon"), column); +} + +QString PostgresSchemaGrammar::typeMultiPolygonZ(const ColumnDefinition &column) const +{ + return formatPostGisType(QStringLiteral("multipolygonz"), column); +} + +QString PostgresSchemaGrammar::modifyVirtualAs(const ColumnDefinition &column) const +{ + if (column.virtualAs.isEmpty()) + return {}; + + return QStringLiteral(" generated always as (%1)").arg(column.virtualAs); +} + +QString PostgresSchemaGrammar::modifyStoredAs(const ColumnDefinition &column) const +{ + if (column.storedAs.isEmpty()) + return {}; + + return QStringLiteral(" generated always as (%1) stored").arg(column.storedAs); +} + +QString PostgresSchemaGrammar::modifyCollate(const ColumnDefinition &column) const +{ + if (column.collation.isEmpty()) + return {}; + + return QStringLiteral(" collate %1").arg(wrapValue(column.collation)); +} + +QString PostgresSchemaGrammar::modifyNullable(const ColumnDefinition &column) const +{ + return column.nullable ? QStringLiteral(" null") : QStringLiteral(" not null"); +} + +QString PostgresSchemaGrammar::modifyDefault(const ColumnDefinition &column) const +{ + // CUR schema, duplicate silverqx + const auto &defaultValue = column.defaultValue; + + if (!defaultValue.isValid() || defaultValue.isNull()) + return {}; + + // CUR schema, note about security in docs, unprepared and unescaped silverqx + return QStringLiteral(" default %1").arg(getDefaultValue(defaultValue)); +} + +QString PostgresSchemaGrammar::modifyIncrement(const ColumnDefinition &column) const +{ + static const std::unordered_set serials { + ColumnType::BigInteger, ColumnType::Integer, ColumnType::MediumInteger, + ColumnType::SmallInteger, ColumnType::TinyInteger + }; + + if ((serials.contains(column.type) || !column.generatedAs.isEmpty()) && + column.autoIncrement + ) + return QStringLiteral(" primary key"); + + return {}; } } // namespace Orm::SchemaNs::Grammars diff --git a/src/orm/schema/grammars/schemagrammar.cpp b/src/orm/schema/grammars/schemagrammar.cpp index 66279368d..7f3671335 100644 --- a/src/orm/schema/grammars/schemagrammar.cpp +++ b/src/orm/schema/grammars/schemagrammar.cpp @@ -37,12 +37,12 @@ QString SchemaGrammar::compileDropAllViews(const QVector &/*unused*/) c throw Exceptions::RuntimeError(NotImplemented); } -QString SchemaGrammar::compileGetAllTables() const +QString SchemaGrammar::compileGetAllTables(const QVector &/*unused*/) const { throw Exceptions::RuntimeError(NotImplemented); } -QString SchemaGrammar::compileGetAllViews() const +QString SchemaGrammar::compileGetAllViews(const QVector &/*unused*/) const { throw Exceptions::RuntimeError(NotImplemented); } diff --git a/src/orm/schema/indexdefinitionreference.cpp b/src/orm/schema/indexdefinitionreference.cpp new file mode 100644 index 000000000..fa2ba3f65 --- /dev/null +++ b/src/orm/schema/indexdefinitionreference.cpp @@ -0,0 +1,34 @@ +#include "orm/schema/indexdefinitionreference.hpp" + +#include "orm/schema/columndefinition.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Orm::SchemaNs +{ + +/* public */ + +IndexDefinitionReference::IndexDefinitionReference(IndexCommand &indexCommand) + : m_indexCommand(indexCommand) +{} + +IndexDefinitionReference & +IndexDefinitionReference::algorithm(const QString &algorithm) +{ + m_indexCommand.get().algorithm = algorithm; + + return *this; +} + +IndexDefinitionReference & +IndexDefinitionReference::language(const QString &language) +{ + m_indexCommand.get().language = language; + + return *this; +} + +} // namespace Orm::SchemaNs + +TINYORM_END_COMMON_NAMESPACE diff --git a/src/orm/schema/mysqlschemabuilder.cpp b/src/orm/schema/mysqlschemabuilder.cpp index 1b8b6744b..2379ae076 100644 --- a/src/orm/schema/mysqlschemabuilder.cpp +++ b/src/orm/schema/mysqlschemabuilder.cpp @@ -10,6 +10,8 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::SchemaNs { +/* public */ + QSqlQuery MySqlSchemaBuilder::createDatabase(const QString &name) const { return m_connection.statement( @@ -73,21 +75,21 @@ void MySqlSchemaBuilder::dropAllViews() const QSqlQuery MySqlSchemaBuilder::getAllTables() const { // CUR schema, use postproccessor processColumnListing() silverqx - return m_connection.select(m_grammar.compileGetAllTables()); + return m_connection.selectFromWriteConnection(m_grammar.compileGetAllTables()); } QSqlQuery MySqlSchemaBuilder::getAllViews() const { - return m_connection.select(m_grammar.compileGetAllViews()); + return m_connection.selectFromWriteConnection(m_grammar.compileGetAllViews()); } QStringList MySqlSchemaBuilder::getColumnListing(const QString &table) const { const auto table_ = NOSPACE.arg(m_connection.getTablePrefix(), table); - auto query = m_connection.select(m_grammar.compileColumnListing(), { - m_connection.getDatabaseName(), table_ - }); + auto query = m_connection.selectFromWriteConnection( + m_grammar.compileColumnListing(), + {m_connection.getDatabaseName(), table_}); return m_connection.getPostProcessor().processColumnListing(query); } @@ -98,9 +100,9 @@ bool MySqlSchemaBuilder::hasTable(const QString &table) const Q_ASSERT(m_connection.driver()->hasFeature(QSqlDriver::QuerySize)); - return m_connection.select(m_grammar.compileTableExists(), - {m_connection.getDatabaseName(), table_}) - .size() > 0; + return m_connection.selectFromWriteConnection( + m_grammar.compileTableExists(), + {m_connection.getDatabaseName(), table_}).size() > 0; } } // namespace Orm::SchemaNs diff --git a/src/orm/schema/postgresschemabuilder.cpp b/src/orm/schema/postgresschemabuilder.cpp index 942a830ec..74756c52c 100644 --- a/src/orm/schema/postgresschemabuilder.cpp +++ b/src/orm/schema/postgresschemabuilder.cpp @@ -1,5 +1,8 @@ #include "orm/schema/postgresschemabuilder.hpp" +#include +#include + #include "orm/databaseconnection.hpp" TINYORM_BEGIN_COMMON_NAMESPACE @@ -9,20 +12,130 @@ namespace Orm::SchemaNs /* public */ +QSqlQuery PostgresSchemaBuilder::createDatabase(const QString &name) const +{ + // CUR schema, duplicates silverqx + return m_connection.unprepared( + m_grammar.compileCreateDatabase(name, m_connection)); +} + +QSqlQuery PostgresSchemaBuilder::dropDatabaseIfExists(const QString &name) const +{ + // CUR schema, duplicates silverqx + return m_connection.unprepared( + m_grammar.compileDropDatabaseIfExists(name)); +} + +// CUR schema, test in functional tests silverqx +void PostgresSchemaBuilder::dropAllTables() const +{ + auto query = getAllTables(); + + // No fields in the record + if (query.record().isEmpty()) + return; + + /* ConnectionFactory provides a default value 'spatial_ref_sys' for this option + for the QPSQL driver. */ + const auto excludedTables = m_connection.getConfig(dont_drop).value(); + + QVector tables; + if (const auto size = query.size(); size > 0) + tables.reserve(size); + + while (query.next()) + if (auto table = query.value(0).value(); + !excludedTables.contains(table) + ) + tables << std::move(table); + + if (tables.isEmpty()) + return; + + m_connection.unprepared(m_grammar.compileDropAllTables(tables)); +} + +// CUR schema, duplicate silverqx +// CUR schema, test in functional tests silverqx +void PostgresSchemaBuilder::dropAllViews() const +{ + auto query = getAllViews(); + + // No fields in the record + if (query.record().isEmpty()) + return; + + /* For these it throws that needed by the postgis extension and proposes to delete + extension instead, so exclude them. */ + const QSet excludedViews {QStringLiteral("geography_columns"), + QStringLiteral("geometry_columns")}; + + QVector views; + if (const auto size = query.size(); size > 0) + views.reserve(size); + + while (query.next()) + if (auto view = query.value(0).value(); + !excludedViews.contains(view) + ) + views << std::move(view); + + if (views.isEmpty()) + return; + + m_connection.unprepared(m_grammar.compileDropAllViews(views)); +} + +QSqlQuery PostgresSchemaBuilder::getAllTables() const +{ + auto schemaList = m_connection.getConfig(schema_).value(); + + QVector schema; + std::ranges::move(schemaList, std::back_inserter(schema)); + + // CUR schema, use postproccessor processColumnListing() silverqx + return m_connection.selectFromWriteConnection( + m_grammar.compileGetAllTables(std::move(schema))); +} + +QSqlQuery PostgresSchemaBuilder::getAllViews() const +{ + auto schemaList = m_connection.getConfig(schema_).value(); + + QVector schema; + std::ranges::move(schemaList, std::back_inserter(schema)); + + return m_connection.selectFromWriteConnection( + m_grammar.compileGetAllViews(std::move(schema))); +} + QStringList PostgresSchemaBuilder::getColumnListing(const QString &table) const { auto [schema, table_] = parseSchemaAndTable(table); table_ = NOSPACE.arg(m_connection.getTablePrefix(), table); - auto query = m_connection.select(m_grammar.compileColumnListing(), { - schema, table_ - }); + auto query = m_connection.selectFromWriteConnection( + m_grammar.compileColumnListing(), {schema, table_}); return m_connection.getPostProcessor().processColumnListing(query); } -std::pair +bool PostgresSchemaBuilder::hasTable(const QString &table) const +{ + auto [schema, table_] = parseSchemaAndTable(table); + + table_ = NOSPACE.arg(m_connection.getTablePrefix(), table); + + Q_ASSERT(m_connection.driver()->hasFeature(QSqlDriver::QuerySize)); + + return m_connection.selectFromWriteConnection( + m_grammar.compileTableExists(), {schema, table_}).size() > 0; +} + +/* protected */ + +std::tuple PostgresSchemaBuilder::parseSchemaAndTable(const QString &table) const { QString schema; diff --git a/src/orm/schema/schemaconstants_extern.cpp b/src/orm/schema/schemaconstants_extern.cpp index d11a762eb..645da9e43 100644 --- a/src/orm/schema/schemaconstants_extern.cpp +++ b/src/orm/schema/schemaconstants_extern.cpp @@ -6,21 +6,24 @@ namespace Orm::SchemaNs::Constants { // Command names - const QString Create = QStringLiteral("create"); - const QString Add = QStringLiteral("add"); - const QString Change = QStringLiteral("change"); - const QString Drop = QStringLiteral("drop"); - const QString DropIfExists = QStringLiteral("dropIfExists"); - const QString Rename = QStringLiteral("rename"); - const QString DropColumn = QStringLiteral("dropColumn"); - const QString RenameColumn = QStringLiteral("renameColumn"); - const QString DropPrimary = QStringLiteral("dropPrimary"); - const QString DropUnique = QStringLiteral("dropUnique"); - const QString DropIndex = QStringLiteral("dropIndex"); - const QString DropFullText = QStringLiteral("dropFullText"); - const QString DropSpatialIndex = QStringLiteral("dropSpatialIndex"); - const QString DropForeign = QStringLiteral("dropForeign"); - const QString RenameIndex = QStringLiteral("renameIndex"); + const QString Create = QStringLiteral("create"); + const QString Add = QStringLiteral("add"); + const QString Change = QStringLiteral("change"); + const QString Drop = QStringLiteral("drop"); + const QString DropIfExists = QStringLiteral("dropIfExists"); + const QString Rename = QStringLiteral("rename"); + const QString DropColumn = QStringLiteral("dropColumn"); + const QString RenameColumn = QStringLiteral("renameColumn"); + const QString DropPrimary = QStringLiteral("dropPrimary"); + const QString DropUnique = QStringLiteral("dropUnique"); + const QString DropIndex = QStringLiteral("dropIndex"); + const QString DropFullText = QStringLiteral("dropFullText"); + const QString DropSpatialIndex = QStringLiteral("dropSpatialIndex"); + const QString DropForeign = QStringLiteral("dropForeign"); + const QString RenameIndex = QStringLiteral("renameIndex"); + + // PostgreSQL specific command + const QString Comment = QStringLiteral("comment"); // Indexes const QString Primary = QStringLiteral("primary"); diff --git a/src/src.pri b/src/src.pri index 428112e24..8ae531905 100644 --- a/src/src.pri +++ b/src/src.pri @@ -43,6 +43,7 @@ sourcesList += \ $$PWD/orm/schema/grammars/postgresschemagrammar.cpp \ $$PWD/orm/schema/grammars/schemagrammar.cpp \ $$PWD/orm/schema/grammars/sqliteschemagrammar.cpp \ + $$PWD/orm/schema/indexdefinitionreference.cpp \ $$PWD/orm/schema/mysqlschemabuilder.cpp \ $$PWD/orm/schema/postgresschemabuilder.cpp \ $$PWD/orm/schema/schemabuilder.cpp \ diff --git a/tests/TinyUtils/src/databases.cpp b/tests/TinyUtils/src/databases.cpp index 0c3b62cee..30b549a52 100644 --- a/tests/TinyUtils/src/databases.cpp +++ b/tests/TinyUtils/src/databases.cpp @@ -5,6 +5,21 @@ #include "orm/exceptions/runtimeerror.hpp" #include "orm/utils/type.hpp" +using Orm::Constants::EMPTY; +using Orm::Constants::H127001; +using Orm::Constants::InnoDB; +using Orm::Constants::P3306; +using Orm::Constants::P5432; +using Orm::Constants::PUBLIC; +using Orm::Constants::QMYSQL; +using Orm::Constants::QPSQL; +using Orm::Constants::QSQLITE; +using Orm::Constants::ROOT; +using Orm::Constants::TZ00; +using Orm::Constants::UTC; +using Orm::Constants::UTF8; +using Orm::Constants::UTF8MB4; +using Orm::Constants::UTF8MB40900aici; using Orm::Constants::database_; using Orm::Constants::driver_; using Orm::Constants::charset_; @@ -12,32 +27,18 @@ using Orm::Constants::check_database_exists; using Orm::Constants::collation_; using Orm::Constants::engine_; using Orm::Constants::foreign_key_constraints; -using Orm::Constants::H127001; using Orm::Constants::host_; -using Orm::Constants::InnoDB; using Orm::Constants::isolation_level; using Orm::Constants::options_; -using Orm::Constants::P3306; -using Orm::Constants::P5432; using Orm::Constants::password_; using Orm::Constants::port_; +using Orm::Constants::postgres_; using Orm::Constants::prefix_; using Orm::Constants::prefix_indexes; -using Orm::Constants::PUBLIC; -using Orm::Constants::QMYSQL; -using Orm::Constants::QPSQL; -using Orm::Constants::QSQLITE; -using Orm::Constants::ROOT; using Orm::Constants::schema_; using Orm::Constants::strict_; using Orm::Constants::timezone_; using Orm::Constants::username_; -using Orm::Constants::EMPTY; -using Orm::Constants::TZ00; -using Orm::Constants::UTC; -using Orm::Constants::UTF8; -using Orm::Constants::UTF8MB4; -using Orm::Constants::UTF8MB40900aici; using Orm::DB; @@ -228,14 +229,15 @@ Databases::postgresConfiguration() {port_, qEnvironmentVariable("DB_PGSQL_PORT", P5432)}, {database_, qEnvironmentVariable("DB_PGSQL_DATABASE", EMPTY)}, {schema_, qEnvironmentVariable("DB_PGSQL_SCHEMA", PUBLIC)}, - {username_, qEnvironmentVariable("DB_PGSQL_USERNAME", - QStringLiteral("postgres"))}, + {username_, qEnvironmentVariable("DB_PGSQL_USERNAME", postgres_)}, {password_, qEnvironmentVariable("DB_PGSQL_PASSWORD", EMPTY)}, {charset_, qEnvironmentVariable("DB_PGSQL_CHARSET", UTF8)}, // I don't use timezone types in postgres anyway {timezone_, UTC}, {prefix_, EMPTY}, {prefix_indexes, true}, + // ConnectionFactory provides a default value for this, this is only for reference +// {dont_drop, QStringList {QStringLiteral("spatial_ref_sys")}}, {options_, QVariantHash()}, }; diff --git a/tests/auto/functional/tom/migrate/tst_migrate.cpp b/tests/auto/functional/tom/migrate/tst_migrate.cpp index d5430f7fe..ec858aa13 100644 --- a/tests/auto/functional/tom/migrate/tst_migrate.cpp +++ b/tests/auto/functional/tom/migrate/tst_migrate.cpp @@ -61,12 +61,13 @@ private slots: private: /*! Prepare arguments and invoke runCommand(). */ [[nodiscard]] int - invokeCommand(const QString &name, std::vector &&arguments = {}) const; + invokeCommand(const QString &connection, const QString &name, + std::vector &&arguments = {}) const; /*! Create a tom application instance and invoke the given command. */ int runCommand(int &argc, const std::vector &argv) const; /*! Invoke the status command to obtain results. */ - inline int invokeTestStatusCommand() const; + inline int invokeTestStatusCommand(const QString &connection) const; /*! Get result of the last status command. */ Status status() const; /*! Create a status object for comparing with the result of the status(). */ @@ -75,10 +76,7 @@ private: Status createResetStatus() const; /*! Prepare the migration database for running. */ - void prepareDatabase() const; - - /*! Connection name used in this test case. */ - QString m_connection {}; + void prepareDatabase(const QStringList &connections) const; /*! Migrations table name. */ inline static const auto MigrationsTable = QStringLiteral("migrations_unit_testing"); @@ -140,12 +138,19 @@ namespace void tst_Migrate::initTestCase() { - m_connection = Databases::createConnection(Databases::MYSQL); + const auto &connections = + Databases::createConnections({Databases::MYSQL, Databases::POSTGRESQL}); - if (m_connection.isEmpty()) + if (connections.isEmpty()) QSKIP(QStringLiteral("%1 autotest skipped, environment variables " - "for '%2' connection have not been defined.") - .arg("tst_Migrate", Databases::MYSQL).toUtf8().constData(), ); + "for ANY connection have not been defined.") + .arg("tst_Migrate").toUtf8().constData(), ); + + QTest::addColumn("connection"); + + // Run all tests for all supported database connections + for (const auto &connection : connections) + QTest::newRow(connection.toUtf8().constData()) << connection; /* Modify the migrate:status command to not output a status table to the console but instead return a result as the vector, this vector is then used for comparing @@ -153,21 +158,23 @@ void tst_Migrate::initTestCase() TomApplication::enableInUnitTests(); // Prepare the migration database for running - prepareDatabase(); + prepareDatabase(connections); } void tst_Migrate::cleanup() const { + QFETCH_GLOBAL(QString, connection); + /* All test methods need this except for two of them (reset and I don't remember second), I will not implement special logic to skip this for these two methods. */ { - auto exitCode = invokeCommand(MigrateReset); + auto exitCode = invokeCommand(connection, MigrateReset); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createResetStatus(), status()); @@ -176,14 +183,16 @@ void tst_Migrate::cleanup() const void tst_Migrate::migrate() const { + QFETCH_GLOBAL(QString, connection); + { - auto exitCode = invokeCommand(Migrate); + auto exitCode = invokeCommand(connection, Migrate); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyMigrated), status()); @@ -192,14 +201,16 @@ void tst_Migrate::migrate() const void tst_Migrate::migrate_Step() const { + QFETCH_GLOBAL(QString, connection); + { - auto exitCode = invokeCommand(Migrate, {"--step"}); + auto exitCode = invokeCommand(connection, Migrate, {"--step"}); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyStepMigrated), status()); @@ -208,14 +219,16 @@ void tst_Migrate::migrate_Step() const void tst_Migrate::reset() const { + QFETCH_GLOBAL(QString, connection); + { - auto exitCode = invokeCommand(MigrateReset); + auto exitCode = invokeCommand(connection, MigrateReset); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createResetStatus(), status()); @@ -224,14 +237,16 @@ void tst_Migrate::reset() const void tst_Migrate::rollback_OnMigrate() const { + QFETCH_GLOBAL(QString, connection); + { - auto exitCode = invokeCommand(Migrate); + auto exitCode = invokeCommand(connection, Migrate); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyMigrated), status()); @@ -239,13 +254,13 @@ void tst_Migrate::rollback_OnMigrate() const // rollback on previous migrate w/o --step { - auto exitCode = invokeCommand(MigrateRollback); + auto exitCode = invokeCommand(connection, MigrateRollback); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createResetStatus(), status()); @@ -254,14 +269,16 @@ void tst_Migrate::rollback_OnMigrate() const void tst_Migrate::rollback_OnMigrateWithStep() const { + QFETCH_GLOBAL(QString, connection); + { - auto exitCode = invokeCommand(Migrate, {"--step"}); + auto exitCode = invokeCommand(connection, Migrate, {"--step"}); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyStepMigrated), status()); @@ -269,13 +286,13 @@ void tst_Migrate::rollback_OnMigrateWithStep() const // rollback on previous migrate with --step { - auto exitCode = invokeCommand(MigrateRollback); + auto exitCode = invokeCommand(connection, MigrateRollback); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus({ @@ -289,14 +306,16 @@ void tst_Migrate::rollback_OnMigrateWithStep() const void tst_Migrate::rollback_Step_OnMigrate() const { + QFETCH_GLOBAL(QString, connection); + { - auto exitCode = invokeCommand(Migrate); + auto exitCode = invokeCommand(connection, Migrate); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyMigrated), status()); @@ -304,13 +323,13 @@ void tst_Migrate::rollback_Step_OnMigrate() const // rollback on previous migrate w/o --step { - auto exitCode = invokeCommand(MigrateRollback, {"--step=2"}); + auto exitCode = invokeCommand(connection, MigrateRollback, {"--step=2"}); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus({ @@ -324,14 +343,16 @@ void tst_Migrate::rollback_Step_OnMigrate() const void tst_Migrate::rollback_Step_OnMigrateWithStep() const { + QFETCH_GLOBAL(QString, connection); + { - auto exitCode = invokeCommand(Migrate, {"--step"}); + auto exitCode = invokeCommand(connection, Migrate, {"--step"}); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyStepMigrated), status()); @@ -339,13 +360,13 @@ void tst_Migrate::rollback_Step_OnMigrateWithStep() const // rollback on previous migrate with --step { - auto exitCode = invokeCommand(MigrateRollback, {"--step=2"}); + auto exitCode = invokeCommand(connection, MigrateRollback, {"--step=2"}); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus({ @@ -359,14 +380,16 @@ void tst_Migrate::rollback_Step_OnMigrateWithStep() const void tst_Migrate::refresh_OnMigrate() const { + QFETCH_GLOBAL(QString, connection); + { - auto exitCode = invokeCommand(Migrate); + auto exitCode = invokeCommand(connection, Migrate); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyMigrated), status()); @@ -374,13 +397,13 @@ void tst_Migrate::refresh_OnMigrate() const // refresh on previous migrate w/o --step { - auto exitCode = invokeCommand(MigrateRefresh); + auto exitCode = invokeCommand(connection, MigrateRefresh); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyMigrated), status()); @@ -389,14 +412,16 @@ void tst_Migrate::refresh_OnMigrate() const void tst_Migrate::refresh_OnMigrateWithStep() const { + QFETCH_GLOBAL(QString, connection); + { - auto exitCode = invokeCommand(Migrate, {"--step"}); + auto exitCode = invokeCommand(connection, Migrate, {"--step"}); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyStepMigrated), status()); @@ -404,13 +429,13 @@ void tst_Migrate::refresh_OnMigrateWithStep() const // refresh on previous migrate with --step { - auto exitCode = invokeCommand(MigrateRefresh); + auto exitCode = invokeCommand(connection, MigrateRefresh); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyMigrated), status()); @@ -419,14 +444,16 @@ void tst_Migrate::refresh_OnMigrateWithStep() const void tst_Migrate::refresh_Step() const { + QFETCH_GLOBAL(QString, connection); + { - auto exitCode = invokeCommand(Migrate); + auto exitCode = invokeCommand(connection, Migrate); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyMigrated), status()); @@ -434,13 +461,13 @@ void tst_Migrate::refresh_Step() const // refresh on previous migrate w/o --step { - auto exitCode = invokeCommand(MigrateRefresh, {"--step=2"}); + auto exitCode = invokeCommand(connection, MigrateRefresh, {"--step=2"}); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus({ @@ -454,14 +481,16 @@ void tst_Migrate::refresh_Step() const void tst_Migrate::refresh_StepMigrate() const { + QFETCH_GLOBAL(QString, connection); + { - auto exitCode = invokeCommand(Migrate); + auto exitCode = invokeCommand(connection, Migrate); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyMigrated), status()); @@ -469,13 +498,13 @@ void tst_Migrate::refresh_StepMigrate() const // refresh on previous migrate w/o --step { - auto exitCode = invokeCommand(MigrateRefresh, {"--step-migrate"}); + auto exitCode = invokeCommand(connection, MigrateRefresh, {"--step-migrate"}); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyStepMigrated), status()); @@ -484,14 +513,16 @@ void tst_Migrate::refresh_StepMigrate() const void tst_Migrate::refresh_Step_StepMigrate() const { + QFETCH_GLOBAL(QString, connection); + { - auto exitCode = invokeCommand(Migrate); + auto exitCode = invokeCommand(connection, Migrate); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus(FullyMigrated), status()); @@ -499,13 +530,14 @@ void tst_Migrate::refresh_Step_StepMigrate() const // refresh on previous migrate w/o --step { - auto exitCode = invokeCommand(MigrateRefresh, {"--step=2", "--step-migrate"}); + auto exitCode = invokeCommand(connection, MigrateRefresh, + {"--step=2", "--step-migrate"}); QVERIFY(exitCode == EXIT_SUCCESS); } { - auto exitCode = invokeTestStatusCommand(); + auto exitCode = invokeTestStatusCommand(connection); QVERIFY(exitCode == EXIT_SUCCESS); QCOMPARE(createStatus({ @@ -519,18 +551,15 @@ void tst_Migrate::refresh_Step_StepMigrate() const /* private */ -int tst_Migrate::invokeCommand(const QString &name, +int tst_Migrate::invokeCommand(const QString &connection, const QString &name, std::vector &&arguments) const { static const auto connectionTmpl = QStringLiteral("--database=%1"); // Prepare fake argc and argv const auto nameArr = name.toUtf8(); - // FUTURE tests tom, when the schema builder will support more db drivers, I can run it on all supported connections, code will look like in the tst_querybuilder.cpp, then I will fetch connection name in every test method using QFETCH_GLOBAL() and I will pass this connection name to the invokeCommand(), so I will discard m_connection and will use method parameter connection here silverqx - /* Schema builder is implemented only for the MySQL driver, so I can use m_connection - here as the default connection. */ // DB connection to use - const auto connectionArr = connectionTmpl.arg(m_connection).toUtf8(); + const auto connectionArr = connectionTmpl.arg(connection).toUtf8(); std::vector argv { #ifdef _WIN32 @@ -570,9 +599,9 @@ int tst_Migrate::runCommand(int &argc, const std::vector &argv) co return EXIT_FAILURE; } -int tst_Migrate::invokeTestStatusCommand() const +int tst_Migrate::invokeTestStatusCommand(const QString &connection) const { - return invokeCommand(MigrateStatus); + return invokeCommand(connection, MigrateStatus); } Status tst_Migrate::status() const @@ -595,25 +624,27 @@ Status tst_Migrate::createResetStatus() const }; } -void tst_Migrate::prepareDatabase() const +void tst_Migrate::prepareDatabase(const QStringList &connections) const { - // Ownership of a unique_ptr() - const auto schema = Databases::manager()->connection(m_connection) - .getSchemaBuilder(); + for (const auto &connection : connections) { + // Ownership of a unique_ptr() + const auto schema = Databases::manager()->connection(connection) + .getSchemaBuilder(); - // Create the migrations table if needed - if (!schema->hasTable(MigrationsTable)) { - auto exitCode = invokeCommand(MigrateInstall); + // Create the migrations table if needed + if (!schema->hasTable(MigrationsTable)) { + auto exitCode = invokeCommand(connection, MigrateInstall); + + QVERIFY(exitCode == EXIT_SUCCESS); + + return; + } + + // Reset the migrations table + auto exitCode = invokeCommand(connection, MigrateReset); QVERIFY(exitCode == EXIT_SUCCESS); - - return; } - - // Reset the migrations table - auto exitCode = invokeCommand(MigrateReset); - - QVERIFY(exitCode == EXIT_SUCCESS); } QTEST_MAIN(tst_Migrate) diff --git a/tests/auto/unit/orm/schema/CMakeLists.txt b/tests/auto/unit/orm/schema/CMakeLists.txt index 250964c9b..fcb381827 100644 --- a/tests/auto/unit/orm/schema/CMakeLists.txt +++ b/tests/auto/unit/orm/schema/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(blueprint) add_subdirectory(mysql_schemabuilder) +add_subdirectory(postgresql_schemabuilder) diff --git a/tests/auto/unit/orm/schema/mysql_schemabuilder/tst_mysql_schemabuilder.cpp b/tests/auto/unit/orm/schema/mysql_schemabuilder/tst_mysql_schemabuilder.cpp index a04bb765e..df879c6fd 100644 --- a/tests/auto/unit/orm/schema/mysql_schemabuilder/tst_mysql_schemabuilder.cpp +++ b/tests/auto/unit/orm/schema/mysql_schemabuilder/tst_mysql_schemabuilder.cpp @@ -11,15 +11,15 @@ #include "databases.hpp" -using Orm::Constants::charset_; -using Orm::Constants::driver_; -using Orm::Constants::collation_; using Orm::Constants::ID; using Orm::Constants::MyISAM; using Orm::Constants::NAME; using Orm::Constants::QMYSQL; using Orm::Constants::SIZE; using Orm::Constants::UTF8; +using Orm::Constants::charset_; +using Orm::Constants::driver_; +using Orm::Constants::collation_; using Orm::DB; using Orm::Exceptions::LogicError; @@ -677,9 +677,9 @@ void tst_Mysql_SchemaBuilder::modifiers() const table.string("name2").comment("name2 note"); table.string("name3"); table.string("name4").invisible(); - table.string("name5").charset("utf8"); + table.string("name5").charset(UTF8); table.string("name6").collation("utf8mb4_unicode_ci"); - table.string("name7").charset("utf8").collation("utf8_unicode_ci"); + table.string("name7").charset(UTF8).collation("utf8_unicode_ci"); table.bigInteger("big_int").isUnsigned(); table.bigInteger("big_int1"); }); diff --git a/tests/auto/unit/orm/schema/postgresql_schemabuilder/CMakeLists.txt b/tests/auto/unit/orm/schema/postgresql_schemabuilder/CMakeLists.txt index 8945f7001..728d570eb 100644 --- a/tests/auto/unit/orm/schema/postgresql_schemabuilder/CMakeLists.txt +++ b/tests/auto/unit/orm/schema/postgresql_schemabuilder/CMakeLists.txt @@ -1,16 +1,16 @@ -project(mysql_schemabuilder +project(postgresql_schemabuilder LANGUAGES CXX ) -add_executable(mysql_schemabuilder +add_executable(postgresql_schemabuilder tst_postgresql_schemabuilder.cpp ) -add_test(NAME mysql_schemabuilder COMMAND mysql_schemabuilder) +add_test(NAME postgresql_schemabuilder COMMAND postgresql_schemabuilder) include(TinyTestCommon) if(ORM) - tiny_configure_test(mysql_schemabuilder INCLUDE_MODELS) + tiny_configure_test(postgresql_schemabuilder INCLUDE_MODELS) else() - tiny_configure_test(mysql_schemabuilder) + tiny_configure_test(postgresql_schemabuilder) endif() diff --git a/tests/auto/unit/orm/schema/postgresql_schemabuilder/postgresql_schemabuilder.pro b/tests/auto/unit/orm/schema/postgresql_schemabuilder/postgresql_schemabuilder.pro new file mode 100644 index 000000000..8506e20d8 --- /dev/null +++ b/tests/auto/unit/orm/schema/postgresql_schemabuilder/postgresql_schemabuilder.pro @@ -0,0 +1,6 @@ +include($$TINYORM_SOURCE_TREE/tests/qmake/common.pri) +include($$TINYORM_SOURCE_TREE/tests/qmake/TinyUtils.pri) +!disable_orm: \ + include($$TINYORM_SOURCE_TREE/tests/models/models.pri) + +SOURCES = tst_postgresql_schemabuilder.cpp diff --git a/tests/auto/unit/orm/schema/postgresql_schemabuilder/tst_postgresql_schemabuilder.cpp b/tests/auto/unit/orm/schema/postgresql_schemabuilder/tst_postgresql_schemabuilder.cpp new file mode 100644 index 000000000..b563559d4 --- /dev/null +++ b/tests/auto/unit/orm/schema/postgresql_schemabuilder/tst_postgresql_schemabuilder.cpp @@ -0,0 +1,1599 @@ +#include +#include + +#include "orm/db.hpp" +#include "orm/exceptions/logicerror.hpp" +#include "orm/schema.hpp" + +#ifndef TINYORM_DISABLE_ORM +# include "models/user.hpp" +#endif + +#include "databases.hpp" + +using Orm::Constants::ID; +using Orm::Constants::NAME; +using Orm::Constants::PUBLIC; +using Orm::Constants::QPSQL; +using Orm::Constants::SIZE; +using Orm::Constants::UTF8; +using Orm::Constants::UcsBasic; +using Orm::Constants::charset_; +using Orm::Constants::driver_; + +using Orm::DB; +using Orm::Exceptions::LogicError; +using Orm::Schema; +using Orm::SchemaNs::Blueprint; +using Orm::SchemaNs::Constants::Cascade; +using Orm::SchemaNs::Constants::Restrict; + +using TestUtils::Databases; + +class tst_PostgreSQL_SchemaBuilder : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void createDatabase() const; + void createDatabase_Charset_Collation() const; + void dropDatabaseIfExists() const; + + void createTable() const; + void createTable_Temporary() const; + void createTable_Charset_Collation_Engine() const; + + void timestamps_rememberToken_CreateAndDrop() const; + + void modifyTable() const; + + void dropTable() const; + void dropTableIfExists() const; + + void rename() const; + + void dropColumns() const; + void renameColumn() const; + + void dropAllTypes() const; + + void getAllTables() const; + void getAllViews() const; + + void enableForeignKeyConstraints() const; + void disableForeignKeyConstraints() const; + + void getColumnListing() const; + + void hasTable() const; + + void defaultStringLength_Set() const; + + void modifiers() const; + void modifier_defaultValue_WithExpression() const; + void modifier_defaultValue_WithBoolean() const; + + void useCurrent() const; + void useCurrentOnUpdate() const; + + void indexes_Fluent() const; + void indexes_Blueprint() const; + + void renameIndex() const; + + void dropIndex_ByIndexName() const; + void dropIndex_ByColumn() const; + void dropIndex_ByMultipleColumns() const; + + void foreignKey() const; + void foreignKey_TerserSyntax() const; +#ifndef TINYORM_DISABLE_ORM + void foreignKey_WithModel() const; +#endif + + void dropForeign() const; + +// NOLINTNEXTLINE(readability-redundant-access-specifiers) +private: + /*! Table or database name used in tests. */ + inline static const auto Firewalls = QStringLiteral("firewalls"); + + /*! Connection name used in this test case. */ + QString m_connection {}; +}; + +void tst_PostgreSQL_SchemaBuilder::initTestCase() +{ + m_connection = Databases::createConnection(Databases::POSTGRESQL); + + if (m_connection.isEmpty()) + QSKIP(QStringLiteral("%1 autotest skipped, environment variables " + "for '%2' connection have not been defined.") + .arg("tst_PostgreSQL_SchemaBuilder", Databases::POSTGRESQL) + .toUtf8().constData(), ); +} + +void tst_PostgreSQL_SchemaBuilder::createDatabase() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).createDatabase(Firewalls); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + R"(create database "firewalls" encoding "utf8")"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::createDatabase_Charset_Collation() const +{ + static const auto postgresCreateDb = QStringLiteral( + "tinyorm_postgres_tests_create_db"); + + // Create a new connection with different charset + DB::addConnection({ + {driver_, QPSQL}, + {charset_, "WIN1250"}, + }, postgresCreateDb); + + auto log = DB::connection(postgresCreateDb).pretend([](auto &connection) + { + Schema::on(connection.getName()).createDatabase(Firewalls); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + R"(create database "firewalls" encoding "WIN1250")"); + QVERIFY(firstLog.boundValues.isEmpty()); + + // Restore + DB::removeConnection(postgresCreateDb); +} + +void tst_PostgreSQL_SchemaBuilder::dropDatabaseIfExists() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).dropDatabaseIfExists(Firewalls); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + R"(drop database if exists "firewalls")"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::createTable() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.id(); + table.Char("char"); + table.Char("char_10", 10); + table.string("string"); + table.string("string_22", 22); + table.tinyText("tiny_text"); + table.text("text"); + table.mediumText("medium_text"); + table.longText("long_text"); + + table.integer("integer"); + table.tinyInteger("tinyInteger"); + table.smallInteger("smallInteger"); + table.mediumInteger("mediumInteger"); + table.bigInteger("bigInteger"); + + // PostgreSQL doesn't have unsigned integers, so they should be same as above + table.unsignedInteger("unsignedInteger"); + table.unsignedTinyInteger("unsignedTinyInteger"); + table.unsignedSmallInteger("unsignedSmallInteger"); + table.unsignedMediumInteger("unsignedMediumInteger"); + table.unsignedBigInteger("unsignedBigInteger"); + + table.uuid(); + table.ipAddress(); + table.macAddress(); + }); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "create table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"char\" char(255) not null, " + "\"char_10\" char(10) not null, " + "\"string\" varchar(255) not null, " + "\"string_22\" varchar(22) not null, " + "\"tiny_text\" varchar(255) not null, " + "\"text\" text not null, " + "\"medium_text\" text not null, " + "\"long_text\" text not null, " + "\"integer\" integer not null, " + "\"tinyInteger\" smallint not null, " + "\"smallInteger\" smallint not null, " + "\"mediumInteger\" integer not null, " + "\"bigInteger\" bigint not null, " + "\"unsignedInteger\" integer not null, " + "\"unsignedTinyInteger\" smallint not null, " + "\"unsignedSmallInteger\" smallint not null, " + "\"unsignedMediumInteger\" integer not null, " + "\"unsignedBigInteger\" bigint not null, " + "\"uuid\" uuid not null, " + "\"ip_address\" inet not null, " + "\"mac_address\" macaddr not null)"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::createTable_Temporary() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.temporary(); + + table.id(); + table.string(NAME); + }); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "create temporary table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"name\" varchar(255) not null)"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::createTable_Charset_Collation_Engine() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + // Ignored with the PosrgreSQL grammar + table.charset = "WIN1250"; + + table.id(); + table.string(NAME); + }); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "create table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"name\" varchar(255) not null)"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::timestamps_rememberToken_CreateAndDrop() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.id(); + + table.timestamps(); + table.rememberToken(); + }); + + Schema::on(connection.getName()) + .table(Firewalls, [](Blueprint &table) + { + table.dropTimestamps(); + table.dropRememberToken(); + }); + + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.id(); + + table.timestamps(3); + }); + }); + + QCOMPARE(log.size(), 4); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "create table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"created_at\" timestamp(0) without time zone null, " + "\"updated_at\" timestamp(0) without time zone null, " + "\"remember_token\" varchar(100) null)"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + "alter table \"firewalls\" " + "drop column \"created_at\", drop column \"updated_at\""); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + "alter table \"firewalls\" drop column \"remember_token\""); + QVERIFY(log2.boundValues.isEmpty()); + + const auto &log3 = log.at(3); + QCOMPARE(log3.query, + "create table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"created_at\" timestamp(3) without time zone null, " + "\"updated_at\" timestamp(3) without time zone null)"); + QVERIFY(log3.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::modifyTable() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .table(Firewalls, [](Blueprint &table) + { + table.Char("char"); + table.Char("char_10", 10); + table.string("string"); + table.string("string_22", 22); + table.tinyText("tiny_text"); + table.text("text"); + table.mediumText("medium_text"); + table.longText("long_text"); + + table.integer("integer").nullable(); + table.tinyInteger("tinyInteger"); + table.smallInteger("smallInteger"); + table.mediumInteger("mediumInteger"); + + table.dropColumn("long_text"); + table.dropColumns({"medium_text", "text"}); + table.dropColumns("smallInteger", "mediumInteger"); + + table.renameColumn("integer", "integer_renamed"); + table.renameColumn("string_22", "string_22_renamed"); + }); + }); + + QCOMPARE(log.size(), 6); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "alter table \"firewalls\" " + "add column \"char\" char(255) not null, " + "add column \"char_10\" char(10) not null, " + "add column \"string\" varchar(255) not null, " + "add column \"string_22\" varchar(22) not null, " + "add column \"tiny_text\" varchar(255) not null, " + "add column \"text\" text not null, " + "add column \"medium_text\" text not null, " + "add column \"long_text\" text not null, " + "add column \"integer\" integer null, " + "add column \"tinyInteger\" smallint not null, " + "add column \"smallInteger\" smallint not null, " + "add column \"mediumInteger\" integer not null"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + "alter table \"firewalls\" drop column \"long_text\""); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + "alter table \"firewalls\" " + "drop column \"medium_text\", drop column \"text\""); + QVERIFY(log2.boundValues.isEmpty()); + + const auto &log3 = log.at(3); + QCOMPARE(log3.query, + "alter table \"firewalls\" " + "drop column \"smallInteger\", drop column \"mediumInteger\""); + QVERIFY(log3.boundValues.isEmpty()); + + const auto &log4 = log.at(4); + QCOMPARE(log4.query, + "alter table \"firewalls\" " + "rename column \"integer\" to \"integer_renamed\""); + QVERIFY(log4.boundValues.isEmpty()); + + const auto &log5 = log.at(5); + QCOMPARE(log5.query, + "alter table \"firewalls\" " + "rename column \"string_22\" to \"string_22_renamed\""); + QVERIFY(log5.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::dropTable() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).drop(Firewalls); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, "drop table \"firewalls\""); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::dropTableIfExists() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).dropIfExists(Firewalls); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, "drop table if exists \"firewalls\""); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::rename() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).rename("secured", Firewalls); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, "alter table \"secured\" rename to \"firewalls\""); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::dropColumns() const +{ + { + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).dropColumn(Firewalls, NAME); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + R"(alter table "firewalls" drop column "name")"); + QVERIFY(firstLog.boundValues.isEmpty()); + } + { + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).dropColumns(Firewalls, {NAME, SIZE}); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + R"(alter table "firewalls" drop column "name", drop column "size")"); + QVERIFY(firstLog.boundValues.isEmpty()); + } + { + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).dropColumns(Firewalls, NAME, SIZE); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + R"(alter table "firewalls" drop column "name", drop column "size")"); + QVERIFY(firstLog.boundValues.isEmpty()); + } +} + +void tst_PostgreSQL_SchemaBuilder::renameColumn() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).renameColumn(Firewalls, NAME, "first_name"); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "alter table \"firewalls\" rename column \"name\" to \"first_name\""); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::dropAllTypes() const +{ + QVERIFY_EXCEPTION_THROWN(Schema::on(m_connection).dropAllTypes(), LogicError); +} + +void tst_PostgreSQL_SchemaBuilder::getAllTables() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).getAllTables(); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "select tablename from pg_catalog.pg_tables " + "where schemaname in ('public')"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::getAllViews() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).getAllViews(); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "select viewname from pg_catalog.pg_views " + "where schemaname in ('public')"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::enableForeignKeyConstraints() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).enableForeignKeyConstraints(); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, "SET CONSTRAINTS ALL IMMEDIATE;"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::disableForeignKeyConstraints() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()).disableForeignKeyConstraints(); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, "SET CONSTRAINTS ALL DEFERRED;"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::getColumnListing() const +{ + auto &connection = DB::connection(m_connection); + + auto log = connection.pretend([](auto &connection_) + { + Schema::on(connection_.getName()).getColumnListing(Firewalls); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "select column_name " + "from information_schema.columns " + "where table_schema = ? and table_name = ?"); + QCOMPARE(firstLog.boundValues, + QVector({QVariant(PUBLIC), QVariant(Firewalls)})); +} + +void tst_PostgreSQL_SchemaBuilder::hasTable() const +{ + auto &connection = DB::connection(m_connection); + + auto log = connection.pretend([](auto &connection_) + { + Schema::on(connection_.getName()).hasTable(Firewalls); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "select * " + "from information_schema.tables " + "where table_schema = ? and table_name = ? and " + "table_type = 'BASE TABLE'"); + QCOMPARE(firstLog.boundValues, + QVector({QVariant(PUBLIC), QVariant(Firewalls)})); +} + +void tst_PostgreSQL_SchemaBuilder::defaultStringLength_Set() const +{ + QVERIFY(Blueprint::DefaultStringLength == Orm::SchemaNs::DefaultStringLength); + + Schema::defaultStringLength(191); + QVERIFY(Blueprint::DefaultStringLength == 191); + + // Restore + Schema::defaultStringLength(Orm::SchemaNs::DefaultStringLength); + QVERIFY(Blueprint::DefaultStringLength == Orm::SchemaNs::DefaultStringLength); +} + +void tst_PostgreSQL_SchemaBuilder::modifiers() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.bigInteger(ID).autoIncrement().startingValue(5); + table.bigInteger("big_int"); + table.string(NAME).defaultValue("guest"); + table.string("name1").nullable(); + table.string("name2").comment("name2 note"); + table.string("name3"); + // PostgreSQL doesn't support charset on the column + table.string("name5").charset(UTF8); + table.string("name6").collation(UcsBasic); + // PostgreSQL doesn't support charset on the column + table.string("name7").charset(UTF8).collation(UcsBasic); + }); + /* Tests from and also integerIncrements, this would of course fail on real DB + as you can not have two primary keys. */ + Schema::on(connection.getName()) + .table(Firewalls, [](Blueprint &table) + { + table.integerIncrements(ID).from(15); + }); + }); + + QCOMPARE(log.size(), 5); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "create table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"big_int\" bigint not null, " + "\"name\" varchar(255) not null default 'guest', " + "\"name1\" varchar(255) null, " + "\"name2\" varchar(255) not null, " + "\"name3\" varchar(255) not null, " + "\"name5\" varchar(255) not null, " + "\"name6\" varchar(255) collate \"ucs_basic\" not null, " + "\"name7\" varchar(255) collate \"ucs_basic\" not null)"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + R"(alter sequence "firewalls_id_seq" restart with 5)"); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + R"(comment on column "firewalls"."name2" is 'name2 note')"); + QVERIFY(log2.boundValues.isEmpty()); + + const auto &log3 = log.at(3); + QCOMPARE(log3.query, + "alter table \"firewalls\" add column \"id\" serial primary key not null"); + QVERIFY(log3.boundValues.isEmpty()); + + const auto &log4 = log.at(4); + QCOMPARE(log4.query, + R"(alter sequence "firewalls_id_seq" restart with 15)"); + QVERIFY(log4.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::modifier_defaultValue_WithExpression() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.string(NAME).defaultValue("guest"); + table.string("name_raw").defaultValue(DB::raw("'guest_raw'")); + }); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "create table \"firewalls\" (" + "\"name\" varchar(255) not null default 'guest', " + "\"name_raw\" varchar(255) not null default 'guest_raw')"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::modifier_defaultValue_WithBoolean() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.boolean("boolean"); + table.boolean("boolean_false").defaultValue(false); + table.boolean("boolean_true").defaultValue(true); + table.boolean("boolean_0").defaultValue(0); + table.boolean("boolean_1").defaultValue(1); + }); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "create table \"firewalls\" (" + "\"boolean\" boolean not null, " + "\"boolean_false\" boolean not null default '0', " + "\"boolean_true\" boolean not null default '1', " + "\"boolean_0\" boolean not null default '0', " + "\"boolean_1\" boolean not null default '1')"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::useCurrent() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.dateTime("created"); + table.dateTime("created_current").useCurrent(); + + table.timestamp("created_t"); + table.timestamp("created_t_current").useCurrent(); + }); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "create table \"firewalls\" (" + "\"created\" timestamp(0) without time zone not null, " + "\"created_current\" timestamp(0) without time zone " + "default CURRENT_TIMESTAMP not null, " + "\"created_t\" timestamp(0) without time zone not null, " + "\"created_t_current\" timestamp(0) without time zone " + "default CURRENT_TIMESTAMP not null)"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::useCurrentOnUpdate() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.dateTime("updated"); + // PostgreSQL doesn't support on update + table.dateTime("updated_current").useCurrentOnUpdate(); + + table.timestamp("updated_t"); + table.timestamp("updated_t_current").useCurrentOnUpdate(); + }); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "create table \"firewalls\" (" + "\"updated\" timestamp(0) without time zone not null, " + "\"updated_current\" timestamp(0) without time zone not null, " + "\"updated_t\" timestamp(0) without time zone not null, " + "\"updated_t_current\" timestamp(0) without time zone not null)"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::indexes_Fluent() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + // Fluent indexes + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.id(); + + table.string("name_u").unique(); + + table.string("name_i").index(); + table.string("name_i_cn").index("name_i_cn_index"); + + table.string("name_f").fulltext(); + table.string("name_f_cn").fulltext("name_f_cn_fulltext"); + + table.geometry("coordinates_s").spatialIndex(); + table.geometry("coordinates_s_cn").spatialIndex("coordinates_s_cn_spatial"); + }); + }); + + QCOMPARE(log.size(), 8); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "create table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"name_u\" varchar(255) not null, " + "\"name_i\" varchar(255) not null, " + "\"name_i_cn\" varchar(255) not null, " + "\"name_f\" varchar(255) not null, " + "\"name_f_cn\" varchar(255) not null, " + "\"coordinates_s\" geography(geometry, 4326) not null, " + "\"coordinates_s_cn\" geography(geometry, 4326) not null)"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_name_u_unique\" unique (\"name_u\")"); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + R"(create index "firewalls_name_i_index" on "firewalls" ("name_i"))"); + QVERIFY(log2.boundValues.isEmpty()); + + const auto &log3 = log.at(3); + QCOMPARE(log3.query, + R"(create index "name_i_cn_index" on "firewalls" ("name_i_cn"))"); + QVERIFY(log3.boundValues.isEmpty()); + + const auto &log4 = log.at(4); + QCOMPARE(log4.query, + "create index \"firewalls_name_f_fulltext\" on \"firewalls\" " + "using gin ((to_tsvector('english', \"name_f\")))"); + QVERIFY(log4.boundValues.isEmpty()); + + const auto &log5 = log.at(5); + QCOMPARE(log5.query, + "create index \"name_f_cn_fulltext\" on \"firewalls\" " + "using gin ((to_tsvector('english', \"name_f_cn\")))"); + QVERIFY(log5.boundValues.isEmpty()); + + const auto &log6 = log.at(6); + QCOMPARE(log6.query, + "create index \"firewalls_coordinates_s_spatialindex\" on \"firewalls\" " + "using gist (\"coordinates_s\")"); + QVERIFY(log6.boundValues.isEmpty()); + + const auto &log7 = log.at(7); + QCOMPARE(log7.query, + "create index \"coordinates_s_cn_spatial\" on \"firewalls\" " + "using gist (\"coordinates_s_cn\")"); + QVERIFY(log7.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::indexes_Blueprint() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + // Blueprint indexes + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.id(); + + table.string("name_u"); + table.unique({"name_u"}, "name_u_unique"); + + table.string("name_i"); + table.index({"name_i"}); + + table.string("name_i_cn"); + table.index("name_i_cn", "name_i_cn_index"); + + table.string("name_r"); + table.string("name_r1"); + table.rawIndex(DB::raw(R"("name_r", name_r1)"), "name_r_raw"); + + table.string("name_f"); + table.fullText({"name_f"}); + + table.string("name_f_cn"); + table.fullText("name_f_cn", "name_f_cn_fulltext"); + + table.geometry("coordinates_s").isGeometry(); + table.spatialIndex("coordinates_s"); + + table.point("coordinates_s_cn", 3200).isGeometry(); + table.spatialIndex("coordinates_s_cn", "coordinates_s_cn_spatial"); + }); + }); + + QCOMPARE(log.size(), 9); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "create table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"name_u\" varchar(255) not null, " + "\"name_i\" varchar(255) not null, " + "\"name_i_cn\" varchar(255) not null, " + "\"name_r\" varchar(255) not null, " + "\"name_r1\" varchar(255) not null, " + "\"name_f\" varchar(255) not null, " + "\"name_f_cn\" varchar(255) not null, " + "\"coordinates_s\" geometry(geometry) not null, " + "\"coordinates_s_cn\" geometry(point, 3200) not null)"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + "alter table \"firewalls\" " + "add constraint \"name_u_unique\" unique (\"name_u\")"); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + R"(create index "firewalls_name_i_index" on "firewalls" ("name_i"))"); + QVERIFY(log2.boundValues.isEmpty()); + + const auto &log3 = log.at(3); + QCOMPARE(log3.query, + R"(create index "name_i_cn_index" on "firewalls" ("name_i_cn"))"); + QVERIFY(log3.boundValues.isEmpty()); + + const auto &log4 = log.at(4); + QCOMPARE(log4.query, + R"(create index "name_r_raw" on "firewalls" ("name_r", name_r1))"); + QVERIFY(log4.boundValues.isEmpty()); + + const auto &log5 = log.at(5); + QCOMPARE(log5.query, + "create index \"firewalls_name_f_fulltext\" on \"firewalls\" " + "using gin ((to_tsvector('english', \"name_f\")))"); + QVERIFY(log5.boundValues.isEmpty()); + + const auto &log6 = log.at(6); + QCOMPARE(log6.query, + "create index \"name_f_cn_fulltext\" on \"firewalls\" " + "using gin ((to_tsvector('english', \"name_f_cn\")))"); + QVERIFY(log6.boundValues.isEmpty()); + + const auto &log7 = log.at(7); + QCOMPARE(log7.query, + "create index \"firewalls_coordinates_s_spatialindex\" on \"firewalls\" " + "using gist (\"coordinates_s\")"); + QVERIFY(log3.boundValues.isEmpty()); + + const auto &log8 = log.at(8); + QCOMPARE(log8.query, + "create index \"coordinates_s_cn_spatial\" on \"firewalls\" " + "using gist (\"coordinates_s_cn\")"); + QVERIFY(log8.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::renameIndex() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.id(); + + table.string(NAME).unique(); + }); + + Schema::on(connection.getName()) + .table(Firewalls, [](Blueprint &table) + { + table.renameIndex("firewalls_name_unique", "firewalls_name_unique_renamed"); + }); + }); + + QCOMPARE(log.size(), 3); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "create table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"name\" varchar(255) not null)"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_name_unique\" unique (\"name\")"); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + "alter index \"firewalls_name_unique\" " + "rename to \"firewalls_name_unique_renamed\""); + QVERIFY(log2.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::dropIndex_ByIndexName() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.unsignedInteger(ID); + table.primary(ID); + + table.string("name_u").unique(); + table.string("name_i").index(); + table.string("name_f").fulltext(); + table.geometry("coordinates_s").spatialIndex(); + }); + + Schema::on(connection.getName()) + .table(Firewalls, [](Blueprint &table) + { + table.dropPrimary(); + table.dropUnique("firewalls_name_u_unique"); + table.dropIndex("firewalls_name_i_index"); + table.dropFullText("firewalls_name_f_fulltext"); + table.dropSpatialIndex("firewalls_coordinates_s_spatialindex"); + }); + }); + + QCOMPARE(log.size(), 11); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "create table \"firewalls\" (" + "\"id\" integer not null, " + "\"name_u\" varchar(255) not null, " + "\"name_i\" varchar(255) not null, " + "\"name_f\" varchar(255) not null, " + "\"coordinates_s\" geography(geometry, 4326) not null)"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + R"(alter table "firewalls" add primary key ("id"))"); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_name_u_unique\" unique (\"name_u\")"); + QVERIFY(log2.boundValues.isEmpty()); + + const auto &log3 = log.at(3); + QCOMPARE(log3.query, + R"(create index "firewalls_name_i_index" on "firewalls" ("name_i"))"); + QVERIFY(log3.boundValues.isEmpty()); + + const auto &log4 = log.at(4); + QCOMPARE(log4.query, + "create index \"firewalls_name_f_fulltext\" on \"firewalls\" " + "using gin ((to_tsvector('english', \"name_f\")))"); + QVERIFY(log4.boundValues.isEmpty()); + + const auto &log5 = log.at(5); + QCOMPARE(log5.query, + "create index \"firewalls_coordinates_s_spatialindex\" on \"firewalls\" " + "using gist (\"coordinates_s\")"); + QVERIFY(log5.boundValues.isEmpty()); + + const auto &log6 = log.at(6); + QCOMPARE(log6.query, + R"(alter table "firewalls" drop constraint "firewalls_pkey")"); + QVERIFY(log6.boundValues.isEmpty()); + + const auto &log7 = log.at(7); + QCOMPARE(log7.query, + R"(alter table "firewalls" drop constraint "firewalls_name_u_unique")"); + QVERIFY(log3.boundValues.isEmpty()); + + const auto &log8 = log.at(8); + QCOMPARE(log8.query, + R"(drop index "firewalls_name_i_index")"); + QVERIFY(log8.boundValues.isEmpty()); + + const auto &log9 = log.at(9); + QCOMPARE(log9.query, + R"(drop index "firewalls_name_f_fulltext")"); + QVERIFY(log9.boundValues.isEmpty()); + + const auto &log10 = log.at(10); + QCOMPARE(log10.query, + R"(drop index "firewalls_coordinates_s_spatialindex")"); + QVERIFY(log10.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::dropIndex_ByColumn() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.unsignedInteger(ID); + table.primary(ID); + + table.string("name_u").unique(); + table.string("name_i").index(); + table.string("name_f").fulltext(); + table.geometry("coordinates_s").spatialIndex(); + }); + + Schema::on(connection.getName()) + .table(Firewalls, [](Blueprint &table) + { + table.dropPrimary(); + table.dropUnique({"name_u"}); + table.dropIndex({"name_i"}); + table.dropFullText({"name_f"}); + table.dropSpatialIndex({"coordinates_s"}); + }); + }); + + QCOMPARE(log.size(), 11); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "create table \"firewalls\" (" + "\"id\" integer not null, " + "\"name_u\" varchar(255) not null, " + "\"name_i\" varchar(255) not null, " + "\"name_f\" varchar(255) not null, " + "\"coordinates_s\" geography(geometry, 4326) not null)"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + R"(alter table "firewalls" add primary key ("id"))"); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_name_u_unique\" unique (\"name_u\")"); + QVERIFY(log2.boundValues.isEmpty()); + + const auto &log3 = log.at(3); + QCOMPARE(log3.query, + R"(create index "firewalls_name_i_index" on "firewalls" ("name_i"))"); + QVERIFY(log3.boundValues.isEmpty()); + + const auto &log4 = log.at(4); + QCOMPARE(log4.query, + "create index \"firewalls_name_f_fulltext\" on \"firewalls\" " + "using gin ((to_tsvector('english', \"name_f\")))"); + QVERIFY(log4.boundValues.isEmpty()); + + const auto &log5 = log.at(5); + QCOMPARE(log5.query, + "create index \"firewalls_coordinates_s_spatialindex\" on \"firewalls\" " + "using gist (\"coordinates_s\")"); + QVERIFY(log5.boundValues.isEmpty()); + + const auto &log6 = log.at(6); + QCOMPARE(log6.query, + R"(alter table "firewalls" drop constraint "firewalls_pkey")"); + QVERIFY(log6.boundValues.isEmpty()); + + const auto &log7 = log.at(7); + QCOMPARE(log7.query, + R"(alter table "firewalls" drop constraint "firewalls_name_u_unique")"); + QVERIFY(log3.boundValues.isEmpty()); + + const auto &log8 = log.at(8); + QCOMPARE(log8.query, + R"(drop index "firewalls_name_i_index")"); + QVERIFY(log8.boundValues.isEmpty()); + + const auto &log9 = log.at(9); + QCOMPARE(log9.query, + R"(drop index "firewalls_name_f_fulltext")"); + QVERIFY(log9.boundValues.isEmpty()); + + const auto &log10 = log.at(10); + QCOMPARE(log10.query, + R"(drop index "firewalls_coordinates_s_spatialindex")"); + QVERIFY(log10.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::dropIndex_ByMultipleColumns() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.unsignedInteger(ID); + table.unsignedInteger("id1"); + table.primary({ID, "id1"}); + + table.string("name_u"); + table.string("name_u1"); + table.unique({"name_u", "name_u1"}); + + table.string("name_i"); + table.string("name_i1"); + table.index({"name_i", "name_i1"}); + + table.string("name_f"); + table.string("name_f1"); + table.fullText({"name_f", "name_f1"}); + }); + + Schema::on(connection.getName()) + .table(Firewalls, [](Blueprint &table) + { + table.dropPrimary({ID, "id1"}); + table.dropUnique({"name_u", "name_u1"}); + table.dropIndex({"name_i", "name_i1"}); + table.dropFullText({"name_f", "name_f1"}); + }); + }); + + QCOMPARE(log.size(), 9); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "create table \"firewalls\" (" + "\"id\" integer not null, " + "\"id1\" integer not null, " + "\"name_u\" varchar(255) not null, " + "\"name_u1\" varchar(255) not null, " + "\"name_i\" varchar(255) not null, " + "\"name_i1\" varchar(255) not null, " + "\"name_f\" varchar(255) not null, " + "\"name_f1\" varchar(255) not null)"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + R"(alter table "firewalls" add primary key ("id", "id1"))"); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_name_u_name_u1_unique\" " + "unique (\"name_u\", \"name_u1\")"); + QVERIFY(log2.boundValues.isEmpty()); + + const auto &log3 = log.at(3); + QCOMPARE(log3.query, + "create index \"firewalls_name_i_name_i1_index\" " + "on \"firewalls\" (\"name_i\", \"name_i1\")"); + QVERIFY(log3.boundValues.isEmpty()); + + const auto &log4 = log.at(4); + QCOMPARE(log4.query, + "create index \"firewalls_name_f_name_f1_fulltext\" on \"firewalls\" " + "using gin ((to_tsvector('english', \"name_f\") || " + "to_tsvector('english', \"name_f1\")))"); + QVERIFY(log4.boundValues.isEmpty()); + + const auto &log5 = log.at(5); + QCOMPARE(log5.query, + R"(alter table "firewalls" drop constraint "firewalls_pkey")"); + QVERIFY(log5.boundValues.isEmpty()); + + const auto &log6 = log.at(6); + QCOMPARE(log6.query, + "alter table \"firewalls\" drop constraint " + "\"firewalls_name_u_name_u1_unique\""); + QVERIFY(log3.boundValues.isEmpty()); + + const auto &log7 = log.at(7); + QCOMPARE(log7.query, + R"(drop index "firewalls_name_i_name_i1_index")"); + QVERIFY(log7.boundValues.isEmpty()); + + const auto &log8 = log.at(8); + QCOMPARE(log8.query, + R"(drop index "firewalls_name_f_name_f1_fulltext")"); + QVERIFY(log8.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::foreignKey() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.id(); + + table.unsignedBigInteger("user_id"); + table.unsignedBigInteger("torrent_id"); + table.unsignedBigInteger("role_id").nullable(); + + table.foreign("user_id").references(ID).on("users") + .onDelete(Cascade).onUpdate(Restrict); + table.foreign("torrent_id").references(ID).on("torrents") + .restrictOnDelete().restrictOnUpdate(); + table.foreign("role_id").references(ID).on("roles") + .nullOnDelete().cascadeOnUpdate(); + }); + }); + + QCOMPARE(log.size(), 4); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "create table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"user_id\" bigint not null, " + "\"torrent_id\" bigint not null, " + "\"role_id\" bigint null)"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_user_id_foreign\" " + "foreign key (\"user_id\") " + "references \"users\" (\"id\") " + "on delete cascade on update restrict"); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_torrent_id_foreign\" " + "foreign key (\"torrent_id\") " + "references \"torrents\" (\"id\") " + "on delete restrict on update restrict"); + QVERIFY(log2.boundValues.isEmpty()); + + const auto &log3 = log.at(3); + QCOMPARE(log3.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_role_id_foreign\" " + "foreign key (\"role_id\") " + "references \"roles\" (\"id\") " + "on delete set null on update cascade"); + QVERIFY(log3.boundValues.isEmpty()); +} + +void tst_PostgreSQL_SchemaBuilder::foreignKey_TerserSyntax() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.id(); + + table.foreignId("user_id").constrained() + .onDelete(Cascade).onUpdate(Restrict); + table.foreignId("torrent_id").constrained() + .restrictOnDelete().restrictOnUpdate(); + table.foreignId("role_id").nullable().constrained() + .nullOnDelete().cascadeOnUpdate(); + }); + }); + + QCOMPARE(log.size(), 4); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "create table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"user_id\" bigint not null, " + "\"torrent_id\" bigint not null, " + "\"role_id\" bigint null)"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_user_id_foreign\" " + "foreign key (\"user_id\") " + "references \"users\" (\"id\") " + "on delete cascade on update restrict"); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_torrent_id_foreign\" " + "foreign key (\"torrent_id\") " + "references \"torrents\" (\"id\") " + "on delete restrict on update restrict"); + QVERIFY(log2.boundValues.isEmpty()); + + const auto &log3 = log.at(3); + QCOMPARE(log3.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_role_id_foreign\" " + "foreign key (\"role_id\") " + "references \"roles\" (\"id\") " + "on delete set null on update cascade"); + QVERIFY(log3.boundValues.isEmpty()); +} + +#ifndef TINYORM_DISABLE_ORM +void tst_PostgreSQL_SchemaBuilder::foreignKey_WithModel() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Models::Torrent torrent; + Models::User user; + + Schema::on(connection.getName()) + .create(Firewalls, [&torrent, &user](Blueprint &table) + { + table.id(); + + table.foreignIdFor(torrent).constrained() + .onDelete(Cascade).onUpdate(Restrict); + table.foreignIdFor(user).nullable().constrained() + .nullOnDelete().cascadeOnUpdate(); + }); + }); + + QCOMPARE(log.size(), 3); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "create table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"torrent_id\" bigint not null, " + "\"user_id\" bigint null)"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_torrent_id_foreign\" " + "foreign key (\"torrent_id\") " + "references \"torrents\" (\"id\") " + "on delete cascade on update restrict"); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_user_id_foreign\" " + "foreign key (\"user_id\") " + "references \"users\" (\"id\") " + "on delete set null on update cascade"); + QVERIFY(log2.boundValues.isEmpty()); +} +#endif + +void tst_PostgreSQL_SchemaBuilder::dropForeign() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + table.id(); + + table.foreignId("user_id").constrained() + .onDelete(Cascade).onUpdate(Restrict); + table.foreignId("torrent_id").constrained() + .restrictOnDelete().restrictOnUpdate(); + table.foreignId("role_id").nullable().constrained() + .nullOnDelete().cascadeOnUpdate(); + + // By column name + table.dropForeign({"user_id"}); + // By index name + table.dropForeign("firewalls_torrent_id_foreign"); + // Drop index and also a column + table.dropConstrainedForeignId("role_id"); + }); + }); + + QCOMPARE(log.size(), 8); + + // I leave these comparisons here, even if they are doubled from the previous test + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "create table \"firewalls\" (" + "\"id\" bigserial primary key not null, " + "\"user_id\" bigint not null, " + "\"torrent_id\" bigint not null, " + "\"role_id\" bigint null)"); + QVERIFY(log0.boundValues.isEmpty()); + + const auto &log1 = log.at(1); + QCOMPARE(log1.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_user_id_foreign\" " + "foreign key (\"user_id\") " + "references \"users\" (\"id\") " + "on delete cascade on update restrict"); + QVERIFY(log1.boundValues.isEmpty()); + + const auto &log2 = log.at(2); + QCOMPARE(log2.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_torrent_id_foreign\" " + "foreign key (\"torrent_id\") " + "references \"torrents\" (\"id\") " + "on delete restrict on update restrict"); + QVERIFY(log2.boundValues.isEmpty()); + + const auto &log3 = log.at(3); + QCOMPARE(log3.query, + "alter table \"firewalls\" " + "add constraint \"firewalls_role_id_foreign\" " + "foreign key (\"role_id\") " + "references \"roles\" (\"id\") " + "on delete set null on update cascade"); + QVERIFY(log3.boundValues.isEmpty()); + + const auto &log4 = log.at(4); + QCOMPARE(log4.query, + R"(alter table "firewalls" drop constraint "firewalls_user_id_foreign")"); + QVERIFY(log4.boundValues.isEmpty()); + + const auto &log5 = log.at(5); + QCOMPARE(log5.query, + R"(alter table "firewalls" drop constraint "firewalls_torrent_id_foreign")"); + QVERIFY(log5.boundValues.isEmpty()); + + const auto &log6 = log.at(6); + QCOMPARE(log6.query, + R"(alter table "firewalls" drop constraint "firewalls_role_id_foreign")"); + QVERIFY(log6.boundValues.isEmpty()); + + const auto &log7 = log.at(7); + QCOMPARE(log7.query, + R"(alter table "firewalls" drop column "role_id")"); + QVERIFY(log7.boundValues.isEmpty()); +} + +QTEST_MAIN(tst_PostgreSQL_SchemaBuilder) + +#include "tst_postgresql_schemabuilder.moc" diff --git a/tests/auto/unit/orm/schema/schema.pro b/tests/auto/unit/orm/schema/schema.pro index 1edf4a899..5a088c5d4 100644 --- a/tests/auto/unit/orm/schema/schema.pro +++ b/tests/auto/unit/orm/schema/schema.pro @@ -3,3 +3,4 @@ TEMPLATE = subdirs SUBDIRS = \ blueprint \ mysql_schemabuilder \ + postgresql_schemabuilder \