Skip to main content

hyperdb_compile_check/
diagnostic.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! `ValidationError` variants and human-readable formatting.
5//!
6//! All types here use plain Rust strings — no `syn`/`proc-macro2` token types.
7//! The proc-macro shell converts these into `compile_error!` token streams.
8
9/// The result of validating one `query_as!` / `query_scalar!` invocation.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum ValidationError {
12    /// The struct used in `query_as!` has not been registered via
13    /// `#[derive(Table)] #[hyperdb(register)]`.
14    StructNotRegistered {
15        /// The Rust struct ident string.
16        struct_name: String,
17    },
18
19    /// One or more tables referenced by the SQL are not in the registry.
20    TablesNotRegistered {
21        /// Table names that were referenced but not registered.
22        tables: Vec<String>,
23    },
24
25    /// The query's result schema is missing columns that the target struct
26    /// requires (one entry per missing column name).
27    MissingColumns {
28        /// The Rust struct ident string.
29        struct_name: String,
30        /// Column names present in the struct but absent from the query result.
31        missing: Vec<String>,
32    },
33
34    /// The SQL references a column that does not exist on any table in the
35    /// query (SQLSTATE 42703). Distinct from `MissingColumns`, which is when
36    /// the query is valid but omits a column the struct needs.
37    UnknownColumn {
38        /// The column identifier Hyper reported as undefined.
39        column: String,
40    },
41
42    /// The SQL has a syntax error; the message is forwarded verbatim from Hyper.
43    SqlSyntaxError {
44        /// Hyper's error message.
45        message: String,
46    },
47
48    /// An unexpected Hyper error occurred during dry-run.
49    HyperError {
50        /// Hyper's error message.
51        message: String,
52    },
53}
54
55impl ValidationError {
56    /// Human-readable diagnostic message suitable for embedding in
57    /// `compile_error!("...")`.
58    pub fn to_diagnostic(&self) -> String {
59        match self {
60            Self::StructNotRegistered { struct_name } => format!(
61                "type `{struct_name}` must `#[derive(Table)]` with `#[hyperdb(register)]` \
62                 to be used with `query_as!`"
63            ),
64            Self::TablesNotRegistered { tables } => {
65                if tables.len() == 1 {
66                    format!(
67                        "table {:?} is not registered; did you forget \
68                         `#[derive(Table)] #[hyperdb(register)]` on the struct that maps to it?",
69                        tables[0]
70                    )
71                } else {
72                    format!(
73                        "tables {tables:?} are not registered; add \
74                         `#[derive(Table)] #[hyperdb(register)]` to the structs that map to them"
75                    )
76                }
77            }
78            Self::MissingColumns {
79                struct_name,
80                missing,
81            } => {
82                if missing.len() == 1 {
83                    format!(
84                        "`{struct_name}` requires column {:?} but the query does not project it; \
85                         add it to the SELECT list or remove the field from `{struct_name}`",
86                        missing[0]
87                    )
88                } else {
89                    format!(
90                        "`{struct_name}` requires columns {missing:?} but the query does not \
91                         project them; add them to the SELECT list or remove the fields from \
92                         `{struct_name}`"
93                    )
94                }
95            }
96            Self::UnknownColumn { column } => format!(
97                "column {column:?} does not exist on any table in the query; \
98                 check for a typo or a renamed/dropped column"
99            ),
100            Self::SqlSyntaxError { message } => {
101                format!("SQL syntax error: {message}")
102            }
103            Self::HyperError { message } => {
104                format!("Hyper validation error: {message}")
105            }
106        }
107    }
108}
109
110impl std::fmt::Display for ValidationError {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        f.write_str(&self.to_diagnostic())
113    }
114}
115
116impl std::error::Error for ValidationError {}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn struct_not_registered_message() {
124        let e = ValidationError::StructNotRegistered {
125            struct_name: "User".into(),
126        };
127        assert!(e.to_diagnostic().contains("User"));
128        assert!(e.to_diagnostic().contains("derive(Table)"));
129    }
130
131    #[test]
132    fn single_missing_column_message() {
133        let e = ValidationError::MissingColumns {
134            struct_name: "User".into(),
135            missing: vec!["email".into()],
136        };
137        let msg = e.to_diagnostic();
138        assert!(msg.contains("email"), "message: {msg}");
139        assert!(msg.contains("User"), "message: {msg}");
140    }
141
142    #[test]
143    fn multi_missing_columns_message() {
144        let e = ValidationError::MissingColumns {
145            struct_name: "User".into(),
146            missing: vec!["email".into(), "name".into()],
147        };
148        let msg = e.to_diagnostic();
149        assert!(msg.contains("email"), "message: {msg}");
150        assert!(msg.contains("name"), "message: {msg}");
151    }
152
153    #[test]
154    fn single_table_not_registered_message() {
155        let e = ValidationError::TablesNotRegistered {
156            tables: vec!["ghosts".into()],
157        };
158        assert!(e.to_diagnostic().contains("ghosts"));
159        assert!(e.to_diagnostic().contains("derive(Table)"));
160    }
161
162    #[test]
163    fn unknown_column_message() {
164        let e = ValidationError::UnknownColumn { column: "d".into() };
165        let msg = e.to_diagnostic();
166        assert!(msg.contains("\"d\""), "message: {msg}");
167        assert!(msg.contains("does not exist"), "message: {msg}");
168        assert!(msg.contains("typo"), "message: {msg}");
169    }
170}