From 798457c29d0e658099009711e4de37c19caf9682 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 12 Mar 2025 19:50:26 +0100 Subject: [PATCH] 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)); }