Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ on:
- '**.cs'
- '**.csproj'

worflow_dispatch:

env:
REPODB_MYSQL_CONSTR_SYS: "Server=127.0.0.1;Port=43306;Database=sys;User ID=root;Password=ddd53e85-b15e-4da8-91e5-a7d3b00a0ab2;"
REPODB_ORACLE_CONSTR_MASTER: "Data Source=127.0.0.1:41521/FREEPDB1;User Id=system;Password=oracle;"
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ services:
POSTGRES_PASSWORD: ddd53e85-b15e-4da8-91e5-a7d3b00a0ab2

sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
image: mcr.microsoft.com/mssql/server:2025-latest
ports:
- 127.0.0.1:41433:1433
environment:
Expand Down
2 changes: 1 addition & 1 deletion src/RepoDb.Core.IntegrationTests/EnumPropertyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2097,7 +2097,7 @@ public TDbType Set(TEnum input, PropertyHandlerSetOptions options)
=> input == null || !enumToDb.TryGetValue(input, out var v) ? default : v;
}

public class CustomedEnumModel<TEnum> where TEnum : struct
public class CustomedEnumModel<TEnum> where TEnum : unmanaged, Enum
{
public TEnum? Value { get; set; }
}
Expand Down
12 changes: 6 additions & 6 deletions src/RepoDb.Core.IntegrationTests/TypeConversionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2216,7 +2216,7 @@ public void TestSqlConnectionInsertAndQueryConversionFromDoubleToBigInt()
var data = connection.Query<DoubleToBigIntClass>(e => e.SessionId == (Guid)id).FirstOrDefault();

// Assert
Assert.AreEqual(12346, data.ColumnBigInt);
Assert.AreEqual(12345, data.ColumnBigInt); // Legacy RepoDB indirectly used bankers rounding for this conversion, thus the current expected value is 12345 instead of the old 12346.
}

#endregion
Expand Down Expand Up @@ -2249,7 +2249,7 @@ public void TestSqlConnectionInsertAndQueryConversionFromDoubleToInt()
var data = connection.Query<DoubleToIntClass>(e => e.SessionId == (Guid)id).FirstOrDefault();

// Assert
Assert.AreEqual(12346, data.ColumnInt);
Assert.AreEqual(12345, data.ColumnInt); // Legacy RepoDB indirectly used bankers rounding for this conversion, thus the current expected value is 12345 instead of the old 12346.
}

#endregion
Expand Down Expand Up @@ -2282,7 +2282,7 @@ public void TestSqlConnectionInsertAndQueryConversionFromDoubleToSmallInt()
var data = connection.Query<DoubleToSmallIntClass>(e => e.SessionId == (Guid)id).FirstOrDefault();

// Assert
Assert.AreEqual(12346, data.ColumnInt);
Assert.AreEqual(12345, data.ColumnInt); // Legacy RepoDB indirectly used bankers rounding for this conversion, thus the current expected value is 12345 instead of the old 12346.
}

#endregion
Expand Down Expand Up @@ -2480,7 +2480,7 @@ public void TestSqlConnectionInsertAndQueryConversionFromFloatToBigInt()
var data = connection.Query<FloatToBigIntClass>(e => e.SessionId == (Guid)id).FirstOrDefault();

// Assert
Assert.AreEqual(12346, data.ColumnBigInt);
Assert.AreEqual(12345, data.ColumnBigInt); // Legacy RepoDB indirectly used bankers rounding for this conversion, thus the current expected value is 12345 instead of the old 12346.
}

#endregion
Expand Down Expand Up @@ -2513,7 +2513,7 @@ public void TestSqlConnectionInsertAndQueryConversionFromFloatToInt()
var data = connection.Query<FloatToIntClass>(e => e.SessionId == (Guid)id).FirstOrDefault();

// Assert
Assert.AreEqual(12346, data.ColumnInt);
Assert.AreEqual(12345, data.ColumnInt); // Legacy RepoDB indirectly used bankers rounding for this conversion, thus the current expected value is 12345 instead of the old 12346.
}

#endregion
Expand Down Expand Up @@ -2546,7 +2546,7 @@ public void TestSqlConnectionInsertAndQueryConversionFromFloatToSmallInt()
var data = connection.Query<FloatToSmallIntClass>(e => e.SessionId == (Guid)id).FirstOrDefault();

// Assert
Assert.AreEqual(12346, data.ColumnInt);
Assert.AreEqual(12345, data.ColumnInt); // Legacy RepoDB indirectly used bankers rounding for this conversion, thus the current expected value is 12345 instead of the old 12346.
}

#endregion
Expand Down
122 changes: 122 additions & 0 deletions src/RepoDb.SqlServer.IntegrationTests/Common/VectorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Common;
using System.Text.Json.Nodes;
using Microsoft.Data;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlTypes;
using RepoDb.SqlServer.IntegrationTests.Setup;
using RepoDb.TestCore;

namespace RepoDb.SqlServer.IntegrationTests.Common;

[TestClass]
public class VectorTests : DbTestBase<SqlServerDbInstance>
{

protected override void InitializeCore() => Database.Initialize();

public override DbConnection CreateConnection() => new SqlConnection(Database.ConnectionString);

class Vectors
{
public int Id { get; set; }
public SqlVector<float> VectorData { get; set; }
}

[TestMethod]
public void RunVectorTest()
{
using var connection = (SqlConnection)CreateConnection();

if (connection.GetDbHelper().GetDbConnectionRuntimeInformation(connection, null) is { } rti
&& rti.EngineVersion.Major < 17)
{
return; // Vector support was added with SqlServer 2025
}

string tableName = nameof(Vectors);
var vectorDimensionCount = 3;

using (var command = connection.CreateCommand($@"
IF OBJECT_ID('{tableName}', 'U') IS NOT NULL DROP TABLE {tableName};
IF OBJECT_ID('{tableName}Copy', 'U') IS NOT NULL DROP TABLE {tableName}Copy;"))
{
command.ExecuteNonQuery();
}

using (var command = connection.CreateCommand($@"
CREATE TABLE {tableName} (
Id INT IDENTITY(1,1) PRIMARY KEY,
VectorData VECTOR({vectorDimensionCount})
);

CREATE TABLE {tableName}Copy (
Id INT IDENTITY(1,1) PRIMARY KEY,
VectorData VECTOR({vectorDimensionCount})
);"))
{
command.ExecuteNonQuery();
}

// Raw insert, like Microsoft sample code
using (var command = (SqlCommand)connection.CreateCommand($"INSERT INTO {tableName} (VectorData) VALUES (@VectorData)"))
{
var param = command.Parameters.Add("@VectorData", SqlDbTypeExtensions.Vector);

// Insert null using DBNull.Value
param.Value = DBNull.Value;
command.ExecuteNonQuery();

// Insert non-null vector
param.Value = new SqlVector<float>(new float[] { 3.14159f, 1.61803f, 1.41421f });
command.ExecuteNonQuery();

// Insert typed null vector
param.Value = SqlVector<float>.CreateNull(vectorDimensionCount);
command.ExecuteNonQuery();

// Prepare once and reuse for loop
command.Prepare();
for (int i = 0; i < 10; i++)
{
param.Value = new SqlVector<float>(new float[]
{
i + 0.1f,
i + 0.2f,
i + 0.3f
});
command.ExecuteNonQuery();
}
}

// And do this the RepoDb way
connection.Insert(new Vectors
{
VectorData = new SqlVector<float>(new float[] { 0.1f, 0.2f, 0.3f })
});

foreach (var c in connection.QueryAll<Vectors>())
{
if (!c.VectorData.IsNull)
{
float[] values = c.VectorData.Memory.ToArray();
Console.WriteLine("VectorData: " + string.Join(", ", values));
}
else
{
Console.WriteLine("VectorData: NULL");
}
}

foreach (var c in connection.ExecuteQuery<double?>($"SELECT VECTOR_DISTANCE(@how, {nameof(Vectors.VectorData)}, @qv) FROM {nameof(Vectors)}",
new
{
qv = new SqlVector<float>(new float[] { 1, 2, 3 }),
how = "euclidean"
})
)
{
Console.WriteLine(c);
}
}
}
106 changes: 93 additions & 13 deletions src/RepoDb.SqlServer/DbHelpers/SqlServerDbHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Data.Common;
using System.Text.RegularExpressions;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlTypes;
using RepoDb.DbSettings;
using RepoDb.Enumerations;
using RepoDb.Extensions;
Expand All @@ -20,7 +21,7 @@ public sealed class SqlServerDbHelper : BaseDbHelper
/// Creates a new instance of <see cref="SqlServerDbHelper"/> class.
/// </summary>
public SqlServerDbHelper()
: this(new SqlServerDbTypeNameToClientTypeResolver())
: this(SqlServerDbTypeNameToClientTypeResolver.Instance)
{ }

/// <summary>
Expand Down Expand Up @@ -153,15 +154,54 @@ public override DbFieldCollection GetFields(IDbConnection connection,
};

// Iterate and extract
using var reader = (DbDataReader)connection.ExecuteReader(commandText, param, transaction: transaction);

var dbFields = new List<DbField>();
using (var reader = (DbDataReader)connection.ExecuteReader(commandText, param, transaction: transaction))
{
// Iterate the list of the fields
while (reader.Read())
{
dbFields.Add(ReaderToDbField(reader));
}
}

// Iterate the list of the fields
while (reader.Read())
#if NET // Half support is #if NET, so no need to check for other types
if (dbFields.Any(x => x.Type == typeof(SqlVector<float>)))
{
dbFields.Add(ReaderToDbField(reader));
// If any of the fields is of type SqlVector<float>, we need to check the actual subtype of the vector, as SQL Server supports both float and real vectors.
// We can't just always query vector_base_type as that column is SqlServer 2025+

var cols = dbFields.Where(x => x.Type == typeof(SqlVector<float>)).Select(x=>x.FieldName).ToList();

foreach (var (name, base_type) in connection.ExecuteQuery<(string name, int vector_base_type)>(@"
SELECT
c.name,
c.vector_base_type
FROM sys.columns c
JOIN sys.types t ON c.user_type_id = t.user_type_id
JOIN sys.tables tbl ON c.object_id = tbl.object_id
JOIN sys.schemas s ON tbl.schema_id = s.schema_id
WHERE s.name = @Schema AND tbl.name = @TableName AND c.name IN (@Columns)",
new
{
param.Schema,
param.TableName,
Columns = cols
}))
{
// base_type = 0 is float. 1 is half. others undefined
if (base_type == 1)
{
int i = dbFields.FindIndex(x => x.FieldName == name);
var from = dbFields[i];

dbFields[i] = new DbField(from.FieldName, from.IsPrimary, from.IsIdentity, from.IsNullable,
typeof(SqlVector<Half>),
from.Size, from.Precision, from.Scale, from.DatabaseType, from.HasDefaultValue, from.IsGenerated, from.Provider);
}
;
}
}
#endif

// Return the list of fields
return new(dbFields);
Expand Down Expand Up @@ -189,18 +229,58 @@ public override async ValueTask<DbFieldCollection> GetFieldsAsync(IDbConnection
TableName = DataEntityExtension.GetTableName(tableName, setting)
};

// Iterate and extract
using var reader = (DbDataReader)await connection.ExecuteReaderAsync(commandText, param,
transaction: transaction, cancellationToken: cancellationToken);

var dbFields = new List<DbField>();

// Iterate the list of the fields
while (await reader.ReadAsync(cancellationToken))
// Iterate and extract
using (var reader = (DbDataReader)await connection.ExecuteReaderAsync(commandText, param,
transaction: transaction, cancellationToken: cancellationToken))
{
dbFields.Add(await ReaderToDbFieldAsync(reader, cancellationToken));
// Iterate the list of the fields
while (await reader.ReadAsync(cancellationToken))
{
dbFields.Add(await ReaderToDbFieldAsync(reader, cancellationToken));
}
}

#if NET // Half support is #if NET, so no need to check for other types
if (dbFields.Any(x => x.Type == typeof(SqlVector<float>)))
{
// If any of the fields is of type SqlVector<float>, we need to check the actual subtype of the vector, as SQL Server supports both float and real vectors.
// We can't just always query vector_base_type as that column is SqlServer 2025+

var cols = dbFields.Where(x => x.Type == typeof(SqlVector<float>)).Select(x => x.FieldName).ToList();

foreach (var (name, base_type) in await connection.ExecuteQueryAsync<(string name, int vector_base_type)>(@"
SELECT
c.name,
c.vector_base_type
FROM sys.columns c
JOIN sys.types t ON c.user_type_id = t.user_type_id
JOIN sys.tables tbl ON c.object_id = tbl.object_id
JOIN sys.schemas s ON tbl.schema_id = s.schema_id
WHERE s.name = @Schema AND tbl.name = @TableName AND c.name IN (@Columns)",
new
{
param.Schema,
param.TableName,
Columns = cols
}, cancellationToken: cancellationToken))
{
// base_type = 0 is float. 1 is half. others undefined
if (base_type == 1)
{
int i = dbFields.FindIndex(x => x.FieldName == name);
var from = dbFields[i];

dbFields[i] = new DbField(from.FieldName, from.IsPrimary, from.IsIdentity, from.IsNullable,
typeof(SqlVector<Half>),
from.Size, from.Precision, from.Scale, from.DatabaseType, from.HasDefaultValue, from.IsGenerated, from.Provider);
}
;
}
}
#endif

// Return the list of fields
return new(dbFields);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,6 @@ DbType.VarNumeric or
_ => "NVARCHAR",
};
}

public static readonly DbTypeToSqlServerStringNameResolver Instance = new();
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class SqlServerConvertFieldResolver : DbConvertFieldResolver
/// </summary>
public SqlServerConvertFieldResolver()
: this(ClientTypeToDbTypeResolver.Instance,
new DbTypeToSqlServerStringNameResolver())
DbTypeToSqlServerStringNameResolver.Instance)
{ }

/// <summary>
Expand Down Expand Up @@ -50,4 +50,7 @@ public SqlServerConvertFieldResolver(IResolver<Type, DbType?> dbTypeResolver,
}

#endregion


public static readonly SqlServerConvertFieldResolver Instance = new();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using RepoDb.Interfaces;
using Microsoft.Data.SqlTypes;
using RepoDb.Interfaces;
using RepoDb.Types;

namespace RepoDb.Resolvers;
Expand Down Expand Up @@ -48,7 +49,10 @@ public virtual Type Resolve(string dbTypeName)
typeof(TimeSpan),
"tinyint" => typeof(byte),
"uniqueidentifier" => typeof(Guid),
"vector" => typeof(SqlVector<float>),
_ => typeof(object),
};
}

public static readonly SqlServerDbTypeNameToClientTypeResolver Instance = new();
}
Loading
Loading