diff --git a/.github/sponsors/ducklabs-logo.png b/.github/sponsors/ducklabs-logo.png new file mode 100644 index 00000000..c57cbd64 Binary files /dev/null and b/.github/sponsors/ducklabs-logo.png differ 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 +} diff --git a/README.md b/README.md index 35c9ea61..e77eb67e 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,6 @@ If you encounter a bug with the library [Create an Issue](https://github.com/Gio A big thanks to [DuckLabs](https://ducklabs.com/) and [AWS Open Source Software Fund](https://github.com/aws/dotnet-foss) for sponsoring the project! -[![DuckLabs](https://raw.githubusercontent.com/Giorgi/DuckDB.NET/main/.github/sponsors/duckdb-labs-logo.png)](https://ducklabs.com/) +[![DuckLabs](https://raw.githubusercontent.com/Giorgi/DuckDB.NET/main/.github/sponsors/ducklabs-logo.png)](https://ducklabs.com/) [![AWS](https://raw.githubusercontent.com/Giorgi/DuckDB.NET/main/.github/sponsors/aws-logo-small.png)](https://github.com/aws/dotnet-foss)