Skip to content

Commit bf1b19c

Browse files
rojiCopilot
andauthored
.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 <223556219+Copilot@users.noreply.github.com>
1 parent 1b2f318 commit bf1b19c

2 files changed

Lines changed: 87 additions & 5 deletions

File tree

dotnet/src/VectorData/SqlServer/SqlServerCommandBuilder.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ internal static List<SqlCommand> CreateTable(
3131
if (ifNotExists)
3232
{
3333
sb.Append("IF OBJECT_ID(N'");
34-
sb.AppendTableName(schema, tableName);
34+
sb.AppendTableNameInsideLiteral(schema, tableName);
3535
sb.AppendLine("', N'U') IS NULL");
3636
}
3737
sb.AppendLine("BEGIN");
@@ -125,22 +125,22 @@ internal static List<SqlCommand> CreateTable(
125125
// Full-text indexes require a unique index (we use the primary key)
126126
sb.AppendLine("DECLARE @pkIndexName NVARCHAR(128);");
127127
sb.Append("SELECT @pkIndexName = name FROM sys.indexes WHERE object_id = OBJECT_ID(N'");
128-
sb.AppendTableName(schema, tableName);
128+
sb.AppendTableNameInsideLiteral(schema, tableName);
129129
sb.AppendLine("') AND is_primary_key = 1;");
130130

131131
sb.AppendLine("DECLARE @ftSql NVARCHAR(MAX);");
132132
sb.Append("SET @ftSql = N'CREATE FULLTEXT INDEX ON ");
133-
sb.AppendTableName(schema, tableName).Append(" (");
133+
sb.AppendTableNameInsideLiteral(schema, tableName).Append(" (");
134134
for (int i = 0; i < fullTextProperties.Count; i++)
135135
{
136-
sb.AppendIdentifier(fullTextProperties[i].StorageName);
136+
sb.AppendIdentifierInsideLiteral(fullTextProperties[i].StorageName);
137137
if (i < fullTextProperties.Count - 1)
138138
{
139139
sb.Append(',');
140140
}
141141
}
142142
sb.Append(") KEY INDEX ' + QUOTENAME(@pkIndexName) + N' ON ");
143-
sb.AppendIdentifier(catalogName).AppendLine("';");
143+
sb.AppendIdentifierInsideLiteral(catalogName).AppendLine("';");
144144
sb.AppendLine("EXEC sp_executesql @ftSql;");
145145
}
146146

@@ -897,6 +897,30 @@ internal static StringBuilder AppendIdentifier(this StringBuilder sb, string ide
897897
return sb;
898898
}
899899

900+
/// <summary>
901+
/// Same as <see cref="AppendTableName"/>, but for use inside a SQL string literal (N'...'),
902+
/// where single quotes must be escaped by doubling them.
903+
/// </summary>
904+
internal static StringBuilder AppendTableNameInsideLiteral(this StringBuilder sb, string? schema, string tableName)
905+
{
906+
int start = sb.Length;
907+
sb.AppendTableName(schema, tableName);
908+
sb.Replace("'", "''", start, sb.Length - start);
909+
return sb;
910+
}
911+
912+
/// <summary>
913+
/// Same as <see cref="AppendIdentifier"/>, but for use inside a SQL string literal (N'...'),
914+
/// where single quotes must be escaped by doubling them.
915+
/// </summary>
916+
internal static StringBuilder AppendIdentifierInsideLiteral(this StringBuilder sb, string identifier)
917+
{
918+
int start = sb.Length;
919+
sb.AppendIdentifier(identifier);
920+
sb.Replace("'", "''", start, sb.Length - start);
921+
return sb;
922+
}
923+
900924
private static StringBuilder AppendIdentifiers(this StringBuilder sb,
901925
IEnumerable<PropertyModel> properties,
902926
string? prefix = null,

dotnet/test/VectorData/SqlServer.ConformanceTests/SqlServerCommandBuilderTests.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,36 @@ public void AppendTableName(string? schema, string table, string expectedFullNam
2727
Assert.Equal(expectedFullName, result.ToString());
2828
}
2929

30+
[Theory]
31+
[InlineData("schema", "name", "[schema].[name]")]
32+
[InlineData(null, "name", "[name]")]
33+
[InlineData("schema", "it's", "[schema].[it''s]")]
34+
[InlineData("it's", "name", "[it''s].[name]")]
35+
[InlineData("it's", "it's", "[it''s].[it''s]")]
36+
[InlineData(null, "it's", "[it''s]")]
37+
[InlineData("schema", "[brackets]", "[schema].[[brackets]]]")]
38+
public void AppendTableNameInsideLiteral(string? schema, string table, string expectedFullName)
39+
{
40+
StringBuilder result = new();
41+
42+
SqlServerCommandBuilder.AppendTableNameInsideLiteral(result, schema, table);
43+
44+
Assert.Equal(expectedFullName, result.ToString());
45+
}
46+
47+
[Theory]
48+
[InlineData("name", "[name]")]
49+
[InlineData("it's", "[it''s]")]
50+
[InlineData("two''quotes", "[two''''quotes]")]
51+
public void AppendIdentifierInsideLiteral(string identifier, string expected)
52+
{
53+
StringBuilder result = new();
54+
55+
SqlServerCommandBuilder.AppendIdentifierInsideLiteral(result, identifier);
56+
57+
Assert.Equal(expected, result.ToString());
58+
}
59+
3060
[Theory]
3161
[InlineData("name", "@name_")] // typical name
3262
[InlineData("na me", "@na_")] // contains a whitespace, an illegal parameter name character
@@ -149,6 +179,34 @@ PRIMARY KEY ([id])
149179
Assert.Equal(expectedCommand, command.CommandText, ignoreLineEndingDifferences: true);
150180
}
151181

182+
[Fact]
183+
public void CreateTable_WithSingleQuoteInName()
184+
{
185+
var model = BuildModel(
186+
[
187+
new VectorStoreKeyProperty("id", typeof(long)),
188+
new VectorStoreDataProperty("name", typeof(string)),
189+
]);
190+
191+
using SqlConnection connection = CreateConnection();
192+
193+
var commands = SqlServerCommandBuilder.CreateTable(connection, "it's", "ta'ble", ifNotExists: true, model);
194+
195+
var command = Assert.Single(commands);
196+
Assert.Equal(
197+
"IF OBJECT_ID(N'[it''s].[ta''ble]', N'U') IS NULL" + Environment.NewLine +
198+
"""
199+
BEGIN
200+
CREATE TABLE [it's].[ta'ble] (
201+
[id] BIGINT IDENTITY,
202+
[name] NVARCHAR(MAX),
203+
PRIMARY KEY ([id])
204+
);
205+
END;
206+
""",
207+
command.CommandText, ignoreLineEndingDifferences: true);
208+
}
209+
152210
[Fact]
153211
public void CreateTable_WithDiskAnnIndex()
154212
{

0 commit comments

Comments
 (0)