Skip to main content

use_sql_query/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Common SQL query kind labels.
8#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum SqlQueryKind {
10    Select,
11    Insert,
12    Update,
13    Delete,
14    Create,
15    Alter,
16    Drop,
17    Truncate,
18    Merge,
19    Explain,
20    Vacuum,
21    #[default]
22    Unknown,
23}
24
25impl SqlQueryKind {
26    /// Returns the stable uppercase query kind label.
27    #[must_use]
28    pub const fn as_str(self) -> &'static str {
29        match self {
30            Self::Select => "SELECT",
31            Self::Insert => "INSERT",
32            Self::Update => "UPDATE",
33            Self::Delete => "DELETE",
34            Self::Create => "CREATE",
35            Self::Alter => "ALTER",
36            Self::Drop => "DROP",
37            Self::Truncate => "TRUNCATE",
38            Self::Merge => "MERGE",
39            Self::Explain => "EXPLAIN",
40            Self::Vacuum => "VACUUM",
41            Self::Unknown => "UNKNOWN",
42        }
43    }
44
45    /// Returns whether the query kind is conservatively read-only.
46    #[must_use]
47    pub const fn is_read(self) -> bool {
48        matches!(self, Self::Select | Self::Explain)
49    }
50
51    /// Returns whether the query kind commonly mutates data or schema.
52    #[must_use]
53    pub const fn is_write(self) -> bool {
54        matches!(
55            self,
56            Self::Insert
57                | Self::Update
58                | Self::Delete
59                | Self::Create
60                | Self::Alter
61                | Self::Drop
62                | Self::Truncate
63                | Self::Merge
64                | Self::Vacuum
65        )
66    }
67
68    /// Returns whether the query kind commonly changes schema.
69    #[must_use]
70    pub const fn is_schema_change(self) -> bool {
71        matches!(
72            self,
73            Self::Create | Self::Alter | Self::Drop | Self::Truncate
74        )
75    }
76
77    /// Returns whether the query kind is conservatively destructive.
78    #[must_use]
79    pub const fn is_destructive(self) -> bool {
80        matches!(self, Self::Delete | Self::Drop | Self::Truncate)
81    }
82
83    /// Returns a broad query intent label.
84    #[must_use]
85    pub const fn intent(self) -> SqlQueryIntent {
86        if self.is_read() {
87            SqlQueryIntent::Read
88        } else if self.is_schema_change() {
89            SqlQueryIntent::SchemaChange
90        } else if matches!(self, Self::Vacuum) {
91            SqlQueryIntent::Maintenance
92        } else if self.is_write() {
93            SqlQueryIntent::Write
94        } else {
95            SqlQueryIntent::Unknown
96        }
97    }
98
99    /// Returns a broad safety label.
100    #[must_use]
101    pub const fn safety(self) -> SqlQuerySafety {
102        if self.is_destructive() {
103            SqlQuerySafety::Destructive
104        } else if self.is_write() {
105            SqlQuerySafety::Mutating
106        } else if self.is_read() {
107            SqlQuerySafety::ReadOnly
108        } else {
109            SqlQuerySafety::Unknown
110        }
111    }
112
113    /// Classifies the first SQL-looking word without full parsing.
114    #[must_use]
115    pub fn classify(input: &str) -> Self {
116        input
117            .split_whitespace()
118            .next()
119            .and_then(|word| word.parse().ok())
120            .unwrap_or(Self::Unknown)
121    }
122}
123
124impl fmt::Display for SqlQueryKind {
125    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
126        formatter.write_str(self.as_str())
127    }
128}
129
130impl FromStr for SqlQueryKind {
131    type Err = SqlQueryKindParseError;
132
133    fn from_str(input: &str) -> Result<Self, Self::Err> {
134        match normalized_kind(input)?.as_str() {
135            "SELECT" => Ok(Self::Select),
136            "INSERT" => Ok(Self::Insert),
137            "UPDATE" => Ok(Self::Update),
138            "DELETE" => Ok(Self::Delete),
139            "CREATE" => Ok(Self::Create),
140            "ALTER" => Ok(Self::Alter),
141            "DROP" => Ok(Self::Drop),
142            "TRUNCATE" => Ok(Self::Truncate),
143            "MERGE" => Ok(Self::Merge),
144            "EXPLAIN" => Ok(Self::Explain),
145            "VACUUM" => Ok(Self::Vacuum),
146            _ => Ok(Self::Unknown),
147        }
148    }
149}
150
151/// Broad query intent labels.
152#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
153pub enum SqlQueryIntent {
154    Read,
155    Write,
156    SchemaChange,
157    Maintenance,
158    #[default]
159    Unknown,
160}
161
162/// Broad query safety labels.
163#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
164pub enum SqlQuerySafety {
165    ReadOnly,
166    Mutating,
167    Destructive,
168    #[default]
169    Unknown,
170}
171
172/// Error returned when parsing SQL query kinds fails.
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub enum SqlQueryKindParseError {
175    Empty,
176}
177
178impl fmt::Display for SqlQueryKindParseError {
179    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
180        match self {
181            Self::Empty => formatter.write_str("SQL query kind cannot be empty"),
182        }
183    }
184}
185
186impl Error for SqlQueryKindParseError {}
187
188fn normalized_kind(input: &str) -> Result<String, SqlQueryKindParseError> {
189    let trimmed = input.trim();
190    if trimmed.is_empty() {
191        Err(SqlQueryKindParseError::Empty)
192    } else {
193        Ok(trimmed.to_ascii_uppercase())
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::{SqlQueryIntent, SqlQueryKind, SqlQueryKindParseError, SqlQuerySafety};
200
201    #[test]
202    fn classifies_query_kinds() -> Result<(), SqlQueryKindParseError> {
203        let delete: SqlQueryKind = "delete".parse()?;
204        assert!(SqlQueryKind::Select.is_read());
205        assert!(delete.is_write());
206        assert!(delete.is_destructive());
207        assert_eq!(SqlQueryKind::Create.intent(), SqlQueryIntent::SchemaChange);
208        assert_eq!(SqlQueryKind::Drop.safety(), SqlQuerySafety::Destructive);
209        Ok(())
210    }
211
212    #[test]
213    fn classifies_first_token_conservatively() {
214        assert_eq!(
215            SqlQueryKind::classify(" select * from users"),
216            SqlQueryKind::Select
217        );
218        assert_eq!(
219            SqlQueryKind::classify("with users as (...)"),
220            SqlQueryKind::Unknown
221        );
222    }
223}