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}