diff --git a/src/Octonica.ClickHouseClient.Tests/TestEnum.cs b/src/Octonica.ClickHouseClient.Tests/TestEnum.cs new file mode 100644 index 0000000..90cff0d --- /dev/null +++ b/src/Octonica.ClickHouseClient.Tests/TestEnum.cs @@ -0,0 +1,28 @@ +#region License Apache 2.0 +/* Copyright 2020 Octonica + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#endregion + +namespace Octonica.ClickHouseClient.Tests +{ + internal enum TestEnum + { + None = 0, + Value1 = 42, + Value2 = -134, + Value3 = 32000, + Value4 = int.MaxValue + } +} diff --git a/src/Octonica.ClickHouseClient.Tests/TypeTests.cs b/src/Octonica.ClickHouseClient.Tests/TypeTests.cs index 041819e..fd6ae30 100644 --- a/src/Octonica.ClickHouseClient.Tests/TypeTests.cs +++ b/src/Octonica.ClickHouseClient.Tests/TypeTests.cs @@ -26,6 +26,7 @@ using System.Threading.Tasks; using Octonica.ClickHouseClient.Exceptions; using Octonica.ClickHouseClient.Protocol; +using Octonica.ClickHouseClient.Types; using TimeZoneConverter; using Xunit; @@ -396,6 +397,18 @@ public async Task ReadEnumScalar() Assert.Equal("Hello, world! :)", result); } + [Fact] + public async Task ReadClrEnumScalar() + { + await using var connection = await OpenConnectionAsync(); + + await using var cmd = connection.CreateCommand("SELECT CAST(42 AS Enum('' = 0, 'b' = -129, 'Hello, world! :)' = 42))"); + + var settings = new ClickHouseColumnSettings(new ClickHouseEnumConverter()); + var result = await cmd.ExecuteScalarAsync(settings); + Assert.Equal(TestEnum.Value1, result); + } + [Fact] public async Task ReadInt32ArrayColumn() { diff --git a/src/Octonica.ClickHouseClient/Protocol/ClickHouseColumnSettings.cs b/src/Octonica.ClickHouseClient/Protocol/ClickHouseColumnSettings.cs index 02fea90..a4031cf 100644 --- a/src/Octonica.ClickHouseClient/Protocol/ClickHouseColumnSettings.cs +++ b/src/Octonica.ClickHouseClient/Protocol/ClickHouseColumnSettings.cs @@ -17,16 +17,30 @@ using System; using System.Text; +using Octonica.ClickHouseClient.Types; namespace Octonica.ClickHouseClient.Protocol { public class ClickHouseColumnSettings { - public Encoding StringEncoding { get; } + public Encoding? StringEncoding { get; } + + public IClickHouseEnumConverter? EnumConverter { get; } public ClickHouseColumnSettings(Encoding stringEncoding) { StringEncoding = stringEncoding ?? throw new ArgumentNullException(nameof(stringEncoding)); } + + public ClickHouseColumnSettings(IClickHouseEnumConverter enumConverter) + { + EnumConverter = enumConverter ?? throw new ArgumentNullException(nameof(enumConverter)); + } + + public ClickHouseColumnSettings(Encoding? stringEncoding = null, IClickHouseEnumConverter? enumConverter = null) + { + StringEncoding = stringEncoding; + EnumConverter = enumConverter; + } } } diff --git a/src/Octonica.ClickHouseClient/Types/ClickHouseEnumConverter.cs b/src/Octonica.ClickHouseClient/Types/ClickHouseEnumConverter.cs new file mode 100644 index 0000000..a412062 --- /dev/null +++ b/src/Octonica.ClickHouseClient/Types/ClickHouseEnumConverter.cs @@ -0,0 +1,49 @@ +#region License Apache 2.0 +/* Copyright 2020 Octonica + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#endregion + +using System; +using System.Collections.Generic; + +namespace Octonica.ClickHouseClient.Types +{ + public sealed class ClickHouseEnumConverter : IClickHouseEnumConverter + where TEnum : Enum + { + private readonly Dictionary _values; + + public ClickHouseEnumConverter() + { + var enumValues = Enum.GetValues(typeof(TEnum)); + _values = new Dictionary(enumValues.Length); + foreach (var enumValue in enumValues) + { + var intValue = Convert.ToInt32(enumValue); + _values[intValue] = (TEnum) enumValue!; + } + } + + T IClickHouseEnumConverter.Dispatch(IClickHouseEnumConverterDispatcher dispatcher) + { + return dispatcher.Dispatch(this); + } + + public bool TryMap(int value, string stringValue, out TEnum enumValue) + { + return _values.TryGetValue(value, out enumValue!); + } + } +} diff --git a/src/Octonica.ClickHouseClient/Types/Enum16TypeInfo.cs b/src/Octonica.ClickHouseClient/Types/Enum16TypeInfo.cs index 4775659..c3e2b0c 100644 --- a/src/Octonica.ClickHouseClient/Types/Enum16TypeInfo.cs +++ b/src/Octonica.ClickHouseClient/Types/Enum16TypeInfo.cs @@ -35,6 +35,11 @@ private Enum16TypeInfo(string typeName, string complexTypeName, IEnumerable internalReader, IReadOnlyDictionary reversedEnumMap) + { + return new EnumColumnReader(internalReader, reversedEnumMap); + } + protected override IClickHouseColumnTypeInfo CreateDetailedTypeInfo(string complexTypeName, IEnumerable> values) { return new Enum16TypeInfo(TypeName, complexTypeName, values); @@ -64,5 +69,31 @@ protected override bool TryParse(ReadOnlySpan text, out short value) { return short.TryParse(text, out value); } + + private sealed class EnumColumnReader : EnumColumnReaderBase + { + public EnumColumnReader(StructureReaderBase internalReader, IReadOnlyDictionary reversedEnumMap) + : base(internalReader, reversedEnumMap) + { + } + + protected override EnumTableColumnDispatcherBase CreateColumnDispatcher(IClickHouseTableColumn column, IReadOnlyDictionary reversedEnumMap) + { + return new EnumTableColumnDispatcher(column, reversedEnumMap); + } + } + + private sealed class EnumTableColumnDispatcher : EnumTableColumnDispatcherBase + { + public EnumTableColumnDispatcher(IClickHouseTableColumn column, IReadOnlyDictionary reversedEnumMap) + : base(column, reversedEnumMap) + { + } + + protected override bool TryMap(IClickHouseEnumConverter enumConverter, short value, string stringValue, out TEnum enumValue) + { + return enumConverter.TryMap(value, stringValue, out enumValue); + } + } } } diff --git a/src/Octonica.ClickHouseClient/Types/Enum8TypeInfo.cs b/src/Octonica.ClickHouseClient/Types/Enum8TypeInfo.cs index 9550097..801662c 100644 --- a/src/Octonica.ClickHouseClient/Types/Enum8TypeInfo.cs +++ b/src/Octonica.ClickHouseClient/Types/Enum8TypeInfo.cs @@ -34,6 +34,11 @@ private Enum8TypeInfo(string typeName, string complexTypeName, IEnumerable internalReader, IReadOnlyDictionary reversedEnumMap) + { + return new EnumColumnReader(internalReader, reversedEnumMap); + } + protected override IClickHouseColumnTypeInfo CreateDetailedTypeInfo(string complexTypeName, IEnumerable> values) { return new Enum8TypeInfo(TypeName, complexTypeName, values); @@ -56,5 +61,31 @@ protected override bool TryParse(ReadOnlySpan text, out sbyte value) { return sbyte.TryParse(text, out value); } + + private sealed class EnumColumnReader : EnumColumnReaderBase + { + public EnumColumnReader(StructureReaderBase internalReader, IReadOnlyDictionary reversedEnumMap) + : base(internalReader, reversedEnumMap) + { + } + + protected override EnumTableColumnDispatcherBase CreateColumnDispatcher(IClickHouseTableColumn column, IReadOnlyDictionary reversedEnumMap) + { + return new EnumTableColumnDispatcher(column, reversedEnumMap); + } + } + + private sealed class EnumTableColumnDispatcher : EnumTableColumnDispatcherBase + { + public EnumTableColumnDispatcher(IClickHouseTableColumn column, IReadOnlyDictionary reversedEnumMap) + : base(column, reversedEnumMap) + { + } + + protected override bool TryMap(IClickHouseEnumConverter enumConverter, sbyte value, string stringValue, out TEnum enumValue) + { + return enumConverter.TryMap(value, stringValue, out enumValue); + } + } } } diff --git a/src/Octonica.ClickHouseClient/Types/EnumTableColumn.cs b/src/Octonica.ClickHouseClient/Types/EnumTableColumn.cs index cd030e6..5119216 100644 --- a/src/Octonica.ClickHouseClient/Types/EnumTableColumn.cs +++ b/src/Octonica.ClickHouseClient/Types/EnumTableColumn.cs @@ -21,18 +21,18 @@ namespace Octonica.ClickHouseClient.Types { - internal sealed class EnumTableColumn : IClickHouseTableColumn - where TValue : struct + internal sealed class EnumTableColumn : IClickHouseTableColumn + where TKey : struct { - private readonly IClickHouseTableColumn _internalColumn; - private readonly IReadOnlyDictionary _reversedEnumMap; + private readonly IClickHouseTableColumn _internalColumn; + private readonly IReadOnlyDictionary _valueMap; public int RowCount => _internalColumn.RowCount; - public EnumTableColumn(IClickHouseTableColumn internalColumn, IReadOnlyDictionary reversedEnumMap) + public EnumTableColumn(IClickHouseTableColumn internalColumn, IReadOnlyDictionary valueMap) { _internalColumn = internalColumn; - _reversedEnumMap = reversedEnumMap; + _valueMap = valueMap; } public bool IsNull(int index) @@ -44,7 +44,7 @@ public bool IsNull(int index) public string GetValue(int index) { var value = _internalColumn.GetValue(index); - if (!_reversedEnumMap.TryGetValue(value, out var strValue)) + if (!_valueMap.TryGetValue(value, out var strValue)) throw new InvalidCastException($"There is no string representation for the value {value} in the enum."); return strValue; @@ -64,4 +64,61 @@ object IClickHouseTableColumn.GetValue(int index) return null; } } + + internal sealed class EnumTableColumn : IClickHouseTableColumn + where TKey : struct + where TEnum : Enum + { + private readonly IClickHouseTableColumn _internalColumn; + private readonly IReadOnlyDictionary _enumMap; + private readonly IReadOnlyDictionary _stringMap; + + public int RowCount => _internalColumn.RowCount; + + public EnumTableColumn(IClickHouseTableColumn internalColumn, IReadOnlyDictionary enumMap, IReadOnlyDictionary stringMap) + { + _internalColumn = internalColumn; + _enumMap = enumMap; + _stringMap = stringMap; + } + + public bool IsNull(int index) + { + Debug.Assert(!_internalColumn.IsNull(index)); + return false; + } + + public TEnum GetValue(int index) + { + var value = _internalColumn.GetValue(index); + if (!_enumMap.TryGetValue(value, out var strValue)) + { + if (_stringMap.TryGetValue(value, out var nativeValue)) + throw new InvalidCastException($"The value '{nativeValue}'={value} of the enum can't be converted to the type '{typeof(Enum).FullName}'."); + + throw new InvalidCastException($"The value {value} doesn't belong to the enum."); + } + + return strValue; + } + + object IClickHouseTableColumn.GetValue(int index) + { + return GetValue(index); + } + + public IClickHouseTableColumn? TryReinterpret() + { + IClickHouseTableColumn? reinterpretedColumn; + if (typeof(T) == typeof(string)) + reinterpretedColumn = (IClickHouseTableColumn) (object) new EnumTableColumn(_internalColumn, _stringMap); + else + reinterpretedColumn = _internalColumn.TryReinterpret(); + + if (reinterpretedColumn == null) + return null; + + return new ReinterpretedTableColumn(this, reinterpretedColumn); + } + } } diff --git a/src/Octonica.ClickHouseClient/Types/EnumTypeInfoBase.cs b/src/Octonica.ClickHouseClient/Types/EnumTypeInfoBase.cs index 97eb027..396c5b6 100644 --- a/src/Octonica.ClickHouseClient/Types/EnumTypeInfoBase.cs +++ b/src/Octonica.ClickHouseClient/Types/EnumTypeInfoBase.cs @@ -78,13 +78,15 @@ public IClickHouseTypeInfo GetGenericArgument(int index) public IClickHouseColumnReader CreateColumnReader(int rowCount) { - if (_reversedEnumMap == null) + if (_enumMap == null || _reversedEnumMap == null) throw new ClickHouseException(ClickHouseErrorCodes.TypeNotFullySpecified, "The list of items is not specified."); var internalReader = CreateInternalColumnReader(rowCount); - return new EnumColumnReader(internalReader, _reversedEnumMap); + return CreateColumnReader(internalReader, _reversedEnumMap); } + protected abstract EnumColumnReaderBase CreateColumnReader(StructureReaderBase internalReader, IReadOnlyDictionary reversedEnumMap); + public IClickHouseColumnWriter CreateColumnWriter(string columnName, IReadOnlyList rows, ClickHouseColumnSettings? columnSettings) { if (_enumMap == null) @@ -140,12 +142,12 @@ public IClickHouseColumnTypeInfo GetDetailedTypeInfo(List> protected abstract bool TryParse(ReadOnlySpan text, out TValue value); - private sealed class EnumColumnReader : IClickHouseColumnReader + protected abstract class EnumColumnReaderBase : IClickHouseColumnReader { private readonly StructureReaderBase _internalReader; private readonly IReadOnlyDictionary _reversedEnumMap; - - public EnumColumnReader(StructureReaderBase internalReader, IReadOnlyDictionary reversedEnumMap) + + public EnumColumnReaderBase(StructureReaderBase internalReader, IReadOnlyDictionary reversedEnumMap) { _internalReader = internalReader; _reversedEnumMap = reversedEnumMap; @@ -164,8 +166,45 @@ public SequenceSize Skip(ReadOnlySequence sequence, int maxElementsCount, public IClickHouseTableColumn EndRead(ClickHouseColumnSettings? settings) { var column = _internalReader.EndRead(); + var enumConverter = settings?.EnumConverter; + if (enumConverter != null) + { + var dispatcher = CreateColumnDispatcher(column, _reversedEnumMap); + return enumConverter.Dispatch(dispatcher); + } + return new EnumTableColumn(column, _reversedEnumMap); } + + protected abstract EnumTableColumnDispatcherBase CreateColumnDispatcher(IClickHouseTableColumn column, IReadOnlyDictionary reversedEnumMap); + } + + protected abstract class EnumTableColumnDispatcherBase : IClickHouseEnumConverterDispatcher + { + private readonly IClickHouseTableColumn _column; + private readonly IReadOnlyDictionary _reversedEnumMap; + + public EnumTableColumnDispatcherBase(IClickHouseTableColumn column, IReadOnlyDictionary reversedEnumMap) + { + _column = column; + _reversedEnumMap = reversedEnumMap; + } + + public IClickHouseTableColumn Dispatch(IClickHouseEnumConverter enumConverter) + where TEnum : Enum + { + var map = new Dictionary(_reversedEnumMap.Count); + foreach (var pair in _reversedEnumMap) + { + if (TryMap(enumConverter, pair.Key, pair.Value, out var enumValue)) + map.Add(pair.Key, enumValue); + } + + return new EnumTableColumn(_column, map, _reversedEnumMap); + } + + protected abstract bool TryMap(IClickHouseEnumConverter enumConverter, TValue value, string stringValue, out TEnum enumValue) + where TEnum : Enum; } } } diff --git a/src/Octonica.ClickHouseClient/Types/IClickHouseEnumConverter.cs b/src/Octonica.ClickHouseClient/Types/IClickHouseEnumConverter.cs new file mode 100644 index 0000000..d106bef --- /dev/null +++ b/src/Octonica.ClickHouseClient/Types/IClickHouseEnumConverter.cs @@ -0,0 +1,40 @@ +#region License Apache 2.0 +/* Copyright 2020 Octonica + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Octonica.ClickHouseClient.Types +{ + public interface IClickHouseEnumConverter + { + public T Dispatch(IClickHouseEnumConverterDispatcher dispatcher); + } + + public interface IClickHouseEnumConverter : IClickHouseEnumConverter + where TEnum : Enum + { + bool TryMap(int value, string stringValue, [NotNullWhen(true)] out TEnum enumValue); + } + + public interface IClickHouseEnumConverterDispatcher + { + public T Dispatch(IClickHouseEnumConverter enumConverter) + where TEnum : Enum; + } +}