From 9f2f0ce5a6233c0254c3cbc665022bc823084664 Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Wed, 14 Oct 2026 18:08:25 -0500 Subject: [PATCH 1/6] Support ADO.NET schema data types and metadata probing --- DuckDB.NET.Data/DuckDBSchema.cs | 156 ++++++++++++++++++++++++++++++-- DuckDB.NET.Test/SchemaTests.cs | 42 ++++++++- 2 files changed, 190 insertions(+), 8 deletions(-) diff --git a/DuckDB.NET.Data/DuckDBSchema.cs b/DuckDB.NET.Data/DuckDBSchema.cs index 63e0e98b..f0395106 100644 --- a/DuckDB.NET.Data/DuckDBSchema.cs +++ b/DuckDB.NET.Data/DuckDBSchema.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Text; +using DuckDB.NET.Native; namespace DuckDB.NET.Data; @@ -13,10 +14,14 @@ internal static class DuckDBSchema private static readonly string[] IndexesRestrictions = ["index_catalog", "index_schema", "table_name", "index_name"]; - public static DataTable GetSchema(DuckDBConnection connection, string collectionName, string?[]? restrictionValues) => - collectionName.ToUpperInvariant() switch + public static DataTable GetSchema(DuckDBConnection connection, string collectionName, string?[]? restrictionValues) + { + ValidateRestrictions(collectionName, restrictionValues); + return collectionName.ToUpperInvariant() switch { "METADATACOLLECTIONS" => GetMetaDataCollections(), + "DATASOURCEINFORMATION" => GetDataSourceInformation(connection), + "DATATYPES" => GetDataTypes(), "RESTRICTIONS" => GetRestrictions(), "RESERVEDWORDS" => GetReservedWords(connection), "TABLES" => GetTables(connection, restrictionValues), @@ -25,6 +30,17 @@ public static DataTable GetSchema(DuckDBConnection connection, string collection "INDEXES" => GetIndexes(connection, restrictionValues), _ => throw new ArgumentOutOfRangeException(nameof(collectionName), collectionName, "Invalid collection name.") }; + } + + private static void ValidateRestrictions(string collectionName, string?[]? restrictionValues) + { + if (restrictionValues is { Length: > 0 } + && collectionName.ToUpperInvariant() is "METADATACOLLECTIONS" or "DATASOURCEINFORMATION" or "DATATYPES" or "RESTRICTIONS" or "RESERVEDWORDS") + { + throw new ArgumentException("This collection does not support restrictions.", nameof(restrictionValues)); + } + + } private static DataTable GetMetaDataCollections() => new(DbMetaDataCollectionNames.MetaDataCollections) @@ -37,9 +53,11 @@ private static DataTable GetMetaDataCollections() => }, Rows = { + { DbMetaDataCollectionNames.DataSourceInformation, 0, 0 }, + { DbMetaDataCollectionNames.DataTypes, 0, 0 }, { DbMetaDataCollectionNames.MetaDataCollections, 0, 0 }, - { DbMetaDataCollectionNames.Restrictions, 0, 0 }, { DbMetaDataCollectionNames.ReservedWords, 0, 0 }, + { DbMetaDataCollectionNames.Restrictions, 0, 0 }, { DuckDbMetaDataCollectionNames.Tables, TableRestrictions.Length, 3 }, { DuckDbMetaDataCollectionNames.Columns, ColumnRestrictions.Length, 4 }, { DuckDbMetaDataCollectionNames.ForeignKeys, ForeignKeyRestrictions.Length, 3 }, @@ -47,6 +65,130 @@ private static DataTable GetMetaDataCollections() => } }; + private static DataTable GetDataSourceInformation(DuckDBConnection connection) => + new(DbMetaDataCollectionNames.DataSourceInformation) + { + Columns = + { + { DbMetaDataColumnNames.CompositeIdentifierSeparatorPattern, typeof(string) }, + { DbMetaDataColumnNames.DataSourceProductName, typeof(string) }, + { DbMetaDataColumnNames.DataSourceProductVersion, typeof(string) }, + { DbMetaDataColumnNames.DataSourceProductVersionNormalized, typeof(string) }, + { DbMetaDataColumnNames.GroupByBehavior, typeof(int) }, + { DbMetaDataColumnNames.IdentifierCase, typeof(int) }, + { DbMetaDataColumnNames.IdentifierPattern, typeof(string) }, + { DbMetaDataColumnNames.OrderByColumnsInSelect, typeof(bool) }, + { DbMetaDataColumnNames.ParameterMarkerFormat, typeof(string) }, + { DbMetaDataColumnNames.ParameterMarkerPattern, typeof(string) }, + { DbMetaDataColumnNames.ParameterNameMaxLength, typeof(int) }, + { DbMetaDataColumnNames.ParameterNamePattern, typeof(string) }, + { DbMetaDataColumnNames.QuotedIdentifierCase, typeof(int) }, + { DbMetaDataColumnNames.QuotedIdentifierPattern, typeof(string) }, + { DbMetaDataColumnNames.StatementSeparatorPattern, typeof(string) }, + { DbMetaDataColumnNames.StringLiteralPattern, typeof(string) }, + { DbMetaDataColumnNames.SupportedJoinOperators, typeof(int) } + }, + Rows = + { + { + @"\.", "DuckDB", connection.ServerVersion, NormalizeVersion(connection.ServerVersion), + (int)GroupByBehavior.MustContainAll, + (int)IdentifierCase.Insensitive, + @"^[\p{L}_][\p{L}\p{N}_$]*$", false, + "${0}", @"(\$[\p{L}_][\p{L}\p{N}_]*)", DBNull.Value, + @"^[\p{L}_][\p{L}\p{N}_$]*$", (int)IdentifierCase.Insensitive, + "^(([^\\\"]|\\\"\\\")*)$", ";", @"'(([^']|'')*)'", (int)(SupportedJoinOperators.Inner | SupportedJoinOperators.LeftOuter + | SupportedJoinOperators.RightOuter | SupportedJoinOperators.FullOuter) + } + } + }; + + private static DataTable GetDataTypes() + { + var table = new DataTable(DbMetaDataCollectionNames.DataTypes); + foreach (var column in new (string Name, Type Type)[] + { + (DbMetaDataColumnNames.TypeName, typeof(string)), (DbMetaDataColumnNames.ProviderDbType, typeof(int)), + (DbMetaDataColumnNames.DataType, typeof(string)), (DbMetaDataColumnNames.ColumnSize, typeof(long)), + (DbMetaDataColumnNames.CreateFormat, typeof(string)), (DbMetaDataColumnNames.CreateParameters, typeof(string)), + (DbMetaDataColumnNames.IsAutoIncrementable, typeof(bool)), (DbMetaDataColumnNames.IsBestMatch, typeof(bool)), + (DbMetaDataColumnNames.IsCaseSensitive, typeof(bool)), (DbMetaDataColumnNames.IsConcurrencyType, typeof(bool)), + (DbMetaDataColumnNames.IsFixedLength, typeof(bool)), (DbMetaDataColumnNames.IsFixedPrecisionScale, typeof(bool)), + (DbMetaDataColumnNames.IsLiteralSupported, typeof(bool)), (DbMetaDataColumnNames.IsLong, typeof(bool)), + (DbMetaDataColumnNames.IsNullable, typeof(bool)), (DbMetaDataColumnNames.IsSearchable, typeof(bool)), + (DbMetaDataColumnNames.IsSearchableWithLike, typeof(bool)), (DbMetaDataColumnNames.IsUnsigned, typeof(bool)), + (DbMetaDataColumnNames.LiteralPrefix, typeof(string)), (DbMetaDataColumnNames.LiteralSuffix, typeof(string)), + (DbMetaDataColumnNames.MaximumScale, typeof(short)), (DbMetaDataColumnNames.MinimumScale, typeof(short)), + ("NativeDataType", typeof(string)) + }) + { + table.Columns.Add(column.Name, column.Type); + } + + AddType(table, "BOOLEAN", DuckDBType.Boolean, typeof(bool), isBestMatch: true); + AddType(table, "TINYINT", DuckDBType.TinyInt, typeof(sbyte), isBestMatch: true); + AddType(table, "SMALLINT", DuckDBType.SmallInt, typeof(short), isBestMatch: true); + AddType(table, "INTEGER", DuckDBType.Integer, typeof(int), isBestMatch: true); + AddType(table, "BIGINT", DuckDBType.BigInt, typeof(long), isBestMatch: true); + AddType(table, "UTINYINT", DuckDBType.UnsignedTinyInt, typeof(byte), isUnsigned: true, isBestMatch: true); + AddType(table, "USMALLINT", DuckDBType.UnsignedSmallInt, typeof(ushort), isUnsigned: true, isBestMatch: true); + AddType(table, "UINTEGER", DuckDBType.UnsignedInteger, typeof(uint), isUnsigned: true, isBestMatch: true); + AddType(table, "UBIGINT", DuckDBType.UnsignedBigInt, typeof(ulong), isUnsigned: true, isBestMatch: true); + AddType(table, "FLOAT", DuckDBType.Float, typeof(float), isBestMatch: true); + AddType(table, "DOUBLE", DuckDBType.Double, typeof(double), isBestMatch: true); + AddType(table, "DECIMAL", DuckDBType.Decimal, typeof(decimal), createFormat: "DECIMAL({0},{1})", createParameters: "precision,scale", isBestMatch: true); + AddType(table, "VARCHAR", DuckDBType.Varchar, typeof(string), long.MaxValue, isCaseSensitive: true, isBestMatch: true); + AddType(table, "BLOB", DuckDBType.Blob, typeof(byte[]), long.MaxValue, isLong: true, isBestMatch: true); + AddType(table, "UUID", DuckDBType.Uuid, typeof(Guid), isBestMatch: true); + AddType(table, "DATE", DuckDBType.Date, typeof(DateOnly), isBestMatch: true); + AddType(table, "TIME", DuckDBType.Time, typeof(TimeOnly), isBestMatch: true); + AddType(table, "TIMETZ", DuckDBType.TimeTz, typeof(DateTimeOffset), isBestMatch: true); + AddType(table, "TIMESTAMP", DuckDBType.Timestamp, typeof(DateTime), isBestMatch: true); + AddType(table, "TIMESTAMPTZ", DuckDBType.TimestampTz, typeof(DateTimeOffset), isBestMatch: true); + AddType(table, "INTERVAL", DuckDBType.Interval, typeof(TimeSpan), isBestMatch: true); + AddType(table, "HUGEINT", DuckDBType.HugeInt, typeof(System.Numerics.BigInteger), isBestMatch: true); + AddType(table, "UHUGEINT", DuckDBType.UnsignedHugeInt, typeof(System.Numerics.BigInteger), isUnsigned: true); + AddType(table, "VARINT", DuckDBType.VarInt, typeof(System.Numerics.BigInteger)); + AddType(table, "LIST", DuckDBType.List, typeof(object)); + AddType(table, "ARRAY", DuckDBType.Array, typeof(object)); + AddType(table, "STRUCT", DuckDBType.Struct, typeof(object)); + AddType(table, "MAP", DuckDBType.Map, typeof(object)); + AddType(table, "UNION", DuckDBType.Union, typeof(object)); + AddType(table, "ENUM", DuckDBType.Enum, typeof(string)); + AddType(table, "BIT", DuckDBType.Bit, typeof(string)); + + return table; + } + + private static void AddType( + DataTable table, + string typeName, + DuckDBType providerType, + Type dataType, + long columnSize = -1, + string? createFormat = null, + string? createParameters = null, + bool isUnsigned = false, + bool isCaseSensitive = false, + bool isLong = false, + bool isBestMatch = false) + { + table.Rows.Add( + typeName, (int)providerType, dataType.FullName!, columnSize, + createFormat ?? (object)DBNull.Value, createParameters ?? (object)DBNull.Value, + false, isBestMatch, isCaseSensitive, false, false, false, true, isLong, + true, true, isCaseSensitive, isUnsigned ? (object)true : DBNull.Value, + DBNull.Value, DBNull.Value, DBNull.Value, DBNull.Value, typeName); + } + + private static string NormalizeVersion(string serverVersion) + { + var value = serverVersion.TrimStart('v', 'V'); + return Version.TryParse(value, out var version) + ? $"{version.Major:00}.{version.Minor:000}.{(version.Build < 0 ? 0 : version.Build):0000}.{(version.Revision < 0 ? 0 : version.Revision):0000}" + : serverVersion; + } + private static DataTable GetRestrictions() => new(DbMetaDataCollectionNames.Restrictions) { @@ -74,10 +216,10 @@ private static DataTable GetRestrictions() => { DuckDbMetaDataCollectionNames.ForeignKeys, "Table", "table_name", 3 }, { DuckDbMetaDataCollectionNames.ForeignKeys, "Constraint", "constraint_name", 4 }, - { DuckDbMetaDataCollectionNames.Indexes, "Catalog", "constraint_catalog", 1 }, - { DuckDbMetaDataCollectionNames.Indexes, "Schema", "constraint_schema", 2 }, + { DuckDbMetaDataCollectionNames.Indexes, "Catalog", "index_catalog", 1 }, + { DuckDbMetaDataCollectionNames.Indexes, "Schema", "index_schema", 2 }, { DuckDbMetaDataCollectionNames.Indexes, "Table", "table_name", 3 }, - { DuckDbMetaDataCollectionNames.Indexes, "Constraint", "constraint_name", 4 }, + { DuckDbMetaDataCollectionNames.Indexes, "Index", "index_name", 4 }, }, }; @@ -218,4 +360,4 @@ private static DataTable GetDataTable(string tableName, DuckDBCommand command) return table; } -} \ No newline at end of file +} diff --git a/DuckDB.NET.Test/SchemaTests.cs b/DuckDB.NET.Test/SchemaTests.cs index f9c2a4f4..e34885dd 100644 --- a/DuckDB.NET.Test/SchemaTests.cs +++ b/DuckDB.NET.Test/SchemaTests.cs @@ -20,6 +20,17 @@ public void InvalidCollection() { Assert.Throws(() => Connection.GetSchema("Invalid")); } + + [Theory] + [InlineData("MetaDataCollections")] + [InlineData("DataSourceInformation")] + [InlineData("DataTypes")] + [InlineData("Restrictions")] + [InlineData("ReservedWords")] + public void CollectionsWithoutRestrictionsRejectRestrictionValues(string collectionName) + { + Assert.Throws(() => Connection.GetSchema(collectionName, ["unexpected"])); + } [Fact] public void MetaDataCollections() @@ -119,6 +130,35 @@ public void ColumnsWithRestrictions() Assert.Equal("foo_id", schema.Rows[0]["column_name"]); } + [Fact] + public void ColumnsExposeDataType() + { + var schema = Connection.GetSchema("Columns", [null, null, "foo", "foo_id"]); + Assert.Equal("INTEGER", schema.Rows[0]["data_type"]); + } + + [Fact] + public void DataSourceInformationSupportsProbing() + { + var schema = Connection.GetSchema(DbMetaDataCollectionNames.DataSourceInformation); + var row = Assert.IsType(Assert.Single(schema.Rows)); + Assert.Equal("DuckDB", row["DataSourceProductName"]); + Assert.Equal(Connection.ServerVersion, row["DataSourceProductVersion"]); + Assert.Equal("${0}", row["ParameterMarkerFormat"]); + } + + [Fact] + public void DataTypesExposeStandardAdoNetMetadata() + { + var schema = Connection.GetSchema("DataTypes"); + var integer = schema.Rows.Cast().Single(row => (string)row["TypeName"] == "INTEGER"); + + Assert.Equal(typeof(int).FullName, integer["DataType"]); + Assert.Equal((int)DuckDBType.Integer, integer["ProviderDbType"]); + Assert.True((bool)integer["IsBestMatch"]); + Assert.True((bool)integer["IsSearchable"]); + } + [Fact] public void ColumnsTooManyRestrictions() { @@ -173,4 +213,4 @@ public void IndexesTooManyRestrictions() private static IEnumerable GetValues(DataTable schema, string columnName) => schema.Rows.Cast().Select(r => (string)r[columnName]); -} \ No newline at end of file +} From 2001f3b8b5cab2e88fd5f98c597e97aa8b7f050c Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Mon, 19 Oct 2026 18:35:25 -0500 Subject: [PATCH 2/6] Query DuckDB catalog for schema data types --- DuckDB.NET.Data/DuckDBSchema.cs | 129 +++++++++++++++++++++++--------- DuckDB.NET.Test/SchemaTests.cs | 16 ++++ 2 files changed, 110 insertions(+), 35 deletions(-) diff --git a/DuckDB.NET.Data/DuckDBSchema.cs b/DuckDB.NET.Data/DuckDBSchema.cs index f0395106..e733ca4e 100644 --- a/DuckDB.NET.Data/DuckDBSchema.cs +++ b/DuckDB.NET.Data/DuckDBSchema.cs @@ -21,7 +21,7 @@ public static DataTable GetSchema(DuckDBConnection connection, string collection { "METADATACOLLECTIONS" => GetMetaDataCollections(), "DATASOURCEINFORMATION" => GetDataSourceInformation(connection), - "DATATYPES" => GetDataTypes(), + "DATATYPES" => GetDataTypes(connection), "RESTRICTIONS" => GetRestrictions(), "RESERVEDWORDS" => GetReservedWords(connection), "TABLES" => GetTables(connection, restrictionValues), @@ -103,7 +103,7 @@ private static DataTable GetDataSourceInformation(DuckDBConnection connection) = } }; - private static DataTable GetDataTypes() + private static DataTable GetDataTypes(DuckDBConnection connection) { var table = new DataTable(DbMetaDataCollectionNames.DataTypes); foreach (var column in new (string Name, Type Type)[] @@ -125,41 +125,99 @@ private static DataTable GetDataTypes() table.Columns.Add(column.Name, column.Type); } - AddType(table, "BOOLEAN", DuckDBType.Boolean, typeof(bool), isBestMatch: true); - AddType(table, "TINYINT", DuckDBType.TinyInt, typeof(sbyte), isBestMatch: true); - AddType(table, "SMALLINT", DuckDBType.SmallInt, typeof(short), isBestMatch: true); - AddType(table, "INTEGER", DuckDBType.Integer, typeof(int), isBestMatch: true); - AddType(table, "BIGINT", DuckDBType.BigInt, typeof(long), isBestMatch: true); - AddType(table, "UTINYINT", DuckDBType.UnsignedTinyInt, typeof(byte), isUnsigned: true, isBestMatch: true); - AddType(table, "USMALLINT", DuckDBType.UnsignedSmallInt, typeof(ushort), isUnsigned: true, isBestMatch: true); - AddType(table, "UINTEGER", DuckDBType.UnsignedInteger, typeof(uint), isUnsigned: true, isBestMatch: true); - AddType(table, "UBIGINT", DuckDBType.UnsignedBigInt, typeof(ulong), isUnsigned: true, isBestMatch: true); - AddType(table, "FLOAT", DuckDBType.Float, typeof(float), isBestMatch: true); - AddType(table, "DOUBLE", DuckDBType.Double, typeof(double), isBestMatch: true); - AddType(table, "DECIMAL", DuckDBType.Decimal, typeof(decimal), createFormat: "DECIMAL({0},{1})", createParameters: "precision,scale", isBestMatch: true); - AddType(table, "VARCHAR", DuckDBType.Varchar, typeof(string), long.MaxValue, isCaseSensitive: true, isBestMatch: true); - AddType(table, "BLOB", DuckDBType.Blob, typeof(byte[]), long.MaxValue, isLong: true, isBestMatch: true); - AddType(table, "UUID", DuckDBType.Uuid, typeof(Guid), isBestMatch: true); - AddType(table, "DATE", DuckDBType.Date, typeof(DateOnly), isBestMatch: true); - AddType(table, "TIME", DuckDBType.Time, typeof(TimeOnly), isBestMatch: true); - AddType(table, "TIMETZ", DuckDBType.TimeTz, typeof(DateTimeOffset), isBestMatch: true); - AddType(table, "TIMESTAMP", DuckDBType.Timestamp, typeof(DateTime), isBestMatch: true); - AddType(table, "TIMESTAMPTZ", DuckDBType.TimestampTz, typeof(DateTimeOffset), isBestMatch: true); - AddType(table, "INTERVAL", DuckDBType.Interval, typeof(TimeSpan), isBestMatch: true); - AddType(table, "HUGEINT", DuckDBType.HugeInt, typeof(System.Numerics.BigInteger), isBestMatch: true); - AddType(table, "UHUGEINT", DuckDBType.UnsignedHugeInt, typeof(System.Numerics.BigInteger), isUnsigned: true); - AddType(table, "VARINT", DuckDBType.VarInt, typeof(System.Numerics.BigInteger)); - AddType(table, "LIST", DuckDBType.List, typeof(object)); - AddType(table, "ARRAY", DuckDBType.Array, typeof(object)); - AddType(table, "STRUCT", DuckDBType.Struct, typeof(object)); - AddType(table, "MAP", DuckDBType.Map, typeof(object)); - AddType(table, "UNION", DuckDBType.Union, typeof(object)); - AddType(table, "ENUM", DuckDBType.Enum, typeof(string)); - AddType(table, "BIT", DuckDBType.Bit, typeof(string)); + using var command = connection.CreateCommand(); + command.CommandText = "SELECT type_name, logical_type FROM duckdb_types() ORDER BY type_name"; + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + var typeName = reader.GetString(0); + var logicalTypeName = reader.GetString(1); + var providerType = ParseDuckDBType(logicalTypeName); + + AddType( + table, typeName, providerType, GetClrType(providerType), + createFormat: providerType == DuckDBType.Decimal ? "DECIMAL({0},{1})" : null, + createParameters: providerType == DuckDBType.Decimal ? "precision,scale" : null, + isUnsigned: providerType is DuckDBType.UnsignedTinyInt or DuckDBType.UnsignedSmallInt + or DuckDBType.UnsignedInteger or DuckDBType.UnsignedBigInt or DuckDBType.UnsignedHugeInt, + isCaseSensitive: providerType == DuckDBType.Varchar, + isLong: providerType == DuckDBType.Blob, + isBestMatch: typeName.Equals(logicalTypeName, StringComparison.OrdinalIgnoreCase), + nativeTypeName: logicalTypeName); + } return table; } + private static DuckDBType ParseDuckDBType(string typeName) => typeName.ToUpperInvariant().Replace(" WITH TIME ZONE", "_TZ") switch + { + "BOOLEAN" => DuckDBType.Boolean, + "TINYINT" => DuckDBType.TinyInt, + "SMALLINT" => DuckDBType.SmallInt, + "INTEGER" => DuckDBType.Integer, + "BIGINT" => DuckDBType.BigInt, + "UTINYINT" => DuckDBType.UnsignedTinyInt, + "USMALLINT" => DuckDBType.UnsignedSmallInt, + "UINTEGER" => DuckDBType.UnsignedInteger, + "UBIGINT" => DuckDBType.UnsignedBigInt, + "FLOAT" => DuckDBType.Float, + "DOUBLE" => DuckDBType.Double, + "DECIMAL" => DuckDBType.Decimal, + "VARCHAR" => DuckDBType.Varchar, + "BLOB" => DuckDBType.Blob, + "UUID" => DuckDBType.Uuid, + "DATE" => DuckDBType.Date, + "TIME" => DuckDBType.Time, + "TIME_TZ" => DuckDBType.TimeTz, + "TIMESTAMP" => DuckDBType.Timestamp, + "TIMESTAMP_S" => DuckDBType.TimestampS, + "TIMESTAMP_MS" => DuckDBType.TimestampMs, + "TIMESTAMP_NS" => DuckDBType.TimestampNs, + "TIMESTAMP_TZ" => DuckDBType.TimestampTz, + "INTERVAL" => DuckDBType.Interval, + "HUGEINT" => DuckDBType.HugeInt, + "UHUGEINT" => DuckDBType.UnsignedHugeInt, + "VARINT" => DuckDBType.VarInt, + "LIST" => DuckDBType.List, + "ARRAY" => DuckDBType.Array, + "STRUCT" => DuckDBType.Struct, + "MAP" => DuckDBType.Map, + "UNION" => DuckDBType.Union, + "ENUM" => DuckDBType.Enum, + "BIT" => DuckDBType.Bit, + "ANY" => DuckDBType.Any, + "SQLNULL" => DuckDBType.SqlNull, + _ => DuckDBType.Invalid + }; + + private static Type GetClrType(DuckDBType type) => type switch + { + DuckDBType.Boolean => typeof(bool), + DuckDBType.TinyInt => typeof(sbyte), + DuckDBType.SmallInt => typeof(short), + DuckDBType.Integer => typeof(int), + DuckDBType.BigInt => typeof(long), + DuckDBType.UnsignedTinyInt => typeof(byte), + DuckDBType.UnsignedSmallInt => typeof(ushort), + DuckDBType.UnsignedInteger => typeof(uint), + DuckDBType.UnsignedBigInt => typeof(ulong), + DuckDBType.Float => typeof(float), + DuckDBType.Double => typeof(double), + DuckDBType.Decimal => typeof(decimal), + DuckDBType.Varchar => typeof(string), + DuckDBType.Blob => typeof(byte[]), + DuckDBType.Uuid => typeof(Guid), + DuckDBType.Date => typeof(DateOnly), + DuckDBType.Time => typeof(TimeOnly), + DuckDBType.TimeTz or DuckDBType.TimestampTz => typeof(DateTimeOffset), + DuckDBType.Timestamp or DuckDBType.TimestampS or DuckDBType.TimestampMs or DuckDBType.TimestampNs => typeof(DateTime), + DuckDBType.Interval => typeof(TimeSpan), + DuckDBType.HugeInt or DuckDBType.UnsignedHugeInt or DuckDBType.VarInt => typeof(System.Numerics.BigInteger), + DuckDBType.Enum or DuckDBType.Bit => typeof(string), + DuckDBType.SqlNull => typeof(DBNull), + _ => typeof(object) + }; + private static void AddType( DataTable table, string typeName, @@ -171,14 +229,15 @@ private static void AddType( bool isUnsigned = false, bool isCaseSensitive = false, bool isLong = false, - bool isBestMatch = false) + bool isBestMatch = false, + string? nativeTypeName = null) { table.Rows.Add( typeName, (int)providerType, dataType.FullName!, columnSize, createFormat ?? (object)DBNull.Value, createParameters ?? (object)DBNull.Value, false, isBestMatch, isCaseSensitive, false, false, false, true, isLong, true, true, isCaseSensitive, isUnsigned ? (object)true : DBNull.Value, - DBNull.Value, DBNull.Value, DBNull.Value, DBNull.Value, typeName); + DBNull.Value, DBNull.Value, DBNull.Value, DBNull.Value, nativeTypeName ?? typeName); } private static string NormalizeVersion(string serverVersion) diff --git a/DuckDB.NET.Test/SchemaTests.cs b/DuckDB.NET.Test/SchemaTests.cs index e34885dd..6a9202c3 100644 --- a/DuckDB.NET.Test/SchemaTests.cs +++ b/DuckDB.NET.Test/SchemaTests.cs @@ -159,6 +159,22 @@ public void DataTypesExposeStandardAdoNetMetadata() Assert.True((bool)integer["IsSearchable"]); } + [Fact] + public void DataTypesExposeEveryDuckDbType() + { + var schema = Connection.GetSchema("DataTypes"); + var providerTypes = schema.Rows.Cast() + .Select(row => (DuckDBType)(int)row["ProviderDbType"]) + .ToHashSet(); + + var expectedTypes = Enum.GetValues() + .Where(type => type is not (DuckDBType.Invalid or DuckDBType.Any or DuckDBType.VarInt or DuckDBType.SqlNull)) + .ToHashSet(); + + Assert.True(expectedTypes.IsSubsetOf(providerTypes), + $"Missing: {string.Join(", ", expectedTypes.Except(providerTypes))}"); + } + [Fact] public void ColumnsTooManyRestrictions() { From f9b625f96c68a612691527400fac7e14273b68e0 Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Mon, 19 Oct 2026 18:37:00 -0500 Subject: [PATCH 3/6] Deduplicate catalog data type rows --- DuckDB.NET.Data/DuckDBSchema.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDB.NET.Data/DuckDBSchema.cs b/DuckDB.NET.Data/DuckDBSchema.cs index e733ca4e..8774c93c 100644 --- a/DuckDB.NET.Data/DuckDBSchema.cs +++ b/DuckDB.NET.Data/DuckDBSchema.cs @@ -126,7 +126,7 @@ private static DataTable GetDataTypes(DuckDBConnection connection) } using var command = connection.CreateCommand(); - command.CommandText = "SELECT type_name, logical_type FROM duckdb_types() ORDER BY type_name"; + command.CommandText = "SELECT DISTINCT type_name, logical_type FROM duckdb_types() ORDER BY type_name"; using var reader = command.ExecuteReader(); while (reader.Read()) { From d20770cb9a0dbd60cd16508e024b46b67a7a88ff Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Tue, 20 Oct 2026 08:52:41 -0500 Subject: [PATCH 4/6] Fix DuckDB DataTypes mappings --- DuckDB.NET.Data/DuckDBSchema.cs | 28 +++++++++++++++++++++++----- DuckDB.NET.Test/SchemaTests.cs | 16 +++++++++++++++- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/DuckDB.NET.Data/DuckDBSchema.cs b/DuckDB.NET.Data/DuckDBSchema.cs index 8774c93c..40cbf01e 100644 --- a/DuckDB.NET.Data/DuckDBSchema.cs +++ b/DuckDB.NET.Data/DuckDBSchema.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.IO; using System.Text; using DuckDB.NET.Native; @@ -128,15 +129,26 @@ private static DataTable GetDataTypes(DuckDBConnection connection) using var command = connection.CreateCommand(); command.CommandText = "SELECT DISTINCT type_name, logical_type FROM duckdb_types() ORDER BY type_name"; using var reader = command.ExecuteReader(); + var types = new Dictionary(StringComparer.OrdinalIgnoreCase); while (reader.Read()) { var typeName = reader.GetString(0); var logicalTypeName = reader.GetString(1); + + if (!types.TryGetValue(typeName, out var existing) + || typeName.Equals(logicalTypeName, StringComparison.Ordinal) && !existing.TypeName.Equals(existing.LogicalType, StringComparison.Ordinal)) + { + types[typeName] = (typeName, logicalTypeName); + } + } + + foreach (var (typeName, logicalTypeName) in types.Values.OrderBy(type => type.TypeName, StringComparer.OrdinalIgnoreCase)) + { var providerType = ParseDuckDBType(logicalTypeName); AddType( table, typeName, providerType, GetClrType(providerType), - createFormat: providerType == DuckDBType.Decimal ? "DECIMAL({0},{1})" : null, + createFormat: providerType == DuckDBType.Decimal ? "DECIMAL({0},{1})" : typeName, createParameters: providerType == DuckDBType.Decimal ? "precision,scale" : null, isUnsigned: providerType is DuckDBType.UnsignedTinyInt or DuckDBType.UnsignedSmallInt or DuckDBType.UnsignedInteger or DuckDBType.UnsignedBigInt or DuckDBType.UnsignedHugeInt, @@ -177,7 +189,7 @@ private static DataTable GetDataTypes(DuckDBConnection connection) "INTERVAL" => DuckDBType.Interval, "HUGEINT" => DuckDBType.HugeInt, "UHUGEINT" => DuckDBType.UnsignedHugeInt, - "VARINT" => DuckDBType.VarInt, + "VARINT" or "BIGNUM" => DuckDBType.VarInt, "LIST" => DuckDBType.List, "ARRAY" => DuckDBType.Array, "STRUCT" => DuckDBType.Struct, @@ -186,7 +198,12 @@ private static DataTable GetDataTypes(DuckDBConnection connection) "ENUM" => DuckDBType.Enum, "BIT" => DuckDBType.Bit, "ANY" => DuckDBType.Any, - "SQLNULL" => DuckDBType.SqlNull, + "SQLNULL" or "NULL" => DuckDBType.SqlNull, + "STRING_LITERAL" => DuckDBType.StringLiteral, + "INTEGER_LITERAL" => DuckDBType.IntegerLiteral, + "TIME_NS" => DuckDBType.TimeNs, + "GEOMETRY" => DuckDBType.Geometry, + "VARIANT" => DuckDBType.Variant, _ => DuckDBType.Invalid }; @@ -205,11 +222,12 @@ private static DataTable GetDataTypes(DuckDBConnection connection) DuckDBType.Double => typeof(double), DuckDBType.Decimal => typeof(decimal), DuckDBType.Varchar => typeof(string), - DuckDBType.Blob => typeof(byte[]), + DuckDBType.Blob => typeof(Stream), DuckDBType.Uuid => typeof(Guid), DuckDBType.Date => typeof(DateOnly), DuckDBType.Time => typeof(TimeOnly), - DuckDBType.TimeTz or DuckDBType.TimestampTz => typeof(DateTimeOffset), + DuckDBType.TimeTz => typeof(DateTimeOffset), + DuckDBType.TimestampTz => typeof(DateTime), DuckDBType.Timestamp or DuckDBType.TimestampS or DuckDBType.TimestampMs or DuckDBType.TimestampNs => typeof(DateTime), DuckDBType.Interval => typeof(TimeSpan), DuckDBType.HugeInt or DuckDBType.UnsignedHugeInt or DuckDBType.VarInt => typeof(System.Numerics.BigInteger), diff --git a/DuckDB.NET.Test/SchemaTests.cs b/DuckDB.NET.Test/SchemaTests.cs index 6a9202c3..325fec3c 100644 --- a/DuckDB.NET.Test/SchemaTests.cs +++ b/DuckDB.NET.Test/SchemaTests.cs @@ -155,8 +155,22 @@ public void DataTypesExposeStandardAdoNetMetadata() Assert.Equal(typeof(int).FullName, integer["DataType"]); Assert.Equal((int)DuckDBType.Integer, integer["ProviderDbType"]); + Assert.Equal("INTEGER", integer["CreateFormat"]); Assert.True((bool)integer["IsBestMatch"]); Assert.True((bool)integer["IsSearchable"]); + + var blob = schema.Rows.Cast().First(row => (int)row["ProviderDbType"] == (int)DuckDBType.Blob); + Assert.Equal(typeof(Stream).FullName, blob["DataType"]); + + var timestampTz = schema.Rows.Cast().First(row => (int)row["ProviderDbType"] == (int)DuckDBType.TimestampTz); + Assert.Equal(typeof(DateTime).FullName, timestampTz["DataType"]); + + var bignum = schema.Rows.Cast().First(row => (int)row["ProviderDbType"] == (int)DuckDBType.VarInt); + Assert.Equal(typeof(System.Numerics.BigInteger).FullName, bignum["DataType"]); + + Assert.DoesNotContain( + schema.Rows.Cast().GroupBy(row => (string)row["TypeName"], StringComparer.OrdinalIgnoreCase), + group => group.Count() > 1); } [Fact] @@ -168,7 +182,7 @@ public void DataTypesExposeEveryDuckDbType() .ToHashSet(); var expectedTypes = Enum.GetValues() - .Where(type => type is not (DuckDBType.Invalid or DuckDBType.Any or DuckDBType.VarInt or DuckDBType.SqlNull)) + .Where(type => type is not (DuckDBType.Invalid or DuckDBType.Any or DuckDBType.StringLiteral or DuckDBType.IntegerLiteral)) .ToHashSet(); Assert.True(expectedTypes.IsSubsetOf(providerTypes), From 08068b9c57c657647dd0b868ed6b05e464653b7c Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Tue, 20 Oct 2026 09:57:43 -0500 Subject: [PATCH 5/6] Remove schema validation formatting gap --- DuckDB.NET.Data/DuckDBSchema.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/DuckDB.NET.Data/DuckDBSchema.cs b/DuckDB.NET.Data/DuckDBSchema.cs index 40cbf01e..d956a72f 100644 --- a/DuckDB.NET.Data/DuckDBSchema.cs +++ b/DuckDB.NET.Data/DuckDBSchema.cs @@ -40,7 +40,6 @@ private static void ValidateRestrictions(string collectionName, string?[]? restr { throw new ArgumentException("This collection does not support restrictions.", nameof(restrictionValues)); } - } private static DataTable GetMetaDataCollections() => From 4de083f14dea0d5317a79f2fbf94946e4d3d39cf Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Tue, 20 Oct 2026 11:31:09 -0500 Subject: [PATCH 6/6] Canonicalize schema type names --- DuckDB.NET.Data/DuckDBSchema.cs | 15 ++++++++++++--- DuckDB.NET.Test/SchemaTests.cs | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/DuckDB.NET.Data/DuckDBSchema.cs b/DuckDB.NET.Data/DuckDBSchema.cs index d956a72f..ea1a1ae6 100644 --- a/DuckDB.NET.Data/DuckDBSchema.cs +++ b/DuckDB.NET.Data/DuckDBSchema.cs @@ -144,16 +144,17 @@ private static DataTable GetDataTypes(DuckDBConnection connection) foreach (var (typeName, logicalTypeName) in types.Values.OrderBy(type => type.TypeName, StringComparer.OrdinalIgnoreCase)) { var providerType = ParseDuckDBType(logicalTypeName); + var displayTypeName = typeName.Equals(logicalTypeName, StringComparison.OrdinalIgnoreCase) ? logicalTypeName : typeName; AddType( - table, typeName, providerType, GetClrType(providerType), - createFormat: providerType == DuckDBType.Decimal ? "DECIMAL({0},{1})" : typeName, + table, displayTypeName, providerType, GetClrType(providerType), + createFormat: GetCreateFormat(providerType, displayTypeName), createParameters: providerType == DuckDBType.Decimal ? "precision,scale" : null, isUnsigned: providerType is DuckDBType.UnsignedTinyInt or DuckDBType.UnsignedSmallInt or DuckDBType.UnsignedInteger or DuckDBType.UnsignedBigInt or DuckDBType.UnsignedHugeInt, isCaseSensitive: providerType == DuckDBType.Varchar, isLong: providerType == DuckDBType.Blob, - isBestMatch: typeName.Equals(logicalTypeName, StringComparison.OrdinalIgnoreCase), + isBestMatch: displayTypeName.Equals(logicalTypeName, StringComparison.Ordinal), nativeTypeName: logicalTypeName); } @@ -206,6 +207,14 @@ private static DataTable GetDataTypes(DuckDBConnection connection) _ => DuckDBType.Invalid }; + private static string? GetCreateFormat(DuckDBType type, string typeName) => type switch + { + DuckDBType.Decimal => "DECIMAL({0},{1})", + DuckDBType.List or DuckDBType.Array or DuckDBType.Struct or DuckDBType.Map or DuckDBType.Union + or DuckDBType.Enum or DuckDBType.SqlNull or DuckDBType.Invalid or DuckDBType.Any => null, + _ => typeName + }; + private static Type GetClrType(DuckDBType type) => type switch { DuckDBType.Boolean => typeof(bool), diff --git a/DuckDB.NET.Test/SchemaTests.cs b/DuckDB.NET.Test/SchemaTests.cs index 325fec3c..24950fb1 100644 --- a/DuckDB.NET.Test/SchemaTests.cs +++ b/DuckDB.NET.Test/SchemaTests.cs @@ -173,6 +173,20 @@ public void DataTypesExposeStandardAdoNetMetadata() group => group.Count() > 1); } + [Fact] + public void DataTypesUseCanonicalNamesOnFreshConnection() + { + using var connection = new DuckDBConnection("DataSource=:memory:"); + connection.Open(); + + var schema = connection.GetSchema("DataTypes"); + var integer = schema.Rows.Cast().Single(row => (string)row["TypeName"] == "INTEGER"); + var list = schema.Rows.Cast().Single(row => (string)row["TypeName"] == "LIST"); + + Assert.Equal("INTEGER", integer["CreateFormat"]); + Assert.Equal(DBNull.Value, list["CreateFormat"]); + } + [Fact] public void DataTypesExposeEveryDuckDbType() {