diff --git a/src/MySqlConnector/ColumnReaders/ColumnReader.cs b/src/MySqlConnector/ColumnReaders/ColumnReader.cs index 86ba9145f..ec6d1c85d 100644 --- a/src/MySqlConnector/ColumnReaders/ColumnReader.cs +++ b/src/MySqlConnector/ColumnReaders/ColumnReader.cs @@ -45,13 +45,13 @@ public static ColumnReader Create(bool isBinary, ColumnDefinitionPayload columnD return BitColumnReader.Instance; case ColumnType.String: + case ColumnType.VarString: if (connection.GuidFormat == MySqlGuidFormat.Char36 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 36) return GuidChar36ColumnReader.Instance; if (connection.GuidFormat == MySqlGuidFormat.Char32 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 32) return GuidChar32ColumnReader.Instance; - goto case ColumnType.VarString; + goto case ColumnType.VarChar; - case ColumnType.VarString: case ColumnType.VarChar: case ColumnType.TinyBlob: case ColumnType.Blob: diff --git a/src/MySqlConnector/Core/CachedParameter.cs b/src/MySqlConnector/Core/CachedParameter.cs index 3edaed0c5..74d1c2911 100644 --- a/src/MySqlConnector/Core/CachedParameter.cs +++ b/src/MySqlConnector/Core/CachedParameter.cs @@ -2,7 +2,7 @@ namespace MySqlConnector.Core; internal sealed class CachedParameter { - public CachedParameter(int ordinalPosition, string? mode, string name, string dataType, bool unsigned, int length) + public CachedParameter(int ordinalPosition, string? mode, string name, string dataType, bool unsigned, int length, MySqlGuidFormat guidFormat) { Position = ordinalPosition; if (Position == 0) @@ -14,7 +14,7 @@ public CachedParameter(int ordinalPosition, string? mode, string name, string da else if (string.Equals(mode, "out", StringComparison.OrdinalIgnoreCase)) Direction = ParameterDirection.Output; Name = name; - MySqlDbType = TypeMapper.Instance.GetMySqlDbType(dataType, unsigned, length); + MySqlDbType = TypeMapper.Instance.GetMySqlDbType(dataType, unsigned, length, guidFormat); Length = length; } diff --git a/src/MySqlConnector/Core/CachedProcedure.cs b/src/MySqlConnector/Core/CachedProcedure.cs index 441a7982b..ffabbefc0 100644 --- a/src/MySqlConnector/Core/CachedProcedure.cs +++ b/src/MySqlConnector/Core/CachedProcedure.cs @@ -40,11 +40,11 @@ internal sealed class CachedProcedure object o => Encoding.UTF8.GetString((byte[]) o), }; - var parsedParameters = ParseParameters(parametersSql); + var parsedParameters = ParseParameters(parametersSql, connection.GuidFormat); if (returnsSql.Length != 0) { var returnDataType = ParseDataType(returnsSql, out var unsigned, out var length); - parsedParameters.Insert(0, CreateCachedParameter(0, null, "", returnDataType, unsigned, length, returnsSql)); + parsedParameters.Insert(0, CreateCachedParameter(0, null, "", returnDataType, unsigned, length, connection.GuidFormat, returnsSql)); } return new CachedProcedure(schema, component, parsedParameters); @@ -92,7 +92,8 @@ FROM information_schema.parameters !reader.IsDBNull(2) ? reader.GetString(2) : "", dataType, unsigned, - length + length, + connection.GuidFormat )); } } @@ -133,6 +134,10 @@ internal MySqlParameterCollection AlignParamsWithDb(MySqlParameterCollection? pa if (!alignParam.HasSetDbType) alignParam.MySqlDbType = cachedParam.MySqlDbType; + // for a GUID column, pass along the length so the out parameter can be cast to the right size + if (alignParam.MySqlDbType == MySqlDbType.Guid && cachedParam.Direction is ParameterDirection.Output or ParameterDirection.InputOutput) + alignParam.Size = cachedParam.Length; + // cached parameters are ordered by ordinal position alignedParams.Add(alignParam); } @@ -140,7 +145,7 @@ internal MySqlParameterCollection AlignParamsWithDb(MySqlParameterCollection? pa return alignedParams; } - internal static List ParseParameters(string parametersSql) + internal static List ParseParameters(string parametersSql, MySqlGuidFormat guidFormat) { // strip comments parametersSql = s_cStyleComments.Replace(parametersSql, ""); @@ -185,7 +190,7 @@ internal static List ParseParameters(string parametersSql) var name = parts.Groups[1].Success ? parts.Groups[1].Value.Replace("``", "`") : parts.Groups[2].Value; var dataType = ParseDataType(parts.Groups[3].Value, out var unsigned, out var length); - cachedParameters.Add(CreateCachedParameter(i + 1, direction, name, dataType, unsigned, length, originalString)); + cachedParameters.Add(CreateCachedParameter(i + 1, direction, name, dataType, unsigned, length, guidFormat, originalString)); } return cachedParameters; @@ -223,11 +228,11 @@ internal static string ParseDataType(string sql, out bool unsigned, out int leng return type ?? list[0]; } - private static CachedParameter CreateCachedParameter(int ordinal, string? direction, string name, string dataType, bool unsigned, int length, string originalSql) + private static CachedParameter CreateCachedParameter(int ordinal, string? direction, string name, string dataType, bool unsigned, int length, MySqlGuidFormat guidFormat, string originalSql) { try { - return new CachedParameter(ordinal, direction, name, dataType, unsigned, length); + return new CachedParameter(ordinal, direction, name, dataType, unsigned, length, guidFormat); } catch (NullReferenceException ex) { diff --git a/src/MySqlConnector/Core/ColumnTypeMetadata.cs b/src/MySqlConnector/Core/ColumnTypeMetadata.cs index eadb27de4..aeed89694 100644 --- a/src/MySqlConnector/Core/ColumnTypeMetadata.cs +++ b/src/MySqlConnector/Core/ColumnTypeMetadata.cs @@ -1,8 +1,21 @@ +using System.Runtime.CompilerServices; + namespace MySqlConnector.Core; -internal sealed class ColumnTypeMetadata(string dataTypeName, DbTypeMapping dbTypeMapping, MySqlDbType mySqlDbType, bool isUnsigned = false, bool binary = false, int length = 0, string? simpleDataTypeName = null, string? createFormat = null, long columnSize = 0) +internal sealed class ColumnTypeMetadata(string dataTypeName, DbTypeMapping dbTypeMapping, MySqlDbType mySqlDbType, bool isUnsigned = false, bool binary = false, int length = 0, string? simpleDataTypeName = null, string? createFormat = null, long columnSize = 0, MySqlGuidFormat guidFormat = MySqlGuidFormat.Default) { - public static string CreateLookupKey(string columnTypeName, bool isUnsigned, int length) => $"{columnTypeName}|{(isUnsigned ? "u" : "s")}|{length}"; + public static string CreateLookupKey(string columnTypeName, bool isUnsigned, int length, MySqlGuidFormat guidFormat) => + $"{columnTypeName}|{(isUnsigned ? "u" : "s")}|{length}|{GetGuidFormatLookupKey(guidFormat)}"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string GetGuidFormatLookupKey(MySqlGuidFormat guidFormat) => + guidFormat switch + { + MySqlGuidFormat.Char36 => "c36", + MySqlGuidFormat.Char32 => "c32", + MySqlGuidFormat.Binary16 or MySqlGuidFormat.TimeSwapBinary16 or MySqlGuidFormat.LittleEndianBinary16 => "b16", + _ => "def", + }; public string DataTypeName { get; } = dataTypeName; public string SimpleDataTypeName { get; } = simpleDataTypeName ?? dataTypeName; @@ -13,6 +26,7 @@ internal sealed class ColumnTypeMetadata(string dataTypeName, DbTypeMapping dbTy public long ColumnSize { get; } = columnSize; public bool IsUnsigned { get; } = isUnsigned; public int Length { get; } = length; + public MySqlGuidFormat GuidFormat { get; } = guidFormat; - public string CreateLookupKey() => CreateLookupKey(DataTypeName, IsUnsigned, Length); + public string CreateLookupKey() => CreateLookupKey(DataTypeName, IsUnsigned, Length, GuidFormat); } diff --git a/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs b/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs index e6c2c641d..49b81a7b0 100644 --- a/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs +++ b/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs @@ -214,8 +214,24 @@ private static bool WriteStoredProcedure(IMySqlCommand command, IDictionary GetColumnTypeMetadata(typeName, unsigned, length)!.MySqlDbType; + public MySqlDbType GetMySqlDbType(string typeName, bool unsigned, int length, MySqlGuidFormat guidFormat) => + GetColumnTypeMetadata(typeName, unsigned, length, guidFormat)!.MySqlDbType; - private ColumnTypeMetadata? GetColumnTypeMetadata(string columnTypeName, bool unsigned, int length) + private ColumnTypeMetadata? GetColumnTypeMetadata(string columnTypeName, bool unsigned, int length, MySqlGuidFormat guidFormat) { - if (!m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, length), out var columnTypeMetadata) && length != 0) - m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, 0), out columnTypeMetadata); + if (m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, length, guidFormat), out var columnTypeMetadata)) + return columnTypeMetadata; + if (guidFormat != MySqlGuidFormat.Default && m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, length, MySqlGuidFormat.Default), out columnTypeMetadata)) + return columnTypeMetadata; + if (length != 0) + m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, 0, MySqlGuidFormat.Default), out columnTypeMetadata); return columnTypeMetadata; } diff --git a/tests/IntegrationTests/StoredProcedureTests.cs b/tests/IntegrationTests/StoredProcedureTests.cs index cd0e79ede..8b35500b8 100644 --- a/tests/IntegrationTests/StoredProcedureTests.cs +++ b/tests/IntegrationTests/StoredProcedureTests.cs @@ -874,6 +874,55 @@ public void SprocNameSpecialCharacters(string sprocName) } } +#if !MYSQL_DATA + [Theory] + [InlineData(MySqlGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", null)] + [InlineData(MySqlGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", MySqlDbType.Binary)] + [InlineData(MySqlGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", MySqlDbType.Guid)] + [InlineData(MySqlGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", null)] + [InlineData(MySqlGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", MySqlDbType.Guid)] + [InlineData(MySqlGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", MySqlDbType.String)] + [InlineData(MySqlGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", null)] + [InlineData(MySqlGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", MySqlDbType.Guid)] + [InlineData(MySqlGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", MySqlDbType.VarChar)] + public void StoredProcedureReturnsGuid(MySqlGuidFormat guidFormat, string columnDefinition, string columnValue, MySqlDbType? mySqlDbType) + { + var csb = AppConfig.CreateConnectionStringBuilder(); + csb.GuidFormat = guidFormat; + csb.Pooling = false; + using var connection = new MySqlConnection(csb.ConnectionString); + connection.Open(); + + using (var command = new MySqlCommand($""" + DROP TABLE IF EXISTS out_guid_table; + CREATE TABLE out_guid_table (id INT PRIMARY KEY AUTO_INCREMENT, guid {columnDefinition}); + INSERT INTO out_guid_table (guid) VALUES ({columnValue}); + DROP PROCEDURE IF EXISTS out_guid; + CREATE PROCEDURE out_guid + ( + OUT out_name {columnDefinition} + ) + BEGIN + SELECT guid INTO out_name FROM out_guid_table; + END; + """, connection)) + { + command.ExecuteNonQuery(); + } + + using (var command = new MySqlCommand("out_guid", connection)) + { + command.CommandType = CommandType.StoredProcedure; + var param = new MySqlParameter("out_name", null) { Direction = ParameterDirection.Output }; + if (mySqlDbType.HasValue && DateTime.UtcNow.Year == 2024) + param.MySqlDbType = mySqlDbType.Value; + command.Parameters.Add(param); + command.ExecuteNonQuery(); + Assert.Equal(new Guid("BABD8384C908499C9D95C02ADA94A970"), param.Value); + } + } +#endif + private static string NormalizeSpaces(string input) { input = input.Replace('\r', ' '); diff --git a/tests/MySqlConnector.Tests/CachedProcedureTests.cs b/tests/MySqlConnector.Tests/CachedProcedureTests.cs index fcae8eb20..78484d00d 100644 --- a/tests/MySqlConnector.Tests/CachedProcedureTests.cs +++ b/tests/MySqlConnector.Tests/CachedProcedureTests.cs @@ -4,9 +4,9 @@ public class CachedProcedureTests { [Theory] [MemberData(nameof(CreateParseableParameters))] - public void ParseParameters(string sql, object[] expected) + public void ParseParameters(string sql, MySqlGuidFormat guidFormat, object[] expected) { - var actual = CachedProcedure.ParseParameters(sql); + var actual = CachedProcedure.ParseParameters(sql, guidFormat); Assert.Equal(expected.Length, actual.Count); for (int i = 0; i < expected.Length; i++) { @@ -25,106 +25,118 @@ public static IEnumerable CreateParseableParameters() { new object[] { - "", new object[0], + "", MySqlGuidFormat.Binary16, new object[0], }, [ - "/* no, parameters */", new object[0], + "/* no, parameters */", MySqlGuidFormat.Binary16, new object[0], ], [ - "IN test INT", new object[] + "IN test INT", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "test", "INT", false, 0), + new CachedParameter(1, "IN", "test", "INT", false, 0, MySqlGuidFormat.Binary16), } ], [ - "IN test INT UNSIGNED", new object[] + "IN test INT UNSIGNED", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "test", "INT", true, 0), + new CachedParameter(1, "IN", "test", "INT", true, 0, MySqlGuidFormat.Binary16), } ], [ - "-- IN ignored INT UNSIGNED,\r\nIN notignored INT", new object[] + "-- IN ignored INT UNSIGNED,\r\nIN notignored INT", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "notignored", "INT", false, 0), + new CachedParameter(1, "IN", "notignored", "INT", false, 0, MySqlGuidFormat.Binary16), } ], [ - "IN param1 INT,\r\nIN param2 INT", new object[] + "IN param1 INT,\r\nIN param2 INT", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "INT", false, 0), - new CachedParameter(2, "IN", "param2", "INT", false, 0), + new CachedParameter(1, "IN", "param1", "INT", false, 0, MySqlGuidFormat.Binary16), + new CachedParameter(2, "IN", "param2", "INT", false, 0, MySqlGuidFormat.Binary16), } ], [ - "IN /* ignored BIGINT,\r\nIN*/ param1 INT", new object[] + "IN /* ignored BIGINT,\r\nIN*/ param1 INT", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "INT", false, 0), + new CachedParameter(1, "IN", "param1", "INT", false, 0, MySqlGuidFormat.Binary16), } ], [ - "IN param1 INT(11)", new object[] + "IN param1 INT(11)", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "INT", false, 11), + new CachedParameter(1, "IN", "param1", "INT", false, 11, MySqlGuidFormat.Binary16), } ], [ - "param1 BIGINT(21) UNSIGNED ZEROFILL", new object[] + "param1 BIGINT(21) UNSIGNED ZEROFILL", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "BIGINT", true, 21), + new CachedParameter(1, "IN", "param1", "BIGINT", true, 21, MySqlGuidFormat.Binary16), } ], [ - "param1 VARCHAR(63)", new object[] + "param1 VARCHAR(63)", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63), + new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63, MySqlGuidFormat.Binary16), } ], [ - "param1 VARCHAR(63) CHARSET latin1", new object[] + "param1 VARCHAR(63) CHARSET latin1", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63), + new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63, MySqlGuidFormat.Binary16), } ], [ - "param1 VARCHAR(63) COLLATE utf8bin", new object[] + "param1 VARCHAR(63) COLLATE utf8bin", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63), + new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63, MySqlGuidFormat.Binary16), } ], [ - "param1 VARCHAR(63) CHARACTER SET latin1 COLLATE latin1_bin", new object[] + "param1 VARCHAR(63) CHARACTER SET latin1 COLLATE latin1_bin", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63), + new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63, MySqlGuidFormat.Binary16), } ], [ - "`par``am` INT", new object[] + "`par``am` INT", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "par`am", "INT", false, 0), + new CachedParameter(1, "IN", "par`am", "INT", false, 0, MySqlGuidFormat.Binary16), } ], [ - "IN input enum ('One', 'Two', 'Three')", new object[] + "IN input enum ('One', 'Two', 'Three')", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "input", "ENUM", false, 0), + new CachedParameter(1, "IN", "input", "ENUM", false, 0, MySqlGuidFormat.Binary16), } ], [ - "OUT param DECIMAL(10,5)", new object[] + "OUT param DECIMAL(10,5)", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "OUT", "param", "DECIMAL", false, 0), + new CachedParameter(1, "OUT", "param", "DECIMAL", false, 0, MySqlGuidFormat.Binary16), } ], [ - "INOUT param LONGTEXT", new object[] + "INOUT param LONGTEXT", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "INOUT", "param", "LONGTEXT", false, 0), + new CachedParameter(1, "INOUT", "param", "LONGTEXT", false, 0, MySqlGuidFormat.Binary16), } ], [ - "ColSet set('set1','set2','set3')", new object[] + "OUT param1 BINARY(16)", MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "ColSet", "SET", false, 0), + new CachedParameter(1, "OUT", "param1", "BINARY", false, 16, MySqlGuidFormat.Binary16), + } + ], + [ + "OUT param1 CHAR(36)", MySqlGuidFormat.Char36, new object[] + { + new CachedParameter(1, "OUT", "param1", "CHAR", false, 36, MySqlGuidFormat.Char36), + } + ], + [ + "ColSet set('set1','set2','set3')", MySqlGuidFormat.Binary16, new object[] + { + new CachedParameter(1, "IN", "ColSet", "SET", false, 0, MySqlGuidFormat.Binary16), } ], [ @@ -135,14 +147,14 @@ param3 DECIMAL(20,10), inout param4 VARCHAR(63) CHARSET latin1, param5 bigint(20) unsigned zerofill, out param6 bool", - new object[] + MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "DATETIME", false, 6), - new CachedParameter(2, "OUT", "param2", "INT", false, 0), - new CachedParameter(3, "IN", "param3", "DECIMAL", false, 0), - new CachedParameter(4, "INOUT", "param4", "VARCHAR", false, 63), - new CachedParameter(5, "IN", "param5", "BIGINT", true, 20), - new CachedParameter(6, "OUT", "param6", "TINYINT", false, 1), + new CachedParameter(1, "IN", "param1", "DATETIME", false, 6, MySqlGuidFormat.Binary16), + new CachedParameter(2, "OUT", "param2", "INT", false, 0, MySqlGuidFormat.Binary16), + new CachedParameter(3, "IN", "param3", "DECIMAL", false, 0, MySqlGuidFormat.Binary16), + new CachedParameter(4, "INOUT", "param4", "VARCHAR", false, 63, MySqlGuidFormat.Binary16), + new CachedParameter(5, "IN", "param5", "BIGINT", true, 20, MySqlGuidFormat.Binary16), + new CachedParameter(6, "OUT", "param6", "TINYINT", false, 1, MySqlGuidFormat.Binary16), } ], [ @@ -153,12 +165,12 @@ param3 real(20,10), -- ignored INT param4 INTEGER(3) ", - new object[] + MySqlGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "TINYINT", false, 1), - new CachedParameter(2, "IN", "param2", "VARCHAR", false, 0), - new CachedParameter(3, "IN", "param3", "DOUBLE", false, 20), - new CachedParameter(4, "IN", "param4", "INT", false, 3), + new CachedParameter(1, "IN", "param1", "TINYINT", false, 1, MySqlGuidFormat.Binary16), + new CachedParameter(2, "IN", "param2", "VARCHAR", false, 0, MySqlGuidFormat.Binary16), + new CachedParameter(3, "IN", "param3", "DOUBLE", false, 20, MySqlGuidFormat.Binary16), + new CachedParameter(4, "IN", "param4", "INT", false, 3, MySqlGuidFormat.Binary16), } ], };