diff --git a/Directory.Build.props b/Directory.Build.props
index ed73ab1d..eb286756 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -65,6 +65,22 @@
+
+
+ false
+
+
+
+ RELEASE-NOTE.md
+
+
+
+
+ True
+
+
+
+
false
diff --git a/DuckDB.NET.Bindings/Bindings.csproj b/DuckDB.NET.Bindings/Bindings.csproj
index 82bad60b..927d520c 100644
--- a/DuckDB.NET.Bindings/Bindings.csproj
+++ b/DuckDB.NET.Bindings/Bindings.csproj
@@ -18,6 +18,11 @@
enable
+
+ true
+ $(NoWarn);1591
+
+
$(Description) $(NoNativeText)
@@ -36,7 +41,7 @@
<_NativeLib Include="linux-x64" LibUrl="$(DuckDbArtifactRoot)/duckdb-binaries-linux-amd64.zip" InnerZipName="libduckdb-linux-amd64.zip" Condition="$([MSBuild]::IsOSPlatform('Linux'))" />
<_NativeLib Include="osx" LibUrl="$(DuckDbArtifactRoot)/duckdb-binaries-osx.zip" InnerZipName="libduckdb-osx-universal.zip" Condition="$([MSBuild]::IsOSPlatform('OSX'))" />
-
+
diff --git a/DuckDB.NET.Bindings/DownloadNativeLibs.targets b/DuckDB.NET.Bindings/DownloadNativeLibs.targets
index b344ff87..4ddbb86b 100644
--- a/DuckDB.NET.Bindings/DownloadNativeLibs.targets
+++ b/DuckDB.NET.Bindings/DownloadNativeLibs.targets
@@ -1,5 +1,10 @@
+
+ 5
+ 1000
+
+
@@ -18,8 +23,8 @@
diff --git a/DuckDB.NET.Bindings/DuckDBNativeObjects.cs b/DuckDB.NET.Bindings/DuckDBNativeObjects.cs
index 9dba9689..6ce5311a 100644
--- a/DuckDB.NET.Bindings/DuckDBNativeObjects.cs
+++ b/DuckDB.NET.Bindings/DuckDBNativeObjects.cs
@@ -1,4 +1,6 @@
-namespace DuckDB.NET.Native;
+using System.ComponentModel;
+
+namespace DuckDB.NET.Native;
public enum DuckDBState
{
@@ -249,4 +251,103 @@ public struct DuckDBQueryProgress
public double Percentage { get; }
public ulong RowsProcessed { get; }
public ulong TotalRowsToProcess { get; }
+}
+
+public enum DuckDBMetricType
+{
+ // Core metrics
+ CpuTime = 2,
+ CumulativeCardinality = 4,
+ CumulativeRowsScanned = 7,
+ ExtraInfo = 3,
+ Latency = 11,
+ QueryName = 0,
+ ResultSetSize = 10,
+ [Description("Not available yet see(https://github.com/duckdb/duckdb/issues/22592)")]
+ RowsReturned = 12,
+ // Execution metrics
+ BlockedThreadTime = 1,
+ SystemPeakBufferMemory = 14,
+ SystemPeakTempDirSize = 15,
+ TotalMemoryAllocated = 91,
+ // File metrics
+ AttachLoadStorageLatency = 92,
+ AttachReplayWalLatency = 93,
+ CheckpointLatency = 94,
+ CommitLocalStorageLatency = 95,
+ TotalBytesRead = 16,
+ TotalBytesWritten = 17,
+ WaitingToAttachLatency = 96,
+ WalReplayEntryCount = 97,
+ WriteToWalLatency = 98,
+ // Operator metrics
+ OperatorCardinality = 6,
+ OperatorName = 13,
+ OperatorRowsScanned = 8,
+ OperatorTiming = 9,
+ OperatorType = 5,
+ // Optimizer metrics
+ OptimizerExpressionRewriter = 26,
+ OptimizerFilterPullup = 27,
+ OptimizerFilterPushdown = 28,
+ OptimizerEmptyResultPullup = 29,
+ OptimizerCteFilterPusher = 30,
+ OptimizerRegexRange = 31,
+ OptimizerInClause = 32,
+ OptimizerJoinOrder = 33,
+ OptimizerDeliminator = 34,
+ OptimizerUnnestRewriter = 35,
+ OptimizerUnusedColumns = 36,
+ OptimizerStatisticsPropagation = 37,
+ OptimizerCommonSubexpressions = 38,
+ OptimizerCommonAggregate = 39,
+ OptimizerColumnLifetime = 40,
+ OptimizerBuildSideProbeSide = 41,
+ OptimizerLimitPushdown = 42,
+ OptimizerTopN = 43,
+ OptimizerCompressedMaterialization = 44,
+ OptimizerDuplicateGroups = 45,
+ OptimizerReorderFilter = 46,
+ OptimizerSamplingPushdown = 47,
+ OptimizerJoinFilterPushdown = 48,
+ OptimizerExtension = 49,
+ OptimizerMaterializedCte = 50,
+ OptimizerSumRewriter = 51,
+ OptimizerLateMaterialization = 52,
+ OptimizerCteInlining = 53,
+ OptimizerRowGroupPruner = 54,
+ OptimizerTopNWindowElimination = 55,
+ OptimizerCommonSubplan = 56,
+ OptimizerJoinElimination = 57,
+ OptimizerWindowSelfJoin = 58,
+ // PhaseTiming metrics
+ AllOptimizers = 18,
+ CumulativeOptimizerTiming = 19,
+ PhysicalPlanner = 22,
+ PhysicalPlannerColumnBinding = 23,
+ PhysicalPlannerCreatePlan = 25,
+ PhysicalPlannerResolveTypes = 24,
+ Planner = 20,
+ PlannerBinding = 21
+}
+
+public enum DuckDBProfilingFormat
+{
+ QueryTree,
+ Json,
+ QueryTreeOptimizer,
+ NoOutput
+}
+
+public enum DuckDBProfilingMode
+{
+ Standard,
+ Detailed,
+ All
+}
+
+public enum DuckDBProfilingCoverage
+{
+ Select,
+ All
}
\ No newline at end of file
diff --git a/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs b/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs
index e42f90c6..42164ab6 100644
--- a/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs
+++ b/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs
@@ -1,4 +1,6 @@
-namespace DuckDB.NET.Native;
+using System.Collections.Generic;
+
+namespace DuckDB.NET.Native;
public class DuckDBDatabase() : SafeHandleZeroOrMinusOneIsInvalid(true)
{
@@ -59,6 +61,17 @@ protected override bool ReleaseHandle()
}
}
+public class DuckDBProfilingInfo() : SafeHandleZeroOrMinusOneIsInvalid(true)
+{
+ // No explicit destroy/free function for duckdb_profiling_info in the C API as of now.
+ // If DuckDB adds a destroy function in the future, call it here.
+ protected override bool ReleaseHandle()
+ {
+ // No-op: DuckDB does not require explicit destruction of profiling info handles.
+ return true;
+ }
+}
+
public class DuckDBLogicalType() : SafeHandleZeroOrMinusOneIsInvalid(true)
{
protected override bool ReleaseHandle()
@@ -161,6 +174,25 @@ public T GetValue()
static T Cast(TSource value) => Unsafe.As(ref value);
}
+ public Dictionary GetMapValue() where TKey : notnull
+ {
+ var result = new Dictionary();
+
+ ulong entryCount = NativeMethods.Value.DuckDBGetMapSize(this);
+ for (ulong i = 0; i < entryCount; i++)
+ {
+ using var key = NativeMethods.Value.DuckDBGetMapKey(this, i);
+ using var value = NativeMethods.Value.DuckDBGetMapValue(this, i);
+
+ var keyValue = key.GetValue();
+ var valueValue = value.GetValue();
+
+ result.Add(keyValue, valueValue);
+ }
+
+ return result;
+ }
+
private DateTime GetTimestampValue(DuckDBTimestampStruct timestampStruct, DuckDBType duckDBType)
{
var additionalTicks = 0;
@@ -194,6 +226,7 @@ private DateTimeOffset GetTimeTzValue()
var timeTz = NativeMethods.DateTimeHelpers.DuckDBFromTimeTz(timeTzStruct);
return new DateTimeOffset(timeTz.Time.ToDateTime(), TimeSpan.FromSeconds(timeTz.Offset));
}
+
}
public class DuckDBClientContext() : SafeHandleZeroOrMinusOneIsInvalid(true)
diff --git a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.ProfilingInfo.cs b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.ProfilingInfo.cs
new file mode 100644
index 00000000..b170dc9b
--- /dev/null
+++ b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.ProfilingInfo.cs
@@ -0,0 +1,31 @@
+
+namespace DuckDB.NET.Native;
+
+public partial class NativeMethods
+{
+ //https://duckdb.org/docs/current/clients/c/api#profiling-info
+ public static partial class ProfilingInfo
+ {
+
+ [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_get_profiling_info")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ public static partial DuckDBProfilingInfo DuckDBGetProfilingInfo(DuckDBNativeConnection connection);
+
+ [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_profiling_info_get_value")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ public static partial DuckDBValue DuckDBProfilingInfoGetValue(DuckDBProfilingInfo info, [MarshalAs(UnmanagedType.LPStr)] string key);
+
+ [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_profiling_info_get_metrics")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ public static partial DuckDBValue DuckDBProfilingInfoGetMetrics(DuckDBProfilingInfo info);
+
+ [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_profiling_info_get_child_count")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ public static partial ulong DuckDBProfilingInfoGetChildCount(DuckDBProfilingInfo info);
+
+ [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_profiling_info_get_child")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ public static partial DuckDBProfilingInfo DuckDBProfilingInfoGetChild(DuckDBProfilingInfo info, ulong index);
+
+ }
+}
\ No newline at end of file
diff --git a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs
index 518fe61f..df14d9e3 100644
--- a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs
+++ b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs
@@ -215,6 +215,18 @@ public static partial class Value
[return: MarshalUsing(typeof(DuckDBCallerOwnedStringMarshaller))]
public static partial string DuckDBGetVarchar(DuckDBValue value);
+ [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_get_map_size")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ public static partial ulong DuckDBGetMapSize(DuckDBValue value);
+
+ [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_get_map_key")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ public static partial DuckDBValue DuckDBGetMapKey(DuckDBValue value, ulong index);
+
+ [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_get_map_value")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ public static partial DuckDBValue DuckDBGetMapValue(DuckDBValue value, ulong index);
+
[LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_create_list_value")]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static partial DuckDBValue DuckDBCreateListValue(DuckDBLogicalType logicalType, IntPtr[] values, long count);
@@ -223,6 +235,10 @@ public static partial class Value
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static partial DuckDBValue DuckDBCreateArrayValue(DuckDBLogicalType logicalType, IntPtr[] values, long count);
+ [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_create_map_value")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ public static partial DuckDBValue DuckDBCreateMapValue(DuckDBLogicalType logicalType, IntPtr[] keys, IntPtr[] values, long count);
+
// Maybe [SuppressGCTransition]: new Value — one small allocation
[LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_create_null_value")]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
@@ -251,5 +267,14 @@ public static DuckDBValue DuckDBCreateArrayValue(DuckDBLogicalType logicalType,
return duckDBValue;
}
+
+ public static DuckDBValue DuckDBCreateMapValue(DuckDBLogicalType logicalType, DuckDBValue[] keys, DuckDBValue[] values, int count)
+ {
+ var duckDBValue = DuckDBCreateMapValue(logicalType, keys.Select(item => item.DangerousGetHandle()).ToArray(), values.Select(item => item.DangerousGetHandle()).ToArray(), count);
+
+ duckDBValue.SetChildValues([.. keys, .. values]);
+
+ return duckDBValue;
+ }
}
}
diff --git a/DuckDB.NET.Data/Common/TimerUtils.cs b/DuckDB.NET.Data/Common/TimerUtils.cs
new file mode 100644
index 00000000..f18f3b40
--- /dev/null
+++ b/DuckDB.NET.Data/Common/TimerUtils.cs
@@ -0,0 +1,76 @@
+using System.Diagnostics;
+
+namespace DuckDB.NET.Data.Common;
+
+internal sealed class TimerUtils
+{
+ private static readonly long Frequency = Stopwatch.Frequency;
+
+ ///
+ /// Gets the current timestamp value for high-resolution timing operations.
+ ///
+ /// The returned value can be used with Stopwatch.Frequency to calculate elapsed time intervals with high
+ /// precision. This method is intended for scenarios where precise timing is required, such as performance
+ /// measurements.
+ /// A long integer representing the current timestamp, as provided by the system's high-resolution performance counter.
+ internal static long TimerCurrent() => Stopwatch.GetTimestamp();
+
+ ///
+ /// Converts a timer value to its equivalent duration in milliseconds.
+ ///
+ /// The conversion uses the timer's frequency to determine the number of milliseconds represented by the
+ /// timer value. Ensure that the timer value and frequency are based on the same timer source.
+ /// The timer value to convert, typically representing elapsed timer ticks.
+ /// The equivalent duration in milliseconds calculated from the specified timer value.
+ internal static long TimerToMilliseconds(long timerValue)
+ {
+ // sentinel handling: preserve "infinite" indicator
+ if (timerValue == long.MaxValue)
+ {
+ return long.MaxValue;
+ }
+
+ return timerValue * 1000 / Frequency;
+ }
+
+ ///
+ /// Converts a timer tick duration (as returned by ) to a .
+ ///
+ /// The timer tick duration to convert.
+ /// A representing the duration of the provided timer ticks.
+ internal static TimeSpan TimerToTimeSpan(long timerValue)
+ {
+ // sentinel handling: preserve "infinite" indicator
+ if (timerValue == long.MaxValue)
+ {
+ return TimeSpan.MaxValue;
+ }
+
+ // Convert stopwatch ticks to DateTime/TimeSpan ticks. TimeSpan.TicksPerSecond = 10_000_000.
+ long timeSpanTicks = timerValue * TimeSpan.TicksPerSecond / Frequency;
+ return TimeSpan.FromTicks(timeSpanTicks);
+ }
+
+ ///
+ /// Converts a TimeSpan tick count (100-nanosecond ticks) to milliseconds as a double.
+ /// Handles sentinel long.MaxValue as an infinite duration.
+ ///
+ internal static double TimeSpanTicksToMilliseconds(long timeSpanTicks)
+ {
+ if (timeSpanTicks == long.MaxValue)
+ {
+ return double.MaxValue;
+ }
+
+ return TimeSpan.FromTicks(timeSpanTicks).TotalMilliseconds;
+ }
+
+ ///
+ /// Calculates the elapsed number of ticks between two tick count values.
+ ///
+ /// The starting tick count value.
+ /// The ending tick count value.
+ /// The difference between the ending and starting tick count values, representing the elapsed ticks.
+ internal static long CalculateTickCountElapsed(long startTick, long endTick)
+ => endTick - startTick; // keep as long, no truncation
+}
diff --git a/DuckDB.NET.Data/Connection/FileReference.cs b/DuckDB.NET.Data/Connection/FileReference.cs
index 24482986..541e7553 100644
--- a/DuckDB.NET.Data/Connection/FileReference.cs
+++ b/DuckDB.NET.Data/Connection/FileReference.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using DuckDB.NET.Data.Profiling;
+using System.IO;
namespace DuckDB.NET.Data.Connection;
@@ -13,6 +14,17 @@ internal class FileReference(string filename)
public long ConnectionCount { get; private set; } //don't need a long, but it is slightly faster on 64 bit systems
+ ///
+ /// Gets a value indicating whether profiling is enabled for the current context.
+ /// Is used to ensure that the DuckDBDatabase is created with the correct profiling mode when a connection is opened on the same database
+ ///
+ public bool IsProfilingEnabled { get; internal set; }
+
+ ///
+ /// Gets the profiling options used to configure performance profiling behavior.
+ ///
+ public ProfilingOptions? ProfilingOptions { get; internal set; }
+
public long Decrement()
{
return --ConnectionCount;
diff --git a/DuckDB.NET.Data/Data.csproj b/DuckDB.NET.Data/Data.csproj
index 6a8e00e8..8652de08 100644
--- a/DuckDB.NET.Data/Data.csproj
+++ b/DuckDB.NET.Data/Data.csproj
@@ -16,6 +16,11 @@ New features:
enable
+
+ true
+ $(NoWarn);1591
+
+
$(Description) $(NoNativeText)
diff --git a/DuckDB.NET.Data/DuckDBConnection.cs b/DuckDB.NET.Data/DuckDBConnection.cs
index 54bfbbe6..24760dfb 100644
--- a/DuckDB.NET.Data/DuckDBConnection.cs
+++ b/DuckDB.NET.Data/DuckDBConnection.cs
@@ -1,4 +1,7 @@
using DuckDB.NET.Data.Connection;
+using DuckDB.NET.Data.Profiling;
+using DuckDB.NET.Data.Profiling.Statistics;
+using DuckDB.NET.Data.Profiling.Statistics.Summary;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
@@ -12,10 +15,14 @@ public partial class DuckDBConnection : DbConnection
private DuckDBConnectionString? parsedConnection;
private ConnectionReference? connectionReference;
private bool inMemoryDuplication = false;
-
private static readonly StateChangeEventArgs FromClosedToOpenEventArgs = new(ConnectionState.Closed, ConnectionState.Open);
private static readonly StateChangeEventArgs FromOpenToClosedEventArgs = new(ConnectionState.Open, ConnectionState.Closed);
+ // Statistics support
+ private ConnectionStatistics? statistics;
+ private bool profilingEnabled;
+ private ProfilingOptions? profilingOptions;
+
#region Protected Properties
protected override DbProviderFactory? DbProviderFactory => DuckDBClientFactory.Instance;
@@ -72,6 +79,12 @@ public override string DataSource
public DuckDBNativeConnection NativeConnection => connectionReference?.NativeConnection
?? throw new InvalidOperationException("The DuckDBConnection must be open to access the native connection.");
+ ///
+ /// Gets the underlying native DuckDB database instance associated with the current connection.
+ ///
+ public DuckDBDatabase NativeDatabase => connectionReference?.FileReferenceCounter.Database
+ ?? throw new InvalidOperationException("The DuckDBConnection must be open to access the native database.");
+
public override string ServerVersion => NativeMethods.Startup.DuckDBLibraryVersion();
public override ConnectionState State => connectionState;
@@ -93,7 +106,10 @@ public override void Close()
connectionManager.ReturnConnectionReference(connectionReference);
}
+ UpdateStatistics();
+
connectionState = ConnectionState.Closed;
+
OnStateChange(FromOpenToClosedEventArgs);
}
@@ -109,6 +125,22 @@ public override void Open()
: connectionManager.GetConnectionReference(ParsedConnection);
connectionState = ConnectionState.Open;
+
+ // If profilingOptions has no explicit value for this connection, inherit the file-backed DB settings.
+ // Otherwise persist the explicit options to the shared FileReference so other connections see them.
+ if (!profilingOptions.HasValue)
+ {
+ profilingEnabled = connectionReference.FileReferenceCounter.IsProfilingEnabled;
+ profilingOptions = connectionReference.FileReferenceCounter.ProfilingOptions;
+ }
+ else
+ {
+ connectionReference.FileReferenceCounter.IsProfilingEnabled = profilingEnabled;
+ connectionReference.FileReferenceCounter.ProfilingOptions = profilingOptions.Value;
+ }
+
+ InitProfiling();
+
OnStateChange(FromClosedToOpenEventArgs);
}
@@ -184,7 +216,7 @@ string GetTableName()
/// The AppenderMap type defining the mappings
/// The table name
/// A type-safe mapped appender
- public DuckDBMappedAppender CreateAppender(string table)
+ public DuckDBMappedAppender CreateAppender(string table)
where TMap : Mapping.DuckDBAppenderMap, new()
{
return CreateAppender(null, null, table);
@@ -230,6 +262,8 @@ protected override void Dispose(bool disposing)
{
Close();
}
+
+ statistics?.Dispose();
}
base.Dispose(disposing);
@@ -258,6 +292,8 @@ public DuckDBConnection Duplicate()
parsedConnection = ParsedConnection,
inMemoryDuplication = true,
connectionReference = connectionReference,
+ profilingEnabled = profilingEnabled,
+ profilingOptions = profilingOptions,
};
return duplicatedConnection;
@@ -277,4 +313,232 @@ public DuckDBQueryProgress GetQueryProgress()
EnsureConnectionOpen();
return NativeMethods.Startup.DuckDBQueryProgress(NativeConnection);
}
+
+ #region profiling
+
+ public bool IsProfilingEnabled => profilingEnabled;
+
+ ///
+ /// Retrieves a summary of the collected profiling statistics, including connection time, execution time, and any relevant metrics.
+ /// If profiling is not enabled, this method will return an empty summary.
+ ///
+ /// A containing the collected statistics.
+ public ProfilingSummary RetrieveStatistics()
+ {
+ if (statistics != null)
+ {
+ UpdateStatistics();
+ return statistics.GetProfilingSummary();
+ }
+ else
+ {
+ // Profiling not enabled for this connection
+ return new ProfilingSummary();
+ }
+ }
+
+ ///
+ /// Enables profiling for the current connection, optionally using the specified profiling options.
+ ///
+ /// If profiling is enabled while the connection is already open, profiling is initialized immediately.
+ /// Otherwise, profiling will be initialized when the connection is opened.
+ /// An optional set of profiling options to configure profiling behavior. If null, default options are used.
+ public void EnableProfiling(ProfilingOptions? options = null)
+ {
+ profilingEnabled = true;
+ this.profilingOptions = options ?? new ProfilingOptions(); // use provided options or default options if null
+
+ if (connectionReference?.FileReferenceCounter is { } fileRefCounter)
+ {
+ fileRefCounter.IsProfilingEnabled = true;
+ fileRefCounter.ProfilingOptions = this.profilingOptions.Value;
+ }
+
+ if (State == ConnectionState.Open)
+ {
+ InitProfiling();
+ }
+ }
+
+ ///
+ /// Disables profiling for the current session, with an option to reset collected statistics.
+ ///
+ /// If profiling is already disabled, calling this method has no effect. Resetting statistics
+ /// clears all previously collected profiling data, which cannot be recovered.
+ /// true to reset all collected profiling statistics after disabling profiling; otherwise, false.
+ public void DisableProfiling(bool resetStatistics = false)
+ {
+ // stop
+ statistics?.StopTimer();
+
+ if (resetStatistics)
+ {
+ ResetStatistics();
+ }
+
+ DisableProfiling();
+ }
+
+ ///
+ /// Resets all collected profiling statistics to their initial state.
+ ///
+ /// This method has no effect if profiling is not enabled. Use this method to clear accumulated
+ /// profiling data before starting a new measurement period.
+ public void ResetStatistics()
+ {
+ if (IsProfilingEnabled)
+ {
+ statistics?.Reset();
+ }
+ }
+
+ ///
+ /// Initializes the profiling statistics for the current connection.
+ ///
+ /// This method prepares internal data structures to collect and store profiling information
+ /// related to the connection's activity. It should be called before attempting to access profiling or statistics
+ /// data for the connection.
+ private void InitProfiling()
+ {
+ EnsureConnectionOpen();
+
+ if (IsProfilingEnabled)
+ {
+ // Dispose any existing statistics instance to ensure fresh state and remove previous mapping
+ // from the global ConnectionStatistics cache.
+ try
+ {
+ statistics?.Dispose();
+ }
+ finally
+ {
+ statistics = new ConnectionStatistics(NativeConnection, profilingEnabled, this.profilingOptions);
+ }
+ LoadStatisticsProfile();
+ }
+ }
+
+ ///
+ /// Modifies the current profiling options used for collecting performance statistics.
+ ///
+ /// The new profiling options to apply. Cannot be null.
+ /// Thrown if profiling is not enabled when attempting to edit profiling options.
+ public void EditProfilingOptions(ProfilingOptions options)
+ {
+ if (!IsProfilingEnabled)
+ {
+ throw new InvalidOperationException("Profiling must be enabled to edit profiling options.");
+ }
+
+ this.profilingOptions = options;
+
+ if (connectionReference?.FileReferenceCounter is { } fileRefCounter)
+ {
+ fileRefCounter.ProfilingOptions = options;
+ }
+
+ statistics?.Reset();
+ LoadStatisticsProfile();
+ }
+
+ ///
+ /// Disables query profiling for the current database connection if profiling is enabled.
+ ///
+ /// This method has no effect if profiling is already disabled. Disabling profiling may improve
+ /// performance for subsequent queries.
+ /// Thrown if an error occurs while disabling profiling on the database connection.
+ private void DisableProfiling()
+ {
+ if (IsProfilingEnabled && State == ConnectionState.Open)
+ {
+ var state = NativeMethods.Query.DuckDBQuery(NativeConnection, "CALL disable_profiling();", out var queryResult);
+ EnsureStateIsSuccess(state, queryResult, "Error disabling profiling.");
+
+ if (this.profilingOptions is { OutputPath: not null })
+ {
+ // If output path is set, we need to disable profiling at the end of each session to ensure the file is properly flushed and closed.
+ state = NativeMethods.Query.DuckDBQuery(NativeConnection, "SET profiling_output = ''", out var queryResult2);
+ EnsureStateIsSuccess(state, queryResult2, "Error disabling profiling.");
+ }
+ }
+
+ this.profilingOptions = null;
+
+ if (connectionReference?.FileReferenceCounter is { } fileRefCounter)
+ {
+ fileRefCounter.IsProfilingEnabled = false;
+ fileRefCounter.ProfilingOptions = null;
+ }
+
+ statistics?.DisableQueryExecutionTracing();
+ profilingEnabled = false;
+ }
+
+ ///
+ /// Configures and enables query profiling for the current database connection based on the specified profiling
+ /// options.
+ ///
+ /// Profiling is only enabled for original in-memory connections; duplicated in-memory
+ /// connections share the same profiling state and are not reconfigured. This method applies settings such as
+ /// profiling format, coverage, mode, output path, and enabled metrics according to the current profiling
+ /// options.
+ /// Thrown if an error occurs while setting profiling configuration options.
+ private void LoadStatisticsProfile()
+ {
+ statistics?.StartTimer();
+
+ if (IsProfilingEnabled && profilingOptions is { } options)
+ {
+ // Build SQL and emit lightweight diagnostics to help reproduce issues when tests run together
+ var sql = ProfilingOptionsExtensions.ToDuckDBProfilingOptionString(options);
+
+ var state = NativeMethods.Query.DuckDBQuery(NativeConnection, sql, out var queryResult);
+ EnsureStateIsSuccess(state, queryResult, "Error configuring profiling settings.");
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void EnsureStateIsSuccess(DuckDBState state, DuckDBResult result, string fallbackMessage)
+ {
+ try
+ {
+ if (!state.IsSuccess())
+ {
+ var errorMessage = NativeMethods.Query.DuckDBResultError(ref result);
+ var errorType = NativeMethods.Query.DuckDBResultErrorType(ref result);
+
+ if (string.IsNullOrEmpty(errorMessage))
+ {
+ errorMessage = fallbackMessage;
+ }
+
+ if (errorType == DuckDBErrorType.Interrupt)
+ {
+ throw new OperationCanceledException();
+ }
+
+ var innerException = UdfExceptionStore.Retrieve(NativeConnection);
+ throw innerException != null
+ ? new DuckDBException(errorMessage, innerException)
+ : new DuckDBException(errorMessage, errorType);
+ }
+ }
+ finally
+ {
+ result.Close();
+ }
+ }
+
+ private void UpdateStatistics()
+ {
+ if (ConnectionState.Open == State)
+ {
+ // update timestamp
+ statistics?.StopTimer();
+ }
+
+ // delegate the rest of the work to the SqlStatistics class
+ statistics?.UpdateStatistics();
+ }
+ #endregion
}
diff --git a/DuckDB.NET.Data/Extensions/DuckDBMetricsExtensions.cs b/DuckDB.NET.Data/Extensions/DuckDBMetricsExtensions.cs
new file mode 100644
index 00000000..26bbf06f
--- /dev/null
+++ b/DuckDB.NET.Data/Extensions/DuckDBMetricsExtensions.cs
@@ -0,0 +1,92 @@
+using DuckDB.NET.Data.Profiling;
+using System.Collections.Frozen;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+
+namespace DuckDB.NET.Data.Extensions
+{
+ internal static class DuckDBMetricsExtensions
+ {
+ // Cache the snake_case conversions of DuckDBProfilingFormat enum values for efficient lookup.
+ private static readonly FrozenDictionary ProfilingFormatCache =
+ Enum.GetValues()
+ .ToFrozenDictionary(f => f, f => JsonNamingPolicy.SnakeCaseLower.ConvertName(f.ToString()));
+
+ // Cache the snake_case conversions of DuckDBMetricType enum values for efficient lookup.
+ private static readonly FrozenDictionary MetricTypeCache =
+ Enum.GetValues()
+ .ToFrozenDictionary(m => m, m => JsonNamingPolicy.SnakeCaseUpper.ConvertName(m.ToString()));
+
+ // Create a reverse lookup cache for string to DuckDBMetricType conversions.
+ private static readonly FrozenDictionary StringToMetricTypeCache =
+ MetricTypeCache.ToFrozenDictionary(kv => kv.Value, kv => kv.Key);
+
+ // create a reverse lookup cache for string to DuckDBProfilingFormat conversions.
+ private static readonly FrozenDictionary StringToProfilingFormatCache =
+ ProfilingFormatCache.ToFrozenDictionary(kv => kv.Value, kv => kv.Key);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static string ToDuckDBMetricTypeCollectionString(this DuckDBMetricTypeCollection metrics)
+ {
+ return $"{{{string.Join(",", metrics.Select(m => $"\"{m.ToDuckDBMetricTypeString()}\": \"TRUE\""))}}}";
+ }
+
+ ///
+ /// Converts the specified DuckDB profiling format to its corresponding string representation.
+ ///
+ /// The DuckDB profiling format to convert.
+ /// A string that represents the specified DuckDB profiling format.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static string ToDuckDBProfilingFormatString(this DuckDBProfilingFormat format)
+ {
+ return ProfilingFormatCache[format];
+ }
+
+ ///
+ /// Converts the specified DuckDB metric type to its corresponding string representation.
+ ///
+ /// The DuckDB metric type to convert.
+ /// A string that represents the specified DuckDB metric type.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static string ToDuckDBMetricTypeString(this DuckDBMetricType metricType)
+ {
+ if (metricType == DuckDBMetricType.RowsReturned)
+ {
+ throw new DuckDBException($"The '{DuckDBMetricType.RowsReturned}' metric is not supported yet.", DuckDBErrorType.InvalidInput);
+ }
+
+ return MetricTypeCache[metricType];
+ }
+
+ ///
+ /// Attempts to parse the specified string into a corresponding DuckDBMetricType value.
+ ///
+ /// The string representation of the DuckDB metric type to parse. Cannot be null.
+ /// When this method returns, contains the DuckDBMetricType value equivalent to the parsed string, if the conversion
+ /// succeeded; otherwise, the default value.
+ /// true if the string was successfully parsed into a DuckDBMetricType value; otherwise, false.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool TryParseDuckDBMetricType(string metricTypeString, out DuckDBMetricType metricType)
+ {
+ return StringToMetricTypeCache.TryGetValue(metricTypeString, out metricType);
+ }
+
+ ///
+ /// Attempts to parse the specified string into a corresponding DuckDB profiling format value.
+ ///
+ /// Use this method to safely convert a string to a DuckDB profiling format without
+ /// throwing an exception if the conversion fails.
+ /// The string representation of the DuckDB profiling format to parse. This value is compared case-sensitively
+ /// against known profiling format names.
+ /// When this method returns, contains the parsed DuckDB profiling format value if the parse operation succeeds;
+ /// otherwise, contains the default value.
+ /// true if the string was successfully parsed into a DuckDB profiling format; otherwise, false.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool TryParseDuckDBProfilingFormat(string profilingFormatString, out DuckDBProfilingFormat profilingFormat)
+ {
+ return StringToProfilingFormatCache.TryGetValue(profilingFormatString, out profilingFormat);
+ }
+
+ }
+}
diff --git a/DuckDB.NET.Data/Extensions/ProfilingOptionsExtensions.cs b/DuckDB.NET.Data/Extensions/ProfilingOptionsExtensions.cs
new file mode 100644
index 00000000..bba66011
--- /dev/null
+++ b/DuckDB.NET.Data/Extensions/ProfilingOptionsExtensions.cs
@@ -0,0 +1,38 @@
+using DuckDB.NET.Data.Profiling;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DuckDB.NET.Data.Extensions
+{
+ internal static class ProfilingOptionsExtensions
+ {
+ public static string ToDuckDBProfilingOptionString(this ProfilingOptions options)
+ {
+ var sb = new StringBuilder();
+
+ sb.AppendLine("CALL enable_profiling(");
+ sb.AppendLine($" format := '{options.Format.ToDuckDBProfilingFormatString()}',");
+ sb.AppendLine($" coverage := '{options.Coverage}',");
+ sb.AppendLine($" mode := '{options.Mode}'");
+
+ if (!string.IsNullOrEmpty(options.OutputPath))
+ sb.AppendLine($" ,save_location := '{options.OutputPath.Replace("'", "''")}'");
+
+ if (options.EnabledMetrics is { Count: > 0 })
+ sb.AppendLine($" ,metrics := '{(options.EnabledMetrics).ToDuckDBMetricTypeCollectionString()}'");
+ else
+ sb.AppendLine($" ,metrics := '{DuckDBMetrics.DefaultMetrics.ToDuckDBMetricTypeCollectionString()}'");
+
+ sb.AppendLine(");");
+
+ // If no output path is provided, disable profiling output to file
+ if (string.IsNullOrEmpty(options.OutputPath))
+ sb.AppendLine("SET profiling_output = '';");
+
+ return sb.ToString();
+ }
+ }
+}
diff --git a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs
index 435dfc58..44125bf4 100644
--- a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs
+++ b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs
@@ -93,6 +93,7 @@ public static DuckDBValue ToDuckDBValue(this object? item, DuckDBLogicalType log
(DuckDBType.Blob, byte[] value) => NativeMethods.Value.DuckDBCreateBlob(value, value.Length),
(DuckDBType.List, ICollection value) => CreateCollectionValue(logicalType, value, true, dbType),
(DuckDBType.Array, ICollection value) => CreateCollectionValue(logicalType, value, false, dbType),
+ (DuckDBType.Map, IDictionary value) => CreateMapValue(logicalType, value),
_ when ValueCreators.TryGetValue(dbType, out var converter) => converter(item),
_ => NativeMethods.Value.DuckDBCreateVarchar(item.ToString())
};
@@ -139,6 +140,29 @@ private static DuckDBValue CreateCollectionValue(DuckDBLogicalType logicalType,
: NativeMethods.Value.DuckDBCreateArrayValue(collectionItemType, values, collection.Count);
}
+ private static DuckDBValue CreateMapValue(DuckDBLogicalType logicalType, IDictionary dictionary)
+ {
+ //get key and value types from logical type
+ using var keyType = NativeMethods.LogicalType.DuckDBMapTypeKeyType(logicalType);
+ using var valueType = NativeMethods.LogicalType.DuckDBMapTypeValueType(logicalType);
+
+ //get duckdb types for key and value types
+ var keyDuckDBType = NativeMethods.LogicalType.DuckDBGetTypeId(keyType);
+ var valueDuckDBType = NativeMethods.LogicalType.DuckDBGetTypeId(valueType);
+
+ var keys = new DuckDBValue[dictionary.Count];
+ var values = new DuckDBValue[dictionary.Count];
+ var index = 0;
+ foreach (DictionaryEntry entry in dictionary)
+ {
+ keys[index] = entry.Key.ToDuckDBValue(keyType, keyDuckDBType, DbType.Object);
+ values[index] = entry.Value.ToDuckDBValue(valueType, valueDuckDBType, DbType.Object);
+ index++;
+ }
+
+ return NativeMethods.Value.DuckDBCreateMapValue(logicalType, keys, values, dictionary.Count);
+ }
+
private static DuckDBValue DecimalToDuckDBValue(decimal value)
{
var mantissa = value.GetMantissa();
diff --git a/DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs b/DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs
index d074baab..2733a9ae 100644
--- a/DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs
+++ b/DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs
@@ -1,11 +1,13 @@
-using System.Linq;
using DuckDB.NET.Data.Connection;
+using DuckDB.NET.Data.Profiling.Statistics;
+using System.Linq;
namespace DuckDB.NET.Data.PreparedStatement;
internal sealed class PreparedStatement : IDisposable
{
private readonly DuckDBPreparedStatement statement;
+ private StatementProfiler? statementProfiler;
private PreparedStatement(DuckDBPreparedStatement statement)
{
@@ -18,67 +20,119 @@ public static IEnumerable PrepareMultiple(DuckDBNativeConnection c
using (extractedStatements)
{
+ _ = ConnectionStatistics.TryGetFor(connection, out var stats);
+ // Initialize the query profiler for the entire batch of statements. The profiler will be responsible for tracking the execution of all statements within this batch.
+ var queryProfiler = stats?.CreateQueryProfiler(statementCount);
+ queryProfiler?.StartTimer();
+
if (statementCount <= 0)
{
var error = NativeMethods.ExtractStatements.DuckDBExtractStatementsError(extractedStatements);
+
+ queryProfiler?.SetState(DuckDBState.Error, DuckDBErrorType.Parser, error);
+ queryProfiler?.StopTimer();
+
throw new DuckDBException(error);
}
- for (int index = 0; index < statementCount; index++)
+ try
{
- var status = NativeMethods.ExtractStatements.DuckDBPrepareExtractedStatement(connection, extractedStatements, index, out var statement);
- if (status.IsSuccess())
+ for (int index = 0; index < statementCount; index++)
{
- using var preparedStatement = new PreparedStatement(statement);
- yield return preparedStatement.Execute(parameters, useStreamingMode, connection);
- }
- else
- {
- var errorMessage = NativeMethods.PreparedStatements.DuckDBPrepareError(statement);
+ var status = NativeMethods.ExtractStatements.DuckDBPrepareExtractedStatement(connection, extractedStatements, index, out var statement);
- if (string.IsNullOrEmpty(errorMessage))
+ var statementProfiler = queryProfiler?.CreateStatementProfiler(index);
+
+ if (status.IsSuccess())
{
- errorMessage = "DuckDBQuery failed";
+ using var preparedStatement = new PreparedStatement(statement);
+ preparedStatement.statementProfiler = statementProfiler;
+
+ var result = preparedStatement.Execute(parameters, useStreamingMode, connection);
+
+ // Stop the query profiler after the last statement has been executed.
+ // This ensures that the total execution time for the entire batch of statements is accurately captured
+ // and not after the data retrieval of the last statement.
+ if (index == statementCount - 1)
+ {
+ queryProfiler?.StopTimer();
+ }
+
+ yield return result;
}
+ else
+ {
+ var errorMessage = NativeMethods.PreparedStatements.DuckDBPrepareError(statement);
+
+ if (string.IsNullOrEmpty(errorMessage))
+ {
+ errorMessage = "DuckDBQuery failed";
+ }
- throw new DuckDBException(errorMessage, UdfExceptionStore.Retrieve(connection));
+ // Initialize the statement profiler for the current statement. This allows for detailed profiling of each individual statement within the batch
+ statementProfiler?.SetState(status, errorMessage);
+
+ throw new DuckDBException(errorMessage, UdfExceptionStore.Retrieve(connection));
+ }
}
}
+ finally
+ {
+ // Ensure that the query profiler is stopped in case of any exceptions during statement preparation or execution.
+ queryProfiler?.StopTimer();
+ }
}
}
private DuckDBResult Execute(DuckDBParameterCollection parameterCollection, bool useStreamingMode, DuckDBNativeConnection connection)
{
- BindParameters(statement, parameterCollection);
+ // Start the statement profiler timer to measure the execution time of this statement.
+ // The profiler will also capture any relevant metrics or state changes during execution.
+ var profiler = this.statementProfiler;
+ profiler?.StartTimer();
- var status = useStreamingMode
- ? NativeMethods.PreparedStatements.DuckDBExecutePreparedStreaming(statement, out var queryResult)
- : NativeMethods.PreparedStatements.DuckDBExecutePrepared(statement, out queryResult);
-
- if (!status.IsSuccess())
+ try
{
- var errorMessage = NativeMethods.Query.DuckDBResultError(ref queryResult);
- var errorType = NativeMethods.Query.DuckDBResultErrorType(ref queryResult);
- queryResult.Close();
- if (string.IsNullOrEmpty(errorMessage))
- {
- errorMessage = "DuckDB execution failed";
- }
+ BindParameters(statement, parameterCollection);
+
+ var status = useStreamingMode
+ ? NativeMethods.PreparedStatements.DuckDBExecutePreparedStreaming(statement, out var queryResult)
+ : NativeMethods.PreparedStatements.DuckDBExecutePrepared(statement, out queryResult);
- if (errorType == DuckDBErrorType.Interrupt)
+ if (!status.IsSuccess())
{
- throw new OperationCanceledException();
+ var errorMessage = NativeMethods.Query.DuckDBResultError(ref queryResult);
+ var errorType = NativeMethods.Query.DuckDBResultErrorType(ref queryResult);
+ queryResult.Close();
+
+ if (string.IsNullOrEmpty(errorMessage))
+ {
+ errorMessage = "DuckDB execution failed";
+ }
+
+ profiler?.SetState(status, errorType, errorMessage);
+
+ if (errorType == DuckDBErrorType.Interrupt)
+ {
+ throw new OperationCanceledException();
+ }
+
+ var innerException = UdfExceptionStore.Retrieve(connection);
+ throw innerException != null
+ ? new DuckDBException(errorMessage, innerException)
+ : new DuckDBException(errorMessage, errorType);
}
- var innerException = UdfExceptionStore.Retrieve(connection);
- throw innerException != null
- ? new DuckDBException(errorMessage, innerException)
- : new DuckDBException(errorMessage, errorType);
- }
+ profiler?.AcquireMetrics();
- return queryResult;
+ return queryResult;
+ }
+ finally
+ {
+ profiler?.StopTimer();
+ }
}
private static void BindParameters(DuckDBPreparedStatement preparedStatement, DuckDBParameterCollection parameterCollection)
diff --git a/DuckDB.NET.Data/Profiling/DuckDBMetricTypeCollection.cs b/DuckDB.NET.Data/Profiling/DuckDBMetricTypeCollection.cs
new file mode 100644
index 00000000..6a71e8f6
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/DuckDBMetricTypeCollection.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace DuckDB.NET.Data.Profiling
+{
+ public class DuckDBMetricTypeCollection : List
+ {
+ public DuckDBMetricTypeCollection() { }
+
+ public DuckDBMetricTypeCollection(IEnumerable collection) : base(collection) { }
+ }
+}
diff --git a/DuckDB.NET.Data/Profiling/DuckDBMetrics.cs b/DuckDB.NET.Data/Profiling/DuckDBMetrics.cs
new file mode 100644
index 00000000..a9e65ad4
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/DuckDBMetrics.cs
@@ -0,0 +1,48 @@
+using System.Linq;
+
+namespace DuckDB.NET.Data.Profiling
+{
+ public static class DuckDBMetrics
+ {
+ public static readonly DuckDBMetricTypeCollection AllMetrics = new DuckDBMetricTypeCollection(Enum.GetValues().Cast());
+
+ public static readonly DuckDBMetricTypeCollection DefaultMetrics = new DuckDBMetricTypeCollection(
+ [
+ // Basic query metadata
+ DuckDBMetricType.QueryName,
+ DuckDBMetricType.Latency,
+ DuckDBMetricType.CpuTime,
+ DuckDBMetricType.BlockedThreadTime,
+
+ // Result and cardinality
+ DuckDBMetricType.ResultSetSize,
+ // DuckDBMetricType.RowsReturned, // Not enabled due to known issue
+ DuckDBMetricType.CumulativeCardinality,
+ DuckDBMetricType.CumulativeRowsScanned,
+ DuckDBMetricType.ExtraInfo,
+
+ // Operator-level metrics
+ DuckDBMetricType.OperatorCardinality,
+ DuckDBMetricType.OperatorName,
+ DuckDBMetricType.OperatorRowsScanned,
+ DuckDBMetricType.OperatorTiming,
+ DuckDBMetricType.OperatorType,
+
+ // Memory / IO / storage metrics
+ DuckDBMetricType.TotalBytesRead,
+ DuckDBMetricType.TotalBytesWritten,
+ DuckDBMetricType.TotalMemoryAllocated,
+ DuckDBMetricType.SystemPeakBufferMemory,
+ DuckDBMetricType.SystemPeakTempDirSize,
+
+ // WAL / attach / checkpoint metrics
+ DuckDBMetricType.AttachLoadStorageLatency,
+ DuckDBMetricType.AttachReplayWalLatency,
+ DuckDBMetricType.CheckpointLatency,
+ DuckDBMetricType.CommitLocalStorageLatency,
+ DuckDBMetricType.WaitingToAttachLatency,
+ DuckDBMetricType.WalReplayEntryCount,
+ DuckDBMetricType.WriteToWalLatency
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/DuckDB.NET.Data/Profiling/DuckDBProfilingInfoWrapper.cs b/DuckDB.NET.Data/Profiling/DuckDBProfilingInfoWrapper.cs
new file mode 100644
index 00000000..cacc32b3
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/DuckDBProfilingInfoWrapper.cs
@@ -0,0 +1,62 @@
+namespace DuckDB.NET.Native;
+
+public class DuckDBProfilingInfoWrapper : IDisposable
+{
+ private DuckDBProfilingInfo handle;
+ private bool disposed;
+
+ public DuckDBProfilingInfoWrapper(DuckDBProfilingInfo handle)
+ {
+ ArgumentNullException.ThrowIfNull(handle);
+
+ this.handle = handle;
+ }
+
+ public static DuckDBProfilingInfoWrapper? GetProfilingInfo(DuckDBNativeConnection connection)
+ {
+ ArgumentNullException.ThrowIfNull(connection);
+
+ var handle = NativeMethods.ProfilingInfo.DuckDBGetProfilingInfo(connection);
+ if (handle.IsInvalid || handle.IsClosed)
+ {
+ return null;
+ }
+ return new DuckDBProfilingInfoWrapper(handle);
+ }
+
+ public DuckDBValue GetValue(string key)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+
+ return NativeMethods.ProfilingInfo.DuckDBProfilingInfoGetValue(handle, key);
+ }
+
+ public DuckDBValue GetMetrics()
+ {
+ return NativeMethods.ProfilingInfo.DuckDBProfilingInfoGetMetrics(handle);
+ }
+
+ public ulong GetChildCount()
+ {
+ return NativeMethods.ProfilingInfo.DuckDBProfilingInfoGetChildCount(handle);
+ }
+
+ public DuckDBProfilingInfoWrapper GetChild(ulong index)
+ {
+ var childHandle = NativeMethods.ProfilingInfo.DuckDBProfilingInfoGetChild(handle, index);
+ if (childHandle.IsInvalid || childHandle.IsClosed)
+ {
+ throw new InvalidOperationException($"Failed to get child profiling info at index {index}.");
+ }
+ return new DuckDBProfilingInfoWrapper(childHandle);
+ }
+
+ public void Dispose()
+ {
+ if (!disposed)
+ {
+ handle?.Dispose();
+ disposed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/DuckDB.NET.Data/Profiling/MetricType.cs b/DuckDB.NET.Data/Profiling/MetricType.cs
new file mode 100644
index 00000000..e69de29b
diff --git a/DuckDB.NET.Data/Profiling/ProfilingInfo.cs b/DuckDB.NET.Data/Profiling/ProfilingInfo.cs
new file mode 100644
index 00000000..2e919912
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/ProfilingInfo.cs
@@ -0,0 +1,59 @@
+using DuckDB.NET.Data.Profiling.Statistics;
+
+internal sealed class ProfilingInfo: IDisposable
+{
+ private readonly DuckDBNativeConnection connection;
+ private DuckDBProfilingInfoWrapper? duckDBProfilingInfoWrapper;
+ private bool isDisposed = false;
+
+ internal ProfilingInfo(DuckDBNativeConnection connection)
+ {
+ this.connection = connection;
+ }
+
+ ///
+ /// Tries to prepare the profiling info by retrieving it from the connection. Returns true if successful, false otherwise.
+ ///
+ ///
+ internal bool TryPrepare()
+ {
+ var profilingInfo = DuckDBProfilingInfoWrapper.GetProfilingInfo(connection);
+
+ if (profilingInfo == null)
+ {
+ return false;
+ }
+
+ duckDBProfilingInfoWrapper = profilingInfo;
+ return true;
+ }
+
+ ///
+ /// Gets the raw metrics as a dictionary of string key-value pairs. Throws an exception if the profiling info is not prepared.
+ ///
+ ///
+ ///
+ internal Dictionary GetRawMetrics()
+ {
+ if (duckDBProfilingInfoWrapper == null)
+ {
+ throw new InvalidOperationException("Profiling info is not prepared. Call TryPrepare() first.");
+ }
+
+ using var metricsValue = duckDBProfilingInfoWrapper.GetMetrics();
+ if (metricsValue.IsNull())
+ {
+ return [];
+ }
+ return metricsValue.GetMapValue();
+ }
+
+ public void Dispose()
+ {
+ if (!isDisposed)
+ {
+ duckDBProfilingInfoWrapper?.Dispose();
+ isDisposed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/DuckDB.NET.Data/Profiling/ProfilingOptions.cs b/DuckDB.NET.Data/Profiling/ProfilingOptions.cs
new file mode 100644
index 00000000..36dbef1b
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/ProfilingOptions.cs
@@ -0,0 +1,36 @@
+namespace DuckDB.NET.Data.Profiling
+{
+ public readonly struct ProfilingOptions
+ {
+ ///
+ /// The profiling output format to use when generating profiling results.
+ ///
+ public DuckDBProfilingFormat Format { get; init; }
+
+ ///
+ /// The profiling mode used for DuckDB query execution.
+ ///
+ public DuckDBProfilingMode Mode { get; init; }
+
+ ///
+ /// The profiling coverage settings used for DuckDB query analysis.
+ ///
+ public DuckDBProfilingCoverage Coverage { get; init; }
+
+ ///
+ /// The output directory path where generated files will be placed.
+ ///
+ public string OutputPath { get; init; }
+
+ ///
+ /// The collection of enabled metrics for profiling.
+ ///
+ public DuckDBMetricTypeCollection EnabledMetrics { get; init; }
+
+ ///
+ /// The threshold (in milliseconds) used for metrics acquisition.
+ ///
+ /// Execution times below this threshold may not trigger metrics collection.
+ public int MetricsThreshold { get; init; }
+ }
+}
diff --git a/DuckDB.NET.Data/Profiling/Statistics/ConnectionStatistics.cs b/DuckDB.NET.Data/Profiling/Statistics/ConnectionStatistics.cs
new file mode 100644
index 00000000..288201dc
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/Statistics/ConnectionStatistics.cs
@@ -0,0 +1,181 @@
+using DuckDB.NET.Data.Common;
+using DuckDB.NET.Data.Profiling.Statistics.Summary;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace DuckDB.NET.Data.Profiling.Statistics;
+
+internal sealed class ConnectionStatistics : ExecutionProfiler, IDisposable
+{
+
+ // internal values that are not exposed through properties
+ private static readonly ConcurrentDictionary ByNativeConnection = new();
+
+ private bool enableQueryExecutionTracing;
+ private readonly ConcurrentDictionary queryProfilers = new();
+ private bool isDisposed = false;
+ private int nextQueryId = 0;
+
+ // internal values that are exposed through properties
+ internal long connectionTime;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The native DuckDB connection.
+ /// Indicates whether query execution tracing is enabled.
+ /// The profiling options for the connection.
+ internal ConnectionStatistics(DuckDBNativeConnection duckDBNativeConnection, bool enableQueryExecutionTracing = false, ProfilingOptions? profilingOptions = default)
+ : base(duckDBNativeConnection, profilingOptions)
+ {
+ this.enableQueryExecutionTracing = enableQueryExecutionTracing;
+
+ // Add or update the mapping for the native connection to this ConnectionStatistics instance
+ ByNativeConnection.AddOrUpdate(duckDBNativeConnection, this, (_, _) => this);
+ }
+
+ ///
+ /// Enables tracing of query execution for diagnostic or debugging purposes.
+ ///
+ /// After calling this method, additional diagnostic information about query execution may be collected
+ /// or logged. This can assist in troubleshooting or performance analysis. Tracing remains enabled until explicitly
+ /// disabled
+ internal void EnableQueryExecutionTracing()
+ {
+ this.enableQueryExecutionTracing = true;
+ }
+
+ ///
+ /// Disables tracing of query execution for the current instance.
+ ///
+ /// After calling this method, query execution tracing will no longer be performed until explicitly
+ /// re-enabled. This may affect the ability to diagnose or audit query behavior.
+ internal void DisableQueryExecutionTracing()
+ {
+ this.enableQueryExecutionTracing = false;
+ }
+
+ ///
+ /// Attempts to retrieve a associated with the specified connection.
+ ///
+ /// The native DuckDB connection for which to obtain statistics.
+ /// When this method returns, contains the statistics for the specified connection if found; otherwise, .
+ /// if statistics are found for the specified connection; otherwise, .
+ internal static bool TryGetFor(DuckDBNativeConnection nativeConn, out ConnectionStatistics? stats)
+ {
+ // Fast path: if no connections have profiling enabled, skip the lookup entirely
+ if (ByNativeConnection.IsEmpty)
+ {
+ stats = null;
+ return false;
+ }
+ return ByNativeConnection.TryGetValue(nativeConn, out stats);
+ }
+
+ ///
+ /// Creates a new for the specified query if query execution tracing is enabled.
+ ///
+ /// The number of statements in the query to be profiled. Must be non-negative.
+ /// A new instance of if query execution tracing is enabled; otherwise, .
+ internal QueryProfiler? CreateQueryProfiler(int statementCount)
+ {
+ if (!enableQueryExecutionTracing)
+ {
+ return null;
+ }
+
+ var id = GetNextQueryId();
+
+ var queryProfiler = new QueryProfiler(id, statementCount, duckDBNativeConnection, profilingOptions);
+ queryProfilers[id] = queryProfiler;
+ return queryProfiler;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int GetNextQueryId()
+ {
+ return Interlocked.Increment(ref nextQueryId);
+ }
+
+ ///
+ /// Stops all active timers for the connection and its associated query profilers, recording their elapsed times.
+ ///
+ public override void StopTimer()
+ {
+ foreach (var profiler in queryProfilers.Values)
+ {
+ profiler?.StopTimer();
+ }
+
+ base.StopTimer();
+ }
+
+ internal void UpdateStatistics()
+ {
+ // update connection time
+ if (endTime >= startTime && long.MaxValue > (endTime - startTime).Ticks)
+ {
+ connectionTime = (endTime - startTime).Ticks;
+ }
+ else
+ {
+ connectionTime = long.MaxValue;
+ }
+ }
+
+ private IEnumerable ReadSummaries()
+ {
+ foreach (var queryProfiler in queryProfilers.Values.OrderBy(qp => qp.StartTime))
+ {
+ yield return queryProfiler.GetSummary();
+ }
+ }
+
+ internal ProfilingSummary GetProfilingSummary()
+ {
+ var connectionTimeMs = TimerUtils.TimeSpanTicksToMilliseconds(connectionTime);
+
+ // QueryProfiler.ExecutionTime is already exposed in milliseconds via ExecutionTime property
+ var executionTimeMs = queryProfilers.Values.Sum(qp => (double)qp.ExecutionTime);
+
+ return new ProfilingSummary(
+ startTime,
+ endTime,
+ connectionTimeMs,
+ executionTimeMs,
+ queryProfilers.Values.Count,
+ [.. ReadSummaries()]);
+ }
+
+ public override void Reset()
+ {
+ executionTime = 0;
+ startTimestamp = null;
+ connectionTime = 0;
+ startTime = default;
+ endTime = default;
+
+ queryProfilers.Clear();
+
+ StartTimer();
+ }
+
+ // call on connection close/dispose
+ private void RemoveMapping()
+ {
+ ByNativeConnection.TryRemove(duckDBNativeConnection, out _);
+ }
+
+ public void Dispose()
+ {
+ if (!isDisposed)
+ {
+ RemoveMapping();
+ isDisposed = true;
+ }
+ }
+}
diff --git a/DuckDB.NET.Data/Profiling/Statistics/ExecutionProfiler.cs b/DuckDB.NET.Data/Profiling/Statistics/ExecutionProfiler.cs
new file mode 100644
index 00000000..b568e73a
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/Statistics/ExecutionProfiler.cs
@@ -0,0 +1,111 @@
+using DuckDB.NET.Data.Common;
+
+namespace DuckDB.NET.Data.Profiling.Statistics;
+
+internal abstract class ExecutionProfiler(DuckDBNativeConnection duckDBNativeConnection, ProfilingOptions? profilingOptions) : IExecutionProfiler
+{
+ protected readonly DuckDBNativeConnection duckDBNativeConnection = duckDBNativeConnection;
+ protected readonly ProfilingOptions? profilingOptions = profilingOptions;
+
+ protected DuckDBState state;
+ protected string? errorMessage;
+
+ protected long? startTimestamp;
+ protected long executionTime;
+ protected DateTimeOffset startTime;
+ protected DateTimeOffset endTime;
+
+ internal long ExecutionTime => TimerUtils.TimerToMilliseconds(executionTime);
+ internal DateTimeOffset StartTime => startTime;
+ internal DateTimeOffset EndTime => endTime;
+
+ ///
+ /// Starts the timer, initiating the timing operation.
+ ///
+ public virtual void StartTimer()
+ {
+ if (!startTimestamp.HasValue)
+ {
+ startTimestamp = TimerUtils.TimerCurrent();
+ startTime = DateTimeOffset.UtcNow;
+ }
+ }
+
+ ///
+ /// Stops the currently running timer, if active.
+ ///
+ /// Call this method to halt the timer and record its elapsed time. If the timer is not running, calling
+ /// this method has no effect.
+ public virtual void StopTimer()
+ {
+ ReleaseAndUpdateExecutionTimer();
+ }
+
+ ///
+ /// Releases the current execution timer and updates the accumulated execution time and end time for the operation.
+ ///
+ protected virtual void ReleaseAndUpdateExecutionTimer()
+ {
+ if (startTimestamp.HasValue)
+ {
+ long elapsed = TimerUtils.CalculateTickCountElapsed(startTimestamp.Value, TimerUtils.TimerCurrent());
+ executionTime += elapsed;
+
+ // Convert the total accumulated high-resolution ticks to a TimeSpan and apply to the wall-clock start time.
+ // Use the cumulative executionTime so that multiple start/stop segments are represented correctly.
+ var totalSpan = TimerUtils.TimerToTimeSpan(executionTime);
+ endTime = startTime.Add(totalSpan);
+
+ startTimestamp = null;
+ }
+ }
+
+ ///
+ /// Collects and updates performance or usage metrics for the current instance.
+ ///
+ public virtual void AcquireMetrics() { }
+
+ ///
+ /// Resets the object to its initial state.
+ ///
+ public virtual void Reset() { }
+
+ ///
+ /// Gets the current state of the current operation, indicating success or error
+ ///
+ public DuckDBState State => state;
+
+ ///
+ /// Gets the error message associated with the current operation.
+ ///
+ public string ErrorMessage => errorMessage ?? string.Empty;
+
+ ///
+ /// Sets the current state of the DuckDB connection or operation.
+ ///
+ /// The new state to assign to the DuckDB instance.
+ public void SetState(DuckDBState state) => this.state = state;
+
+ ///
+ /// Sets the current state and associated error message for the DuckDB instance.
+ ///
+ /// The new state to assign to the DuckDB instance.
+ /// The error message to associate with the specified state. Can be null or empty if no error message is required.
+ public void SetState(DuckDBState state, string message)
+ {
+ this.state = state;
+ this.errorMessage = message;
+ }
+
+ ///
+ /// Sets the current state and error information for the DuckDB connection or operation.
+ ///
+ /// The new state to assign to the DuckDB connection or operation.
+ /// The type of error associated with the state change. Used to categorize the error message.
+ /// A descriptive message providing details about the error or state change. Cannot be null.
+ public void SetState(DuckDBState state, DuckDBErrorType errorType, string message)
+ {
+ this.state = state;
+ this.errorMessage = $"{errorType} error: {message}";
+ }
+}
\ No newline at end of file
diff --git a/DuckDB.NET.Data/Profiling/Statistics/IExecutionProfiler.cs b/DuckDB.NET.Data/Profiling/Statistics/IExecutionProfiler.cs
new file mode 100644
index 00000000..854fca3c
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/Statistics/IExecutionProfiler.cs
@@ -0,0 +1,14 @@
+namespace DuckDB.NET.Data.Profiling.Statistics
+{
+ // Single interface — replaces both IExecutionTracerBase and IExecutionProfiler
+ internal interface IExecutionProfiler
+ {
+ void StartTimer();
+ void StopTimer();
+ void SetState(DuckDBState state);
+ void SetState(DuckDBState state, string message);
+ void SetState(DuckDBState state, DuckDBErrorType errorType, string message);
+ void AcquireMetrics();
+ DuckDBState State { get; }
+ }
+}
\ No newline at end of file
diff --git a/DuckDB.NET.Data/Profiling/Statistics/ProfilingInfoMetrics.cs b/DuckDB.NET.Data/Profiling/Statistics/ProfilingInfoMetrics.cs
new file mode 100644
index 00000000..645cb9aa
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/Statistics/ProfilingInfoMetrics.cs
@@ -0,0 +1,22 @@
+namespace DuckDB.NET.Data.Profiling.Statistics;
+
+
+public sealed class ProfilingInfoMetrics : Dictionary
+{
+ internal static ProfilingInfoMetrics FromRawMetrics(Dictionary dict)
+ {
+ ArgumentNullException.ThrowIfNull(dict);
+
+ ProfilingInfoMetrics result = [];
+
+ foreach (var kvp in dict)
+ {
+ if (DuckDBMetricsExtensions.TryParseDuckDBMetricType(kvp.Key, out var metricType))
+ {
+ result.Add(metricType, kvp.Value);
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/DuckDB.NET.Data/Profiling/Statistics/QueryProfiler.cs b/DuckDB.NET.Data/Profiling/Statistics/QueryProfiler.cs
new file mode 100644
index 00000000..89ee2507
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/Statistics/QueryProfiler.cs
@@ -0,0 +1,69 @@
+using DuckDB.NET.Data.Common;
+using DuckDB.NET.Data.Profiling.Statistics.Summary;
+using System.Linq;
+
+namespace DuckDB.NET.Data.Profiling.Statistics;
+
+internal sealed class QueryProfiler(int queryIdentifier, int statementCount, DuckDBNativeConnection connection, ProfilingOptions? profilingOptions)
+: ExecutionProfiler(connection, profilingOptions)
+{
+ private readonly StatementProfiler[] statementProfilers = new StatementProfiler[statementCount];
+
+
+ internal int StatementCount => statementCount;
+
+ internal int QueryIdentifier => queryIdentifier;
+
+ internal StatementProfiler CreateStatementProfiler(int index)
+ {
+ var profiler = new StatementProfiler(index, duckDBNativeConnection, profilingOptions);
+ statementProfilers[index] = profiler;
+ return profiler;
+ }
+
+ ///
+ /// Creates a summary of the profiling query, including execution times, statement information, and state.
+ ///
+ /// A object containing details about the query's execution, statements, state, and
+ /// any error message.
+ internal ProfilingQuerySummary GetSummary()
+ {
+ var childWithError = statementProfilers.Where(sp => sp?.State == DuckDBState.Error).FirstOrDefault();
+
+ state = state == DuckDBState.Error ? state : (childWithError?.State ?? DuckDBState.Success);
+ errorMessage = !string.IsNullOrEmpty(ErrorMessage) ? ErrorMessage : (childWithError?.ErrorMessage ?? string.Empty);
+
+ return new ProfilingQuerySummary(
+ startTime,
+ endTime,
+ TimerUtils.TimerToMilliseconds(executionTime),
+ statementCount,
+ [.. statementProfilers.Select(sp => sp?.GetSummary() ?? default)],
+ state,
+ errorMessage
+ );
+ }
+
+ ///
+ /// Stops all the statement profilers associated with this query profiler.
+ ///
+ public override void StopTimer()
+ {
+
+ foreach (var profiler in statementProfilers)
+ {
+ profiler?.StopTimer();
+ }
+
+ base.StopTimer();
+ }
+
+ ///
+ public override void Reset()
+ {
+ executionTime = 0;
+ startTimestamp = null;
+ startTime = default;
+ endTime = default;
+ }
+}
diff --git a/DuckDB.NET.Data/Profiling/Statistics/StatementProfiler.cs b/DuckDB.NET.Data/Profiling/Statistics/StatementProfiler.cs
new file mode 100644
index 00000000..a30be188
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/Statistics/StatementProfiler.cs
@@ -0,0 +1,68 @@
+using DuckDB.NET.Data.Common;
+using DuckDB.NET.Data.Profiling.Statistics.Summary;
+
+namespace DuckDB.NET.Data.Profiling.Statistics;
+
+internal sealed class StatementProfiler(int queryIndex, DuckDBNativeConnection connection, ProfilingOptions? profilingOptions)
+: ExecutionProfiler(connection, profilingOptions)
+{
+ private Dictionary rawMetrics = [];
+
+ ///
+ /// Gets the profiling metrics associated with the current operation.
+ ///
+ internal ProfilingInfoMetrics Info => ProfilingInfoMetrics.FromRawMetrics(rawMetrics);
+
+ ///
+ /// Creates a summary of the profiling statement, including execution timing, order, metrics, state, and any associated
+ /// message.
+ ///
+ /// A instance containing the collected profiling data for the statement.
+ internal ProfilingStatementSummary GetSummary()
+ {
+ return new ProfilingStatementSummary(
+ StartTime: startTime,
+ EndTime: endTime,
+ ExecutionTimeMilliseconds: TimerUtils.TimerToMilliseconds(executionTime),
+ Order: queryIndex,
+ Metrics: Info,
+ State: state,
+ Message: ErrorMessage);
+ }
+
+ ///
+ public override void AcquireMetrics()
+ {
+ // If a metrics threshold is set, check if the elapsed execution time meets the threshold before acquiring metrics.
+ var threshold = profilingOptions?.MetricsThreshold ?? 0;
+ if (threshold > 0)
+ {
+ // defensive: if timer wasn't started, skip acquiring metrics based on threshold
+ if (!startTimestamp.HasValue)
+ {
+ return;
+ }
+
+ long elapsed = TimerUtils.CalculateTickCountElapsed(startTimestamp.Value, TimerUtils.TimerCurrent());
+
+ if (TimerUtils.TimerToMilliseconds(elapsed) < threshold)
+ return;
+ }
+
+ using var profile = new ProfilingInfo(duckDBNativeConnection);
+
+ if (profile.TryPrepare())
+ {
+ rawMetrics = profile.GetRawMetrics();
+ }
+ }
+
+ ///
+ public override void Reset()
+ {
+ executionTime = 0;
+ startTimestamp = null;
+ startTime = default;
+ endTime = default;
+ }
+}
diff --git a/DuckDB.NET.Data/Profiling/Statistics/Summary/ProfilingQuerySummary.cs b/DuckDB.NET.Data/Profiling/Statistics/Summary/ProfilingQuerySummary.cs
new file mode 100644
index 00000000..ad52c64d
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/Statistics/Summary/ProfilingQuerySummary.cs
@@ -0,0 +1,25 @@
+namespace DuckDB.NET.Data.Profiling.Statistics.Summary;
+
+public readonly record struct ProfilingQuerySummary(
+ DateTimeOffset StartTime,
+ DateTimeOffset EndTime,
+ double ExecutionTimeMilliseconds,
+ int StatementCount,
+ ProfilingStatementSummary[] StatementSummaries,
+ DuckDBState State,
+ string Message)
+{
+ public IDictionary ToDictionary()
+ {
+ return new Dictionary(7)
+ {
+ ["StartTime"] = StartTime,
+ ["EndTime"] = EndTime,
+ ["ExecutionTime"] = ExecutionTimeMilliseconds,
+ ["StatementCount"] = StatementCount,
+ ["StatementSummaries"] = StatementSummaries,
+ ["State"] = State,
+ ["Message"] = Message
+ };
+ }
+}
diff --git a/DuckDB.NET.Data/Profiling/Statistics/Summary/ProfilingStatementSummary.cs b/DuckDB.NET.Data/Profiling/Statistics/Summary/ProfilingStatementSummary.cs
new file mode 100644
index 00000000..d2f8c98b
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/Statistics/Summary/ProfilingStatementSummary.cs
@@ -0,0 +1,25 @@
+namespace DuckDB.NET.Data.Profiling.Statistics.Summary;
+
+public readonly record struct ProfilingStatementSummary(
+ DateTimeOffset StartTime,
+ DateTimeOffset EndTime,
+ double ExecutionTimeMilliseconds,
+ int Order,
+ ProfilingInfoMetrics Metrics,
+ DuckDBState State,
+ string Message)
+{
+ public IDictionary ToDictionary()
+ {
+ return new Dictionary(7)
+ {
+ ["StartTime"] = StartTime,
+ ["EndTime"] = EndTime,
+ ["ExecutionTime"] = ExecutionTimeMilliseconds,
+ ["Order"] = Order,
+ ["Metrics"] = Metrics,
+ ["State"] = State,
+ ["Message"] = Message
+ };
+ }
+}
diff --git a/DuckDB.NET.Data/Profiling/Statistics/Summary/ProfilingSummary.cs b/DuckDB.NET.Data/Profiling/Statistics/Summary/ProfilingSummary.cs
new file mode 100644
index 00000000..17b81721
--- /dev/null
+++ b/DuckDB.NET.Data/Profiling/Statistics/Summary/ProfilingSummary.cs
@@ -0,0 +1,23 @@
+namespace DuckDB.NET.Data.Profiling.Statistics.Summary;
+
+public readonly record struct ProfilingSummary(
+ DateTimeOffset StartTime,
+ DateTimeOffset EndTime,
+ double ConnectionTimeMilliseconds,
+ double ExecutionTimeMilliseconds,
+ int QueryCount,
+ ProfilingQuerySummary[]? QuerySummaryList)
+{
+ public IDictionary ToDictionary()
+ {
+ return new Dictionary(6)
+ {
+ ["StartTime"] = StartTime,
+ ["EndTime"] = EndTime,
+ ["ConnectionTime"] = ConnectionTimeMilliseconds,
+ ["ExecutionTime"] = ExecutionTimeMilliseconds,
+ ["QueryCount"] = QueryCount,
+ ["QuerySummaryList"] = QuerySummaryList ?? Array.Empty()
+ };
+ }
+}
diff --git a/DuckDB.NET.Samples/NativeDebugResolver.cs b/DuckDB.NET.Samples/NativeDebugResolver.cs
new file mode 100644
index 00000000..0428e72e
--- /dev/null
+++ b/DuckDB.NET.Samples/NativeDebugResolver.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+internal static class NativeDebugResolver
+{
+ private static IntPtr _duckdbHandle;
+ private static bool _initialized;
+
+ public static void Initialize(string DuckDBDebugdllPath)
+ {
+ if (_initialized) return;
+ _initialized = true;
+
+ string repo = DuckDBDebugdllPath;
+ string duckdbDir = Path.Combine(repo, "build", "debug", "src", "Debug");
+
+ string duckdbPath = Path.Combine(duckdbDir, "duckdb.dll");
+
+ if (!File.Exists(duckdbPath)) throw new FileNotFoundException("duckdb.dll not found", duckdbPath);
+
+ // Optional but useful: preload debug runtime deps near duckdb.dll
+ //PreloadIfExists(Path.Combine(duckdbDir, "zlibd1.dll"));
+ //PreloadIfExists(Path.Combine(duckdbDir, "libcrypto-3-x64.dll"));
+ //PreloadIfExists(Path.Combine(duckdbDir, "libssl-3-x64.dll"));
+ //PreloadIfExists(Path.Combine(duckdbDir, "libcurl-d.dll"));
+
+ _duckdbHandle = NativeLibrary.Load(duckdbPath);
+
+ // Install resolver for the assembly that contains your DllImport declarations.
+ // If your DllImports are in another assembly, pass that assembly instead.
+ NativeLibrary.SetDllImportResolver(
+ typeof(NativeDebugResolver).Assembly,
+ Resolve);
+ }
+
+ private static IntPtr Resolve(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
+ {
+ if (IsDuckDbName(libraryName))
+ {
+ return _duckdbHandle;
+ }
+
+ return IntPtr.Zero;
+ }
+
+ private static bool IsDuckDbName(string name)
+ {
+ return name.Equals("duckdb", StringComparison.OrdinalIgnoreCase)
+ || name.Equals("duckdb.dll", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static void PreloadIfExists(string path)
+ {
+ if (File.Exists(path))
+ {
+ NativeLibrary.Load(path);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/DuckDB.NET.Samples/Program.cs b/DuckDB.NET.Samples/Program.cs
index f575c49c..f9555336 100644
--- a/DuckDB.NET.Samples/Program.cs
+++ b/DuckDB.NET.Samples/Program.cs
@@ -1,12 +1,19 @@
using Dapper;
using DuckDB.NET.Data;
+using DuckDB.NET.Data.Profiling;
+using DuckDB.NET.Data.Profiling.Statistics;
+using DuckDB.NET.Data.Profiling.Statistics.Summary;
using DuckDB.NET.Native;
using DuckDB.NET.Test.Helpers;
using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Reflection.Metadata;
using static DuckDB.NET.Native.NativeMethods;
namespace DuckDB.NET.Samples
@@ -20,6 +27,10 @@ static void Main(string[] args)
Console.Error.WriteLine("native assembly not found");
return;
}
+ //NativeDebugResolver.Initialize();
+
+ PrintVersion();
+
DapperSample();
@@ -28,6 +39,22 @@ static void Main(string[] args)
LowLevelBindingsSample();
BulkDataLoad();
+
+ ParametersBinding();
+
+ //Profiling();
+ }
+
+ private static void PrintVersion()
+ {
+ using var con = new DuckDBConnection("data source=:memory:");
+ con.Open();
+
+ using var cmd = con.CreateCommand();
+ cmd.CommandText = "PRAGMA Version";
+
+ var version = cmd.ExecuteScalar();
+ Console.WriteLine("DuckDB version: {0}", version);
}
private static void DapperSample()
@@ -137,7 +164,7 @@ private static void LowLevelBindingsSample()
}
}
}
-
+
private static void BulkDataLoad()
{
Stopwatch stopwatch = Stopwatch.StartNew();
@@ -167,8 +194,8 @@ private static void BulkDataLoad()
var row = appender.CreateRow();
row
.AppendValue(i)
- .AppendValue(Convert.ToSingle(i+2))
- .AppendValue(Convert.ToDouble(i+4))
+ .AppendValue(Convert.ToSingle(i + 2))
+ .AppendValue(Convert.ToDouble(i + 4))
.AppendValue($"varchar {i.ToString()}")
.EndRow();
}
@@ -185,7 +212,132 @@ private static void BulkDataLoad()
Console.WriteLine($"ElapsedMilliseconds {stopwatch.ElapsedMilliseconds}");
Console.WriteLine($"connection state before {connection.State}");
Console.WriteLine($"connection state after {connection.State}");
+
+ }
+
+ private static void ParametersBinding()
+ {
+
+ using var duckDBConnection = new DuckDBConnection("Data Source=:memory:");
+ duckDBConnection.Open();
+
+ var testDicto = new Dictionary
+ {
+ ["foo"] = "42",
+ ["bar"] = "test"
+ };
+ var testList = new List { "foo", "bar" };
+ var testDictoList = new Dictionary>
+ {
+ ["foo"] = testList
+ };
+
+ using var command = duckDBConnection.CreateCommand();
+ command.CommandText = "CREATE TABLE TestTable (foo MAP(string, string), bar MAP(string, string[]), baz string[])";
+ command.ExecuteNonQuery();
+
+ command.CommandText = "INSERT INTO TestTable (foo, bar, baz) VALUES ($testDicto, $testDictoList, $testList)";
+ command.Parameters.Add(new DuckDBParameter(testDicto));
+ command.Parameters.Add(new DuckDBParameter(testDictoList));
+ command.Parameters.Add(new DuckDBParameter(testList));
+ command.ExecuteNonQuery();
+
+ command.CommandText = "SELECT foo, bar, baz FROM TestTable";
+ using var reader = command.ExecuteReader();
+ PrintQueryResults(reader);
+ }
+
+ private static void Profiling()
+ {
+
+ using var con = new DuckDBConnection("data source=:memory:");
+ var options = new ProfilingOptions
+ {
+ Format = DuckDBProfilingFormat.NoOutput,
+ Mode = DuckDBProfilingMode.Detailed,
+ Coverage = DuckDBProfilingCoverage.Select,
+ EnabledMetrics =
+ [
+ DuckDBMetricType.QueryName,
+ DuckDBMetricType.Latency,
+ DuckDBMetricType.CpuTime,
+ DuckDBMetricType.ResultSetSize,
+ DuckDBMetricType.CumulativeRowsScanned,
+ DuckDBMetricType.TotalBytesRead,
+ DuckDBMetricType.TotalBytesWritten,
+ DuckDBMetricType.TotalMemoryAllocated,
+ DuckDBMetricType.SystemPeakBufferMemory,
+ DuckDBMetricType.SystemPeakTempDirSize,
+ DuckDBMetricType.WriteToWalLatency,
+ ],
+ MetricsThreshold = 100
+ };
+
+ con.Open();
+
+ var id = Guid.NewGuid().ToString("N");
+
+ using var dBCommand = con.CreateCommand();
+
+ LoadTpch(con);
+
+ con.EnableProfiling(options);
+
+ using var cmd = con.CreateCommand();
+ cmd.CommandText = "SELECT a:1;";
+ using var reader = cmd.ExecuteReader();
+ var metrics = con.RetrieveStatistics();
+
+ cmd.CommandText = $"PRAGMA tpch(1);";
+ cmd.ExecuteNonQuery();
+
+ metrics = con.RetrieveStatistics();
+
+ Console.WriteLine($"QuerySummaryList: {metrics.QuerySummaryList?.Length}");
+
+ cmd.CommandText = $"PRAGMA tpch(2); CREATE TABLE test (id int);";
+ cmd.ExecuteNonQuery();
+
+ metrics = con.RetrieveStatistics();
+ Console.WriteLine($"QuerySummaryList: {metrics.QuerySummaryList?.Length}");
+
+ PrintMetrics(metrics);
+ }
+
+ private static void LoadTpch(DuckDBConnection connection, int scaleFactor = 1)
+ {
+ using var cmd = connection.CreateCommand();
+ cmd.CommandText = "INSTALL tpch;";
+ cmd.ExecuteNonQuery();
+ cmd.CommandText = "LOAD tpch;";
+ cmd.ExecuteNonQuery();
+ // Generate TPCH sf=1 dataset into the current database
+ cmd.CommandText = $"CALL dbgen(sf={scaleFactor});";
+ cmd.ExecuteNonQuery();
+ }
+
+ private static void PrintMetrics(ProfilingSummary profilingSummary)
+ {
+ profilingSummary.ToDictionary().ToList().ForEach(kv => Console.WriteLine($"{kv.Key}: {(kv.Value is ProfilingQuerySummary[] ? PrintQueryInfo((ProfilingQuerySummary[])kv.Value) : kv.Value)}"));
+ }
+ private static string PrintQueryInfo(ProfilingQuerySummary[] profilingSummary)
+ {
+ return string.Join("", profilingSummary.Select((q, i) => $"\nQuery {i}: " + (PrintQueryInfo(q))));
+ }
+ private static string PrintQueryInfo(ProfilingQuerySummary profilingSummary)
+ {
+ return string.Join("", profilingSummary.ToDictionary().ToList().Select(kv => $"\n\t{kv.Key}: {(kv.Value is ProfilingInfoMetrics[] ? PrintStatementMetrics((ProfilingInfoMetrics[])kv.Value) : kv.Value)}"));
+ }
+
+ private static string PrintStatementMetrics(ProfilingInfoMetrics[] profilingSummaries)
+ {
+ return string.Join("", profilingSummaries.Select((s, i) => $"\n\tStatement {i}: " + PrintStatementMetrics(s)));
+ }
+
+ private static string PrintStatementMetrics(ProfilingInfoMetrics profilingSummary)
+ {
+ return string.Join("", profilingSummary.ToDictionary().ToList().Select(kv => $"\n\t\t{kv.Key}: {kv.Value}"));
}
private static void PrintQueryResults(DbDataReader queryResult)
@@ -197,7 +349,7 @@ private static void PrintQueryResults(DbDataReader queryResult)
}
Console.WriteLine();
-
+
while (queryResult.Read())
{
for (int ordinal = 0; ordinal < queryResult.FieldCount; ordinal++)
@@ -208,7 +360,9 @@ private static void PrintQueryResults(DbDataReader queryResult)
continue;
}
var val = queryResult.GetValue(ordinal);
- Console.Write(val);
+
+ Console.Write(FormatValue(val));
+
Console.Write(" ");
}
@@ -240,6 +394,38 @@ private static void PrintQueryResults(DuckDBResult queryResult)
Console.WriteLine();
}
}
+
+ private static string FormatValue(object? value)
+ {
+ if (value is null)
+ {
+ return "NULL";
+ }
+
+ if (value is string s)
+ {
+ return s;
+ }
+
+ if (value is IDictionary dictionary)
+ {
+ var parts = new List();
+ foreach (DictionaryEntry entry in dictionary)
+ {
+ parts.Add($"{FormatValue(entry.Key)}:{FormatValue(entry.Value)}");
+ }
+
+ return "{" + string.Join(", ", parts) + "}";
+ }
+
+ if (value is IEnumerable enumerable)
+ {
+ var parts = enumerable.Cast