388 lines
14 KiB
C#
388 lines
14 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.Extensions.VectorData;
|
|
using Microsoft.Extensions.VectorData.ProviderServices;
|
|
using Microsoft.SemanticKernel.Connectors.SqliteVec;
|
|
using Xunit;
|
|
|
|
namespace SemanticKernel.Connectors.SqliteVec.UnitTests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="SqliteCommandBuilder"/> class.
|
|
/// </summary>
|
|
public sealed class SqliteCommandBuilderTests : IDisposable
|
|
{
|
|
private readonly SqliteCommand _command;
|
|
private readonly SqliteConnection _connection;
|
|
|
|
public SqliteCommandBuilderTests()
|
|
{
|
|
this._command = new() { Connection = this._connection };
|
|
this._connection = new();
|
|
}
|
|
|
|
[Fact]
|
|
public void ItBuildsTableCountCommand()
|
|
{
|
|
// Arrange
|
|
const string TableName = "TestTable";
|
|
|
|
// Act
|
|
var command = SqliteCommandBuilder.BuildTableCountCommand(this._connection, TableName);
|
|
|
|
// Assert
|
|
Assert.Equal("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=@tableName;", command.CommandText);
|
|
Assert.Equal("@tableName", command.Parameters[0].ParameterName);
|
|
Assert.Equal(TableName, command.Parameters[0].Value);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void ItBuildsCreateTableCommand(bool ifNotExists)
|
|
{
|
|
// Arrange
|
|
const string TableName = "TestTable";
|
|
|
|
var columns = new List<SqliteColumn>
|
|
{
|
|
new("Column1", "Type1", isPrimary: true),
|
|
new("Column2", "Type2", isPrimary: false) { IsNullable = true, Configuration = new() { ["distance_metric"] = "l2" } },
|
|
new("Column3", "Type3", isPrimary: false) { IsNullable = false },
|
|
new("Column4", "Type4", isPrimary: false) { IsNullable = true },
|
|
};
|
|
|
|
// Act
|
|
var command = SqliteCommandBuilder.BuildCreateTableCommand(this._connection, TableName, columns, ifNotExists);
|
|
|
|
// Assert
|
|
Assert.Contains("CREATE TABLE", command.CommandText);
|
|
Assert.Contains(TableName, command.CommandText);
|
|
|
|
Assert.Equal(ifNotExists, command.CommandText.Contains("IF NOT EXISTS"));
|
|
|
|
Assert.Contains("\"Column1\" Type1 PRIMARY KEY", command.CommandText);
|
|
Assert.Contains("\"Column2\" Type2 distance_metric=l2", command.CommandText);
|
|
Assert.Contains("\"Column3\" Type3 NOT NULL", command.CommandText);
|
|
Assert.Contains("\"Column4\" Type4", command.CommandText);
|
|
Assert.DoesNotContain("\"Column4\" Type4 NOT NULL", command.CommandText);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void ItBuildsCreateVirtualTableCommand(bool ifNotExists)
|
|
{
|
|
// Arrange
|
|
const string TableName = "TestTable";
|
|
|
|
var columns = new List<SqliteColumn>
|
|
{
|
|
new("Column1", "Type1", isPrimary: true),
|
|
new("Column2", "Type2", isPrimary: false) { IsNullable = true, Configuration = new() { ["distance_metric"] = "l2" } },
|
|
};
|
|
|
|
// Act
|
|
var command = SqliteCommandBuilder.BuildCreateVirtualTableCommand(this._connection, TableName, columns, ifNotExists);
|
|
|
|
// Assert
|
|
Assert.Contains("CREATE VIRTUAL TABLE", command.CommandText);
|
|
Assert.Contains(TableName, command.CommandText);
|
|
Assert.Contains("USING vec0", command.CommandText);
|
|
|
|
Assert.Equal(ifNotExists, command.CommandText.Contains("IF NOT EXISTS"));
|
|
|
|
Assert.Contains("Column1 Type1 PRIMARY KEY", command.CommandText);
|
|
Assert.Contains("Column2 Type2 distance_metric=l2", command.CommandText);
|
|
}
|
|
|
|
[Fact]
|
|
public void ItBuildsDropTableCommand()
|
|
{
|
|
// Arrange
|
|
const string TableName = "TestTable";
|
|
|
|
// Act
|
|
var command = SqliteCommandBuilder.BuildDropTableCommand(this._connection, TableName);
|
|
|
|
// Assert
|
|
Assert.Equal("DROP TABLE IF EXISTS \"TestTable\";", command.CommandText);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void ItBuildsInsertCommand_without_autogenerated_key(bool replaceIfExists)
|
|
{
|
|
// Arrange
|
|
const string TableName = "TestTable";
|
|
|
|
var model = BuildModel(
|
|
[
|
|
new VectorStoreKeyProperty("Id", typeof(int)),
|
|
new VectorStoreDataProperty("Name", typeof(string)),
|
|
new VectorStoreDataProperty("Age", typeof(string)),
|
|
new VectorStoreDataProperty("Address", typeof(string)),
|
|
]);
|
|
|
|
var records = new List<Dictionary<string, object?>>
|
|
{
|
|
new() { ["Id"] = 1, ["Name"] = "NameValue1", ["Age"] = "AgeValue1", ["Address"] = "AddressValue1" },
|
|
new() { ["Id"] = 2, ["Name"] = "NameValue2", ["Age"] = "AgeValue2", ["Address"] = "AddressValue2" },
|
|
};
|
|
|
|
// Act
|
|
var command = SqliteCommandBuilder.BuildInsertCommand(
|
|
this._connection,
|
|
TableName,
|
|
model,
|
|
records,
|
|
generatedEmbeddings: null,
|
|
data: true,
|
|
replaceIfExists: replaceIfExists);
|
|
|
|
// Assert
|
|
Assert.Equal(replaceIfExists, command.CommandText.Contains("OR REPLACE"));
|
|
|
|
Assert.Contains($"INTO \"{TableName}\" (\"Id\", \"Name\", \"Age\", \"Address\")", command.CommandText);
|
|
Assert.Contains("VALUES (@Id0, @Name0, @Age0, @Address0)", command.CommandText);
|
|
Assert.Contains("VALUES (@Id1, @Name1, @Age1, @Address1)", command.CommandText);
|
|
Assert.DoesNotContain("RETURNING", command.CommandText);
|
|
|
|
Assert.Equal("@Id0", command.Parameters[0].ParameterName);
|
|
Assert.Equal(1, command.Parameters[0].Value);
|
|
|
|
Assert.Equal("@Name0", command.Parameters[1].ParameterName);
|
|
Assert.Equal("NameValue1", command.Parameters[1].Value);
|
|
|
|
Assert.Equal("@Age0", command.Parameters[2].ParameterName);
|
|
Assert.Equal("AgeValue1", command.Parameters[2].Value);
|
|
|
|
Assert.Equal("@Address0", command.Parameters[3].ParameterName);
|
|
Assert.Equal("AddressValue1", command.Parameters[3].Value);
|
|
|
|
Assert.Equal("@Id1", command.Parameters[4].ParameterName);
|
|
Assert.Equal(2, command.Parameters[4].Value);
|
|
|
|
Assert.Equal("@Name1", command.Parameters[5].ParameterName);
|
|
Assert.Equal("NameValue2", command.Parameters[5].Value);
|
|
|
|
Assert.Equal("@Age1", command.Parameters[6].ParameterName);
|
|
Assert.Equal("AgeValue2", command.Parameters[6].Value);
|
|
|
|
Assert.Equal("@Address1", command.Parameters[7].ParameterName);
|
|
Assert.Equal("AddressValue2", command.Parameters[7].Value);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void ItBuildsInsertCommand_with_autogenerated_key(bool replaceIfExists)
|
|
{
|
|
// Arrange
|
|
const string TableName = "TestTable";
|
|
|
|
var model = BuildModel(
|
|
[
|
|
new VectorStoreKeyProperty("Id", typeof(int)),
|
|
new VectorStoreDataProperty("Name", typeof(string)),
|
|
new VectorStoreDataProperty("Age", typeof(string)),
|
|
new VectorStoreDataProperty("Address", typeof(string)),
|
|
]);
|
|
|
|
var records = new List<Dictionary<string, object?>>
|
|
{
|
|
new() { ["Id"] = default(int), ["Name"] = "NameValue1", ["Age"] = "AgeValue1", ["Address"] = "AddressValue1" },
|
|
new() { ["Id"] = default(int), ["Name"] = "NameValue2", ["Age"] = "AgeValue2", ["Address"] = "AddressValue2" },
|
|
};
|
|
|
|
// Act
|
|
var command = SqliteCommandBuilder.BuildInsertCommand(
|
|
this._connection,
|
|
TableName,
|
|
model,
|
|
records,
|
|
generatedEmbeddings: null,
|
|
data: true,
|
|
replaceIfExists: replaceIfExists);
|
|
|
|
// Assert
|
|
Assert.DoesNotContain("OR REPLACE", command.CommandText);
|
|
|
|
Assert.Contains($"INTO \"{TableName}\" (\"Name\", \"Age\", \"Address\")", command.CommandText);
|
|
Assert.Contains("VALUES (@Name0, @Age0, @Address0)", command.CommandText);
|
|
Assert.Contains("VALUES (@Name1, @Age1, @Address1)", command.CommandText);
|
|
Assert.Contains("RETURNING \"Id\"", command.CommandText);
|
|
|
|
Assert.Equal("@Name0", command.Parameters[0].ParameterName);
|
|
Assert.Equal("NameValue1", command.Parameters[0].Value);
|
|
|
|
Assert.Equal("@Name1", command.Parameters[3].ParameterName);
|
|
Assert.Equal("NameValue2", command.Parameters[3].Value);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData("Age")]
|
|
public void ItBuildsSelectCommand(string? orderByPropertyName)
|
|
{
|
|
// Arrange
|
|
const string TableName = "TestTable";
|
|
|
|
var model = BuildModel(
|
|
[
|
|
new VectorStoreKeyProperty("Id", typeof(string)),
|
|
new VectorStoreDataProperty("Name", typeof(string)),
|
|
new VectorStoreDataProperty("Age", typeof(string)),
|
|
new VectorStoreDataProperty("Address", typeof(string)),
|
|
]);
|
|
var conditions = new List<SqliteWhereCondition>
|
|
{
|
|
new SqliteWhereEqualsCondition("Name", "NameValue"),
|
|
new SqliteWhereInCondition("Age", [10, 20, 30]),
|
|
};
|
|
FilteredRecordRetrievalOptions<Dictionary<string, object?>> filterOptions = new();
|
|
if (!string.IsNullOrWhiteSpace(orderByPropertyName))
|
|
{
|
|
filterOptions.OrderBy = orderBy => orderBy.Ascending(record => record[orderByPropertyName]);
|
|
}
|
|
|
|
// Act
|
|
var command = SqliteCommandBuilder.BuildSelectDataCommand<Dictionary<string, object?>>(this._connection, TableName, model, conditions, filterOptions);
|
|
|
|
// Assert
|
|
Assert.Contains("SELECT \"Id\",\"Name\",\"Age\",\"Address\"", command.CommandText);
|
|
Assert.Contains($"FROM \"{TableName}\"", command.CommandText);
|
|
|
|
Assert.Contains("\"Name\" = @Name0", command.CommandText);
|
|
Assert.Contains("\"Age\" IN (@Age0, @Age1, @Age2)", command.CommandText);
|
|
|
|
Assert.Equal(!string.IsNullOrWhiteSpace(orderByPropertyName), command.CommandText.Contains($"ORDER BY \"{orderByPropertyName}\""));
|
|
|
|
Assert.Equal("@Name0", command.Parameters[0].ParameterName);
|
|
Assert.Equal("NameValue", command.Parameters[0].Value);
|
|
|
|
Assert.Equal("@Age0", command.Parameters[1].ParameterName);
|
|
Assert.Equal(10, command.Parameters[1].Value);
|
|
|
|
Assert.Equal("@Age1", command.Parameters[2].ParameterName);
|
|
Assert.Equal(20, command.Parameters[2].Value);
|
|
|
|
Assert.Equal("@Age2", command.Parameters[3].ParameterName);
|
|
Assert.Equal(30, command.Parameters[3].Value);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData("Age")]
|
|
public void ItBuildsSelectInnerJoinCommand(string? orderByPropertyName)
|
|
{
|
|
// Arrange
|
|
const string DataTable = "DataTable";
|
|
const string VectorTable = "VectorTable";
|
|
const string JoinColumnName = "Id";
|
|
|
|
var model = BuildModel(
|
|
[
|
|
new VectorStoreKeyProperty("Id", typeof(string)),
|
|
new VectorStoreDataProperty("Name", typeof(string)),
|
|
new VectorStoreVectorProperty("Age", typeof(ReadOnlyMemory<float>), 10),
|
|
new VectorStoreVectorProperty("Address", typeof(ReadOnlyMemory<float>), 10),
|
|
]);
|
|
|
|
var conditions = new List<SqliteWhereCondition>
|
|
{
|
|
new SqliteWhereEqualsCondition("Name", "NameValue"),
|
|
new SqliteWhereInCondition("Age", [10, 20, 30]),
|
|
};
|
|
FilteredRecordRetrievalOptions<Dictionary<string, object?>> filterOptions = new();
|
|
if (!string.IsNullOrWhiteSpace(orderByPropertyName))
|
|
{
|
|
filterOptions.OrderBy = orderBy => orderBy.Ascending(record => record[orderByPropertyName]);
|
|
}
|
|
|
|
// Act
|
|
var command = SqliteCommandBuilder.BuildSelectInnerJoinCommand(
|
|
this._connection,
|
|
VectorTable,
|
|
DataTable,
|
|
JoinColumnName,
|
|
model,
|
|
conditions,
|
|
true,
|
|
filterOptions);
|
|
|
|
// Assert
|
|
Assert.Contains("SELECT \"DataTable\".\"Id\",\"DataTable\".\"Name\",\"VectorTable\".\"Age\",\"VectorTable\".\"Address\"", command.CommandText);
|
|
Assert.Contains("FROM \"VectorTable\"", command.CommandText);
|
|
|
|
Assert.Contains("INNER JOIN \"DataTable\" ON \"VectorTable\".\"Id\" = \"DataTable\".\"Id\"", command.CommandText);
|
|
|
|
Assert.Contains("\"Name\" = @Name0", command.CommandText);
|
|
Assert.Contains("\"Age\" IN (@Age0, @Age1, @Age2)", command.CommandText);
|
|
|
|
Assert.Equal(!string.IsNullOrWhiteSpace(orderByPropertyName), command.CommandText.Contains($"ORDER BY \"DataTable\".\"{orderByPropertyName}\""));
|
|
|
|
Assert.Equal("@Name0", command.Parameters[0].ParameterName);
|
|
Assert.Equal("NameValue", command.Parameters[0].Value);
|
|
|
|
Assert.Equal("@Age0", command.Parameters[1].ParameterName);
|
|
Assert.Equal(10, command.Parameters[1].Value);
|
|
|
|
Assert.Equal("@Age1", command.Parameters[2].ParameterName);
|
|
Assert.Equal(20, command.Parameters[2].Value);
|
|
|
|
Assert.Equal("@Age2", command.Parameters[3].ParameterName);
|
|
Assert.Equal(30, command.Parameters[3].Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void ItBuildsDeleteCommand()
|
|
{
|
|
// Arrange
|
|
const string TableName = "TestTable";
|
|
|
|
var conditions = new List<SqliteWhereCondition>
|
|
{
|
|
new SqliteWhereEqualsCondition("Name", "NameValue"),
|
|
new SqliteWhereInCondition("Age", [10, 20, 30]),
|
|
};
|
|
|
|
// Act
|
|
var command = SqliteCommandBuilder.BuildDeleteCommand(this._connection, TableName, conditions);
|
|
|
|
// Assert
|
|
Assert.Contains("DELETE FROM \"TestTable\"", command.CommandText);
|
|
|
|
Assert.Contains("\"Name\" = @Name0", command.CommandText);
|
|
Assert.Contains("\"Age\" IN (@Age0, @Age1, @Age2)", command.CommandText);
|
|
|
|
Assert.Equal("@Name0", command.Parameters[0].ParameterName);
|
|
Assert.Equal("NameValue", command.Parameters[0].Value);
|
|
|
|
Assert.Equal("@Age0", command.Parameters[1].ParameterName);
|
|
Assert.Equal(10, command.Parameters[1].Value);
|
|
|
|
Assert.Equal("@Age1", command.Parameters[2].ParameterName);
|
|
Assert.Equal(20, command.Parameters[2].Value);
|
|
|
|
Assert.Equal("@Age2", command.Parameters[3].ParameterName);
|
|
Assert.Equal(30, command.Parameters[3].Value);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
this._command.Dispose();
|
|
this._connection.Dispose();
|
|
}
|
|
|
|
private static CollectionModel BuildModel(List<VectorStoreProperty> properties)
|
|
=> new SqliteModelBuilder()
|
|
.BuildDynamic(new() { Properties = properties }, defaultEmbeddingGenerator: null);
|
|
}
|