diff --git a/README.md b/README.md index 8507cc3..cd1a8f0 100644 --- a/README.md +++ b/README.md @@ -241,13 +241,17 @@ Possible customizations: * `IDENTITY [true|false]`: mark the property as the identity * `AUTOGENERATED [true|false]`: mark the property as autogenerated by the database backend * `TRANSIENT [true|false]`: mark the property as transient + * `NOT_NULL [true|false]`: mark the property as not null + * `FOREIGN_KEY [true|false]`: mark the relation as having a foreign key in the generated schema + * `UNIQUE []`: mark the property as unique (properties with same `common_name` will create a compound unique index) Restrictions and requirements: * There can be only one `IDENTITY` * `IDENTITY` is required for `AUTOGENERATED` * `TRANSIENT` cannot be combined with `IDENTITY` -* Renaming columns and tables to names containing QtOrm keywords (`IDENTITY`, `COLUMN`, `TRANSIENT`, etc.) is not supported. +* Renaming columns and tables to names containing QtOrm keywords (`IDENTITY`, `COLUMN`, `TRANSIENT`, etc.) or the `!` and `,` characters is not supported. +* Schema `Append` mode will not set `NOT_NULL` on `UNIQUE` on the new column(s) #### Relationships diff --git a/src/orm/qormglobal.h b/src/orm/qormglobal.h index 2ff6f98..4b17977 100644 --- a/src/orm/qormglobal.h +++ b/src/orm/qormglobal.h @@ -155,7 +155,10 @@ namespace QOrm Autogenerated, Identity, Transient, - Schema + Schema, + NotNull, + ForeignKey, + Unique }; inline auto qHash(Keyword value) { diff --git a/src/orm/qormmetadatacache.cpp b/src/orm/qormmetadatacache.cpp index efceb0e..b87e98a 100644 --- a/src/orm/qormmetadatacache.cpp +++ b/src/orm/qormmetadatacache.cpp @@ -52,7 +52,10 @@ namespace {QOrm::Keyword::Column, QLatin1String("COLUMN")}, {QOrm::Keyword::Identity, QLatin1String("IDENTITY")}, {QOrm::Keyword::Transient, QLatin1String("TRANSIENT")}, - {QOrm::Keyword::Autogenerated, QLatin1String("AUTOGENERATED")}}; + {QOrm::Keyword::Autogenerated, QLatin1String("AUTOGENERATED")}, + {QOrm::Keyword::NotNull, QLatin1String("NOT_NULL")}, + {QOrm::Keyword::ForeignKey, QLatin1String("FOREIGN_KEY")}, + {QOrm::Keyword::Unique, QLatin1String("UNIQUE")}}; template KeywordPosition findNextKeyword(const QString& data, @@ -301,6 +304,47 @@ namespace ormPropertyInfo.insert(QOrm::Keyword::Autogenerated, isAutogenerated.value_or(true)); } + else if (keywordPosition.keyword->id == QOrm::Keyword::NotNull) + { + auto extractResult = extractBoolean(data, pos, PropertyKeywords); + + if (!extractResult.has_value()) + { + qFatal("QtOrm: syntax error in %s in Q_ORM_PROPERTY(%s ...) after NOT_NULL", + qMetaObject.className(), + qPrintable(propertyName)); + } + + std::optional isNotNull = extractResult->value; + keywordPosition = extractResult->nextKeyword; + + ormPropertyInfo.insert(QOrm::Keyword::NotNull, isNotNull.value_or(true)); + } + else if (keywordPosition.keyword->id == QOrm::Keyword::ForeignKey) + { + auto extractResult = extractBoolean(data, pos, PropertyKeywords); + + if (!extractResult.has_value()) + { + qFatal("QtOrm: syntax error in %s in Q_ORM_PROPERTY(%s ...) after FOREIGN_KEY", + qMetaObject.className(), + qPrintable(propertyName)); + } + + std::optional hasForeignKey = extractResult->value; + keywordPosition = extractResult->nextKeyword; + + ormPropertyInfo.insert(QOrm::Keyword::ForeignKey, hasForeignKey.value_or(true)); + } + else if (keywordPosition.keyword->id == QOrm::Keyword::Unique) + { + auto extractResult = extractString(data, pos, PropertyKeywords); + + QString uniqueGroup = extractResult.value; + keywordPosition = extractResult.nextKeyword; + + ormPropertyInfo.insert(QOrm::Keyword::Unique, uniqueGroup); + } } return ormPropertyInfo; @@ -320,6 +364,11 @@ class QOrmMetadataCachePrivate const QOrmMetadata* referencedEntity = nullptr; bool isTransient = false; bool isEnumeration{false}; + bool isNotNull = false; + bool hasForeignKey = false; + bool isUnique = false; + QString uniqueGroup; + QMetaType::Type dataType{QMetaType::UnknownType}; }; @@ -464,6 +513,10 @@ void QOrmMetadataCachePrivate::initialize(const QByteArray& className, descriptor.dataType, descriptor.referencedEntity, descriptor.isTransient, + descriptor.isNotNull, + descriptor.hasForeignKey, + descriptor.isUnique, + descriptor.uniqueGroup, userPropertyMetadata); auto idx = static_cast(data->m_propertyMappings.size() - 1); @@ -496,6 +549,10 @@ QOrmMetadataCachePrivate::MappingDescriptor QOrmMetadataCachePrivate::mappingDes bool isObjectId = qstricmp(property.name(), "id") == 0; bool isAutogenerated = isObjectId; bool isTransient = !property.isStored(); + bool isNotNull = false; + bool hasForeignKey = false; + bool isUnique = false; + QString uniqueGroup; // Check if defaults are overridden by the user property metadata if (userPropertyMetadata.contains(QOrm::Keyword::Column)) @@ -519,11 +576,31 @@ QOrmMetadataCachePrivate::MappingDescriptor QOrmMetadataCachePrivate::mappingDes isTransient = userPropertyMetadata.value(QOrm::Keyword::Transient).toBool(); } + if (userPropertyMetadata.contains(QOrm::Keyword::NotNull)) + { + isNotNull = userPropertyMetadata.value(QOrm::Keyword::NotNull).toBool(); + } + + if (userPropertyMetadata.contains(QOrm::Keyword::ForeignKey)) + { + hasForeignKey = userPropertyMetadata.value(QOrm::Keyword::ForeignKey).toBool(); + } + + if (userPropertyMetadata.contains(QOrm::Keyword::Unique)) + { + isUnique = true; + uniqueGroup = userPropertyMetadata.value(QOrm::Keyword::Unique).toString(); + } + descriptor.classPropertyName = QString::fromUtf8(property.name()); descriptor.tableFieldName = tableFieldName; descriptor.isObjectId = isObjectId; descriptor.isAutogenerated = isAutogenerated; descriptor.isTransient = isTransient; + descriptor.isNotNull = isNotNull; + descriptor.hasForeignKey = hasForeignKey; + descriptor.isUnique = isUnique; + descriptor.uniqueGroup = uniqueGroup; #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) descriptor.dataType = property.type() == QVariant::UserType ? static_cast(property.userType()) diff --git a/src/orm/qormpropertymapping.cpp b/src/orm/qormpropertymapping.cpp index 85efcd4..c7c0b6b 100644 --- a/src/orm/qormpropertymapping.cpp +++ b/src/orm/qormpropertymapping.cpp @@ -40,6 +40,20 @@ QDebug operator<<(QDebug dbg, const QOrmPropertyMapping& propertyMapping) if (propertyMapping.isTransient()) dbg << ", transient"; + if (propertyMapping.isNotNull()) + dbg << ", not null"; + + if (propertyMapping.hasForeignKey()) + dbg << ", fk"; + + if (propertyMapping.isUnique()) { + dbg << ", unique"; + + if (!propertyMapping.uniqueGroup().isEmpty()) { + dbg << "(" << propertyMapping.uniqueGroup() << ")"; + } + } + dbg << ")"; return dbg; @@ -58,6 +72,10 @@ class QOrmPropertyMappingPrivate : public QSharedData QMetaType::Type dataType, const QOrmMetadata* referencedEntity, bool isTransient, + bool isNotNull, + bool hasForeignKey, + bool isUnique, + QString uniqueGroup, QOrmUserMetadata userMetadata) : m_enclosingEntity{enclosingEntity} , m_qMetaProperty{std::move(qMetaProperty)} @@ -68,6 +86,10 @@ class QOrmPropertyMappingPrivate : public QSharedData , m_dataType{dataType} , m_referencedEntity{referencedEntity} , m_isTransient{isTransient} + , m_isNotNull{isNotNull} + , m_hasForeignKey{hasForeignKey} + , m_isUnique{isUnique} + , m_uniqueGroup{uniqueGroup} , m_userMetadata{std::move(userMetadata)} { } @@ -81,6 +103,10 @@ class QOrmPropertyMappingPrivate : public QSharedData QMetaType::Type m_dataType{QMetaType::UnknownType}; const QOrmMetadata* m_referencedEntity{nullptr}; bool m_isTransient{false}; + bool m_isNotNull{false}; + bool m_hasForeignKey{false}; + bool m_isUnique{false}; + QString m_uniqueGroup; QOrmUserMetadata m_userMetadata; }; @@ -93,6 +119,10 @@ QOrmPropertyMapping::QOrmPropertyMapping(const QOrmMetadata& enclosingEntity, QMetaType::Type dataType, const QOrmMetadata* referencedEntity, bool isTransient, + bool isNotNull, + bool hasForeignKey, + bool isUnique, + QString uniqueGroup, QOrmUserMetadata userMetadata) : d{new QOrmPropertyMappingPrivate{enclosingEntity, std::move(qMetaProperty), @@ -103,6 +133,10 @@ QOrmPropertyMapping::QOrmPropertyMapping(const QOrmMetadata& enclosingEntity, dataType, referencedEntity, isTransient, + isNotNull, + hasForeignKey, + isUnique, + uniqueGroup, std::move(userMetadata)}} { } @@ -172,6 +206,26 @@ bool QOrmPropertyMapping::isTransient() const return d->m_isTransient; } +bool QOrmPropertyMapping::isNotNull() const +{ + return d->m_isNotNull; +} + +bool QOrmPropertyMapping::hasForeignKey() const +{ + return d->m_hasForeignKey; +} + +bool QOrmPropertyMapping::isUnique() const +{ + return d->m_isUnique; +} + +QString QOrmPropertyMapping::uniqueGroup() const +{ + return d->m_uniqueGroup; +} + const QOrmUserMetadata& QOrmPropertyMapping::userMetadata() const { return d->m_userMetadata; diff --git a/src/orm/qormpropertymapping.h b/src/orm/qormpropertymapping.h index f696f2d..d5621fe 100644 --- a/src/orm/qormpropertymapping.h +++ b/src/orm/qormpropertymapping.h @@ -45,6 +45,10 @@ class Q_ORM_EXPORT QOrmPropertyMapping QMetaType::Type dataType, const QOrmMetadata* referencedEntity, bool isTransient, + bool isNotNull, + bool hasForeignKey, + bool isUnique, + QString uniqueGroup, QOrmUserMetadata userMetadata); QOrmPropertyMapping(const QOrmPropertyMapping&); QOrmPropertyMapping(QOrmPropertyMapping&&); @@ -64,6 +68,10 @@ class Q_ORM_EXPORT QOrmPropertyMapping [[nodiscard]] bool isReference() const; [[nodiscard]] const QOrmMetadata* referencedEntity() const; [[nodiscard]] bool isTransient() const; + [[nodiscard]] bool isNotNull() const; + [[nodiscard]] bool hasForeignKey() const; + [[nodiscard]] bool isUnique() const; + [[nodiscard]] QString uniqueGroup() const; [[nodiscard]] const QOrmUserMetadata& userMetadata() const; private: diff --git a/src/orm/qormsessionconfiguration.cpp b/src/orm/qormsessionconfiguration.cpp index 303ed8f..abebad0 100644 --- a/src/orm/qormsessionconfiguration.cpp +++ b/src/orm/qormsessionconfiguration.cpp @@ -59,6 +59,7 @@ static QOrmSqliteConfiguration _build_json_sqlite_configuration(const QJsonObjec sqlConfiguration.setDatabaseName(object["databaseName"].toString()); sqlConfiguration.setVerbose(object["verbose"].toBool(false)); sqlConfiguration.setConnectOptions(object["connectOptions"].toString()); + sqlConfiguration.setForeignKeysEnabled(object["foreignKeysEnabled"].toBool(false)); QString schemaModeStr = object["schemaMode"].toString("validate").toLower(); diff --git a/src/orm/qormsqliteconfiguration.cpp b/src/orm/qormsqliteconfiguration.cpp index e59133f..89cafee 100644 --- a/src/orm/qormsqliteconfiguration.cpp +++ b/src/orm/qormsqliteconfiguration.cpp @@ -62,4 +62,14 @@ void QOrmSqliteConfiguration::setSchemaMode(SchemaMode schemaMode) m_schemaMode = schemaMode; } +bool QOrmSqliteConfiguration::foreignKeysEnabled() const +{ + return m_foreignKeysEnabled; +} + +void QOrmSqliteConfiguration::setForeignKeysEnabled(bool foreignKeysEnabled) +{ + m_foreignKeysEnabled = foreignKeysEnabled; +} + QT_END_NAMESPACE diff --git a/src/orm/qormsqliteconfiguration.h b/src/orm/qormsqliteconfiguration.h index 47c866f..9f9ab9e 100644 --- a/src/orm/qormsqliteconfiguration.h +++ b/src/orm/qormsqliteconfiguration.h @@ -55,11 +55,15 @@ class Q_ORM_EXPORT QOrmSqliteConfiguration SchemaMode schemaMode() const; void setSchemaMode(SchemaMode schemaMode); + [[nodiscard]] bool foreignKeysEnabled() const; + void setForeignKeysEnabled(bool foreignKeysEnabled); + private: QString m_connectOptions; QString m_databaseName; bool m_verbose{false}; SchemaMode m_schemaMode; + bool m_foreignKeysEnabled{false}; }; QT_END_NAMESPACE diff --git a/src/orm/qormsqliteprovider.cpp b/src/orm/qormsqliteprovider.cpp index 3403157..c73a1b4 100644 --- a/src/orm/qormsqliteprovider.cpp +++ b/src/orm/qormsqliteprovider.cpp @@ -74,6 +74,8 @@ class QOrmSqliteProviderPrivate QString toSqlType(QMetaType::Type type); [[nodiscard]] bool canConvertFromSqliteToQProperty(QMetaType::Type fromSqlType, QMetaType::Type toQPropertyType); + [[nodiscard]] bool fieldHasForeignKey(const QSqlField& field); + [[nodiscard]] QString fieldGetUniqueConstraint(const QSqlField& field); Q_REQUIRED_RESULT QOrmError lastDatabaseError() const; @@ -141,6 +143,40 @@ bool QOrmSqliteProviderPrivate::canConvertFromSqliteToQProperty(QMetaType::Type #endif } +bool QOrmSqliteProviderPrivate::fieldHasForeignKey(const QSqlField& field) +{ + QSqlQuery query = prepareAndExecute( + R"(SELECT 1 FROM pragma_foreign_key_list(:table_name) WHERE "from" = :column)", + {{":table_name", field.tableName()},{":column", field.name()}}); + + // query.size() is not supported for sqlite, but first() will confirm whether we got at least + // one result which is what we care about + return query.first(); +} + +QString QOrmSqliteProviderPrivate::fieldGetUniqueConstraint(const QSqlField& field) +{ + // This query returns a string representing the columns in a unique index for the field + // The column names are also wrapped between ! to avoid partial matches with LIKE + // This can then be used to check if the schema needs updating + QSqlQuery query = prepareAndExecute( + R"( + SELECT group_concat(printf('!%s!', index_info.name)) AS columns + FROM pragma_index_list(:table_name) AS index_list, + pragma_index_info(index_list.name) AS index_info + WHERE index_list.[unique] = 1 + GROUP BY index_list.name + HAVING columns LIKE printf('%%!%s!%%', :column))", + {{":table_name", field.tableName()},{":column", field.name()}}); + + if (!query.first()) + { + return ""; + } + + return query.record().field("columns").value().toString(); +} + QOrmError QOrmSqliteProviderPrivate::lastDatabaseError() const { return QOrmError{QOrm::ErrorType::Provider, m_database.lastError().text()}; @@ -570,6 +606,14 @@ QOrmError QOrmSqliteProviderPrivate::updateSchema(const QOrmRelation& relation) << " mapping."; updateNeeded = true; } + else if (fieldHasForeignKey(field) != mapping->hasForeignKey()) + { + qCDebug(qtorm).noquote().nospace() + << "updating table " << relation.mapping()->tableName() << ": field " + << field.name() << " foreign key (" << !mapping->hasForeignKey() + << ") differs from the mapping (" << mapping->hasForeignKey() << ")"; + updateNeeded = true; + } } #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) else if (!canConvertFromSqliteToQProperty(static_cast(field.type()), @@ -586,6 +630,64 @@ QOrmError QOrmSqliteProviderPrivate::updateSchema(const QOrmRelation& relation) << " are incompatible."; updateNeeded = true; } + else if ((field.requiredStatus() == QSqlField::Required) != mapping->isNotNull()) + { + qCDebug(qtorm).noquote().nospace() + << "updating table " << relation.mapping()->tableName() << ": field " + << field.name() << " nullability (" << field.requiredStatus() + << ") differs from the mapping (" << mapping->isNotNull() << ")"; + updateNeeded = true; + } + else + { + QString fieldUniqueColumns = fieldGetUniqueConstraint(field); + + if (mapping->isUnique() != !fieldUniqueColumns.isEmpty() || + (mapping->isUnique() && mapping->uniqueGroup().isEmpty() && fieldUniqueColumns != QString("!%1!").arg(field.name()))) + { + qCDebug(qtorm).noquote().nospace() + << "updating table " << relation.mapping()->tableName() << ": field " + << field.name() << " unique constraint differs from the mapping (" + << mapping->isUnique() << "," << mapping->uniqueGroup() << ")"; + updateNeeded = true; + } + else if (mapping->isUnique() && !mapping->uniqueGroup().isEmpty()) + { + QStringList columnsInGroup = fieldUniqueColumns.split(","); + for (auto& column : columnsInGroup) + column.remove('!'); + + for (const auto& column : columnsInGroup) { + const QOrmPropertyMapping* groupColumnMapping = + relation.mapping()->tableFieldMapping(column); + + if (groupColumnMapping == nullptr || !groupColumnMapping->isUnique() || + groupColumnMapping->uniqueGroup() != mapping->uniqueGroup()) + { + qCDebug(qtorm).noquote().nospace() + << "updating table " << relation.mapping()->tableName() << ": field " + << column << " should be in the same unique constraint as " + << field.name(); + updateNeeded = true; + break; + } + } + + for (const auto& propertyMapping : relation.mapping()->propertyMappings()) + { + if (propertyMapping.isUnique() && propertyMapping.uniqueGroup() == mapping->uniqueGroup() && + !columnsInGroup.contains(propertyMapping.tableFieldName())) + { + qCDebug(qtorm).noquote().nospace() + << "updating table " << relation.mapping()->tableName() << ": field " + << propertyMapping.tableFieldName() << " should be in the same unique constraint as " + << field.name(); + updateNeeded = true; + break; + } + } + } + } } // Check if there are non-transient class properties that are not mapped in the database. @@ -1121,6 +1223,11 @@ QOrmError QOrmSqliteProvider::connectToBackend() { return d->lastDatabaseError(); } + + if (d->m_sqlConfiguration.foreignKeysEnabled()) + { + return d->setForeignKeysEnabled(true); + } } return QOrmError{QOrm::ErrorType::None, {}}; diff --git a/src/orm/qormsqlitestatementgenerator_p.cpp b/src/orm/qormsqlitestatementgenerator_p.cpp index 04812a9..6c7dc44 100644 --- a/src/orm/qormsqlitestatementgenerator_p.cpp +++ b/src/orm/qormsqlitestatementgenerator_p.cpp @@ -204,7 +204,7 @@ QString QOrmSqliteStatementGenerator::generateUpdateStatement(const QOrmMetadata QString whereClause = generateWhereClause(QOrmFilter{*relation.objectIdMapping() == objectId}, boundParameters); - QStringList parts = {"UPDATE", relation.tableName(), "SET", setList.join(','), whereClause}; + QStringList parts = {"UPDATE", escapeIdentifier(relation.tableName()), "SET", setList.join(','), whereClause}; return parts.join(QChar(' ')); } @@ -512,6 +512,7 @@ QString QOrmSqliteStatementGenerator::generateCreateTableStatement( std::optional overrideTableName) { QStringList fields; + QMap uniqueGroups; for (const QOrmPropertyMapping& mapping : entity.propertyMappings()) { @@ -526,6 +527,13 @@ QString QOrmSqliteStatementGenerator::generateCreateTableStatement( columnDefs += {escapeIdentifier(mapping.tableFieldName()), toSqliteType(mapping.referencedEntity()->objectIdMapping()->dataType())}; + + if (mapping.hasForeignKey()) + columnDefs += {QStringLiteral("REFERENCES"), + escapeIdentifier(mapping.referencedEntity()->tableName()), + QStringLiteral("("), + escapeIdentifier(mapping.referencedEntity()->objectIdMapping()->tableFieldName()), + QStringLiteral(")")}; } else { @@ -539,15 +547,37 @@ QString QOrmSqliteStatementGenerator::generateCreateTableStatement( columnDefs.push_back(QStringLiteral("AUTOINCREMENT")); } + if (mapping.isNotNull()) + columnDefs.push_back(QStringLiteral("NOT NULL")); + + if (mapping.isUnique()) + { + if (mapping.uniqueGroup().isEmpty()) + { + columnDefs.push_back(QStringLiteral("UNIQUE")); + } + else + { + uniqueGroups[mapping.uniqueGroup()].push_back(escapeIdentifier(mapping.tableFieldName())); + } + } + fields.push_back(columnDefs.join(' ')); } + for (const QStringList& columns : std::as_const(uniqueGroups)) { + QStringList uniqueDef = {QStringLiteral("UNIQUE("), + columns.join(','), + QStringLiteral(")")}; + fields.push_back(uniqueDef.join(' ')); + } + QString fieldsStr = fields.join(','); Q_ASSERT(!overrideTableName.has_value() || !overrideTableName->isEmpty()); - QString effectiveTableName{overrideTableName.value_or(escapeIdentifier(entity.tableName()))}; + QString effectiveTableName{overrideTableName.value_or(entity.tableName())}; - return QStringLiteral("CREATE TABLE %1(%2)").arg(effectiveTableName, fieldsStr); + return QStringLiteral("CREATE TABLE %1(%2)").arg(escapeIdentifier(effectiveTableName), fieldsStr); } QString QOrmSqliteStatementGenerator::generateAlterTableAddColumnStatement( @@ -568,10 +598,23 @@ QString QOrmSqliteStatementGenerator::generateAlterTableAddColumnStatement( dataType = toSqliteType(propertyMapping.dataType()); } - return QStringLiteral("ALTER TABLE %1 ADD COLUMN %2 %3") + QString possibleFk = ""; + + if (propertyMapping.isReference() && propertyMapping.hasForeignKey()) + { + QStringList fkDef = {QStringLiteral("REFERENCES"), + escapeIdentifier(propertyMapping.referencedEntity()->tableName()), + QStringLiteral("("), + escapeIdentifier(propertyMapping.referencedEntity()->objectIdMapping()->tableFieldName()), + QStringLiteral(")")}; + possibleFk = fkDef.join(' '); + } + + return QStringLiteral("ALTER TABLE %1 ADD COLUMN %2 %3 %4") .arg(escapeIdentifier(relation.tableName()), escapeIdentifier(propertyMapping.tableFieldName()), - dataType); + dataType, + possibleFk); } QString QOrmSqliteStatementGenerator::generateDropTableStatement(const QOrmMetadata& entity) diff --git a/tests/auto/qormsession/CMakeLists.txt b/tests/auto/qormsession/CMakeLists.txt index 192a2d3..6d827c9 100644 --- a/tests/auto/qormsession/CMakeLists.txt +++ b/tests/auto/qormsession/CMakeLists.txt @@ -4,10 +4,16 @@ qtorm_add_unit_test(NAME tst_ormsession SOURCES domain/person.cpp domain/province.cpp domain/town.cpp + domain/withnotnull.cpp + domain/withforeignkey.cpp + domain/withunique.cpp domain/person.h domain/province.h domain/town.h + domain/withnotnull.h + domain/withforeignkey.h + domain/withunique.h ormsession.qrc diff --git a/tests/auto/qormsession/domain/withforeignkey.cpp b/tests/auto/qormsession/domain/withforeignkey.cpp new file mode 100644 index 0000000..f501b34 --- /dev/null +++ b/tests/auto/qormsession/domain/withforeignkey.cpp @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2026 Maciej Sopyło + * + * This file is part of QtOrm library. + * + * QtOrm is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QtOrm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with QtOrm. If not, see . + */ + +#include "withforeignkey.h" + +WithForeignKey::WithForeignKey(QObject* parent) + : QObject{parent} +{ +} diff --git a/tests/auto/qormsession/domain/withforeignkey.h b/tests/auto/qormsession/domain/withforeignkey.h new file mode 100644 index 0000000..41ce097 --- /dev/null +++ b/tests/auto/qormsession/domain/withforeignkey.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2019-2021 Dmitriy Purgin + * Copyright (C) 2019-2021 sequality software engineering e.U. + * Copyright (C) 2026 Maciej Sopyło + * + * This file is part of QtOrm library. + * + * QtOrm is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QtOrm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with QtOrm. If not, see . + */ + +#pragma once + +#include +#include + +#include +#include "withnotnull.h" + +class WithForeignKey: public QObject +{ + Q_OBJECT + + Q_PROPERTY(int id READ id WRITE setId NOTIFY idChanged) + Q_PROPERTY(WithNotNull* data READ data WRITE setData NOTIFY dataChanged) + + Q_ORM_PROPERTY(data FOREIGN_KEY) + +public: + Q_INVOKABLE WithForeignKey(QObject* parent = nullptr); + + [[nodiscard]] int id() const { return m_id; } + void setId(int id) + { + if (m_id != id) + { + m_id = id; + emit idChanged(); + } + } + + [[nodiscard]] WithNotNull* data() const { return m_data; } + void setData(WithNotNull* data) + { + if (m_data != data) + { + m_data = data; + emit dataChanged(); + } + } + +signals: + void idChanged(); + void dataChanged(); + +private: + int m_id{0}; + WithNotNull* m_data{nullptr}; +}; diff --git a/tests/auto/qormsession/domain/withnotnull.cpp b/tests/auto/qormsession/domain/withnotnull.cpp new file mode 100644 index 0000000..085b580 --- /dev/null +++ b/tests/auto/qormsession/domain/withnotnull.cpp @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2026 Maciej Sopyło + * + * This file is part of QtOrm library. + * + * QtOrm is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QtOrm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with QtOrm. If not, see . + */ + +#include "withnotnull.h" + +WithNotNull::WithNotNull(QObject* parent) + : QObject{parent} +{ +} diff --git a/tests/auto/qormsession/domain/withnotnull.h b/tests/auto/qormsession/domain/withnotnull.h new file mode 100644 index 0000000..570f05b --- /dev/null +++ b/tests/auto/qormsession/domain/withnotnull.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019-2021 Dmitriy Purgin + * Copyright (C) 2019-2021 sequality software engineering e.U. + * Copyright (C) 2026 Maciej Sopyło + * + * This file is part of QtOrm library. + * + * QtOrm is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QtOrm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with QtOrm. If not, see . + */ + +#pragma once + +#include +#include + +#include + +class WithNotNull : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int id READ id WRITE setId NOTIFY idChanged) + Q_PROPERTY(int data READ data WRITE setData NOTIFY dataChanged) + + Q_ORM_PROPERTY(data NOT_NULL) + +public: + Q_INVOKABLE WithNotNull(QObject* parent = nullptr); + + [[nodiscard]] int id() const { return m_id; } + void setId(int id) + { + if (m_id != id) + { + m_id = id; + emit idChanged(); + } + } + + [[nodiscard]] int data() const { return m_data; } + void setData(const int data) + { + if (m_data != data) + { + m_data = data; + emit dataChanged(); + } + } + +signals: + void idChanged(); + void dataChanged(); + +private: + int m_id{0}; + int m_data; +}; diff --git a/tests/auto/qormsession/domain/withunique.cpp b/tests/auto/qormsession/domain/withunique.cpp new file mode 100644 index 0000000..5ceccd3 --- /dev/null +++ b/tests/auto/qormsession/domain/withunique.cpp @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019-2021 Dmitriy Purgin + * Copyright (C) 2026 Maciej Sopyło + * + * This file is part of QtOrm library. + * + * QtOrm is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QtOrm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with QtOrm. If not, see . + */ + +#include "withunique.h" + +WithUnique::WithUnique(QObject* parent) + : QObject{parent} +{ +} diff --git a/tests/auto/qormsession/domain/withunique.h b/tests/auto/qormsession/domain/withunique.h new file mode 100644 index 0000000..1c88b15 --- /dev/null +++ b/tests/auto/qormsession/domain/withunique.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2019-2021 Dmitriy Purgin + * Copyright (C) 2019-2021 sequality software engineering e.U. + * Copyright (C) 2026 Maciej Sopyło + * + * This file is part of QtOrm library. + * + * QtOrm is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QtOrm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with QtOrm. If not, see . + */ + +#pragma once + +#include +#include + +#include + +class WithUnique: public QObject +{ + Q_OBJECT + + Q_PROPERTY(int id READ id WRITE setId NOTIFY idChanged) + Q_PROPERTY(QString single READ single WRITE setSingle NOTIFY singleChanged) + Q_PROPERTY(QString multiOne READ multiOne WRITE setMultiOne NOTIFY multiOneChanged) + Q_PROPERTY(QString multiTwo READ multiTwo WRITE setMultiTwo NOTIFY multiTwoChanged) + + Q_ORM_PROPERTY(single UNIQUE) + Q_ORM_PROPERTY(multiOne UNIQUE multi) + Q_ORM_PROPERTY(multiTwo UNIQUE multi) + +public: + Q_INVOKABLE WithUnique(QObject* parent = nullptr); + + [[nodiscard]] int id() const { return m_id; } + void setId(int id) + { + if (m_id != id) + { + m_id = id; + emit idChanged(); + } + } + + [[nodiscard]] QString single() const { return m_single; } + void setSingle(QString single) + { + if (m_single != single) + { + m_single = single; + emit singleChanged(); + } + } + + [[nodiscard]] QString multiOne() const { return m_multiOne; } + void setMultiOne(QString multiOne) + { + if (m_multiOne != multiOne) + { + m_multiOne = multiOne; + emit multiOneChanged(); + } + } + + [[nodiscard]] QString multiTwo() const { return m_multiTwo; } + void setMultiTwo(QString multiTwo) + { + if (m_multiTwo != multiTwo) + { + m_multiTwo = multiTwo; + emit multiTwoChanged(); + } + } +signals: + void idChanged(); + void singleChanged(); + void multiOneChanged(); + void multiTwoChanged(); + +private: + int m_id{0}; + QString m_single; + QString m_multiOne; + QString m_multiTwo; +}; diff --git a/tests/auto/qormsession/qormsession.pro b/tests/auto/qormsession/qormsession.pro index e414b8b..0d98ae8 100644 --- a/tests/auto/qormsession/qormsession.pro +++ b/tests/auto/qormsession/qormsession.pro @@ -8,10 +8,16 @@ SOURCES += tst_ormsession.cpp \ domain/province.cpp \ domain/town.cpp \ domain/person.cpp \ + domain/withnotnull.cpp \ + domain/withforeignkey.cpp \ + domain/withunique.cpp \ HEADERS += \ domain/province.h \ domain/town.h \ domain/person.h \ + domain/withnotnull.h \ + domain/withforeignkey.h \ + domain/withunique.h \ RESOURCES += ormsession.qrc diff --git a/tests/auto/qormsession/qormsession.qbs b/tests/auto/qormsession/qormsession.qbs index d61bbad..dd524c5 100644 --- a/tests/auto/qormsession/qormsession.qbs +++ b/tests/auto/qormsession/qormsession.qbs @@ -10,6 +10,9 @@ QtApplication { "domain/person.cpp", "domain/person.h", "domain/province.cpp", "domain/province.h", "domain/town.cpp", "domain/town.h", + "domain/withnotnull.cpp", "domain/withnotnull.h", + "domain/withforeignkey.cpp", "domain/withforeignkey.h", + "domain/withunique.cpp", "domain/withunique.h", "tst_ormsession.cpp", "ormsession.qrc"] } diff --git a/tests/auto/qormsession/qtorm_update_schema.json b/tests/auto/qormsession/qtorm_update_schema.json index f7f99d3..3ef72a1 100644 --- a/tests/auto/qormsession/qtorm_update_schema.json +++ b/tests/auto/qormsession/qtorm_update_schema.json @@ -4,6 +4,7 @@ "sqlite": { "databaseName": "testdb.db", "schemaMode": "update", + "foreignKeysEnabled": true, "verbose": true } } diff --git a/tests/auto/qormsession/tst_ormsession.cpp b/tests/auto/qormsession/tst_ormsession.cpp index f626a72..b84a355 100644 --- a/tests/auto/qormsession/tst_ormsession.cpp +++ b/tests/auto/qormsession/tst_ormsession.cpp @@ -31,10 +31,14 @@ #include #include #include +#include #include "domain/person.h" #include "domain/province.h" #include "domain/town.h" +#include "domain/withnotnull.h" +#include "domain/withforeignkey.h" +#include "domain/withunique.h" #include "private/qormglobal_p.h" @@ -82,6 +86,12 @@ private slots: void testSchemaAppendCreatesTablesAndAddsColumns(); void testSchemaUpdateCreatesTablesAndAddsColumns(); void testSchemaUpdateRemovesColumns(); + void testSchemaUpdateUpdatesNotNull(); + void testSchemaUpdateUpdatesForeignKey(); + void testSchemaUpdateUpdatesUnique(); + void testSchemaUpdateUpdatesUniqueMulti(); + + void testForeignKeysEnabled(); }; SqliteSessionTest::SqliteSessionTest() @@ -99,7 +109,7 @@ void SqliteSessionTest::init() if (db.exists()) QVERIFY(db.remove()); - qRegisterOrmEntity(); + qRegisterOrmEntity(); } void SqliteSessionTest::cleanup() @@ -976,6 +986,208 @@ void SqliteSessionTest::testSchemaUpdateRemovesColumns() } } +void SqliteSessionTest::testSchemaUpdateUpdatesNotNull() +{ + { + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); + db.setDatabaseName("testdb.db"); + QVERIFY(db.open()); + + static const QStringList statements{ + "CREATE TABLE WithNotNull(id INTEGER PRIMARY KEY AUTOINCREMENT, data INTEGER)", + "INSERT INTO WithNotNull(id, data) VALUES(1, 2)", + "INSERT INTO WithNotNull(id, data) VALUES(2, 3)"}; + + for (const QString& statement : statements) + { + qDebug() << "Executing" << statement; + QSqlQuery query{db}; + QVERIFY(query.exec(statement)); + QCOMPARE(query.lastError().type(), QSqlError::NoError); + } + + db.close(); + QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection); + } + + { + QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_update_schema.json")}; + + auto result = session.from().select(); + QCOMPARE(result.error().type(), QOrm::ErrorType::None); + auto notNullData = result.toVector(); + QCOMPARE(notNullData.size(), 2); + QCOMPARE(notNullData[0]->id(), 1); + QCOMPARE(notNullData[1]->id(), 2); + } + + { + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); + db.setDatabaseName("testdb.db"); + QVERIFY(db.open()); + + QVERIFY(db.tables().contains("WithNotNull")); + QSqlRecord record = db.record("WithNotNull"); + QCOMPARE(record.count(), 2); + QVERIFY(record.contains("id")); + QVERIFY(record.contains("data")); + QCOMPARE(record.field("data").requiredStatus(), QSqlField::Required); + + db.close(); + QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection); + } +} + +void SqliteSessionTest::testSchemaUpdateUpdatesForeignKey() +{ + { + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); + db.setDatabaseName("testdb.db"); + QVERIFY(db.open()); + + static const QStringList statements{ + "CREATE TABLE WithNotNull(id INTEGER PRIMARY KEY AUTOINCREMENT, data INTEGER NOT NULL)", + "INSERT INTO WithNotNull(id, data) VALUES(1, 2)", + "CREATE TABLE WithForeignKey(id INTEGER PRIMARY KEY AUTOINCREMENT, data_id INTEGER)", + "INSERT INTO WithForeignKey(id, data_id) VALUES(1, 1)", + "INSERT INTO WithForeignKey(id, data_id) VALUES(2, 1)"}; + + for (const QString& statement : statements) + { + qDebug() << "Executing" << statement; + QSqlQuery query{db}; + QVERIFY(query.exec(statement)); + QCOMPARE(query.lastError().type(), QSqlError::NoError); + } + + db.close(); + QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection); + } + + { + QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_update_schema.json")}; + + auto result = session.from().select(); + QCOMPARE(result.error().type(), QOrm::ErrorType::None); + auto fkData = result.toVector(); + QCOMPARE(fkData.size(), 2); + QCOMPARE(fkData[0]->id(), 1); + QCOMPARE(fkData[1]->id(), 2); + } + + { + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); + db.setDatabaseName("testdb.db"); + QVERIFY(db.open()); + + QSqlQuery query{db}; + QVERIFY(query.exec(R"(SELECT 1 FROM pragma_foreign_key_list('WithForeignKey') WHERE "from" = 'data_id';)")); + QCOMPARE(query.lastError().type(), QSqlError::NoError); + QVERIFY(query.first()); + + db.close(); + QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection); + } +} + +void SqliteSessionTest::testSchemaUpdateUpdatesUnique() +{ + { + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); + db.setDatabaseName("testdb.db"); + QVERIFY(db.open()); + + static const QStringList statements{ + "CREATE TABLE WithUnique(id INTEGER PRIMARY KEY AUTOINCREMENT, single TEXT, multione TEXT, multitwo TEXT, UNIQUE(multione, multitwo))", + "INSERT INTO WithUnique(id, single, multione, multitwo) VALUES(1, 'test', 'test', 'test')", + "INSERT INTO WithUnique(id, single, multione, multitwo) VALUES(2, 'test2', 'test', 'test2')"}; + + for (const QString& statement : statements) + { + qDebug() << "Executing" << statement; + QSqlQuery query{db}; + QVERIFY(query.exec(statement)); + QCOMPARE(query.lastError().type(), QSqlError::NoError); + } + + db.close(); + QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection); + } + + { + QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_update_schema.json")}; + + auto result = session.from().select(); + QCOMPARE(result.error().type(), QOrm::ErrorType::None); + auto fkData = result.toVector(); + QCOMPARE(fkData.size(), 2); + QCOMPARE(fkData[0]->id(), 1); + QCOMPARE(fkData[1]->id(), 2); + } + + { + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); + db.setDatabaseName("testdb.db"); + QVERIFY(db.open()); + + QSqlQuery query{db}; + QVERIFY(!query.exec("INSERT INTO WithUnique(id, single, multione, multitwo) VALUES(3, 'test', 'test', 'test3')")); + QCOMPARE(query.lastError().type(), QSqlError::ConnectionError); + + db.close(); + QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection); + } +} + +void SqliteSessionTest::testSchemaUpdateUpdatesUniqueMulti() +{ + { + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); + db.setDatabaseName("testdb.db"); + QVERIFY(db.open()); + + static const QStringList statements{ + "CREATE TABLE WithUnique(id INTEGER PRIMARY KEY AUTOINCREMENT, single TEXT UNIQUE, multione TEXT UNIQUE, multitwo TEXT)", + "INSERT INTO WithUnique(id, single, multione, multitwo) VALUES(1, 'test', 'test', 'test')", + "INSERT INTO WithUnique(id, single, multione, multitwo) VALUES(2, 'test2', 'test2', 'test')"}; + + for (const QString& statement : statements) + { + qDebug() << "Executing" << statement; + QSqlQuery query{db}; + QVERIFY(query.exec(statement)); + QCOMPARE(query.lastError().type(), QSqlError::NoError); + } + + db.close(); + QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection); + } + + { + QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_update_schema.json")}; + + auto result = session.from().select(); + QCOMPARE(result.error().type(), QOrm::ErrorType::None); + auto fkData = result.toVector(); + QCOMPARE(fkData.size(), 2); + QCOMPARE(fkData[0]->id(), 1); + QCOMPARE(fkData[1]->id(), 2); + } + + { + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); + db.setDatabaseName("testdb.db"); + QVERIFY(db.open()); + + QSqlQuery query{db}; + QVERIFY(query.exec("INSERT INTO WithUnique(id, single, multione, multitwo) VALUES(3, 'test3', 'test', 'test3')")); + QCOMPARE(query.lastError().type(), QSqlError::NoError); + + db.close(); + QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection); + } +} + void SqliteSessionTest::testRemoveInstance() { QOrmSession session; @@ -1050,6 +1262,24 @@ void SqliteSessionTest::testRemoveWithFilter() } } +void SqliteSessionTest::testForeignKeysEnabled() +{ + QOrmSqliteConfiguration sqliteConfiguration{}; + sqliteConfiguration.setForeignKeysEnabled(true); + sqliteConfiguration.setDatabaseName(":memory:"); + QOrmSqliteProvider* sqliteProvider = new QOrmSqliteProvider{sqliteConfiguration}; + QOrmSessionConfiguration sessionConfiguration{sqliteProvider, true}; + QOrmSession session{sessionConfiguration}; + + QOrmSqliteProvider* provider = + static_cast(session.configuration().provider()); + provider->connectToBackend(); + QSqlQuery query{provider->database()}; + + QVERIFY(query.exec("PRAGMA foreign_keys") && query.next()); + QCOMPARE(query.value("foreign_keys").toBool(), true); +} + QTEST_GUILESS_MAIN(SqliteSessionTest) #include "tst_ormsession.moc" diff --git a/tests/auto/qormsqlitestatementgenerator/domain/community.h b/tests/auto/qormsqlitestatementgenerator/domain/community.h index 0b06bcc..3ad2de6 100644 --- a/tests/auto/qormsqlitestatementgenerator/domain/community.h +++ b/tests/auto/qormsqlitestatementgenerator/domain/community.h @@ -33,12 +33,18 @@ class Community : public QObject Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) Q_PROPERTY(int population READ population WRITE setPopulation NOTIFY populationChanged) Q_PROPERTY(Province* province READ province WRITE setProvince NOTIFY provinceChanged) + Q_PROPERTY(int neverNull READ neverNull WRITE setNeverNull NOTIFY neverNullChanged) + Q_PROPERTY(QString code READ code WRITE setCode NOTIFY codeChanged) Q_PROPERTY(bool hasLargePopulation READ hasLargePopulation STORED false) Q_PROPERTY(bool hasSmallPopulation READ hasSmallPopulation) Q_ORM_CLASS(TABLE communities) + Q_ORM_PROPERTY(name UNIQUE nameProvince) Q_ORM_PROPERTY(communityId COLUMN community_id IDENTITY) Q_ORM_PROPERTY(hasSmallPopulation TRANSIENT) + Q_ORM_PROPERTY(neverNull NOT_NULL) + Q_ORM_PROPERTY(province FOREIGN_KEY UNIQUE nameProvince) + Q_ORM_PROPERTY(code UNIQUE) public: Q_INVOKABLE Community(QObject* parent = nullptr); @@ -83,6 +89,26 @@ class Community : public QObject } } + int neverNull() const { return m_neverNull; } + void setNeverNull(int neverNull) + { + if (m_neverNull != neverNull) + { + m_neverNull = neverNull; + emit neverNullChanged(); + } + } + + QString code() const { return m_code; } + void setCode(QString code) + { + if (m_code != code) + { + m_code = code; + emit codeChanged(); + } + } + bool hasLargePopulation() const { return m_population > 5000; } bool hasSmallPopulation() const { return !hasLargePopulation(); } @@ -91,10 +117,14 @@ class Community : public QObject void nameChanged(); void populationChanged(); void provinceChanged(); + void neverNullChanged(); + void codeChanged(); private: long m_communityId{0}; QString m_name; int m_population{0}; Province* m_province{nullptr}; + int m_neverNull{1}; + QString m_code; }; diff --git a/tests/auto/qormsqlitestatementgenerator/tst_sqlitestatementgenerator.cpp b/tests/auto/qormsqlitestatementgenerator/tst_sqlitestatementgenerator.cpp index 14441b4..90641c3 100644 --- a/tests/auto/qormsqlitestatementgenerator/tst_sqlitestatementgenerator.cpp +++ b/tests/auto/qormsqlitestatementgenerator/tst_sqlitestatementgenerator.cpp @@ -73,6 +73,7 @@ private slots: void testAlterTableAddColumn(); void testAlterTableAddColumnWithReference(); + void testAlterTableAddColumnWithForeignKey(); void testSelectWithLimitOffset(); void testSelectWithNamespace(); @@ -145,6 +146,7 @@ void SqliteStatementGenerator::testInsertForCustomizedEntity() hagenberg->setCommunityId(4232); hagenberg->setName("Hagenberg"); hagenberg->setPopulation(4000); + hagenberg->setNeverNull(5); QVariantMap boundParameters; QString statement = @@ -152,11 +154,12 @@ void SqliteStatementGenerator::testInsertForCustomizedEntity() QCOMPARE( statement, - R"(INSERT INTO "communities"("community_id","name","population","province_id") VALUES(:community_id,:name,:population,:province_id))"); + R"(INSERT INTO "communities"("community_id","name","population","province_id","nevernull","code") VALUES(:community_id,:name,:population,:province_id,:nevernull,:code))"); QCOMPARE(boundParameters[":community_id"], 4232); QCOMPARE(boundParameters[":name"], "Hagenberg"); QCOMPARE(boundParameters[":population"], 4000); QCOMPARE(boundParameters[":province_id"], QVariant::fromValue(nullptr)); + QCOMPARE(boundParameters[":nevernull"], 5); } void SqliteStatementGenerator::testInsertWithNamespace() @@ -298,7 +301,7 @@ void SqliteStatementGenerator::testUpdateWithManyToOne() upperAustria.get(), boundParameters); - QCOMPARE(statement, R"(UPDATE Province SET name = :name WHERE "id" = :id)"); + QCOMPARE(statement, R"(UPDATE "Province" SET name = :name WHERE "id" = :id)"); QCOMPARE(boundParameters[":name"], QString::fromUtf8("Oberösterreich")); QCOMPARE(boundParameters[":id"], 1); } @@ -316,7 +319,7 @@ void SqliteStatementGenerator::testUpdateWithOneToMany() generator.generateUpdateStatement(cache.get(), hagenberg.get(), boundParameters); QCOMPARE(statement, - R"(UPDATE Town SET name = :name,province_id = :province_id WHERE "id" = :id)"); + R"(UPDATE "Town" SET name = :name,province_id = :province_id WHERE "id" = :id)"); QCOMPARE(boundParameters[":name"], QString::fromUtf8("Hagenberg")); QCOMPARE(boundParameters[":province_id"], 1); QCOMPARE(boundParameters[":id"], 2); @@ -334,7 +337,7 @@ void SqliteStatementGenerator::testUpdateWithOneToManyNullReference() generator.generateUpdateStatement(cache.get(), hagenberg.get(), boundParameters); QCOMPARE(statement, - R"(UPDATE Town SET name = :name,province_id = :province_id WHERE "id" = :id)"); + R"(UPDATE "Town" SET name = :name,province_id = :province_id WHERE "id" = :id)"); QCOMPARE(boundParameters[":name"], QString::fromUtf8("Hagenberg")); QCOMPARE(boundParameters[":province_id"], QVariant::fromValue(nullptr)); QCOMPARE(boundParameters[":id"], 2); @@ -403,7 +406,7 @@ void SqliteStatementGenerator::testCreateTableForCustomizedEntity() QOrmMetadataCache cache; QCOMPARE( QOrmSqliteStatementGenerator{}.generateCreateTableStatement(cache.get()), - R"(CREATE TABLE "communities"("community_id" INTEGER PRIMARY KEY,"name" TEXT,"population" INTEGER,"province_id" INTEGER))"); + R"(CREATE TABLE "communities"("community_id" INTEGER PRIMARY KEY,"name" TEXT,"population" INTEGER,"province_id" INTEGER REFERENCES "Province" ( "id" ),"nevernull" INTEGER NOT NULL,"code" TEXT UNIQUE,UNIQUE( "name","province_id" )))"); } void SqliteStatementGenerator::testCreateTableWithQVariant() @@ -437,7 +440,7 @@ void SqliteStatementGenerator::testAlterTableAddColumn() QString actual = QOrmSqliteStatementGenerator{}.generateAlterTableAddColumnStatement( cache.get(), *cache.get().classPropertyMapping("name")); - QCOMPARE(actual, R"(ALTER TABLE "Person" ADD COLUMN "name" TEXT)"); + QCOMPARE(actual, R"(ALTER TABLE "Person" ADD COLUMN "name" TEXT )"); } void SqliteStatementGenerator::testAlterTableAddColumnWithReference() @@ -446,7 +449,16 @@ void SqliteStatementGenerator::testAlterTableAddColumnWithReference() QString actual = QOrmSqliteStatementGenerator{}.generateAlterTableAddColumnStatement( cache.get(), *cache.get().classPropertyMapping("province")); - QCOMPARE(actual, R"(ALTER TABLE "Town" ADD COLUMN "province_id" INTEGER)"); + QCOMPARE(actual, R"(ALTER TABLE "Town" ADD COLUMN "province_id" INTEGER )"); +} + +void SqliteStatementGenerator::testAlterTableAddColumnWithForeignKey() +{ + QOrmMetadataCache cache; + QString actual = QOrmSqliteStatementGenerator{}.generateAlterTableAddColumnStatement( + cache.get(), *cache.get().classPropertyMapping("province")); + + QCOMPARE(actual, R"(ALTER TABLE "communities" ADD COLUMN "province_id" INTEGER REFERENCES "Province" ( "id" ))"); } void SqliteStatementGenerator::testSelectWithLimitOffset()