Skip to content

Commit da4b33a

Browse files
committed
Merge pull request #40 from AmpScm/refactor/more-expression-conversions
More expression conversions
2 parents 9cf9daf + e43d48a commit da4b33a

43 files changed

Lines changed: 684 additions & 385 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
POSTGRES_PASSWORD: ddd53e85-b15e-4da8-91e5-a7d3b00a0ab2
4949

5050
sqlserver:
51-
image: mcr.microsoft.com/mssql/server:2022-latest
51+
image: mcr.microsoft.com/mssql/server:2025-latest
5252
ports:
5353
- 127.0.0.1:41433:1433
5454
env:

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ services:
2525
POSTGRES_PASSWORD: ddd53e85-b15e-4da8-91e5-a7d3b00a0ab2
2626

2727
sqlserver:
28-
image: mcr.microsoft.com/mssql/server:2022-latest
28+
image: mcr.microsoft.com/mssql/server:2025-latest
2929
ports:
3030
- 127.0.0.1:41433:1433
3131
environment:

src/RepoDb.Core.IntegrationTests/EnumPropertyTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2097,7 +2097,7 @@ public TDbType Set(TEnum input, PropertyHandlerSetOptions options)
20972097
=> input == null || !enumToDb.TryGetValue(input, out var v) ? default : v;
20982098
}
20992099

2100-
public class CustomedEnumModel<TEnum> where TEnum : struct
2100+
public class CustomedEnumModel<TEnum> where TEnum : unmanaged, Enum
21012101
{
21022102
public TEnum? Value { get; set; }
21032103
}

src/RepoDb.Core.IntegrationTests/TypeConversionsTest.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2216,7 +2216,7 @@ public void TestSqlConnectionInsertAndQueryConversionFromDoubleToBigInt()
22162216
var data = connection.Query<DoubleToBigIntClass>(e => e.SessionId == (Guid)id).FirstOrDefault();
22172217

22182218
// Assert
2219-
Assert.AreEqual(12346, data.ColumnBigInt);
2219+
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.
22202220
}
22212221

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

22512251
// Assert
2252-
Assert.AreEqual(12346, data.ColumnInt);
2252+
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.
22532253
}
22542254

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

22842284
// Assert
2285-
Assert.AreEqual(12346, data.ColumnInt);
2285+
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.
22862286
}
22872287

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

24822482
// Assert
2483-
Assert.AreEqual(12346, data.ColumnBigInt);
2483+
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.
24842484
}
24852485

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

25152515
// Assert
2516-
Assert.AreEqual(12346, data.ColumnInt);
2516+
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.
25172517
}
25182518

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

25482548
// Assert
2549-
Assert.AreEqual(12346, data.ColumnInt);
2549+
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.
25502550
}
25512551

25522552
#endregion
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using System.ComponentModel.DataAnnotations.Schema;
2+
using System.Data.Common;
3+
using System.Text.Json.Nodes;
4+
using Microsoft.Data;
5+
using Microsoft.Data.SqlClient;
6+
using Microsoft.Data.SqlTypes;
7+
using RepoDb.SqlServer.IntegrationTests.Setup;
8+
using RepoDb.TestCore;
9+
10+
namespace RepoDb.SqlServer.IntegrationTests.Common;
11+
12+
[TestClass]
13+
public class VectorTests : DbTestBase<SqlServerDbInstance>
14+
{
15+
16+
protected override void InitializeCore() => Database.Initialize();
17+
18+
public override DbConnection CreateConnection() => new SqlConnection(Database.ConnectionString);
19+
20+
class Vectors
21+
{
22+
public int Id { get; set; }
23+
public SqlVector<float> VectorData { get; set; }
24+
}
25+
26+
[TestMethod]
27+
public void RunVectorTest()
28+
{
29+
using var connection = (SqlConnection)CreateConnection();
30+
31+
if (connection.GetDbHelper().GetDbConnectionRuntimeInformation(connection, null) is { } rti
32+
&& rti.EngineVersion.Major < 17)
33+
{
34+
return; // Vector support was added with SqlServer 2025
35+
}
36+
37+
string tableName = nameof(Vectors);
38+
var vectorDimensionCount = 3;
39+
40+
using (var command = connection.CreateCommand($@"
41+
IF OBJECT_ID('{tableName}', 'U') IS NOT NULL DROP TABLE {tableName};
42+
IF OBJECT_ID('{tableName}Copy', 'U') IS NOT NULL DROP TABLE {tableName}Copy;"))
43+
{
44+
command.ExecuteNonQuery();
45+
}
46+
47+
using (var command = connection.CreateCommand($@"
48+
CREATE TABLE {tableName} (
49+
Id INT IDENTITY(1,1) PRIMARY KEY,
50+
VectorData VECTOR({vectorDimensionCount})
51+
);
52+
53+
CREATE TABLE {tableName}Copy (
54+
Id INT IDENTITY(1,1) PRIMARY KEY,
55+
VectorData VECTOR({vectorDimensionCount})
56+
);"))
57+
{
58+
command.ExecuteNonQuery();
59+
}
60+
61+
// Raw insert, like Microsoft sample code
62+
using (var command = (SqlCommand)connection.CreateCommand($"INSERT INTO {tableName} (VectorData) VALUES (@VectorData)"))
63+
{
64+
var param = command.Parameters.Add("@VectorData", SqlDbTypeExtensions.Vector);
65+
66+
// Insert null using DBNull.Value
67+
param.Value = DBNull.Value;
68+
command.ExecuteNonQuery();
69+
70+
// Insert non-null vector
71+
param.Value = new SqlVector<float>(new float[] { 3.14159f, 1.61803f, 1.41421f });
72+
command.ExecuteNonQuery();
73+
74+
// Insert typed null vector
75+
param.Value = SqlVector<float>.CreateNull(vectorDimensionCount);
76+
command.ExecuteNonQuery();
77+
78+
// Prepare once and reuse for loop
79+
command.Prepare();
80+
for (int i = 0; i < 10; i++)
81+
{
82+
param.Value = new SqlVector<float>(new float[]
83+
{
84+
i + 0.1f,
85+
i + 0.2f,
86+
i + 0.3f
87+
});
88+
command.ExecuteNonQuery();
89+
}
90+
}
91+
92+
// And do this the RepoDb way
93+
connection.Insert(new Vectors
94+
{
95+
VectorData = new SqlVector<float>(new float[] { 0.1f, 0.2f, 0.3f })
96+
});
97+
98+
foreach (var c in connection.QueryAll<Vectors>())
99+
{
100+
if (!c.VectorData.IsNull)
101+
{
102+
float[] values = c.VectorData.Memory.ToArray();
103+
Console.WriteLine("VectorData: " + string.Join(", ", values));
104+
}
105+
else
106+
{
107+
Console.WriteLine("VectorData: NULL");
108+
}
109+
}
110+
111+
foreach (var c in connection.ExecuteQuery<double?>($"SELECT VECTOR_DISTANCE(@how, {nameof(Vectors.VectorData)}, @qv) FROM {nameof(Vectors)}",
112+
new
113+
{
114+
qv = new SqlVector<float>(new float[] { 1, 2, 3 }),
115+
how = "euclidean"
116+
})
117+
)
118+
{
119+
Console.WriteLine(c);
120+
}
121+
}
122+
}

src/RepoDb.SqlServer/DbHelpers/SqlServerDbHelper.cs

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Data.Common;
44
using System.Text.RegularExpressions;
55
using Microsoft.Data.SqlClient;
6+
using Microsoft.Data.SqlTypes;
67
using RepoDb.DbSettings;
78
using RepoDb.Enumerations;
89
using RepoDb.Extensions;
@@ -20,7 +21,7 @@ public sealed class SqlServerDbHelper : BaseDbHelper
2021
/// Creates a new instance of <see cref="SqlServerDbHelper"/> class.
2122
/// </summary>
2223
public SqlServerDbHelper()
23-
: this(new SqlServerDbTypeNameToClientTypeResolver())
24+
: this(SqlServerDbTypeNameToClientTypeResolver.Instance)
2425
{ }
2526

2627
/// <summary>
@@ -153,15 +154,54 @@ public override DbFieldCollection GetFields(IDbConnection connection,
153154
};
154155

155156
// Iterate and extract
156-
using var reader = (DbDataReader)connection.ExecuteReader(commandText, param, transaction: transaction);
157-
158157
var dbFields = new List<DbField>();
158+
using (var reader = (DbDataReader)connection.ExecuteReader(commandText, param, transaction: transaction))
159+
{
160+
// Iterate the list of the fields
161+
while (reader.Read())
162+
{
163+
dbFields.Add(ReaderToDbField(reader));
164+
}
165+
}
159166

160-
// Iterate the list of the fields
161-
while (reader.Read())
167+
#if NET // Half support is #if NET, so no need to check for other types
168+
if (dbFields.Any(x => x.Type == typeof(SqlVector<float>)))
162169
{
163-
dbFields.Add(ReaderToDbField(reader));
170+
// 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.
171+
// We can't just always query vector_base_type as that column is SqlServer 2025+
172+
173+
var cols = dbFields.Where(x => x.Type == typeof(SqlVector<float>)).Select(x=>x.FieldName).ToList();
174+
175+
foreach (var (name, base_type) in connection.ExecuteQuery<(string name, int vector_base_type)>(@"
176+
SELECT
177+
c.name,
178+
c.vector_base_type
179+
FROM sys.columns c
180+
JOIN sys.types t ON c.user_type_id = t.user_type_id
181+
JOIN sys.tables tbl ON c.object_id = tbl.object_id
182+
JOIN sys.schemas s ON tbl.schema_id = s.schema_id
183+
WHERE s.name = @Schema AND tbl.name = @TableName AND c.name IN (@Columns)",
184+
new
185+
{
186+
param.Schema,
187+
param.TableName,
188+
Columns = cols
189+
}))
190+
{
191+
// base_type = 0 is float. 1 is half. others undefined
192+
if (base_type == 1)
193+
{
194+
int i = dbFields.FindIndex(x => x.FieldName == name);
195+
var from = dbFields[i];
196+
197+
dbFields[i] = new DbField(from.FieldName, from.IsPrimary, from.IsIdentity, from.IsNullable,
198+
typeof(SqlVector<Half>),
199+
from.Size, from.Precision, from.Scale, from.DatabaseType, from.HasDefaultValue, from.IsGenerated, from.Provider);
200+
}
201+
;
202+
}
164203
}
204+
#endif
165205

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

192-
// Iterate and extract
193-
using var reader = (DbDataReader)await connection.ExecuteReaderAsync(commandText, param,
194-
transaction: transaction, cancellationToken: cancellationToken);
195-
196232
var dbFields = new List<DbField>();
197233

198-
// Iterate the list of the fields
199-
while (await reader.ReadAsync(cancellationToken))
234+
// Iterate and extract
235+
using (var reader = (DbDataReader)await connection.ExecuteReaderAsync(commandText, param,
236+
transaction: transaction, cancellationToken: cancellationToken))
200237
{
201-
dbFields.Add(await ReaderToDbFieldAsync(reader, cancellationToken));
238+
// Iterate the list of the fields
239+
while (await reader.ReadAsync(cancellationToken))
240+
{
241+
dbFields.Add(await ReaderToDbFieldAsync(reader, cancellationToken));
242+
}
202243
}
203244

245+
#if NET // Half support is #if NET, so no need to check for other types
246+
if (dbFields.Any(x => x.Type == typeof(SqlVector<float>)))
247+
{
248+
// 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.
249+
// We can't just always query vector_base_type as that column is SqlServer 2025+
250+
251+
var cols = dbFields.Where(x => x.Type == typeof(SqlVector<float>)).Select(x => x.FieldName).ToList();
252+
253+
foreach (var (name, base_type) in await connection.ExecuteQueryAsync<(string name, int vector_base_type)>(@"
254+
SELECT
255+
c.name,
256+
c.vector_base_type
257+
FROM sys.columns c
258+
JOIN sys.types t ON c.user_type_id = t.user_type_id
259+
JOIN sys.tables tbl ON c.object_id = tbl.object_id
260+
JOIN sys.schemas s ON tbl.schema_id = s.schema_id
261+
WHERE s.name = @Schema AND tbl.name = @TableName AND c.name IN (@Columns)",
262+
new
263+
{
264+
param.Schema,
265+
param.TableName,
266+
Columns = cols
267+
}, cancellationToken: cancellationToken))
268+
{
269+
// base_type = 0 is float. 1 is half. others undefined
270+
if (base_type == 1)
271+
{
272+
int i = dbFields.FindIndex(x => x.FieldName == name);
273+
var from = dbFields[i];
274+
275+
dbFields[i] = new DbField(from.FieldName, from.IsPrimary, from.IsIdentity, from.IsNullable,
276+
typeof(SqlVector<Half>),
277+
from.Size, from.Precision, from.Scale, from.DatabaseType, from.HasDefaultValue, from.IsGenerated, from.Provider);
278+
}
279+
;
280+
}
281+
}
282+
#endif
283+
204284
// Return the list of fields
205285
return new(dbFields);
206286
}

src/RepoDb.SqlServer/Resolvers/DbTypeToSqlServerStringNameResolver.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,6 @@ DbType.VarNumeric or
5252
_ => "NVARCHAR",
5353
};
5454
}
55+
56+
public static readonly DbTypeToSqlServerStringNameResolver Instance = new();
5557
}

src/RepoDb.SqlServer/Resolvers/SqlServerConvertFieldResolver.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class SqlServerConvertFieldResolver : DbConvertFieldResolver
1414
/// </summary>
1515
public SqlServerConvertFieldResolver()
1616
: this(ClientTypeToDbTypeResolver.Instance,
17-
new DbTypeToSqlServerStringNameResolver())
17+
DbTypeToSqlServerStringNameResolver.Instance)
1818
{ }
1919

2020
/// <summary>
@@ -50,4 +50,7 @@ public SqlServerConvertFieldResolver(IResolver<Type, DbType?> dbTypeResolver,
5050
}
5151

5252
#endregion
53+
54+
55+
public static readonly SqlServerConvertFieldResolver Instance = new();
5356
}

src/RepoDb.SqlServer/Resolvers/SqlServerDbTypeNameToClientTypeResolver.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using RepoDb.Interfaces;
1+
using Microsoft.Data.SqlTypes;
2+
using RepoDb.Interfaces;
23
using RepoDb.Types;
34

45
namespace RepoDb.Resolvers;
@@ -48,7 +49,10 @@ public virtual Type Resolve(string dbTypeName)
4849
typeof(TimeSpan),
4950
"tinyint" => typeof(byte),
5051
"uniqueidentifier" => typeof(Guid),
52+
"vector" => typeof(SqlVector<float>),
5153
_ => typeof(object),
5254
};
5355
}
56+
57+
public static readonly SqlServerDbTypeNameToClientTypeResolver Instance = new();
5458
}

0 commit comments

Comments
 (0)