chore: import upstream snapshot with attribution
This commit is contained in:
+78
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.ModelTests;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.Xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests.ModelTests;
|
||||
|
||||
public class SqlServerBasicModelTests(SqlServerBasicModelTests.Fixture fixture)
|
||||
: BasicModelTests<string>(fixture), IClassFixture<SqlServerBasicModelTests.Fixture>
|
||||
{
|
||||
private const int SqlServerMaxParameters = 2_100;
|
||||
|
||||
[ConditionalFact]
|
||||
private async Task Split_batches_to_account_for_max_parameter_limit()
|
||||
{
|
||||
var collection = fixture.Collection;
|
||||
Record[] inserted = Enumerable.Range(0, SqlServerMaxParameters + 1).Select(i => new Record()
|
||||
{
|
||||
Key = fixture.GenerateNextKey<string>(),
|
||||
Number = 100 + i,
|
||||
Text = i.ToString(),
|
||||
Vector = Enumerable.Range(0, 3).Select(j => (float)(i + j)).ToArray()
|
||||
}).ToArray();
|
||||
var keys = inserted.Select(record => record.Key).ToArray();
|
||||
|
||||
Assert.Empty(await collection.GetAsync(keys).ToArrayAsync());
|
||||
await collection.UpsertAsync(inserted);
|
||||
|
||||
var received = await collection.GetAsync(keys).ToArrayAsync();
|
||||
foreach (var record in inserted)
|
||||
{
|
||||
record.AssertEqual(
|
||||
received.Single(r => r.Key.Equals(record.Key, StringComparison.Ordinal)),
|
||||
includeVectors: false,
|
||||
fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
await collection.DeleteAsync(keys);
|
||||
Assert.Empty(await collection.GetAsync(keys).ToArrayAsync());
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public async Task Upsert_batch_is_atomic()
|
||||
{
|
||||
var collection = fixture.Collection;
|
||||
Record[] inserted = Enumerable.Range(0, SqlServerMaxParameters + 1).Select(i => new Record()
|
||||
{
|
||||
// The last Key is set to NULL, so it must not be inserted and the whole batch should fail
|
||||
Key = i < SqlServerMaxParameters ? fixture.GenerateNextKey<string>() : null!,
|
||||
Number = 100 + i,
|
||||
Text = i.ToString(),
|
||||
Vector = Enumerable.Range(0, 3).Select(j => (float)(i + j)).ToArray()
|
||||
}).ToArray();
|
||||
|
||||
var keys = inserted.Select(record => record.Key).Where(key => key is not null).ToArray();
|
||||
Assert.Empty(await collection.GetAsync(keys).ToArrayAsync());
|
||||
|
||||
VectorStoreException ex = await Assert.ThrowsAsync<VectorStoreException>(() => collection.UpsertAsync(inserted));
|
||||
Assert.Equal("UpsertBatch", ex.OperationName);
|
||||
|
||||
var metadata = collection.GetService(typeof(VectorStoreCollectionMetadata)) as VectorStoreCollectionMetadata;
|
||||
|
||||
Assert.NotNull(metadata?.CollectionName);
|
||||
Assert.Equal(metadata.CollectionName, ex.CollectionName);
|
||||
|
||||
// Make sure that no records were inserted!
|
||||
Assert.Empty(await collection.GetAsync(keys).ToArrayAsync());
|
||||
}
|
||||
|
||||
public new class Fixture : BasicModelTests<string>.Fixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.ModelTests;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests.ModelTests;
|
||||
|
||||
public class SqlServerDynamicModelTests(SqlServerDynamicModelTests.Fixture fixture)
|
||||
: DynamicModelTests<string>(fixture), IClassFixture<SqlServerDynamicModelTests.Fixture>
|
||||
{
|
||||
public new class Fixture : DynamicModelTests<string>.Fixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.ModelTests;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests.ModelTests;
|
||||
|
||||
public class SqlServerMultiVectorModelTests(SqlServerMultiVectorModelTests.Fixture fixture)
|
||||
: MultiVectorModelTests<string>(fixture), IClassFixture<SqlServerMultiVectorModelTests.Fixture>
|
||||
{
|
||||
public new class Fixture : MultiVectorModelTests<string>.Fixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.ModelTests;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests.ModelTests;
|
||||
|
||||
public class SqlServerNoDataModelTests(SqlServerNoDataModelTests.Fixture fixture)
|
||||
: NoDataModelTests<string>(fixture), IClassFixture<SqlServerNoDataModelTests.Fixture>
|
||||
{
|
||||
public new class Fixture : NoDataModelTests<string>.Fixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.ModelTests;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests.ModelTests;
|
||||
|
||||
public class SqlServerNoVectorModelTests(SqlServerNoVectorModelTests.Fixture fixture)
|
||||
: NoVectorModelTests<string>(fixture), IClassFixture<SqlServerNoVectorModelTests.Fixture>
|
||||
{
|
||||
public new class Fixture : NoVectorModelTests<string>.Fixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
@@ -0,0 +1,52 @@
|
||||
# SQL Server Vector Store Conformance Tests
|
||||
|
||||
This project contains conformance tests for the SQL Server Vector Store implementation.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
By default, the tests will automatically use a testcontainer to spin up a SQL Server instance. Docker must be running on your machine for this to work.
|
||||
|
||||
### Using an External SQL Server Instance
|
||||
|
||||
If you want to run the tests against an external SQL Server instance (e.g., Azure SQL, a local SQL Server, or any other instance), you can provide a connection string through one of the following methods:
|
||||
|
||||
#### Option 1: Environment Variable
|
||||
|
||||
Set the `SqlServer__ConnectionString` environment variable:
|
||||
|
||||
```bash
|
||||
# Bash/Linux/macOS
|
||||
export SqlServer__ConnectionString="Server=myserver.database.windows.net;Database=mydb;User Id=myuser;Password=mypassword;"
|
||||
|
||||
# PowerShell
|
||||
$env:SqlServer__ConnectionString = "Server=myserver.database.windows.net;Database=mydb;User Id=myuser;Password=mypassword;"
|
||||
```
|
||||
|
||||
#### Option 2: Configuration File
|
||||
|
||||
Create a `testsettings.development.json` file in this directory with the following content:
|
||||
|
||||
```json
|
||||
{
|
||||
"SqlServer": {
|
||||
"ConnectionString": "Server=myserver.database.windows.net;Database=mydb;User Id=myuser;Password=mypassword;"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This file is git-ignored and safe for local development.
|
||||
|
||||
#### Option 3: User Secrets
|
||||
|
||||
```bash
|
||||
cd test/VectorData/SqlServer.ConformanceTests
|
||||
dotnet user-secrets set "SqlServer:ConnectionString" "Server=myserver.database.windows.net;Database=mydb;User Id=myuser;Password=mypassword;"
|
||||
```
|
||||
|
||||
## Benefits of Using an External Instance
|
||||
|
||||
Using an external SQL Server instance can be beneficial when:
|
||||
- You want to avoid the overhead of spinning up Docker containers
|
||||
- You need to test against Azure SQL specifically
|
||||
- You want faster test execution (no container startup time)
|
||||
- You're running tests in an environment where Docker is not available
|
||||
@@ -0,0 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0;net472</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>SqlServer.ConformanceTests</RootNamespace>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
|
||||
<NoWarn>$(NoWarn);CA2007,SKEXP0001,VSTHRD111;CS1685</NoWarn>
|
||||
<UserSecretsId>b7762d10-e29b-4bb1-8b74-b6d69a667dd4</UserSecretsId>
|
||||
|
||||
<NoWarn>$(NoWarn);MEVD9000,MEVD9001</NoWarn> <!-- Experimental MEVD connector-facing APIs -->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Testcontainers.MsSql" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\VectorData\SqlServer\SqlServer.csproj" />
|
||||
<ProjectReference Include="..\VectorData.ConformanceTests\VectorData.ConformanceTests.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests;
|
||||
|
||||
public class SqlServerCollectionManagementTests(SqlServerCollectionManagementTests.Fixture fixture)
|
||||
: CollectionManagementTests<string>(fixture), IClassFixture<SqlServerCollectionManagementTests.Fixture>
|
||||
{
|
||||
public class Fixture : VectorStoreFixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,743 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Data.SqlTypes;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using Microsoft.SemanticKernel.Connectors.SqlServer;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests;
|
||||
|
||||
public class SqlServerCommandBuilderTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("schema", "name", "[schema].[name]")]
|
||||
[InlineData(null, "name", "[name]")]
|
||||
[InlineData("schema", "[brackets]", "[schema].[[brackets]]]")]
|
||||
[InlineData(null, "[needsEscaping]", "[[needsEscaping]]]")]
|
||||
[InlineData("needs]escaping", "[brackets]", "[needs]]escaping].[[brackets]]]")]
|
||||
public void AppendTableName(string? schema, string table, string expectedFullName)
|
||||
{
|
||||
StringBuilder result = new();
|
||||
|
||||
SqlServerCommandBuilder.AppendTableName(result, schema, table);
|
||||
|
||||
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
|
||||
[InlineData("123", "@_")] // starts with a digit, also not allowed
|
||||
[InlineData("ĄŻŚĆ_doesNotStartWithAscii", "@_")] // starts with a non-ASCII character
|
||||
public void AppendParameterName(string propertyName, string expectedPrefix)
|
||||
{
|
||||
StringBuilder builder = new();
|
||||
StringBuilder expectedBuilder = new();
|
||||
KeyPropertyModel keyProperty = new(propertyName, typeof(string));
|
||||
|
||||
int paramIndex = 0; // we need a dedicated variable to ensure that AppendParameterName increments the index
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
Assert.Equal(paramIndex, i);
|
||||
SqlServerCommandBuilder.AppendParameterName(builder, keyProperty, ref paramIndex, out string parameterName);
|
||||
Assert.Equal($"{expectedPrefix}{i}", parameterName);
|
||||
expectedBuilder.Append(parameterName);
|
||||
}
|
||||
|
||||
Assert.Equal(expectedBuilder.ToString(), builder.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("schema", "simpleName", "[simpleName]")]
|
||||
[InlineData("schema", "[needsEscaping]", "[[needsEscaping]]]")]
|
||||
public void DropTable(string schema, string table, string expectedTable)
|
||||
{
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
using SqlCommand command = SqlServerCommandBuilder.DropTableIfExists(connection, schema, table);
|
||||
|
||||
Assert.Equal($"DROP TABLE IF EXISTS [{schema}].{expectedTable}", command.CommandText);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("schema", "simpleName")]
|
||||
[InlineData("schema", "[needsEscaping]")]
|
||||
public void SelectTableName(string schema, string table)
|
||||
{
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
using SqlCommand command = SqlServerCommandBuilder.SelectTableName(connection, schema, table);
|
||||
|
||||
Assert.Equal(
|
||||
"""
|
||||
SELECT TABLE_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_TYPE = 'BASE TABLE'
|
||||
AND (@schema is NULL or TABLE_SCHEMA = @schema)
|
||||
AND TABLE_NAME = @tableName
|
||||
"""
|
||||
, command.CommandText);
|
||||
Assert.Equal(schema, command.Parameters[0].Value);
|
||||
Assert.Equal(table, command.Parameters[1].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectTableNames()
|
||||
{
|
||||
const string SchemaName = "theSchemaName";
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
using SqlCommand command = SqlServerCommandBuilder.SelectTableNames(connection, SchemaName);
|
||||
|
||||
Assert.Equal(
|
||||
"""
|
||||
SELECT TABLE_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_TYPE = 'BASE TABLE'
|
||||
AND (@schema is NULL or TABLE_SCHEMA = @schema)
|
||||
"""
|
||||
, command.CommandText);
|
||||
Assert.Equal(SchemaName, command.Parameters[0].Value);
|
||||
Assert.Equal("@schema", command.Parameters[0].ParameterName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void CreateTable(bool ifNotExists)
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("simpleName", typeof(string)),
|
||||
new VectorStoreDataProperty("with space", typeof(int)) { IsIndexed = true },
|
||||
new VectorStoreDataProperty("nullableInt", typeof(int?)),
|
||||
new VectorStoreDataProperty("flag", typeof(bool)),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 10),
|
||||
new VectorStoreVectorProperty("nullableEmbedding", typeof(ReadOnlyMemory<float>?), 10)
|
||||
]);
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
var commands = SqlServerCommandBuilder.CreateTable(connection, "schema", "table", ifNotExists, model);
|
||||
|
||||
var command = Assert.Single(commands);
|
||||
string expectedCommand =
|
||||
"""
|
||||
BEGIN
|
||||
CREATE TABLE [schema].[table] (
|
||||
[id] BIGINT IDENTITY,
|
||||
[simpleName] NVARCHAR(MAX),
|
||||
[with space] INT NOT NULL,
|
||||
[nullableInt] INT,
|
||||
[flag] BIT NOT NULL,
|
||||
[embedding] VECTOR(10) NOT NULL,
|
||||
[nullableEmbedding] VECTOR(10),
|
||||
PRIMARY KEY ([id])
|
||||
);
|
||||
CREATE INDEX index_table_withspace ON [schema].[table]([with space]);
|
||||
END;
|
||||
""";
|
||||
if (ifNotExists)
|
||||
{
|
||||
expectedCommand = "IF OBJECT_ID(N'[schema].[table]', N'U') IS NULL" + Environment.NewLine + expectedCommand;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
{
|
||||
IndexKind = IndexKind.DiskAnn,
|
||||
DistanceFunction = DistanceFunction.CosineDistance
|
||||
}
|
||||
]);
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
var commands = SqlServerCommandBuilder.CreateTable(connection, "schema", "table", ifNotExists: false, model);
|
||||
|
||||
Assert.Equal(3, commands.Count);
|
||||
Assert.Equal(
|
||||
"""
|
||||
BEGIN
|
||||
CREATE TABLE [schema].[table] (
|
||||
[id] BIGINT IDENTITY,
|
||||
[name] NVARCHAR(MAX),
|
||||
[embedding] VECTOR(10) NOT NULL,
|
||||
PRIMARY KEY ([id])
|
||||
);
|
||||
END;
|
||||
""", commands[0].CommandText, ignoreLineEndingDifferences: true);
|
||||
Assert.Equal("ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES = ON;", commands[1].CommandText);
|
||||
Assert.Equal(
|
||||
"""
|
||||
CREATE VECTOR INDEX index_table_embedding ON [schema].[table]([embedding]) WITH (METRIC = 'COSINE', TYPE = 'DISKANN');
|
||||
|
||||
""", commands[2].CommandText, ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateTable_WithDiskAnnIndex_EuclideanDistance()
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
{
|
||||
IndexKind = IndexKind.DiskAnn,
|
||||
DistanceFunction = DistanceFunction.EuclideanDistance
|
||||
}
|
||||
]);
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
var commands = SqlServerCommandBuilder.CreateTable(connection, "schema", "table", ifNotExists: false, model);
|
||||
|
||||
Assert.Equal(3, commands.Count);
|
||||
Assert.Equal(
|
||||
"""
|
||||
BEGIN
|
||||
CREATE TABLE [schema].[table] (
|
||||
[id] BIGINT IDENTITY,
|
||||
[embedding] VECTOR(10) NOT NULL,
|
||||
PRIMARY KEY ([id])
|
||||
);
|
||||
END;
|
||||
""", commands[0].CommandText, ignoreLineEndingDifferences: true);
|
||||
Assert.Equal("ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES = ON;", commands[1].CommandText);
|
||||
Assert.Equal(
|
||||
"""
|
||||
CREATE VECTOR INDEX index_table_embedding ON [schema].[table]([embedding]) WITH (METRIC = 'EUCLIDEAN', TYPE = 'DISKANN');
|
||||
|
||||
""", commands[2].CommandText, ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateTable_WithUnsupportedIndexKind_Throws()
|
||||
{
|
||||
Assert.Throws<NotSupportedException>(() =>
|
||||
BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
{
|
||||
IndexKind = IndexKind.Hnsw
|
||||
}
|
||||
]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectVector_WithDiskAnnIndex()
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 3)
|
||||
{
|
||||
IndexKind = IndexKind.DiskAnn,
|
||||
DistanceFunction = DistanceFunction.CosineDistance
|
||||
}
|
||||
]);
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
var options = new VectorSearchOptions<Dictionary<string, object?>> { IncludeVectors = true };
|
||||
using SqlCommand command = SqlServerCommandBuilder.SelectVector(
|
||||
connection, "schema", "table",
|
||||
model.VectorProperties[0], model,
|
||||
top: 5, options,
|
||||
new SqlVector<float>(new float[] { 1f, 2f, 3f }));
|
||||
|
||||
Assert.Equal(
|
||||
"""
|
||||
SELECT TOP(5) WITH APPROXIMATE t.[id],t.[name],t.[embedding],
|
||||
s.[distance] AS [score]
|
||||
FROM VECTOR_SEARCH(TABLE = [schema].[table] AS t, COLUMN = [embedding], SIMILAR_TO = @vector, METRIC = 'COSINE') AS s
|
||||
ORDER BY [score] ASC
|
||||
""", command.CommandText, ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectVector_WithDiskAnnIndex_WithSkip()
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 3)
|
||||
{
|
||||
IndexKind = IndexKind.DiskAnn,
|
||||
DistanceFunction = DistanceFunction.CosineDistance
|
||||
}
|
||||
]);
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
var options = new VectorSearchOptions<Dictionary<string, object?>> { IncludeVectors = false, Skip = 3 };
|
||||
using SqlCommand command = SqlServerCommandBuilder.SelectVector(
|
||||
connection, "schema", "table",
|
||||
model.VectorProperties[0], model,
|
||||
top: 5, options,
|
||||
new SqlVector<float>(new float[] { 1f, 2f, 3f }));
|
||||
|
||||
Assert.Equal(
|
||||
"""
|
||||
SELECT * FROM (SELECT TOP(8) WITH APPROXIMATE t.[id],t.[name],
|
||||
s.[distance] AS [score]
|
||||
FROM VECTOR_SEARCH(TABLE = [schema].[table] AS t, COLUMN = [embedding], SIMILAR_TO = @vector, METRIC = 'COSINE') AS s
|
||||
ORDER BY [score] ASC
|
||||
) AS [inner]
|
||||
ORDER BY [score] ASC
|
||||
OFFSET 3 ROWS FETCH NEXT 5 ROWS ONLY;
|
||||
""", command.CommandText, ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectVector_WithDiskAnnIndex_WithFilter()
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 3)
|
||||
{
|
||||
IndexKind = IndexKind.DiskAnn,
|
||||
DistanceFunction = DistanceFunction.CosineDistance
|
||||
}
|
||||
]);
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
var options = new VectorSearchOptions<Dictionary<string, object?>>
|
||||
{
|
||||
Filter = d => (string)d["name"]! == "test"
|
||||
};
|
||||
|
||||
using SqlCommand command = SqlServerCommandBuilder.SelectVector(
|
||||
connection, "schema", "table",
|
||||
model.VectorProperties[0], model,
|
||||
top: 5, options,
|
||||
new SqlVector<float>(new float[] { 1f, 2f, 3f }));
|
||||
|
||||
Assert.Equal(
|
||||
"""
|
||||
SELECT TOP(5) WITH APPROXIMATE t.[id],t.[name],
|
||||
s.[distance] AS [score]
|
||||
FROM VECTOR_SEARCH(TABLE = [schema].[table] AS t, COLUMN = [embedding], SIMILAR_TO = @vector, METRIC = 'COSINE') AS s
|
||||
WHERE (t.[name] = 'test')
|
||||
ORDER BY [score] ASC
|
||||
""", command.CommandText, ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectVector_WithDiskAnnIndex_WithScoreThreshold()
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 3)
|
||||
{
|
||||
IndexKind = IndexKind.DiskAnn,
|
||||
DistanceFunction = DistanceFunction.CosineDistance
|
||||
}
|
||||
]);
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
var options = new VectorSearchOptions<Dictionary<string, object?>>
|
||||
{
|
||||
IncludeVectors = true,
|
||||
ScoreThreshold = 0.5f
|
||||
};
|
||||
using SqlCommand command = SqlServerCommandBuilder.SelectVector(
|
||||
connection, "schema", "table",
|
||||
model.VectorProperties[0], model,
|
||||
top: 5, options,
|
||||
new SqlVector<float>(new float[] { 1f, 2f, 3f }));
|
||||
|
||||
Assert.Equal(
|
||||
"""
|
||||
SELECT TOP(5) WITH APPROXIMATE t.[id],t.[name],t.[embedding],
|
||||
s.[distance] AS [score]
|
||||
FROM VECTOR_SEARCH(TABLE = [schema].[table] AS t, COLUMN = [embedding], SIMILAR_TO = @vector, METRIC = 'COSINE') AS s
|
||||
WHERE s.[distance] <= @scoreThreshold
|
||||
ORDER BY [score] ASC
|
||||
""", command.CommandText, ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectVector_WithDiskAnnIndex_WithFilterAndScoreThreshold()
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 3)
|
||||
{
|
||||
IndexKind = IndexKind.DiskAnn,
|
||||
DistanceFunction = DistanceFunction.CosineDistance
|
||||
}
|
||||
]);
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
var options = new VectorSearchOptions<Dictionary<string, object?>>
|
||||
{
|
||||
Filter = d => (string)d["name"]! == "test",
|
||||
ScoreThreshold = 0.5f
|
||||
};
|
||||
|
||||
using SqlCommand command = SqlServerCommandBuilder.SelectVector(
|
||||
connection, "schema", "table",
|
||||
model.VectorProperties[0], model,
|
||||
top: 5, options,
|
||||
new SqlVector<float>(new float[] { 1f, 2f, 3f }));
|
||||
|
||||
Assert.Equal(
|
||||
"""
|
||||
SELECT TOP(5) WITH APPROXIMATE t.[id],t.[name],
|
||||
s.[distance] AS [score]
|
||||
FROM VECTOR_SEARCH(TABLE = [schema].[table] AS t, COLUMN = [embedding], SIMILAR_TO = @vector, METRIC = 'COSINE') AS s
|
||||
WHERE (t.[name] = 'test')
|
||||
AND s.[distance] <= @scoreThreshold
|
||||
ORDER BY [score] ASC
|
||||
""", command.CommandText, ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectHybrid_WithDiskAnnIndex_WithFilter()
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)) { IsFullTextIndexed = true },
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 3)
|
||||
{
|
||||
IndexKind = IndexKind.DiskAnn,
|
||||
DistanceFunction = DistanceFunction.CosineDistance
|
||||
}
|
||||
]);
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
var options = new HybridSearchOptions<Dictionary<string, object?>>
|
||||
{
|
||||
Filter = d => (string)d["name"]! == "test"
|
||||
};
|
||||
|
||||
using SqlCommand command = SqlServerCommandBuilder.SelectHybrid(
|
||||
connection, "schema", "table",
|
||||
model.VectorProperties[0], model.DataProperties.First(p => p.IsFullTextIndexed), model,
|
||||
top: 5, options,
|
||||
new SqlVector<float>(new float[] { 1f, 2f, 3f }),
|
||||
"keyword");
|
||||
|
||||
Assert.Contains("SELECT TOP(@candidateCount) WITH APPROXIMATE", command.CommandText);
|
||||
Assert.Contains("WHERE (t.[name] = 'test')", command.CommandText);
|
||||
Assert.Contains("VECTOR_SEARCH(TABLE =", command.CommandText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Upsert()
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)) { IsAutoGenerated = false },
|
||||
new VectorStoreDataProperty("simpleString", typeof(string)),
|
||||
new VectorStoreDataProperty("simpleInt", typeof(int)),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
]);
|
||||
|
||||
Dictionary<string, object?>[] records =
|
||||
[
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
{ "id", 0L },
|
||||
{ "simpleString", "nameValue0" },
|
||||
{ "simpleInt", 134 },
|
||||
{ "embedding", new ReadOnlyMemory<float>([10.0f]) }
|
||||
},
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
{ "id", 1L },
|
||||
{ "simpleString", "nameValue1" },
|
||||
{ "simpleInt", 135 },
|
||||
{ "embedding", new ReadOnlyMemory<float>([11.0f]) }
|
||||
}
|
||||
];
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
using SqlCommand command = connection.CreateCommand();
|
||||
|
||||
Assert.True(SqlServerCommandBuilder.Upsert<long>(command, "schema", "table", model, records, firstRecordIndex: 0, generatedEmbeddings: null));
|
||||
|
||||
string expectedCommand =
|
||||
""""
|
||||
MERGE INTO [schema].[table] AS t
|
||||
USING (VALUES (@id_0,@simpleString_1,@simpleInt_2,@embedding_3)) AS s ([id],[simpleString],[simpleInt],[embedding])
|
||||
ON (t.[id] = s.[id])
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET t.[simpleString] = s.[simpleString],t.[simpleInt] = s.[simpleInt],t.[embedding] = s.[embedding]
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT ([id],[simpleString],[simpleInt],[embedding])
|
||||
VALUES (s.[id],s.[simpleString],s.[simpleInt],s.[embedding])
|
||||
OUTPUT inserted.[id];
|
||||
|
||||
MERGE INTO [schema].[table] AS t
|
||||
USING (VALUES (@id_4,@simpleString_5,@simpleInt_6,@embedding_7)) AS s ([id],[simpleString],[simpleInt],[embedding])
|
||||
ON (t.[id] = s.[id])
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET t.[simpleString] = s.[simpleString],t.[simpleInt] = s.[simpleInt],t.[embedding] = s.[embedding]
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT ([id],[simpleString],[simpleInt],[embedding])
|
||||
VALUES (s.[id],s.[simpleString],s.[simpleInt],s.[embedding])
|
||||
OUTPUT inserted.[id];
|
||||
|
||||
|
||||
"""";
|
||||
|
||||
Assert.Equal(expectedCommand, command.CommandText, ignoreLineEndingDifferences: true);
|
||||
|
||||
for (int i = 0; i < records.Length; i++)
|
||||
{
|
||||
Assert.Equal($"@id_{4 * i + 0}", command.Parameters[4 * i + 0].ParameterName);
|
||||
Assert.Equal((long)i, command.Parameters[4 * i + 0].Value);
|
||||
Assert.Equal($"@simpleString_{4 * i + 1}", command.Parameters[4 * i + 1].ParameterName);
|
||||
Assert.Equal($"nameValue{i}", command.Parameters[4 * i + 1].Value);
|
||||
Assert.Equal($"@simpleInt_{4 * i + 2}", command.Parameters[4 * i + 2].ParameterName);
|
||||
Assert.Equal(134 + i, command.Parameters[4 * i + 2].Value);
|
||||
Assert.Equal($"@embedding_{4 * i + 3}", command.Parameters[4 * i + 3].ParameterName);
|
||||
var vector = Assert.IsType<SqlVector<float>>(command.Parameters[4 * i + 3].Value);
|
||||
Assert.Equal([10 + i], vector.Memory.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteSingle()
|
||||
{
|
||||
KeyPropertyModel keyProperty = new("id", typeof(long));
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
using SqlCommand command = SqlServerCommandBuilder.DeleteSingle(connection,
|
||||
"schema", "tableName", keyProperty, 123L);
|
||||
|
||||
Assert.Equal("DELETE FROM [schema].[tableName] WHERE [id] = @id_0", command.CommandText);
|
||||
Assert.Equal(123L, command.Parameters[0].Value);
|
||||
Assert.Equal("@id_0", command.Parameters[0].ParameterName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteMany()
|
||||
{
|
||||
string[] keys = ["key1", "key2"];
|
||||
KeyPropertyModel keyProperty = new("id", typeof(string));
|
||||
using SqlConnection connection = CreateConnection();
|
||||
using SqlCommand command = connection.CreateCommand();
|
||||
|
||||
Assert.True(SqlServerCommandBuilder.DeleteMany(command, "schema", "tableName", keyProperty, keys));
|
||||
|
||||
Assert.Equal("DELETE FROM [schema].[tableName] WHERE [id] IN (@id_0,@id_1)", command.CommandText);
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
Assert.Equal(keys[i], command.Parameters[i].Value);
|
||||
Assert.Equal($"@id_{i}", command.Parameters[i].ParameterName);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectSingle()
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreDataProperty("age", typeof(int)),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
]);
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
using SqlCommand command = SqlServerCommandBuilder.SelectSingle(connection, "schema", "tableName", model, 123L, includeVectors: true);
|
||||
|
||||
Assert.Equal(
|
||||
"""""
|
||||
SELECT [id],[name],[age],[embedding]
|
||||
FROM [schema].[tableName]
|
||||
WHERE [id] = @id_0
|
||||
""""", command.CommandText, ignoreLineEndingDifferences: true);
|
||||
Assert.Equal(123L, command.Parameters[0].Value);
|
||||
Assert.Equal("@id_0", command.Parameters[0].ParameterName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectMany()
|
||||
{
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreDataProperty("age", typeof(int)),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
]);
|
||||
|
||||
long[] keys = [123L, 456L, 789L];
|
||||
using SqlConnection connection = CreateConnection();
|
||||
using SqlCommand command = connection.CreateCommand();
|
||||
|
||||
Assert.True(SqlServerCommandBuilder.SelectMany(command,
|
||||
"schema", "tableName", model, keys, includeVectors: true));
|
||||
|
||||
Assert.Equal(
|
||||
"""""
|
||||
SELECT [id],[name],[age],[embedding]
|
||||
FROM [schema].[tableName]
|
||||
WHERE [id] IN (@id_0,@id_1,@id_2)
|
||||
""""", command.CommandText, ignoreLineEndingDifferences: true);
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
Assert.Equal(keys[i], command.Parameters[i].Value);
|
||||
Assert.Equal($"@id_{i}", command.Parameters[i].ParameterName);
|
||||
}
|
||||
}
|
||||
|
||||
// We create a connection using a fake connection string just to be able to create the SqlCommand.
|
||||
private static SqlConnection CreateConnection()
|
||||
=> new("Server=localhost;Database=master;Integrated Security=True;");
|
||||
|
||||
private static CollectionModel BuildModel(List<VectorStoreProperty> properties)
|
||||
=> new SqlServerModelBuilder()
|
||||
.BuildDynamic(new() { Properties = properties }, defaultEmbeddingGenerator: null);
|
||||
|
||||
#if NET // NRT detection via NullabilityInfoContext is only available on .NET 6+
|
||||
[Fact]
|
||||
public void CreateTable_WithNrtAnnotations()
|
||||
{
|
||||
var model = new SqlServerModelBuilder().Build(
|
||||
typeof(NrtTestRecord),
|
||||
typeof(long),
|
||||
definition: null,
|
||||
defaultEmbeddingGenerator: null);
|
||||
|
||||
using SqlConnection connection = CreateConnection();
|
||||
|
||||
var commands = SqlServerCommandBuilder.CreateTable(connection, "schema", "table", ifNotExists: false, model);
|
||||
|
||||
var command = Assert.Single(commands);
|
||||
|
||||
Assert.Equal(
|
||||
"""
|
||||
BEGIN
|
||||
CREATE TABLE [schema].[table] (
|
||||
[Id] BIGINT IDENTITY,
|
||||
[NonNullableString] NVARCHAR(MAX) NOT NULL,
|
||||
[NullableString] NVARCHAR(MAX),
|
||||
[NonNullableInt] INT NOT NULL,
|
||||
[NullableInt] INT,
|
||||
[NonNullableBool] BIT NOT NULL,
|
||||
[Embedding] VECTOR(10) NOT NULL,
|
||||
PRIMARY KEY ([Id])
|
||||
);
|
||||
END;
|
||||
""", command.CommandText, ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
|
||||
#pragma warning disable CA1812 // Class is used via reflection
|
||||
private sealed class NrtTestRecord
|
||||
{
|
||||
[VectorStoreKey]
|
||||
public long Id { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public string NonNullableString { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public string? NullableString { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public int NonNullableInt { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public int? NullableInt { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public bool NonNullableBool { get; set; }
|
||||
|
||||
[VectorStoreVector(10)]
|
||||
public ReadOnlyMemory<float> Embedding { get; set; }
|
||||
}
|
||||
#pragma warning restore CA1812
|
||||
#pragma warning restore CS8618
|
||||
#endif
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.SemanticKernel.Connectors.SqlServer;
|
||||
using VectorData.ConformanceTests;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests;
|
||||
|
||||
public class SqlServerDependencyInjectionTests
|
||||
: DependencyInjectionTests<SqlServerVectorStore, SqlServerCollection<string, DependencyInjectionTests<string>.Record>, string, DependencyInjectionTests<string>.Record>
|
||||
{
|
||||
protected const string ConnectionString = "Server=localhost;Database=master;Integrated Security=True;";
|
||||
|
||||
protected override void PopulateConfiguration(ConfigurationManager configuration, object? serviceKey = null)
|
||||
=> configuration.AddInMemoryCollection(
|
||||
[
|
||||
new(CreateConfigKey("SqlServer", serviceKey, "ConnectionString"), ConnectionString),
|
||||
]);
|
||||
|
||||
private static string ConnectionStringProvider(IServiceProvider sp)
|
||||
=> sp.GetRequiredService<IConfiguration>().GetRequiredSection("SqlServer:ConnectionString").Value!;
|
||||
|
||||
private static string ConnectionStringProvider(IServiceProvider sp, object serviceKey)
|
||||
=> sp.GetRequiredService<IConfiguration>().GetRequiredSection(CreateConfigKey("SqlServer", serviceKey, "ConnectionString")).Value!;
|
||||
|
||||
public override IEnumerable<Func<IServiceCollection, object?, string, ServiceLifetime, IServiceCollection>> CollectionDelegates
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return (services, serviceKey, name, lifetime) => serviceKey is null
|
||||
? services.AddSqlServerCollection<string, Record>(
|
||||
name, connectionString: ConnectionString, lifetime: lifetime)
|
||||
: services.AddKeyedSqlServerCollection<string, Record>(
|
||||
serviceKey, name, connectionString: ConnectionString, lifetime: lifetime);
|
||||
|
||||
yield return (services, serviceKey, name, lifetime) => serviceKey is null
|
||||
? services.AddSqlServerCollection<string, Record>(
|
||||
name, ConnectionStringProvider, lifetime: lifetime)
|
||||
: services.AddKeyedSqlServerCollection<string, Record>(
|
||||
serviceKey, name, sp => ConnectionStringProvider(sp, serviceKey), lifetime: lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<Func<IServiceCollection, object?, ServiceLifetime, IServiceCollection>> StoreDelegates
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return (services, serviceKey, lifetime) => serviceKey is null
|
||||
? services.AddSqlServerVectorStore(
|
||||
ConnectionStringProvider, lifetime: lifetime)
|
||||
: services.AddKeyedSqlServerVectorStore(
|
||||
serviceKey, sp => ConnectionStringProvider(sp, serviceKey), lifetime: lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectionStringProviderCantBeNull()
|
||||
{
|
||||
IServiceCollection services = new ServiceCollection();
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => services.AddSqlServerVectorStore(connectionStringProvider: null!));
|
||||
Assert.Throws<ArgumentNullException>(() => services.AddKeyedSqlServerVectorStore(serviceKey: "notNull", connectionStringProvider: null!));
|
||||
Assert.Throws<ArgumentNullException>(() => services.AddSqlServerCollection<string, Record>(name: "notNull", connectionStringProvider: null!));
|
||||
Assert.Throws<ArgumentNullException>(() => services.AddKeyedSqlServerCollection<string, Record>(serviceKey: "notNull", name: "notNull", connectionStringProvider: null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectionStringCantBeNullOrEmpty()
|
||||
{
|
||||
IServiceCollection services = new ServiceCollection();
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => services.AddSqlServerCollection<string, Record>(
|
||||
name: "notNull", connectionString: null!));
|
||||
Assert.Throws<ArgumentException>(() => services.AddSqlServerCollection<string, Record>(
|
||||
name: "notNull", connectionString: ""));
|
||||
Assert.Throws<ArgumentNullException>(() => services.AddKeyedSqlServerCollection<string, Record>(
|
||||
serviceKey: "notNull", name: "notNull", connectionString: null!));
|
||||
Assert.Throws<ArgumentException>(() => services.AddKeyedSqlServerCollection<string, Record>(
|
||||
serviceKey: "notNull", name: "notNull", connectionString: ""));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests;
|
||||
|
||||
public class SqlServerDistanceFunctionTests(SqlServerDistanceFunctionTests.Fixture fixture)
|
||||
: DistanceFunctionTests<int>(fixture), IClassFixture<SqlServerDistanceFunctionTests.Fixture>
|
||||
{
|
||||
public override Task CosineSimilarity() => Assert.ThrowsAsync<NotSupportedException>(base.CosineSimilarity);
|
||||
public override Task DotProductSimilarity() => Assert.ThrowsAsync<NotSupportedException>(base.DotProductSimilarity);
|
||||
public override Task EuclideanSquaredDistance() => Assert.ThrowsAsync<NotSupportedException>(base.EuclideanSquaredDistance);
|
||||
public override Task HammingDistance() => Assert.ThrowsAsync<NotSupportedException>(base.HammingDistance);
|
||||
public override Task ManhattanDistance() => Assert.ThrowsAsync<NotSupportedException>(base.ManhattanDistance);
|
||||
|
||||
public new class Fixture() : DistanceFunctionTests<int>.Fixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests;
|
||||
|
||||
public class SqlServerEmbeddingGenerationTests(SqlServerEmbeddingGenerationTests.StringVectorFixture stringVectorFixture, SqlServerEmbeddingGenerationTests.RomOfFloatVectorFixture romOfFloatVectorFixture)
|
||||
: EmbeddingGenerationTests<int>(stringVectorFixture, romOfFloatVectorFixture), IClassFixture<SqlServerEmbeddingGenerationTests.StringVectorFixture>, IClassFixture<SqlServerEmbeddingGenerationTests.RomOfFloatVectorFixture>
|
||||
{
|
||||
public new class StringVectorFixture : EmbeddingGenerationTests<int>.StringVectorFixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
|
||||
public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator)
|
||||
=> SqlServerTestStore.Instance.GetVectorStore(new() { EmbeddingGenerator = embeddingGenerator });
|
||||
|
||||
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionStoreRegistrationDelegates =>
|
||||
[
|
||||
services => services.AddSqlServerVectorStore(sp => SqlServerTestStore.Instance.ConnectionString)
|
||||
];
|
||||
|
||||
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionCollectionRegistrationDelegates =>
|
||||
[
|
||||
services => services.AddSqlServerCollection<int, RecordWithAttributes>(this.CollectionName, sp => SqlServerTestStore.Instance.ConnectionString),
|
||||
services => services.AddSqlServerCollection<int, RecordWithAttributes>(this.CollectionName, SqlServerTestStore.Instance.ConnectionString),
|
||||
];
|
||||
}
|
||||
|
||||
public new class RomOfFloatVectorFixture : EmbeddingGenerationTests<int>.RomOfFloatVectorFixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
|
||||
public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator)
|
||||
=> SqlServerTestStore.Instance.GetVectorStore(new() { EmbeddingGenerator = embeddingGenerator });
|
||||
|
||||
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionStoreRegistrationDelegates =>
|
||||
[
|
||||
services => services.AddSqlServerVectorStore(sp => SqlServerTestStore.Instance.ConnectionString)
|
||||
];
|
||||
|
||||
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionCollectionRegistrationDelegates =>
|
||||
[
|
||||
services => services.AddSqlServerCollection<int, RecordWithAttributes>(this.CollectionName, sp => SqlServerTestStore.Instance.ConnectionString),
|
||||
services => services.AddSqlServerCollection<int, RecordWithAttributes>(this.CollectionName, SqlServerTestStore.Instance.ConnectionString),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace SqlServer.ConformanceTests;
|
||||
|
||||
#pragma warning disable CS0252 // Possible unintended reference comparison; left hand side needs cast
|
||||
|
||||
public class SqlServerFilterTests(SqlServerFilterTests.Fixture fixture)
|
||||
: FilterTests<int>(fixture), IClassFixture<SqlServerFilterTests.Fixture>
|
||||
{
|
||||
public override async Task Not_over_Or()
|
||||
{
|
||||
// Test sends: WHERE (NOT (("Int" = 8) OR ("String" = 'foo')))
|
||||
// There's a NULL string in the database, and relational null semantics in conjunction with negation makes the default implementation fail.
|
||||
await Assert.ThrowsAsync<FailException>(() => base.Not_over_Or());
|
||||
|
||||
// Compensate by adding a null check:
|
||||
await this.TestFilterAsync(
|
||||
r => r.String != null && !(r.Int == 8 || r.String == "foo"),
|
||||
r => r["String"] != null && !((int)r["Int"]! == 8 || r["String"] == "foo"));
|
||||
}
|
||||
|
||||
public override async Task NotEqual_with_string()
|
||||
{
|
||||
// As above, null semantics + negation
|
||||
await Assert.ThrowsAsync<FailException>(() => base.NotEqual_with_string());
|
||||
|
||||
await this.TestFilterAsync(
|
||||
r => r.String != null && r.String != "foo",
|
||||
r => r["String"] != null && r["String"] != "foo");
|
||||
}
|
||||
|
||||
public new class Fixture : FilterTests<int>.Fixture
|
||||
{
|
||||
private static readonly string s_uniqueName = Guid.NewGuid().ToString();
|
||||
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
|
||||
protected override string CollectionNameBase => s_uniqueName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests;
|
||||
|
||||
public class SqlServerHybridSearchTests(
|
||||
SqlServerHybridSearchTests.VectorAndStringFixture vectorAndStringFixture,
|
||||
SqlServerHybridSearchTests.MultiTextFixture multiTextFixture)
|
||||
: HybridSearchTests<int>(vectorAndStringFixture, multiTextFixture),
|
||||
IClassFixture<SqlServerHybridSearchTests.VectorAndStringFixture>,
|
||||
IClassFixture<SqlServerHybridSearchTests.MultiTextFixture>
|
||||
{
|
||||
public new class VectorAndStringFixture : HybridSearchTests<int>.VectorAndStringFixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
|
||||
public new class MultiTextFixture : HybridSearchTests<int>.MultiTextFixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.Xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests;
|
||||
|
||||
public class SqlServerIndexKindTests(SqlServerIndexKindTests.Fixture fixture)
|
||||
: IndexKindTests<int>(fixture), IClassFixture<SqlServerIndexKindTests.Fixture>
|
||||
{
|
||||
// Latest version vector indexes are only available in Azure SQL, not in on-prem SQL Server.
|
||||
// They also require at least 100 rows before the vector index can be created,
|
||||
// so we override the test to insert data first, then create the index.
|
||||
[ConditionalFact]
|
||||
[AzureSqlRequired]
|
||||
public virtual async Task DiskAnn()
|
||||
{
|
||||
const string CollectionName = "IndexKindTests_DiskAnn";
|
||||
|
||||
// Step 1: Create the table using Flat index (no vector index) so we can insert data.
|
||||
VectorStoreCollectionDefinition flatDefinition = new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty(nameof(SearchRecord.Key), typeof(int)),
|
||||
new VectorStoreDataProperty(nameof(SearchRecord.Int), typeof(int)),
|
||||
new VectorStoreVectorProperty(nameof(SearchRecord.Vector), typeof(ReadOnlyMemory<float>), dimensions: 3)
|
||||
{
|
||||
IndexKind = IndexKind.Flat,
|
||||
DistanceFunction = DistanceFunction.CosineDistance
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
using var flatCollection = fixture.TestStore.CreateCollection<int, SearchRecord>(CollectionName, flatDefinition);
|
||||
await flatCollection.EnsureCollectionDeletedAsync();
|
||||
await flatCollection.EnsureCollectionExistsAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// Step 2: Insert the 3 test rows + 97 filler rows to meet the 100-row minimum.
|
||||
SearchRecord[] testRecords =
|
||||
[
|
||||
new() { Key = 1, Int = 1, Vector = new([1, 2, 3]) },
|
||||
new() { Key = 2, Int = 2, Vector = new([10, 30, 50]) },
|
||||
new() { Key = 3, Int = 3, Vector = new([100, 40, 70]) }
|
||||
];
|
||||
|
||||
await flatCollection.UpsertAsync(testRecords);
|
||||
|
||||
var fillerRecords = Enumerable.Range(100, 97)
|
||||
.Select(i => new SearchRecord
|
||||
{
|
||||
Key = i,
|
||||
Int = i,
|
||||
Vector = new([i * 0.1f, i * 0.2f, i * 0.3f])
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
await flatCollection.UpsertAsync(fillerRecords);
|
||||
|
||||
// Step 3: Create the DiskANN vector index via raw SQL now that data is in the table.
|
||||
using var connection = new SqlConnection(SqlServerTestStore.Instance.ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
using (var createIndex = new SqlCommand(
|
||||
$"CREATE VECTOR INDEX index_{CollectionName}_Vector ON [{CollectionName}]([Vector]) WITH (METRIC = 'COSINE', TYPE = 'DISKANN');",
|
||||
connection))
|
||||
{
|
||||
await createIndex.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// Step 4: Create a new collection instance with DiskAnn to route searches through VECTOR_SEARCH().
|
||||
VectorStoreCollectionDefinition diskAnnDefinition = new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty(nameof(SearchRecord.Key), typeof(int)),
|
||||
new VectorStoreDataProperty(nameof(SearchRecord.Int), typeof(int)),
|
||||
new VectorStoreVectorProperty(nameof(SearchRecord.Vector), typeof(ReadOnlyMemory<float>), dimensions: 3)
|
||||
{
|
||||
IndexKind = IndexKind.DiskAnn,
|
||||
DistanceFunction = DistanceFunction.CosineDistance
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
using var diskAnnCollection = fixture.TestStore.CreateCollection<int, SearchRecord>(CollectionName, diskAnnDefinition);
|
||||
|
||||
var result = await diskAnnCollection.SearchAsync(new ReadOnlyMemory<float>([10, 30, 50]), top: 1).SingleAsync();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Record.Int);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await flatCollection.EnsureCollectionDeletedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public new class Fixture() : IndexKindTests<int>.Fixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using VectorData.ConformanceTests;
|
||||
|
||||
namespace SqlServer.ConformanceTests;
|
||||
|
||||
public class SqlServerTestSuiteImplementationTests : TestSuiteImplementationTests
|
||||
{
|
||||
protected override ICollection<Type> IgnoredTestBases { get; } =
|
||||
[
|
||||
// Hybrid search not supported
|
||||
typeof(HybridSearchTests<>)
|
||||
];
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Data.SqlClient;
|
||||
using VectorData.ConformanceTests.Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Skips the test(s) when the database is not Azure SQL Database or SQL database in Microsoft Fabric.
|
||||
/// This is used for tests that require Azure SQL features not available in on-prem SQL Server (e.g. DiskAnn vector indexes).
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]
|
||||
public sealed class AzureSqlRequiredAttribute : Attribute, ITestCondition
|
||||
{
|
||||
private static bool? s_isAzureSql;
|
||||
|
||||
public async ValueTask<bool> IsMetAsync()
|
||||
{
|
||||
if (s_isAzureSql is not null)
|
||||
{
|
||||
return s_isAzureSql.Value;
|
||||
}
|
||||
|
||||
var connectionString = SqlServerTestStore.Instance.ConnectionString;
|
||||
|
||||
using var connection = new SqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT SERVERPROPERTY('EngineEdition')";
|
||||
var result = await command.ExecuteScalarAsync();
|
||||
var engineEdition = Convert.ToInt32(result);
|
||||
|
||||
// 5 = Azure SQL Database, 11 = SQL database in Microsoft Fabric
|
||||
s_isAzureSql = engineEdition is 5 or 11;
|
||||
return s_isAzureSql.Value;
|
||||
}
|
||||
|
||||
public string SkipReason
|
||||
=> "This test requires Azure SQL Database or SQL database in Microsoft Fabric.";
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace SqlServer.ConformanceTests.Support;
|
||||
|
||||
#pragma warning disable CA1810 // Initialize all static fields when those fields are declared
|
||||
|
||||
internal static class SqlServerTestEnvironment
|
||||
{
|
||||
public static readonly string? ConnectionString;
|
||||
|
||||
public static bool IsConnectionStringDefined => ConnectionString is not null;
|
||||
|
||||
static SqlServerTestEnvironment()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile(path: "testsettings.json", optional: true)
|
||||
.AddJsonFile(path: "testsettings.development.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.AddUserSecrets<SqlServerTestStore>()
|
||||
.Build();
|
||||
|
||||
var sqlServerSection = configuration.GetSection("SqlServer");
|
||||
ConnectionString = sqlServerSection["ConnectionString"];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel.Connectors.SqlServer;
|
||||
using Testcontainers.MsSql;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
|
||||
namespace SqlServer.ConformanceTests.Support;
|
||||
|
||||
#pragma warning disable CA1001 // Type owns disposable fields but is not disposable
|
||||
|
||||
internal sealed class SqlServerTestStore : TestStore
|
||||
{
|
||||
public static SqlServerTestStore Instance { get; } = new();
|
||||
|
||||
private static readonly MsSqlContainer s_container = new MsSqlBuilder()
|
||||
.WithImage("mcr.microsoft.com/mssql/server:2025-latest")
|
||||
.Build();
|
||||
|
||||
private string? _connectionString;
|
||||
private bool _useExternalInstance;
|
||||
|
||||
public string ConnectionString => this._connectionString ?? throw new InvalidOperationException("Not initialized");
|
||||
|
||||
public SqlServerVectorStore GetVectorStore(SqlServerVectorStoreOptions options)
|
||||
=> new(this.ConnectionString, options);
|
||||
|
||||
public override string DefaultDistanceFunction => DistanceFunction.CosineDistance;
|
||||
|
||||
private SqlServerTestStore()
|
||||
{
|
||||
}
|
||||
|
||||
protected override async Task StartAsync()
|
||||
{
|
||||
if (SqlServerTestEnvironment.IsConnectionStringDefined)
|
||||
{
|
||||
this._connectionString = SqlServerTestEnvironment.ConnectionString!;
|
||||
this._useExternalInstance = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use testcontainer if no external connection string is provided
|
||||
await s_container.StartAsync();
|
||||
this._connectionString = s_container.GetConnectionString();
|
||||
this._useExternalInstance = false;
|
||||
}
|
||||
|
||||
this.DefaultVectorStore = new SqlServerVectorStore(this._connectionString);
|
||||
}
|
||||
|
||||
protected override async Task StopAsync()
|
||||
{
|
||||
// Only stop the container if we started it
|
||||
if (!this._useExternalInstance)
|
||||
{
|
||||
await s_container.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task WaitForDataAsync<TKey, TRecord>(
|
||||
VectorStoreCollection<TKey, TRecord> collection,
|
||||
int recordCount,
|
||||
Expression<Func<TRecord, bool>>? filter = null,
|
||||
Expression<Func<TRecord, object?>>? vectorProperty = null,
|
||||
int? vectorSize = null,
|
||||
object? dummyVector = null)
|
||||
{
|
||||
// First wait for the base data to be visible via vector search
|
||||
await base.WaitForDataAsync(collection, recordCount, filter, vectorProperty, vectorSize, dummyVector);
|
||||
|
||||
// Then wait for full-text population to complete (if any full-text indexes exist)
|
||||
await this.WaitForFullTextPopulationAsync(collection.Name);
|
||||
}
|
||||
|
||||
private async Task WaitForFullTextPopulationAsync(string tableName)
|
||||
{
|
||||
using var connection = new SqlConnection(this.ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
// Query to check if full-text population is complete
|
||||
var checkSql = @"
|
||||
SELECT COUNT(*)
|
||||
FROM sys.fulltext_indexes fi
|
||||
JOIN sys.tables t ON fi.object_id = t.object_id
|
||||
WHERE t.name = @tableName
|
||||
AND fi.has_crawl_completed = 0";
|
||||
|
||||
using var command = new SqlCommand(checkSql, connection);
|
||||
command.Parameters.AddWithValue("@tableName", tableName);
|
||||
|
||||
for (int i = 0; i < 100; i++) // Wait up to 10 seconds
|
||||
{
|
||||
var result = await command.ExecuteScalarAsync();
|
||||
|
||||
if (result is int count && count == 0)
|
||||
{
|
||||
// Either no full-text indexes exist or all are populated
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
|
||||
// Don't fail the test - some tests might not need full-text
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.TypeTests;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests.TypeTests;
|
||||
|
||||
public class SqlServerDataTypeTests(SqlServerDataTypeTests.Fixture fixture)
|
||||
: DataTypeTests<Guid, DataTypeTests<Guid>.DefaultRecord>(fixture), IClassFixture<SqlServerDataTypeTests.Fixture>
|
||||
{
|
||||
public override Task String_array()
|
||||
=> this.Test<string[]>(
|
||||
"StringArray",
|
||||
["foo", "bar"],
|
||||
["foo", "baz"],
|
||||
// SQL Server doesn't support comparing JSON
|
||||
isFilterable: false);
|
||||
|
||||
public new class Fixture : DataTypeTests<Guid, DataTypeTests<Guid>.DefaultRecord>.Fixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Data.SqlTypes;
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.TypeTests;
|
||||
using VectorData.ConformanceTests.Xunit;
|
||||
using Xunit;
|
||||
|
||||
#pragma warning disable CA2000 // Dispose objects before losing scope
|
||||
|
||||
namespace SqlServer.ConformanceTests.TypeTests;
|
||||
|
||||
public class SqlServerEmbeddingTypeTests(SqlServerEmbeddingTypeTests.Fixture fixture)
|
||||
: EmbeddingTypeTests<Guid>(fixture), IClassFixture<SqlServerEmbeddingTypeTests.Fixture>
|
||||
{
|
||||
[ConditionalFact]
|
||||
public virtual Task SqlVector_of_float()
|
||||
=> this.Test<SqlVector<float>>(
|
||||
new SqlVector<float>(new float[] { 1, 2, 3 }),
|
||||
new ReadOnlyMemoryEmbeddingGenerator<float>([1, 2, 3]),
|
||||
vectorEqualityAsserter: (e, a) => Assert.Equal(e.Memory.ToArray(), a.Memory.ToArray()));
|
||||
|
||||
public new class Fixture : EmbeddingTypeTests<Guid>.Fixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SqlServer.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.TypeTests;
|
||||
using VectorData.ConformanceTests.Xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace SqlServer.ConformanceTests.TypeTests;
|
||||
|
||||
public class SqlServerKeyTypeTests(SqlServerKeyTypeTests.Fixture fixture)
|
||||
: KeyTypeTests(fixture), IClassFixture<SqlServerKeyTypeTests.Fixture>
|
||||
{
|
||||
[ConditionalFact]
|
||||
public virtual Task Int() => this.Test<int>(8, supportsAutoGeneration: true);
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual Task Long() => this.Test<long>(8L, supportsAutoGeneration: true);
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual Task String() => this.Test<string>("foo", "bar");
|
||||
|
||||
public new class Fixture : KeyTypeTests.Fixture
|
||||
{
|
||||
public override TestStore TestStore => SqlServerTestStore.Instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
// Optional: Provide a connection string to use an external SQL Server instance instead of testcontainers
|
||||
// This is useful for running tests against Azure SQL or other external SQL Server instances
|
||||
// If not specified, tests will automatically use a testcontainer
|
||||
"SqlServer": {
|
||||
"ConnectionString": ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user