Skip to main content

spg_engine/
notify.rs

1//! v7.39 (round 222) — LISTEN / NOTIFY delivery. Was accept-and-drop
2//! since v7.37.17; now notifications are real: LISTEN subscribes the
3//! session, NOTIFY queues on the transaction and delivers at COMMIT
4//! (PG semantics: transactional, deduplicated within the tx, dropped
5//! at ROLLBACK; immediate under autocommit), and the wire layer drains
6//! [`Engine::take_notifications`] into 'A' NotificationResponse
7//! messages after each statement. Delivery is same-process (the
8//! engine's session-state architecture wall, like `session_params`):
9//! every pgwire connection shares the engine, so a NOTIFY from one
10//! connection reaches a LISTEN from another at the next statement
11//! boundary. Idle-connection push (no statement in flight) is not
12//! implemented — psycopg2 / libpq poll patterns receive on their next
13//! interaction.
14
15use alloc::string::String;
16use alloc::vec::Vec;
17
18use crate::{EngineError, QueryResult};
19
20impl crate::Engine {
21    /// Drain committed notifications (channel, payload). Wire layers emit
22    /// each as a NotificationResponse; embedded callers consume directly.
23    pub fn take_notifications(&mut self) -> Vec<(String, String)> {
24        core::mem::take(&mut self.delivered_notifies)
25    }
26
27    /// COMMIT boundary — release the transaction's pending notifications
28    /// to the delivery queue (only channels someone LISTENs on).
29    pub(crate) fn notifies_on_commit(&mut self) {
30        let pending = core::mem::take(&mut self.tx_pending_notifies);
31        for (ch, payload) in pending {
32            if self.listen_channels.contains(&ch) {
33                self.delivered_notifies.push((ch, payload));
34            }
35        }
36    }
37
38    /// ROLLBACK boundary — the aborted transaction's notifications vanish.
39    pub(crate) fn notifies_on_rollback(&mut self) {
40        self.tx_pending_notifies.clear();
41    }
42
43    pub(crate) fn exec_listen(&mut self, channel: String) -> Result<QueryResult, EngineError> {
44        self.listen_channels.insert(channel);
45        Ok(QueryResult::CommandOk {
46            affected: 0,
47            modified_catalog: false,
48        })
49    }
50
51    pub(crate) fn exec_unlisten(
52        &mut self,
53        channel: Option<String>,
54    ) -> Result<QueryResult, EngineError> {
55        match channel {
56            Some(c) => {
57                self.listen_channels.remove(&c);
58            }
59            None => self.listen_channels.clear(),
60        }
61        Ok(QueryResult::CommandOk {
62            affected: 0,
63            modified_catalog: false,
64        })
65    }
66
67    pub(crate) fn exec_notify(
68        &mut self,
69        channel: String,
70        payload: Option<String>,
71    ) -> Result<QueryResult, EngineError> {
72        let payload = payload.unwrap_or_default();
73        if self.in_transaction() {
74            // PG deduplicates identical (channel, payload) pairs within one
75            // transaction.
76            if !self
77                .tx_pending_notifies
78                .iter()
79                .any(|(c, p)| *c == channel && *p == payload)
80            {
81                self.tx_pending_notifies.push((channel, payload));
82            }
83        } else if self.listen_channels.contains(&channel) {
84            // Autocommit: immediate delivery.
85            self.delivered_notifies.push((channel, payload));
86        }
87        Ok(QueryResult::CommandOk {
88            affected: 0,
89            modified_catalog: false,
90        })
91    }
92}