Skip to content

Commit 38e531f

Browse files
rojiCopilot
andcommitted
Fix single-quote escaping in OBJECT_ID and dynamic SQL string literals
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. Added AppendTableNameInsideLiteral and AppendIdentifierInsideLiteral helpers 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 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1b2f318 commit 38e531f

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)