better_duck_core/raw/client_context.rs
1//! RAII wrapper for `duckdb_client_context`.
2//!
3//! A client context is obtained from a connection (or from a table function's bind
4//! info) and exposes the stable connection id. The handle is owned — it must be
5//! destroyed with `duckdb_destroy_client_context` — but it is only meaningful while
6//! the connection (or bind call) it came from is alive. [`ClientContext`] is
7//! therefore **lifetime-bound** to that owner: the borrow it captures prevents it
8//! from outliving the connection at compile time, and `Drop` destroys the handle
9//! exactly once.
10// FFI pointer args are used safely inside `unsafe` blocks.
11#![allow(clippy::not_unsafe_ptr_arg_deref)]
12
13use std::marker::PhantomData;
14
15use crate::ffi::{
16 duckdb_client_context, duckdb_client_context_get_connection_id, duckdb_destroy_client_context,
17};
18
19/// An owned `duckdb_client_context`, borrow-tied to the owner it came from so it
20/// cannot outlive it.
21pub struct ClientContext<'owner> {
22 ctx: duckdb_client_context,
23 _owner: PhantomData<&'owner ()>,
24}
25
26impl<'owner> ClientContext<'owner> {
27 /// Wraps a raw client-context handle, or `None` if null.
28 ///
29 /// # Safety
30 ///
31 /// `ctx` must be a `duckdb_client_context` produced for an owner that outlives
32 /// `'owner` (a connection or a live bind call), and ownership of the handle is
33 /// transferred here (destroyed on drop).
34 pub(crate) unsafe fn from_raw(ctx: duckdb_client_context) -> Option<ClientContext<'owner>> {
35 if ctx.is_null() {
36 return None;
37 }
38 Some(ClientContext { ctx, _owner: PhantomData })
39 }
40
41 /// The stable connection id of the connection this context belongs to.
42 #[must_use]
43 pub fn connection_id(&self) -> u64 {
44 // SAFETY: `self.ctx` is a valid, non-null client context owned by `self`.
45 unsafe { duckdb_client_context_get_connection_id(self.ctx) as u64 }
46 }
47
48 /// The raw handle, borrowed for the duration of `&self` (e.g. for
49 /// `duckdb_expression_fold`). The caller must not destroy it.
50 #[cfg(feature = "udf")]
51 pub(crate) fn as_raw(&self) -> duckdb_client_context {
52 self.ctx
53 }
54}
55
56impl Drop for ClientContext<'_> {
57 fn drop(&mut self) {
58 if !self.ctx.is_null() {
59 // SAFETY: `self.ctx` is a valid, non-null client context created by one of
60 // the get-context functions and not yet destroyed; destroyed once here.
61 unsafe { duckdb_destroy_client_context(&mut self.ctx) };
62 }
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use crate::connection::Connection;
69
70 #[test]
71 fn connection_exposes_a_stable_client_context_id() {
72 let conn = Connection::open_in_memory().unwrap();
73 let id = conn.client_context().expect("client context").connection_id();
74 // The same connection reports the same id on a second fetch.
75 assert_eq!(conn.client_context().unwrap().connection_id(), id);
76 }
77
78 #[test]
79 fn distinct_connections_on_one_db_have_distinct_ids() {
80 let a = Connection::open_in_memory().unwrap();
81 let b = a.try_clone().expect("second connection on the same database");
82 let ida = a.client_context().unwrap().connection_id();
83 let idb = b.client_context().unwrap().connection_id();
84 assert_ne!(ida, idb, "each connection has its own id");
85 }
86}