Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions dotnet/src/VectorData/SqlServer/SqlServerCommandBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ internal static List<SqlCommand> CreateTable(
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");
Expand Down Expand Up @@ -125,22 +125,22 @@ internal static List<SqlCommand> CreateTable(
// 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;");
}

Expand Down Expand Up @@ -897,6 +897,30 @@ internal static StringBuilder AppendIdentifier(this StringBuilder sb, string ide
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,36 @@ public void AppendTableName(string? schema, string table, string expectedFullNam
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
Expand Down Expand Up @@ -149,6 +179,34 @@ PRIMARY KEY ([id])
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()
{
Expand Down
Loading