mirror of
https://github.com/silverqx/TinyORM.git
synced 2026-05-07 17:19:37 -05:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user