schema modifying columns 🔥🚀🥳

Added the change() method which allows to modify the type and attributes
of existing columns.

 - added renameTo() for MySQL which can be used with the change()
 - added tests
 - updated docs
This commit is contained in:
silverqx
2023-03-14 16:29:07 +01:00
parent a8552f20c6
commit 8097037805
13 changed files with 663 additions and 66 deletions
+19 -1
View File
@@ -1058,7 +1058,25 @@ When using the MySQL database, the `after` method may be used to add columns aft
### Modifying Columns
Not implemented.
The `change` method allows you to modify the type and attributes of existing columns. For example, you may wish to increase the size of a `string` column. To see the `change` method in action, let's increase the size of the `name` column from 25 to 50. To accomplish this, we simply define the new state of the column and then call the `change` method:
#include <orm/schema.hpp>
Schema::table("users", [](Blueprint &table)
{
table.string("name", 50).change();
});
When modifying a column, you must explicitly include all of the modifiers you want to keep on the column definition - any missing attribute will be dropped. For example, to retain the `unsigned`, `default`, and `comment` attributes, you must call each modifier explicitly when changing the column:
Schema::table("users", [](Blueprint &table)
{
table.integer("votes").isUnsigned().defaultValue(1).comment("my comment").change();
});
:::info
The `change` method and modifying columns is not implemented for the `SQLite` database because it doesn't support modifying columns out of the box.
:::
#### Renaming Columns
+10 -6
View File
@@ -117,6 +117,8 @@ namespace Orm::SchemaNs
QString column;
/*! Column comment value. */
QString comment;
/*! Indicates whether a column will be changed or created. */
bool change = false;
};
/*! Table auto-incrementing column starting value command (MySQL/PostgreSQL). */
@@ -201,18 +203,20 @@ namespace Orm::SchemaNs
QVariant onUpdate {};
/*! Create a SQL compliant identity column (PostgreSQL). */
QString generatedAs {};
/*! Create a stored generated column (MySQL/PostgreSQL/SQLite). */
QString storedAs {};
/*! Create a virtual generated column (MySQL/PostgreSQL/SQLite). */
QString virtualAs {};
/*! Rename a column, used with the change() method (MySQL). */
QString renameTo {};
/*! Set the starting value of an auto-incrementing field (MySQL/PostgreSQL),
alias for the 'startingValue'. */
std::optional<quint64> from = std::nullopt;
/*! Allow NULL values to be inserted into the column. */
std::optional<bool> nullable = std::nullopt; // Has to be optional because of virtualAs() and storedAs(), look at MySqlSchemaGrammar::modifyNullable()
/*! Set the starting value of an auto-incrementing field (MySQL/PostgreSQL). */
std::optional<quint64> startingValue = std::nullopt;
/*! Allow NULL values to be inserted into the column. */
std::optional<bool> nullable = std::nullopt; // Has to be optional because of virtualAs() and storedAs(), look at MySQL::modifyNullable()
/*! Create a stored generated column (MySQL/PostgreSQL/SQLite). */
std::optional<QString> storedAs = std::nullopt; // Has to be optional because of modifyStoredAs(), look at PostgresSchemaGrammar and tst_PostgreSQL_SchemaBuilder::drop_StoredAs() (to support "drop expression if exists")
/*! Create a virtual generated column (MySQL/PostgreSQL/SQLite). */
std::optional<QString> virtualAs = std::nullopt; // Has to be optional because of modifyVirtualAs(), look at PostgresSchemaGrammar (to support "drop expression if exists")
// Place boolean data members at the end to avoid excessive padding
/*! Used as a modifier for generatedAs() (PostgreSQL). */
@@ -56,6 +56,8 @@ namespace Orm::SchemaNs
ColumnReferenceType &always();
/*! Set INTEGER column as auto-increment (primary key). */
ColumnReferenceType &autoIncrement();
/*! Change the column. */
ColumnReferenceType &change();
/*! Specify a character set for the column (MySQL). */
ColumnReferenceType &charset(const QString &charset);
/*! Specify a collation for the column (MySQL/PostgreSQL/SQL Server). */
@@ -83,18 +85,24 @@ namespace Orm::SchemaNs
/*! The spatial reference identifier (SRID) of a geometry identifies the SRS
in which the geometry is defined (MySQL/PostgreSQL), alias for the 'srid'. */
ColumnReferenceType &projection(quint32 value);
/*! Rename a column, use with the change() method (MySQL). */
ColumnReferenceType &renameTo(QString columnName);
/*! 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). */
/*! Create a stored generated column (MySQL/PostgreSQL/SQLite). With PostgreSQL
use an empty or null QString() to drop a generated column along with
the change() method call. */
ColumnReferenceType &storedAs(QString expression);
/*! Set the TIMESTAMP column to use CURRENT_TIMESTAMP as default value. */
ColumnReferenceType &useCurrent();
/*! Set the TIMESTAMP column to use CURRENT_TIMESTAMP when updating (MySQL). */
ColumnReferenceType &useCurrentOnUpdate();
/*! Create a virtual generated column (MySQL/PostgreSQL/SQLite). */
/*! Create a virtual generated column (MySQL/PostgreSQL/SQLite). With PostgreSQL
use an empty or null QString() to drop a generated column along with
the change() method call. */
ColumnReferenceType &virtualAs(QString expression);
/*! Add an index. */
@@ -167,6 +175,15 @@ namespace Orm::SchemaNs
return columnReference();
}
template<ColumnReferenceReturn R>
typename ColumnDefinitionReference<R>::ColumnReferenceType &
ColumnDefinitionReference<R>::change()
{
m_columnDefinition.get().change = true;
return columnReference();
}
template<ColumnReferenceReturn R>
typename ColumnDefinitionReference<R>::ColumnReferenceType &
ColumnDefinitionReference<R>::charset(const QString &charset)
@@ -275,6 +292,15 @@ namespace Orm::SchemaNs
return columnReference();
}
template<ColumnReferenceReturn R>
typename ColumnDefinitionReference<R>::ColumnReferenceType &
ColumnDefinitionReference<R>::renameTo(QString columnName)
{
m_columnDefinition.get().renameTo = std::move(columnName);
return columnReference();
}
template<ColumnReferenceReturn R>
typename ColumnDefinitionReference<R>::ColumnReferenceType &
ColumnDefinitionReference<R>::startingValue(const int startingValue)
@@ -73,6 +73,10 @@ namespace Grammars
/*! Compile an add column command. */
QVector<QString> compileAdd(const Blueprint &blueprint,
const BasicCommand &command) const;
/*! Compile a change column command. */
QVector<QString> compileChange(const Blueprint &blueprint,
const BasicCommand &command) const;
/*! Compile a drop column command. */
QVector<QString> compileDropColumn(const Blueprint &blueprint,
const DropColumnsCommand &command) const;
@@ -81,6 +81,9 @@ namespace Grammars
/*! Compile an add column command. */
QVector<QString> compileAdd(const Blueprint &blueprint,
const BasicCommand &command) const;
/*! Compile a change column command. */
QVector<QString> compileChange(const Blueprint &blueprint,
const BasicCommand &command) const;
/*! Compile a drop column command. */
QVector<QString> compileDropColumn(const Blueprint &blueprint,
const DropColumnsCommand &command) const;
@@ -157,6 +160,8 @@ namespace Grammars
/*! Add the column modifiers to the definition. */
QString addModifiers(QString &&sql,
const ColumnDefinition &column) const override;
/*! Add the column modifiers to the definition for "alter column" (change()). */
QVector<QString> getModifiersForChange(const ColumnDefinition &column) const;
/*! Compile the auto-incrementing column starting value. */
QVector<QString>
@@ -259,19 +264,36 @@ namespace Grammars
QString typeMultiPolygonZ(const ColumnDefinition &column) const;
/*! Get the SQL for a collation column modifier. */
QString modifyCollate(const ColumnDefinition &column) const;
QVector<QString> modifyCollate(const ColumnDefinition &column) const;
/*! Get the SQL for an auto-increment column modifier. */
QString modifyIncrement(const ColumnDefinition &column) const;
QVector<QString> modifyIncrement(const ColumnDefinition &column) const;
/*! Get the SQL for a nullable column modifier. */
QString modifyNullable(const ColumnDefinition &column) const;
QVector<QString> modifyNullable(const ColumnDefinition &column) const;
/*! Get the SQL for a default column modifier. */
QString modifyDefault(const ColumnDefinition &column) const;
QVector<QString> modifyDefault(const ColumnDefinition &column) const;
/*! Get the SQL for a generated virtual column modifier. */
QString modifyVirtualAs(const ColumnDefinition &column) const;
QVector<QString> modifyVirtualAs(const ColumnDefinition &column) const;
/*! Get the SQL for a generated stored column modifier. */
QString modifyStoredAs(const ColumnDefinition &column) const;
QVector<QString> modifyStoredAs(const ColumnDefinition &column) const;
/*! Get the SQL for an identity column modifier. */
QString modifyGeneratedAs(const ColumnDefinition &column) const;
QVector<QString> modifyGeneratedAs(const ColumnDefinition &column) const;
private:
/*! Throw if modifying a generated column using the change() method. */
static void throwIfModifyingGeneratedColumn();
/*! Modifier methods array for the "alter column" (change()). */
constexpr static std::array m_modifierMethodsForChange {
&PostgresSchemaGrammar::modifyIncrement,
&PostgresSchemaGrammar::modifyNullable,
&PostgresSchemaGrammar::modifyDefault,
&PostgresSchemaGrammar::modifyVirtualAs,
&PostgresSchemaGrammar::modifyStoredAs,
&PostgresSchemaGrammar::modifyGeneratedAs,
};
/*! Size of the modifier methods array. */
constexpr static auto
m_modifierMethodsForChangeSize = m_modifierMethodsForChange.size();
};
/* public */
@@ -67,6 +67,10 @@ namespace Grammars
virtual QString compileColumnListing(const QString &table = "") const = 0;
/* Compile methods for commands */
/*! Compile a change column command. */
QVector<QString> compileChange(const Blueprint &blueprint,
const BasicCommand &command) const;
/*! Compile a drop table command. */
QVector<QString> compileDrop(const Blueprint &blueprint,
const BasicCommand &command) const;
+3 -3
View File
@@ -582,8 +582,8 @@ void Blueprint::addImpliedCommands(const SchemaGrammar &grammar)
if (!getAddedColumns().isEmpty() && !creating())
m_commands.emplace_front(createCommand<BasicCommand>({{}, Add}));
// if (!getChangedColumns().isEmpty() && !creating())
// m_commands.prepend(createCommand(Change));
if (!getChangedColumns().isEmpty() && !creating())
m_commands.emplace_front(createCommand<BasicCommand>({{}, Change}));
addFluentIndexes();
@@ -676,7 +676,7 @@ void Blueprint::addFluentCommands(const SchemaGrammar &grammar)
// Comment command (PostgreSQL)
else if (commandName == Comment && std::invoke(shouldAdd, column)) T_UNLIKELY
addCommand<CommentCommand>(
{{}, commandName, column.name, column.comment});
{{}, commandName, column.name, column.comment, column.change});
}
IndexDefinitionReference
+34 -5
View File
@@ -110,6 +110,33 @@ QVector<QString> MySqlSchemaGrammar::compileAdd(const Blueprint &blueprint,
prefixArray(Add, getColumns(blueprint))))};
}
QVector<QString> MySqlSchemaGrammar::compileChange(const Blueprint &blueprint,
const BasicCommand &/*unused*/) const
{
auto changedColumns = blueprint.getChangedColumns();
QVector<QString> columns;
columns.reserve(changedColumns.size());
for (auto &column : changedColumns) {
const auto isRenaming = !column.renameTo.isEmpty();
columns << addModifiers(
QStringLiteral("%1 %2%3 %4")
.arg(isRenaming ? QStringLiteral("change")
: QStringLiteral("modify"),
wrap(column),
isRenaming ? QStringLiteral(" %1")
.arg(BaseGrammar::wrap(column.renameTo))
: "",
getType(column)),
column);
}
return {QStringLiteral("alter table %1 %2").arg(wrapTable(blueprint),
columnizeWithoutWrap(columns))};
}
QVector<QString>
MySqlSchemaGrammar::compileDropColumn(const Blueprint &blueprint,
const DropColumnsCommand &command) const
@@ -254,6 +281,7 @@ MySqlSchemaGrammar::invokeCompileMethod(const CommandDefinition &command,
QString(command.name) -> enum. */
static const std::unordered_map<QString, CompileMemFn> cached {
{Add, bind(&MySqlSchemaGrammar::compileAdd)},
{Change, bind(&MySqlSchemaGrammar::compileChange)},
{Rename, bind(&MySqlSchemaGrammar::compileRename)},
{Drop, bind(&MySqlSchemaGrammar::compileDrop)},
{DropIfExists, bind(&MySqlSchemaGrammar::compileDropIfExists)},
@@ -839,18 +867,18 @@ QString MySqlSchemaGrammar::modifyCollate(const ColumnDefinition &column) const
QString MySqlSchemaGrammar::modifyVirtualAs(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
{
if (column.virtualAs.isEmpty())
if (!column.virtualAs || column.virtualAs->isEmpty())
return {};
return QStringLiteral(" generated always as (%1)").arg(column.virtualAs);
return QStringLiteral(" generated always as (%1)").arg(*column.virtualAs);
}
QString MySqlSchemaGrammar::modifyStoredAs(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
{
if (column.storedAs.isEmpty())
if (!column.storedAs || column.storedAs->isEmpty())
return {};
return QStringLiteral(" generated always as (%1) stored").arg(column.storedAs);
return QStringLiteral(" generated always as (%1) stored").arg(*column.storedAs);
}
QString MySqlSchemaGrammar::modifyNullable(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
@@ -860,7 +888,8 @@ QString MySqlSchemaGrammar::modifyNullable(const ColumnDefinition &column) const
storedAs), it accepts both, null and also not null for generated columns, I have
tried it. */
if (!m_isMaria ||
(column.virtualAs.isEmpty() && column.storedAs.isEmpty())
((!column.virtualAs || column.virtualAs->isEmpty()) &&
(!column.storedAs || column.storedAs->isEmpty()))
)
return column.nullable && *column.nullable ? QStringLiteral(" null")
: QStringLiteral(" not null");
+154 -37
View File
@@ -3,6 +3,8 @@
#include <unordered_set>
#include "orm/databaseconnection.hpp"
#include "orm/exceptions/logicerror.hpp"
#include "orm/macros/likely.hpp"
#include "orm/utils/type.hpp"
TINYORM_BEGIN_COMMON_NAMESPACE
@@ -118,6 +120,38 @@ PostgresSchemaGrammar::compileAdd(const Blueprint &blueprint,
getColumns(blueprint))))};
}
QVector<QString>
PostgresSchemaGrammar::compileChange(const Blueprint &blueprint,
const BasicCommand &/*unused*/) const
{
auto changedColumns = blueprint.getChangedColumns();
QVector<QString> columns;
columns.reserve(changedColumns.size());
for (auto &column : changedColumns) {
QVector<QString> changes;
changes.reserve(m_modifierMethodsForChangeSize + 1);
const auto collate = modifyCollate(column);
// The column type with the collate has to be defined at once
changes << QStringLiteral("type %1%2")
.arg(getType(column),
collate.isEmpty() ? EMPTY : collate.constFirst());
// All other modifiers have to be alone so the "alter column" can be prepended
changes << getModifiersForChange(column);
columns << columnizeWithoutWrap(
prefixArray(QStringLiteral("alter column %1").arg(wrap(column)),
changes));
}
return {QStringLiteral("alter table %1 %2").arg(wrapTable(blueprint),
columnizeWithoutWrap(columns))};
}
QVector<QString>
PostgresSchemaGrammar::compileDropColumn(const Blueprint &blueprint,
const DropColumnsCommand &command) const
@@ -258,12 +292,16 @@ QVector<QString>
PostgresSchemaGrammar::compileComment(const Blueprint &blueprint,
const CommentCommand &command) const
{
const auto isCommentEmpty = command.comment.isEmpty();
if (isCommentEmpty && !command.change)
return {};
return {QStringLiteral("comment on column %1.%2 is %3")
.arg(wrapTable(blueprint), BaseGrammar::wrap(command.column),
command.column.isEmpty()
// Remove a column comment
? null_
: quoteString(escapeString(command.comment)))};
// Remove a column comment (used during change())
isCommentEmpty ? null_
: quoteString(escapeString(command.comment)))};
}
QVector<QString>
@@ -317,6 +355,7 @@ PostgresSchemaGrammar::invokeCompileMethod(const CommandDefinition &command,
QString(command.name) -> enum. */
static const std::unordered_map<QString, CompileMemFn> cached {
{Add, bind(&PostgresSchemaGrammar::compileAdd)},
{Change, bind(&PostgresSchemaGrammar::compileChange)},
{Rename, bind(&PostgresSchemaGrammar::compileRename)},
{Drop, bind(&PostgresSchemaGrammar::compileDrop)},
{DropIfExists, bind(&PostgresSchemaGrammar::compileDropIfExists)},
@@ -384,11 +423,27 @@ QString PostgresSchemaGrammar::addModifiers(QString &&sql,
};
for (const auto method : modifierMethods)
sql += std::invoke(method, this, column);
/* Postgres is different here, it returns a vector as it needs to return
2 modifiers from the modifyGeneratedAs(). */
sql += ContainerUtils::join(
std::invoke(method, this, column), EMPTY);
return std::move(sql);
}
QVector<QString>
PostgresSchemaGrammar::getModifiersForChange(const ColumnDefinition &column) const
{
QVector<QString> modifiers;
modifiers.reserve(static_cast<QVector<QString>::size_type>(
m_modifierMethodsForChangeSize));
for (const auto method : m_modifierMethodsForChange)
modifiers << std::invoke(method, this, column);
return modifiers;
}
QVector<QString>
PostgresSchemaGrammar::compileAutoIncrementStartingValue( // NOLINT(readability-convert-member-functions-to-static)
const Blueprint &blueprint,
@@ -804,81 +859,143 @@ QString PostgresSchemaGrammar::typeMultiPolygonZ(const ColumnDefinition &column)
return formatPostGisType(QStringLiteral("multipolygonz"), column);
}
QString PostgresSchemaGrammar::modifyCollate(const ColumnDefinition &column) const
QVector<QString>
PostgresSchemaGrammar::modifyCollate(const ColumnDefinition &column) const
{
if (column.collation.isEmpty())
return {};
return QStringLiteral(" collate %1").arg(wrapValue(column.collation));
return {QStringLiteral(" collate %1").arg(wrapValue(column.collation))};
}
QString PostgresSchemaGrammar::modifyIncrement(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
QVector<QString>
PostgresSchemaGrammar::modifyIncrement(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
{
static const std::unordered_set serials {
ColumnType::BigInteger, ColumnType::Integer, ColumnType::MediumInteger,
ColumnType::SmallInteger, ColumnType::TinyInteger,
};
if ((serials.contains(column.type) || !column.generatedAs.isNull()) &&
// I'm not going to invert this condition for the early return 🤯
if (!column.change &&
(serials.contains(column.type) || !column.generatedAs.isNull()) && // Can't be generatedAs.isEmpty()!
column.autoIncrement
)
return QStringLiteral(" primary key");
return {QStringLiteral(" primary key")};
return {};
}
QString PostgresSchemaGrammar::modifyNullable(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
QVector<QString>
PostgresSchemaGrammar::modifyNullable(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
{
if (column.change)
return {column.nullable && *column.nullable ? QStringLiteral("drop not null")
: QStringLiteral("set not null")};
/* PostgreSQL doesn't need any special logic for generated columns (virtualAs and
storedAs), it accepts both, null and also not null for generated columns, I have
tried it. */
return column.nullable && *column.nullable ? QStringLiteral(" null")
: QStringLiteral(" not null");
return {column.nullable && *column.nullable ? QStringLiteral(" null")
: QStringLiteral(" not null")};
}
QString PostgresSchemaGrammar::modifyDefault(const ColumnDefinition &column) const
QVector<QString>
PostgresSchemaGrammar::modifyDefault(const ColumnDefinition &column) const
{
const auto &defaultValue = column.defaultValue;
if (!defaultValue.isValid() || defaultValue.isNull())
return {};
const auto isNotValidOrNull = !defaultValue.isValid() || defaultValue.isNull();
// Default value is already quoted and escaped inside the getDefaultValue()
return QStringLiteral(" default %1").arg(getDefaultValue(defaultValue));
if (column.change)
return {isNotValidOrNull ? QStringLiteral("drop default")
: QStringLiteral("set default %1")
.arg(getDefaultValue(defaultValue))};
if (isNotValidOrNull)
return {};
return {QStringLiteral(" default %1").arg(getDefaultValue(defaultValue))};
}
QString PostgresSchemaGrammar::modifyVirtualAs(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
QVector<QString>
PostgresSchemaGrammar::modifyVirtualAs(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
{
/* Currently, PostgreSQL 15 doesn't support virtual generated columns, only stored,
so this is useless. */
if (column.virtualAs.isEmpty())
so this method is useless. */
if (!column.virtualAs)
return {};
return QStringLiteral(" generated always as (%1)").arg(column.virtualAs);
if (column.change) {
if (column.virtualAs->isEmpty()) T_LIKELY
return {QStringLiteral("drop expression if exists")};
else T_UNLIKELY
throwIfModifyingGeneratedColumn();
}
return {QStringLiteral(" generated always as (%1)").arg(*column.virtualAs)};
}
QString PostgresSchemaGrammar::modifyStoredAs(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
QVector<QString>
PostgresSchemaGrammar::modifyStoredAs(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
{
if (column.storedAs.isEmpty())
if (!column.storedAs)
return {};
return QStringLiteral(" generated always as (%1) stored").arg(column.storedAs);
if (column.change) {
if (column.storedAs->isEmpty()) T_LIKELY
return {QStringLiteral("drop expression if exists")};
else T_UNLIKELY
throwIfModifyingGeneratedColumn();
}
return {QStringLiteral(" generated always as (%1) stored").arg(*column.storedAs)};
}
QString PostgresSchemaGrammar::modifyGeneratedAs(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
QVector<QString>
PostgresSchemaGrammar::modifyGeneratedAs(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
{
// Nothing to do, generatedAs was not defined
if (column.generatedAs.isNull())
return {};
QString sql;
sql.reserve(100);
return QStringLiteral(" generated %1 as identity%2")
.arg(
// ALWAYS and BY DEFAULT clause
column.always ? QStringLiteral("always") : QStringLiteral("by default"),
// Sequence options clause
!column.generatedAs.isEmpty() ? QStringLiteral(" (%1)")
.arg(column.generatedAs)
: QString(""));
/* generatedAs.isNull() mean it was not defined at all, and isEmpty() it was called
like generatedAs(). */
if (!column.generatedAs.isNull())
sql += QStringLiteral(" generated %1 as identity%2")
.arg(
// ALWAYS and BY DEFAULT clause
column.always ? QStringLiteral("always")
: QStringLiteral("by default"),
// Sequence options clause
!column.generatedAs.isEmpty() ? QStringLiteral(" (%1)")
.arg(column.generatedAs)
: QString(""));
if (column.change) {
QVector<QString> changes;
changes.reserve(2);
changes << QStringLiteral("drop identity if exists");
if (!sql.isEmpty())
changes << std::move(sql.prepend(Add));
return changes;
}
return {sql};
}
/* private */
void PostgresSchemaGrammar::throwIfModifyingGeneratedColumn()
{
throw Exceptions::LogicError(
"The PostgreSQL database does not support modifying generated columns.");
}
} // namespace Orm::SchemaNs::Grammars
@@ -55,6 +55,13 @@ QString SchemaGrammar::compileTableExists() const
throw Exceptions::RuntimeError(NotImplemented);
}
QVector<QString> SchemaGrammar::compileChange(const Blueprint &/*unused*/,
const BasicCommand &/*unused*/) const
{
throw Exceptions::LogicError(
"This database driver does not support changing columns.");
}
/* Compile methods for commands */
QVector<QString>
@@ -253,6 +253,7 @@ SQLiteSchemaGrammar::invokeCompileMethod(const CommandDefinition &command,
QString(command.name) -> enum. */
static const std::unordered_map<QString, CompileMemFn> cached {
{Add, bind(&SQLiteSchemaGrammar::compileAdd)},
{Change, bind(&SQLiteSchemaGrammar::compileChange)},
{Rename, bind(&SQLiteSchemaGrammar::compileRename)},
{Drop, bind(&SQLiteSchemaGrammar::compileDrop)},
{DropIfExists, bind(&SQLiteSchemaGrammar::compileDropIfExists)},
@@ -762,19 +763,19 @@ QString SQLiteSchemaGrammar::typeComputed(const ColumnDefinition &/*unused*/) co
QString SQLiteSchemaGrammar::modifyVirtualAs(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
{
// FEATURE schema json silverqx
if (column.virtualAs.isEmpty())
if (!column.virtualAs || column.virtualAs->isEmpty())
return {};
return QStringLiteral(" generated always as (%1)").arg(column.virtualAs);
return QStringLiteral(" generated always as (%1)").arg(*column.virtualAs);
}
QString SQLiteSchemaGrammar::modifyStoredAs(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
{
// FEATURE schema json silverqx
if (column.storedAs.isEmpty())
if (!column.storedAs || column.storedAs->isEmpty())
return {};
return QStringLiteral(" generated always as (%1) stored").arg(column.storedAs);
return QStringLiteral(" generated always as (%1) stored").arg(*column.storedAs);
}
QString SQLiteSchemaGrammar::modifyNullable(const ColumnDefinition &column) const // NOLINT(readability-convert-member-functions-to-static)
@@ -793,8 +794,13 @@ QString SQLiteSchemaGrammar::modifyDefault(const ColumnDefinition &column) const
{
const auto &defaultValue = column.defaultValue;
/* From SQLite docs:
Generated columns may not have a default value (they may not use the "DEFAULT"
clause). The value of a generated column is always the value specified by
the expression that follows the "AS" keyword. */
if (!defaultValue.isValid() || defaultValue.isNull() ||
!column.virtualAs.isEmpty() || !column.storedAs.isEmpty()
(column.virtualAs && !column.virtualAs->isEmpty()) ||
(column.storedAs && !column.storedAs->isEmpty())
)
return {};
@@ -85,6 +85,8 @@ private Q_SLOTS:
void modifier_defaultValue_WithBoolean() const;
void modifier_defaultValue_Escaping() const;
void change_modifiers() const;
void useCurrent() const;
void useCurrentOnUpdate() const;
@@ -1056,6 +1058,88 @@ and tab end)");
QVERIFY(firstLog.boundValues.isEmpty());
}
void tst_MySql_SchemaBuilder::change_modifiers() const
{
auto log = DB::connection(m_connection).pretend([](auto &connection)
{
Schema::on(connection.getName())
.table(Firewalls, [](Blueprint &table)
{
table.bigInteger(ID).autoIncrement().isUnsigned().startingValue(5).change();
table.bigInteger("big_int").isUnsigned().change();
table.bigInteger("big_int1").change();
table.string(NAME).defaultValue("guest").change();
table.string("name1").nullable().change();
table.string("name2").comment("name2 note").change();
table.string("name3", 191).change();
table.string("name4").invisible().change();
table.string("name5").charset(UTF8).change();
table.string("name6").collation("utf8mb4_unicode_ci").change();
table.string("name7").charset(UTF8).collation(UTF8Unicodeci).change();
table.string("name8_old", 64).renameTo("name8").change();
table.Double("amount", 6, 2).change();
table.multiPolygon("positions").srid(1234).storedAs("expression").change();
table.multiPoint("positions1").srid(1234).virtualAs("expression").nullable()
.change();
table.timestamp("added_on").nullable(false).useCurrent().change();
table.timestamp("updated_at", 4).useCurrent().useCurrentOnUpdate().change();
});
/* 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.string(NAME).after("big_int").change();
table.integerIncrements(ID).from(15).first().change();
});
});
QCOMPARE(log.size(), 4);
const auto &log0 = log.at(0);
QCOMPARE(log0.query,
"alter table `firewalls` "
"modify `id` bigint unsigned not null auto_increment primary key, "
"modify `big_int` bigint unsigned not null, "
"modify `big_int1` bigint not null, "
"modify `name` varchar(255) not null default 'guest', "
"modify `name1` varchar(255) null, "
"modify `name2` varchar(255) not null comment 'name2 note', "
"modify `name3` varchar(191) not null, "
"modify `name4` varchar(255) not null invisible, "
"modify `name5` varchar(255) character set 'utf8' not null, "
"modify `name6` varchar(255) collate 'utf8mb4_unicode_ci' not null, "
"modify `name7` varchar(255) character set 'utf8' collate 'utf8_unicode_ci' "
"not null, "
"change `name8_old` `name8` varchar(64) not null, "
"modify `amount` double(6, 2) not null, "
"modify `positions` multipolygon generated always as (expression) stored "
"not null srid 1234, "
"modify `positions1` multipoint generated always as (expression) "
"null srid 1234, "
"modify `added_on` timestamp not null default current_timestamp, "
"modify `updated_at` timestamp(4) not null default current_timestamp(4) "
"on update current_timestamp(4)");
QVERIFY(log0.boundValues.isEmpty());
const auto &log1 = log.at(1);
QCOMPARE(log1.query,
"alter table `firewalls` auto_increment = 5");
QVERIFY(log1.boundValues.isEmpty());
const auto &log2 = log.at(2);
QCOMPARE(log2.query,
"alter table `firewalls` "
"modify `name` varchar(255) not null after `big_int`, "
"modify `id` int unsigned not null auto_increment primary key first");
QVERIFY(log2.boundValues.isEmpty());
const auto &log3 = log.at(3);
QCOMPARE(log3.query,
"alter table `firewalls` auto_increment = 15");
QVERIFY(log3.boundValues.isEmpty());
}
void tst_MySql_SchemaBuilder::useCurrent() const
{
auto log = DB::connection(m_connection).pretend([](auto &connection)
@@ -91,6 +91,8 @@ private Q_SLOTS:
void modifier_defaultValue_WithBoolean() const;
void modifier_defaultValue_Escaping() const;
void change_modifiers() const;
void useCurrent() const;
void useCurrentOnUpdate() const;
@@ -110,6 +112,10 @@ private Q_SLOTS:
void virtualAs_StoredAs_ModifyTable() const;
void virtualAs_StoredAs_Nullable_ModifyTable() const;
void change_VirtualAs_ThrowException() const;
void change_StoredAs_ThrowException() const;
void drop_StoredAs() const;
void indexes_Fluent() const;
void indexes_Blueprint() const;
@@ -1255,6 +1261,212 @@ and tab end)");
QVERIFY(firstLog.boundValues.isEmpty());
}
void tst_PostgreSQL_SchemaBuilder::change_modifiers() const
{
auto log = DB::connection(m_connection).pretend([](auto &connection)
{
Schema::on(connection.getName())
.table(Firewalls, [](Blueprint &table)
{
table.bigInteger(ID).autoIncrement().startingValue(5).change();
// PostgreSQL doesn't support signed modifier or signed numbers
table.bigInteger("big_int").isUnsigned().change();
table.bigInteger("big_int1").change();
table.string(NAME).defaultValue("guest").change();
table.string("name1").nullable().change();
table.string("name2").comment("name2 note").change();
table.string("name3", 191).change();
// PostgreSQL doesn't support invisible columns
// table.string("name4").invisible().change();
// PostgreSQL doesn't support charset on the column
table.string("name5").charset(UTF8).change();
table.string("name6").collation(UcsBasic).change();
// PostgreSQL doesn't support charset on the column
table.string("name7").charset(UTF8).collation(UcsBasic).change();
// PostgreSQL doesn't support renaming columns during the change() call
// table.string("name8_old", 64).renameTo("name8").change();
table.Double("amount", 6, 2).change();
// PostgreSQL doesn't support changing generated columns
// table.multiPolygon("positions").srid(1234).storedAs("expression").change();
table.point("positions1").isGeometry().projection(1234).change();
table.timestamp("added_on").nullable(false).useCurrent().change();
// PostgreSQL doesn't support useCurrentOnUpdate()
// table.timestamp("updated_at", 4).useCurrent().useCurrentOnUpdate().change();
});
/* 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).change();
});
});
// The following is really wild 🤯
QCOMPARE(log.size(), 18);
const auto &log0 = log.at(0);
QCOMPARE(log0.query,
"alter table \"firewalls\" "
"alter column \"id\" type bigserial, "
"alter column \"id\" set not null, "
"alter column \"id\" drop default, "
"alter column \"id\" drop identity if exists, "
"alter column \"big_int\" type bigint, "
"alter column \"big_int\" set not null, "
"alter column \"big_int\" drop default, "
"alter column \"big_int\" drop identity if exists, "
"alter column \"big_int1\" type bigint, "
"alter column \"big_int1\" set not null, "
"alter column \"big_int1\" drop default, "
"alter column \"big_int1\" drop identity if exists, "
"alter column \"name\" type varchar(255), "
"alter column \"name\" set not null, "
"alter column \"name\" set default 'guest', "
"alter column \"name\" drop identity if exists, "
"alter column \"name1\" type varchar(255), "
"alter column \"name1\" drop not null, "
"alter column \"name1\" drop default, "
"alter column \"name1\" drop identity if exists, "
"alter column \"name2\" type varchar(255), "
"alter column \"name2\" set not null, "
"alter column \"name2\" drop default, "
"alter column \"name2\" drop identity if exists, "
"alter column \"name3\" type varchar(191), "
"alter column \"name3\" set not null, "
"alter column \"name3\" drop default, "
"alter column \"name3\" drop identity if exists, "
"alter column \"name5\" type varchar(255), "
"alter column \"name5\" set not null, "
"alter column \"name5\" drop default, "
"alter column \"name5\" drop identity if exists, "
"alter column \"name6\" type varchar(255) collate \"ucs_basic\", "
"alter column \"name6\" set not null, "
"alter column \"name6\" drop default, "
"alter column \"name6\" drop identity if exists, "
"alter column \"name7\" type varchar(255) collate \"ucs_basic\", "
"alter column \"name7\" set not null, "
"alter column \"name7\" drop default, "
"alter column \"name7\" drop identity if exists, "
"alter column \"amount\" type double precision, "
"alter column \"amount\" set not null, "
"alter column \"amount\" drop default, "
"alter column \"amount\" drop identity if exists, "
"alter column \"positions1\" type geometry(point, 1234), "
"alter column \"positions1\" set not null, "
"alter column \"positions1\" drop default, "
"alter column \"positions1\" drop identity if exists, "
"alter column \"added_on\" type timestamp(0) without time zone, "
"alter column \"added_on\" set not null, "
"alter column \"added_on\" set default current_timestamp, "
"alter column \"added_on\" drop identity if exists");
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"."id" is null)");
QVERIFY(log2.boundValues.isEmpty());
const auto &log3 = log.at(3);
QCOMPARE(log3.query,
R"(comment on column "firewalls"."big_int" is null)");
QVERIFY(log3.boundValues.isEmpty());
const auto &log4 = log.at(4);
QCOMPARE(log4.query,
R"(comment on column "firewalls"."big_int1" is null)");
QVERIFY(log4.boundValues.isEmpty());
const auto &log5 = log.at(5);
QCOMPARE(log5.query,
R"(comment on column "firewalls"."name" is null)");
QVERIFY(log5.boundValues.isEmpty());
const auto &log6 = log.at(6);
QCOMPARE(log6.query,
R"(comment on column "firewalls"."name1" is null)");
QVERIFY(log6.boundValues.isEmpty());
const auto &log7 = log.at(7);
QCOMPARE(log7.query,
R"(comment on column "firewalls"."name2" is 'name2 note')");
QVERIFY(log7.boundValues.isEmpty());
const auto &log8 = log.at(8);
QCOMPARE(log8.query,
R"(comment on column "firewalls"."name3" is null)");
QVERIFY(log8.boundValues.isEmpty());
const auto &log9 = log.at(9);
QCOMPARE(log9.query,
R"(comment on column "firewalls"."name5" is null)");
QVERIFY(log9.boundValues.isEmpty());
const auto &log10 = log.at(10);
QCOMPARE(log10.query,
R"(comment on column "firewalls"."name6" is null)");
QVERIFY(log10.boundValues.isEmpty());
const auto &log11 = log.at(11);
QCOMPARE(log11.query,
R"(comment on column "firewalls"."name7" is null)");
QVERIFY(log11.boundValues.isEmpty());
const auto &log12 = log.at(12);
QCOMPARE(log12.query,
R"(comment on column "firewalls"."amount" is null)");
QVERIFY(log12.boundValues.isEmpty());
const auto &log13 = log.at(13);
QCOMPARE(log13.query,
R"(comment on column "firewalls"."positions1" is null)");
QVERIFY(log13.boundValues.isEmpty());
const auto &log14 = log.at(14);
QCOMPARE(log14.query,
R"(comment on column "firewalls"."added_on" is null)");
QVERIFY(log14.boundValues.isEmpty());
const auto &log15 = log.at(15);
QCOMPARE(log15.query,
"alter table \"firewalls\" "
"alter column \"id\" type serial, "
"alter column \"id\" set not null, "
"alter column \"id\" drop default, "
"alter column \"id\" drop identity if exists");
QVERIFY(log15.boundValues.isEmpty());
const auto &log16 = log.at(16);
QCOMPARE(log16.query,
R"(alter sequence "firewalls_id_seq" restart with 15)");
QVERIFY(log16.boundValues.isEmpty());
const auto &log17 = log.at(17);
QCOMPARE(log17.query,
R"(comment on column "firewalls"."id" is null)");
QVERIFY(log17.boundValues.isEmpty());
}
void tst_PostgreSQL_SchemaBuilder::useCurrent() const
{
auto log = DB::connection(m_connection).pretend([](auto &connection)
@@ -1636,6 +1848,70 @@ void tst_PostgreSQL_SchemaBuilder::virtualAs_StoredAs_Nullable_ModifyTable() con
QVERIFY(firstLog.boundValues.isEmpty());
}
void tst_PostgreSQL_SchemaBuilder::change_VirtualAs_ThrowException() const
{
QVERIFY_EXCEPTION_THROWN(
DB::connection(m_connection).pretend([](auto &connection)
{
Schema::on(connection.getName())
.table(Firewalls, [](Blueprint &table)
{
/* Currently, PostgreSQL 15 doesn't support virtual generated columns,
only stored, but I test it anyway. Changing generated column must throw
exception, PostgreSQL doesn't support modifying generated columns. */
table.integer("discounted_virtual").virtualAs("price - 5").change();
});
}),
LogicError);
}
void tst_PostgreSQL_SchemaBuilder::change_StoredAs_ThrowException() const
{
QVERIFY_EXCEPTION_THROWN(
DB::connection(m_connection).pretend([](auto &connection)
{
Schema::on(connection.getName())
.table(Firewalls, [](Blueprint &table)
{
/* Currently, PostgreSQL 15 doesn't support virtual generated columns,
only stored, but I test it anyway. Changing generated column must throw
exception, PostgreSQL doesn't support modifying generated columns. */
table.integer("discounted_virtual").storedAs("price - 5").change();
});
}),
LogicError);
}
void tst_PostgreSQL_SchemaBuilder::drop_StoredAs() const
{
auto log = DB::connection(m_connection).pretend([](auto &connection)
{
Schema::on(connection.getName())
.table(Firewalls, [](Blueprint &table)
{
// Because of this the CommandDefinition::storedAs must be std::optional
table.integer("foo").storedAs(QString()).nullable().change();
});
});
QCOMPARE(log.size(), 2);
const auto &log0 = log.at(0);
QCOMPARE(log0.query,
"alter table \"firewalls\" "
"alter column \"foo\" type integer, "
"alter column \"foo\" drop not null, "
"alter column \"foo\" drop default, "
"alter column \"foo\" drop expression if exists, "
"alter column \"foo\" drop identity if exists");
QVERIFY(log0.boundValues.isEmpty());
const auto &log1 = log.at(1);
QCOMPARE(log1.query,
R"(comment on column "firewalls"."foo" is null)");
QVERIFY(log1.boundValues.isEmpty());
}
void tst_PostgreSQL_SchemaBuilder::indexes_Fluent() const
{
auto log = DB::connection(m_connection).pretend([](auto &connection)