From 798457c29d0e658099009711e4de37c19caf9682 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 12 Mar 2025 19:50:26 +0100 Subject: [PATCH 1/9] test improvements: - introduce CollectionConformanceTests - make GenerateNextKey thread-safe - make sure the tests don't create collection if there is no need to - implement the tests for Postgres, SQL Server and Redis (partially) --- .../CRUD/PostgresBatchConformanceTests.cs | 4 +- ...ostgresGenericDataModelConformanceTests.cs | 4 +- .../CRUD/PostgresRecordConformanceTests.cs | 12 ++ .../PostgresCollectionConformanceTests.cs | 12 ++ .../PostgresGenericDataModelFixture.cs | 10 ++ .../Support/PostgresSimpleModelFixture.cs | 10 ++ .../RedisGenericDataModelConformanceTests.cs | 12 ++ .../CRUD/RedisRecordConformanceTests.cs | 12 ++ .../Support/RedisFixture.cs | 10 ++ .../Support/RedisGenericDataModelFixture.cs | 10 ++ .../Support/RedisSimpleModelFixture.cs | 10 ++ .../CRUD/SqlServerBatchConformanceTests.cs | 4 +- ...lServerGenericDataModelConformanceTests.cs | 4 +- .../CRUD/SqlServerRecordConformanceTests.cs | 12 ++ .../SqlServerCollectionConformanceTests.cs | 12 ++ .../SqlServerGenericDataModelFixture.cs | 10 ++ .../Support/SqlServerSimpleModelFixture.cs | 10 ++ .../CRUD/BatchConformanceTests.cs | 151 ++++++++++++---- .../CRUD/ConformanceTestsBase.cs | 34 ---- .../CRUD/GenericDataModelConformanceTests.cs | 166 +++++++++++++----- .../CRUD/RecordConformanceTests.cs | 117 ++++++++++++ .../Collections/CollectionConformanceTests.cs | 111 ++++++++++++ .../Models/SimpleModel.cs | 24 ++- .../Support/GenericDataModelFixture.cs | 82 +++++++++ .../Support/SimpleModelFixture.cs | 60 +++++++ .../Support/VectorStoreCollectionFixture.cs | 2 +- .../Support/VectorStoreFixture.cs | 4 +- 27 files changed, 789 insertions(+), 120 deletions(-) create mode 100644 dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/CRUD/PostgresRecordConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/Collections/PostgresCollectionConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/Support/PostgresGenericDataModelFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/Support/PostgresSimpleModelFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/CRUD/RedisGenericDataModelConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/CRUD/RedisRecordConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisGenericDataModelFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisSimpleModelFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/CRUD/SqlServerRecordConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/Collections/SqlServerCollectionConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/Support/SqlServerGenericDataModelFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/Support/SqlServerSimpleModelFixture.cs delete mode 100644 dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/ConformanceTestsBase.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/RecordConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Collections/CollectionConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/GenericDataModelFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/SimpleModelFixture.cs diff --git a/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/CRUD/PostgresBatchConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/CRUD/PostgresBatchConformanceTests.cs index b798bab8e437..6344ab981b03 100644 --- a/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/CRUD/PostgresBatchConformanceTests.cs +++ b/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/CRUD/PostgresBatchConformanceTests.cs @@ -6,7 +6,7 @@ namespace PostgresIntegrationTests.CRUD; -public class PostgresBatchConformanceTests(PostgresFixture fixture) - : BatchConformanceTests(fixture), IClassFixture +public class PostgresBatchConformanceTests(PostgresSimpleModelFixture fixture) + : BatchConformanceTests(fixture), IClassFixture { } diff --git a/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/CRUD/PostgresGenericDataModelConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/CRUD/PostgresGenericDataModelConformanceTests.cs index 1a72c1b59e01..98451084af94 100644 --- a/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/CRUD/PostgresGenericDataModelConformanceTests.cs +++ b/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/CRUD/PostgresGenericDataModelConformanceTests.cs @@ -6,7 +6,7 @@ namespace PostgresIntegrationTests.CRUD; -public class PostgresGenericDataModelConformanceTests(PostgresFixture fixture) - : GenericDataModelConformanceTests(fixture), IClassFixture +public class PostgresGenericDataModelConformanceTests(PostgresGenericDataModelFixture fixture) + : GenericDataModelConformanceTests(fixture), IClassFixture { } diff --git a/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/CRUD/PostgresRecordConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/CRUD/PostgresRecordConformanceTests.cs new file mode 100644 index 000000000000..09177b9f91e3 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/CRUD/PostgresRecordConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using PostgresIntegrationTests.Support; +using VectorDataSpecificationTests.CRUD; +using Xunit; + +namespace PostgresIntegrationTests.CRUD; + +public class PostgresRecordConformanceTests(PostgresSimpleModelFixture fixture) + : RecordConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/Collections/PostgresCollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/Collections/PostgresCollectionConformanceTests.cs new file mode 100644 index 000000000000..ccfa0af7915a --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/Collections/PostgresCollectionConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using PostgresIntegrationTests.Support; +using VectorDataSpecificationTests.Collections; +using Xunit; + +namespace PostgresIntegrationTests.Collections; + +public class PostgresCollectionConformanceTests(PostgresFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/Support/PostgresGenericDataModelFixture.cs b/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/Support/PostgresGenericDataModelFixture.cs new file mode 100644 index 000000000000..c5b9a96b405f --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/Support/PostgresGenericDataModelFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace PostgresIntegrationTests.Support; + +public class PostgresGenericDataModelFixture : GenericDataModelFixture +{ + public override TestStore TestStore => PostgresTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/Support/PostgresSimpleModelFixture.cs b/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/Support/PostgresSimpleModelFixture.cs new file mode 100644 index 000000000000..87019f7c8b6d --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/Support/PostgresSimpleModelFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace PostgresIntegrationTests.Support; + +public class PostgresSimpleModelFixture : SimpleModelFixture +{ + public override TestStore TestStore => PostgresTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/CRUD/RedisGenericDataModelConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/CRUD/RedisGenericDataModelConformanceTests.cs new file mode 100644 index 000000000000..8806430fb9a0 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/CRUD/RedisGenericDataModelConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using RedisIntegrationTests.Support; +using VectorDataSpecificationTests.CRUD; +using Xunit; + +namespace RedisIntegrationTests.CRUD; + +public class RedisGenericDataModelConformanceTests(RedisGenericDataModelFixture fixture) + : GenericDataModelConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/CRUD/RedisRecordConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/CRUD/RedisRecordConformanceTests.cs new file mode 100644 index 000000000000..cab8188524fd --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/CRUD/RedisRecordConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using RedisIntegrationTests.Support; +using VectorDataSpecificationTests.CRUD; +using Xunit; + +namespace RedisIntegrationTests.CRUD; + +public class RedisRecordConformanceTests(RedisSimpleModelFixture fixture) + : RecordConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisFixture.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisFixture.cs new file mode 100644 index 000000000000..a8c7004eaec2 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace RedisIntegrationTests.Support; + +public class RedisFixture : VectorStoreFixture +{ + public override TestStore TestStore => RedisTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisGenericDataModelFixture.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisGenericDataModelFixture.cs new file mode 100644 index 000000000000..f5297b67bcb0 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisGenericDataModelFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace RedisIntegrationTests.Support; + +public class RedisGenericDataModelFixture : GenericDataModelFixture +{ + public override TestStore TestStore => RedisTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisSimpleModelFixture.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisSimpleModelFixture.cs new file mode 100644 index 000000000000..eb1d8dd70160 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisSimpleModelFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace RedisIntegrationTests.Support; + +public class RedisSimpleModelFixture : SimpleModelFixture +{ + public override TestStore TestStore => RedisTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/CRUD/SqlServerBatchConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/CRUD/SqlServerBatchConformanceTests.cs index 1e8ee17dd6f4..1914a08b74fe 100644 --- a/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/CRUD/SqlServerBatchConformanceTests.cs +++ b/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/CRUD/SqlServerBatchConformanceTests.cs @@ -6,7 +6,7 @@ namespace SqlServerIntegrationTests.CRUD; -public class SqlServerBatchConformanceTests(SqlServerFixture fixture) - : BatchConformanceTests(fixture), IClassFixture +public class SqlServerBatchConformanceTests(SqlServerSimpleModelFixture fixture) + : BatchConformanceTests(fixture), IClassFixture { } diff --git a/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/CRUD/SqlServerGenericDataModelConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/CRUD/SqlServerGenericDataModelConformanceTests.cs index 5b98a7d46a11..d3f67389e764 100644 --- a/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/CRUD/SqlServerGenericDataModelConformanceTests.cs +++ b/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/CRUD/SqlServerGenericDataModelConformanceTests.cs @@ -6,7 +6,7 @@ namespace SqlServerIntegrationTests.CRUD; -public class SqlServerGenericDataModelConformanceTests(SqlServerFixture fixture) - : GenericDataModelConformanceTests(fixture), IClassFixture +public class SqlServerGenericDataModelConformanceTests(SqlServerGenericDataModelFixture fixture) + : GenericDataModelConformanceTests(fixture), IClassFixture { } diff --git a/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/CRUD/SqlServerRecordConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/CRUD/SqlServerRecordConformanceTests.cs new file mode 100644 index 000000000000..6072445cab55 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/CRUD/SqlServerRecordConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using SqlServerIntegrationTests.Support; +using VectorDataSpecificationTests.CRUD; +using Xunit; + +namespace SqlServerIntegrationTests.CRUD; + +public class SqlServerRecordConformanceTests(SqlServerSimpleModelFixture fixture) + : RecordConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/Collections/SqlServerCollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/Collections/SqlServerCollectionConformanceTests.cs new file mode 100644 index 000000000000..471a5a9f2423 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/Collections/SqlServerCollectionConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using SqlServerIntegrationTests.Support; +using VectorDataSpecificationTests.Collections; +using Xunit; + +namespace SqlServerIntegrationTests.Collections; + +public class SqlServerCollectionConformanceTests(SqlServerFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/Support/SqlServerGenericDataModelFixture.cs b/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/Support/SqlServerGenericDataModelFixture.cs new file mode 100644 index 000000000000..d3be9dbe419d --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/Support/SqlServerGenericDataModelFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace SqlServerIntegrationTests.Support; + +public class SqlServerGenericDataModelFixture : GenericDataModelFixture +{ + public override TestStore TestStore => SqlServerTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/Support/SqlServerSimpleModelFixture.cs b/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/Support/SqlServerSimpleModelFixture.cs new file mode 100644 index 000000000000..534b53d6a047 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/SqlServerIntegrationTests/Support/SqlServerSimpleModelFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace SqlServerIntegrationTests.Support; + +public class SqlServerSimpleModelFixture : SimpleModelFixture +{ + public override TestStore TestStore => SqlServerTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/BatchConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/BatchConformanceTests.cs index ace837591a74..21712ad6c2ce 100644 --- a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/BatchConformanceTests.cs +++ b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/BatchConformanceTests.cs @@ -7,63 +7,150 @@ namespace VectorDataSpecificationTests.CRUD; -public abstract class BatchConformanceTests(VectorStoreFixture fixture) - : ConformanceTestsBase>(fixture) where TKey : notnull +public abstract class BatchConformanceTests(SimpleModelFixture fixture) where TKey : notnull { [ConditionalFact] - public async Task UpsertBatchAsync_EmptyBatch_DoesNotThrow() + public async Task GetBatchAsyncThrowsArgumentNullExceptionForNullKeys() { - await this.ExecuteAsync(async collection => - { - Assert.Empty(await collection.UpsertBatchAsync([]).ToArrayAsync()); - }); + ArgumentNullException ex = await Assert.ThrowsAsync(() => fixture.Collection.GetBatchAsync(keys: null!).ToArrayAsync().AsTask()); + Assert.Equal("keys", ex.ParamName); } [ConditionalFact] - public async Task DeleteBatchAsync_EmptyBatch_DoesNotThrow() + public async Task GetBatchAsyncDoesNotThrowForEmptyBatch() { - await this.ExecuteAsync(async collection => - { - await collection.DeleteBatchAsync([]); - }); + Assert.Empty(await fixture.Collection.GetBatchAsync([]).ToArrayAsync()); } [ConditionalFact] - public async Task GetBatchAsync_EmptyBatch_DoesNotThrow() + public Task GetBatchAsyncReturnsInsertedRecords_WithVectors() + => this.GetBatchAsyncReturnsInsertedRecords(includeVectors: true); + + [ConditionalFact] + public Task GetBatchAsyncReturnsInsertedRecords_WithoutVectors() + => this.GetBatchAsyncReturnsInsertedRecords(includeVectors: false); + + private async Task GetBatchAsyncReturnsInsertedRecords(bool includeVectors) { - await this.ExecuteAsync(async collection => + var expectedRecords = fixture.TestData.Take(2); // the last two records can get deleted by other tests + var ids = expectedRecords.Select(record => record.Id); + + var received = await fixture.Collection.GetBatchAsync(ids, new() { IncludeVectors = includeVectors }).ToArrayAsync(); + + foreach (var record in expectedRecords) { - Assert.Empty(await collection.GetBatchAsync([]).ToArrayAsync()); - }); + record.AssertEqual(this.GetRecord(received, record.Id), includeVectors); + } } [ConditionalFact] - public async Task UpsertBatchAsync_NullBatch_ThrowsArgumentNullException() + public async Task UpsertBatchAsyncThrowsArgumentNullExceptionForNullBatch() { - await this.ExecuteAsync(async collection => - { - ArgumentNullException ex = await Assert.ThrowsAsync(() => collection.UpsertBatchAsync(records: null!).ToArrayAsync().AsTask()); - Assert.Equal("records", ex.ParamName); - }); + ArgumentNullException ex = await Assert.ThrowsAsync(() => fixture.Collection.UpsertBatchAsync(records: null!).ToArrayAsync().AsTask()); + Assert.Equal("records", ex.ParamName); + } + + [ConditionalFact] + public async Task UpsertBatchAsyncDoesNotThrowForEmptyBatch() + { + Assert.Empty(await fixture.Collection.UpsertBatchAsync([]).ToArrayAsync()); } [ConditionalFact] - public async Task DeleteBatchAsync_NullKeys_ThrowsArgumentNullException() + public Task UpsertBatchAsyncCanInsertNewRecord_WithVectors() + => this.UpsertBatchAsyncCanInsertNewRecords(includeVectors: true); + + [ConditionalFact] + public Task UpsertBatchAsyncCanInsertNewRecord_WithoutVectors() + => this.UpsertBatchAsyncCanInsertNewRecords(includeVectors: false); + + private async Task UpsertBatchAsyncCanInsertNewRecords(bool includeVectors) { - await this.ExecuteAsync(async collection => + var collection = fixture.Collection; + SimpleModel[] inserted = Enumerable.Range(0, 10).Select(i => new SimpleModel() { - ArgumentNullException ex = await Assert.ThrowsAsync(() => collection.DeleteBatchAsync(keys: null!)); - Assert.Equal("keys", ex.ParamName); - }); + Id = fixture.GenerateNextKey(), + Number = 100 + i, + Text = i.ToString(), + Floats = Enumerable.Range(0, SimpleModel.DimensionCount).Select(j => (float)(i + j)).ToArray() + }).ToArray(); + var keys = inserted.Select(record => record.Id).ToArray(); + + Assert.Empty(await collection.GetBatchAsync(keys).ToArrayAsync()); + var receivedKeys = await collection.UpsertBatchAsync(inserted).ToArrayAsync(); + Assert.Equal(keys.ToHashSet(), receivedKeys.ToHashSet()); // .ToHashSet() to ignore order + + var received = await collection.GetBatchAsync(keys, new() { IncludeVectors = includeVectors }).ToArrayAsync(); + foreach (var record in inserted) + { + record.AssertEqual(this.GetRecord(received, record.Id), includeVectors); + } } [ConditionalFact] - public async Task GetBatchAsync_NullKeys_ThrowsArgumentNullException() + public Task UpsertBatchAsyncCanUpdateExistingRecords_WithVectors() + => this.UpsertBatchAsyncCanUpdateExistingRecords(includeVectors: true); + + [ConditionalFact] + public Task UpsertBatchAsyncCanUpdateExistingRecords_WithoutVectors() + => this.UpsertBatchAsyncCanUpdateExistingRecords(includeVectors: false); + + private async Task UpsertBatchAsyncCanUpdateExistingRecords(bool includeVectors) { - await this.ExecuteAsync(async collection => + SimpleModel[] inserted = Enumerable.Range(0, 10).Select(i => new SimpleModel() + { + Id = fixture.GenerateNextKey(), + Number = 100 + i, + Text = i.ToString(), + Floats = Enumerable.Range(0, SimpleModel.DimensionCount).Select(j => (float)(i + j)).ToArray() + }).ToArray(); + await fixture.Collection.UpsertBatchAsync(inserted).ToArrayAsync(); + + SimpleModel[] updated = inserted.Select(i => new SimpleModel() + { + Id = i.Id, + Text = i.Text + "updated", + Number = i.Number + 200, + Floats = i.Floats + }).ToArray(); + + var keys = await fixture.Collection.UpsertBatchAsync(updated).ToArrayAsync(); + Assert.Equal( + updated.Select(r => r.Id).OrderBy(id => id).ToArray(), + keys.OrderBy(id => id).ToArray()); + + var received = await fixture.Collection.GetBatchAsync(keys, new() { IncludeVectors = includeVectors }).ToArrayAsync(); + foreach (var record in updated) { - ArgumentNullException ex = await Assert.ThrowsAsync(() => collection.GetBatchAsync(keys: null!).ToArrayAsync().AsTask()); - Assert.Equal("keys", ex.ParamName); - }); + record.AssertEqual(this.GetRecord(received, record.Id), includeVectors); + } + } + + [ConditionalFact] + public async Task DeleteBatchAsyncDoesNotThrowForEmptyBatch() + { + await fixture.Collection.DeleteBatchAsync([]); } + + [ConditionalFact] + public async Task DeleteBatchAsyncThrowsArgumentNullExceptionForNullKeys() + { + ArgumentNullException ex = await Assert.ThrowsAsync(() => fixture.Collection.DeleteBatchAsync(keys: null!)); + Assert.Equal("keys", ex.ParamName); + } + + [ConditionalFact] + public async Task DeleteBatchAsyncDeletesTheRecords() + { + TKey[] idsToRemove = [fixture.TestData[2].Id, fixture.TestData[3].Id]; + + Assert.NotEmpty(await fixture.Collection.GetBatchAsync(idsToRemove).ToArrayAsync()); + await fixture.Collection.DeleteBatchAsync(idsToRemove); + Assert.Empty(await fixture.Collection.GetBatchAsync(idsToRemove).ToArrayAsync()); + } + + // The order of records in the received array is not guaranteed + // to match the order of keys in the requested keys array. + private SimpleModel GetRecord(SimpleModel[] received, TKey key) + => received.Single(r => r.Id!.Equals(key)); } diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/ConformanceTestsBase.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/ConformanceTestsBase.cs deleted file mode 100644 index 21a6c95f8986..000000000000 --- a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/ConformanceTestsBase.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.VectorData; -using VectorDataSpecificationTests.Support; - -namespace VectorDataSpecificationTests.CRUD; - -// TKey is a generic parameter because different connectors support different key types. -public abstract class ConformanceTestsBase(VectorStoreFixture fixture) where TKey : notnull -{ - protected VectorStoreFixture Fixture { get; } = fixture; - - protected virtual string GetUniqueCollectionName() => Guid.NewGuid().ToString(); - - protected virtual VectorStoreRecordDefinition? GetRecordDefinition() => null; - - protected async Task ExecuteAsync(Func, Task> test) - { - string collectionName = this.GetUniqueCollectionName(); - var collection = this.Fixture.TestStore.DefaultVectorStore.GetCollection(collectionName, - this.GetRecordDefinition()); - - await collection.CreateCollectionAsync(); - - try - { - await test(collection); - } - finally - { - await collection.DeleteCollectionAsync(); - } - } -} diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/GenericDataModelConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/GenericDataModelConformanceTests.cs index 91ac166aafd4..dc905f82aea8 100644 --- a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/GenericDataModelConformanceTests.cs +++ b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/GenericDataModelConformanceTests.cs @@ -7,60 +7,142 @@ namespace VectorDataSpecificationTests.CRUD; -public abstract class GenericDataModelConformanceTests(VectorStoreFixture fixture) - : ConformanceTestsBase>(fixture) where TKey : notnull +public abstract class GenericDataModelConformanceTests(GenericDataModelFixture fixture) where TKey : notnull { - private const string KeyPropertyName = "key"; - private const string StringPropertyName = "text"; - private const string IntegerPropertyName = "integer"; - private const string EmbeddingPropertyName = "embedding"; - private const int DimensionCount = 10; - - protected override VectorStoreRecordDefinition? GetRecordDefinition() - => new() - { - Properties = - [ - new VectorStoreRecordKeyProperty(KeyPropertyName, typeof(TKey)), - new VectorStoreRecordDataProperty(StringPropertyName, typeof(string)), - new VectorStoreRecordDataProperty(IntegerPropertyName, typeof(int)), - new VectorStoreRecordVectorProperty(EmbeddingPropertyName, typeof(ReadOnlyMemory)) - { - Dimensions = DimensionCount - } - ] - }; + [ConditionalFact] + public async Task GetAsyncThrowsArgumentNullExceptionForNullKey() + { + ArgumentNullException ex = await Assert.ThrowsAsync(() => fixture.Collection.GetAsync(default!)); + Assert.Equal("key", ex.ParamName); + } + + [ConditionalFact] + public async Task GetAsyncReturnsNullForNonExistingKey() + { + TKey key = fixture.GenerateNextKey(); + + Assert.Null(await fixture.Collection.GetAsync(key)); + } [ConditionalFact] - public async Task CanInsertUpdateAndDelete() + public Task GetAsyncReturnsInsertedRecord_WithVectors() + => this.GetAsyncReturnsInsertedRecord(includeVectors: true); + + [ConditionalFact] + public Task GetAsyncReturnsInsertedRecord_WithoutVectors() + => this.GetAsyncReturnsInsertedRecord(includeVectors: false); + + private async Task GetAsyncReturnsInsertedRecord(bool includeVectors) { - await this.ExecuteAsync(async collection => + var expectedRecord = fixture.TestData[0]; + + var received = await fixture.Collection.GetAsync(expectedRecord.Key, new() { IncludeVectors = includeVectors }); + + AssertEqual(expectedRecord, received, includeVectors); + } + + [ConditionalFact] + public Task UpsertAsyncCanInsertNewRecord_WithVectors() + => this.UpsertAsyncCanInsertNewRecord(includeVectors: true); + + [ConditionalFact] + public Task UpsertAsyncCanInsertNewRecord_WithoutVectors() + => this.UpsertAsyncCanInsertNewRecord(includeVectors: false); + + private async Task UpsertAsyncCanInsertNewRecord(bool includeVectors) + { + var collection = fixture.Collection; + TKey expectedKey = fixture.GenerateNextKey(); + VectorStoreGenericDataModel inserted = new(expectedKey) { - VectorStoreGenericDataModel inserted = new(key: this.Fixture.GenerateNextKey()); - inserted.Data.Add(StringPropertyName, "some"); - inserted.Data.Add(IntegerPropertyName, 123); - inserted.Vectors.Add(EmbeddingPropertyName, new ReadOnlyMemory(Enumerable.Repeat(0.1f, DimensionCount).ToArray())); + Data = + { + [GenericDataModelFixture.StringPropertyName] = "some", + [GenericDataModelFixture.IntegerPropertyName] = 123 + }, + Vectors = + { + [GenericDataModelFixture.EmbeddingPropertyName] = new ReadOnlyMemory(Enumerable.Repeat(0.1f, GenericDataModelFixture.DimensionCount).ToArray()) + } + }; - TKey key = await collection.UpsertAsync(inserted); - Assert.Equal(inserted.Key, key); + Assert.Null(await collection.GetAsync(expectedKey)); + TKey key = await collection.UpsertAsync(inserted); + Assert.Equal(expectedKey, key); - VectorStoreGenericDataModel? received = await collection.GetAsync(key, new() { IncludeVectors = true }); - Assert.NotNull(received); + var received = await collection.GetAsync(expectedKey, new() { IncludeVectors = includeVectors }); + AssertEqual(inserted, received, includeVectors); + } - Assert.Equal(received.Key, key); - foreach (var pair in inserted.Data) + [ConditionalFact] + public Task UpsertAsyncCanUpdateExistingRecord_WithVectors() + => this.UpsertAsyncCanUpdateExistingRecord(includeVectors: true); + + [ConditionalFact] + public Task UpsertAsyncCanUpdateExistingRecord__WithoutVectors() + => this.UpsertAsyncCanUpdateExistingRecord(includeVectors: false); + + private async Task UpsertAsyncCanUpdateExistingRecord(bool includeVectors) + { + var collection = fixture.Collection; + var existingRecord = fixture.TestData[1]; + VectorStoreGenericDataModel updated = new(existingRecord.Key) + { + Data = + { + [GenericDataModelFixture.StringPropertyName] = "different", + [GenericDataModelFixture.IntegerPropertyName] = 456 + }, + Vectors = { - Assert.Equal(pair.Value, received.Data[pair.Key]); + [GenericDataModelFixture.EmbeddingPropertyName] = new ReadOnlyMemory(Enumerable.Repeat(0.7f, GenericDataModelFixture.DimensionCount).ToArray()) } + }; - Assert.Equal( - ((ReadOnlyMemory)inserted.Vectors[EmbeddingPropertyName]!).ToArray(), - ((ReadOnlyMemory)received.Vectors[EmbeddingPropertyName]!).ToArray()); + Assert.NotNull(await collection.GetAsync(existingRecord.Key)); + TKey key = await collection.UpsertAsync(updated); + Assert.Equal(existingRecord.Key, key); + + var received = await collection.GetAsync(existingRecord.Key, new() { IncludeVectors = includeVectors }); + AssertEqual(updated, received, includeVectors); + } + + [ConditionalFact] + public async Task DeleteAsyncDoesNotThrowForNonExistingKey() + { + TKey key = fixture.GenerateNextKey(); + + await fixture.Collection.DeleteAsync(key); + } - await collection.DeleteAsync(key); + [ConditionalFact] + public async Task DeleteAsyncDeletesTheRecord() + { + var recordToRemove = fixture.TestData[2]; + + Assert.NotNull(await fixture.Collection.GetAsync(recordToRemove.Key)); + await fixture.Collection.DeleteAsync(recordToRemove.Key); + Assert.Null(await fixture.Collection.GetAsync(recordToRemove.Key)); + } - received = await collection.GetAsync(key); - Assert.Null(received); - }); + private static void AssertEqual(VectorStoreGenericDataModel expected, VectorStoreGenericDataModel? actual, bool includeVectors) + { + Assert.NotNull(actual); + Assert.Equal(expected.Key, actual.Key); + foreach (var pair in expected.Data) + { + Assert.Equal(pair.Value, actual.Data[pair.Key]); + } + + if (includeVectors) + { + Assert.Equal( + ((ReadOnlyMemory)expected.Vectors[GenericDataModelFixture.EmbeddingPropertyName]!).ToArray(), + ((ReadOnlyMemory)actual.Vectors[GenericDataModelFixture.EmbeddingPropertyName]!).ToArray()); + } + else + { + Assert.Empty(actual.Vectors); + } } } diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/RecordConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/RecordConformanceTests.cs new file mode 100644 index 000000000000..5a3d0d0081ea --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/CRUD/RecordConformanceTests.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Models; +using VectorDataSpecificationTests.Support; +using VectorDataSpecificationTests.Xunit; +using Xunit; + +namespace VectorDataSpecificationTests.CRUD; + +public class RecordConformanceTests(SimpleModelFixture fixture) where TKey : notnull +{ + [ConditionalFact] + public async Task GetAsyncThrowsArgumentNullExceptionForNullKey() + { + ArgumentNullException ex = await Assert.ThrowsAsync(() => fixture.Collection.GetAsync(default!)); + Assert.Equal("key", ex.ParamName); + } + + [ConditionalFact] + public async Task GetAsyncReturnsNullForNonExistingKey() + { + TKey key = fixture.GenerateNextKey(); + + Assert.Null(await fixture.Collection.GetAsync(key)); + } + + [ConditionalFact] + public Task GetAsyncReturnsInsertedRecord_WithVectors() + => this.GetAsyncReturnsInsertedRecord(includeVectors: true); + + [ConditionalFact] + public Task GetAsyncReturnsInsertedRecord_WithoutVectors() + => this.GetAsyncReturnsInsertedRecord(includeVectors: false); + + private async Task GetAsyncReturnsInsertedRecord(bool includeVectors) + { + var expectedRecord = fixture.TestData[0]; + + var received = await fixture.Collection.GetAsync(expectedRecord.Id, new() { IncludeVectors = includeVectors }); + + expectedRecord.AssertEqual(received, includeVectors); + } + + [ConditionalFact] + public Task UpsertAsyncCanInsertNewRecord_WithVectors() + => this.UpsertAsyncCanInsertNewRecord(includeVectors: true); + + [ConditionalFact] + public Task UpsertAsyncCanInsertNewRecord_WithoutVectors() + => this.UpsertAsyncCanInsertNewRecord(includeVectors: false); + + private async Task UpsertAsyncCanInsertNewRecord(bool includeVectors) + { + var collection = fixture.Collection; + TKey expectedKey = fixture.GenerateNextKey(); + SimpleModel inserted = new() + { + Id = expectedKey, + Text = "some", + Number = 123, + Floats = new ReadOnlyMemory(Enumerable.Repeat(0.1f, SimpleModel.DimensionCount).ToArray()) + }; + + Assert.Null(await collection.GetAsync(expectedKey)); + TKey key = await collection.UpsertAsync(inserted); + Assert.Equal(expectedKey, key); + + var received = await collection.GetAsync(expectedKey, new() { IncludeVectors = includeVectors }); + inserted.AssertEqual(received, includeVectors); + } + + [ConditionalFact] + public Task UpsertAsyncCanUpdateExistingRecord_WithVectors() + => this.UpsertAsyncCanUpdateExistingRecord(includeVectors: true); + + [ConditionalFact] + public Task UpsertAsyncCanUpdateExistingRecord__WithoutVectors() + => this.UpsertAsyncCanUpdateExistingRecord(includeVectors: false); + + private async Task UpsertAsyncCanUpdateExistingRecord(bool includeVectors) + { + var collection = fixture.Collection; + var existingRecord = fixture.TestData[1]; + SimpleModel updated = new() + { + Id = existingRecord.Id, + Text = "updated", + Number = 456, + Floats = new ReadOnlyMemory(Enumerable.Repeat(0.2f, SimpleModel.DimensionCount).ToArray()) + }; + + Assert.NotNull(await collection.GetAsync(existingRecord.Id)); + TKey key = await collection.UpsertAsync(updated); + Assert.Equal(existingRecord.Id, key); + + var received = await collection.GetAsync(existingRecord.Id, new() { IncludeVectors = includeVectors }); + updated.AssertEqual(received, includeVectors); + } + + [ConditionalFact] + public async Task DeleteAsyncDoesNotThrowForNonExistingKey() + { + TKey key = fixture.GenerateNextKey(); + + await fixture.Collection.DeleteAsync(key); + } + + [ConditionalFact] + public async Task DeleteAsyncDeletesTheRecord() + { + var recordToRemove = fixture.TestData[2]; + + Assert.NotNull(await fixture.Collection.GetAsync(recordToRemove.Id)); + await fixture.Collection.DeleteAsync(recordToRemove.Id); + Assert.Null(await fixture.Collection.GetAsync(recordToRemove.Id)); + } +} diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Collections/CollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Collections/CollectionConformanceTests.cs new file mode 100644 index 000000000000..ca5a0c1dfc20 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Collections/CollectionConformanceTests.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.VectorData; +using VectorDataSpecificationTests.Models; +using VectorDataSpecificationTests.Support; +using VectorDataSpecificationTests.Xunit; +using Xunit; + +namespace VectorDataSpecificationTests.Collections; + +public class CollectionConformanceTests(VectorStoreFixture fixture) where TKey : notnull +{ + [ConditionalFact] + public Task DeleteCollectionDoesNotThrowForNonExistingCollection() + => this.DeleteNonExistingCollection>(); + + [ConditionalFact] + public Task DeleteCollectionDoesNotThrowForNonExistingCollection_GenericDataModel() + => this.DeleteNonExistingCollection>(); + + [ConditionalFact] + public Task CreateCollectionIfNotExistsCalledMoreThanOnceDoesNotThrow() + => this.CreateCollectionIfNotExistsMoreThanOnce>(); + + [ConditionalFact] + public Task CreateCollectionIfNotExistsCalledMoreThanOnceDoesNotThrow_GenericDataModel() + => this.CreateCollectionIfNotExistsMoreThanOnce>(); + + [ConditionalFact] + public Task CreateCollectionCalledMoreThanOnceThrowsVectorStoreOperationException() + => this.CreateCollectionMoreThanOnce>(); + + [ConditionalFact] + public Task CreateCollectionCalledMoreThanOnceThrowsVectorStoreOperationException_GenericDataModel() + => this.CreateCollectionMoreThanOnce>(); + + private async Task> GetNonExistingCollectionAsync() + { + var collectionName = fixture.GetUniqueCollectionName(); + VectorStoreRecordDefinition? definition = null; + if (typeof(TRecord) == typeof(VectorStoreGenericDataModel)) + { + definition = new() + { + Properties = + [ + new VectorStoreRecordKeyProperty(nameof(VectorStoreGenericDataModel.Key), typeof(TKey)), + new VectorStoreRecordDataProperty("string", typeof(string)), + new VectorStoreRecordDataProperty("integer", typeof(int)), + new VectorStoreRecordVectorProperty("embedding", typeof(ReadOnlyMemory)) + { + Dimensions = 10 + } + ] + }; + } + + var collection = fixture.TestStore.DefaultVectorStore.GetCollection(collectionName, definition); + + Assert.False(await collection.CollectionExistsAsync()); + + return collection; + } + + private async Task DeleteNonExistingCollection() + { + var collection = await this.GetNonExistingCollectionAsync(); + + await collection.DeleteCollectionAsync(); + } + + private async Task CreateCollectionIfNotExistsMoreThanOnce() + { + var collection = await this.GetNonExistingCollectionAsync(); + + try + { + await collection.CreateCollectionIfNotExistsAsync(); + + Assert.True(await collection.CollectionExistsAsync()); + Assert.True(await fixture.TestStore.DefaultVectorStore.ListCollectionNamesAsync().ContainsAsync(collection.CollectionName)); + + await collection.CreateCollectionIfNotExistsAsync(); + } + finally + { + await collection.DeleteCollectionAsync(); + } + } + + private async Task CreateCollectionMoreThanOnce() + { + var collection = await this.GetNonExistingCollectionAsync(); + + try + { + await collection.CreateCollectionAsync(); + + Assert.True(await collection.CollectionExistsAsync()); + Assert.True(await fixture.TestStore.DefaultVectorStore.ListCollectionNamesAsync().ContainsAsync(collection.CollectionName)); + + await collection.CreateCollectionIfNotExistsAsync(); + + await Assert.ThrowsAsync(() => collection.CreateCollectionAsync()); + } + finally + { + await collection.DeleteCollectionAsync(); + } + } +} diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Models/SimpleModel.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Models/SimpleModel.cs index 0646f0fe2f1f..13a47e386516 100644 --- a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Models/SimpleModel.cs +++ b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Models/SimpleModel.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Extensions.VectorData; +using Xunit; namespace VectorDataSpecificationTests.Models; @@ -11,8 +12,10 @@ namespace VectorDataSpecificationTests.Models; /// TKey is a generic parameter because different connectors support different key types. public sealed class SimpleModel { + public const int DimensionCount = 3; + [VectorStoreRecordKey(StoragePropertyName = "key")] - public TKey? Id { get; set; } + public TKey Id { get; set; } = default!; [VectorStoreRecordData(StoragePropertyName = "text")] public string? Text { get; set; } @@ -20,6 +23,23 @@ public sealed class SimpleModel [VectorStoreRecordData(StoragePropertyName = "number")] public int Number { get; set; } - [VectorStoreRecordVector(Dimensions: 10, StoragePropertyName = "embedding")] + [VectorStoreRecordVector(Dimensions: DimensionCount, StoragePropertyName = "embedding")] public ReadOnlyMemory Floats { get; set; } + + public void AssertEqual(SimpleModel? other, bool includeVectors) + { + Assert.NotNull(other); + Assert.Equal(this.Id, other.Id); + Assert.Equal(this.Text, other.Text); + Assert.Equal(this.Number, other.Number); + + if (includeVectors) + { + Assert.Equal(this.Floats.ToArray(), other.Floats.ToArray()); + } + else + { + Assert.Equal(0, other.Floats.Length); + } + } } diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/GenericDataModelFixture.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/GenericDataModelFixture.cs new file mode 100644 index 000000000000..333ec1cdfea8 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/GenericDataModelFixture.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.VectorData; + +namespace VectorDataSpecificationTests.Support; + +public abstract class GenericDataModelFixture : VectorStoreCollectionFixture> + where TKey : notnull +{ + public const string KeyPropertyName = "key"; + public const string StringPropertyName = "text"; + public const string IntegerPropertyName = "integer"; + public const string EmbeddingPropertyName = "embedding"; + public const int DimensionCount = 3; + + protected override VectorStoreRecordDefinition GetRecordDefinition() + => new() + { + Properties = + [ + new VectorStoreRecordKeyProperty(KeyPropertyName, typeof(TKey)), + new VectorStoreRecordDataProperty(StringPropertyName, typeof(string)), + new VectorStoreRecordDataProperty(IntegerPropertyName, typeof(int)), + new VectorStoreRecordVectorProperty(EmbeddingPropertyName, typeof(ReadOnlyMemory)) + { + Dimensions = DimensionCount + } + ] + }; + + protected override List> BuildTestData() => + [ + new(this.GenerateNextKey()) + { + Data = + { + [StringPropertyName] = "first", + [IntegerPropertyName] = 1 + }, + Vectors = + { + [EmbeddingPropertyName] = new ReadOnlyMemory(Enumerable.Repeat(0.1f, DimensionCount).ToArray()) + } + }, + new(this.GenerateNextKey()) + { + Data = + { + [StringPropertyName] = "second", + [IntegerPropertyName] = 2 + }, + Vectors = + { + [EmbeddingPropertyName] = new ReadOnlyMemory(Enumerable.Repeat(0.2f, DimensionCount).ToArray()) + } + }, + new(this.GenerateNextKey()) + { + Data = + { + [StringPropertyName] = "third", + [IntegerPropertyName] = 3 + }, + Vectors = + { + [EmbeddingPropertyName] = new ReadOnlyMemory(Enumerable.Repeat(0.3f, DimensionCount).ToArray()) + } + }, + new(this.GenerateNextKey()) + { + Data = + { + [StringPropertyName] = "fourth", + [IntegerPropertyName] = 4 + }, + Vectors = + { + [EmbeddingPropertyName] = new ReadOnlyMemory(Enumerable.Repeat(0.4f, DimensionCount).ToArray()) + } + } + ]; +} diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/SimpleModelFixture.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/SimpleModelFixture.cs new file mode 100644 index 000000000000..b5c688c01835 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/SimpleModelFixture.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.VectorData; +using VectorDataSpecificationTests.Models; + +namespace VectorDataSpecificationTests.Support; + +public abstract class SimpleModelFixture : VectorStoreCollectionFixture> + where TKey : notnull +{ + protected override List> BuildTestData() => + [ + new() + { + Id = this.GenerateNextKey(), + Number = 1, + Text = "UsedByGetTests", + Floats = Enumerable.Repeat(0.1f, SimpleModel.DimensionCount).ToArray() + }, + new() + { + Id = this.GenerateNextKey(), + Number = 2, + Text = "UsedByUpdateTests", + Floats = Enumerable.Repeat(0.2f, SimpleModel.DimensionCount).ToArray() + }, + new() + { + Id = this.GenerateNextKey(), + Number = 3, + Text = "UsedByDeleteTests", + Floats = Enumerable.Repeat(0.3f, SimpleModel.DimensionCount).ToArray() + }, + new() + { + Id = this.GenerateNextKey(), + Number = 4, + Text = "UsedByDeleteBatchTests", + Floats = Enumerable.Repeat(0.4f, SimpleModel.DimensionCount).ToArray() + } + ]; + + protected override VectorStoreRecordDefinition GetRecordDefinition() + => new() + { + Properties = + [ + new VectorStoreRecordKeyProperty(nameof(SimpleModel.Id), typeof(TKey)), + new VectorStoreRecordVectorProperty(nameof(SimpleModel.Floats), typeof(ReadOnlyMemory?)) + { + Dimensions = SimpleModel.DimensionCount, + DistanceFunction = this.DistanceFunction, + IndexKind = this.IndexKind + }, + + new VectorStoreRecordDataProperty(nameof(SimpleModel.Number), typeof(int)) { IsFilterable = true }, + new VectorStoreRecordDataProperty(nameof(SimpleModel.Text), typeof(string)) { IsFilterable = true }, + ] + }; +} diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/VectorStoreCollectionFixture.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/VectorStoreCollectionFixture.cs index f6506b6fa5de..9ae5703056f2 100644 --- a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/VectorStoreCollectionFixture.cs +++ b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/VectorStoreCollectionFixture.cs @@ -13,10 +13,10 @@ public abstract class VectorStoreCollectionFixture : VectorStoreF { private List? _testData; - protected abstract string CollectionName { get; } protected abstract VectorStoreRecordDefinition GetRecordDefinition(); protected abstract List BuildTestData(); + protected virtual string CollectionName => Guid.NewGuid().ToString(); protected virtual string DistanceFunction => this.TestStore.DefaultDistanceFunction; protected virtual string IndexKind => this.TestStore.DefaultIndexKind; diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/VectorStoreFixture.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/VectorStoreFixture.cs index af4d97e95a29..e750f66f5cfc 100644 --- a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/VectorStoreFixture.cs +++ b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/VectorStoreFixture.cs @@ -13,6 +13,8 @@ public abstract class VectorStoreFixture : IAsyncLifetime public virtual string DefaultDistanceFunction => this.TestStore.DefaultDistanceFunction; public virtual string DefaultIndexKind => this.TestStore.DefaultIndexKind; + public virtual string GetUniqueCollectionName() => Guid.NewGuid().ToString(); + public virtual Task InitializeAsync() => this.TestStore.ReferenceCountingStartAsync(); @@ -20,5 +22,5 @@ public virtual Task DisposeAsync() => this.TestStore.ReferenceCountingStopAsync(); public virtual TKey GenerateNextKey() - => this.TestStore.GenerateKey(this._nextKeyValue++); + => this.TestStore.GenerateKey(Interlocked.Increment(ref this._nextKeyValue)); } From 68a5709945c321d9d5b0ea0f12fa678b665a1ba8 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 13 Mar 2025 09:44:35 +0100 Subject: [PATCH 2/9] implement CollectionConformanceTests for Redis, Weaviate, Qdrant, Mongo, InMemory, Sqlite, Cosmos and Azure AI Search --- ...zureAISearchVectorStoreRecordCollection.cs | 14 ++++++-- ...mosDBMongoDBVectorStoreRecordCollection.cs | 25 ++++++++----- .../InMemoryVectorStoreRecordCollection.cs | 14 +++++--- .../MongoDBVectorStoreRecordCollection.cs | 29 ++++++++++----- .../QdrantVectorStoreRecordCollection.cs | 25 ++++++++++--- ...RedisHashSetVectorStoreRecordCollection.cs | 22 ++++++++++-- .../RedisJsonVectorStoreRecordCollection.cs | 20 +++++++++-- ...liteVectorStoreCollectionCommandBuilder.cs | 2 +- .../WeaviateVectorStore.cs | 1 + .../WeaviateVectorStoreRecordCollection.cs | 21 ++++++++++- ...ectorStoreCollectionCommandBuilderTests.cs | 2 +- .../WeaviateKernelBuilderExtensionsTests.cs | 2 +- ...eaviateServiceCollectionExtensionsTests.cs | 2 +- ...eaviateVectorStoreRecordCollectionTests.cs | 32 ++++++++++++++--- .../WeaviateVectorStoreTests.cs | 2 +- ...AzureAISearchCollectionConformanceTests.cs | 12 +++++++ .../Support/AzureAISearchFixture.cs | 10 ++++++ ...CosmosMongoDBCollectionConformanceTests.cs | 12 +++++++ .../Support/CosmosMongoDBFixture.cs | 10 ++++++ .../CosmosNoSQLCollectionConformanceTests.cs | 12 +++++++ .../Support/CosmosNoSQLFixture.cs | 10 ++++++ .../InMemoryCollectionConformanceTests.cs | 12 +++++++ .../Support/InMemoryFixture.cs | 10 ++++++ .../MongoDBCollectionConformanceTests.cs | 12 +++++++ .../Support/MongoDBFixture.cs | 10 ++++++ .../QdrantCollectionConformanceTests.cs | 12 +++++++ .../Support/QdrantFixture.cs | 10 ++++++ ...RedisCollectionConformanceTests_HashSet.cs | 12 +++++++ .../RedisCollectionConformanceTests_Json.cs | 12 +++++++ .../Filter/RedisBasicFilterTests.cs | 8 ++--- .../Support/RedisGenericDataModelFixture.cs | 2 +- .../Support/RedisHashSetFixture.cs | 10 ++++++ .../{RedisFixture.cs => RedisJsonFixture.cs} | 4 +-- .../Support/RedisSimpleModelFixture.cs | 2 +- .../Support/RedisTestStore.cs | 12 +++---- .../SqliteCollectionConformanceTests.cs | 12 +++++++ .../Support/SqliteFixture.cs | 10 ++++++ .../Collections/CollectionConformanceTests.cs | 35 ++++++++++++++++--- .../WeaviateCollectionConformanceTests.cs | 12 +++++++ .../Support/WeaviateFixture.cs | 13 +++++++ 40 files changed, 427 insertions(+), 62 deletions(-) create mode 100644 dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/Collections/AzureAISearchCollectionConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/Support/AzureAISearchFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Collections/CosmosMongoDBCollectionConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosMongoDBFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/Collections/CosmosNoSQLCollectionConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/Support/CosmosNoSQLFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/InMemoryIntegrationTests/Collections/InMemoryCollectionConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/InMemoryIntegrationTests/Support/InMemoryFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/MongoDBIntegrationTests/Collections/MongoDBCollectionConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/MongoDBIntegrationTests/Support/MongoDBFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Collections/RedisCollectionConformanceTests_HashSet.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Collections/RedisCollectionConformanceTests_Json.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisHashSetFixture.cs rename dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/{RedisFixture.cs => RedisJsonFixture.cs} (53%) create mode 100644 dotnet/src/VectorDataIntegrationTests/SqliteIntegrationTests/Collections/SqliteCollectionConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/SqliteIntegrationTests/Support/SqliteFixture.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/Collections/WeaviateCollectionConformanceTests.cs create mode 100644 dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/Support/WeaviateFixture.cs diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreRecordCollection.cs index 5f99cbb16a67..eda71258ef24 100644 --- a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreRecordCollection.cs @@ -229,9 +229,19 @@ public virtual async Task CreateCollectionIfNotExistsAsync(CancellationToken can /// public virtual Task DeleteCollectionAsync(CancellationToken cancellationToken = default) { - return this.RunOperationAsync( + return this.RunOperationAsync( "DeleteIndex", - () => this._searchIndexClient.DeleteIndexAsync(this._collectionName, cancellationToken)); + async () => + { + try + { + return await this._searchIndexClient.DeleteIndexAsync(this._collectionName, cancellationToken).ConfigureAwait(false); + } + catch (RequestFailedException ex) when (ex.Status == 404) + { + return null!; + } + }); } /// diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollection.cs index 67bf3197395d..13e31475447d 100644 --- a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollection.cs @@ -102,20 +102,29 @@ public virtual Task CollectionExistsAsync(CancellationToken cancellationTo /// public virtual async Task CreateCollectionAsync(CancellationToken cancellationToken = default) { - await this.RunOperationAsync("CreateCollection", - () => this._mongoDatabase.CreateCollectionAsync(this.CollectionName, cancellationToken: cancellationToken)).ConfigureAwait(false); + // The IMongoDatabase.CreateCollectionAsync "Creates a new collection if not already available". + // To make sure that all the connectors are consistent, we throw when the collection exists. + if (await this.CollectionExistsAsync(cancellationToken).ConfigureAwait(false)) + { + throw new VectorStoreOperationException("Collection already exists.") + { + VectorStoreType = DatabaseName, + CollectionName = this.CollectionName, + OperationName = "CreateCollection" + }; + } - await this.RunOperationAsync("CreateIndexes", - () => this.CreateIndexesAsync(this.CollectionName, cancellationToken: cancellationToken)).ConfigureAwait(false); + await this.CreateCollectionIfNotExistsAsync(cancellationToken).ConfigureAwait(false); } /// public virtual async Task CreateCollectionIfNotExistsAsync(CancellationToken cancellationToken = default) { - if (!await this.CollectionExistsAsync(cancellationToken).ConfigureAwait(false)) - { - await this.CreateCollectionAsync(cancellationToken).ConfigureAwait(false); - } + await this.RunOperationAsync("CreateCollection", + () => this._mongoDatabase.CreateCollectionAsync(this.CollectionName, cancellationToken: cancellationToken)).ConfigureAwait(false); + + await this.RunOperationAsync("CreateIndexes", + () => this.CreateIndexesAsync(this.CollectionName, cancellationToken: cancellationToken)).ConfigureAwait(false); } /// diff --git a/dotnet/src/Connectors/Connectors.Memory.InMemory/InMemoryVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.InMemory/InMemoryVectorStoreRecordCollection.cs index 449a50796a13..6fbcdf2633bf 100644 --- a/dotnet/src/Connectors/Connectors.Memory.InMemory/InMemoryVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.InMemory/InMemoryVectorStoreRecordCollection.cs @@ -113,13 +113,19 @@ public Task CollectionExistsAsync(CancellationToken cancellationToken = de /// public Task CreateCollectionAsync(CancellationToken cancellationToken = default) { - if (!this._internalCollections.ContainsKey(this._collectionName)) + if (!this._internalCollections.ContainsKey(this._collectionName) + && this._internalCollections.TryAdd(this._collectionName, new ConcurrentDictionary()) + && this._internalCollectionTypes.TryAdd(this._collectionName, typeof(TRecord))) { - this._internalCollections.TryAdd(this._collectionName, new ConcurrentDictionary()); - this._internalCollectionTypes.TryAdd(this._collectionName, typeof(TRecord)); + return Task.CompletedTask; } - return Task.CompletedTask; + return Task.FromException(new VectorStoreOperationException("Collection already exists.") + { + VectorStoreType = "InMemory", + CollectionName = this.CollectionName, + OperationName = "CreateCollection" + }); } /// diff --git a/dotnet/src/Connectors/Connectors.Memory.MongoDB/MongoDBVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.MongoDB/MongoDBVectorStoreRecordCollection.cs index 7b749c04dc16..dc2aa163a803 100644 --- a/dotnet/src/Connectors/Connectors.Memory.MongoDB/MongoDBVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.MongoDB/MongoDBVectorStoreRecordCollection.cs @@ -104,6 +104,26 @@ public virtual Task CollectionExistsAsync(CancellationToken cancellationTo /// public virtual async Task CreateCollectionAsync(CancellationToken cancellationToken = default) { + // The IMongoDatabase.CreateCollectionAsync "Creates a new collection if not already available". + // To make sure that all the connectors are consistent, we throw when the collection exists. + if (await this.CollectionExistsAsync(cancellationToken).ConfigureAwait(false)) + { + throw new VectorStoreOperationException("Collection already exists.") + { + VectorStoreType = DatabaseName, + CollectionName = this.CollectionName, + OperationName = "CreateCollection" + }; + } + + await this.CreateCollectionIfNotExistsAsync(cancellationToken).ConfigureAwait(false); + } + + /// + public virtual async Task CreateCollectionIfNotExistsAsync(CancellationToken cancellationToken = default) + { + // The IMongoDatabase.CreateCollectionAsync "Creates a new collection if not already available". + // So for CreateCollectionIfNotExistsAsync, we don't perform an additional check. await this.RunOperationAsync("CreateCollection", () => this._mongoDatabase.CreateCollectionAsync(this.CollectionName, cancellationToken: cancellationToken)).ConfigureAwait(false); @@ -115,15 +135,6 @@ await this.RunOperationWithRetryAsync( cancellationToken).ConfigureAwait(false); } - /// - public virtual async Task CreateCollectionIfNotExistsAsync(CancellationToken cancellationToken = default) - { - if (!await this.CollectionExistsAsync(cancellationToken).ConfigureAwait(false)) - { - await this.CreateCollectionAsync(cancellationToken).ConfigureAwait(false); - } - } - /// public virtual async Task DeleteAsync(string key, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStoreRecordCollection.cs index 4d8db3e63e81..760aeaae24f4 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStoreRecordCollection.cs @@ -238,11 +238,26 @@ public virtual async Task CreateCollectionIfNotExistsAsync(CancellationToken can /// public virtual Task DeleteCollectionAsync(CancellationToken cancellationToken = default) - { - return this.RunOperationAsync( - "DeleteCollection", - () => this._qdrantClient.DeleteCollectionAsync(this._collectionName, null, cancellationToken)); - } + => this.RunOperationAsync("DeleteCollection", + async () => + { + try + { + await this._qdrantClient.DeleteCollectionAsync(this._collectionName, null, cancellationToken).ConfigureAwait(false); + } + catch (QdrantException) + { + // There is no reliable way to check if the operation failed because the + // collection does not exist based on the exception itself. + // So we just check here if it exists, and if not, ignore the exception. + if (!await this.CollectionExistsAsync(cancellationToken).ConfigureAwait(false)) + { + return; + } + + throw; + } + }); /// public virtual async Task GetAsync(ulong key, GetRecordOptions? options = null, CancellationToken cancellationToken = default) diff --git a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisHashSetVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisHashSetVectorStoreRecordCollection.cs index 6c99d9b03d8e..a08fe1e86628 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisHashSetVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisHashSetVectorStoreRecordCollection.cs @@ -195,9 +195,25 @@ public virtual async Task CreateCollectionIfNotExistsAsync(CancellationToken can } /// - public virtual Task DeleteCollectionAsync(CancellationToken cancellationToken = default) + public virtual async Task DeleteCollectionAsync(CancellationToken cancellationToken = default) { - return this.RunOperationAsync("FT.DROPINDEX", () => this._database.FT().DropIndexAsync(this._collectionName)); + try + { + await this.RunOperationAsync("FT.DROPINDEX", + () => this._database.FT().DropIndexAsync(this._collectionName)).ConfigureAwait(false); + } + catch (VectorStoreOperationException ex) when (ex.InnerException is RedisServerException) + { + // The RedisServerException does not expose any reliable way of checking if the index does not exist. + // It just sets the message to "Unknown index name". + // We catch the exception and ignore it, but only after checking that the index does not exist. + if (!await this.CollectionExistsAsync(cancellationToken).ConfigureAwait(false)) + { + return; + } + + throw; + } } /// @@ -425,7 +441,7 @@ private async Task RunOperationAsync(string operationName, Func> o { return await operation.Invoke().ConfigureAwait(false); } - catch (RedisConnectionException ex) + catch (RedisException ex) { throw new VectorStoreOperationException("Call to vector store failed.", ex) { diff --git a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisJsonVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisJsonVectorStoreRecordCollection.cs index 14a8e56222d9..af6a0a7d220f 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisJsonVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisJsonVectorStoreRecordCollection.cs @@ -177,9 +177,25 @@ public virtual async Task CreateCollectionIfNotExistsAsync(CancellationToken can } /// - public virtual Task DeleteCollectionAsync(CancellationToken cancellationToken = default) + public virtual async Task DeleteCollectionAsync(CancellationToken cancellationToken = default) { - return this.RunOperationAsync("FT.DROPINDEX", () => this._database.FT().DropIndexAsync(this._collectionName)); + try + { + await this.RunOperationAsync("FT.DROPINDEX", + () => this._database.FT().DropIndexAsync(this._collectionName)).ConfigureAwait(false); + } + catch (VectorStoreOperationException ex) when (ex.InnerException is RedisServerException) + { + // The RedisServerException does not expose any reliable way of checking if the index does not exist. + // It just sets the message to "Unknown index name". + // We catch the exception and ignore it, but only after checking that the index does not exist. + if (!await this.CollectionExistsAsync(cancellationToken).ConfigureAwait(false)) + { + return; + } + + throw; + } } /// diff --git a/dotnet/src/Connectors/Connectors.Memory.Sqlite/SqliteVectorStoreCollectionCommandBuilder.cs b/dotnet/src/Connectors/Connectors.Memory.Sqlite/SqliteVectorStoreCollectionCommandBuilder.cs index 837e3044ddc7..701c61bb3236 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Sqlite/SqliteVectorStoreCollectionCommandBuilder.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Sqlite/SqliteVectorStoreCollectionCommandBuilder.cs @@ -87,7 +87,7 @@ public DbCommand BuildCreateVirtualTableCommand( public DbCommand BuildDropTableCommand(string tableName) { - string query = $"DROP TABLE [{tableName}];"; + string query = $"DROP TABLE IF EXISTS [{tableName}];"; var command = this._connection.CreateCommand(); diff --git a/dotnet/src/Connectors/Connectors.Memory.Weaviate/WeaviateVectorStore.cs b/dotnet/src/Connectors/Connectors.Memory.Weaviate/WeaviateVectorStore.cs index dbf7f46b5f59..1c45d1e3ac65 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Weaviate/WeaviateVectorStore.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Weaviate/WeaviateVectorStore.cs @@ -43,6 +43,7 @@ public WeaviateVectorStore(HttpClient httpClient, WeaviateVectorStoreOptions? op } /// + /// The collection name must start with a capital letter and contain only ASCII letters and digits. public virtual IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null) where TKey : notnull { diff --git a/dotnet/src/Connectors/Connectors.Memory.Weaviate/WeaviateVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.Weaviate/WeaviateVectorStoreRecordCollection.cs index 98d38fc9a8c1..ccf3264b41d1 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Weaviate/WeaviateVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Weaviate/WeaviateVectorStoreRecordCollection.cs @@ -118,6 +118,7 @@ public class WeaviateVectorStoreRecordCollection : IVectorStoreRecordCo /// /// The name of the collection that this will access. /// Optional configuration options for this class. + /// The collection name must start with a capital letter and contain only ASCII letters and digits. public WeaviateVectorStoreRecordCollection( HttpClient httpClient, string collectionName, @@ -125,7 +126,7 @@ public WeaviateVectorStoreRecordCollection( { // Verify. Verify.NotNull(httpClient); - Verify.NotNullOrWhiteSpace(collectionName); + VerifyCollectionName(collectionName); var endpoint = (options?.Endpoint ?? httpClient.BaseAddress) ?? throw new ArgumentException($"Weaviate endpoint should be provided via HttpClient.BaseAddress property or {nameof(WeaviateVectorStoreRecordCollectionOptions)} options parameter."); @@ -535,5 +536,23 @@ private static void VerifyVectorParam(TVector vector) } } + private static void VerifyCollectionName(string collectionName) + { + Verify.NotNullOrWhiteSpace(collectionName); + + char first = collectionName[0]; + if (!(first is >= 'A' and <= 'Z')) + { + throw new ArgumentException("Collection name must start with an uppercase ASCII letter.", nameof(collectionName)); + } + + foreach (char character in collectionName) + { + if (!((character is >= 'a' and <= 'z') || (character is >= 'A' and <= 'Z') || (character is >= '0' and <= '9'))) + { + throw new ArgumentException("Collection name must contain only ASCII letters and digits.", nameof(collectionName)); + } + } + } #endregion } diff --git a/dotnet/src/Connectors/Connectors.Sqlite.UnitTests/SqliteVectorStoreCollectionCommandBuilderTests.cs b/dotnet/src/Connectors/Connectors.Sqlite.UnitTests/SqliteVectorStoreCollectionCommandBuilderTests.cs index 370756cb4344..314e1e162420 100644 --- a/dotnet/src/Connectors/Connectors.Sqlite.UnitTests/SqliteVectorStoreCollectionCommandBuilderTests.cs +++ b/dotnet/src/Connectors/Connectors.Sqlite.UnitTests/SqliteVectorStoreCollectionCommandBuilderTests.cs @@ -104,7 +104,7 @@ public void ItBuildsDropTableCommand() var command = this._commandBuilder.BuildDropTableCommand(TableName); // Assert - Assert.Equal("DROP TABLE [TestTable];", command.CommandText); + Assert.Equal("DROP TABLE IF EXISTS [TestTable];", command.CommandText); } [Theory] diff --git a/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateKernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateKernelBuilderExtensionsTests.cs index fb4c27773431..23b34cdbc2ba 100644 --- a/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateKernelBuilderExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateKernelBuilderExtensionsTests.cs @@ -43,7 +43,7 @@ public void AddWeaviateVectorStoreRecordCollectionRegistersClass() this._kernelBuilder.Services.AddSingleton(httpClient); // Act - this._kernelBuilder.AddWeaviateVectorStoreRecordCollection("testcollection"); + this._kernelBuilder.AddWeaviateVectorStoreRecordCollection("Testcollection"); var kernel = this._kernelBuilder.Build(); diff --git a/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateServiceCollectionExtensionsTests.cs index 07efc26dd22d..e33f735ebc4f 100644 --- a/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateServiceCollectionExtensionsTests.cs @@ -43,7 +43,7 @@ public void AddVectorStoreRecordCollectionRegistersClass() this._serviceCollection.AddSingleton(httpClient); // Act - this._serviceCollection.AddWeaviateVectorStoreRecordCollection("testcollection"); + this._serviceCollection.AddWeaviateVectorStoreRecordCollection("Testcollection"); // Assert this.AssertVectorStoreRecordCollectionCreated(); diff --git a/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateVectorStoreRecordCollectionTests.cs b/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateVectorStoreRecordCollectionTests.cs index f31cd9929360..0b3e39cac291 100644 --- a/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateVectorStoreRecordCollectionTests.cs @@ -32,7 +32,7 @@ public WeaviateVectorStoreRecordCollectionTests() public void ConstructorForModelWithoutKeyThrowsException() { // Act & Assert - var exception = Assert.Throws(() => new WeaviateVectorStoreRecordCollection(this._mockHttpClient, "collection")); + var exception = Assert.Throws(() => new WeaviateVectorStoreRecordCollection(this._mockHttpClient, "Collection")); Assert.Contains("No key property found", exception.Message); } @@ -43,7 +43,7 @@ public void ConstructorWithoutEndpointThrowsException() using var httpClient = new HttpClient(); // Act & Assert - var exception = Assert.Throws(() => new WeaviateVectorStoreRecordCollection(httpClient, "collection")); + var exception = Assert.Throws(() => new WeaviateVectorStoreRecordCollection(httpClient, "Collection")); Assert.Contains("Weaviate endpoint should be provided", exception.Message); } @@ -53,7 +53,7 @@ public void ConstructorWithDeclarativeModelInitializesCollection() // Act & Assert var collection = new WeaviateVectorStoreRecordCollection( this._mockHttpClient, - "collection"); + "Collection"); Assert.NotNull(collection); } @@ -70,7 +70,7 @@ public void ConstructorWithImperativeModelInitializesCollection() // Act var collection = new WeaviateVectorStoreRecordCollection( this._mockHttpClient, - "collection", + "Collection", new() { VectorStoreRecordDefinition = definition }); // Assert @@ -93,6 +93,30 @@ public async Task CollectionExistsReturnsValidResultAsync(HttpResponseMessage re Assert.Equal(expectedResult, actualResult); } + [Theory] + [InlineData("notStartingWithCapitalLetter")] + [InlineData("0startingWithDigit")] + [InlineData("contains spaces")] + [InlineData("contains-dashes")] + [InlineData("contains_underscores")] + [InlineData("contains$specialCharacters")] + [InlineData("contains!specialCharacters")] + [InlineData("contains@specialCharacters")] + [InlineData("contains#specialCharacters")] + [InlineData("contains%specialCharacters")] + [InlineData("contains^specialCharacters")] + [InlineData("contains&specialCharacters")] + [InlineData("contains*specialCharacters")] + [InlineData("contains(specialCharacters")] + [InlineData("contains)specialCharacters")] + [InlineData("containsNonAsciiĄ")] + [InlineData("containsNonAsciią")] + public void CollectionCtorRejectsInvalidNames(string collectionName) + { + ArgumentException argumentException = Assert.Throws(() => new WeaviateVectorStoreRecordCollection(this._mockHttpClient, collectionName)); + Assert.Equal("collectionName", argumentException.ParamName); + } + [Fact] public async Task CreateCollectionUsesValidCollectionSchemaAsync() { diff --git a/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateVectorStoreTests.cs b/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateVectorStoreTests.cs index e51af8124daf..5a99f4c1ee20 100644 --- a/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateVectorStoreTests.cs +++ b/dotnet/src/Connectors/Connectors.Weaviate.UnitTests/WeaviateVectorStoreTests.cs @@ -44,7 +44,7 @@ public void GetCollectionWithSupportedKeyReturnsCollection() var sut = new WeaviateVectorStore(this._mockHttpClient); // Act - var collection = sut.GetCollection("collection1"); + var collection = sut.GetCollection("Collection1"); // Assert Assert.NotNull(collection); diff --git a/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/Collections/AzureAISearchCollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/Collections/AzureAISearchCollectionConformanceTests.cs new file mode 100644 index 000000000000..5ae3ad191f4b --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/Collections/AzureAISearchCollectionConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using AzureAISearchIntegrationTests.Support; +using VectorDataSpecificationTests.Collections; +using Xunit; + +namespace AzureAISearchIntegrationTests.Collections; + +public class AzureAISearchCollectionConformanceTests(AzureAISearchFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/Support/AzureAISearchFixture.cs b/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/Support/AzureAISearchFixture.cs new file mode 100644 index 000000000000..b5a6cf4f5c49 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/Support/AzureAISearchFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace AzureAISearchIntegrationTests.Support; + +public class AzureAISearchFixture : VectorStoreFixture +{ + public override TestStore TestStore => AzureAISearchTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Collections/CosmosMongoDBCollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Collections/CosmosMongoDBCollectionConformanceTests.cs new file mode 100644 index 000000000000..f8f03906747a --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Collections/CosmosMongoDBCollectionConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using CosmosMongoDBIntegrationTests.Support; +using VectorDataSpecificationTests.Collections; +using Xunit; + +namespace CosmosMongoDBIntegrationTests.Collections; + +public class CosmosMongoDBCollectionConformanceTests(CosmosMongoDBFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosMongoDBFixture.cs b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosMongoDBFixture.cs new file mode 100644 index 000000000000..69be997865f6 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosMongoDBFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace CosmosMongoDBIntegrationTests.Support; + +public class CosmosMongoDBFixture : VectorStoreFixture +{ + public override TestStore TestStore => CosmosMongoDBTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/Collections/CosmosNoSQLCollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/Collections/CosmosNoSQLCollectionConformanceTests.cs new file mode 100644 index 000000000000..222e868a8e4b --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/Collections/CosmosNoSQLCollectionConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using CosmosNoSQLIntegrationTests.Support; +using VectorDataSpecificationTests.Collections; +using Xunit; + +namespace CosmosNoSQLIntegrationTests.Collections; + +public class CosmosNoSQLCollectionConformanceTests(CosmosNoSQLFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/Support/CosmosNoSQLFixture.cs b/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/Support/CosmosNoSQLFixture.cs new file mode 100644 index 000000000000..9ddaad05be85 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/Support/CosmosNoSQLFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace CosmosNoSQLIntegrationTests.Support; + +public class CosmosNoSQLFixture : VectorStoreFixture +{ + public override TestStore TestStore => CosmosNoSqlTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/InMemoryIntegrationTests/Collections/InMemoryCollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/InMemoryIntegrationTests/Collections/InMemoryCollectionConformanceTests.cs new file mode 100644 index 000000000000..ceddaf074da9 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/InMemoryIntegrationTests/Collections/InMemoryCollectionConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using InMemoryIntegrationTests.Support; +using VectorDataSpecificationTests.Collections; +using Xunit; + +namespace InMemoryIntegrationTests.Collections; + +public class InMemoryCollectionConformanceTests(InMemoryFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/InMemoryIntegrationTests/Support/InMemoryFixture.cs b/dotnet/src/VectorDataIntegrationTests/InMemoryIntegrationTests/Support/InMemoryFixture.cs new file mode 100644 index 000000000000..6a7eddb89dc3 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/InMemoryIntegrationTests/Support/InMemoryFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace InMemoryIntegrationTests.Support; + +public class InMemoryFixture : VectorStoreFixture +{ + public override TestStore TestStore => InMemoryTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/MongoDBIntegrationTests/Collections/MongoDBCollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/MongoDBIntegrationTests/Collections/MongoDBCollectionConformanceTests.cs new file mode 100644 index 000000000000..587dd6541f33 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/MongoDBIntegrationTests/Collections/MongoDBCollectionConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using MongoDBIntegrationTests.Support; +using VectorDataSpecificationTests.Collections; +using Xunit; + +namespace MongoDBIntegrationTests.Collections; + +public class MongoDBCollectionConformanceTests(MongoDBFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/MongoDBIntegrationTests/Support/MongoDBFixture.cs b/dotnet/src/VectorDataIntegrationTests/MongoDBIntegrationTests/Support/MongoDBFixture.cs new file mode 100644 index 000000000000..00c429506cec --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/MongoDBIntegrationTests/Support/MongoDBFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace MongoDBIntegrationTests.Support; + +public class MongoDBFixture : VectorStoreFixture +{ + public override TestStore TestStore => MongoDBTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests.cs new file mode 100644 index 000000000000..fd74e874a9b5 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using QdrantIntegrationTests.Support; +using VectorDataSpecificationTests.Collections; +using Xunit; + +namespace QdrantIntegrationTests.Collections; + +public class QdrantCollectionConformanceTests(QdrantFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantFixture.cs b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantFixture.cs new file mode 100644 index 000000000000..208ec927aa9e --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace QdrantIntegrationTests.Support; + +public class QdrantFixture : VectorStoreFixture +{ + public override TestStore TestStore => QdrantTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Collections/RedisCollectionConformanceTests_HashSet.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Collections/RedisCollectionConformanceTests_HashSet.cs new file mode 100644 index 000000000000..a3b7c411d8e4 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Collections/RedisCollectionConformanceTests_HashSet.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using RedisIntegrationTests.Support; +using VectorDataSpecificationTests.Collections; +using Xunit; + +namespace RedisIntegrationTests.Collections; + +public class RedisCollectionConformanceTests_HashSet(RedisHashSetFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Collections/RedisCollectionConformanceTests_Json.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Collections/RedisCollectionConformanceTests_Json.cs new file mode 100644 index 000000000000..97d28ef6d17e --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Collections/RedisCollectionConformanceTests_Json.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using RedisIntegrationTests.Support; +using VectorDataSpecificationTests.Collections; +using Xunit; + +namespace RedisIntegrationTests.Collections; + +public class RedisCollectionConformanceTests_Json(RedisJsonFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Filter/RedisBasicFilterTests.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Filter/RedisBasicFilterTests.cs index fe50977e006e..7ec4f834a5f0 100644 --- a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Filter/RedisBasicFilterTests.cs +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Filter/RedisBasicFilterTests.cs @@ -70,7 +70,7 @@ public class RedisJsonCollectionBasicFilterTests(RedisJsonCollectionBasicFilterT { public new class Fixture : BasicFilterTests.Fixture { - public override TestStore TestStore => RedisTestStore.Instance; + public override TestStore TestStore => RedisTestStore.JsonInstance; protected override string CollectionName => "JsonCollectionFilterTests"; @@ -83,7 +83,7 @@ protected override VectorStoreRecordDefinition GetRecordDefinition() protected override IVectorStoreRecordCollection CreateCollection() => new RedisJsonVectorStoreRecordCollection( - RedisTestStore.Instance.Database, + RedisTestStore.JsonInstance.Database, this.CollectionName, new() { VectorStoreRecordDefinition = this.GetRecordDefinition() }); } @@ -122,7 +122,7 @@ public override Task Legacy_AnyTagEqualTo_List() public new class Fixture : BasicFilterTests.Fixture { - public override TestStore TestStore => RedisTestStore.Instance; + public override TestStore TestStore => RedisTestStore.HashSetInstance; protected override string CollectionName => "HashSetCollectionFilterTests"; @@ -138,7 +138,7 @@ protected override VectorStoreRecordDefinition GetRecordDefinition() protected override IVectorStoreRecordCollection CreateCollection() => new RedisHashSetVectorStoreRecordCollection( - RedisTestStore.Instance.Database, + RedisTestStore.HashSetInstance.Database, this.CollectionName, new() { VectorStoreRecordDefinition = this.GetRecordDefinition() }); diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisGenericDataModelFixture.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisGenericDataModelFixture.cs index f5297b67bcb0..3a63d1d77f76 100644 --- a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisGenericDataModelFixture.cs +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisGenericDataModelFixture.cs @@ -6,5 +6,5 @@ namespace RedisIntegrationTests.Support; public class RedisGenericDataModelFixture : GenericDataModelFixture { - public override TestStore TestStore => RedisTestStore.Instance; + public override TestStore TestStore => RedisTestStore.JsonInstance; } diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisHashSetFixture.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisHashSetFixture.cs new file mode 100644 index 000000000000..72e624712364 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisHashSetFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace RedisIntegrationTests.Support; + +public class RedisHashSetFixture : VectorStoreFixture +{ + public override TestStore TestStore => RedisTestStore.HashSetInstance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisFixture.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisJsonFixture.cs similarity index 53% rename from dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisFixture.cs rename to dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisJsonFixture.cs index a8c7004eaec2..45fe503ba13c 100644 --- a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisFixture.cs +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisJsonFixture.cs @@ -4,7 +4,7 @@ namespace RedisIntegrationTests.Support; -public class RedisFixture : VectorStoreFixture +public class RedisJsonFixture : VectorStoreFixture { - public override TestStore TestStore => RedisTestStore.Instance; + public override TestStore TestStore => RedisTestStore.JsonInstance; } diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisSimpleModelFixture.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisSimpleModelFixture.cs index eb1d8dd70160..f91aefd9055c 100644 --- a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisSimpleModelFixture.cs +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisSimpleModelFixture.cs @@ -6,5 +6,5 @@ namespace RedisIntegrationTests.Support; public class RedisSimpleModelFixture : SimpleModelFixture { - public override TestStore TestStore => RedisTestStore.Instance; + public override TestStore TestStore => RedisTestStore.JsonInstance; } diff --git a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisTestStore.cs b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisTestStore.cs index a1dd2f02c0bc..6ee6f058da46 100644 --- a/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisTestStore.cs +++ b/dotnet/src/VectorDataIntegrationTests/RedisIntegrationTests/Support/RedisTestStore.cs @@ -10,15 +10,19 @@ namespace RedisIntegrationTests.Support; internal sealed class RedisTestStore : TestStore { - public static RedisTestStore Instance { get; } = new(); + public static RedisTestStore JsonInstance { get; } = new(RedisStorageType.Json); + public static RedisTestStore HashSetInstance { get; } = new(RedisStorageType.HashSet); private readonly RedisContainer _container = new RedisBuilder() .WithImage("redis/redis-stack") .Build(); + private readonly RedisStorageType _storageType; private IDatabase? _database; private RedisVectorStore? _defaultVectorStore; + private RedisTestStore(RedisStorageType storageType) => this._storageType = storageType; + public IDatabase Database => this._database ?? throw new InvalidOperationException("Not initialized"); public override IVectorStore DefaultVectorStore => this._defaultVectorStore ?? throw new InvalidOperationException("Not initialized"); @@ -26,16 +30,12 @@ internal sealed class RedisTestStore : TestStore public RedisVectorStore GetVectorStore(RedisVectorStoreOptions options) => new(this.Database, options); - private RedisTestStore() - { - } - protected override async Task StartAsync() { await this._container.StartAsync(); var redis = await ConnectionMultiplexer.ConnectAsync($"{this._container.Hostname}:{this._container.GetMappedPublicPort(6379)},connectTimeout=60000,connectRetry=5"); this._database = redis.GetDatabase(); - this._defaultVectorStore = new(this._database); + this._defaultVectorStore = new(this._database, new() { StorageType = this._storageType }); } protected override Task StopAsync() diff --git a/dotnet/src/VectorDataIntegrationTests/SqliteIntegrationTests/Collections/SqliteCollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/SqliteIntegrationTests/Collections/SqliteCollectionConformanceTests.cs new file mode 100644 index 000000000000..32af892024c0 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/SqliteIntegrationTests/Collections/SqliteCollectionConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using SqliteIntegrationTests.Support; +using VectorDataSpecificationTests.Collections; +using Xunit; + +namespace SqliteIntegrationTests.Collections; + +public class SqliteCollectionConformanceTests(SqliteFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/SqliteIntegrationTests/Support/SqliteFixture.cs b/dotnet/src/VectorDataIntegrationTests/SqliteIntegrationTests/Support/SqliteFixture.cs new file mode 100644 index 000000000000..7cd6da2d48c1 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/SqliteIntegrationTests/Support/SqliteFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace SqliteIntegrationTests.Support; + +public class SqliteFixture : VectorStoreFixture +{ + public override TestStore TestStore => SqliteTestStore.Instance; +} diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Collections/CollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Collections/CollectionConformanceTests.cs index ca5a0c1dfc20..16f8679df842 100644 --- a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Collections/CollectionConformanceTests.cs +++ b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Collections/CollectionConformanceTests.cs @@ -8,7 +8,7 @@ namespace VectorDataSpecificationTests.Collections; -public class CollectionConformanceTests(VectorStoreFixture fixture) where TKey : notnull +public abstract class CollectionConformanceTests(VectorStoreFixture fixture) where TKey : notnull { [ConditionalFact] public Task DeleteCollectionDoesNotThrowForNonExistingCollection() @@ -18,6 +18,14 @@ public Task DeleteCollectionDoesNotThrowForNonExistingCollection() public Task DeleteCollectionDoesNotThrowForNonExistingCollection_GenericDataModel() => this.DeleteNonExistingCollection>(); + [ConditionalFact] + public Task CreateCollectionCreatesTheCollection() + => this.CreateCollection>(); + + [ConditionalFact] + public Task CreateCollectionCreatesTheCollection_GenericDataModel() + => this.CreateCollection>(); + [ConditionalFact] public Task CreateCollectionIfNotExistsCalledMoreThanOnceDoesNotThrow() => this.CreateCollectionIfNotExistsMoreThanOnce>(); @@ -69,14 +77,31 @@ private async Task DeleteNonExistingCollection() await collection.DeleteCollectionAsync(); } - private async Task CreateCollectionIfNotExistsMoreThanOnce() + private async Task CreateCollection() { var collection = await this.GetNonExistingCollectionAsync(); + await collection.CreateCollectionAsync(); + try { - await collection.CreateCollectionIfNotExistsAsync(); + Assert.True(await collection.CollectionExistsAsync()); + Assert.True(await fixture.TestStore.DefaultVectorStore.ListCollectionNamesAsync().ContainsAsync(collection.CollectionName)); + } + finally + { + await collection.DeleteCollectionAsync(); + } + } + + private async Task CreateCollectionIfNotExistsMoreThanOnce() + { + var collection = await this.GetNonExistingCollectionAsync(); + + await collection.CreateCollectionIfNotExistsAsync(); + try + { Assert.True(await collection.CollectionExistsAsync()); Assert.True(await fixture.TestStore.DefaultVectorStore.ListCollectionNamesAsync().ContainsAsync(collection.CollectionName)); @@ -92,10 +117,10 @@ private async Task CreateCollectionMoreThanOnce() { var collection = await this.GetNonExistingCollectionAsync(); + await collection.CreateCollectionAsync(); + try { - await collection.CreateCollectionAsync(); - Assert.True(await collection.CollectionExistsAsync()); Assert.True(await fixture.TestStore.DefaultVectorStore.ListCollectionNamesAsync().ContainsAsync(collection.CollectionName)); diff --git a/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/Collections/WeaviateCollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/Collections/WeaviateCollectionConformanceTests.cs new file mode 100644 index 000000000000..e839b02ad942 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/Collections/WeaviateCollectionConformanceTests.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Collections; +using WeaviateIntegrationTests.Support; +using Xunit; + +namespace WeaviateIntegrationTests.Collections; + +public class WeaviateCollectionConformanceTests(WeaviateFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/Support/WeaviateFixture.cs b/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/Support/WeaviateFixture.cs new file mode 100644 index 000000000000..ce408e7e7eac --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/Support/WeaviateFixture.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace WeaviateIntegrationTests.Support; + +public class WeaviateFixture : VectorStoreFixture +{ + public override TestStore TestStore => WeaviateTestStore.Instance; + + // Weaviate requires the name to start with a capital letter and not contain any chars other than a-Z and 0-9. + public override string GetUniqueCollectionName() => $"A{Guid.NewGuid():N}"; +} From d7f149d1b20d1a289f54e71e1964c29a388d269b Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 13 Mar 2025 15:42:40 +0100 Subject: [PATCH 3/9] copy the testsettings.json files to bin when they are present --- .../AzureAISearchIntegrationTests.csproj | 9 +++++++++ .../CosmosMongoDBIntegrationTests.csproj | 9 +++++++++ .../CosmosNoSQLIntegrationTests.csproj | 9 +++++++++ 3 files changed, 27 insertions(+) diff --git a/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/AzureAISearchIntegrationTests.csproj b/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/AzureAISearchIntegrationTests.csproj index 0fcc13f45809..688796758267 100644 --- a/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/AzureAISearchIntegrationTests.csproj +++ b/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/AzureAISearchIntegrationTests.csproj @@ -28,4 +28,13 @@ + + + Always + + + Always + + + diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/CosmosMongoDBIntegrationTests.csproj b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/CosmosMongoDBIntegrationTests.csproj index aaf0dcf8160b..84515a000e81 100644 --- a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/CosmosMongoDBIntegrationTests.csproj +++ b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/CosmosMongoDBIntegrationTests.csproj @@ -26,4 +26,13 @@ + + + Always + + + Always + + + diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/CosmosNoSQLIntegrationTests.csproj b/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/CosmosNoSQLIntegrationTests.csproj index dd8e3f7a9ba0..715b01505df7 100644 --- a/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/CosmosNoSQLIntegrationTests.csproj +++ b/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/CosmosNoSQLIntegrationTests.csproj @@ -26,4 +26,13 @@ + + + Always + + + Always + + + From 61aa3ae8e7bb403340ecf03b88f7b52a98ccbf6c Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 13 Mar 2025 15:43:52 +0100 Subject: [PATCH 4/9] adjust the namespaces of CosmosMongoDBIntegrationTests project --- .../CosmosMongoDBIntegrationTests.csproj | 2 +- .../Filter/CosmosMongoBasicFilterTests.cs | 4 ++-- .../Properties/AssemblyAttributes.cs | 2 +- .../Support/CosmosConnectionStringRequiredAttribute.cs | 3 +-- .../Support/CosmosMongoDBTestEnvironment.cs | 2 +- .../Support/CosmosMongoDBTestStore.cs | 3 +-- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/CosmosMongoDBIntegrationTests.csproj b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/CosmosMongoDBIntegrationTests.csproj index 84515a000e81..59a720d7dddd 100644 --- a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/CosmosMongoDBIntegrationTests.csproj +++ b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/CosmosMongoDBIntegrationTests.csproj @@ -6,7 +6,7 @@ enable true false - MongoDBIntegrationTests + CosmosMongoDBIntegrationTests diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Filter/CosmosMongoBasicFilterTests.cs b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Filter/CosmosMongoBasicFilterTests.cs index f7e52a5ec19a..50dfc677ad00 100644 --- a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Filter/CosmosMongoBasicFilterTests.cs +++ b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Filter/CosmosMongoBasicFilterTests.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. -using MongoDBIntegrationTests.Support; +using CosmosMongoDBIntegrationTests.Support; using VectorDataSpecificationTests.Filter; using VectorDataSpecificationTests.Support; using VectorDataSpecificationTests.Xunit; using Xunit; -namespace MongoDBIntegrationTests.Filter; +namespace CosmosMongoDBIntegrationTests.Filter; public class CosmosMongoBasicFilterTests(CosmosMongoBasicFilterTests.Fixture fixture) : BasicFilterTests(fixture), IClassFixture diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Properties/AssemblyAttributes.cs b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Properties/AssemblyAttributes.cs index 4e8438d68759..ed3fb05ab20f 100644 --- a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Properties/AssemblyAttributes.cs +++ b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Properties/AssemblyAttributes.cs @@ -1,3 +1,3 @@ // Copyright (c) Microsoft. All rights reserved. -[assembly: CosmosIntegrationTests.Support.CosmosConnectionStringRequired] +[assembly: CosmosMongoDBIntegrationTests.Support.CosmosConnectionStringRequired] diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosConnectionStringRequiredAttribute.cs b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosConnectionStringRequiredAttribute.cs index c944d36eb78c..7aaba96f3cfe 100644 --- a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosConnectionStringRequiredAttribute.cs +++ b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosConnectionStringRequiredAttribute.cs @@ -1,9 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. -using CosmosNoSQLIntegrationTests.Support; using VectorDataSpecificationTests.Xunit; -namespace CosmosIntegrationTests.Support; +namespace CosmosMongoDBIntegrationTests.Support; /// /// Checks whether the sqlite_vec extension is properly installed, and skips the test(s) otherwise. diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosMongoDBTestEnvironment.cs b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosMongoDBTestEnvironment.cs index 1adcb225e66d..df6550d05237 100644 --- a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosMongoDBTestEnvironment.cs +++ b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosMongoDBTestEnvironment.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Configuration; -namespace CosmosNoSQLIntegrationTests.Support; +namespace CosmosMongoDBIntegrationTests.Support; #pragma warning disable CA1810 // Initialize all static fields when those fields are declared diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosMongoDBTestStore.cs b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosMongoDBTestStore.cs index b0d4c379ecf4..fba1d18c8a7f 100644 --- a/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosMongoDBTestStore.cs +++ b/dotnet/src/VectorDataIntegrationTests/CosmosMongoDBIntegrationTests/Support/CosmosMongoDBTestStore.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. -using CosmosNoSQLIntegrationTests.Support; using Microsoft.Extensions.VectorData; using Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; using MongoDB.Driver; using VectorDataSpecificationTests.Support; -namespace MongoDBIntegrationTests.Support; +namespace CosmosMongoDBIntegrationTests.Support; public sealed class CosmosMongoDBTestStore : TestStore { From 66c8db4c20a855ae0a4ca9d31e1f4df2b1996bd5 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 13 Mar 2025 15:46:02 +0100 Subject: [PATCH 5/9] add link to the docs to show where the changes are coming from --- .../WeaviateVectorStoreRecordCollection.cs | 1 + .../WeaviateIntegrationTests/Support/WeaviateFixture.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/dotnet/src/Connectors/Connectors.Memory.Weaviate/WeaviateVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.Weaviate/WeaviateVectorStoreRecordCollection.cs index ccf3264b41d1..393b9a841cbb 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Weaviate/WeaviateVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Weaviate/WeaviateVectorStoreRecordCollection.cs @@ -540,6 +540,7 @@ private static void VerifyCollectionName(string collectionName) { Verify.NotNullOrWhiteSpace(collectionName); + // Based on https://weaviate.io/developers/weaviate/starter-guides/managing-collections#collection--property-names char first = collectionName[0]; if (!(first is >= 'A' and <= 'Z')) { diff --git a/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/Support/WeaviateFixture.cs b/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/Support/WeaviateFixture.cs index ce408e7e7eac..ac3b64f89006 100644 --- a/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/Support/WeaviateFixture.cs +++ b/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/Support/WeaviateFixture.cs @@ -9,5 +9,6 @@ public class WeaviateFixture : VectorStoreFixture public override TestStore TestStore => WeaviateTestStore.Instance; // Weaviate requires the name to start with a capital letter and not contain any chars other than a-Z and 0-9. + // Source: https://weaviate.io/developers/weaviate/starter-guides/managing-collections#collection--property-names public override string GetUniqueCollectionName() => $"A{Guid.NewGuid():N}"; } From fe13b406cfe60c59f465162382618b886f71d679 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 13 Mar 2025 18:08:11 +0100 Subject: [PATCH 6/9] fix the tests --- ...MongoDBVectorStoreRecordCollectionTests.cs | 32 +++++++++++++++---- ...MongoDBVectorStoreRecordCollectionTests.cs | 32 +++++++++++++++---- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreRecordCollectionTests.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreRecordCollectionTests.cs index 6ef6337e3426..60e2584bf754 100644 --- a/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreRecordCollectionTests.cs @@ -121,6 +121,15 @@ public async Task CreateCollectionInvokesValidMethodsAsync(bool indexExists, int .Setup(l => l.Current) .Returns(indexes); + var mockCursor = new Mock>(); + mockCursor + .Setup(l => l.MoveNextAsync(It.IsAny())) + .ReturnsAsync(true); + + mockCursor + .Setup(l => l.Current) + .Returns([]); + var mockMongoIndexManager = new Mock>(); mockMongoIndexManager @@ -131,6 +140,10 @@ public async Task CreateCollectionInvokesValidMethodsAsync(bool indexExists, int .Setup(l => l.Indexes) .Returns(mockMongoIndexManager.Object); + this._mockMongoDatabase + .Setup(l => l.ListCollectionNamesAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockCursor.Object); + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection(this._mockMongoDatabase.Object, CollectionName); // Act @@ -140,7 +153,11 @@ public async Task CreateCollectionInvokesValidMethodsAsync(bool indexExists, int this._mockMongoDatabase.Verify(l => l.CreateCollectionAsync( CollectionName, It.IsAny(), - It.IsAny()), Times.Once()); + It.IsAny()), Times.Once); + + this._mockMongoDatabase.Verify(l => l.ListCollectionNamesAsync( + It.IsAny(), + It.IsAny()), Times.Once); this._mockMongoDatabase.Verify(l => l.RunCommandAsync( It.Is>(command => @@ -151,9 +168,8 @@ public async Task CreateCollectionInvokesValidMethodsAsync(bool indexExists, int It.IsAny()), Times.Exactly(actualIndexCreations)); } - [Theory] - [MemberData(nameof(CreateCollectionIfNotExistsData))] - public async Task CreateCollectionIfNotExistsInvokesValidMethodsAsync(List collections, int actualCollectionCreations) + [Fact] + public async Task CreateCollectionIfNotExistsInvokesValidMethodsAsync() { // Arrange const string CollectionName = "collection"; @@ -165,7 +181,7 @@ public async Task CreateCollectionIfNotExistsInvokesValidMethodsAsync(List l.Current) - .Returns(collections); + .Returns([]); this._mockMongoDatabase .Setup(l => l.ListCollectionNamesAsync(It.IsAny(), It.IsAny())) @@ -202,7 +218,11 @@ public async Task CreateCollectionIfNotExistsInvokesValidMethodsAsync(List l.CreateCollectionAsync( CollectionName, It.IsAny(), - It.IsAny()), Times.Exactly(actualCollectionCreations)); + It.IsAny()), Times.Exactly(1)); + + this._mockMongoDatabase.Verify(l => l.ListCollectionNamesAsync( + It.IsAny(), + It.IsAny()), Times.Never); } [Fact] diff --git a/dotnet/src/Connectors/Connectors.MongoDB.UnitTests/MongoDBVectorStoreRecordCollectionTests.cs b/dotnet/src/Connectors/Connectors.MongoDB.UnitTests/MongoDBVectorStoreRecordCollectionTests.cs index 6265c18bd6b9..ddf71955621e 100644 --- a/dotnet/src/Connectors/Connectors.MongoDB.UnitTests/MongoDBVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/Connectors/Connectors.MongoDB.UnitTests/MongoDBVectorStoreRecordCollectionTests.cs @@ -121,6 +121,15 @@ public async Task CreateCollectionInvokesValidMethodsAsync(bool indexExists, int .Setup(l => l.Current) .Returns(indexes); + var mockCursor = new Mock>(); + mockCursor + .Setup(l => l.MoveNextAsync(It.IsAny())) + .ReturnsAsync(true); + + mockCursor + .Setup(l => l.Current) + .Returns([]); + var mockMongoIndexManager = new Mock>(); mockMongoIndexManager @@ -131,6 +140,10 @@ public async Task CreateCollectionInvokesValidMethodsAsync(bool indexExists, int .Setup(l => l.Indexes) .Returns(mockMongoIndexManager.Object); + this._mockMongoDatabase + .Setup(l => l.ListCollectionNamesAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockCursor.Object); + var sut = new MongoDBVectorStoreRecordCollection(this._mockMongoDatabase.Object, CollectionName); // Act @@ -140,7 +153,11 @@ public async Task CreateCollectionInvokesValidMethodsAsync(bool indexExists, int this._mockMongoDatabase.Verify(l => l.CreateCollectionAsync( CollectionName, It.IsAny(), - It.IsAny()), Times.Once()); + It.IsAny()), Times.Once); + + this._mockMongoDatabase.Verify(l => l.ListCollectionNamesAsync( + It.IsAny(), + It.IsAny()), Times.Once); this._mockMongoDatabase.Verify(l => l.RunCommandAsync( It.Is>(command => @@ -151,9 +168,8 @@ public async Task CreateCollectionInvokesValidMethodsAsync(bool indexExists, int It.IsAny()), Times.Exactly(actualIndexCreations)); } - [Theory] - [MemberData(nameof(CreateCollectionIfNotExistsData))] - public async Task CreateCollectionIfNotExistsInvokesValidMethodsAsync(List collections, int actualCollectionCreations) + [Fact] + public async Task CreateCollectionIfNotExistsInvokesValidMethodsAsync() { // Arrange const string CollectionName = "collection"; @@ -165,7 +181,7 @@ public async Task CreateCollectionIfNotExistsInvokesValidMethodsAsync(List l.Current) - .Returns(collections); + .Returns([]); this._mockMongoDatabase .Setup(l => l.ListCollectionNamesAsync(It.IsAny(), It.IsAny())) @@ -202,7 +218,11 @@ public async Task CreateCollectionIfNotExistsInvokesValidMethodsAsync(List l.CreateCollectionAsync( CollectionName, It.IsAny(), - It.IsAny()), Times.Exactly(actualCollectionCreations)); + It.IsAny()), Times.Exactly(1)); + + this._mockMongoDatabase.Verify(l => l.ListCollectionNamesAsync( + It.IsAny(), + It.IsAny()), Times.Never); } [Fact] From 8e7ae5337c9ce15b880f94a348b43184a3797204 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 14 Mar 2025 11:53:31 +0100 Subject: [PATCH 7/9] address code review feedback: test both named vectors and a single unnamed vector for Qdrant --- ...QdrantCollectionConformanceTests_NamedVectors.cs} | 4 ++-- ...QdrantCollectionConformanceTests_UnnamedVector.cs | 12 ++++++++++++ .../Filter/QdrantBasicFilterTests.cs | 2 +- ...QdrantFixture.cs => QdrantNamedVectorsFixture.cs} | 4 ++-- .../Support/QdrantTestStore.cs | 10 +++++----- .../Support/QdrantUnnamedVectorFixture.cs | 10 ++++++++++ 6 files changed, 32 insertions(+), 10 deletions(-) rename dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/{QdrantCollectionConformanceTests.cs => QdrantCollectionConformanceTests_NamedVectors.cs} (68%) create mode 100644 dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests_UnnamedVector.cs rename dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/{QdrantFixture.cs => QdrantNamedVectorsFixture.cs} (50%) create mode 100644 dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantUnnamedVectorFixture.cs diff --git a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests.cs b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests_NamedVectors.cs similarity index 68% rename from dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests.cs rename to dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests_NamedVectors.cs index fd74e874a9b5..7f4d0f138907 100644 --- a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests.cs +++ b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests_NamedVectors.cs @@ -6,7 +6,7 @@ namespace QdrantIntegrationTests.Collections; -public class QdrantCollectionConformanceTests(QdrantFixture fixture) - : CollectionConformanceTests(fixture), IClassFixture +public class QdrantCollectionConformanceTests_NamedVectors(QdrantNamedVectorsFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture { } diff --git a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests_UnnamedVector.cs b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests_UnnamedVector.cs new file mode 100644 index 000000000000..5471d83c8996 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Collections/QdrantCollectionConformanceTests_UnnamedVector.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using QdrantIntegrationTests.Support; +using VectorDataSpecificationTests.Collections; +using Xunit; + +namespace QdrantIntegrationTests.Collections; + +public class QdrantCollectionConformanceTests_UnnamedVector(QdrantUnnamedVectorFixture fixture) + : CollectionConformanceTests(fixture), IClassFixture +{ +} diff --git a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Filter/QdrantBasicFilterTests.cs b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Filter/QdrantBasicFilterTests.cs index 13ed231a3207..2ba3b454231b 100644 --- a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Filter/QdrantBasicFilterTests.cs +++ b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Filter/QdrantBasicFilterTests.cs @@ -12,7 +12,7 @@ public class QdrantBasicFilterTests(QdrantBasicFilterTests.Fixture fixture) { public new class Fixture : BasicFilterTests.Fixture { - public override TestStore TestStore => QdrantTestStore.Instance; + public override TestStore TestStore => QdrantTestStore.NamedVectorsInstance; // Qdrant doesn't support the default Flat index kind protected override string IndexKind => Microsoft.Extensions.VectorData.IndexKind.Hnsw; diff --git a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantFixture.cs b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantNamedVectorsFixture.cs similarity index 50% rename from dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantFixture.cs rename to dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantNamedVectorsFixture.cs index 208ec927aa9e..7544ae1653b4 100644 --- a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantFixture.cs +++ b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantNamedVectorsFixture.cs @@ -4,7 +4,7 @@ namespace QdrantIntegrationTests.Support; -public class QdrantFixture : VectorStoreFixture +public class QdrantNamedVectorsFixture : VectorStoreFixture { - public override TestStore TestStore => QdrantTestStore.Instance; + public override TestStore TestStore => QdrantTestStore.NamedVectorsInstance; } diff --git a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantTestStore.cs b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantTestStore.cs index 3537cf8c64e9..c7148c291de4 100644 --- a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantTestStore.cs +++ b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantTestStore.cs @@ -12,9 +12,11 @@ namespace QdrantIntegrationTests.Support; internal sealed class QdrantTestStore : TestStore { - public static QdrantTestStore Instance { get; } = new(); + public static QdrantTestStore NamedVectorsInstance { get; } = new(hasNamedVectors: true); + public static QdrantTestStore UnnamedVectorInstance { get; } = new(hasNamedVectors: false); private readonly QdrantContainer _container = new QdrantBuilder().Build(); + private readonly bool _hasNamedVectors; private QdrantClient? _client; private QdrantVectorStore? _defaultVectorStore; @@ -25,15 +27,13 @@ internal sealed class QdrantTestStore : TestStore public QdrantVectorStore GetVectorStore(QdrantVectorStoreOptions options) => new(this.Client, options); - private QdrantTestStore() - { - } + private QdrantTestStore(bool hasNamedVectors) => this._hasNamedVectors = hasNamedVectors; protected override async Task StartAsync() { await this._container.StartAsync(); this._client = new QdrantClient(this._container.Hostname, this._container.GetMappedPublicPort(QdrantBuilder.QdrantGrpcPort)); - this._defaultVectorStore = new(this._client); + this._defaultVectorStore = new(this._client, new() { HasNamedVectors = this._hasNamedVectors }); } protected override Task StopAsync() diff --git a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantUnnamedVectorFixture.cs b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantUnnamedVectorFixture.cs new file mode 100644 index 000000000000..678f8c820fcc --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/Support/QdrantUnnamedVectorFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.Support; + +namespace QdrantIntegrationTests.Support; + +public class QdrantUnnamedVectorFixture : VectorStoreFixture +{ + public override TestStore TestStore => QdrantTestStore.UnnamedVectorInstance; +} From 72a9f040ab64f1996cb1fb6c46261f7d57735ea6 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 18 Mar 2025 13:30:19 +0100 Subject: [PATCH 8/9] fix the build: Weaviate requires each collection name to start with upercase ASCII letter --- .../Connectors/Memory/BaseVectorStoreTests.cs | 4 +++- .../Connectors/Memory/Weaviate/WeaviateVectorStoreTests.cs | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/BaseVectorStoreTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/BaseVectorStoreTests.cs index 773315a285ee..3a9aff375be3 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/BaseVectorStoreTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/BaseVectorStoreTests.cs @@ -15,11 +15,13 @@ namespace SemanticKernel.IntegrationTests.Connectors.Memory; public abstract class BaseVectorStoreTests(IVectorStore vectorStore) where TKey : notnull { + protected virtual IEnumerable CollectionNames => ["listcollectionnames1", "listcollectionnames2", "listcollectionnames3"]; + [VectorStoreFact] public virtual async Task ItCanGetAListOfExistingCollectionNamesAsync() { // Arrange - var expectedCollectionNames = new List { "listcollectionnames1", "listcollectionnames2", "listcollectionnames3" }; + var expectedCollectionNames = this.CollectionNames; foreach (var collectionName in expectedCollectionNames) { diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateVectorStoreTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateVectorStoreTests.cs index ce278486e9bc..ae609048fb45 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateVectorStoreTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateVectorStoreTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using Microsoft.SemanticKernel.Connectors.Weaviate; using Xunit; @@ -9,4 +10,7 @@ namespace SemanticKernel.IntegrationTests.Connectors.Memory.Weaviate; [Collection("WeaviateVectorStoreCollection")] public sealed class WeaviateVectorStoreTests(WeaviateVectorStoreFixture fixture) : BaseVectorStoreTests(new WeaviateVectorStore(fixture.HttpClient!)) -{ } +{ + // Weaviate requires each collection name to start with upercase ASCII letter. + protected override IEnumerable CollectionNames => ["Listcollectionnames1", "Listcollectionnames2", "Listcollectionnames3"]; +} From bc2eaa10789871723c6ad45dffb2670e5592687c Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 18 Mar 2025 14:02:11 +0100 Subject: [PATCH 9/9] Update dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateVectorStoreTests.cs --- .../Connectors/Memory/Weaviate/WeaviateVectorStoreTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateVectorStoreTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateVectorStoreTests.cs index ae609048fb45..ed42a3322744 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateVectorStoreTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateVectorStoreTests.cs @@ -11,6 +11,6 @@ namespace SemanticKernel.IntegrationTests.Connectors.Memory.Weaviate; public sealed class WeaviateVectorStoreTests(WeaviateVectorStoreFixture fixture) : BaseVectorStoreTests(new WeaviateVectorStore(fixture.HttpClient!)) { - // Weaviate requires each collection name to start with upercase ASCII letter. + // Weaviate requires each collection name to start with uppercase ASCII letter. protected override IEnumerable CollectionNames => ["Listcollectionnames1", "Listcollectionnames2", "Listcollectionnames3"]; }