1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[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 #[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 #[must_use]
47 pub const fn is_read(self) -> bool {
48 matches!(self, Self::Select | Self::Explain)
49 }
50
51 #[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 #[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 #[must_use]
79 pub const fn is_destructive(self) -> bool {
80 matches!(self, Self::Delete | Self::Drop | Self::Truncate)
81 }
82
83 #[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 #[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 #[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#[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#[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#[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}