diff --git a/DuckDB.NET.Data/DuckDBDataReader.cs b/DuckDB.NET.Data/DuckDBDataReader.cs index f9edb0be..c6eaf136 100644 --- a/DuckDB.NET.Data/DuckDBDataReader.cs +++ b/DuckDB.NET.Data/DuckDBDataReader.cs @@ -1,5 +1,6 @@ using DuckDB.NET.Data.DataChunk.Reader; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Runtime.CompilerServices; @@ -249,7 +250,19 @@ public override int GetOrdinal(string name) return index; } - throw new DuckDBException($"Column with name {name} was not found."); + // IDataRecord.GetOrdinal: "performs a case-sensitive lookup first. If it fails, a second case-insensitive search + // is made. GetOrdinal is kana-width insensitive. If the index of the named field is not found, an + // IndexOutOfRangeException is thrown." + var compareInfo = CultureInfo.InvariantCulture.CompareInfo; + for (var i = 0; i < vectorReaders.Length; i++) + { + if (compareInfo.Compare(vectorReaders[i].ColumnName, name, CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0) + { + return i; + } + } + + throw new IndexOutOfRangeException($"Column with name {name} was not found."); } public override string GetString(int ordinal) diff --git a/DuckDB.NET.Test/DuckDBDataReaderTests.cs b/DuckDB.NET.Test/DuckDBDataReaderTests.cs index 133ff737..b1721b38 100644 --- a/DuckDB.NET.Test/DuckDBDataReaderTests.cs +++ b/DuckDB.NET.Test/DuckDBDataReaderTests.cs @@ -53,7 +53,7 @@ public void GetOrdinalReturnsColumnIndex() reader.GetOrdinal("key").Should().Be(0); reader.GetOrdinal("value").Should().Be(1); - reader.Invoking(dataReader => dataReader.GetOrdinal("Random")).Should().Throw(); + reader.Invoking(dataReader => dataReader.GetOrdinal("Random")).Should().Throw(); } [Fact] @@ -69,7 +69,7 @@ public void GetOrdinalRepeatedColumnReturnsFirstIndex() reader.GetOrdinal("key").Should().Be(1); reader.GetOrdinal("value").Should().Be(0); - reader.Invoking(dataReader => dataReader.GetOrdinal("Random")).Should().Throw(); + reader.Invoking(dataReader => dataReader.GetOrdinal("Random")).Should().Throw(); } [Fact] @@ -819,4 +819,29 @@ public void ReadIntegerAsAnotherIntegerType() reader.Invoking(r => r.GetFieldValue(1)).Should().Throw().WithMessage("Cannot cast from Int32 to UInt64 in column negative"); reader.Invoking(r => r.GetFieldValue(2)).Should().Throw().WithMessage("Cannot cast from Int16 to Byte in column small"); } + + // https://learn.microsoft.com/dotnet/api/system.data.idatarecord.getordinal: "GetOrdinal performs a case-sensitive + // lookup first. If it fails, a second case-insensitive search is made. GetOrdinal is kana-width insensitive. If the + // index of the named field is not found, an IndexOutOfRangeException is thrown." + [Fact] + public void GetOrdinalFallsBackToACaseInsensitiveMatch() + { + Command.CommandText = "SELECT 1 AS Id, 2 AS id, 3 AS \"Name\""; + using var reader = Command.ExecuteReader(); + + reader.GetOrdinal("Id").Should().Be(0); + reader.GetOrdinal("id").Should().Be(1, "an exact match wins over a case-insensitive one"); + reader.GetOrdinal("NAME").Should().Be(2); + reader.GetOrdinal("name").Should().Be(2, "the lookup is width insensitive"); + } + + [Fact] + public void GetOrdinalThrowsIndexOutOfRangeForAnUnknownName() + { + Command.CommandText = "SELECT 1 AS Id"; + using var reader = Command.ExecuteReader(); + + reader.Invoking(r => r.GetOrdinal("missing")).Should().Throw(); + } } +