pgdog_plugin/
context.rs

1//! Context passed to and from the plugins.
2
3use std::ops::Deref;
4
5use crate::{bindings::PdRouterContext, PdRoute, PdStatement};
6
7/// PostgreSQL statement, parsed by [`pg_query`].
8///
9/// Implements [`Deref`] on [`PdStatement`], which is passed
10/// in using the FFI interface.
11/// Use the [`PdStatement::protobuf`] method to obtain a reference
12/// to the Abstract Syntax Tree.
13///
14/// ### Example
15///
16/// ```ignore
17/// let ast = statement.protobuf();
18/// println!("{:#?}", ast);
19/// ```
20pub struct Statement {
21    ffi: PdStatement,
22}
23
24impl Deref for Statement {
25    type Target = PdStatement;
26
27    fn deref(&self) -> &Self::Target {
28        &self.ffi
29    }
30}
31
32/// Context information provided by PgDog to the plugin at statement execution. It contains the actual statement and several metadata about
33/// the state of the database cluster:
34///
35/// - Number of shards
36/// - Does it have replicas
37/// - Does it have a primary
38///
39/// ### Example
40///
41/// ```
42/// use pgdog_plugin::{Context, Route, macros, Shard, ReadWrite};
43///
44/// #[macros::route]
45/// fn route(context: Context) -> Route {
46///     let shards = context.shards();
47///     let read_only = context.read_only();
48///     let ast = context.statement().protobuf();
49///
50///     println!("shards: {} (read_only: {})", shards, read_only);
51///     println!("ast: {:#?}", ast);
52///
53///     let read_write = if read_only {
54///         ReadWrite::Read
55///     } else {
56///         ReadWrite::Write
57///     };
58///
59///     Route::new(Shard::Direct(0), read_write)
60/// }
61/// ```
62///
63pub struct Context {
64    ffi: PdRouterContext,
65}
66
67impl From<PdRouterContext> for Context {
68    fn from(value: PdRouterContext) -> Self {
69        Self { ffi: value }
70    }
71}
72
73impl Context {
74    /// Returns a reference to the Abstract Syntax Tree (AST) created by [`pg_query`].
75    ///
76    /// # Example
77    ///
78    /// ```ignore
79    /// let ast = context.statement().protobuf();
80    /// let nodes = ast.nodes();
81    /// ```
82    pub fn statement(&self) -> Statement {
83        Statement {
84            ffi: self.ffi.query,
85        }
86    }
87
88    /// Returns true if the database cluster doesn't have a primary database and can only serve
89    /// read queries.
90    ///
91    /// # Example
92    ///
93    /// ```
94    /// # use pgdog_plugin::Context;
95    /// # let context = unsafe { Context::doc_test() };
96    /// let read_only = context.read_only();
97    ///
98    /// if read_only {
99    ///     println!("Database cluster doesn't have a primary, only replicas.");
100    /// }
101    /// ```
102    pub fn read_only(&self) -> bool {
103        self.ffi.has_primary == 0
104    }
105
106    /// Returns true if the database cluster has replica databases.
107    ///
108    /// # Example
109    ///
110    /// ```
111    /// # use pgdog_plugin::Context;
112    /// # let context = unsafe { Context::doc_test() };
113    /// let has_replicas = context.has_replicas();
114    ///
115    /// if has_replicas {
116    ///     println!("Database cluster can load balance read queries.")
117    /// }
118    /// ```
119    pub fn has_replicas(&self) -> bool {
120        self.ffi.has_replicas == 1
121    }
122
123    /// Returns true if the database cluster has a primary database and can serve write queries.
124    ///
125    /// # Example
126    ///
127    /// ```
128    /// # use pgdog_plugin::Context;
129    /// # let context = unsafe { Context::doc_test() };
130    /// let has_primary = context.has_primary();
131    ///
132    /// if has_primary {
133    ///     println!("Database cluster can serve write queries.");
134    /// }
135    /// ```
136    pub fn has_primary(&self) -> bool {
137        !self.read_only()
138    }
139
140    /// Returns the number of shards in the database cluster.
141    ///
142    /// # Example
143    ///
144    /// ```
145    /// # use pgdog_plugin::Context;
146    /// # let context = unsafe { Context::doc_test() };
147    /// let shards = context.shards();
148    ///
149    /// if shards > 1 {
150    ///     println!("Plugin should consider which shard to route the query to.");
151    /// }
152    /// ```
153    pub fn shards(&self) -> usize {
154        self.ffi.shards as usize
155    }
156
157    /// Returns true if the database cluster has more than one shard.
158    ///
159    /// # Example
160    ///
161    /// ```
162    /// # use pgdog_plugin::Context;
163    /// # let context = unsafe { Context::doc_test() };
164    /// let sharded = context.sharded();
165    /// let shards = context.shards();
166    ///
167    /// if sharded {
168    ///     assert!(shards > 1);
169    /// } else {
170    ///     assert_eq!(shards, 1);
171    /// }
172    /// ```
173    pub fn sharded(&self) -> bool {
174        self.shards() > 1
175    }
176
177    /// Returns true if PgDog strongly believes the statement should be sent to a primary. This indicates
178    /// that the statement is **not** a `SELECT` (e.g. `UPDATE`, `DELETE`, etc.), or a `SELECT` that is very likely to write data to the database, e.g.:
179    ///
180    /// ```sql
181    /// WITH users AS (
182    ///     INSERT INTO users VALUES (1, '[email protected]') RETURNING *
183    /// )
184    /// SELECT * FROM users;
185    /// ```
186    ///
187    /// # Example
188    ///
189    /// ```
190    /// # use pgdog_plugin::Context;
191    /// # let context = unsafe { Context::doc_test() };
192    /// if context.write_override() {
193    ///     println!("We should really send this query to the primary.");
194    /// }
195    /// ```
196    pub fn write_override(&self) -> bool {
197        self.ffi.write_override == 1
198    }
199}
200
201impl Context {
202    /// Used for doc tests only. **Do not use**.
203    ///
204    /// # Safety
205    ///
206    /// Not safe, don't use. We use it for doc tests only.
207    ///
208    pub unsafe fn doc_test() -> Context {
209        use std::{os::raw::c_void, ptr::null};
210
211        Context {
212            ffi: PdRouterContext {
213                shards: 1,
214                has_replicas: 1,
215                has_primary: 1,
216                in_transaction: 0,
217                write_override: 0,
218                query: PdStatement {
219                    version: 1,
220                    len: 0,
221                    data: null::<c_void>() as *mut c_void,
222                },
223            },
224        }
225    }
226}
227
228/// What shard, if any, the statement should be sent to.
229///
230/// ### Example
231///
232/// ```
233/// use pgdog_plugin::Shard;
234///
235/// // Send query to shard 2.
236/// let direct = Shard::Direct(2);
237///
238/// // Send query to all shards.
239/// let cross_shard = Shard::All;
240///
241/// // Let PgDog handle sharding.
242/// let unknown = Shard::Unknown;
243/// ```
244#[derive(Debug, Copy, Clone, PartialEq, Eq)]
245pub enum Shard {
246    /// Direct-to-shard statement, sent to the specified shard only.
247    Direct(usize),
248    /// Send statement to all shards and let PgDog collect and transform the results.
249    All,
250    /// Not clear which shard it should go to, so let PgDog decide.
251    /// Use this if you don't want to handle sharding inside the plugin.
252    Unknown,
253}
254
255impl From<Shard> for i64 {
256    fn from(value: Shard) -> Self {
257        match value {
258            Shard::Direct(value) => value as i64,
259            Shard::All => -1,
260            Shard::Unknown => -2,
261        }
262    }
263}
264
265impl TryFrom<i64> for Shard {
266    type Error = ();
267    fn try_from(value: i64) -> Result<Self, Self::Error> {
268        Ok(if value == -1 {
269            Shard::All
270        } else if value == -2 {
271            Shard::Unknown
272        } else if value >= 0 {
273            Shard::Direct(value as usize)
274        } else {
275            return Err(());
276        })
277    }
278}
279
280impl TryFrom<u8> for ReadWrite {
281    type Error = ();
282
283    fn try_from(value: u8) -> Result<Self, Self::Error> {
284        Ok(if value == 0 {
285            ReadWrite::Write
286        } else if value == 1 {
287            ReadWrite::Read
288        } else if value == 2 {
289            ReadWrite::Unknown
290        } else {
291            return Err(());
292        })
293    }
294}
295
296/// Indicates if the statement is a read or a write. Read statements are sent to a replica, if one is configured.
297/// Write statements are sent to the primary.
298///
299/// ### Example
300///
301/// ```
302/// use pgdog_plugin::ReadWrite;
303///
304/// // The statement should go to a replica.
305/// let read = ReadWrite::Read;
306///
307/// // The statement should go the primary.
308/// let write = ReadWrite::Write;
309///
310/// // Skip and let PgDog decide.
311/// let unknown = ReadWrite::Unknown;
312/// ```
313#[derive(Debug, Copy, Clone, PartialEq, Eq)]
314pub enum ReadWrite {
315    /// Send the statement to a replica, if any are configured.
316    Read,
317    /// Send the statement to the primary.
318    Write,
319    /// Plugin doesn't know if the statement is a read or write. This let's PgDog decide.
320    /// Use this if you don't want to make this decision in the plugin.
321    Unknown,
322}
323
324impl From<ReadWrite> for u8 {
325    fn from(value: ReadWrite) -> Self {
326        match value {
327            ReadWrite::Write => 0,
328            ReadWrite::Read => 1,
329            ReadWrite::Unknown => 2,
330        }
331    }
332}
333
334impl Default for PdRoute {
335    fn default() -> Self {
336        Route::unknown().ffi
337    }
338}
339
340/// Statement route.
341///
342/// PgDog uses this to decide where a query should be sent to. Read statements are sent to a replica,
343/// while write ones are sent the primary. If the cluster has more than one shard, the statement can be
344/// sent to a specific database, or all of them.
345///
346/// ### Example
347///
348/// ```
349/// use pgdog_plugin::{Shard, ReadWrite, Route};
350///
351/// // This sends the query to the primary database of shard 0.
352/// let route = Route::new(Shard::Direct(0), ReadWrite::Write);
353///
354/// // This sends the query to all shards, routing them to a replica
355/// // of each shard, if any are configured.
356/// let route = Route::new(Shard::All, ReadWrite::Read);
357///
358/// // No routing information is available. PgDog will ignore it
359/// // and make its own decision.
360/// let route = Route::unknown();
361pub struct Route {
362    ffi: PdRoute,
363}
364
365impl Default for Route {
366    fn default() -> Self {
367        Self::unknown()
368    }
369}
370
371impl Deref for Route {
372    type Target = PdRoute;
373
374    fn deref(&self) -> &Self::Target {
375        &self.ffi
376    }
377}
378
379impl From<PdRoute> for Route {
380    fn from(value: PdRoute) -> Self {
381        Self { ffi: value }
382    }
383}
384
385impl From<Route> for PdRoute {
386    fn from(value: Route) -> Self {
387        value.ffi
388    }
389}
390
391impl Route {
392    /// Create new route.
393    ///
394    /// # Arguments
395    ///
396    /// * `shard`: Which shard the statement should be sent to.
397    /// * `read_write`: Does the statement read or write data. Read statements are sent to a replica. Write statements are sent to the primary.
398    ///
399    pub fn new(shard: Shard, read_write: ReadWrite) -> Route {
400        Self {
401            ffi: PdRoute {
402                shard: shard.into(),
403                read_write: read_write.into(),
404            },
405        }
406    }
407
408    /// Create new route with no sharding or read/write information.
409    /// Use this if you don't want your plugin to do query routing.
410    /// Plugins that do something else with queries, e.g., logging, metrics,
411    /// can return this route.
412    pub fn unknown() -> Route {
413        Self {
414            ffi: PdRoute {
415                shard: -2,
416                read_write: 2,
417            },
418        }
419    }
420}