chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SemanticKernel.Connectors.PgVector.UnitTests</AssemblyName>
|
||||
<RootNamespace>SemanticKernel.Connectors.PgVector.UnitTests</RootNamespace>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>$(NoWarn);SKEXP0001,VSTHRD111,CA2007,CS1591</NoWarn>
|
||||
<NoWarn>$(NoWarn);MEVD9001</NoWarn> <!-- Experimental MEVD connector-facing APIs -->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\VectorData\PgVector\PgVector.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel.Connectors.PgVector;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.PgVector.UnitTests;
|
||||
|
||||
public class PostgresCollectionTests
|
||||
{
|
||||
private const string TestCollectionName = "testcollection";
|
||||
|
||||
[Fact]
|
||||
public void ThrowsForUnsupportedType()
|
||||
{
|
||||
// Arrange
|
||||
var recordDefinition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties = [
|
||||
new VectorStoreKeyProperty("HotelId", typeof(ulong)),
|
||||
new VectorStoreDataProperty("HotelName", typeof(string)) { IsIndexed = true, IsFullTextIndexed = true },
|
||||
]
|
||||
};
|
||||
var options = new PostgresCollectionOptions()
|
||||
{
|
||||
Definition = recordDefinition
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<NotSupportedException>(() => new PostgresDynamicCollection("Host=localhost;Database=test;", TestCollectionName, options));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using Microsoft.SemanticKernel.Connectors.PgVector;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.PgVector.UnitTests;
|
||||
|
||||
public sealed class PostgresFilterTranslatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void DateTime_Utc_FormattedWithZ()
|
||||
{
|
||||
// Inline new DateTime(..., DateTimeKind.Utc) in expression tree to exercise TranslateConstant
|
||||
var sql = TranslateFilter<TestRecord>(r => r.Created == new DateTime(2024, 6, 15, 10, 30, 45, DateTimeKind.Utc));
|
||||
Assert.Contains("'2024-06-15T10:30:45Z'", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DateTime_Unspecified_FormattedWithoutZ()
|
||||
{
|
||||
// Inline new DateTime(...) has Kind=Unspecified
|
||||
var sql = TranslateFilter<TestRecord>(r => r.Created == new DateTime(2024, 6, 15, 10, 30, 45));
|
||||
Assert.Contains("'2024-06-15T10:30:45'", sql);
|
||||
Assert.DoesNotContain("Z'", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DateTimeOffset_Utc_FormattedWithZ()
|
||||
{
|
||||
// Use a ConstantExpression directly to bypass VisitNew issues with TimeSpan.Zero
|
||||
var sql = TranslateFilterWithConstant<TestRecord, DateTimeOffset>(
|
||||
nameof(TestRecord.CreatedOffset),
|
||||
new DateTimeOffset(2024, 6, 15, 10, 30, 45, TimeSpan.Zero));
|
||||
Assert.Contains("'2024-06-15T10:30:45Z'", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DateTimeOffset_NonZeroOffset_Throws()
|
||||
{
|
||||
Assert.ThrowsAny<NotSupportedException>(() =>
|
||||
TranslateFilterWithConstant<TestRecord, DateTimeOffset>(
|
||||
nameof(TestRecord.CreatedOffset),
|
||||
new DateTimeOffset(2024, 6, 15, 10, 30, 45, TimeSpan.FromHours(2))));
|
||||
}
|
||||
|
||||
private static string TranslateFilter<TRecord>(Expression<Func<TRecord, bool>> filter)
|
||||
{
|
||||
var model = BuildModel();
|
||||
var sb = new StringBuilder();
|
||||
var translator = new PostgresFilterTranslator(model, filter, startParamIndex: 1, sb);
|
||||
translator.Translate(appendWhere: false);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string TranslateFilterWithConstant<TRecord, TValue>(string propertyName, TValue value)
|
||||
{
|
||||
var model = BuildModel();
|
||||
var param = Expression.Parameter(typeof(TRecord), "r");
|
||||
var prop = Expression.Property(param, propertyName);
|
||||
var constant = Expression.Constant(value, typeof(TValue));
|
||||
var body = Expression.Equal(prop, constant);
|
||||
var filter = Expression.Lambda<Func<TRecord, bool>>(body, param);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
var translator = new PostgresFilterTranslator(model, filter, startParamIndex: 1, sb);
|
||||
translator.Translate(appendWhere: false);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static CollectionModel BuildModel()
|
||||
{
|
||||
var definition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Id", typeof(Guid)),
|
||||
new VectorStoreDataProperty("Created", typeof(DateTime)),
|
||||
new VectorStoreDataProperty("CreatedOffset", typeof(DateTimeOffset)),
|
||||
new VectorStoreVectorProperty("Embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
]
|
||||
};
|
||||
|
||||
return new PostgresModelBuilder().BuildDynamic(definition, defaultEmbeddingGenerator: null);
|
||||
}
|
||||
|
||||
#pragma warning disable CA1812 // internal class that is apparently never instantiated.
|
||||
private sealed record TestRecord
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTimeOffset CreatedOffset { get; set; }
|
||||
public ReadOnlyMemory<float> Embedding { get; set; }
|
||||
}
|
||||
#pragma warning restore CA1812
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
|
||||
namespace SemanticKernel.Connectors.PgVector.UnitTests;
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// A test model for the postgres vector store.
|
||||
/// </summary>
|
||||
public record PostgresHotel<T>()
|
||||
{
|
||||
/// <summary>The key of the record.</summary>
|
||||
[VectorStoreKey]
|
||||
public T HotelId { get; init; }
|
||||
|
||||
/// <summary>A string metadata field.</summary>
|
||||
[VectorStoreData()]
|
||||
public string? HotelName { get; set; }
|
||||
|
||||
/// <summary>An int metadata field.</summary>
|
||||
[VectorStoreData()]
|
||||
public int HotelCode { get; set; }
|
||||
|
||||
/// <summary>A float metadata field.</summary>
|
||||
[VectorStoreData()]
|
||||
public float? HotelRating { get; set; }
|
||||
|
||||
/// <summary>A bool metadata field.</summary>
|
||||
[VectorStoreData(StorageName = "parking_is_included")]
|
||||
public bool ParkingIncluded { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public List<string> Tags { get; set; } = [];
|
||||
|
||||
/// <summary>A data field.</summary>
|
||||
[VectorStoreData]
|
||||
public string Description { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>A vector field.</summary>
|
||||
[VectorStoreVector(4, DistanceFunction = IndexKind.Hnsw, IndexKind = DistanceFunction.ManhattanDistance)]
|
||||
public ReadOnlyMemory<float>? DescriptionEmbedding { get; set; }
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel.Connectors.PgVector;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.PgVector.UnitTests;
|
||||
|
||||
public sealed class PostgresPropertyExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("timestamp")]
|
||||
[InlineData("TIMESTAMP")]
|
||||
[InlineData("timestamp without time zone")]
|
||||
[InlineData("TIMESTAMP WITHOUT TIME ZONE")]
|
||||
public void WithStoreType_Timestamp_SetsAnnotation(string storeType)
|
||||
{
|
||||
var property = new VectorStoreDataProperty("test", typeof(DateTime));
|
||||
|
||||
var result = property.WithStoreType(storeType);
|
||||
|
||||
Assert.Same(property, result);
|
||||
Assert.Equal(storeType, property.GetStoreType());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("timestamptz")]
|
||||
[InlineData("timestamp with time zone")]
|
||||
[InlineData("integer")]
|
||||
[InlineData("text")]
|
||||
[InlineData("")]
|
||||
public void WithStoreType_UnsupportedType_Throws(string storeType)
|
||||
{
|
||||
var property = new VectorStoreDataProperty("test", typeof(DateTime));
|
||||
|
||||
Assert.Throws<NotSupportedException>(() => property.WithStoreType(storeType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStoreType_NotSet_ReturnsNull()
|
||||
{
|
||||
var property = new VectorStoreDataProperty("test", typeof(DateTime));
|
||||
|
||||
Assert.Null(property.GetStoreType());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithStoreType_OnlyAllowedOnDateTimeProperties()
|
||||
{
|
||||
// Setting the annotation on a non-DateTime property should succeed at the property level
|
||||
// (validation happens in the model builder), but building the model should throw.
|
||||
var definition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(Guid)),
|
||||
new VectorStoreDataProperty("name", typeof(string)).WithStoreType("timestamp"),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
]
|
||||
};
|
||||
|
||||
Assert.Throws<NotSupportedException>(() =>
|
||||
new PostgresModelBuilder().BuildDynamic(definition, defaultEmbeddingGenerator: null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithStoreType_AllowedOnDateTimeProperty()
|
||||
{
|
||||
var definition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(Guid)),
|
||||
new VectorStoreDataProperty("created", typeof(DateTime)).WithStoreType("timestamp"),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
]
|
||||
};
|
||||
|
||||
// Should not throw
|
||||
var model = new PostgresModelBuilder().BuildDynamic(definition, defaultEmbeddingGenerator: null);
|
||||
Assert.NotNull(model);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithStoreType_AllowedOnKeyProperty()
|
||||
{
|
||||
var property = new VectorStoreKeyProperty("id", typeof(DateTime)).WithStoreType("timestamp");
|
||||
Assert.Equal("timestamp", property.GetStoreType());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithStoreType_OnKeyProperty_NonDateTime_Throws()
|
||||
{
|
||||
var definition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(Guid)).WithStoreType("timestamp"),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
]
|
||||
};
|
||||
|
||||
Assert.Throws<NotSupportedException>(() =>
|
||||
new PostgresModelBuilder().BuildDynamic(definition, defaultEmbeddingGenerator: null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithStoreType_AllowedOnNullableDateTimeProperty()
|
||||
{
|
||||
var definition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("id", typeof(Guid)),
|
||||
new VectorStoreDataProperty("created", typeof(DateTime?)).WithStoreType("timestamp"),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
]
|
||||
};
|
||||
|
||||
// Should not throw
|
||||
var model = new PostgresModelBuilder().BuildDynamic(definition, defaultEmbeddingGenerator: null);
|
||||
Assert.NotNull(model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using Microsoft.SemanticKernel.Connectors.PgVector;
|
||||
using NpgsqlTypes;
|
||||
using Pgvector;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.PgVector.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="PostgresPropertyMapping"/> class.
|
||||
/// </summary>
|
||||
public sealed class PostgresPropertyMappingTests
|
||||
{
|
||||
[Fact]
|
||||
public void MapVectorForStorageModelWithInvalidVectorTypeThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var vector = new List<float> { 1f, 2f, 3f };
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<NotSupportedException>(() => PostgresPropertyMapping.MapVectorForStorageModel(vector));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapVectorForStorageModelReturnsVector()
|
||||
{
|
||||
// Arrange
|
||||
var vector = new ReadOnlyMemory<float>([1.1f, 2.2f, 3.3f, 4.4f]);
|
||||
|
||||
// Act
|
||||
var storageModelVector = PostgresPropertyMapping.MapVectorForStorageModel(vector);
|
||||
|
||||
// Assert
|
||||
var actual = Assert.IsType<Vector>(storageModelVector);
|
||||
Assert.True(actual.ToArray().Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPropertyValueReturnsCorrectValuesForLists()
|
||||
{
|
||||
// Arrange
|
||||
var typesAndExpectedValues = new List<(Type, object)>
|
||||
{
|
||||
(typeof(List<int>), "INTEGER[]"),
|
||||
(typeof(List<float>), "REAL[]"),
|
||||
(typeof(List<double>), "DOUBLE PRECISION[]"),
|
||||
(typeof(List<string>), "TEXT[]"),
|
||||
(typeof(List<bool>), "BOOLEAN[]"),
|
||||
(typeof(List<DateTime>), "TIMESTAMPTZ[]"),
|
||||
(typeof(List<Guid>), "UUID[]"),
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
foreach (var (type, expectedValue) in typesAndExpectedValues)
|
||||
{
|
||||
var property = new DataPropertyModel("test", type);
|
||||
var (pgType, _) = PostgresPropertyMapping.GetPostgresTypeName(property);
|
||||
Assert.Equal(expectedValue, pgType);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPropertyValueReturnsCorrectNullableValue()
|
||||
{
|
||||
// Arrange
|
||||
var typesAndExpectedValues = new List<(Type, object)>
|
||||
{
|
||||
(typeof(short), false),
|
||||
(typeof(short?), true),
|
||||
(typeof(int?), true),
|
||||
(typeof(long), false),
|
||||
(typeof(string), true),
|
||||
(typeof(bool?), true),
|
||||
(typeof(DateTime?), true),
|
||||
(typeof(Guid), false),
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
foreach (var (type, expectedValue) in typesAndExpectedValues)
|
||||
{
|
||||
var property = new DataPropertyModel("test", type);
|
||||
var (_, isNullable) = PostgresPropertyMapping.GetPostgresTypeName(property);
|
||||
Assert.Equal(expectedValue, isNullable);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIndexInfoReturnsCorrectValues()
|
||||
{
|
||||
// Arrange
|
||||
List<PropertyModel> vectorProperties =
|
||||
[
|
||||
new VectorPropertyModel("vector1", typeof(ReadOnlyMemory<float>?)) { IndexKind = IndexKind.Hnsw, Dimensions = 1000 },
|
||||
new VectorPropertyModel("vector2", typeof(ReadOnlyMemory<float>?)) { IndexKind = IndexKind.Flat, Dimensions = 3000 },
|
||||
new VectorPropertyModel("vector3", typeof(ReadOnlyMemory<float>?)) { IndexKind = IndexKind.Hnsw, Dimensions = 900, DistanceFunction = DistanceFunction.ManhattanDistance },
|
||||
new DataPropertyModel("data1", typeof(string)) { IsIndexed = true },
|
||||
new DataPropertyModel("data2", typeof(string)) { IsIndexed = false }
|
||||
];
|
||||
|
||||
// Act
|
||||
var indexInfo = PostgresPropertyMapping.GetIndexInfo(vectorProperties);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, indexInfo.Count);
|
||||
foreach (var (columnName, indexKind, distanceFunction, isVector, isFullText, fullTextLanguage) in indexInfo)
|
||||
{
|
||||
if (columnName == "vector1")
|
||||
{
|
||||
Assert.True(isVector);
|
||||
Assert.False(isFullText);
|
||||
Assert.Equal(IndexKind.Hnsw, indexKind);
|
||||
Assert.Equal(DistanceFunction.CosineDistance, distanceFunction);
|
||||
}
|
||||
else if (columnName == "vector3")
|
||||
{
|
||||
Assert.True(isVector);
|
||||
Assert.False(isFullText);
|
||||
Assert.Equal(IndexKind.Hnsw, indexKind);
|
||||
Assert.Equal(DistanceFunction.ManhattanDistance, distanceFunction);
|
||||
}
|
||||
else if (columnName == "data1")
|
||||
{
|
||||
Assert.False(isVector);
|
||||
Assert.False(isFullText);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Unexpected column name");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(IndexKind.Hnsw, 3000)]
|
||||
public void GetVectorIndexInfoReturnsThrowsForInvalidDimensions(string indexKind, int dimensions)
|
||||
{
|
||||
// Arrange
|
||||
var vectorProperty = new VectorPropertyModel("vector", typeof(ReadOnlyMemory<float>?)) { IndexKind = indexKind, Dimensions = dimensions };
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<NotSupportedException>(() => PostgresPropertyMapping.GetIndexInfo([vectorProperty]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPostgresTypeName_DateTime_WithTimestampAnnotation_ReturnsTimestamp()
|
||||
{
|
||||
var dataProperty = new DataPropertyModel("created", typeof(DateTime));
|
||||
dataProperty.ProviderAnnotations = new() { ["Postgres:StoreType"] = "timestamp" };
|
||||
|
||||
var (pgType, _) = PostgresPropertyMapping.GetPostgresTypeName(dataProperty);
|
||||
Assert.Equal("TIMESTAMP", pgType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPostgresTypeName_DateTime_WithoutAnnotation_ReturnsTimestampTz()
|
||||
{
|
||||
var dataProperty = new DataPropertyModel("created", typeof(DateTime));
|
||||
|
||||
var (pgType, _) = PostgresPropertyMapping.GetPostgresTypeName(dataProperty);
|
||||
Assert.Equal("TIMESTAMPTZ", pgType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPostgresTypeName_NullableDateTime_WithTimestampAnnotation_ReturnsTimestamp()
|
||||
{
|
||||
var dataProperty = new DataPropertyModel("created", typeof(DateTime?));
|
||||
dataProperty.ProviderAnnotations = new() { ["Postgres:StoreType"] = "timestamp" };
|
||||
|
||||
var (pgType, isNullable) = PostgresPropertyMapping.GetPostgresTypeName(dataProperty);
|
||||
Assert.Equal("TIMESTAMP", pgType);
|
||||
Assert.True(isNullable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNpgsqlDbType_DateTime_WithTimestampAnnotation_ReturnsTimestamp()
|
||||
{
|
||||
var dataProperty = new DataPropertyModel("created", typeof(DateTime));
|
||||
dataProperty.ProviderAnnotations = new() { ["Postgres:StoreType"] = "timestamp" };
|
||||
|
||||
var dbType = PostgresPropertyMapping.GetNpgsqlDbType(dataProperty);
|
||||
Assert.Equal(NpgsqlDbType.Timestamp, dbType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNpgsqlDbType_DateTime_WithoutAnnotation_ReturnsTimestampTz()
|
||||
{
|
||||
var dataProperty = new DataPropertyModel("created", typeof(DateTime));
|
||||
|
||||
var dbType = PostgresPropertyMapping.GetNpgsqlDbType(dataProperty);
|
||||
Assert.Equal(NpgsqlDbType.TimestampTz, dbType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using Microsoft.SemanticKernel.Connectors.PgVector;
|
||||
using Npgsql;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace SemanticKernel.Connectors.PgVector.UnitTests;
|
||||
|
||||
public class PostgresSqlBuilderTests
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
private static readonly float[] s_vector = new float[] { 1.0f, 2.0f, 3.0f };
|
||||
|
||||
public PostgresSqlBuilderTests(ITestOutputHelper output)
|
||||
{
|
||||
this._output = output;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void TestBuildCreateTableCommand(bool ifNotExists)
|
||||
{
|
||||
var recordDefinition = new VectorStoreCollectionDefinition()
|
||||
{
|
||||
Properties = [
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreDataProperty("code", typeof(int)),
|
||||
new VectorStoreDataProperty("rating", typeof(float?)),
|
||||
new VectorStoreDataProperty("description", typeof(string)),
|
||||
new VectorStoreDataProperty("parking_is_included", typeof(bool)) { StorageName = "free_parking" },
|
||||
new VectorStoreDataProperty("tags", typeof(List<string>)),
|
||||
new VectorStoreVectorProperty("embedding1", typeof(ReadOnlyMemory<float>), 10)
|
||||
{
|
||||
Dimensions = 10,
|
||||
IndexKind = "hnsw",
|
||||
},
|
||||
new VectorStoreVectorProperty("embedding2", typeof(ReadOnlyMemory<float>?), 10)
|
||||
{
|
||||
Dimensions = 10,
|
||||
IndexKind = "hnsw",
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var model = new PostgresModelBuilder().BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null);
|
||||
|
||||
var sql = PostgresSqlBuilder.BuildCreateTableSql(schema: null, "testcollection", model, pgVersion: new Version(18, 0), ifNotExists: ifNotExists);
|
||||
|
||||
// Check for expected properties; integration tests will validate the actual SQL.
|
||||
Assert.Contains("\"testcollection\" (", sql);
|
||||
Assert.DoesNotContain("\"public\"", sql);
|
||||
Assert.Contains("\"name\" TEXT", sql);
|
||||
Assert.Contains("\"code\" INTEGER NOT NULL", sql);
|
||||
Assert.Contains("\"rating\" REAL", sql);
|
||||
Assert.Contains("\"description\" TEXT", sql);
|
||||
Assert.Contains("\"free_parking\" BOOLEAN NOT NULL", sql);
|
||||
Assert.Contains("\"tags\" TEXT[]", sql);
|
||||
Assert.Contains("\"description\" TEXT", sql);
|
||||
Assert.Contains("\"embedding1\" VECTOR(10) NOT NULL", sql);
|
||||
Assert.Contains("\"embedding2\" VECTOR(10)", sql);
|
||||
Assert.Contains("PRIMARY KEY (\"id\")", sql);
|
||||
|
||||
if (ifNotExists)
|
||||
{
|
||||
Assert.Contains("IF NOT EXISTS", sql);
|
||||
}
|
||||
|
||||
// Output
|
||||
this._output.WriteLine(sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildCreateTableCommand_WithTimestampStoreType()
|
||||
{
|
||||
var recordDefinition = new VectorStoreCollectionDefinition()
|
||||
{
|
||||
Properties = [
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("created_utc", typeof(DateTime)),
|
||||
new VectorStoreDataProperty("created_local", typeof(DateTime)).WithStoreType("timestamp"),
|
||||
new VectorStoreDataProperty("created_nullable", typeof(DateTime?)).WithStoreType("timestamp without time zone"),
|
||||
new VectorStoreVectorProperty("embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
]
|
||||
};
|
||||
|
||||
var model = new PostgresModelBuilder().BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null);
|
||||
|
||||
var sql = PostgresSqlBuilder.BuildCreateTableSql(schema: null, "testcollection", model, pgVersion: new Version(18, 0));
|
||||
|
||||
Assert.Contains("\"created_utc\" TIMESTAMPTZ NOT NULL", sql);
|
||||
Assert.Contains("\"created_local\" TIMESTAMP NOT NULL", sql);
|
||||
Assert.Contains("\"created_nullable\" TIMESTAMP", sql);
|
||||
// Make sure it's TIMESTAMP (not TIMESTAMPTZ) for the nullable one
|
||||
var idx = sql.IndexOf("\"created_nullable\"", StringComparison.Ordinal);
|
||||
var fragment = sql.Substring(idx, sql.IndexOf('\n', idx) - idx);
|
||||
Assert.DoesNotContain("TIMESTAMPTZ", fragment);
|
||||
|
||||
this._output.WriteLine(sql);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(IndexKind.Hnsw, DistanceFunction.EuclideanDistance, true)]
|
||||
[InlineData(IndexKind.Hnsw, DistanceFunction.EuclideanDistance, false)]
|
||||
[InlineData(IndexKind.IvfFlat, DistanceFunction.DotProductSimilarity, true)]
|
||||
[InlineData(IndexKind.IvfFlat, DistanceFunction.DotProductSimilarity, false)]
|
||||
[InlineData(IndexKind.Hnsw, DistanceFunction.CosineDistance, true)]
|
||||
[InlineData(IndexKind.Hnsw, DistanceFunction.CosineDistance, false)]
|
||||
public void TestBuildCreateIndexCommand(string indexKind, string distanceFunction, bool ifNotExists)
|
||||
{
|
||||
var vectorColumn = "embedding1";
|
||||
|
||||
if (indexKind != IndexKind.Hnsw)
|
||||
{
|
||||
Assert.Throws<NotSupportedException>(() => PostgresSqlBuilder.BuildCreateIndexSql(schema: null, "testcollection", vectorColumn, indexKind, distanceFunction, isVector: true, isFullText: false, fullTextLanguage: null, ifNotExists));
|
||||
Assert.Throws<NotSupportedException>(() => PostgresSqlBuilder.BuildCreateIndexSql(schema: null, "testcollection", vectorColumn, indexKind, distanceFunction, isVector: true, isFullText: false, fullTextLanguage: null, ifNotExists));
|
||||
return;
|
||||
}
|
||||
|
||||
var sql = PostgresSqlBuilder.BuildCreateIndexSql(schema: null, "1testcollection", vectorColumn, indexKind, distanceFunction, isVector: true, isFullText: false, fullTextLanguage: null, ifNotExists);
|
||||
|
||||
// Check for expected properties; integration tests will validate the actual SQL.
|
||||
Assert.Contains("CREATE INDEX ", sql);
|
||||
// Make sure ifNotExists is respected
|
||||
if (ifNotExists)
|
||||
{
|
||||
Assert.Contains("CREATE INDEX IF NOT EXISTS", sql);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.DoesNotContain("CREATE INDEX IF NOT EXISTS", sql);
|
||||
}
|
||||
// Make sure the name is escaped, so names starting with a digit are OK.
|
||||
Assert.Contains($"\"1testcollection_{vectorColumn}_index\"", sql);
|
||||
|
||||
Assert.Contains("ON \"1testcollection\" USING hnsw (\"embedding1\" ", sql);
|
||||
if (distanceFunction == null)
|
||||
{
|
||||
// Check for distance function defaults to cosine distance
|
||||
Assert.Contains("vector_cosine_ops)", sql);
|
||||
}
|
||||
else if (distanceFunction == DistanceFunction.CosineDistance)
|
||||
{
|
||||
Assert.Contains("vector_cosine_ops)", sql);
|
||||
}
|
||||
else if (distanceFunction == DistanceFunction.EuclideanDistance)
|
||||
{
|
||||
Assert.Contains("vector_l2_ops)", sql);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException($"Test case for Distance function {distanceFunction} is not implemented.");
|
||||
}
|
||||
// Output
|
||||
this._output.WriteLine(sql);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void TestBuildCreateNonVectorIndexCommand(bool ifNotExists)
|
||||
{
|
||||
var sql = PostgresSqlBuilder.BuildCreateIndexSql("schema", "tableName", "columnName", indexKind: "", distanceFunction: "", isVector: false, isFullText: false, fullTextLanguage: null, ifNotExists);
|
||||
|
||||
var expectedCommandText = ifNotExists
|
||||
? "CREATE INDEX IF NOT EXISTS \"tableName_columnName_index\" ON \"schema\".\"tableName\" (\"columnName\")"
|
||||
: "CREATE INDEX \"tableName_columnName_index\" ON \"schema\".\"tableName\" (\"columnName\")";
|
||||
|
||||
Assert.Equal(expectedCommandText, sql);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, "english")] // Default language
|
||||
[InlineData("spanish", "spanish")]
|
||||
[InlineData("german", "german")]
|
||||
public void TestBuildCreateFullTextIndexCommand(string? configuredLanguage, string expectedLanguage)
|
||||
{
|
||||
var sql = PostgresSqlBuilder.BuildCreateIndexSql("schema", "tableName", "content", indexKind: "", distanceFunction: "", isVector: false, isFullText: true, fullTextLanguage: configuredLanguage, ifNotExists: true);
|
||||
|
||||
var expectedCommandText = $"CREATE INDEX IF NOT EXISTS \"tableName_content_index\" ON \"schema\".\"tableName\" USING GIN (to_tsvector('{expectedLanguage}', \"content\"))";
|
||||
|
||||
Assert.Equal(expectedCommandText, sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildCreateFullTextIndexCommand_EscapesSingleQuotes()
|
||||
{
|
||||
// Verify that single quotes in the language name are properly escaped to prevent SQL injection
|
||||
var sql = PostgresSqlBuilder.BuildCreateIndexSql("schema", "tableName", "content", indexKind: "", distanceFunction: "", isVector: false, isFullText: true, fullTextLanguage: "test'injection", ifNotExists: true);
|
||||
|
||||
var expectedCommandText = "CREATE INDEX IF NOT EXISTS \"tableName_content_index\" ON \"schema\".\"tableName\" USING GIN (to_tsvector('test''injection', \"content\"))";
|
||||
|
||||
Assert.Equal(expectedCommandText, sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildDropTableCommand()
|
||||
{
|
||||
using var command = new NpgsqlCommand();
|
||||
PostgresSqlBuilder.BuildDropTableCommand(command, schema: null, "testcollection");
|
||||
|
||||
// Check for expected properties; integration tests will validate the actual SQL.
|
||||
Assert.Contains("DROP TABLE IF EXISTS \"testcollection\"", command.CommandText);
|
||||
Assert.DoesNotContain("\"public\"", command.CommandText);
|
||||
|
||||
// Output
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildUpsertCommand()
|
||||
{
|
||||
var recordDefinition = new VectorStoreCollectionDefinition()
|
||||
{
|
||||
Properties = [
|
||||
new VectorStoreKeyProperty("id", typeof(int)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreDataProperty("code", typeof(int)),
|
||||
new VectorStoreDataProperty("rating", typeof(float?)),
|
||||
new VectorStoreDataProperty("description", typeof(string)),
|
||||
new VectorStoreDataProperty("parking_is_included", typeof(bool)) { StorageName = "free_parking" },
|
||||
new VectorStoreDataProperty("tags", typeof(List<string>)),
|
||||
new VectorStoreVectorProperty("embedding1", typeof(ReadOnlyMemory<float>), 10)
|
||||
{
|
||||
Dimensions = 10,
|
||||
IndexKind = "hnsw",
|
||||
},
|
||||
new VectorStoreVectorProperty("embedding2", typeof(ReadOnlyMemory<float>?), 10)
|
||||
{
|
||||
Dimensions = 10,
|
||||
IndexKind = "hnsw",
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var model = new PostgresModelBuilder().BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null);
|
||||
|
||||
var record = new Dictionary<string, object?>
|
||||
{
|
||||
["id"] = 123,
|
||||
["name"] = "Hotel",
|
||||
["code"] = 456,
|
||||
["rating"] = 4.5f,
|
||||
["description"] = "Hotel description",
|
||||
["parking_is_included"] = true,
|
||||
["tags"] = new List<string> { "tag1", "tag2" },
|
||||
["embedding1"] = new ReadOnlyMemory<float>(s_vector),
|
||||
};
|
||||
|
||||
using var batch = new NpgsqlBatch();
|
||||
_ = PostgresSqlBuilder.BuildUpsertCommand<int>(batch, schema: null, "testcollection", model, [record], generatedEmbeddings: null);
|
||||
|
||||
// Check for expected properties; integration tests will validate the actual SQL.
|
||||
Assert.Single(batch.BatchCommands);
|
||||
var command = batch.BatchCommands[0];
|
||||
Assert.Contains("INSERT INTO \"testcollection\" (", command.CommandText);
|
||||
Assert.Contains("ON CONFLICT (\"id\")", command.CommandText);
|
||||
Assert.Contains("DO UPDATE SET", command.CommandText);
|
||||
Assert.NotNull(command.Parameters);
|
||||
|
||||
foreach (var (column, index) in record.Keys.Select((key, index) => (key, index)))
|
||||
{
|
||||
var expectedValue = column is "embedding1"
|
||||
? PostgresPropertyMapping.MapVectorForStorageModel((ReadOnlyMemory<float>)record[column]!)
|
||||
: record[column];
|
||||
Assert.Equal(expectedValue, command.Parameters[index].Value);
|
||||
|
||||
// If the key is not the key column, it should be included in the update clause.
|
||||
if (column is "id")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var storageName = column is "parking_is_included" ? "free_parking" : column;
|
||||
|
||||
Assert.Contains($"\"{storageName}\" = EXCLUDED.\"{storageName}\"", command.CommandText);
|
||||
}
|
||||
|
||||
// Output
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildGetCommand()
|
||||
{
|
||||
// Arrange
|
||||
var recordDefinition = new VectorStoreCollectionDefinition()
|
||||
{
|
||||
Properties = [
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreDataProperty("code", typeof(int)),
|
||||
new VectorStoreDataProperty("rating", typeof(float?)),
|
||||
new VectorStoreDataProperty("description", typeof(string)),
|
||||
new VectorStoreDataProperty("parking_is_included", typeof(bool)) { StorageName = "free_parking" },
|
||||
new VectorStoreDataProperty("tags", typeof(List<string>)),
|
||||
new VectorStoreVectorProperty("embedding1", typeof(ReadOnlyMemory<float>), 10)
|
||||
{
|
||||
IndexKind = "hnsw",
|
||||
},
|
||||
new VectorStoreVectorProperty("embedding2", typeof(ReadOnlyMemory<float>?), 10)
|
||||
{
|
||||
IndexKind = "hnsw",
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var model = new PostgresModelBuilder().BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null);
|
||||
|
||||
var key = 123;
|
||||
|
||||
// Act
|
||||
using var command = new NpgsqlCommand();
|
||||
PostgresSqlBuilder.BuildGetCommand(command, schema: null, "testcollection", model, key, includeVectors: true);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("SELECT", command.CommandText);
|
||||
Assert.Contains("\"free_parking\"", command.CommandText);
|
||||
Assert.Contains("\"embedding1\"", command.CommandText);
|
||||
Assert.Contains("FROM \"testcollection\"", command.CommandText);
|
||||
Assert.Contains("WHERE \"id\" = $1", command.CommandText);
|
||||
|
||||
// Output
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildGetBatchCommand()
|
||||
{
|
||||
// Arrange
|
||||
var recordDefinition = new VectorStoreCollectionDefinition()
|
||||
{
|
||||
Properties = [
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreDataProperty("code", typeof(int)),
|
||||
new VectorStoreDataProperty("rating", typeof(float?)),
|
||||
new VectorStoreDataProperty("description", typeof(string)),
|
||||
new VectorStoreDataProperty("parking_is_included", typeof(bool)) { StorageName = "free_parking" },
|
||||
new VectorStoreDataProperty("tags", typeof(List<string>)),
|
||||
new VectorStoreVectorProperty("embedding1", typeof(ReadOnlyMemory<float>), 10)
|
||||
{
|
||||
IndexKind = "hnsw",
|
||||
},
|
||||
new VectorStoreVectorProperty("embedding2", typeof(ReadOnlyMemory<float>?), 10)
|
||||
{
|
||||
IndexKind = "hnsw",
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var keys = new List<long> { 123, 124 };
|
||||
|
||||
var model = new PostgresModelBuilder().BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null);
|
||||
|
||||
// Act
|
||||
using var command = new NpgsqlCommand();
|
||||
PostgresSqlBuilder.BuildGetBatchCommand(command, schema: null, "testcollection", model, keys, includeVectors: true);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("SELECT", command.CommandText);
|
||||
Assert.Contains("\"code\"", command.CommandText);
|
||||
Assert.Contains("\"free_parking\"", command.CommandText);
|
||||
Assert.Contains("FROM \"testcollection\"", command.CommandText);
|
||||
Assert.Contains("WHERE \"id\" = ANY($1)", command.CommandText);
|
||||
Assert.NotNull(command.Parameters);
|
||||
Assert.Single(command.Parameters);
|
||||
Assert.Equal(keys, command.Parameters[0].Value);
|
||||
|
||||
// Output
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildDeleteCommand()
|
||||
{
|
||||
// Arrange
|
||||
var key = 123;
|
||||
|
||||
// Act
|
||||
using var command = new NpgsqlCommand();
|
||||
PostgresSqlBuilder.BuildDeleteCommand(command, schema: null, "testcollection", "id", key);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("DELETE", command.CommandText);
|
||||
Assert.Contains("FROM \"testcollection\"", command.CommandText);
|
||||
Assert.Contains("WHERE \"id\" = $1", command.CommandText);
|
||||
|
||||
// Output
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildDeleteBatchCommand()
|
||||
{
|
||||
// Arrange
|
||||
var keys = new List<long> { 123, 124 };
|
||||
|
||||
// Act
|
||||
using var command = new NpgsqlCommand();
|
||||
var keyProperty = new KeyPropertyModel("id", typeof(long));
|
||||
PostgresSqlBuilder.BuildDeleteBatchCommand(command, schema: null, "testcollection", keyProperty, keys);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("DELETE", command.CommandText);
|
||||
Assert.Contains("FROM \"testcollection\"", command.CommandText);
|
||||
Assert.Contains("WHERE \"id\" = ANY($1)", command.CommandText);
|
||||
Assert.NotNull(command.Parameters);
|
||||
Assert.Single(command.Parameters);
|
||||
Assert.Equal(keys, command.Parameters[0].Value);
|
||||
|
||||
// Output
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
#region Schema-specified tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, "public")]
|
||||
[InlineData("myschema", "myschema")]
|
||||
public void TestBuildDoesTableExistCommand(string? schema, string expectedSchema)
|
||||
{
|
||||
using var command = new NpgsqlCommand();
|
||||
PostgresSqlBuilder.BuildDoesTableExistCommand(command, schema, "testcollection");
|
||||
|
||||
Assert.Contains("table_schema = $1", command.CommandText);
|
||||
Assert.Contains("table_name = $2", command.CommandText);
|
||||
Assert.Equal(2, command.Parameters.Count);
|
||||
Assert.Equal(expectedSchema, command.Parameters[0].Value);
|
||||
Assert.Equal("testcollection", command.Parameters[1].Value);
|
||||
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, "public")]
|
||||
[InlineData("myschema", "myschema")]
|
||||
public void TestBuildGetTablesCommand(string? schema, string expectedSchema)
|
||||
{
|
||||
using var command = new NpgsqlCommand();
|
||||
PostgresSqlBuilder.BuildGetTablesCommand(command, schema);
|
||||
|
||||
Assert.Contains("table_schema = $1", command.CommandText);
|
||||
Assert.Single(command.Parameters);
|
||||
Assert.Equal(expectedSchema, command.Parameters[0].Value);
|
||||
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildCreateTableCommand_WithSchema()
|
||||
{
|
||||
var recordDefinition = new VectorStoreCollectionDefinition()
|
||||
{
|
||||
Properties = [
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreVectorProperty("embedding1", typeof(ReadOnlyMemory<float>), 10) { IndexKind = "hnsw" }
|
||||
]
|
||||
};
|
||||
|
||||
var model = new PostgresModelBuilder().BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null);
|
||||
|
||||
var sql = PostgresSqlBuilder.BuildCreateTableSql(schema: "myschema", "testcollection", model, pgVersion: new Version(18, 0));
|
||||
|
||||
Assert.Contains("\"myschema\".\"testcollection\"", sql);
|
||||
|
||||
this._output.WriteLine(sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildCreateIndexCommand_WithSchema()
|
||||
{
|
||||
var sql = PostgresSqlBuilder.BuildCreateIndexSql("myschema", "testcollection", "embedding1", IndexKind.Hnsw, DistanceFunction.CosineDistance, isVector: true, isFullText: false, fullTextLanguage: null, ifNotExists: true);
|
||||
|
||||
Assert.Contains("ON \"myschema\".\"testcollection\"", sql);
|
||||
|
||||
this._output.WriteLine(sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildDropTableCommand_WithSchema()
|
||||
{
|
||||
using var command = new NpgsqlCommand();
|
||||
PostgresSqlBuilder.BuildDropTableCommand(command, schema: "myschema", "testcollection");
|
||||
|
||||
Assert.Contains("DROP TABLE IF EXISTS \"myschema\".\"testcollection\"", command.CommandText);
|
||||
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildUpsertCommand_WithSchema()
|
||||
{
|
||||
var recordDefinition = new VectorStoreCollectionDefinition()
|
||||
{
|
||||
Properties = [
|
||||
new VectorStoreKeyProperty("id", typeof(int)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreVectorProperty("embedding1", typeof(ReadOnlyMemory<float>), 10) { IndexKind = "hnsw" }
|
||||
]
|
||||
};
|
||||
|
||||
var model = new PostgresModelBuilder().BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null);
|
||||
|
||||
var record = new Dictionary<string, object?>
|
||||
{
|
||||
["id"] = 1,
|
||||
["name"] = "Test",
|
||||
["embedding1"] = new ReadOnlyMemory<float>(s_vector),
|
||||
};
|
||||
|
||||
using var batch = new NpgsqlBatch();
|
||||
_ = PostgresSqlBuilder.BuildUpsertCommand<int>(batch, schema: "myschema", "testcollection", model, [record], generatedEmbeddings: null);
|
||||
|
||||
var command = batch.BatchCommands[0];
|
||||
Assert.Contains("INSERT INTO \"myschema\".\"testcollection\"", command.CommandText);
|
||||
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildGetCommand_WithSchema()
|
||||
{
|
||||
var recordDefinition = new VectorStoreCollectionDefinition()
|
||||
{
|
||||
Properties = [
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreVectorProperty("embedding1", typeof(ReadOnlyMemory<float>), 10) { IndexKind = "hnsw" }
|
||||
]
|
||||
};
|
||||
|
||||
var model = new PostgresModelBuilder().BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null);
|
||||
|
||||
using var command = new NpgsqlCommand();
|
||||
PostgresSqlBuilder.BuildGetCommand(command, schema: "myschema", "testcollection", model, 123, includeVectors: true);
|
||||
|
||||
Assert.Contains("FROM \"myschema\".\"testcollection\"", command.CommandText);
|
||||
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildGetBatchCommand_WithSchema()
|
||||
{
|
||||
var recordDefinition = new VectorStoreCollectionDefinition()
|
||||
{
|
||||
Properties = [
|
||||
new VectorStoreKeyProperty("id", typeof(long)),
|
||||
new VectorStoreDataProperty("name", typeof(string)),
|
||||
new VectorStoreVectorProperty("embedding1", typeof(ReadOnlyMemory<float>), 10) { IndexKind = "hnsw" }
|
||||
]
|
||||
};
|
||||
|
||||
var model = new PostgresModelBuilder().BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null);
|
||||
|
||||
using var command = new NpgsqlCommand();
|
||||
PostgresSqlBuilder.BuildGetBatchCommand(command, schema: "myschema", "testcollection", model, new List<long> { 1, 2 }, includeVectors: true);
|
||||
|
||||
Assert.Contains("FROM \"myschema\".\"testcollection\"", command.CommandText);
|
||||
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildDeleteCommand_WithSchema()
|
||||
{
|
||||
using var command = new NpgsqlCommand();
|
||||
PostgresSqlBuilder.BuildDeleteCommand(command, schema: "myschema", "testcollection", "id", 123);
|
||||
|
||||
Assert.Contains("FROM \"myschema\".\"testcollection\"", command.CommandText);
|
||||
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBuildDeleteBatchCommand_WithSchema()
|
||||
{
|
||||
using var command = new NpgsqlCommand();
|
||||
var keyProperty = new KeyPropertyModel("id", typeof(long));
|
||||
PostgresSqlBuilder.BuildDeleteBatchCommand(command, schema: "myschema", "testcollection", keyProperty, new List<long> { 1, 2 });
|
||||
|
||||
Assert.Contains("FROM \"myschema\".\"testcollection\"", command.CommandText);
|
||||
|
||||
this._output.WriteLine(command.CommandText);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NRT (Nullable Reference Type) detection
|
||||
|
||||
#if NET // NRT detection via NullabilityInfoContext is only available on .NET 6+
|
||||
[Fact]
|
||||
public void TestBuildCreateTableCommand_WithNrtAnnotations()
|
||||
{
|
||||
var model = new PostgresModelBuilder().Build(
|
||||
typeof(NrtTestRecord),
|
||||
typeof(long),
|
||||
definition: null,
|
||||
defaultEmbeddingGenerator: null);
|
||||
|
||||
var sql = PostgresSqlBuilder.BuildCreateTableSql(schema: null, "testcollection", model, pgVersion: new Version(18, 0));
|
||||
|
||||
// Non-nullable reference types should be NOT NULL
|
||||
Assert.Contains("\"NonNullableString\" TEXT NOT NULL", sql);
|
||||
Assert.Contains("\"NonNullableByteArray\" BYTEA NOT NULL", sql);
|
||||
|
||||
// Nullable reference types should not have NOT NULL
|
||||
Assert.Contains("\"NullableString\" TEXT", sql);
|
||||
Assert.DoesNotContain("\"NullableString\" TEXT NOT NULL", sql);
|
||||
Assert.Contains("\"NullableByteArray\" BYTEA", sql);
|
||||
Assert.DoesNotContain("\"NullableByteArray\" BYTEA NOT NULL", sql);
|
||||
|
||||
// Non-nullable value types should be NOT NULL (unchanged from before)
|
||||
Assert.Contains("\"NonNullableInt\" INTEGER NOT NULL", sql);
|
||||
Assert.Contains("\"NonNullableBool\" BOOLEAN NOT NULL", sql);
|
||||
|
||||
// Nullable value types should not have NOT NULL (unchanged from before)
|
||||
Assert.Contains("\"NullableInt\" INTEGER", sql);
|
||||
Assert.DoesNotContain("\"NullableInt\" INTEGER NOT NULL", sql);
|
||||
|
||||
this._output.WriteLine(sql);
|
||||
}
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
|
||||
#pragma warning disable CA1812 // Class is used via reflection
|
||||
private sealed class NrtTestRecord
|
||||
{
|
||||
[VectorStoreKey]
|
||||
public long Id { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public string NonNullableString { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public string? NullableString { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public byte[] NonNullableByteArray { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public byte[]? NullableByteArray { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public int NonNullableInt { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public int? NullableInt { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public bool NonNullableBool { get; set; }
|
||||
|
||||
[VectorStoreVector(10)]
|
||||
public ReadOnlyMemory<float> Embedding { get; set; }
|
||||
}
|
||||
#pragma warning restore CA1812
|
||||
#pragma warning restore CS8618
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user