diff --git a/src/EFCore.MySql/Infrastructure/MariaDbServerVersion.cs b/src/EFCore.MySql/Infrastructure/MariaDbServerVersion.cs index c5f55d110..ed3c06e78 100644 --- a/src/EFCore.MySql/Infrastructure/MariaDbServerVersion.cs +++ b/src/EFCore.MySql/Infrastructure/MariaDbServerVersion.cs @@ -91,6 +91,8 @@ internal MariaDbServerVersionSupport([NotNull] ServerVersion serverVersion) public override bool CommonTableExpressions => ServerVersion.Version >= new Version(10, 2, 1); public override bool LimitWithinInAllAnySomeSubquery => false; public override bool LimitWithNonConstantValue => false; + public override bool Vector => ServerVersion.Version >= new Version(11, 7, 0); + public override bool VectorIndex => ServerVersion.Version >= new Version(11, 7, 0); public override bool JsonTable => ServerVersion.Version >= new Version(10, 6, 0); // Since there seems to be no implicit LATERAL support for JSON_TABLE, this is pretty useless except for cases where the JSON is provided by a parameter instead of a column of an outer table. public override bool JsonValue => true; public override bool JsonOverlaps => ServerVersion.Version >= new Version(10, 9, 0); diff --git a/src/EFCore.MySql/Infrastructure/MySqlServerVersion.cs b/src/EFCore.MySql/Infrastructure/MySqlServerVersion.cs index d6d042bc7..479251d35 100644 --- a/src/EFCore.MySql/Infrastructure/MySqlServerVersion.cs +++ b/src/EFCore.MySql/Infrastructure/MySqlServerVersion.cs @@ -97,6 +97,7 @@ internal MySqlServerVersionSupport([NotNull] ServerVersion serverVersion) public override bool JsonOverlaps => ServerVersion.Version >= new Version(8, 0, 0); public override bool Values => false; public override bool ValuesWithRows => ServerVersion.Version >= new Version(8, 0, 19); + public override bool Vector => ServerVersion.Version >= new Version(9, 0, 0); public override bool WhereSubqueryReferencesOuterQuery => false; public override bool FieldReferenceInTableValueConstructor => true; public override bool CollationCharacterSetApplicabilityWithFullCollationNameColumn => false; diff --git a/src/EFCore.MySql/Infrastructure/ServerVersionSupport.cs b/src/EFCore.MySql/Infrastructure/ServerVersionSupport.cs index dc0b72a8c..70ba756b2 100644 --- a/src/EFCore.MySql/Infrastructure/ServerVersionSupport.cs +++ b/src/EFCore.MySql/Infrastructure/ServerVersionSupport.cs @@ -94,6 +94,8 @@ public virtual bool PropertyOrVersion(string propertyNameOrServerVersion) public virtual bool JsonValue => false; public virtual bool Values => false; public virtual bool ValuesWithRows => false; + public virtual bool Vector => false; + public virtual bool VectorIndex => false; public virtual bool WhereSubqueryReferencesOuterQuery => false; public virtual bool FieldReferenceInTableValueConstructor => false; public virtual bool CollationCharacterSetApplicabilityWithFullCollationNameColumn => false; diff --git a/src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlQuerySqlGenerator.cs b/src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlQuerySqlGenerator.cs index c86ad0e9e..c1ba0a463 100644 --- a/src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlQuerySqlGenerator.cs +++ b/src/EFCore.MySql/Query/ExpressionVisitors/Internal/MySqlQuerySqlGenerator.cs @@ -58,6 +58,7 @@ public class MySqlQuerySqlGenerator : QuerySqlGenerator { "json", new []{ "json" } }, { "char", new []{ "char", "varchar", "text", "tinytext", "mediumtext", "longtext" } }, { "nchar", new []{ "nchar", "nvarchar" } }, + { "vector", new []{ "vector" } }, }; private const ulong LimitUpperBound = 18446744073709551610; diff --git a/src/EFCore.MySql/Storage/Internal/MySqlTypeMapping.cs b/src/EFCore.MySql/Storage/Internal/MySqlTypeMapping.cs index a0be07217..9f6c464a9 100644 --- a/src/EFCore.MySql/Storage/Internal/MySqlTypeMapping.cs +++ b/src/EFCore.MySql/Storage/Internal/MySqlTypeMapping.cs @@ -33,6 +33,7 @@ public MySqlTypeMapping( DbType? dbType = null, bool unicode = false, int? size = null, + StoreTypePostfix storeTypePostfix = StoreTypePostfix.None, ValueConverter valueConverter = null, ValueComparer valueComparer = null, JsonValueReaderWriter jsonValueReaderWriter = null) @@ -44,7 +45,7 @@ public MySqlTypeMapping( valueComparer, jsonValueReaderWriter: jsonValueReaderWriter), storeType, - StoreTypePostfix.None, + storeTypePostfix, dbType, unicode, size)) diff --git a/src/EFCore.MySql/Storage/Internal/MySqlTypeMappingSource.cs b/src/EFCore.MySql/Storage/Internal/MySqlTypeMappingSource.cs index b9ca9914e..2440161cf 100644 --- a/src/EFCore.MySql/Storage/Internal/MySqlTypeMappingSource.cs +++ b/src/EFCore.MySql/Storage/Internal/MySqlTypeMappingSource.cs @@ -69,6 +69,15 @@ public class MySqlTypeMappingSource : RelationalTypeMappingSource private readonly MySqlDateTimeOffsetTypeMapping _dateTimeOffset = MySqlDateTimeOffsetTypeMapping.Default; private readonly MySqlDateTimeOffsetTypeMapping _timeStampOffset = new MySqlDateTimeOffsetTypeMapping("timestamp"); + // vector + private readonly MySqlVectorTypeMapping _floatVector = new MySqlVectorTypeMapping("vector", typeof(float[])); + private readonly MySqlVectorTypeMapping _memoryVector = new MySqlVectorTypeMapping("vector", typeof(Memory)); + private readonly MySqlVectorTypeMapping _readOnlyMemoryVector = new MySqlVectorTypeMapping("vector", typeof(ReadOnlyMemory)); + private readonly MySqlVectorByteTypeMapping _floatByteVector = new MySqlVectorByteTypeMapping("vector", typeof(float[])); + private readonly MySqlVectorByteTypeMapping _memoryByteVector = new MySqlVectorByteTypeMapping("vector", typeof(Memory)); + private readonly MySqlVectorByteTypeMapping _readOnlyByteMemoryVector = new MySqlVectorByteTypeMapping("vector", typeof(ReadOnlyMemory)); + + private readonly RelationalTypeMapping _binaryRowVersion = new MySqlDateTimeTypeMapping( "timestamp", @@ -251,6 +260,31 @@ private void Initialize() { typeof(MySqlJsonString), _jsonDefaultString } }; + // vector + if (_options.ServerVersion.Supports.Vector) + { + _storeTypeMappings[_floatVector.StoreType] = _options.ServerVersion.Type switch + { + ServerType.MariaDb => new RelationalTypeMapping[] { _floatByteVector, _memoryByteVector, _readOnlyByteMemoryVector }, + _ => new RelationalTypeMapping[] { _floatVector, _readOnlyMemoryVector, _memoryVector, } + }; + _clrTypeMappings[typeof(float[])] = _options.ServerVersion.Type switch + { + ServerType.MariaDb => _floatByteVector, + _ => _floatVector, + }; + _clrTypeMappings[typeof(ReadOnlyMemory)] = _options.ServerVersion.Type switch + { + ServerType.MariaDb => _readOnlyByteMemoryVector, + _ => _readOnlyMemoryVector, + }; + _clrTypeMappings[typeof(Memory)] = _options.ServerVersion.Type switch + { + ServerType.MariaDb => _memoryByteVector, + _ => _memoryVector, + }; + } + // Boolean if (_options.DefaultDataTypeMappings.ClrBoolean != MySqlBooleanType.None) { @@ -263,7 +297,7 @@ private void Initialize() // Guid if (_guid != null) { - _storeTypeMappings[_guid.StoreType] = new RelationalTypeMapping[]{ _guid }; + _storeTypeMappings[_guid.StoreType] = new RelationalTypeMapping[] { _guid }; _clrTypeMappings[typeof(Guid)] = _guid; } @@ -284,6 +318,25 @@ protected override RelationalTypeMapping FindMapping(in RelationalTypeMappingInf base.FindMapping(mappingInfo) ?? FindRawMapping(mappingInfo)?.Clone(mappingInfo); + protected override RelationalTypeMapping FindCollectionMapping( + RelationalTypeMappingInfo mappingInfo, + Type modelType, + Type providerType, + CoreTypeMapping elementMapping) + { + // Workaround: prevent EF from trying to treat float[] → byte[] mappings as collections + if (modelType == typeof(byte[]) && (mappingInfo.StoreTypeName?.Equals("vector", StringComparison.OrdinalIgnoreCase) ?? false)) + { + if (_storeTypeMappings.TryGetValue(mappingInfo.StoreTypeName, out var mappings)) + { + return mappings.First(x => x.ClrType == typeof(ReadOnlyMemory) || x.ClrType == typeof(Memory) || x.ClrType == typeof(float[])); + } + } + + // Fallback to base behavior for actual collections + return base.FindCollectionMapping(mappingInfo, modelType, providerType, elementMapping); + } + private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingInfo) { // Use deferred initialization to support connection (string) based type mapping in diff --git a/src/EFCore.MySql/Storage/Internal/MySqlVectorByteTypeMapping.cs b/src/EFCore.MySql/Storage/Internal/MySqlVectorByteTypeMapping.cs new file mode 100644 index 000000000..bc23b45d3 --- /dev/null +++ b/src/EFCore.MySql/Storage/Internal/MySqlVectorByteTypeMapping.cs @@ -0,0 +1,112 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MySqlConnector; + +namespace Pomelo.EntityFrameworkCore.MySql.Storage.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class MySqlVectorByteTypeMapping : MySqlTypeMapping + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public MySqlVectorByteTypeMapping( + [NotNull] string storeType, + [NotNull] Type clrType, + int? size = null) + : base( + storeType, + clrType, + MySqlDbType.Vector, + size: size, + unicode: false, + storeTypePostfix: StoreTypePostfix.Size, + valueConverter: GetValueConverter(clrType), + valueComparer: GetValueComparer(clrType)) + { + } + + protected MySqlVectorByteTypeMapping(RelationalTypeMappingParameters parameters, MySqlDbType mySqlDbType) + : base(parameters, mySqlDbType) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new MySqlVectorByteTypeMapping(parameters, MySqlDbType); + private static ValueConverter GetValueConverter(Type clrType) + { + return clrType switch + { + Type t when t == typeof(float[]) => + new ValueConverter( + v => FloatArrayToBytes(v), + v => BytesToFloatArray(v)), + + Type t when t == typeof(ReadOnlyMemory) => + new ValueConverter, byte[]>( + v => FloatArrayToBytes(v.ToArray()), + v => new ReadOnlyMemory(BytesToFloatArray(v))), + + Type t when t == typeof(Memory) => + new ValueConverter, byte[]>( + v => FloatArrayToBytes(v.ToArray()), + v => new Memory(BytesToFloatArray(v))), + + _ => throw new InvalidOperationException($"Unsupported CLR type for vector: {clrType}"), + }; + } + + private static ValueComparer GetValueComparer(Type clrType) + { + return clrType switch + { + Type t when t == typeof(float[]) => + new ValueComparer( + (a, b) => a.SequenceEqual(b), + v => v.Aggregate(0, (hash, x) => HashCode.Combine(hash, x.GetHashCode())), + v => v.ToArray()), + + Type t when t == typeof(ReadOnlyMemory) => + new ValueComparer>( + (a, b) => a.ToArray().SequenceEqual(b.ToArray()), + v => v.ToArray().Aggregate(0, (hash, x) => HashCode.Combine(hash, x.GetHashCode())), + v => new ReadOnlyMemory(v.ToArray())), + + Type t when t == typeof(Memory) => + new ValueComparer>( + (a, b) => a.ToArray().SequenceEqual(b.ToArray()), + v => v.ToArray().Aggregate(0, (hash, x) => HashCode.Combine(hash, x.GetHashCode())), + v => new Memory(v.ToArray())), + + _ => throw new InvalidOperationException($"Unsupported CLR type for vector: {clrType}"), + }; + } + + private static byte[] FloatArrayToBytes(float[] values) + { + var result = new byte[values.Length * sizeof(float)]; + Buffer.BlockCopy(values, 0, result, 0, result.Length); + return result; + } + + private static float[] BytesToFloatArray(byte[] bytes) + { + var result = new float[bytes.Length / sizeof(float)]; + Buffer.BlockCopy(bytes, 0, result, 0, bytes.Length); + return result; + } + } +} diff --git a/src/EFCore.MySql/Storage/Internal/MySqlVectorTypeMapping.cs b/src/EFCore.MySql/Storage/Internal/MySqlVectorTypeMapping.cs new file mode 100644 index 000000000..9d3b767f7 --- /dev/null +++ b/src/EFCore.MySql/Storage/Internal/MySqlVectorTypeMapping.cs @@ -0,0 +1,58 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MySqlConnector; + +namespace Pomelo.EntityFrameworkCore.MySql.Storage.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class MySqlVectorTypeMapping : MySqlTypeMapping + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public MySqlVectorTypeMapping( + [NotNull] string storeType, + [NotNull] Type clrType, + int? size = null) + : base( + storeType, + clrType, + MySqlDbType.Vector, + size: size, + unicode: false, + storeTypePostfix: StoreTypePostfix.Size, + valueConverter: GetValueConverter(clrType)) + { + } + + protected MySqlVectorTypeMapping(RelationalTypeMappingParameters parameters, MySqlDbType mySqlDbType) + : base(parameters, mySqlDbType) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new MySqlVectorTypeMapping(parameters, MySqlDbType); + + private static ValueConverter GetValueConverter(Type clrType) + { + return clrType switch + { + Type t when t == typeof(float[]) => new ValueConverter>(v => v.AsMemory(), v => v.ToArray()), + Type t when t == typeof(ReadOnlyMemory) => null, + Type t when t == typeof(Memory) => new ValueConverter, ReadOnlyMemory>(v => v, v => v.ToArray()), + _ => throw new InvalidOperationException($"Unsupported CLR type for vector: {clrType}"), + }; + } + } +} diff --git a/test/EFCore.MySql.IntegrationTests/Models/DataTypes.cs b/test/EFCore.MySql.IntegrationTests/Models/DataTypes.cs index 81f3cb1d6..b3324b58e 100644 --- a/test/EFCore.MySql.IntegrationTests/Models/DataTypes.cs +++ b/test/EFCore.MySql.IntegrationTests/Models/DataTypes.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; namespace Pomelo.EntityFrameworkCore.MySql.IntegrationTests.Models { @@ -101,9 +103,20 @@ public class DataTypesVariable public byte[] TypeByteArrayN { get; set; } + [MaxLength(384)] + [Column(TypeName = "vector")] + public float[] TypeVectorFloatArray { get; set; } - // json not null - [Required] + [MaxLength(384)] + [Column(TypeName = "vector")] + public ReadOnlyMemory TypeVectorReadonlyMemory { get; set; } + + [MaxLength(384)] + [Column(TypeName = "vector")] + public Memory TypeVectorMemory { get; set; } + + // json not null + [Required] public List TypeJsonArray { get; set; } [Required] @@ -117,6 +130,10 @@ public class DataTypesVariable // static method to create a new empty object public static readonly byte[] EmptyByteArray = Array.Empty(); + + // MariaDb requires a fully filled vector to store in the database. Using [0.0, 0.0, ..., 0.0] + // as an empty vector + public static readonly float[] EmptyFloatArray = Enumerable.Repeat(0.0f, 384).ToArray(); public static readonly List EmptyJsonArray = new List(); public static readonly Dictionary EmptyJsonObject = new Dictionary(); @@ -128,6 +145,9 @@ public static DataTypesVariable CreateEmpty() TypeString255 = "", TypeByteArray = EmptyByteArray, TypeByteArray255 = EmptyByteArray, + TypeVectorFloatArray = EmptyFloatArray, + TypeVectorMemory = EmptyFloatArray, + TypeVectorReadonlyMemory = EmptyFloatArray, TypeJsonArray = EmptyJsonArray, TypeJsonObject = EmptyJsonObject, }; diff --git a/test/EFCore.MySql.IntegrationTests/Tests/Models/DataTypesTest.cs b/test/EFCore.MySql.IntegrationTests/Tests/Models/DataTypesTest.cs index 15acfd2a6..78d917576 100644 --- a/test/EFCore.MySql.IntegrationTests/Tests/Models/DataTypesTest.cs +++ b/test/EFCore.MySql.IntegrationTests/Tests/Models/DataTypesTest.cs @@ -77,20 +77,20 @@ void testEmpty(DataTypesSimple emptyDb) Assert.Null(emptyDb.TypeGuidN); } - const sbyte testSbyte = (sbyte) -128; - const byte testByte = (byte) 255; - const char testChar = 'a'; - const float testFloat = (float) 1.23456789e38; - - var dateTime = new DateTime(2016, 10, 11, 1, 2, 3, 456); - var dateTimeOffset = dateTime + TimeSpan.FromMilliseconds(123.456); - var timeSpan = new TimeSpan(1, 2, 3, 4, 5); - const TestEnum testEnum = TestEnum.TestOne; - const TestEnumByte testEnumByte = TestEnumByte.TestOne; - var guid = Guid.NewGuid(); - - // test each data type with a valid value - // ReSharper disable once ObjectCreationAsStatement + const sbyte testSbyte = (sbyte)-128; + const byte testByte = (byte)255; + const char testChar = 'a'; + const float testFloat = (float)1.23456789e38; + + var dateTime = new DateTime(2016, 10, 11, 1, 2, 3, 456); + var dateTimeOffset = dateTime + TimeSpan.FromMilliseconds(123.456); + var timeSpan = new TimeSpan(1, 2, 3, 4, 5); + const TestEnum testEnum = TestEnum.TestOne; + const TestEnumByte testEnumByte = TestEnumByte.TestOne; + var guid = Guid.NewGuid(); + + // test each data type with a valid value + // ReSharper disable once ObjectCreationAsStatement DataTypesSimple newValueMem() => new DataTypesSimple { // bool @@ -216,69 +216,74 @@ void testValue(DataTypesSimple valueDb) } // create test data objects - var emptyMemAsync = new DataTypesSimple(); - var emptyMemSync = new DataTypesSimple(); - var valueMemAsync = newValueMem(); - var valueMemSync = newValueMem(); - - // save them to the database - using (var scope = new AppDbScope()) - { - var db = scope.AppDb; - db.DataTypesSimple.Add(emptyMemAsync); - db.DataTypesSimple.Add(valueMemAsync); - await db.SaveChangesAsync(); - - db.DataTypesSimple.Add(emptyMemSync); - db.DataTypesSimple.Add(valueMemSync); - db.SaveChanges(); - } - - // load them from the database and run tests - using (var scope = new AppDbScope()) - { - var db = scope.AppDb; - // ReSharper disable once AccessToDisposedClosure - async Task fromDbAsync(DataTypesSimple dt) => await db.DataTypesSimple.FirstOrDefaultAsync(m => m.Id == dt.Id); - - // ReSharper disable once AccessToDisposedClosure - DataTypesSimple fromDbSync(DataTypesSimple dt) => db.DataTypesSimple.FirstOrDefault(m => m.Id == dt.Id); - - testEmpty(await fromDbAsync(emptyMemAsync)); - testEmpty(fromDbSync(emptyMemSync)); - testValue(await fromDbAsync(valueMemAsync)); - testValue(fromDbSync(valueMemSync)); - } + var emptyMemAsync = new DataTypesSimple(); + var emptyMemSync = new DataTypesSimple(); + var valueMemAsync = newValueMem(); + var valueMemSync = newValueMem(); + + // save them to the database + using (var scope = new AppDbScope()) + { + var db = scope.AppDb; + db.DataTypesSimple.Add(emptyMemAsync); + db.DataTypesSimple.Add(valueMemAsync); + await db.SaveChangesAsync(); + + db.DataTypesSimple.Add(emptyMemSync); + db.DataTypesSimple.Add(valueMemSync); + db.SaveChanges(); + } + + // load them from the database and run tests + using (var scope = new AppDbScope()) + { + var db = scope.AppDb; + // ReSharper disable once AccessToDisposedClosure + async Task fromDbAsync(DataTypesSimple dt) => await db.DataTypesSimple.FirstOrDefaultAsync(m => m.Id == dt.Id); + + // ReSharper disable once AccessToDisposedClosure + DataTypesSimple fromDbSync(DataTypesSimple dt) => db.DataTypesSimple.FirstOrDefault(m => m.Id == dt.Id); + + testEmpty(await fromDbAsync(emptyMemAsync)); + testEmpty(fromDbSync(emptyMemSync)); + testValue(await fromDbAsync(valueMemAsync)); + testValue(fromDbSync(valueMemSync)); + } } - [Fact] - public async Task TestDataTypesVariable() - { - void testEmpty(DataTypesVariable valueDb) - { - // string not null - Assert.Equal("", valueDb.TypeString); - Assert.Equal("", valueDb.TypeString255); - // string null - Assert.Null(valueDb.TypeStringN); - Assert.Null(valueDb.TypeString255N); - - // binary not null - Assert.Equal(DataTypesVariable.EmptyByteArray, valueDb.TypeByteArray); - Assert.Equal(DataTypesVariable.EmptyByteArray, valueDb.TypeByteArray255); - // binary null - Assert.Null(valueDb.TypeByteArrayN); - Assert.Null(valueDb.TypeByteArray255N); - - // json not null - Assert.Equal(DataTypesVariable.EmptyJsonArray, valueDb.TypeJsonArray); - Assert.Equal(DataTypesVariable.EmptyJsonObject, valueDb.TypeJsonObject); - // json null - Assert.Null(valueDb.TypeJsonArrayN); - Assert.Null(valueDb.TypeJsonObjectN); - } - - var string255 = new string('a', 255); + [Fact] + public async Task TestDataTypesVariable() + { + var emptyVector = Enumerable.Repeat(0.0f, 384).ToArray(); + void testEmpty(DataTypesVariable valueDb) + { + // string not null + Assert.Equal("", valueDb.TypeString); + Assert.Equal("", valueDb.TypeString255); + // string null + Assert.Null(valueDb.TypeStringN); + Assert.Null(valueDb.TypeString255N); + + // binary not null + Assert.Equal(DataTypesVariable.EmptyByteArray, valueDb.TypeByteArray); + Assert.Equal(DataTypesVariable.EmptyByteArray, valueDb.TypeByteArray255); + // binary null + Assert.Null(valueDb.TypeByteArrayN); + Assert.Null(valueDb.TypeByteArray255N); + + Assert.Equal(emptyVector, valueDb.TypeVectorFloatArray); + Assert.Equal(emptyVector, valueDb.TypeVectorMemory); + Assert.Equal(emptyVector, valueDb.TypeVectorReadonlyMemory); + + // json not null + Assert.Equal(DataTypesVariable.EmptyJsonArray, valueDb.TypeJsonArray); + Assert.Equal(DataTypesVariable.EmptyJsonObject, valueDb.TypeJsonObject); + // json null + Assert.Null(valueDb.TypeJsonArrayN); + Assert.Null(valueDb.TypeJsonObjectN); + } + + var string255 = new string('a', 255); var string10K = new string('a', 10000); var byte255 = new byte[255]; @@ -287,100 +292,110 @@ void testEmpty(DataTypesVariable valueDb) { if (i < 255) { - byte255[i] = (byte) 'a'; + byte255[i] = (byte)'a'; } - byte10K[i] = (byte) 'a'; + byte10K[i] = (byte)'a'; } - var jsonArray = new List {"test"}; - var jsonObject = new Dictionary {{"test", "test"}}; + var jsonArray = new List { "test" }; + var jsonObject = new Dictionary { { "test", "test" } }; + var vector = Enumerable.Range(0, 384).Select(x => x / 3.0f).ToArray(); // test each data type with a valid value - DataTypesVariable newValueMem() => new DataTypesVariable - { - // string not null - TypeString = string10K, - TypeString255 = string255, // should be truncated by DBMS - // string null - TypeStringN = string10K, - TypeString255N = string255, // should be truncated by DBMS - - // binary not null - TypeByteArray = byte10K, - TypeByteArray255 = byte255, // should be truncated by DBMS - // binary null - TypeByteArrayN = byte10K, - TypeByteArray255N = byte255, // should be truncated by DBMS - - // json not null - TypeJsonArray = jsonArray, - TypeJsonObject = jsonObject, - // json null - TypeJsonArrayN = jsonArray, - TypeJsonObjectN = jsonObject, - }; - - void testValue(DataTypesVariable valueDb) - { - // string not null - Assert.Equal(string10K, valueDb.TypeString); - Assert.Equal(string255, valueDb.TypeString255); - // string null - Assert.Equal(string10K, valueDb.TypeStringN); - Assert.Equal(string255, valueDb.TypeString255N); - - // binary not null - Assert.Equal(byte10K, valueDb.TypeByteArray); - Assert.Equal(byte255, valueDb.TypeByteArray255); - // binary null - Assert.Equal(byte10K, valueDb.TypeByteArrayN); - Assert.Equal(byte255, valueDb.TypeByteArray255N); - - // json not null - Assert.Equal(jsonArray, valueDb.TypeJsonArray); - Assert.Equal(jsonObject, valueDb.TypeJsonObject); - // json null - Assert.Equal(jsonArray, valueDb.TypeJsonArrayN); - Assert.Equal(jsonObject, valueDb.TypeJsonObjectN); - } - - // create test data objects - var emptyMemAsync = DataTypesVariable.CreateEmpty(); - var emptyMemSync = DataTypesVariable.CreateEmpty(); - var valueMemAsync = newValueMem(); - var valueMemSync = newValueMem(); - - // save them to the database - using (var scope = new AppDbScope()) - { - var db = scope.AppDb; - db.DataTypesVariable.Add(emptyMemAsync); - db.DataTypesVariable.Add(valueMemAsync); - await db.SaveChangesAsync(); - - db.DataTypesVariable.Add(emptyMemSync); - db.DataTypesVariable.Add(valueMemSync); - db.SaveChanges(); - } - - // load them from the database and run tests - using (var scope = new AppDbScope()) - { - var db = scope.AppDb; - // ReSharper disable once AccessToDisposedClosure - async Task fromDbAsync(DataTypesVariable dt) => await db.DataTypesVariable.FirstOrDefaultAsync(m => m.Id == dt.Id); - - // ReSharper disable once AccessToDisposedClosure - DataTypesVariable fromDbSync(DataTypesVariable dt) => db.DataTypesVariable.FirstOrDefault(m => m.Id == dt.Id); - - testEmpty(await fromDbAsync(emptyMemAsync)); - testEmpty(fromDbSync(emptyMemSync)); - testValue(await fromDbAsync(valueMemAsync)); - testValue(fromDbSync(valueMemSync)); - } - - } + DataTypesVariable newValueMem() => new DataTypesVariable + { + // string not null + TypeString = string10K, + TypeString255 = string255, // should be truncated by DBMS + // string null + TypeStringN = string10K, + TypeString255N = string255, // should be truncated by DBMS + + // binary not null + TypeByteArray = byte10K, + TypeByteArray255 = byte255, // should be truncated by DBMS + // binary null + TypeByteArrayN = byte10K, + TypeByteArray255N = byte255, // should be truncated by DBMS + + // vector + TypeVectorFloatArray = vector, + TypeVectorMemory = vector, + TypeVectorReadonlyMemory = vector, + + // json not null + TypeJsonArray = jsonArray, + TypeJsonObject = jsonObject, + // json null + TypeJsonArrayN = jsonArray, + TypeJsonObjectN = jsonObject, + }; + + void testValue(DataTypesVariable valueDb) + { + // string not null + Assert.Equal(string10K, valueDb.TypeString); + Assert.Equal(string255, valueDb.TypeString255); + // string null + Assert.Equal(string10K, valueDb.TypeStringN); + Assert.Equal(string255, valueDb.TypeString255N); + + // binary not null + Assert.Equal(byte10K, valueDb.TypeByteArray); + Assert.Equal(byte255, valueDb.TypeByteArray255); + // binary null + Assert.Equal(byte10K, valueDb.TypeByteArrayN); + Assert.Equal(byte255, valueDb.TypeByteArray255N); + + Assert.Equal(vector, valueDb.TypeVectorFloatArray); + Assert.Equal(vector, valueDb.TypeVectorMemory); + Assert.Equal(vector, valueDb.TypeVectorReadonlyMemory); + + // json not null + Assert.Equal(jsonArray, valueDb.TypeJsonArray); + Assert.Equal(jsonObject, valueDb.TypeJsonObject); + // json null + Assert.Equal(jsonArray, valueDb.TypeJsonArrayN); + Assert.Equal(jsonObject, valueDb.TypeJsonObjectN); + } + + // create test data objects + var emptyMemAsync = DataTypesVariable.CreateEmpty(); + var emptyMemSync = DataTypesVariable.CreateEmpty(); + var valueMemAsync = newValueMem(); + var valueMemSync = newValueMem(); + + // save them to the database + using (var scope = new AppDbScope()) + { + var db = scope.AppDb; + db.DataTypesVariable.Add(emptyMemAsync); + db.DataTypesVariable.Add(valueMemAsync); + await db.SaveChangesAsync(); + + db.DataTypesVariable.Add(emptyMemSync); + db.DataTypesVariable.Add(valueMemSync); + db.SaveChanges(); + } + + // load them from the database and run tests + using (var scope = new AppDbScope()) + { + var db = scope.AppDb; + // ReSharper disable once AccessToDisposedClosure + async Task fromDbAsync(DataTypesVariable dt) => await db.DataTypesVariable.FirstOrDefaultAsync(m => m.Id == dt.Id); + + // ReSharper disable once AccessToDisposedClosure + DataTypesVariable fromDbSync(DataTypesVariable dt) => db.DataTypesVariable.FirstOrDefault(m => m.Id == dt.Id); + + testEmpty(await fromDbAsync(emptyMemAsync)); + testEmpty(fromDbSync(emptyMemSync)); + testValue(await fromDbAsync(valueMemAsync)); + testValue(fromDbSync(valueMemSync)); + } + + } }