mysqladm/core/database_privileges/
diff.rs

1//! This module contains datastructures and logic for comparing database privileges,
2//! generating, validating and reducing diffs between two sets of database privileges.
3
4use super::base::{DatabasePrivilegeRow, db_priv_field_human_readable_name};
5use crate::core::types::{MySQLDatabase, MySQLUser};
6use prettytable::Table;
7use serde::{Deserialize, Serialize};
8use std::{
9    collections::{BTreeSet, HashMap, hash_map::Entry},
10    fmt,
11};
12
13/// This enum represents a change for a single privilege.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
15pub enum DatabasePrivilegeChange {
16    YesToNo,
17    NoToYes,
18}
19
20impl DatabasePrivilegeChange {
21    pub fn new(p1: bool, p2: bool) -> Option<DatabasePrivilegeChange> {
22        match (p1, p2) {
23            (true, false) => Some(DatabasePrivilegeChange::YesToNo),
24            (false, true) => Some(DatabasePrivilegeChange::NoToYes),
25            _ => None,
26        }
27    }
28}
29
30/// This struct encapsulates the before and after states of the
31/// access privileges for a single user on a single database.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Default)]
33pub struct DatabasePrivilegeRowDiff {
34    // TODO: don't store the db and user here, let the type be stored in a mapping
35    pub db: MySQLDatabase,
36    pub user: MySQLUser,
37    pub select_priv: Option<DatabasePrivilegeChange>,
38    pub insert_priv: Option<DatabasePrivilegeChange>,
39    pub update_priv: Option<DatabasePrivilegeChange>,
40    pub delete_priv: Option<DatabasePrivilegeChange>,
41    pub create_priv: Option<DatabasePrivilegeChange>,
42    pub drop_priv: Option<DatabasePrivilegeChange>,
43    pub alter_priv: Option<DatabasePrivilegeChange>,
44    pub index_priv: Option<DatabasePrivilegeChange>,
45    pub create_tmp_table_priv: Option<DatabasePrivilegeChange>,
46    pub lock_tables_priv: Option<DatabasePrivilegeChange>,
47    pub references_priv: Option<DatabasePrivilegeChange>,
48}
49
50impl DatabasePrivilegeRowDiff {
51    /// Calculates the difference between two [`DatabasePrivilegeRow`] instances.
52    pub fn from_rows(
53        row1: &DatabasePrivilegeRow,
54        row2: &DatabasePrivilegeRow,
55    ) -> DatabasePrivilegeRowDiff {
56        debug_assert!(row1.db == row2.db && row1.user == row2.user);
57
58        DatabasePrivilegeRowDiff {
59            db: row1.db.to_owned(),
60            user: row1.user.to_owned(),
61            select_priv: DatabasePrivilegeChange::new(row1.select_priv, row2.select_priv),
62            insert_priv: DatabasePrivilegeChange::new(row1.insert_priv, row2.insert_priv),
63            update_priv: DatabasePrivilegeChange::new(row1.update_priv, row2.update_priv),
64            delete_priv: DatabasePrivilegeChange::new(row1.delete_priv, row2.delete_priv),
65            create_priv: DatabasePrivilegeChange::new(row1.create_priv, row2.create_priv),
66            drop_priv: DatabasePrivilegeChange::new(row1.drop_priv, row2.drop_priv),
67            alter_priv: DatabasePrivilegeChange::new(row1.alter_priv, row2.alter_priv),
68            index_priv: DatabasePrivilegeChange::new(row1.index_priv, row2.index_priv),
69            create_tmp_table_priv: DatabasePrivilegeChange::new(
70                row1.create_tmp_table_priv,
71                row2.create_tmp_table_priv,
72            ),
73            lock_tables_priv: DatabasePrivilegeChange::new(
74                row1.lock_tables_priv,
75                row2.lock_tables_priv,
76            ),
77            references_priv: DatabasePrivilegeChange::new(
78                row1.references_priv,
79                row2.references_priv,
80            ),
81        }
82    }
83
84    /// Returns true if there are no changes in this diff.
85    pub fn is_empty(&self) -> bool {
86        self.select_priv.is_none()
87            && self.insert_priv.is_none()
88            && self.update_priv.is_none()
89            && self.delete_priv.is_none()
90            && self.create_priv.is_none()
91            && self.drop_priv.is_none()
92            && self.alter_priv.is_none()
93            && self.index_priv.is_none()
94            && self.create_tmp_table_priv.is_none()
95            && self.lock_tables_priv.is_none()
96            && self.references_priv.is_none()
97    }
98
99    /// Retrieves the privilege change for a given privilege name.
100    pub fn get_privilege_change_by_name(
101        &self,
102        privilege_name: &str,
103    ) -> anyhow::Result<Option<DatabasePrivilegeChange>> {
104        match privilege_name {
105            "select_priv" => Ok(self.select_priv),
106            "insert_priv" => Ok(self.insert_priv),
107            "update_priv" => Ok(self.update_priv),
108            "delete_priv" => Ok(self.delete_priv),
109            "create_priv" => Ok(self.create_priv),
110            "drop_priv" => Ok(self.drop_priv),
111            "alter_priv" => Ok(self.alter_priv),
112            "index_priv" => Ok(self.index_priv),
113            "create_tmp_table_priv" => Ok(self.create_tmp_table_priv),
114            "lock_tables_priv" => Ok(self.lock_tables_priv),
115            "references_priv" => Ok(self.references_priv),
116            _ => anyhow::bail!("Unknown privilege name: {}", privilege_name),
117        }
118    }
119
120    /// Merges another diff into this one, combining them in a sequential manner.
121    fn mappend(&mut self, other: &DatabasePrivilegeRowDiff) {
122        debug_assert!(self.db == other.db && self.user == other.user);
123
124        if other.select_priv.is_some() {
125            self.select_priv = other.select_priv;
126        }
127        if other.insert_priv.is_some() {
128            self.insert_priv = other.insert_priv;
129        }
130        if other.update_priv.is_some() {
131            self.update_priv = other.update_priv;
132        }
133        if other.delete_priv.is_some() {
134            self.delete_priv = other.delete_priv;
135        }
136        if other.create_priv.is_some() {
137            self.create_priv = other.create_priv;
138        }
139        if other.drop_priv.is_some() {
140            self.drop_priv = other.drop_priv;
141        }
142        if other.alter_priv.is_some() {
143            self.alter_priv = other.alter_priv;
144        }
145        if other.index_priv.is_some() {
146            self.index_priv = other.index_priv;
147        }
148        if other.create_tmp_table_priv.is_some() {
149            self.create_tmp_table_priv = other.create_tmp_table_priv;
150        }
151        if other.lock_tables_priv.is_some() {
152            self.lock_tables_priv = other.lock_tables_priv;
153        }
154        if other.references_priv.is_some() {
155            self.references_priv = other.references_priv;
156        }
157    }
158
159    /// Removes any no-op changes from the diff, based on the original privilege row.
160    fn remove_noops(&mut self, from: &DatabasePrivilegeRow) {
161        fn new_value(
162            change: &Option<DatabasePrivilegeChange>,
163            from_value: bool,
164        ) -> Option<DatabasePrivilegeChange> {
165            change.as_ref().and_then(|c| match c {
166                DatabasePrivilegeChange::YesToNo if from_value => {
167                    Some(DatabasePrivilegeChange::YesToNo)
168                }
169                DatabasePrivilegeChange::NoToYes if !from_value => {
170                    Some(DatabasePrivilegeChange::NoToYes)
171                }
172                _ => None,
173            })
174        }
175
176        self.select_priv = new_value(&self.select_priv, from.select_priv);
177        self.insert_priv = new_value(&self.insert_priv, from.insert_priv);
178        self.update_priv = new_value(&self.update_priv, from.update_priv);
179        self.delete_priv = new_value(&self.delete_priv, from.delete_priv);
180        self.create_priv = new_value(&self.create_priv, from.create_priv);
181        self.drop_priv = new_value(&self.drop_priv, from.drop_priv);
182        self.alter_priv = new_value(&self.alter_priv, from.alter_priv);
183        self.index_priv = new_value(&self.index_priv, from.index_priv);
184        self.create_tmp_table_priv =
185            new_value(&self.create_tmp_table_priv, from.create_tmp_table_priv);
186        self.lock_tables_priv = new_value(&self.lock_tables_priv, from.lock_tables_priv);
187        self.references_priv = new_value(&self.references_priv, from.references_priv);
188    }
189
190    fn apply(&self, base: &mut DatabasePrivilegeRow) {
191        fn apply_change(change: &Option<DatabasePrivilegeChange>, target: &mut bool) {
192            match change {
193                Some(DatabasePrivilegeChange::YesToNo) => *target = false,
194                Some(DatabasePrivilegeChange::NoToYes) => *target = true,
195                None => {}
196            }
197        }
198
199        apply_change(&self.select_priv, &mut base.select_priv);
200        apply_change(&self.insert_priv, &mut base.insert_priv);
201        apply_change(&self.update_priv, &mut base.update_priv);
202        apply_change(&self.delete_priv, &mut base.delete_priv);
203        apply_change(&self.create_priv, &mut base.create_priv);
204        apply_change(&self.drop_priv, &mut base.drop_priv);
205        apply_change(&self.alter_priv, &mut base.alter_priv);
206        apply_change(&self.index_priv, &mut base.index_priv);
207        apply_change(&self.create_tmp_table_priv, &mut base.create_tmp_table_priv);
208        apply_change(&self.lock_tables_priv, &mut base.lock_tables_priv);
209        apply_change(&self.references_priv, &mut base.references_priv);
210    }
211}
212
213impl fmt::Display for DatabasePrivilegeRowDiff {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        fn format_change(
216            f: &mut fmt::Formatter<'_>,
217            change: &Option<DatabasePrivilegeChange>,
218            field_name: &str,
219        ) -> fmt::Result {
220            if let Some(change) = change {
221                match change {
222                    DatabasePrivilegeChange::YesToNo => f.write_fmt(format_args!(
223                        "{}: Y -> N\n",
224                        db_priv_field_human_readable_name(field_name)
225                    )),
226                    DatabasePrivilegeChange::NoToYes => f.write_fmt(format_args!(
227                        "{}: N -> Y\n",
228                        db_priv_field_human_readable_name(field_name)
229                    )),
230                }
231            } else {
232                Ok(())
233            }
234        }
235
236        format_change(f, &self.select_priv, "select_priv")?;
237        format_change(f, &self.insert_priv, "insert_priv")?;
238        format_change(f, &self.update_priv, "update_priv")?;
239        format_change(f, &self.delete_priv, "delete_priv")?;
240        format_change(f, &self.create_priv, "create_priv")?;
241        format_change(f, &self.drop_priv, "drop_priv")?;
242        format_change(f, &self.alter_priv, "alter_priv")?;
243        format_change(f, &self.index_priv, "index_priv")?;
244        format_change(f, &self.create_tmp_table_priv, "create_tmp_table_priv")?;
245        format_change(f, &self.lock_tables_priv, "lock_tables_priv")?;
246        format_change(f, &self.references_priv, "references_priv")?;
247
248        Ok(())
249    }
250}
251
252/// This enum encapsulates whether a [`DatabasePrivilegeRow`] was introduced, modified or deleted.
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
254pub enum DatabasePrivilegesDiff {
255    New(DatabasePrivilegeRow),
256    Modified(DatabasePrivilegeRowDiff),
257    Deleted(DatabasePrivilegeRow),
258    Noop { db: MySQLDatabase, user: MySQLUser },
259}
260
261impl DatabasePrivilegesDiff {
262    pub fn get_database_name(&self) -> &MySQLDatabase {
263        match self {
264            DatabasePrivilegesDiff::New(p) => &p.db,
265            DatabasePrivilegesDiff::Modified(p) => &p.db,
266            DatabasePrivilegesDiff::Deleted(p) => &p.db,
267            DatabasePrivilegesDiff::Noop { db, .. } => db,
268        }
269    }
270
271    pub fn get_user_name(&self) -> &MySQLUser {
272        match self {
273            DatabasePrivilegesDiff::New(p) => &p.user,
274            DatabasePrivilegesDiff::Modified(p) => &p.user,
275            DatabasePrivilegesDiff::Deleted(p) => &p.user,
276            DatabasePrivilegesDiff::Noop { user, .. } => user,
277        }
278    }
279
280    /// Merges another [`DatabasePrivilegesDiff`] into this one, combining them in a sequential manner.
281    /// For example, if this diff represents a creation and the other represents a modification,
282    /// the result will be a creation with the modifications applied.
283    pub fn mappend(&mut self, other: &DatabasePrivilegesDiff) -> anyhow::Result<()> {
284        debug_assert!(
285            self.get_database_name() == other.get_database_name()
286                && self.get_user_name() == other.get_user_name()
287        );
288
289        if matches!(self, DatabasePrivilegesDiff::Deleted(_))
290            && (matches!(other, DatabasePrivilegesDiff::Modified(_)))
291        {
292            anyhow::bail!("Cannot modify a deleted database privilege row");
293        }
294
295        if matches!(self, DatabasePrivilegesDiff::New(_))
296            && (matches!(other, DatabasePrivilegesDiff::New(_)))
297        {
298            anyhow::bail!("Cannot create an already existing database privilege row");
299        }
300
301        if matches!(self, DatabasePrivilegesDiff::Modified(_))
302            && (matches!(other, DatabasePrivilegesDiff::New(_)))
303        {
304            anyhow::bail!("Cannot create an already existing database privilege row");
305        }
306
307        if matches!(self, DatabasePrivilegesDiff::Noop { .. }) {
308            *self = other.to_owned();
309            return Ok(());
310        } else if matches!(other, DatabasePrivilegesDiff::Noop { .. }) {
311            return Ok(());
312        }
313
314        match (&self, other) {
315            (DatabasePrivilegesDiff::New(_), DatabasePrivilegesDiff::Modified(modified)) => {
316                let inner_row = match self {
317                    DatabasePrivilegesDiff::New(r) => r,
318                    _ => unreachable!(),
319                };
320                modified.apply(inner_row);
321            }
322            (DatabasePrivilegesDiff::Modified(_), DatabasePrivilegesDiff::Modified(modified)) => {
323                let inner_diff = match self {
324                    DatabasePrivilegesDiff::Modified(r) => r,
325                    _ => unreachable!(),
326                };
327                inner_diff.mappend(modified);
328
329                if inner_diff.is_empty() {
330                    let db = inner_diff.db.to_owned();
331                    let user = inner_diff.user.to_owned();
332                    *self = DatabasePrivilegesDiff::Noop { db, user };
333                }
334            }
335            (DatabasePrivilegesDiff::Modified(_), DatabasePrivilegesDiff::Deleted(deleted)) => {
336                *self = DatabasePrivilegesDiff::Deleted(deleted.to_owned());
337            }
338            (DatabasePrivilegesDiff::New(_), DatabasePrivilegesDiff::Deleted(_)) => {
339                let db = self.get_database_name().to_owned();
340                let user = self.get_user_name().to_owned();
341                *self = DatabasePrivilegesDiff::Noop { db, user };
342            }
343            _ => {}
344        }
345
346        Ok(())
347    }
348}
349
350pub type DatabasePrivilegeState<'a> = &'a [DatabasePrivilegeRow];
351
352/// This function calculates the differences between two sets of database privileges.
353/// It returns a set of [`DatabasePrivilegesDiff`] that can be used to display or
354/// apply a set of privilege modifications to the database.
355pub fn diff_privileges(
356    from: DatabasePrivilegeState<'_>,
357    to: &[DatabasePrivilegeRow],
358) -> BTreeSet<DatabasePrivilegesDiff> {
359    let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
360        HashMap::from_iter(
361            from.iter()
362                .cloned()
363                .map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
364        );
365
366    let to_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
367        HashMap::from_iter(
368            to.iter()
369                .cloned()
370                .map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
371        );
372
373    let mut result = BTreeSet::new();
374
375    for p in to {
376        if let Some(old_p) = from_lookup_table.get(&(p.db.to_owned(), p.user.to_owned())) {
377            let diff = DatabasePrivilegeRowDiff::from_rows(old_p, p);
378            if !diff.is_empty() {
379                result.insert(DatabasePrivilegesDiff::Modified(diff));
380            }
381        } else {
382            result.insert(DatabasePrivilegesDiff::New(p.to_owned()));
383        }
384    }
385
386    for p in from {
387        if !to_lookup_table.contains_key(&(p.db.to_owned(), p.user.to_owned())) {
388            result.insert(DatabasePrivilegesDiff::Deleted(p.to_owned()));
389        }
390    }
391
392    result
393}
394
395/// Converts a set of [`DatabasePrivilegeRowDiff`] into a set of [`DatabasePrivilegesDiff`],
396/// representing either creating new privilege rows, or modifying the existing ones.
397///
398/// This is particularly useful for processing CLI arguments.
399pub fn create_or_modify_privilege_rows(
400    from: DatabasePrivilegeState<'_>,
401    to: &BTreeSet<DatabasePrivilegeRowDiff>,
402) -> anyhow::Result<BTreeSet<DatabasePrivilegesDiff>> {
403    let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
404        HashMap::from_iter(
405            from.iter()
406                .cloned()
407                .map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
408        );
409
410    let mut result = BTreeSet::new();
411
412    for diff in to {
413        if let Some(old_p) = from_lookup_table.get(&(diff.db.to_owned(), diff.user.to_owned())) {
414            let mut modified_diff = diff.to_owned();
415            modified_diff.remove_noops(old_p);
416            if !modified_diff.is_empty() {
417                result.insert(DatabasePrivilegesDiff::Modified(modified_diff));
418            }
419        } else {
420            let mut new_row = DatabasePrivilegeRow {
421                db: diff.db.to_owned(),
422                user: diff.user.to_owned(),
423                select_priv: false,
424                insert_priv: false,
425                update_priv: false,
426                delete_priv: false,
427                create_priv: false,
428                drop_priv: false,
429                alter_priv: false,
430                index_priv: false,
431                create_tmp_table_priv: false,
432                lock_tables_priv: false,
433                references_priv: false,
434            };
435            diff.apply(&mut new_row);
436            result.insert(DatabasePrivilegesDiff::New(new_row));
437        }
438    }
439
440    Ok(result)
441}
442
443/// Reduces a set of [`DatabasePrivilegesDiff`] by removing any modifications that would be no-ops.
444/// For example, if a privilege is changed from Yes to No, but it was already No, that change
445/// is removed from the diff.
446///
447/// The `from` parameter is used to determine the current state of the privileges.
448/// The `to` parameter is the set of diffs to be reduced.
449pub fn reduce_privilege_diffs(
450    from: DatabasePrivilegeState<'_>,
451    to: BTreeSet<DatabasePrivilegesDiff>,
452) -> anyhow::Result<BTreeSet<DatabasePrivilegesDiff>> {
453    let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
454        HashMap::from_iter(
455            from.iter()
456                .cloned()
457                .map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
458        );
459
460    let mut result: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegesDiff> = from_lookup_table
461        .iter()
462        .map(|((db, user), _)| {
463            (
464                (db.to_owned(), user.to_owned()),
465                DatabasePrivilegesDiff::Noop {
466                    db: db.to_owned(),
467                    user: user.to_owned(),
468                },
469            )
470        })
471        .collect();
472
473    for diff in to {
474        let entry = result.entry((
475            diff.get_database_name().to_owned(),
476            diff.get_user_name().to_owned(),
477        ));
478        match entry {
479            Entry::Occupied(mut occupied_entry) => {
480                let existing_diff = occupied_entry.get_mut();
481                existing_diff.mappend(&diff)?;
482            }
483            Entry::Vacant(vacant_entry) => {
484                vacant_entry.insert(diff.to_owned());
485            }
486        }
487    }
488
489    for (key, diff) in result.iter_mut() {
490        if let Some(from_row) = from_lookup_table.get(key)
491            && let DatabasePrivilegesDiff::Modified(modified_diff) = diff
492        {
493            modified_diff.remove_noops(from_row);
494            if modified_diff.is_empty() {
495                let db = modified_diff.db.to_owned();
496                let user = modified_diff.user.to_owned();
497                *diff = DatabasePrivilegesDiff::Noop { db, user };
498            }
499        }
500    }
501
502    Ok(result
503        .into_values()
504        .filter(|diff| !matches!(diff, DatabasePrivilegesDiff::Noop { .. }))
505        .collect::<BTreeSet<DatabasePrivilegesDiff>>())
506}
507
508/// Renders a set of [`DatabasePrivilegesDiff`] into a human-readable formatted table.
509pub fn display_privilege_diffs(diffs: &BTreeSet<DatabasePrivilegesDiff>) -> String {
510    let mut table = Table::new();
511    table.set_titles(row!["Database", "User", "Privilege diff",]);
512    for row in diffs {
513        match row {
514            DatabasePrivilegesDiff::New(p) => {
515                table.add_row(row![
516                    p.db,
517                    p.user,
518                    "(Previously unprivileged)\n".to_string() + &p.to_string()
519                ]);
520            }
521            DatabasePrivilegesDiff::Modified(p) => {
522                table.add_row(row![p.db, p.user, p.to_string(),]);
523            }
524            DatabasePrivilegesDiff::Deleted(p) => {
525                table.add_row(row![p.db, p.user, "Removed".to_string()]);
526            }
527            DatabasePrivilegesDiff::Noop { db, user } => {
528                table.add_row(row![db, user, "No changes".to_string()]);
529            }
530        }
531    }
532
533    table.to_string()
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539
540    #[test]
541    fn test_database_privilege_change_creation() {
542        assert_eq!(
543            DatabasePrivilegeChange::new(true, false),
544            Some(DatabasePrivilegeChange::YesToNo),
545        );
546        assert_eq!(
547            DatabasePrivilegeChange::new(false, true),
548            Some(DatabasePrivilegeChange::NoToYes),
549        );
550        assert_eq!(DatabasePrivilegeChange::new(true, true), None);
551        assert_eq!(DatabasePrivilegeChange::new(false, false), None);
552    }
553
554    #[test]
555    fn test_database_privilege_row_diff_from_rows() {
556        let row1 = DatabasePrivilegeRow {
557            db: "db".into(),
558            user: "user".into(),
559
560            select_priv: true,
561            insert_priv: false,
562            update_priv: true,
563            delete_priv: false,
564
565            create_priv: false,
566            drop_priv: false,
567            alter_priv: false,
568            index_priv: false,
569            create_tmp_table_priv: false,
570            lock_tables_priv: false,
571            references_priv: false,
572        };
573        let row2 = DatabasePrivilegeRow {
574            db: "db".into(),
575            user: "user".into(),
576
577            select_priv: true,
578            insert_priv: true,
579            update_priv: false,
580            delete_priv: false,
581
582            create_priv: false,
583            drop_priv: false,
584            alter_priv: false,
585            index_priv: false,
586            create_tmp_table_priv: false,
587            lock_tables_priv: false,
588            references_priv: false,
589        };
590
591        let diff = DatabasePrivilegeRowDiff::from_rows(&row1, &row2);
592        assert_eq!(
593            diff,
594            DatabasePrivilegeRowDiff {
595                db: "db".into(),
596                user: "user".into(),
597                select_priv: None,
598                insert_priv: Some(DatabasePrivilegeChange::NoToYes),
599                update_priv: Some(DatabasePrivilegeChange::YesToNo),
600                delete_priv: None,
601                ..Default::default()
602            },
603        );
604    }
605
606    #[test]
607    fn test_database_privilege_row_diff_is_empty() {
608        let empty_diff = DatabasePrivilegeRowDiff {
609            db: "db".into(),
610            user: "user".into(),
611            ..Default::default()
612        };
613
614        assert!(empty_diff.is_empty());
615
616        let non_empty_diff = DatabasePrivilegeRowDiff {
617            db: "db".into(),
618            user: "user".into(),
619            select_priv: Some(DatabasePrivilegeChange::YesToNo),
620            ..Default::default()
621        };
622
623        assert!(!non_empty_diff.is_empty());
624    }
625
626    // TODO: test in isolation:
627    // DatabasePrivilegeRowDiff::mappend
628    // DatabasePrivilegeRowDiff::remove_noops
629    // DatabasePrivilegeRowDiff::apply
630    //
631    // DatabasePrivilegesDiff::mappend
632    //
633    // reduce_privilege_diffs
634
635    #[test]
636    fn test_diff_privileges() {
637        let row_to_be_modified = DatabasePrivilegeRow {
638            db: "db".into(),
639            user: "user".into(),
640            select_priv: true,
641            insert_priv: true,
642            update_priv: true,
643            delete_priv: true,
644            create_priv: true,
645            drop_priv: true,
646            alter_priv: true,
647            index_priv: false,
648            create_tmp_table_priv: true,
649            lock_tables_priv: true,
650            references_priv: false,
651        };
652
653        let mut row_to_be_deleted = row_to_be_modified.to_owned();
654        "user2".clone_into(&mut row_to_be_deleted.user);
655
656        let from = vec![row_to_be_modified.to_owned(), row_to_be_deleted.to_owned()];
657
658        let mut modified_row = row_to_be_modified.to_owned();
659        modified_row.select_priv = false;
660        modified_row.insert_priv = false;
661        modified_row.index_priv = true;
662
663        let mut new_row = row_to_be_modified.to_owned();
664        "user3".clone_into(&mut new_row.user);
665
666        let to = vec![modified_row.to_owned(), new_row.to_owned()];
667
668        let diffs = diff_privileges(&from, &to);
669
670        assert_eq!(
671            diffs,
672            BTreeSet::from_iter(vec![
673                DatabasePrivilegesDiff::Deleted(row_to_be_deleted),
674                DatabasePrivilegesDiff::Modified(DatabasePrivilegeRowDiff {
675                    db: "db".into(),
676                    user: "user".into(),
677                    select_priv: Some(DatabasePrivilegeChange::YesToNo),
678                    insert_priv: Some(DatabasePrivilegeChange::YesToNo),
679                    index_priv: Some(DatabasePrivilegeChange::NoToYes),
680                    ..Default::default()
681                }),
682                DatabasePrivilegesDiff::New(new_row),
683            ])
684        );
685    }
686}