Skip to main content

hyperdb_api_core/client/
notice.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Notice handling for PostgreSQL/Hyper server messages.
5
6use crate::protocol::message::backend::NoticeResponseBody;
7use tracing::trace;
8
9/// A notice or warning message from the server.
10///
11/// Notices are informational messages that don't indicate failure but may
12/// contain useful information about query execution, deprecation warnings,
13/// or performance hints.
14#[derive(Debug, Clone)]
15pub struct Notice {
16    severity: Option<String>,
17    code: Option<String>,
18    message: String,
19    detail: Option<String>,
20    hint: Option<String>,
21    position: Option<i32>,
22}
23
24impl Notice {
25    /// Returns the severity level (e.g., "WARNING", "NOTICE").
26    #[inline]
27    #[must_use]
28    pub fn severity(&self) -> Option<&str> {
29        self.severity.as_deref()
30    }
31
32    /// Returns the SQLSTATE error code.
33    #[inline]
34    #[must_use]
35    pub fn code(&self) -> Option<&str> {
36        self.code.as_deref()
37    }
38
39    /// Returns the primary message text.
40    #[inline]
41    #[must_use]
42    pub fn message(&self) -> &str {
43        &self.message
44    }
45
46    /// Returns additional detail about the notice.
47    #[inline]
48    #[must_use]
49    pub fn detail(&self) -> Option<&str> {
50        self.detail.as_deref()
51    }
52
53    /// Returns a hint for resolving the issue.
54    #[inline]
55    #[must_use]
56    pub fn hint(&self) -> Option<&str> {
57        self.hint.as_deref()
58    }
59
60    /// Returns the position in the query where the notice was raised.
61    #[inline]
62    #[must_use]
63    pub fn position(&self) -> Option<i32> {
64        self.position
65    }
66}
67
68impl Notice {
69    /// Parses a Notice from a `NoticeResponseBody`.
70    pub(crate) fn from_response_body(body: &NoticeResponseBody) -> Self {
71        let mut severity = None;
72        let mut code = None;
73        let mut message = String::new();
74        let mut detail = None;
75        let mut hint = None;
76        let mut position = None;
77
78        for field in body.fields().filter_map(|r| {
79            r.map_err(|e| trace!(target: "hyperdb_api_core::client", error = %e, "dropped error parsing notice field")).ok()
80        }) {
81            match (field.type_(), field.value()) {
82                    (b'S', Ok(v)) => severity = Some(v.to_string()),
83                    (b'V', Ok(v)) if severity.is_none() => severity = Some(v.to_string()),
84                    (b'C', Ok(v)) => code = Some(v.to_string()),
85                    (b'M', Ok(v)) => message = v.to_string(),
86                    (b'D', Ok(v)) => detail = Some(v.to_string()),
87                    (b'H', Ok(v)) => hint = Some(v.to_string()),
88                    (b'P', Ok(v)) => position = v.parse().ok(),
89                    _ => {}
90                }
91        }
92
93        Notice {
94            severity,
95            code,
96            message,
97            detail,
98            hint,
99            position,
100        }
101    }
102
103    /// Returns true if this is a warning-level notice.
104    #[inline]
105    #[must_use]
106    pub fn is_warning(&self) -> bool {
107        matches!(self.severity(), Some("WARNING" | "WARN"))
108    }
109
110    /// Returns true if this is an informational notice.
111    #[inline]
112    #[must_use]
113    pub fn is_info(&self) -> bool {
114        matches!(self.severity(), Some("NOTICE" | "INFO" | "LOG" | "DEBUG"))
115    }
116}
117
118impl std::fmt::Display for Notice {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        if let Some(sev) = self.severity() {
121            write!(f, "{sev}: ")?;
122        }
123        write!(f, "{}", self.message())?;
124        if let Some(code) = self.code() {
125            write!(f, " ({code})")?;
126        }
127        Ok(())
128    }
129}
130
131/// Type alias for a notice receiver callback.
132///
133/// The callback receives a `Notice` and is called whenever the server sends
134/// a notice or warning message during query execution.
135///
136/// # Thread Safety
137///
138/// The callback must be `Send + Sync` because it may be called from
139/// different threads during concurrent query execution.
140pub type NoticeReceiver = Box<dyn Fn(Notice) + Send + Sync>;
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    fn make_notice(severity: Option<&str>, code: Option<&str>, message: &str) -> Notice {
147        Notice {
148            severity: severity.map(String::from),
149            code: code.map(String::from),
150            message: message.to_string(),
151            detail: None,
152            hint: None,
153            position: None,
154        }
155    }
156
157    #[test]
158    fn test_notice_display() {
159        let notice = make_notice(Some("WARNING"), Some("01000"), "test warning");
160        assert_eq!(format!("{notice}"), "WARNING: test warning (01000)");
161    }
162
163    #[test]
164    fn test_notice_accessors() {
165        let notice = make_notice(Some("WARNING"), Some("01000"), "test");
166        assert_eq!(notice.severity(), Some("WARNING"));
167        assert_eq!(notice.code(), Some("01000"));
168        assert_eq!(notice.message(), "test");
169    }
170
171    #[test]
172    fn test_notice_is_warning() {
173        let warning = make_notice(Some("WARNING"), None, "");
174        assert!(warning.is_warning());
175
176        let info = make_notice(Some("INFO"), None, "");
177        assert!(!info.is_warning());
178        assert!(info.is_info());
179    }
180}