mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
.Net: Fix single-quote escaping in OBJECT_ID and dynamic SQL string literals (#13900)
In the SQL Server MEVD provider, table/schema/column names embedded inside SQL string literals (`N'...'`) were not escaping single quotes. This could produce broken SQL when names contain single quotes. For example, a table named `it's` would generate: ```sql IF OBJECT_ID(N'[dbo].[it's]', N'U') IS NULL ``` which is invalid SQL. The fix produces: ```sql IF OBJECT_ID(N'[dbo].[it''s]', N'U') IS NULL ``` ### Changes Added `AppendTableNameInsideLiteral` and `AppendIdentifierInsideLiteral` helper methods that escape single quotes by doubling them, and updated the 5 call sites where identifiers appear inside string literals: - `OBJECT_ID` in `CreateTable` (ifNotExists check) - `OBJECT_ID` in full-text index PK lookup - Dynamic SQL table name, column names, and catalog name for full-text indexes ### Tests Added unit tests for `AppendTableNameInsideLiteral`, `AppendIdentifierInsideLiteral`, and a `CreateTable_WithSingleQuoteInName` integration test. All 283 tests pass (unit + conformance). Co-authored-by: Copilot <[email protected]>
This commit is contained in:
@@ -31,7 +31,7 @@ internal static class SqlServerCommandBuilder
|
||||
if (ifNotExists)
|
||||
{
|
||||
sb.Append("IF OBJECT_ID(N'");
|
||||
sb.AppendTableName(schema, tableName);
|
||||
sb.AppendTableNameInsideLiteral(schema, tableName);
|
||||
sb.AppendLine("', N'U') IS NULL");
|
||||
}
|
||||
sb.AppendLine("BEGIN");
|
||||
@@ -125,22 +125,22 @@ internal static class SqlServerCommandBuilder
|
||||
// Full-text indexes require a unique index (we use the primary key)
|
||||
sb.AppendLine("DECLARE @pkIndexName NVARCHAR(128);");
|
||||
sb.Append("SELECT @pkIndexName = name FROM sys.indexes WHERE object_id = OBJECT_ID(N'");
|
||||
sb.AppendTableName(schema, tableName);
|
||||
sb.AppendTableNameInsideLiteral(schema, tableName);
|
||||
sb.AppendLine("') AND is_primary_key = 1;");
|
||||
|
||||
sb.AppendLine("DECLARE @ftSql NVARCHAR(MAX);");
|
||||
sb.Append("SET @ftSql = N'CREATE FULLTEXT INDEX ON ");
|
||||
sb.AppendTableName(schema, tableName).Append(" (");
|
||||
sb.AppendTableNameInsideLiteral(schema, tableName).Append(" (");
|
||||
for (int i = 0; i < fullTextProperties.Count; i++)
|
||||
{
|
||||
sb.AppendIdentifier(fullTextProperties[i].StorageName);
|
||||
sb.AppendIdentifierInsideLiteral(fullTextProperties[i].StorageName);
|
||||
if (i < fullTextProperties.Count - 1)
|
||||
{
|
||||
sb.Append(',');
|
||||
}
|
||||
}
|
||||
sb.Append(") KEY INDEX ' + QUOTENAME(@pkIndexName) + N' ON ");
|
||||
sb.AppendIdentifier(catalogName).AppendLine("';");
|
||||
sb.AppendIdentifierInsideLiteral(catalogName).AppendLine("';");
|
||||
sb.AppendLine("EXEC sp_executesql @ftSql;");
|
||||
}
|
||||
|
||||
@@ -897,6 +897,30 @@ internal static class SqlServerCommandBuilder
|
||||
return sb;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="AppendTableName"/>, but for use inside a SQL string literal (N'...'),
|
||||
/// where single quotes must be escaped by doubling them.
|
||||
/// </summary>
|
||||
internal static StringBuilder AppendTableNameInsideLiteral(this StringBuilder sb, string? schema, string tableName)
|
||||
{
|
||||
int start = sb.Length;
|
||||
sb.AppendTableName(schema, tableName);
|
||||
sb.Replace("'", "''", start, sb.Length - start);
|
||||
return sb;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="AppendIdentifier"/>, but for use inside a SQL string literal (N'...'),
|
||||
/// where single quotes must be escaped by doubling them.
|
||||
/// </summary>
|
||||
internal static StringBuilder AppendIdentifierInsideLiteral(this StringBuilder sb, string identifier)
|
||||
{
|
||||
int start = sb.Length;
|
||||
sb.AppendIdentifier(identifier);
|
||||
sb.Replace("'", "''", start, sb.Length - start);
|
||||
return sb;
|
||||
}
|
||||
|
||||
private static StringBuilder AppendIdentifiers(this StringBuilder sb,
|
||||
IEnumerable<PropertyModel> properties,
|
||||
string? prefix = null,
|
||||
|
||||
@@ -27,6 +27,36 @@ public class SqlServerCommandBuilderTests
|
||||
Assert.Equal(expectedFullName, result.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("schema", "name", "[schema].[name]")]
|
||||
[InlineData(null, "name", "[name]")]
|
||||
[InlineData("schema", "it's", "[schema].[it''s]")]
|
||||
[InlineData("it's", "name", "[it''s].[name]")]
|
||||
[InlineData("it's", "it's", "[it''s].[it''s]")]
|
||||
[InlineData(null, "it's", "[it''s]")]
|
||||
[InlineData("schema", "[brackets]", "[schema].[[brackets]]]")]
|
||||
public void AppendTableNameInsideLiteral(string? schema, string table, string expectedFullName)
|
||||
{
|
||||
StringBuilder result = new();
|
||||
|
||||
SqlServerCommandBuilder.AppendTableNameInsideLiteral(result, schema, table);
|
||||
|
||||
Assert.Equal(expectedFullName, result.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("name", "[name]")]
|
||||
[InlineData("it's", "[it''s]")]
|
||||
[InlineData("two''quotes", "[two''''quotes]")]
|
||||
public void AppendIdentifierInsideLiteral(string identifier, string expected)
|
||||
{
|
||||
StringBuilder result = new();
|
||||
|
||||
SqlServerCommandBuilder.AppendIdentifierInsideLiteral(result, identifier);
|
||||
|
||||
Assert.Equal(expected, result.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("name", "@name_")] // typical name
|
||||
[InlineData("na me", "@na_")] // contains a whitespace, an illegal parameter name character
|
||||
@@ -149,6 +179,34 @@ public class SqlServerCommandBuilderTests
|
||||
Assert.Equal(expectedCommand, command.CommandText, ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateTable_WithSingleQuoteInName()
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
]);
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
var commands = SqlServerCommandBuilder.CreateTable(connection, "it's", "ta'ble", ifNotExists: true, model);
|
||||
|
||||
var command = Assert.Single(commands);
|
||||
Assert.Equal(
|
||||
"IF OBJECT_ID(N'[it''s].[ta''ble]', N'U') IS NULL" + Environment.NewLine +
|
||||
"""
|
||||
BEGIN
|
||||
CREATE TABLE [it's].[ta'ble] (
|
||||
[id] BIGINT IDENTITY,
|
||||
[name] NVARCHAR(MAX),
|
||||
PRIMARY KEY ([id])
|
||||
);
|
||||
END;
|
||||
""",
|
||||
command.CommandText, ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateTable_WithDiskAnnIndex()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user