Skip to content

Commit

Permalink
Read GUIDs from out parameters. Fixes #1528
Browse files Browse the repository at this point in the history
Signed-off-by: Bradley Grainger <[email protected]>
  • Loading branch information
bgrainger committed Feb 2, 2025
1 parent fc59a06 commit 944ced6
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 69 deletions.
4 changes: 2 additions & 2 deletions src/MySqlConnector/ColumnReaders/ColumnReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/MySqlConnector/Core/CachedParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}

Expand Down
19 changes: 12 additions & 7 deletions src/MySqlConnector/Core/CachedProcedure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -92,7 +92,8 @@ FROM information_schema.parameters
!reader.IsDBNull(2) ? reader.GetString(2) : "",
dataType,
unsigned,
length
length,
connection.GuidFormat
));
}
}
Expand Down Expand Up @@ -133,14 +134,18 @@ 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);
}

return alignedParams;
}

internal static List<CachedParameter> ParseParameters(string parametersSql)
internal static List<CachedParameter> ParseParameters(string parametersSql, MySqlGuidFormat guidFormat)
{
// strip comments
parametersSql = s_cStyleComments.Replace(parametersSql, "");
Expand Down Expand Up @@ -185,7 +190,7 @@ internal static List<CachedParameter> 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;
Expand Down Expand Up @@ -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)
{
Expand Down
20 changes: 17 additions & 3 deletions src/MySqlConnector/Core/ColumnTypeMetadata.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}
18 changes: 17 additions & 1 deletion src/MySqlConnector/Core/SingleCommandPayloadCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,24 @@ private static bool WriteStoredProcedure(IMySqlCommand command, IDictionary<stri
break;
case ParameterDirection.Output:
outParameters.Add(param);
outParameterNames.Add(outName);
argParameterNames.Add(outName);

// special handling for GUIDs to ensure that the result set has a type and length that will be autodetected as a GUID
switch (param.MySqlDbType, param.Size)
{
case (MySqlDbType.Guid, 16):
outParameterNames.Add($"CAST({outName} AS BINARY(16))");
break;
case (MySqlDbType.Guid, 32):
outParameterNames.Add($"CAST({outName} AS CHAR(32))");
break;
case (MySqlDbType.Guid, 36):
outParameterNames.Add($"CAST({outName} AS CHAR(36))");
break;
default:
outParameterNames.Add(outName);
break;
}
break;
case ParameterDirection.ReturnValue:
returnParameter = param;
Expand Down
18 changes: 13 additions & 5 deletions src/MySqlConnector/Core/TypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ private TypeMapper()
#endif
var typeGuid = AddDbTypeMapping(new(typeof(Guid), [DbType.Guid], convert: convertGuid));
AddColumnTypeMetadata(new("CHAR", typeGuid, MySqlDbType.Guid, length: 36, simpleDataTypeName: "CHAR(36)", createFormat: "CHAR(36)"));
AddColumnTypeMetadata(new("CHAR", typeGuid, MySqlDbType.Guid, length: 32, guidFormat: MySqlGuidFormat.Char32));
AddColumnTypeMetadata(new("CHAR", typeGuid, MySqlDbType.Guid, length: 36, guidFormat: MySqlGuidFormat.Char36));
AddColumnTypeMetadata(new("BINARY", typeGuid, MySqlDbType.Guid, binary: true, length: 16, guidFormat: MySqlGuidFormat.Binary16));

// null
var typeNull = AddDbTypeMapping(new(typeof(object), [DbType.Object]));
Expand Down Expand Up @@ -181,15 +184,20 @@ private void AddColumnTypeMetadata(ColumnTypeMetadata columnTypeMetadata)

public DbTypeMapping? GetDbTypeMapping(string columnTypeName, bool unsigned = false, int length = 0)
{
return GetColumnTypeMetadata(columnTypeName, unsigned, length)?.DbTypeMapping;
return GetColumnTypeMetadata(columnTypeName, unsigned, length, MySqlGuidFormat.Default)?.DbTypeMapping;
}

public MySqlDbType GetMySqlDbType(string typeName, bool unsigned, int length) => 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;
}

Expand Down
49 changes: 49 additions & 0 deletions tests/IntegrationTests/StoredProcedureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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', ' ');
Expand Down
Loading

0 comments on commit 944ced6

Please sign in to comment.