Skip to main content

uni_plugin/traits/
hook.rs

1//! Session-lifecycle hooks — Postgres-style phased.
2//!
3//! The trait expands the legacy `before_query` / `after_query` shape to
4//! phased hooks at parse / analyze / plan / execute_start / execute_end /
5//! before_commit / after_commit / abort. Each phase has a default no-op
6//! implementation so existing hooks that only override the legacy methods
7//! continue to work.
8
9use std::time::Duration;
10
11use datafusion::scalar::ScalarValue;
12use smol_str::SmolStr;
13
14use crate::errors::HookOutcome;
15
16/// Classification of the query under observation.
17///
18/// Mirrors the host's surface-level distinction between Cypher reads,
19/// Locy program evaluations, and Execute (mutation) statements without
20/// pulling a `uni-db` dependency into `uni-plugin` (which would create a
21/// circular dep). The bridge in `uni-db` is responsible for translating
22/// between this enum and the host's `crate::api::hooks::QueryType`.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum QueryType {
25    /// A Cypher query (read or write).
26    #[default]
27    Cypher,
28    /// A Locy program evaluation.
29    Locy,
30    /// An execute (mutation) statement.
31    Execute,
32}
33
34/// Slim mirror of the host's commit metadata.
35///
36/// Surfaced to phased `after_commit` hooks via `CommitContext` so they
37/// observe real post-commit values instead of zero-filled stubs. The
38/// fields are a deliberate subset — anything operationally meaningful
39/// to a hook (commit count, version, WAL LSN, wall-clock duration).
40///
41/// The host's bridge populates this from its own `CommitResult`; this
42/// type stays free of host imports to preserve `uni-plugin`'s
43/// loader-agnostic invariant.
44#[derive(Debug, Clone, Default)]
45pub struct PluginCommitResult {
46    /// Number of mutations committed.
47    pub mutations: u64,
48    /// Database version after commit.
49    pub version: u64,
50    /// WAL log sequence number of the commit (0 when no WAL is configured).
51    pub wal_lsn: u64,
52    /// Duration of the commit operation (lock + WAL + merge).
53    pub duration: Duration,
54}
55
56/// Session-lifecycle hook plugin.
57///
58/// Every method has a default that does nothing; implementations override
59/// only the phases they need. Phased dispatch lets a hook plugin perform
60/// audit at `on_execute_end` without paying parse-time cost, etc.
61pub trait SessionHook: Send + Sync {
62    /// Called after the query source is parsed; the hook may reject parse
63    /// failures or annotate the parse for downstream phases.
64    fn on_parse(&self, _ctx: &ParseContext<'_>) -> HookOutcome {
65        HookOutcome::Continue
66    }
67
68    /// Called after semantic analysis. Useful for row-level security
69    /// predicate injection.
70    fn on_analyze(&self, _ctx: &AnalyzeContext<'_>) -> HookOutcome {
71        HookOutcome::Continue
72    }
73
74    /// Called after logical planning; the hook may rewrite the plan.
75    fn on_plan(&self, _ctx: &PlanContext<'_>) -> HookOutcome {
76        HookOutcome::Continue
77    }
78
79    /// Called immediately before physical execution begins.
80    fn on_execute_start(&self, _ctx: &ExecuteContext<'_>) -> HookOutcome {
81        HookOutcome::Continue
82    }
83
84    /// Called once execution finishes with the collected metrics.
85    fn on_execute_end(&self, _ctx: &ExecuteContext<'_>, _metrics: &QueryMetrics) {}
86
87    /// Called before commit; may reject the transaction.
88    fn before_commit(&self, _ctx: &CommitContext<'_>) -> HookOutcome {
89        HookOutcome::Continue
90    }
91
92    /// Called after a successful commit.
93    fn after_commit(&self, _ctx: &CommitContext<'_>) {}
94
95    /// Called when a transaction aborts (by rollback or error).
96    fn on_abort(&self, _ctx: &AbortContext<'_>) {}
97}
98
99/// Parse-phase context.
100///
101/// `query_type` defaults to [`QueryType::Cypher`] for back-compat with
102/// hooks built against the v1.0 shape; populate via
103/// [`Self::with_query_type`] when the host knows the language up front.
104/// `params` defaults to an empty slice; populate via
105/// [`Self::with_params`] to surface bound query parameters to hooks
106/// (Arrow-shaped to keep `uni-plugin` free of any `uni-common` dep).
107#[derive(Debug)]
108#[non_exhaustive]
109pub struct ParseContext<'a> {
110    /// Raw source text of the query.
111    pub source: &'a str,
112    /// Session identifier.
113    pub session_id: &'a str,
114    /// Query language classification (v1.1).
115    pub query_type: QueryType,
116    /// Bound query parameters as `(name, value)` pairs (v1.1).
117    pub params: &'a [(SmolStr, ScalarValue)],
118}
119
120impl<'a> ParseContext<'a> {
121    /// Construct a parse context with defaults for the v1.1 fields.
122    ///
123    /// Hooks built outside of `uni-plugin` use this constructor; the
124    /// struct is `#[non_exhaustive]` so direct struct-literal
125    /// construction is forbidden. `query_type` defaults to
126    /// [`QueryType::Cypher`]; `params` defaults to an empty slice.
127    /// Override via the builders below.
128    #[must_use]
129    pub fn new(source: &'a str, session_id: &'a str) -> Self {
130        Self {
131            source,
132            session_id,
133            query_type: QueryType::default(),
134            params: &[],
135        }
136    }
137
138    /// Override the query-language classification.
139    #[must_use]
140    pub fn with_query_type(mut self, query_type: QueryType) -> Self {
141        self.query_type = query_type;
142        self
143    }
144
145    /// Attach a borrowed slice of bound query parameters.
146    #[must_use]
147    pub fn with_params(mut self, params: &'a [(SmolStr, ScalarValue)]) -> Self {
148        self.params = params;
149        self
150    }
151}
152
153/// Analyze-phase context.
154#[derive(Debug)]
155#[non_exhaustive]
156pub struct AnalyzeContext<'a> {
157    /// Session identifier.
158    pub session_id: &'a str,
159    /// Lifetime marker.
160    pub _marker: std::marker::PhantomData<&'a ()>,
161}
162
163impl<'a> AnalyzeContext<'a> {
164    /// Construct an analyze context.
165    #[must_use]
166    pub fn new(session_id: &'a str) -> Self {
167        Self {
168            session_id,
169            _marker: std::marker::PhantomData,
170        }
171    }
172}
173
174/// Plan-phase context — placeholder for the actual logical-plan handle.
175#[derive(Debug)]
176#[non_exhaustive]
177pub struct PlanContext<'a> {
178    /// Session identifier.
179    pub session_id: &'a str,
180    /// Lifetime marker.
181    pub _marker: std::marker::PhantomData<&'a ()>,
182}
183
184impl<'a> PlanContext<'a> {
185    /// Construct a plan context.
186    #[must_use]
187    pub fn new(session_id: &'a str) -> Self {
188        Self {
189            session_id,
190            _marker: std::marker::PhantomData,
191        }
192    }
193}
194
195/// Execute-phase context.
196#[derive(Debug)]
197#[non_exhaustive]
198pub struct ExecuteContext<'a> {
199    /// Session identifier.
200    pub session_id: &'a str,
201    /// Lifetime marker.
202    pub _marker: std::marker::PhantomData<&'a ()>,
203}
204
205impl<'a> ExecuteContext<'a> {
206    /// Construct an execute context.
207    #[must_use]
208    pub fn new(session_id: &'a str) -> Self {
209        Self {
210            session_id,
211            _marker: std::marker::PhantomData,
212        }
213    }
214}
215
216/// Commit-phase context.
217///
218/// `before_commit` callers leave `commit_result` as `None` (no result
219/// exists yet). `after_commit` callers should populate it via
220/// [`Self::with_commit_result`] so hooks observe real post-commit
221/// metadata rather than zero-filled stubs (v1.1).
222#[derive(Debug)]
223#[non_exhaustive]
224pub struct CommitContext<'a> {
225    /// Session identifier.
226    pub session_id: &'a str,
227    /// Post-commit metadata (v1.1). `None` in `before_commit`; `Some`
228    /// in `after_commit` when the host bridges the real result through.
229    pub commit_result: Option<&'a PluginCommitResult>,
230}
231
232impl<'a> CommitContext<'a> {
233    /// Construct a commit context with `commit_result = None`.
234    #[must_use]
235    pub fn new(session_id: &'a str) -> Self {
236        Self {
237            session_id,
238            commit_result: None,
239        }
240    }
241
242    /// Attach a borrowed post-commit result; used by `after_commit`.
243    #[must_use]
244    pub fn with_commit_result(mut self, result: &'a PluginCommitResult) -> Self {
245        self.commit_result = Some(result);
246        self
247    }
248}
249
250/// Abort-phase context.
251#[derive(Debug)]
252#[non_exhaustive]
253pub struct AbortContext<'a> {
254    /// Session identifier.
255    pub session_id: &'a str,
256    /// Reason text.
257    pub reason: &'a str,
258}
259
260impl<'a> AbortContext<'a> {
261    /// Construct an abort context.
262    #[must_use]
263    pub fn new(session_id: &'a str, reason: &'a str) -> Self {
264        Self { session_id, reason }
265    }
266}
267
268/// Query execution metrics surfaced to `on_execute_end`.
269#[derive(Clone, Debug, Default)]
270pub struct QueryMetrics {
271    /// Wall-clock duration of the entire query.
272    pub elapsed: Duration,
273    /// Rows produced (sum across output operators).
274    pub rows_out: u64,
275    /// Approximate bytes read from storage.
276    pub bytes_read: u64,
277}