Skip to main content

better_duck_core/raw/
profiling.rs

1//! Owned, recursive query-profiling tree.
2//!
3//! `duckdb_get_profiling_info` returns the root of a profiling tree that is *owned
4//! by the connection* and only valid until the next query — there is no destroy
5//! function for it, and the child pointers are borrowed the same way. To give
6//! callers something that outlives later queries and the connection itself, this
7//! module **materialises** the whole tree eagerly into owned [`ProfilingNode`]s
8//! (metric name→value strings plus child nodes), copying every string out before
9//! returning. Nothing here retains a `duckdb_profiling_info` pointer.
10//!
11//! When profiling is disabled, or no query has run yet, `duckdb_get_profiling_info`
12//! returns null; the safe wrappers surface that as `None`.
13// FFI pointer args are used safely inside `unsafe` blocks.
14#![allow(clippy::not_unsafe_ptr_arg_deref)]
15
16use std::collections::HashMap;
17use std::ffi::{CStr, CString};
18use std::os::raw::c_void;
19
20use crate::ffi::{
21    duckdb_connection, duckdb_destroy_value, duckdb_free, duckdb_get_map_key, duckdb_get_map_size,
22    duckdb_get_map_value, duckdb_get_profiling_info, duckdb_get_varchar, duckdb_profiling_info,
23    duckdb_profiling_info_get_child, duckdb_profiling_info_get_child_count,
24    duckdb_profiling_info_get_metrics, duckdb_profiling_info_get_value, duckdb_value,
25};
26
27/// An owned node of the query-profiling tree: the metrics recorded at this node
28/// (name → value, both strings) plus its child nodes, in DuckDB's order.
29///
30/// Materialised from `duckdb_get_profiling_info`, so it stays valid after further
31/// queries and after the connection is dropped.
32#[derive(Debug, Clone, PartialEq, Eq, Default)]
33pub struct ProfilingNode {
34    metrics: HashMap<String, String>,
35    children: Vec<ProfilingNode>,
36}
37
38impl ProfilingNode {
39    /// All metrics recorded at this node (metric name → value string).
40    #[must_use]
41    pub fn metrics(&self) -> &HashMap<String, String> {
42        &self.metrics
43    }
44
45    /// The value of a single metric at this node, if present.
46    #[must_use]
47    pub fn metric(
48        &self,
49        key: &str,
50    ) -> Option<&str> {
51        self.metrics.get(key).map(String::as_str)
52    }
53
54    /// This node's child nodes, in DuckDB's order.
55    #[must_use]
56    pub fn children(&self) -> &[ProfilingNode] {
57        &self.children
58    }
59
60    /// Recursively materialises the borrowed profiling node `info` into an owned
61    /// [`ProfilingNode`].
62    ///
63    /// # Safety
64    ///
65    /// `info` must be a valid, non-null `duckdb_profiling_info` (a root from
66    /// `duckdb_get_profiling_info` or a child from
67    /// `duckdb_profiling_info_get_child`). The pointer is only read, never destroyed.
68    unsafe fn materialize(info: duckdb_profiling_info) -> ProfilingNode {
69        // SAFETY: `info` is valid; `get_metrics` returns an owned MAP value (or null)
70        // that we read and destroy below.
71        let metrics = unsafe { metrics_of(info) };
72
73        // SAFETY: `info` is valid.
74        let child_count = unsafe { duckdb_profiling_info_get_child_count(info) };
75        let mut children = Vec::with_capacity(child_count as usize);
76        for i in 0..child_count {
77            // SAFETY: `i` is within [0, child_count); the returned child is borrowed
78            // (owned by the connection) and must not be destroyed.
79            let child = unsafe { duckdb_profiling_info_get_child(info, i) };
80            if !child.is_null() {
81                // SAFETY: `child` is a valid, non-null borrowed profiling node.
82                children.push(unsafe { ProfilingNode::materialize(child) });
83            }
84        }
85
86        ProfilingNode { metrics, children }
87    }
88}
89
90/// Reads the metric MAP of a profiling node into an owned `HashMap`.
91///
92/// # Safety
93///
94/// `info` must be a valid `duckdb_profiling_info`.
95unsafe fn metrics_of(info: duckdb_profiling_info) -> HashMap<String, String> {
96    // SAFETY: `info` is valid; `get_metrics` returns an owned MAP `duckdb_value` (or
97    // null) that we destroy before returning.
98    let mut map_value = unsafe { duckdb_profiling_info_get_metrics(info) };
99    if map_value.is_null() {
100        return HashMap::new();
101    }
102    // SAFETY: `map_value` is a valid MAP value; `get_map_size` reads its length.
103    let n = unsafe { duckdb_get_map_size(map_value) };
104    let mut out = HashMap::with_capacity(n as usize);
105    for i in 0..n {
106        // SAFETY: `i` is within [0, n); the key/value are owned `duckdb_value`s that
107        // `owned_varchar` reads and destroys.
108        let key = unsafe { owned_varchar(duckdb_get_map_key(map_value, i)) };
109        // SAFETY: as above.
110        let value = unsafe { owned_varchar(duckdb_get_map_value(map_value, i)) };
111        if let (Some(k), Some(v)) = (key, value) {
112            out.insert(k, v);
113        }
114    }
115    // SAFETY: `map_value` was returned by `get_metrics`; destroy exactly once.
116    unsafe { duckdb_destroy_value(&mut map_value) };
117    out
118}
119
120/// Reads an owned `duckdb_value` as a `String` via `duckdb_get_varchar`, destroying
121/// both the extracted C string and the value itself. Returns `None` if the value or
122/// string is null.
123///
124/// # Safety
125///
126/// `value` must be an owned `duckdb_value` (the caller transfers ownership here).
127unsafe fn owned_varchar(mut value: duckdb_value) -> Option<String> {
128    if value.is_null() {
129        return None;
130    }
131    // SAFETY: `value` is a valid, non-null duckdb_value; `get_varchar` returns a heap
132    // `char*` (or null) that must be freed with `duckdb_free`.
133    let c = unsafe { duckdb_get_varchar(value) };
134    let out = if c.is_null() {
135        None
136    } else {
137        // SAFETY: `c` is a valid, non-null, null-terminated C string.
138        let s = unsafe { CStr::from_ptr(c) }.to_string_lossy().into_owned();
139        // SAFETY: `c` was allocated by DuckDB and ownership transferred to us.
140        unsafe { duckdb_free(c as *mut c_void) };
141        Some(s)
142    };
143    // SAFETY: `value` is owned here; destroy exactly once.
144    unsafe { duckdb_destroy_value(&mut value) };
145    out
146}
147
148/// Materialises the connection's profiling tree, or `None` if profiling is disabled
149/// or no query has run yet.
150///
151/// # Safety
152///
153/// `con` must be a valid open `duckdb_connection`.
154pub(crate) unsafe fn profiling_info(con: duckdb_connection) -> Option<ProfilingNode> {
155    // SAFETY: `con` is a valid connection; the returned root is borrowed (owned by
156    // the connection, valid until the next query) and must not be destroyed. Null
157    // means profiling is disabled or no query has run.
158    let root = unsafe { duckdb_get_profiling_info(con) };
159    if root.is_null() {
160        return None;
161    }
162    // SAFETY: `root` is a valid, non-null borrowed profiling node.
163    Some(unsafe { ProfilingNode::materialize(root) })
164}
165
166/// Fetches a single metric from the connection's root profiling node via
167/// `duckdb_profiling_info_get_value`, or `None` if unavailable.
168///
169/// # Safety
170///
171/// `con` must be a valid open `duckdb_connection`.
172pub(crate) unsafe fn profiling_metric(
173    con: duckdb_connection,
174    key: &str,
175) -> Option<String> {
176    let c_key = CString::new(key).ok()?;
177    // SAFETY: `con` is valid; the returned root is borrowed and not destroyed.
178    let root = unsafe { duckdb_get_profiling_info(con) };
179    if root.is_null() {
180        return None;
181    }
182    // SAFETY: `root` is a valid profiling node; `c_key` is a valid null-terminated
183    // string. `get_value` returns an owned `duckdb_value` (or null) that
184    // `owned_varchar` reads and destroys.
185    let value = unsafe { duckdb_profiling_info_get_value(root, c_key.as_ptr()) };
186    // SAFETY: `value` is owned (or null); `owned_varchar` takes ownership.
187    unsafe { owned_varchar(value) }
188}
189
190#[cfg(test)]
191mod tests {
192    use crate::connection::Connection;
193
194    #[test]
195    fn profiling_disabled_by_default_yields_none() {
196        let mut conn = Connection::open_in_memory().unwrap();
197        conn.execute_batch("SELECT 42").unwrap();
198        // Profiling is off unless explicitly enabled, so there is no tree.
199        assert!(conn.profiling_info().is_none());
200        assert!(conn.profiling_metric("OPERATOR_NAME").is_none());
201    }
202
203    #[test]
204    fn enabled_profiling_materialises_a_tree_that_outlives_later_queries() {
205        let mut conn = Connection::open_in_memory().unwrap();
206        conn.execute_batch("PRAGMA enable_profiling = 'no_output'").unwrap();
207        // Run a query so a profile exists.
208        let _ = conn.execute("SELECT 1 AS a, 2 AS b").unwrap().count();
209
210        let tree = conn.profiling_info().expect("a profiling tree once enabled");
211        // The root records at least one metric, and the tree is a real hierarchy.
212        assert!(!tree.metrics().is_empty() || !tree.children().is_empty());
213
214        // Materialised tree survives a subsequent query (the borrowed DuckDB pointer
215        // would have been invalidated; the owned copy is unaffected).
216        let _ = conn.execute("SELECT 99").unwrap().count();
217        // Still readable — no use-after-free, no panic.
218        let _ = tree.children().len();
219        let _ = tree.metrics().len();
220    }
221
222    #[test]
223    fn single_metric_lookup_uses_get_value() {
224        let mut conn = Connection::open_in_memory().unwrap();
225        conn.execute_batch("PRAGMA enable_profiling = 'no_output'").unwrap();
226        let _ = conn.execute("SELECT 1 AS a").unwrap().count();
227        // `QUERY_NAME` is always present once profiling is enabled; get_value returns
228        // the executed SQL. (Only *known* keys are safe — see the method docs.)
229        assert_eq!(conn.profiling_metric("QUERY_NAME").as_deref(), Some("SELECT 1 AS a"));
230        // With profiling disabled the root is null, so lookup is None (and never
231        // reaches the C-API defect below).
232        let mut off = Connection::open_in_memory().unwrap();
233        off.execute_batch("SELECT 1").unwrap();
234        assert!(off.profiling_metric("QUERY_NAME").is_none());
235    }
236
237    // NOTE: `profiling_metric` with an *unknown* metric key is intentionally not
238    // tested. Despite its C-API docs promising a null return for a missing metric,
239    // `duckdb_profiling_info_get_value` throws a C++ exception that unwinds across the
240    // FFI boundary and aborts the process ("Rust cannot catch foreign exceptions") —
241    // the same upstream defect class as get_table_names/extract_statements. The
242    // method documents that callers must pass a metric key known to exist (e.g. one
243    // from `profiling_info().metrics().keys()`), and the tests only use known keys.
244}