enhanced escaping of special characters

Enhanced escaping of special characters in the schema builder.

 - added note to the docs
 - added unit tests
This commit is contained in:
silverqx
2022-06-01 19:17:43 +02:00
parent f7bec84d0e
commit bf27133ffc
12 changed files with 128 additions and 37 deletions
+15 -15
View File
@@ -786,25 +786,25 @@ The following table contains all of the available column modifiers. This list do
| Modifier | Description |
| -------------------------- | ----------- |
| `.after("column")` | Place the column "after" another column (MySQL). |
| `.after("column")` | Place the column "after" another column <small>(MySQL)</small>. |
| `.autoIncrement()` | Set INTEGER columns as auto-incrementing (primary key). |
| `.charset("utf8mb4")` | Specify a character set for the column (MySQL). |
| <small>`.collation("utf8mb4_unicode_ci")`</small> | Specify a collation for the column (MySQL/PostgreSQL/SQL Server). |
| `.comment("my comment")` | Add a comment to a column (MySQL/PostgreSQL). |
| `.defaultValue(value)` | Specify a "default" value for the column. |
| `.first()` | Place the column "first" in the table (MySQL). |
| `.from(integer)` | Set the starting value of an auto-incrementing field, an alias for `startingValue()` (MySQL / PostgreSQL). |
| `.invisible()` | Make the column "invisible" to `SELECT *` queries (MySQL). |
| `.charset("utf8mb4")` | Specify a character set for the column <small>(MySQL)</small>. |
| <small>`.collation("utf8mb4_unicode_ci")`</small> | Specify a collation for the column <small>(MySQL/PostgreSQL/SQL Server)</small>. |
| `.comment("my comment")` | Add a comment to a column <small>(MySQL / PostgreSQL)</small>.<br/><small>Special characters are escaped.</small> |
| `.defaultValue(value)` | Specify a "default" value for the column.<br/><small>Special characters are escaped.</small> |
| `.first()` | Place the column "first" in the table <small>(MySQL)</small>. |
| `.from(integer)` | Set the starting value of an auto-incrementing field, an alias for `startingValue()` <small>(MySQL / PostgreSQL)</small>. |
| `.invisible()` | Make the column "invisible" to `SELECT *` queries <small>(MySQL)</small>. |
| `.nullable(value = true)` | Allow NULL values to be inserted into the column. |
| `.startingValue(integer)` | Set the starting value of an auto-incrementing field (MySQL / PostgreSQL). |
| `.storedAs(expression)` | Create a stored generated column (MySQL / PostgreSQL). |
| `.unsigned()` | Set INTEGER columns as UNSIGNED (MySQL). |
| `.startingValue(integer)` | Set the starting value of an auto-incrementing field <small>(MySQL / PostgreSQL)</small>. |
| `.storedAs(expression)` | Create a stored generated column <small>(MySQL / PostgreSQL)</small>. |
| `.unsigned()` | Set INTEGER columns as UNSIGNED <small>(MySQL)</small>. |
| `.useCurrent()` | Set TIMESTAMP columns to use CURRENT_TIMESTAMP as default value. |
| `.useCurrentOnUpdate()` | Set TIMESTAMP columns to use CURRENT_TIMESTAMP when a record is updated. |
| `.virtualAs(expression)` | Create a virtual generated column (MySQL). |
| `.generatedAs(expression)` | Create an identity column with specified sequence options (PostgreSQL). |
| `.always()` | Defines the precedence of sequence values over input for an identity column (PostgreSQL). |
| `.isGeometry()` | Set spatial column type to `geometry` - the default type is `geography` (PostgreSQL). |
| `.virtualAs(expression)` | Create a virtual generated column <small>(MySQL)</small>. |
| `.generatedAs(expression)` | Create an identity column with specified sequence options <small>(PostgreSQL)</small>. |
| `.always()` | Defines the precedence of sequence values over input for an identity column <small>(PostgreSQL)</small>. |
| `.isGeometry()` | Set spatial column type to `geometry` - the default type is `geography` <small>(PostgreSQL)</small>. |
#### Default Expressions
+1 -1
View File
@@ -8,7 +8,7 @@ keywords: [c++ orm, supported compilers, tinyorm]
# 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 973 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 975 unit tests 😮.
Windows >=10:
@@ -162,8 +162,8 @@ namespace Grammars
/*! Wrap a single string in keyword identifiers. */
QString wrapValue(QString value) const override;
/*! Escape all MySQL spacial characters desribed in String Literal docs. */
QString addSlashes(QString value) const;
/*! Escape special characters (used by the defaultValue and comment). */
QString escapeString(QString value) const override;
/*! Get the SQL for the column data type. */
QString getType(const ColumnDefinition &column) const override;
@@ -159,6 +159,9 @@ namespace Grammars
QVector<QString> compileDropConstraint(const Blueprint &blueprint,
const IndexCommand &command) const;
/*! Escape special characters (used by the defaultValue and comment). */
QString escapeString(QString value) const override;
/*! Get the SQL for the column data type. */
QString getType(const ColumnDefinition &column) const override;
@@ -96,6 +96,9 @@ namespace Grammars
const Blueprint &blueprint) const = 0;
protected:
/*! Escape special characters (used by the defaultValue and comment). */
virtual QString escapeString(QString value) const = 0;
/*! Get the SQL for the column data type. */
virtual QString getType(const ColumnDefinition &column) const = 0;
/*! Compile the blueprint's column definitions. */
@@ -52,6 +52,9 @@ namespace Orm::SchemaNs::Grammars
QString addModifiers(QString &&sql,
const ColumnDefinition &column) const override;
/*! Escape special characters (used by the defaultValue and comment). */
QString escapeString(QString value) const override;
/*! Get the SQL for the column data type. */
QString getType(const ColumnDefinition &column) const override;
};
+23 -12
View File
@@ -439,18 +439,29 @@ QString MySqlSchemaGrammar::wrapValue(QString value) const
QStringLiteral("``")));
}
QString MySqlSchemaGrammar::addSlashes(QString value) const
QString MySqlSchemaGrammar::escapeString(QString value) const
{
/* Different approach used for the MySQL and PostgreSQL, for MySQL are escaped more
special characters but for PostrgreSQL only single-quote, it doesn't matter
though, it will work anyway.
On MySQL escaping of ^Z, \0, and \ is needed on some environments, described here:
https://dev.mysql.com/doc/refman/8.0/en/string-literals.html
On PostgreSQL escaping using \ is is more SQL standard conforming, described here,
(especially look at the caution box):
https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-SPECIAL-CHARS*/
return value
.replace(QChar(0x001a), "^Z")
.replace(QChar('\\'), "\\\\")
.replace(QChar(QChar::Null), "\\0")
.replace(QChar(QChar::LineFeed), "\\n")
.replace(QChar(QChar::Tabulation), "\\t")
.replace(QChar(0x0008), "\\b")
.replace(QChar(QChar::CarriageReturn), "\\r")
.replace(QChar('"'), "\\\"")
.replace(QChar(0x0027), "\\'");
// No need to escape these
// .replace(QChar(QChar::LineFeed), "\\n")
// .replace(QChar(QChar::Tabulation), "\\t")
// .replace(QChar(0x0008), "\\b")
// .replace(QChar(QChar::CarriageReturn), "\\r")
// .replace(QChar('"'), "\\\"")
.replace(QChar(0x001a), QStringLiteral("^Z"))
.replace(QChar('\\'), QStringLiteral("\\\\"))
.replace(QChar(QChar::Null), QStringLiteral("\\0"))
// 0x0027 = '
.replace(QChar(0x0027), "''");
}
QString MySqlSchemaGrammar::getType(const ColumnDefinition &column) const
@@ -902,6 +913,7 @@ QString MySqlSchemaGrammar::modifyDefault(const ColumnDefinition &column) const
return {};
// CUR schema, note about security in docs, unprepared and unescaped silverqx
// Default value is already quoted and escaped
return QStringLiteral(" default %1").arg(getDefaultValue(defaultValue));
}
@@ -940,9 +952,8 @@ QString MySqlSchemaGrammar::modifyComment(const ColumnDefinition &column) const
if (column.comment.isEmpty())
return {};
// CUR schema docs, note about escaping silverqx
// All escaped special characters will be correctly saved in the comment
return QStringLiteral(" comment %1").arg(quoteString(addSlashes(column.comment)));
return QStringLiteral(" comment %1").arg(quoteString(escapeString(column.comment)));
}
QString MySqlSchemaGrammar::modifySrid(const ColumnDefinition &column) const
@@ -298,13 +298,9 @@ QVector<QString>
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))};
quoteString(escapeString(command.comment)))};
}
QVector<QString>
@@ -442,6 +438,21 @@ PostgresSchemaGrammar::compileDropConstraint(const Blueprint &blueprint,
.arg(wrapTable(blueprint), BaseGrammar::wrap(command.index))};
}
QString PostgresSchemaGrammar::escapeString(QString value) const
{
/* Different approach used for the MySQL and PostgreSQL, for MySQL are escaped more
special characters but for PostrgreSQL only single-quote, it doesn't matter
though, it will work anyway.
On MySQL escaping of ^Z, \0, and \ is needed on some environments, described here:
https://dev.mysql.com/doc/refman/8.0/en/string-literals.html
On PostgreSQL escaping using \ is is more SQL standard conforming, described here,
(especially look at the caution box):
https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-SPECIAL-CHARS*/
// 0x0027 = '
return value.replace(QChar(0x0027), QStringLiteral("''"));
}
QString PostgresSchemaGrammar::getType(const ColumnDefinition &column) const
{
switch (column.type) {
@@ -884,7 +895,7 @@ QString PostgresSchemaGrammar::modifyDefault(const ColumnDefinition &column) con
if (!defaultValue.isValid() || defaultValue.isNull())
return {};
// CUR schema, note about security in docs, unprepared and unescaped silverqx
// Default value is already quoted and escaped
return QStringLiteral(" default %1").arg(getDefaultValue(defaultValue));
}
+1 -1
View File
@@ -147,7 +147,7 @@ QString SchemaGrammar::getDefaultValue(const QVariant &value) const
return value.userType() == QMetaType::Bool
#endif
? quoteString(QString::number(value.value<int>()))
: quoteString(value.value<QString>());
: quoteString(escapeString(value.value<QString>()));
}
QString SchemaGrammar::typeComputed(const ColumnDefinition &/*unused*/) const
@@ -52,6 +52,11 @@ QString SQLiteSchemaGrammar::addModifiers(QString &&/*unused*/,
throw Exceptions::RuntimeError(NotImplemented);
}
QString SQLiteSchemaGrammar::escapeString(QString /*unused*/) const
{
throw Exceptions::RuntimeError(NotImplemented);
}
QString SQLiteSchemaGrammar::getType(const ColumnDefinition &/*unused*/) const
{
throw Exceptions::RuntimeError(NotImplemented);
@@ -74,6 +74,7 @@ private Q_SLOTS:
void modifiers() const;
void modifier_defaultValue_WithExpression() const;
void modifier_defaultValue_WithBoolean() const;
void modifier_defaultValue_Escaping() const;
void useCurrent() const;
void useCurrentOnUpdate() const;
@@ -788,6 +789,33 @@ void tst_Mysql_SchemaBuilder::modifier_defaultValue_WithBoolean() const
QVERIFY(firstLog.boundValues.isEmpty());
}
void tst_Mysql_SchemaBuilder::modifier_defaultValue_Escaping() const
{
auto log = DB::connection(m_connection).pretend([](auto &connection)
{
Schema::on(connection.getName())
.create(Firewalls, [](Blueprint &table)
{
// String contains \t after tab word
table.string("string").defaultValue(R"(Text ' and " or \ newline
and tab end)");
});
});
QVERIFY(!log.isEmpty());
const auto &firstLog = log.first();
QCOMPARE(log.size(), 1);
QCOMPARE(firstLog.query,
// String contains \t after tab word
"create table `firewalls` ("
"`string` varchar(255) not null default 'Text '' and \" or \\\\ newline\n"
"and tab end') "
"default character set utf8mb4 collate 'utf8mb4_0900_ai_ci' "
"engine = InnoDB");
QVERIFY(firstLog.boundValues.isEmpty());
}
void tst_Mysql_SchemaBuilder::useCurrent() const
{
auto log = DB::connection(m_connection).pretend([](auto &connection)
@@ -74,6 +74,7 @@ private Q_SLOTS:
void modifiers() const;
void modifier_defaultValue_WithExpression() const;
void modifier_defaultValue_WithBoolean() const;
void modifier_defaultValue_Escaping() const;
void useCurrent() const;
void useCurrentOnUpdate() const;
@@ -780,6 +781,32 @@ void tst_PostgreSQL_SchemaBuilder::modifier_defaultValue_WithBoolean() const
QVERIFY(firstLog.boundValues.isEmpty());
}
void tst_PostgreSQL_SchemaBuilder::modifier_defaultValue_Escaping() const
{
auto log = DB::connection(m_connection).pretend([](auto &connection)
{
Schema::on(connection.getName())
.create(Firewalls, [](Blueprint &table)
{
// String contains \t after tab word
table.string("string").defaultValue(R"(Text ' and " or \ newline
and tab end)");
});
});
QVERIFY(!log.isEmpty());
const auto &firstLog = log.first();
QCOMPARE(log.size(), 1);
QCOMPARE(firstLog.query,
// String contains \t after tab word
"create table \"firewalls\" ("
"\"string\" varchar(255) not null "
"default 'Text '' and \" or \\ newline\n"
"and tab end')");
QVERIFY(firstLog.boundValues.isEmpty());
}
void tst_PostgreSQL_SchemaBuilder::useCurrent() const
{
auto log = DB::connection(m_connection).pretend([](auto &connection)