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"); + } + } } } diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index 98d26fd8..c69cc98b 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"; @@ -886,7 +891,20 @@ 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. + foreach (var (open, close) in new[] { (OpeningIdentifierPlaceholder, ClosingIdentifierPlaceholder), (opening, closing) }) + { + if (value.StartsWith(open) && value.EndsWith(close)) + { + value = value.Substring(1, value.Length - 2); + break; + } + } + + var escaped = value.Replace(closing, closing + closing); + + return opening + escaped + closing; } /// @@ -971,8 +989,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); } } }