From 79b536a6bf345fcbd7a89581c93f347af099d8e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 21:18:06 +0000 Subject: [PATCH 1/5] Initial plan From 3d82ae2af4dae228f13b88b888e367f5859e532e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 21:22:39 +0000 Subject: [PATCH 2/5] wip: update enum appender mapping --- .../DataChunk/Reader/EnumVectorDataReader.cs | 43 ++++++++--- .../DataChunk/Writer/EnumVectorDataWriter.cs | 73 ++++++++++++------- DuckDB.NET.Test/DuckDBManagedAppenderTests.cs | 63 ++++++++++++++++ 3 files changed, 141 insertions(+), 38 deletions(-) diff --git a/DuckDB.NET.Data/DataChunk/Reader/EnumVectorDataReader.cs b/DuckDB.NET.Data/DataChunk/Reader/EnumVectorDataReader.cs index e10f87f..d2d7851 100644 --- a/DuckDB.NET.Data/DataChunk/Reader/EnumVectorDataReader.cs +++ b/DuckDB.NET.Data/DataChunk/Reader/EnumVectorDataReader.cs @@ -46,14 +46,14 @@ T ToEnumOrString(TSource enumValue) where TSource: IBinaryNumber(ref enumValue); } } @@ -72,12 +72,12 @@ internal override object GetValue(ulong offset, Type targetType) if (targetType == typeof(string)) { - if (!cachedNames.TryGetValue(enumValue, out var name)) - { - cachedNames[enumValue] = name = NativeMethods.LogicalType.DuckDBEnumDictionaryValue(logicalType, enumValue); - } + return GetEnumName(enumValue); + } - return name; + if (targetType.IsEnum) + { + return ConvertToTargetEnum(enumValue, targetType); } return Enum.ToObject(targetType, enumValue); @@ -91,4 +91,25 @@ public override void Dispose() logicalType.Dispose(); base.Dispose(); } + + private string GetEnumName(long enumValue) + { + if (!cachedNames.TryGetValue(enumValue, out var name)) + { + cachedNames[enumValue] = name = NativeMethods.LogicalType.DuckDBEnumDictionaryValue(logicalType, enumValue); + } + + return name; + } + + private object ConvertToTargetEnum(long enumValue, Type targetType) + { + var enumName = GetEnumName(enumValue); + if (Enum.TryParse(targetType, enumName, true, out var parsedEnum)) + { + return parsedEnum; + } + + return Enum.ToObject(targetType, enumValue); + } } \ No newline at end of file diff --git a/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs b/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs index 787b5d4..3eafd90 100644 --- a/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs +++ b/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs @@ -6,29 +6,14 @@ internal sealed unsafe class EnumVectorDataWriter(IntPtr vector, void* vectorDat private readonly uint enumDictionarySize = NativeMethods.LogicalType.DuckDBEnumDictionarySize(logicalType); - private readonly Dictionary enumValues = []; + private readonly Dictionary enumValues = new(StringComparer.OrdinalIgnoreCase); internal override bool AppendString(string value, ulong rowIndex) { - if (enumValues.Count == 0) - { - for (uint index = 0; index < enumDictionarySize; index++) - { - var enumValueName = NativeMethods.LogicalType.DuckDBEnumDictionaryValue(logicalType, index); - enumValues.Add(enumValueName, index); - } - } - + EnsureEnumValuesInitialized(); if (enumValues.TryGetValue(value, out var enumValue)) { - // The following casts to byte and ushort are safe because we ensure in the constructor that the value enumDictionarySize is not too high. - return enumType switch - { - DuckDBType.UnsignedTinyInt => AppendValueInternal((byte)enumValue, rowIndex), - DuckDBType.UnsignedSmallInt => AppendValueInternal((ushort)enumValue, rowIndex), - DuckDBType.UnsignedInteger => AppendValueInternal(enumValue, rowIndex), - _ => throw new InvalidOperationException($"Failed to write Enum column because the internal enum type must be utinyint, usmallint, or uinteger."), - }; + return AppendEnumValue(enumValue, rowIndex); } throw new InvalidOperationException($"Failed to write Enum column because the value \"{value}\" is not valid."); @@ -36,22 +21,56 @@ internal override bool AppendString(string value, ulong rowIndex) internal override bool AppendEnum(TEnum value, ulong rowIndex) { - var enumValue = ConvertEnumValueToUInt64(value); - if (enumValue < enumDictionarySize) + if (typeof(TEnum).IsDefined(typeof(FlagsAttribute), false)) { - // The following casts to byte, ushort and uint are safe because we ensure in the constructor that the value enumDictionarySize is not too high. - return enumType switch + throw new InvalidOperationException("Failed to write Enum column because [Flags] enums are not supported."); + } + + var enumName = Enum.GetName(value); + if (enumName is not null) + { + EnsureEnumValuesInitialized(); + if (enumValues.TryGetValue(enumName, out var enumValue)) { - DuckDBType.UnsignedTinyInt => AppendValueInternal((byte)enumValue, rowIndex), - DuckDBType.UnsignedSmallInt => AppendValueInternal((ushort)enumValue, rowIndex), - DuckDBType.UnsignedInteger => AppendValueInternal((uint)enumValue, rowIndex), - _ => throw new InvalidOperationException($"Failed to write Enum column because the internal enum type must be utinyint, usmallint, or uinteger."), - }; + return AppendEnumValue(enumValue, rowIndex); + } + } + + var enumOrdinal = ConvertEnumValueToUInt64(value); + if (enumOrdinal < enumDictionarySize) + { + return AppendEnumValue(enumOrdinal, rowIndex); } throw new InvalidOperationException($"Failed to write Enum column because the value is outside the range (0-{enumDictionarySize - 1})."); } + private bool AppendEnumValue(ulong enumValue, ulong rowIndex) + { + // The following casts to byte and ushort are safe because we ensure in the constructor that the enumDictionarySize is not too high. + return enumType switch + { + DuckDBType.UnsignedTinyInt => AppendValueInternal((byte)enumValue, rowIndex), + DuckDBType.UnsignedSmallInt => AppendValueInternal((ushort)enumValue, rowIndex), + DuckDBType.UnsignedInteger => AppendValueInternal((uint)enumValue, rowIndex), + _ => throw new InvalidOperationException("Failed to write Enum column because the internal enum type must be utinyint, usmallint, or uinteger."), + }; + } + + private void EnsureEnumValuesInitialized() + { + if (enumValues.Count != 0) + { + return; + } + + for (uint index = 0; index < enumDictionarySize; index++) + { + var enumValueName = NativeMethods.LogicalType.DuckDBEnumDictionaryValue(logicalType, index); + enumValues.Add(enumValueName, index); + } + } + private static ulong ConvertEnumValueToUInt64(TEnum value) where TEnum : Enum { return value.GetTypeCode() switch diff --git a/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs b/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs index e81d861..621cc31 100644 --- a/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs +++ b/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs @@ -411,6 +411,54 @@ public void EnumValues() result.Item9.Should().Be(TestEnum3.Test6699); } + [Fact] + public void EnumValuesWithNonConsecutiveUnderlyingValues() + { + var enumLabelsSql = string.Join(", ", Enum.GetNames().Select(name => $"'{name}'")); + Command.CommandText = $"CREATE TYPE non_consecutive_test_enum AS ENUM ({enumLabelsSql});"; + Command.ExecuteNonQuery(); + + Command.CommandText = "CREATE TABLE managedAppenderNonConsecutiveEnum(a non_consecutive_test_enum, b non_consecutive_test_enum, c non_consecutive_test_enum);"; + Command.ExecuteNonQuery(); + + using (var appender = Connection.CreateAppender("managedAppenderNonConsecutiveEnum")) + { + appender + .CreateRow() + .AppendValue(NonConsecutiveTestEnum.Happy) + .AppendValue(NonConsecutiveTestEnum.Sad) + .AppendValue(NonConsecutiveTestEnum.Neutral) + .EndRow(); + } + + Command.CommandText = "SELECT a, b, c FROM managedAppenderNonConsecutiveEnum"; + using var reader = Command.ExecuteReader(); + reader.Read(); + reader.GetFieldValue(0).Should().Be(NonConsecutiveTestEnum.Happy); + reader.GetFieldValue(1).Should().Be(nameof(NonConsecutiveTestEnum.Sad)); + reader.GetFieldValue(2).Should().Be(NonConsecutiveTestEnum.Neutral); + } + + [Fact] + public void FlagsEnumValuesThrowException() + { + var enumLabelsSql = string.Join(", ", Enum.GetNames().Select(name => $"'{name}'")); + Command.CommandText = $"CREATE TYPE flags_test_enum AS ENUM ({enumLabelsSql});"; + Command.ExecuteNonQuery(); + + Command.CommandText = "CREATE TABLE managedAppenderFlagsEnum(a flags_test_enum);"; + Command.ExecuteNonQuery(); + + Connection.Invoking(dbConnection => + { + using var appender = dbConnection.CreateAppender("managedAppenderFlagsEnum"); + appender + .CreateRow() + .AppendValue(FlagsTestEnum.Happy) + .EndRow(); + }).Should().Throw().Where(exception => exception.Message.Contains("Flags")); + } + [Fact] public void IncompleteRowThrowsException() { @@ -851,4 +899,19 @@ private enum EnumNotValidValueTestEnum { NotValid = 12345, } + + private enum NonConsecutiveTestEnum : byte + { + Happy = 1, + Sad = 2, + Neutral = 4, + } + + [Flags] + private enum FlagsTestEnum : byte + { + Happy = 1, + Sad = 2, + Neutral = 4, + } } \ No newline at end of file From 37862c69b84392e18e9642e02167d969064d8d87 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 21:24:12 +0000 Subject: [PATCH 3/5] fix: map enums by label when appending --- DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs b/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs index 3eafd90..782efd3 100644 --- a/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs +++ b/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs @@ -21,12 +21,13 @@ internal override bool AppendString(string value, ulong rowIndex) internal override bool AppendEnum(TEnum value, ulong rowIndex) { - if (typeof(TEnum).IsDefined(typeof(FlagsAttribute), false)) + var enumValueType = value.GetType(); + if (enumValueType.IsDefined(typeof(FlagsAttribute), false)) { throw new InvalidOperationException("Failed to write Enum column because [Flags] enums are not supported."); } - var enumName = Enum.GetName(value); + var enumName = Enum.GetName(enumValueType, value); if (enumName is not null) { EnsureEnumValuesInitialized(); From 6cf90ff1a65883bfa19ecb6b3e9b01c11fae2a36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 21:26:33 +0000 Subject: [PATCH 4/5] refine enum writer validation --- .../DataChunk/Writer/EnumVectorDataWriter.cs | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs b/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs index 782efd3..6b0b450 100644 --- a/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs +++ b/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs @@ -37,13 +37,7 @@ internal override bool AppendEnum(TEnum value, ulong rowIndex) } } - var enumOrdinal = ConvertEnumValueToUInt64(value); - if (enumOrdinal < enumDictionarySize) - { - return AppendEnumValue(enumOrdinal, rowIndex); - } - - throw new InvalidOperationException($"Failed to write Enum column because the value is outside the range (0-{enumDictionarySize - 1})."); + throw new InvalidOperationException($"Failed to write Enum column because the value \"{value}\" is not valid."); } private bool AppendEnumValue(ulong enumValue, ulong rowIndex) @@ -72,20 +66,4 @@ private void EnsureEnumValuesInitialized() } } - private static ulong ConvertEnumValueToUInt64(TEnum value) where TEnum : Enum - { - return value.GetTypeCode() switch - { - TypeCode.SByte => (ulong)Convert.ToSByte(value), - TypeCode.Byte => Convert.ToByte(value), - TypeCode.Int16 => (ulong)Convert.ToInt16(value), - TypeCode.UInt16 => Convert.ToUInt16(value), - TypeCode.Int32 => (ulong)Convert.ToInt32(value), - TypeCode.UInt32 => Convert.ToUInt32(value), - TypeCode.Int64 => (ulong)Convert.ToInt64(value), - TypeCode.UInt64 => Convert.ToUInt64(value), - _ => throw new InvalidOperationException($"Failed to convert the enum value {value} to ulong."), - }; - } - } From 30360d2baaa2e370df8eeeab5edc5df5160037ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 21:28:46 +0000 Subject: [PATCH 5/5] tighten enum label conversions --- .../DataChunk/Reader/EnumVectorDataReader.cs | 2 +- .../DataChunk/Writer/EnumVectorDataWriter.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/DuckDB.NET.Data/DataChunk/Reader/EnumVectorDataReader.cs b/DuckDB.NET.Data/DataChunk/Reader/EnumVectorDataReader.cs index d2d7851..79d12d4 100644 --- a/DuckDB.NET.Data/DataChunk/Reader/EnumVectorDataReader.cs +++ b/DuckDB.NET.Data/DataChunk/Reader/EnumVectorDataReader.cs @@ -110,6 +110,6 @@ private object ConvertToTargetEnum(long enumValue, Type targetType) return parsedEnum; } - return Enum.ToObject(targetType, enumValue); + throw new InvalidCastException($"Cannot convert DuckDB enum value \"{enumName}\" to {targetType.Name}."); } } \ No newline at end of file diff --git a/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs b/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs index 6b0b450..45544a8 100644 --- a/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs +++ b/DuckDB.NET.Data/DataChunk/Writer/EnumVectorDataWriter.cs @@ -13,7 +13,7 @@ internal override bool AppendString(string value, ulong rowIndex) EnsureEnumValuesInitialized(); if (enumValues.TryGetValue(value, out var enumValue)) { - return AppendEnumValue(enumValue, rowIndex); + return AppendEnumDictionaryIndex(enumValue, rowIndex); } throw new InvalidOperationException($"Failed to write Enum column because the value \"{value}\" is not valid."); @@ -33,21 +33,21 @@ internal override bool AppendEnum(TEnum value, ulong rowIndex) EnsureEnumValuesInitialized(); if (enumValues.TryGetValue(enumName, out var enumValue)) { - return AppendEnumValue(enumValue, rowIndex); + return AppendEnumDictionaryIndex(enumValue, rowIndex); } } throw new InvalidOperationException($"Failed to write Enum column because the value \"{value}\" is not valid."); } - private bool AppendEnumValue(ulong enumValue, ulong rowIndex) + private bool AppendEnumDictionaryIndex(ulong dictionaryIndex, ulong rowIndex) { // The following casts to byte and ushort are safe because we ensure in the constructor that the enumDictionarySize is not too high. return enumType switch { - DuckDBType.UnsignedTinyInt => AppendValueInternal((byte)enumValue, rowIndex), - DuckDBType.UnsignedSmallInt => AppendValueInternal((ushort)enumValue, rowIndex), - DuckDBType.UnsignedInteger => AppendValueInternal((uint)enumValue, rowIndex), + DuckDBType.UnsignedTinyInt => AppendValueInternal((byte)dictionaryIndex, rowIndex), + DuckDBType.UnsignedSmallInt => AppendValueInternal((ushort)dictionaryIndex, rowIndex), + DuckDBType.UnsignedInteger => AppendValueInternal((uint)dictionaryIndex, rowIndex), _ => throw new InvalidOperationException("Failed to write Enum column because the internal enum type must be utinyint, usmallint, or uinteger."), }; }