Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/sponsors/ducklabs-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
156 changes: 149 additions & 7 deletions DuckDB.NET.Data/DuckDBSchema.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Linq;
using System.Text;
using DuckDB.NET.Native;

namespace DuckDB.NET.Data;

Expand All @@ -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),
Expand All @@ -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)
Expand All @@ -37,16 +53,142 @@ 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 },
{ DuckDbMetaDataCollectionNames.Indexes, IndexesRestrictions.Length, 3 },
}
};

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)
{
Expand Down Expand Up @@ -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 },
},
};

Expand Down Expand Up @@ -218,4 +360,4 @@ private static DataTable GetDataTable(string tableName, DuckDBCommand command)

return table;
}
}
}
42 changes: 41 additions & 1 deletion DuckDB.NET.Test/SchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ public void InvalidCollection()
{
Assert.Throws<ArgumentOutOfRangeException>(() => Connection.GetSchema("Invalid"));
}

[Theory]
[InlineData("MetaDataCollections")]
[InlineData("DataSourceInformation")]
[InlineData("DataTypes")]
[InlineData("Restrictions")]
[InlineData("ReservedWords")]
public void CollectionsWithoutRestrictionsRejectRestrictionValues(string collectionName)
{
Assert.Throws<ArgumentException>(() => Connection.GetSchema(collectionName, ["unexpected"]));
}

[Fact]
public void MetaDataCollections()
Expand Down Expand Up @@ -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<DataRow>(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<DataRow>().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()
{
Expand Down Expand Up @@ -173,4 +213,4 @@ public void IndexesTooManyRestrictions()

private static IEnumerable<string> GetValues(DataTable schema, string columnName) =>
schema.Rows.Cast<DataRow>().Select(r => (string)r[columnName]);
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading