From 9fcde4299b97f8a72f53cfe2247521aefc56fe9e Mon Sep 17 00:00:00 2001 From: Yanky Hoffman Date: Mon, 18 Jul 2022 13:13:17 -0400 Subject: [PATCH 1/4] Add (failing) test --- QueryBuilder.Tests/GeneralTests.cs | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/QueryBuilder.Tests/GeneralTests.cs b/QueryBuilder.Tests/GeneralTests.cs index 940517f0..86fc80a2 100644 --- a/QueryBuilder.Tests/GeneralTests.cs +++ b/QueryBuilder.Tests/GeneralTests.cs @@ -2,6 +2,7 @@ using SqlKata.Extensions; using SqlKata.Tests.Infrastructure; using System; +using System.Collections.Generic; using System.Linq; using Xunit; @@ -591,5 +592,53 @@ public void Passing_Negative_Boolean_False_To_Where_Should_Call_WhereTrue_Or_Whe Assert.Equal("SELECT * FROM [Table] WHERE [Col] != cast(0 as bit)", c[EngineCodes.SqlServer].ToString()); } + + [Fact] + public void AllowQuotesAlways() + { + var expected = new Dictionary + { + [EngineCodes.PostgreSql] = "SELECT \"ColumnA\" AS \"A\", \"ColumnB\" AS \"B\", \"ColumnC\" AS \"C\", \"ColumnD\" AS \"D\", \"ColumnE\" AS \"E\", ColumnF AS F, \"ColumnG\", \"ColumnH\", \"ColumnI\", \"ColumnJ\", \"ColumnK\", ColumnL FROM \"Table\"", + [EngineCodes.SqlServer] = "SELECT [ColumnA] AS [A], [ColumnB] AS [B], [ColumnC] AS [C], [ColumnD] AS [D], [ColumnE] AS [E], ColumnF AS F, [ColumnG], [ColumnH], [ColumnI], [ColumnJ], [ColumnK], ColumnL FROM [Table]", + [EngineCodes.MySql] = "SELECT `ColumnA` AS `A`, `ColumnB` AS `B`, `ColumnC` AS `C`, `ColumnD` AS `D`, `ColumnE` AS `E`, ColumnF AS F, `ColumnG`, `ColumnH`, `ColumnI`, `ColumnJ`, `ColumnK`, ColumnL FROM `Table`", + }; + + foreach (var engineCode in new [] {EngineCodes.PostgreSql, EngineCodes.SqlServer, EngineCodes.MySql}) + { + var compiled = Compilers.Compile(new[] { engineCode }, WithQuotedColumns(engineCode)); + + Assert.Equal(compiled[engineCode].ToString(), expected[engineCode]); + } + + Query WithQuotedColumns(string engineCode) + { + var quotes = engineCode switch + { + EngineCodes.PostgreSql => (open: "\"", close: "\""), + EngineCodes.SqlServer => (open: "[", close: "]"), + EngineCodes.MySql => (open: "`", close: "`"), + _ => throw new ArgumentOutOfRangeException(), + }; + + // Return a query `SELECT`ing a combination of columns aliased or not, wrapped with the compiler specific quote or with `[]` + return new Query("Table") + // `Select` with `AS` + .Select("[ColumnA] AS [A]") + .Select($"{quotes.open}ColumnB{quotes.close} AS {quotes.open}B{quotes.close}") + .Select("ColumnC AS C") + // `SelectRaw` with `AS` + .SelectRaw("[ColumnD] AS [D]") + .SelectRaw($"{quotes.open}ColumnE{quotes.close} AS {quotes.open}E{quotes.close}") + .SelectRaw("ColumnF AS F") + // `Select` + .Select("[ColumnG]") + .Select($"{quotes.open}ColumnH{quotes.close}") + .Select("ColumnI") + // `SelectRaw` + .SelectRaw("[ColumnJ]") + .SelectRaw($"{quotes.open}ColumnK{quotes.close}") + .SelectRaw("ColumnL"); + } + } } } From b89a92fc42d808ad598b39a32db15a91e288d491 Mon Sep 17 00:00:00 2001 From: Yanky Hoffman Date: Mon, 18 Jul 2022 13:47:47 -0400 Subject: [PATCH 2/4] Define global identifier quote characters As defined [here](https://sqlkata.com/docs/select#identify-columns-and-tables-inside-raw) --- QueryBuilder/Compilers/Compiler.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index 98d26fd8..98bff254 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -7,6 +7,11 @@ namespace SqlKata.Compilers { public partial class Compiler { + // As defined [here](https://sqlkata.com/docs/select#identify-columns-and-tables-inside-raw) + // the library allows quoting identifiers with `[]` regardless of compiler used. + private const string OpeningIdentifierPlaceholder = "["; + private const string ClosingIdentifierPlaceholder = "]"; + private readonly ConditionsCompilerProvider _compileConditionMethodsProvider; protected virtual string parameterPlaceholder { get; set; } = "?"; protected virtual string parameterPrefix { get; set; } = "@p"; @@ -971,8 +976,8 @@ public virtual string WrapIdentifiers(string input) .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, "{", this.OpeningIdentifier) .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, "}", this.ClosingIdentifier) - .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, "[", this.OpeningIdentifier) - .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, "]", this.ClosingIdentifier); + .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, OpeningIdentifierPlaceholder, this.OpeningIdentifier) + .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, ClosingIdentifierPlaceholder, this.ClosingIdentifier); } } } From 46a9e4f7b2f34be4aafac49d7dbdfce59f4c74fd Mon Sep 17 00:00:00 2001 From: Yanky Hoffman Date: Mon, 18 Jul 2022 13:48:11 -0400 Subject: [PATCH 3/4] Only escape quotes within quoted identifiers --- QueryBuilder/Compilers/Compiler.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index 98bff254..b9670380 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -891,7 +891,17 @@ public virtual string WrapValue(string value) var opening = this.OpeningIdentifier; var closing = this.ClosingIdentifier; - return opening + value.Replace(closing, closing + closing) + closing; + // If value is already wrapped with opening and closing quotes, remove the quotes to allow escaping of the + // remaining quotes the value will be quoted again before returning. + if ((value.Substring(0, 1) == opening && value.Substring(value.Length - 1, 1) == closing) || + (value.Substring(0, 1) == OpeningIdentifierPlaceholder && value.Substring(value.Length - 1, 1) == ClosingIdentifierPlaceholder)) + { + value = value.Substring(1, value.Length - 2); + } + + var escaped = value.Replace(closing, closing + closing); + + return opening + escaped + closing; } /// From 0e89bf831e8a13cd14f9bf96fdded4aed84d16c3 Mon Sep 17 00:00:00 2001 From: Yanky Hoffman Date: Mon, 18 Jul 2022 16:41:28 -0400 Subject: [PATCH 4/4] Simplify checking for wrapped quotes --- QueryBuilder/Compilers/Compiler.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index b9670380..c69cc98b 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -893,10 +893,13 @@ public virtual string WrapValue(string value) // If value is already wrapped with opening and closing quotes, remove the quotes to allow escaping of the // remaining quotes the value will be quoted again before returning. - if ((value.Substring(0, 1) == opening && value.Substring(value.Length - 1, 1) == closing) || - (value.Substring(0, 1) == OpeningIdentifierPlaceholder && value.Substring(value.Length - 1, 1) == ClosingIdentifierPlaceholder)) + foreach (var (open, close) in new[] { (OpeningIdentifierPlaceholder, ClosingIdentifierPlaceholder), (opening, closing) }) { - value = value.Substring(1, value.Length - 2); + if (value.StartsWith(open) && value.EndsWith(close)) + { + value = value.Substring(1, value.Length - 2); + break; + } } var escaped = value.Replace(closing, closing + closing);