Skip to main content

drizzle_mysql/
common.rs

1use crate::prelude::{String, Vec};
2use drizzle_core::{SQLIndexInfo, SQLSchemaType, SQLViewInfo, TableRef};
3use drizzle_types::mysql::ddl::{ViewAlgorithm, ViewCheckOption, ViewSqlSecurity};
4
5/// Saves the session's foreign-key check setting before
6/// [`DISABLE_FOREIGN_KEY_CHECKS`].
7///
8/// A schema whose tables reference each other in a cycle cannot create them
9/// one after another while `MySQL` checks that each reference exists, so its
10/// `create_statements` create those tables with the checks off, the way
11/// `mysqldump` does, and restore the setting after.
12pub const SAVE_FOREIGN_KEY_CHECKS: &str =
13    "SET @drizzle_foreign_key_checks = @@SESSION.foreign_key_checks";
14
15/// Turns foreign-key checks off for the session; see
16/// [`SAVE_FOREIGN_KEY_CHECKS`].
17pub const DISABLE_FOREIGN_KEY_CHECKS: &str = "SET FOREIGN_KEY_CHECKS = 0";
18
19/// Restores the setting [`SAVE_FOREIGN_KEY_CHECKS`] saved and forgets it.
20/// With nothing saved it leaves the setting as it is, so a failed `create`
21/// can always run it.
22pub const RESTORE_FOREIGN_KEY_CHECKS: &str = "SET FOREIGN_KEY_CHECKS = \
23     COALESCE(@drizzle_foreign_key_checks, @@SESSION.foreign_key_checks), \
24     @drizzle_foreign_key_checks = NULL";
25
26/// `MySQL`-specific options of a generated view.
27pub trait MySQLViewInfo: SQLViewInfo + core::fmt::Debug {
28    /// The `ALGORITHM` option, if set.
29    fn algorithm(&self) -> Option<ViewAlgorithm>;
30    /// The `SQL SECURITY` option, if set.
31    fn sql_security(&self) -> Option<ViewSqlSecurity>;
32    /// The `WITH .. CHECK OPTION` mode, if set.
33    fn check_option(&self) -> Option<ViewCheckOption>;
34}
35
36/// Renders the `CREATE VIEW` statement for a generated view.
37///
38/// Writes `CREATE [ALGORITHM=..] [SQL SECURITY ..] VIEW name AS definition
39/// [WITH .. CHECK OPTION];`, with the name backtick-quoted and qualified by
40/// its database when it has one. Returns an empty string for a view marked
41/// `EXISTING`.
42#[must_use]
43pub fn create_view_sql(view: &dyn MySQLViewInfo) -> String {
44    if view.is_existing() {
45        return String::new();
46    }
47
48    let mut sql = String::from("CREATE ");
49    if let Some(algorithm) = view.algorithm() {
50        sql.push_str(match algorithm {
51            ViewAlgorithm::Undefined => "ALGORITHM=UNDEFINED ",
52            ViewAlgorithm::Merge => "ALGORITHM=MERGE ",
53            ViewAlgorithm::Temptable => "ALGORITHM=TEMPTABLE ",
54        });
55    }
56    if let Some(security) = view.sql_security() {
57        sql.push_str(match security {
58            ViewSqlSecurity::Definer => "SQL SECURITY DEFINER ",
59            ViewSqlSecurity::Invoker => "SQL SECURITY INVOKER ",
60        });
61    }
62    sql.push_str("VIEW ");
63    if let Some(database) = drizzle_core::SQLTableInfo::schema(view) {
64        push_identifier(&mut sql, database);
65        sql.push('.');
66    }
67    push_identifier(&mut sql, drizzle_core::SQLTableInfo::name(view));
68    sql.push_str(" AS ");
69    sql.push_str(&view.definition_sql());
70    if let Some(check_option) = view.check_option() {
71        sql.push_str(match check_option {
72            ViewCheckOption::Cascaded => " WITH CASCADED CHECK OPTION",
73            ViewCheckOption::Local => " WITH LOCAL CHECK OPTION",
74        });
75    }
76    sql.push(';');
77    sql
78}
79
80fn flush_identifier(tokens: &mut Vec<String>, token: &mut String) {
81    if !token.is_empty() {
82        tokens.push(core::mem::take(token));
83    }
84}
85
86fn identifier_tokens(sql: &str) -> Vec<String> {
87    #[derive(Clone, Copy)]
88    enum State {
89        Sql,
90        QuotedIdentifier,
91        SingleQuotedString,
92        DoubleQuotedString,
93        LineComment,
94        BlockComment,
95    }
96
97    let mut tokens = Vec::new();
98    let mut token = String::new();
99    let characters: Vec<_> = sql.chars().collect();
100    let mut state = State::Sql;
101    let mut index = 0;
102
103    while index < characters.len() {
104        let character = characters[index];
105        match state {
106            State::Sql => match character {
107                '`' => {
108                    flush_identifier(&mut tokens, &mut token);
109                    state = State::QuotedIdentifier;
110                }
111                '\'' => {
112                    flush_identifier(&mut tokens, &mut token);
113                    state = State::SingleQuotedString;
114                }
115                '"' => {
116                    flush_identifier(&mut tokens, &mut token);
117                    state = State::DoubleQuotedString;
118                }
119                '#' => {
120                    flush_identifier(&mut tokens, &mut token);
121                    state = State::LineComment;
122                }
123                '-' if characters.get(index + 1) == Some(&'-')
124                    && characters
125                        .get(index + 2)
126                        .is_none_or(|next| next.is_whitespace() || next.is_control()) =>
127                {
128                    flush_identifier(&mut tokens, &mut token);
129                    state = State::LineComment;
130                    index += 1;
131                }
132                '/' if characters.get(index + 1) == Some(&'*') => {
133                    flush_identifier(&mut tokens, &mut token);
134                    state = State::BlockComment;
135                    index += 1;
136                }
137                character if character.is_alphanumeric() || matches!(character, '_' | '$') => {
138                    token.push(character);
139                }
140                _ => flush_identifier(&mut tokens, &mut token),
141            },
142            State::QuotedIdentifier => {
143                if character == '`' {
144                    if characters.get(index + 1) == Some(&'`') {
145                        token.push('`');
146                        index += 1;
147                    } else {
148                        flush_identifier(&mut tokens, &mut token);
149                        state = State::Sql;
150                    }
151                } else {
152                    token.push(character);
153                }
154            }
155            State::SingleQuotedString | State::DoubleQuotedString => {
156                let quote = match state {
157                    State::SingleQuotedString => '\'',
158                    State::DoubleQuotedString => '"',
159                    _ => unreachable!(),
160                };
161                if character == '\\' {
162                    index += usize::from(index + 1 < characters.len());
163                } else if character == quote {
164                    if characters.get(index + 1) == Some(&quote) {
165                        index += 1;
166                    } else {
167                        state = State::Sql;
168                    }
169                }
170            }
171            State::LineComment => {
172                if matches!(character, '\n' | '\r') {
173                    state = State::Sql;
174                }
175            }
176            State::BlockComment => {
177                if character == '*' && characters.get(index + 1) == Some(&'/') {
178                    state = State::Sql;
179                    index += 1;
180                }
181            }
182        }
183        index += 1;
184    }
185    flush_identifier(&mut tokens, &mut token);
186    tokens
187}
188
189/// Renders `CREATE VIEW` statements for the managed views of a schema, each
190/// after the views it references.
191///
192/// Views marked `EXISTING` are skipped. A view counts as referencing another
193/// when the other's name appears as an identifier in its definition. Public
194/// only for code emitted by `drizzle-macros`.
195///
196/// # Errors
197///
198/// Returns [`DrizzleError::Statement`](drizzle_core::error::DrizzleError::Statement)
199/// if the views reference each other in a cycle.
200#[doc(hidden)]
201pub fn order_schema_views(
202    views: &[&'static dyn MySQLViewInfo],
203) -> Result<Vec<String>, drizzle_core::error::DrizzleError> {
204    let mut pending: Vec<_> = views
205        .iter()
206        .copied()
207        .filter(|view| !view.is_existing())
208        .collect();
209    pending.sort_by(|left, right| left.qualified_name().cmp(&right.qualified_name()));
210
211    let mut statements = Vec::with_capacity(pending.len());
212    while !pending.is_empty() {
213        let ready = pending.iter().position(|view| {
214            let tokens = identifier_tokens(&view.definition_sql());
215            !pending.iter().any(|candidate| {
216                !core::ptr::eq(*view, *candidate)
217                    && tokens.iter().any(|token| token == candidate.name())
218            })
219        });
220        let Some(ready) = ready else {
221            let names = pending
222                .iter()
223                .map(|view| view.qualified_name().into_owned())
224                .collect::<Vec<_>>()
225                .join(", ");
226            let mut message = String::from("Cyclic view dependency detected in MySQLSchema: ");
227            message.push_str(&names);
228            return Err(drizzle_core::error::DrizzleError::Statement(message.into()));
229        };
230        statements.push(create_view_sql(pending.remove(ready)));
231    }
232    Ok(statements)
233}
234
235fn push_identifier(sql: &mut String, identifier: &str) {
236    sql.push('`');
237    for character in identifier.chars() {
238        if character == '`' {
239            sql.push('`');
240        }
241        sql.push(character);
242    }
243    sql.push('`');
244}
245
246/// The `MySQL` dialect marker for schema items.
247///
248/// Used as a type parameter (`SQLTable<'a, MySQLSchemaType, MySQLValue<'a>>`)
249/// to tie tables, indexes and views to `MySQL`. The variants name the kinds
250/// of schema object.
251#[derive(Debug, Clone)]
252pub enum MySQLSchemaType {
253    /// A table definition.
254    Table(&'static TableRef),
255    /// An index definition.
256    Index(&'static dyn SQLIndexInfo),
257    /// A view definition.
258    View(&'static dyn MySQLViewInfo),
259}
260
261impl SQLSchemaType for MySQLSchemaType {}