diff --git a/.ci/config/config.compression+ssl.json b/.ci/config/config.compression+ssl.json index f7d4263c8..e0bf3504d 100644 --- a/.ci/config/config.compression+ssl.json +++ b/.ci/config/config.compression+ssl.json @@ -4,7 +4,7 @@ "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock", "PasswordlessUser": "no_password", "SecondaryDatabase": "testdb2", - "UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,TlsFingerprintValidation,UuidToBin", + "UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,TlsFingerprintValidation,UuidToBin,Vector", "MySqlBulkLoaderLocalCsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.CSV", "MySqlBulkLoaderLocalTsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.TSV", "CertificatesPath": "../../../../.ci/server/certs" diff --git a/.ci/config/config.compression.json b/.ci/config/config.compression.json index 09326f1f6..d6206ed1d 100644 --- a/.ci/config/config.compression.json +++ b/.ci/config/config.compression.json @@ -4,7 +4,7 @@ "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock", "PasswordlessUser": "no_password", "SecondaryDatabase": "testdb2", - "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime", + "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,Vector,ZeroDateTime", "MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV", "MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV" } diff --git a/.ci/config/config.json b/.ci/config/config.json index bc38f605a..b539060a9 100644 --- a/.ci/config/config.json +++ b/.ci/config/config.json @@ -4,7 +4,7 @@ "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock", "PasswordlessUser": "no_password", "SecondaryDatabase": "testdb2", - "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime", + "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,Vector,ZeroDateTime", "MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV", "MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV" } diff --git a/.ci/config/config.ssl.json b/.ci/config/config.ssl.json index a7511faa4..e136a243d 100644 --- a/.ci/config/config.ssl.json +++ b/.ci/config/config.ssl.json @@ -4,7 +4,7 @@ "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock", "PasswordlessUser": "no_password", "SecondaryDatabase": "testdb2", - "UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,TlsFingerprintValidation,UuidToBin", + "UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,TlsFingerprintValidation,UuidToBin,Vector", "MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV", "MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV", "CertificatesPath": "../../../../.ci/server/certs" diff --git a/.ci/docker-run.sh b/.ci/docker-run.sh index 7a791e21d..4f2999c2d 100755 --- a/.ci/docker-run.sh +++ b/.ci/docker-run.sh @@ -28,7 +28,7 @@ MYSQL=mysql if [[ "$IMAGE" == mariadb* ]]; then MYSQL_EXTRA='--in-predicate-conversion-threshold=100000 --plugin-maturity=beta' fi -if [ "$IMAGE" == "mariadb:11.4" ] || [ "$IMAGE" == "mariadb:11.6" ]; then +if [ "$IMAGE" == "mariadb:11.4" ] || [ "$IMAGE" == "mariadb:11.7" ]; then MYSQL='mariadb' fi diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fa4d3d285..cc793ad80 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -51,7 +51,7 @@ jobs: arguments: 'tests\IntegrationTests\IntegrationTests.csproj -c MySqlData' testRunTitle: 'MySql.Data integration tests' env: - DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,TlsFingerprintValidation,UnixDomainSocket' + DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,TlsFingerprintValidation,UnixDomainSocket,Vector' DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=root;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600' DATA__CERTIFICATESPATH: '$(Build.Repository.LocalPath)\.ci\server\certs\' DATA__MYSQLBULKLOADERLOCALCSVFILE: '$(Build.Repository.LocalPath)\tests\TestData\LoadData_UTF8_BOM_Unix.CSV' @@ -120,7 +120,7 @@ jobs: arguments: '-c Release --no-restore -p:TestTfmsInParallel=false' testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net481/net9.0', 'No SSL') }} env: - DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket' + DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,Vector' DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True;UseCompression=True' - job: windows_integration_tests_2 @@ -158,7 +158,7 @@ jobs: arguments: '-c Release --no-restore -p:TestTfmsInParallel=false' testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net8.0', 'No SSL') }} env: - DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket' + DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,Vector' DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True' - job: linux_integration_tests @@ -171,11 +171,11 @@ jobs: 'MySQL 8.0': image: 'mysql:8.0' connectionStringExtra: 'AllowPublicKeyRetrieval=True' - unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime' + unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,Vector,ZeroDateTime' 'MySQL 8.4': image: 'mysql:8.4' connectionStringExtra: 'AllowPublicKeyRetrieval=True' - unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime' + unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,Vector,ZeroDateTime' 'MySQL 9.2': image: 'mysql:9.2' connectionStringExtra: 'AllowPublicKeyRetrieval=True' @@ -183,19 +183,19 @@ jobs: 'MariaDB 10.6': image: 'mariadb:10.6' connectionStringExtra: '' - unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin' + unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin,Vector' 'MariaDB 10.11': image: 'mariadb:10.11' connectionStringExtra: '' - unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin' + unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin,Vector' 'MariaDB 11.4': image: 'mariadb:11.4' connectionStringExtra: '' - unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection' - 'MariaDB 11.6': - image: 'mariadb:11.6' + unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,UuidToBin,Vector' + 'MariaDB 11.7': + image: 'mariadb:11.7' connectionStringExtra: '' - unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,UuidToBin' + unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,UuidToBin,VectorType' steps: - template: '.ci/integration-tests-steps.yml' parameters: diff --git a/docs/content/home.md b/docs/content/home.md index 0db29f09c..a687d7c49 100644 --- a/docs/content/home.md +++ b/docs/content/home.md @@ -64,7 +64,7 @@ Server | Versions | Notes Amazon Aurora RDS | 2.x, 3.x | Use `Pipelining=False` [for Aurora 2.x](https://mysqlconnector.net/troubleshooting/aurora-freeze/) Azure Database for MySQL | 5.7, 8.0 | Single Server and Flexible Server Google Cloud SQL for MySQL | 5.6, 5.7, 8.0 | -MariaDB | 10.x (**10.6**, **10.11**), 11.x (**11.4**, **11.6**) | +MariaDB | 10.x (**10.6**, **10.11**), 11.x (**11.4**, **11.7**) | MySQL | 5.5, 5.6, 5.7, 8.x (**8.0**, **8.4**), 9.x (**9.2**) | 5.5 is EOL and has some [compatibility issues](https://github.com/mysql-net/MySqlConnector/issues/1192); 5.6 and 5.7 are EOL Percona Server | 5.6, 5.7, 8.0 | PlanetScale | | See PlanetScale [MySQL compatibility notes](https://planetscale.com/docs/reference/mysql-compatibility) diff --git a/src/MySqlConnector/ColumnReaders/ColumnReader.cs b/src/MySqlConnector/ColumnReaders/ColumnReader.cs index 86ba9145f..24baba810 100644 --- a/src/MySqlConnector/ColumnReaders/ColumnReader.cs +++ b/src/MySqlConnector/ColumnReaders/ColumnReader.cs @@ -113,6 +113,9 @@ public static ColumnReader Create(bool isBinary, ColumnDefinitionPayload columnD case ColumnType.Null: return NullColumnReader.Instance; + case ColumnType.Vector: + return VectorColumnReader.Instance; + default: throw new NotImplementedException($"Reading {columnDefinition.ColumnType} not implemented"); } diff --git a/src/MySqlConnector/ColumnReaders/VectorColumnReader.cs b/src/MySqlConnector/ColumnReaders/VectorColumnReader.cs new file mode 100644 index 000000000..637b0f491 --- /dev/null +++ b/src/MySqlConnector/ColumnReaders/VectorColumnReader.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; +using MySqlConnector.Protocol.Payloads; + +namespace MySqlConnector.ColumnReaders; + +internal sealed class VectorColumnReader : ColumnReader +{ + public static VectorColumnReader Instance { get; } = new(); + + public override object ReadValue(ReadOnlySpan data, ColumnDefinitionPayload columnDefinition) => + MemoryMarshal.Cast(data).ToArray(); +} diff --git a/src/MySqlConnector/Core/Row.cs b/src/MySqlConnector/Core/Row.cs index bdbc81658..a9f424c9d 100644 --- a/src/MySqlConnector/Core/Row.cs +++ b/src/MySqlConnector/Core/Row.cs @@ -455,7 +455,7 @@ private void CheckBinaryColumn(int ordinal) if ((column.ColumnFlags & ColumnFlags.Binary) == 0 || (columnType != ColumnType.String && columnType != ColumnType.VarString && columnType != ColumnType.TinyBlob && columnType != ColumnType.Blob && columnType != ColumnType.MediumBlob && columnType != ColumnType.LongBlob && - columnType != ColumnType.Geometry)) + columnType != ColumnType.Geometry && columnType != ColumnType.Vector)) { throw new InvalidCastException($"Can't convert {columnType} to bytes."); } diff --git a/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs b/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs index e6c2c641d..a9abe0b97 100644 --- a/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs +++ b/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs @@ -165,6 +165,10 @@ private static void WriteBinaryParameters(ByteBufferWriter writer, MySqlParamete mySqlDbType = TypeMapper.Instance.GetMySqlDbTypeForDbType(dbType); } + // HACK: MariaDB doesn't have a dedicated Vector type so mark it as binary data + if (mySqlDbType == MySqlDbType.Vector && command.Connection!.Session.ServerVersion.IsMariaDb) + mySqlDbType = MySqlDbType.LongBlob; + writer.Write(TypeMapper.ConvertToColumnTypeAndFlags(mySqlDbType, command.Connection!.GuidFormat)); if (supportsQueryAttributes) diff --git a/src/MySqlConnector/Core/TypeMapper.cs b/src/MySqlConnector/Core/TypeMapper.cs index 6397f9182..1058a99e1 100644 --- a/src/MySqlConnector/Core/TypeMapper.cs +++ b/src/MySqlConnector/Core/TypeMapper.cs @@ -55,6 +55,10 @@ private TypeMapper() AddColumnTypeMetadata(new("DOUBLE", typeDouble, MySqlDbType.Double)); AddColumnTypeMetadata(new("FLOAT", typeFloat, MySqlDbType.Float)); + // vector + var typeFloatArray = AddDbTypeMapping(new(typeof(float[]), [DbType.Object])); + AddColumnTypeMetadata(new("VECTOR", typeFloatArray, MySqlDbType.Vector, binary: true, simpleDataTypeName: "VECTOR", createFormat: "VECTOR({0})")); + // string var typeFixedString = AddDbTypeMapping(new(typeof(string), [DbType.StringFixedLength, DbType.AnsiStringFixedLength], convert: Convert.ToString!)); var typeString = AddDbTypeMapping(new(typeof(string), [DbType.String, DbType.AnsiString, DbType.Xml], convert: Convert.ToString!)); @@ -303,6 +307,9 @@ public static MySqlDbType ConvertToMySqlDbType(ColumnDefinitionPayload columnDef case ColumnType.Set: return MySqlDbType.Set; + case ColumnType.Vector: + return MySqlDbType.Vector; + default: throw new NotImplementedException($"ConvertToMySqlDbType for {columnDefinition.ColumnType} is not implemented"); } @@ -339,6 +346,7 @@ public static ushort ConvertToColumnTypeAndFlags(MySqlDbType dbType, MySqlGuidFo MySqlDbType.NewDecimal => ColumnType.NewDecimal, MySqlDbType.Geometry => ColumnType.Geometry, MySqlDbType.Null => ColumnType.Null, + MySqlDbType.Vector => ColumnType.Vector, _ => throw new NotImplementedException($"ConvertToColumnTypeAndFlags for {dbType} is not implemented"), }; return (ushort) ((byte) columnType | (isUnsigned ? 0x8000 : 0)); diff --git a/src/MySqlConnector/MySqlDataReader.cs b/src/MySqlConnector/MySqlDataReader.cs index 800fafe47..98e07fdd0 100644 --- a/src/MySqlConnector/MySqlDataReader.cs +++ b/src/MySqlConnector/MySqlDataReader.cs @@ -671,7 +671,7 @@ private static async Task ReadOutParametersAsync(IMySqlCommand command, ResultSe if (param.HasSetDbType && !row.IsDBNull(columnIndex)) { var dbTypeMapping = TypeMapper.Instance.GetDbTypeMapping(param.DbType); - if (dbTypeMapping is not null) + if (dbTypeMapping is not null && param.DbType is not DbType.Object) { param.Value = dbTypeMapping.DoConversion(row.GetValue(columnIndex)); continue; diff --git a/src/MySqlConnector/MySqlDbColumn.cs b/src/MySqlConnector/MySqlDbColumn.cs index b55e9a02a..9696951ed 100644 --- a/src/MySqlConnector/MySqlDbColumn.cs +++ b/src/MySqlConnector/MySqlDbColumn.cs @@ -15,6 +15,7 @@ internal MySqlDbColumn(int ordinal, ColumnDefinitionPayload column, bool allowZe var type = columnTypeMetadata.DbTypeMapping.ClrType; var columnSize = type == typeof(string) || type == typeof(Guid) ? column.ColumnLength / ProtocolUtility.GetBytesPerCharacter(column.CharacterSet) : + column.ColumnType == ColumnType.Vector ? column.ColumnLength / 4 : column.ColumnLength; AllowDBNull = (column.ColumnFlags & ColumnFlags.NotNull) == 0; diff --git a/src/MySqlConnector/MySqlDbType.cs b/src/MySqlConnector/MySqlDbType.cs index 77f990383..f1d0708a0 100644 --- a/src/MySqlConnector/MySqlDbType.cs +++ b/src/MySqlConnector/MySqlDbType.cs @@ -37,6 +37,7 @@ public enum MySqlDbType VarChar, String, Geometry, + Vector = 242, UByte = 501, UInt16, UInt32, diff --git a/src/MySqlConnector/MySqlParameter.cs b/src/MySqlConnector/MySqlParameter.cs index 6d493cba8..438ceb49e 100644 --- a/src/MySqlConnector/MySqlParameter.cs +++ b/src/MySqlConnector/MySqlParameter.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Numerics; +using System.Runtime.InteropServices; using System.Text; #if NET8_0_OR_GREATER using System.Text.Unicode; @@ -282,7 +283,7 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions { writer.WriteString(ulongValue); } - else if (Value is byte[] or ReadOnlyMemory or Memory or ArraySegment or MySqlGeometry or MemoryStream) + else if (Value is byte[] or ReadOnlyMemory or Memory or ArraySegment or MySqlGeometry or MemoryStream or float[]) { var inputSpan = Value switch { @@ -291,6 +292,7 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions Memory memory => memory.Span, MySqlGeometry geometry => geometry.ValueSpan, MemoryStream memoryStream => memoryStream.TryGetBuffer(out var streamBuffer) ? streamBuffer.AsSpan() : memoryStream.ToArray().AsSpan(), + float[] floatArray => MemoryMarshal.AsBytes(floatArray.AsSpan()), _ => ((ReadOnlyMemory) Value).Span, }; @@ -729,6 +731,11 @@ private void AppendBinary(ByteBufferWriter writer, object value, StatementPrepar { writer.Write(unchecked((ulong) BitConverter.DoubleToInt64Bits(doubleValue))); } + else if (value is float[] floatArrayValue) + { + writer.WriteLengthEncodedInteger(unchecked((ulong) floatArrayValue.Length * 4)); + writer.Write(MemoryMarshal.AsBytes(floatArrayValue.AsSpan())); + } else if (value is decimal decimalValue) { writer.WriteLengthEncodedAsciiString(decimalValue.ToString(CultureInfo.InvariantCulture)); diff --git a/src/MySqlConnector/Protocol/ColumnType.cs b/src/MySqlConnector/Protocol/ColumnType.cs index 4b3701106..b031a41a5 100644 --- a/src/MySqlConnector/Protocol/ColumnType.cs +++ b/src/MySqlConnector/Protocol/ColumnType.cs @@ -24,6 +24,7 @@ internal enum ColumnType Bit = 16, Timestamp2 = 17, DateTime2 = 18, + Vector = 242, Json = 0xF5, NewDecimal = 0xF6, Enum = 0xF7, diff --git a/src/MySqlConnector/packages.lock.json b/src/MySqlConnector/packages.lock.json index 3fc5d7c53..3b22aa928 100644 --- a/src/MySqlConnector/packages.lock.json +++ b/src/MySqlConnector/packages.lock.json @@ -300,15 +300,6 @@ "System.Memory": "4.5.5" } }, - "Microsoft.NETFramework.ReferenceAssemblies": { - "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3" - } - }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -366,11 +357,6 @@ "resolved": "8.0.0", "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" }, - "Microsoft.NETFramework.ReferenceAssemblies.net48": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==" - }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "8.0.0", diff --git a/tests/IntegrationTests/CharacterSetTests.cs b/tests/IntegrationTests/CharacterSetTests.cs index 2dcb685a8..317d49ddc 100644 --- a/tests/IntegrationTests/CharacterSetTests.cs +++ b/tests/IntegrationTests/CharacterSetTests.cs @@ -79,7 +79,7 @@ public void CollationConnection(bool reopenConnection) var collation = connection.Query(@"select @@collation_connection;").Single(); var expected = connection.ServerVersion.Substring(0, 2) is "8." or "9." ? "utf8mb4_0900_ai_ci" : - connection.ServerVersion.StartsWith("11.4.", StringComparison.Ordinal) || connection.ServerVersion.StartsWith("11.6.", StringComparison.Ordinal) ? "utf8mb4_uca1400_ai_ci" : + connection.ServerVersion.StartsWith("11.4.", StringComparison.Ordinal) || connection.ServerVersion.StartsWith("11.7.", StringComparison.Ordinal) ? "utf8mb4_uca1400_ai_ci" : "utf8mb4_general_ci"; Assert.Equal(expected, collation); } diff --git a/tests/IntegrationTests/DataTypes.cs b/tests/IntegrationTests/DataTypes.cs index b03650cbd..8a95e8360 100644 --- a/tests/IntegrationTests/DataTypes.cs +++ b/tests/IntegrationTests/DataTypes.cs @@ -1,4 +1,6 @@ using System.Globalization; +using System.Runtime.InteropServices; + #if MYSQL_DATA using MySql.Data.Types; #endif @@ -1143,6 +1145,11 @@ private static object CreateGeometry(byte[] data) [InlineData("Int64", "datatypes_integers", MySqlDbType.Int64, 20, typeof(long), "N", 0, 0)] [InlineData("UInt64", "datatypes_integers", MySqlDbType.UInt64, 20, typeof(ulong), "N", 0, 0)] [InlineData("value", "datatypes_json_core", MySqlDbType.JSON, int.MaxValue, typeof(string), "LN", 0, 0)] +#if MYSQL_DATA + [InlineData("value", "datatypes_vector", MySqlDbType.Vector, 12, typeof(byte[]), "N", 0, 31)] +#else + [InlineData("value", "datatypes_vector", MySqlDbType.Vector, 3, typeof(float[]), "N", 0, 31)] +#endif [InlineData("Single", "datatypes_reals", MySqlDbType.Float, 12, typeof(float), "N", 0, 31)] [InlineData("Double", "datatypes_reals", MySqlDbType.Double, 22, typeof(double), "N", 0, 31)] [InlineData("SmallDecimal", "datatypes_reals", MySqlDbType.NewDecimal, 7, typeof(decimal), "N", 5, 2)] @@ -1195,6 +1202,17 @@ private void DoGetSchemaTable(string column, string table, MySqlDbType mySqlDbTy { if (table == "datatypes_json_core" && !AppConfig.SupportsJson) return; + if (table == "datatypes_vector" && !AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Vector)) + return; + + // adjust for databases that don't have a dedicated on-the-wire type for VECTOR(n) + if (mySqlDbType == MySqlDbType.Vector && !AppConfig.SupportedFeatures.HasFlag(ServerFeatures.VectorType)) + { + mySqlDbType = MySqlDbType.VarBinary; + columnSize *= 4; + dataType = typeof(byte[]); + scale = 0; + } var isAutoIncrement = flags.IndexOf('A') != -1; var isKey = flags.IndexOf('K') != -1; @@ -1599,6 +1617,35 @@ public void QueryJson(string column, string[] expected) DoQuery("json_core", column, dataTypeName, expected, reader => reader.GetString(0), omitWhereTest: true); } + [SkippableTheory(ServerFeatures.Vector)] + [InlineData("value", new[] { null, "0,0,0", "1,1,1", "1,2,3", "-1,-1,-1" })] + public void QueryVector(string column, string[] expected) + { + var hasVectorType = AppConfig.SupportedFeatures.HasFlag(ServerFeatures.VectorType); + string dataTypeName = hasVectorType ? "VECTOR" : "BLOB"; + DoQuery("vector", column, dataTypeName, + expected.Select(x => +#if !MYSQL_DATA + hasVectorType ? (object) GetFloatArray(x) : GetByteArray(x)) +#else + // Connector/NET returns the float array as a byte[] + GetByteArray(x)) +#endif + .ToArray(), +#if !MYSQL_DATA + x => hasVectorType ? (float[]) x.GetValue(0) : (byte[]) x.GetValue(0), +#else + // NOTE: Connector/NET returns 'null' for NULL so simulate an exception for the tests + x => x.IsDBNull(0) ? throw new GetValueWhenNullException() : x.GetValue(0), +#endif + omitWhereTest: true); + + static float[] GetFloatArray(string value) => value?.Split(',').Select(x => float.Parse(x, CultureInfo.InvariantCulture)).ToArray(); + + static byte[] GetByteArray(string value) => + GetFloatArray(value) is { } floats ? MemoryMarshal.AsBytes(floats).ToArray() : null; + } + [SkippableTheory(MySqlData = "https://bugs.mysql.com/bug.php?id=97067")] [InlineData(false, "MIN", 0)] [InlineData(false, "MAX", uint.MaxValue)] diff --git a/tests/IntegrationTests/DataTypesFixture.cs b/tests/IntegrationTests/DataTypesFixture.cs index f9ad28a38..9f3fbac72 100644 --- a/tests/IntegrationTests/DataTypesFixture.cs +++ b/tests/IntegrationTests/DataTypesFixture.cs @@ -242,6 +242,28 @@ insert into datatypes_json_core (value) ('{""a"": ""b""}'); "); } + + if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Vector)) + { + // create a helper function for MariaDB 11.7+ + if (Connection.ServerVersion.StartsWith("11.7.", StringComparison.Ordinal)) + Connection.Execute("create function if not exists STRING_TO_VECTOR(s text) returns vector(3) deterministic return Vec_FromText(s);"); + + Connection.Execute(""" + drop table if exists datatypes_vector; + create table datatypes_vector ( + rowid integer not null primary key auto_increment, + value vector(3) null + ); + insert into datatypes_vector (value) + values + (null), + (STRING_TO_VECTOR('[0, 0, 0]')), + (STRING_TO_VECTOR('[1, 1, 1]')), + (STRING_TO_VECTOR('[1, 2, 3]')), + (STRING_TO_VECTOR('[-1, -1, -1]')); + """); + } Connection.Close(); } } diff --git a/tests/IntegrationTests/QueryTests.cs b/tests/IntegrationTests/QueryTests.cs index a6077f5fe..3c0aa6e4a 100644 --- a/tests/IntegrationTests/QueryTests.cs +++ b/tests/IntegrationTests/QueryTests.cs @@ -1,3 +1,5 @@ +using System.Runtime.InteropServices; + namespace IntegrationTests; public class QueryTests : IClassFixture, IDisposable @@ -1688,6 +1690,57 @@ public void GetBytesByName() } #endif + [SkippableTheory(ServerFeatures.Vector)] + [InlineData(false)] + [InlineData(true)] + public void QueryVector(bool prepare) + { + using var connection = new MySqlConnection(AppConfig.ConnectionString); + connection.Open(); + + connection.Execute(""" + drop table if exists test_vector; + create table test_vector(id int auto_increment not null primary key, vec vector(3) not null); + """); + + using var cmd = m_database.Connection.CreateCommand(); + cmd.CommandText = "INSERT INTO test_vector(vec) VALUES(@vec)"; + cmd.Parameters.Add(new MySqlParameter + { + ParameterName = "@vec", + MySqlDbType = MySqlDbType.Vector, + }); + + var floatArray = new[] { 1.2f, 3.4f, 5.6f }; +#if MYSQL_DATA + // Connector/NET requires the float vector to be passed as a byte array + cmd.Parameters[0].Value = MemoryMarshal.AsBytes(floatArray).ToArray(); +#else + cmd.Parameters[0].Value = floatArray; +#endif + + if (prepare) + cmd.Prepare(); + cmd.ExecuteNonQuery(); + + // Select and verify the value + cmd.CommandText = "SELECT vec FROM test_vector"; + if (prepare) + cmd.Prepare(); + + using var reader = cmd.ExecuteReader(); + Assert.True(reader.Read()); + var value = reader.GetValue(0); + +#if MYSQL_DATA + var result = MemoryMarshal.Cast((byte[]) value).ToArray(); +#else + var result = AppConfig.SupportedFeatures.HasFlag(ServerFeatures.VectorType) ? (float[]) value : + MemoryMarshal.Cast((byte[]) value).ToArray(); +#endif + Assert.Equal(floatArray, result); + } + private class BoolTest { public int Id { get; set; } diff --git a/tests/IntegrationTests/ServerFeatures.cs b/tests/IntegrationTests/ServerFeatures.cs index 120b541bf..8e37bc0b2 100644 --- a/tests/IntegrationTests/ServerFeatures.cs +++ b/tests/IntegrationTests/ServerFeatures.cs @@ -45,4 +45,14 @@ public enum ServerFeatures /// Server provides hash of TLS certificate in first OK packet. /// TlsFingerprintValidation = 0x100_0000, + + /// + /// Server supports the VECTOR SQL type. + /// + Vector = 0x200_0000, + + /// + /// Server has a dedicated type on the wire for VECTOR. + /// + VectorType = 0x400_0000, } diff --git a/tests/IntegrationTests/StoredProcedureTests.cs b/tests/IntegrationTests/StoredProcedureTests.cs index cd0e79ede..dc327f863 100644 --- a/tests/IntegrationTests/StoredProcedureTests.cs +++ b/tests/IntegrationTests/StoredProcedureTests.cs @@ -1,3 +1,5 @@ +using System.Runtime.InteropServices; + namespace IntegrationTests; public class StoredProcedureTests : IClassFixture @@ -761,6 +763,39 @@ public void PassJsonParameter() Assert.False(reader.Read()); } + [SkippableTheory(ServerFeatures.Vector | ServerFeatures.VectorType)] + [InlineData(false)] + [InlineData(true)] + public void VectorOutputParameter(bool prepare) + { + using var cmd = m_database.Connection.CreateCommand(); + cmd.CommandText = """ + DROP PROCEDURE IF EXISTS sp_vector_out; + CREATE PROCEDURE sp_vector_out (OUT vec VECTOR) + BEGIN + SELECT STRING_TO_VECTOR('[1.2, 3.4, 5.6]') INTO vec; + END; + """; + cmd.ExecuteNonQuery(); + + cmd.CommandText = "sp_vector_out"; + cmd.CommandType = CommandType.StoredProcedure; + cmd.Parameters.Add(new MySqlParameter + { + Direction = ParameterDirection.Output, + MySqlDbType = MySqlDbType.Vector, + ParameterName = "@vec", + }); + + if (prepare) + cmd.Prepare(); + cmd.ExecuteNonQuery(); + + var value = cmd.Parameters[0].Value; + var result = Assert.IsType(value); + Assert.Equal(new float[] { 1.2f, 3.4f, 5.6f }, MemoryMarshal.Cast(result).ToArray()); + } + private static Action AssertParameter(string name, ParameterDirection direction, MySqlDbType mySqlDbType) { return x =>