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
3 changes: 3 additions & 0 deletions DuckDB.NET.Bindings/NativeMethods/NativeMethods.Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,8 @@ public static class Query

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_result_error_type")]
public static extern DuckDBErrorType DuckDBResultErrorType(ref DuckDBResult result);

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_get_table_names")]
public static extern DuckDBValue DuckDBGetTableNames(DuckDBNativeConnection connection, string query, bool qualified);
}
}
6 changes: 6 additions & 0 deletions DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@
public static class Value
{
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_destroy_value")]
public static extern void DuckDBDestroyValue(ref IntPtr config);

Check warning on line 12 in DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs

View workflow job for this annotation

GitHub Actions / Build and analyze

Make this native method private and provide a wrapper. (https://rules.sonarsource.com/csharp/RSPEC-4200)

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_varchar")]
public static extern DuckDBValue DuckDBCreateVarchar(SafeUnmanagedMemoryHandle value);

Check warning on line 15 in DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs

View workflow job for this annotation

GitHub Actions / Build and analyze

Make this native method private and provide a wrapper. (https://rules.sonarsource.com/csharp/RSPEC-4200)

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_bool")]
public static extern DuckDBValue DuckDBCreateBool(bool value);

Check warning on line 18 in DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs

View workflow job for this annotation

GitHub Actions / Build and analyze

Make this native method private and provide a wrapper. (https://rules.sonarsource.com/csharp/RSPEC-4200)

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_int8")]
public static extern DuckDBValue DuckDBCreateInt8(sbyte value);

Check warning on line 21 in DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs

View workflow job for this annotation

GitHub Actions / Build and analyze

Make this native method private and provide a wrapper. (https://rules.sonarsource.com/csharp/RSPEC-4200)

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_uint8")]
public static extern DuckDBValue DuckDBCreateUInt8(byte value);

Check warning on line 24 in DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs

View workflow job for this annotation

GitHub Actions / Build and analyze

Make this native method private and provide a wrapper. (https://rules.sonarsource.com/csharp/RSPEC-4200)

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_int16")]
public static extern DuckDBValue DuckDBCreateInt16(short value);

Check warning on line 27 in DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs

View workflow job for this annotation

GitHub Actions / Build and analyze

Make this native method private and provide a wrapper. (https://rules.sonarsource.com/csharp/RSPEC-4200)

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_uint16")]
public static extern DuckDBValue DuckDBCreateUInt16(ushort value);
Expand Down Expand Up @@ -159,10 +159,10 @@
public static extern string DuckDBGetVarchar(DuckDBValue value);

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_list_value")]
public static extern DuckDBValue DuckDBCreateListValue(DuckDBLogicalType logicalType, IntPtr[] values, long count);

Check warning on line 162 in DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs

View workflow job for this annotation

GitHub Actions / Build and analyze

All 'DuckDBCreateListValue' method overloads should be adjacent. (https://rules.sonarsource.com/csharp/RSPEC-4136)

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_array_value")]
public static extern DuckDBValue DuckDBCreateArrayValue(DuckDBLogicalType logicalType, IntPtr[] values, long count);

Check warning on line 165 in DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs

View workflow job for this annotation

GitHub Actions / Build and analyze

All 'DuckDBCreateArrayValue' method overloads should be adjacent. (https://rules.sonarsource.com/csharp/RSPEC-4136)

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_null_value")]
public static extern DuckDBValue DuckDBCreateNullValue();
Expand All @@ -170,6 +170,12 @@
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_null_value")]
public static extern bool DuckDBIsNullValue(DuckDBValue value);

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_get_list_size")]
public static extern ulong DuckDBGetListSize(DuckDBValue value);

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_get_list_child")]
public static extern DuckDBValue DuckDBGetListChild(DuckDBValue value, ulong index);

public static DuckDBValue DuckDBCreateListValue(DuckDBLogicalType logicalType, DuckDBValue[] values, int count)
{
var duckDBValue = DuckDBCreateListValue(logicalType, values.Select(item => item.DangerousGetHandle()).ToArray(), count);
Expand Down
104 changes: 101 additions & 3 deletions DuckDB.NET.Data/DuckDBDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,15 +309,26 @@
{ SchemaTableColumn.NumericPrecision, typeof(byte)},
{ SchemaTableColumn.NumericScale, typeof(byte) },
{ SchemaTableColumn.DataType, typeof(Type) },
{ SchemaTableColumn.AllowDBNull, typeof(bool) }
{ SchemaTableColumn.AllowDBNull, typeof(bool) },
{ SchemaTableColumn.BaseSchemaName, typeof(string) },
{ SchemaTableColumn.BaseTableName, typeof(string) },
{ SchemaTableColumn.BaseColumnName, typeof(string) }
}
};

var rowData = new object[7];
// Get table names from the query
// Note: DuckDB's duckdb_get_table_names returns unique table names referenced in the query,
// not per-column mappings. For single-table queries, we can populate BaseTableName.
// For multi-table queries (joins), the mapping is not directly available from the C API.
var tableNames = GetTableNamesFromQuery();
var singleTableName = tableNames != null && tableNames.Length == 1 ? tableNames[0] : null;

var rowData = new object[10];

for (var i = 0; i < FieldCount; i++)
{
rowData[0] = GetName(i);
var columnName = GetName(i);
rowData[0] = columnName;
rowData[1] = i;
rowData[2] = -1;
rowData[5] = GetFieldType(i);
Expand All @@ -333,12 +344,99 @@
rowData[3] = rowData[4] = DBNull.Value;
}

// Set table name information
// For single-table queries, populate the table name
// For multi-table queries, BaseTableName will be DBNull since we cannot determine
// which table each column comes from without additional API support
if (!string.IsNullOrEmpty(singleTableName))
{
// The table name from duckdb_get_table_names with qualified=true should be in the format "schema.table"
// Split by the last dot to handle schema names or table names that might contain dots
var lastDotIndex = singleTableName.LastIndexOf('.');

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (macos-14)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (macos-14)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (macos-14)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (macos-14)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (windows-latest)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (windows-latest)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (windows-latest)

Dereference of a possibly null reference.

Check warning on line 355 in DuckDB.NET.Data/DuckDBDataReader.cs

View workflow job for this annotation

GitHub Actions / Build library (windows-latest)

Dereference of a possibly null reference.
if (lastDotIndex > 0 && lastDotIndex < singleTableName.Length - 1)
{
rowData[7] = singleTableName.Substring(0, lastDotIndex); // BaseSchemaName
rowData[8] = singleTableName.Substring(lastDotIndex + 1); // BaseTableName
}
else
{
// No schema qualifier found, just use the table name
rowData[7] = DBNull.Value; // BaseSchemaName
rowData[8] = singleTableName; // BaseTableName
}
}
else
{
rowData[7] = DBNull.Value;
rowData[8] = DBNull.Value;
}

rowData[9] = columnName; // BaseColumnName

table.Rows.Add(rowData);
}

return table;
}

private string[]? GetTableNamesFromQuery()
{
try
{
var duckDBConnection = command?.Connection as DuckDBConnection;
if (duckDBConnection?.NativeConnection == null || command?.CommandText == null || string.IsNullOrEmpty(command.CommandText))
{
return null;
}

// Call duckdb_get_table_names with qualified=true to get schema-qualified names
using var tableNamesValue = NativeMethods.Query.DuckDBGetTableNames(
duckDBConnection.NativeConnection,
command.CommandText,
true);

if (tableNamesValue.IsNull())
{
return null;
}

// Get the size of the list
var listSize = NativeMethods.Value.DuckDBGetListSize(tableNamesValue);

// If the list is empty, return null
if (listSize == 0)
{
return null;
}

var tableNames = new string[listSize];

// Extract each table name from the list
for (ulong i = 0; i < listSize; i++)
{
using var childValue = NativeMethods.Value.DuckDBGetListChild(tableNamesValue, i);
if (!childValue.IsNull())
{
tableNames[i] = NativeMethods.Value.DuckDBGetVarchar(childValue);
}
else
{
tableNames[i] = string.Empty;
}
}

return tableNames;
}
catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException or InvalidOperationException)
{
// If we fail to get table names due to missing DLL, missing entry point, or operation errors,
// just return null. This ensures backward compatibility - if the feature isn't available or fails,
// we just don't populate the table names.
// We don't log here to avoid noise in normal operation when the feature might not be available.
return null;
}
}

public override void Close()
{
if (closed) return;
Expand Down
63 changes: 63 additions & 0 deletions DuckDB.NET.Test/DuckDBDataReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -433,4 +433,67 @@ public void ReadVarint()
reader.Read();
var value = (BigInteger)reader.GetValue(0);
}

[Fact]
public void GetSchemaTableReturnsBaseTableName()
{
Command.CommandText = "CREATE TABLE test_table(id INTEGER, name VARCHAR);";
Command.ExecuteNonQuery();

Command.CommandText = "INSERT INTO test_table VALUES (1, 'Alice'), (2, 'Bob');";
Command.ExecuteNonQuery();

Command.CommandText = "SELECT id, name FROM test_table";
using var reader = Command.ExecuteReader();

var schemaTable = reader.GetSchemaTable();

schemaTable.Should().NotBeNull();
schemaTable!.Rows.Count.Should().Be(2);

// Check that BaseTableName column exists
schemaTable.Columns.Should().Contain(c => c.ColumnName == "BaseTableName");

// Check that both columns have the same table name
schemaTable.Rows[0]["BaseTableName"].Should().Be("test_table");
schemaTable.Rows[1]["BaseTableName"].Should().Be("test_table");

// Check that BaseColumnName is populated
schemaTable.Rows[0]["BaseColumnName"].Should().Be("id");
schemaTable.Rows[1]["BaseColumnName"].Should().Be("name");
}

[Fact]
public void GetSchemaTableForJoinReturnsDbNull()
{
// Note: DuckDB's duckdb_get_table_names C API returns a list of unique table names
// referenced in the query, but does not provide per-column table name mapping.
// Therefore, for queries with multiple tables (joins), BaseTableName will be DBNull.

Command.CommandText = "CREATE TABLE users(id INTEGER, name VARCHAR);";
Command.ExecuteNonQuery();

Command.CommandText = "CREATE TABLE orders(order_id INTEGER, user_id INTEGER);";
Command.ExecuteNonQuery();

Command.CommandText = "INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob');";
Command.ExecuteNonQuery();

Command.CommandText = "INSERT INTO orders VALUES (100, 1), (200, 2);";
Command.ExecuteNonQuery();

Command.CommandText = "SELECT u.id, u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id";
using var reader = Command.ExecuteReader();

var schemaTable = reader.GetSchemaTable();

schemaTable.Should().NotBeNull();
schemaTable!.Rows.Count.Should().Be(3);

// For join queries, BaseTableName should be DBNull since we can't determine
// which table each column comes from without additional API support
schemaTable.Rows[0]["BaseTableName"].Should().Be(DBNull.Value);
schemaTable.Rows[1]["BaseTableName"].Should().Be(DBNull.Value);
schemaTable.Rows[2]["BaseTableName"].Should().Be(DBNull.Value);
}
}
Loading