diff --git a/dotnet/src/VectorData/SqlServer/SqlServerCommandBuilder.cs b/dotnet/src/VectorData/SqlServer/SqlServerCommandBuilder.cs
index eb5343681b..5ebca00522 100644
--- a/dotnet/src/VectorData/SqlServer/SqlServerCommandBuilder.cs
+++ b/dotnet/src/VectorData/SqlServer/SqlServerCommandBuilder.cs
@@ -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;
}
+ ///
+ /// Same as , but for use inside a SQL string literal (N'...'),
+ /// where single quotes must be escaped by doubling them.
+ ///
+ 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;
+ }
+
+ ///
+ /// Same as , but for use inside a SQL string literal (N'...'),
+ /// where single quotes must be escaped by doubling them.
+ ///
+ 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 properties,
string? prefix = null,
diff --git a/dotnet/test/VectorData/SqlServer.ConformanceTests/SqlServerCommandBuilderTests.cs b/dotnet/test/VectorData/SqlServer.ConformanceTests/SqlServerCommandBuilderTests.cs
index 40f5ddbc96..5ed41af974 100644
--- a/dotnet/test/VectorData/SqlServer.ConformanceTests/SqlServerCommandBuilderTests.cs
+++ b/dotnet/test/VectorData/SqlServer.ConformanceTests/SqlServerCommandBuilderTests.cs
@@ -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()
{