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}