sea-schema-sync 0.17.0-rc.16

🌿 SQL schema definition and discovery
Documentation
use super::DefaultType;
use crate::{rusqlite_types::RusqliteRow, sqlite::def::ColumnVisibility};
use sea_query::{
    Alias, ColumnType, Index, IndexCreateStatement,
    foreign_key::ForeignKeyAction as SeaQueryForeignKeyAction,
};
use std::num::ParseIntError;

/// An SQLite column definition
#[derive(Debug, PartialEq, Clone)]
pub struct ColumnInfo {
    /// Column id
    pub cid: i64,
    /// Column name.
    pub name: String,
    /// Declared type
    pub r#type: ColumnType,
    /// Whether a NOT NULL constraint is present.
    pub not_null: bool,
    pub default_value: DefaultType,
    /// Column is part of the PRIMARY KEY.
    pub primary_key: bool,
    /// Hidden status
    pub hidden: ColumnVisibility,
}

#[cfg(feature = "rusqlite")]
impl ColumnInfo {
    /// Map an [SqliteRow] into a column definition type [ColumnInfo]
    pub fn to_column_def(row: RusqliteRow) -> Result<ColumnInfo, ParseIntError> {
        use crate::rusqlite_types::Row;
        let row = row.sqlite();
        let col_not_null: i8 = row.get(3);
        let is_pk: i8 = row.get(5);
        let default_value: Option<String> = row.get(4);
        let default_value = default_value.unwrap_or_default();
        let hidden: i8 = row.get(6);
        Ok(ColumnInfo {
            cid: row.get(0),
            name: row.get(1),
            r#type: super::parse_type(row.get(2))?,
            not_null: col_not_null != 0 || is_pk == 1,
            default_value: if default_value == "NULL" {
                DefaultType::Null
            } else if default_value.is_empty() {
                DefaultType::Unspecified
            } else {
                let value = default_value.to_owned().replace('\'', "");

                if let Ok(is_int) = value.parse::<i64>() {
                    DefaultType::Integer(is_int)
                } else if let Ok(is_float) = value.parse::<f32>() {
                    DefaultType::Float(is_float)
                } else if value == "CURRENT_TIMESTAMP" {
                    DefaultType::CurrentTimestamp
                } else {
                    DefaultType::String(value)
                }
            },
            primary_key: is_pk != 0,
            hidden: ColumnVisibility::from_hidden(hidden),
        })
    }

    #[inline]
    pub fn is_hidden(&self) -> bool {
        self.hidden == ColumnVisibility::HiddenVirtual
    }
}

#[cfg(all(not(feature = "rusqlite"), not(feature = "sqlx-sqlite")))]
impl ColumnInfo {
    pub fn to_column_def(_: RusqliteRow) -> Result<ColumnInfo, ParseIntError> {
        unimplemented!()
    }

    #[inline]
    pub fn is_hidden(&self) -> bool {
        unimplemented!()
    }
}

/// Maps the index and all columns in the index which is the result of queries
/// `PRAGMA index_list(table_name)` and
/// `SELECT * FROM sqlite_master where name = 'index_name'`
#[derive(Debug, Default, Clone, PartialEq)]
pub struct IndexInfo {
    /// Is it a SQLindex
    pub r#type: String,
    pub index_name: String,
    pub table_name: String,
    pub unique: bool,
    pub origin: String,
    pub partial: i64,
    pub columns: Vec<String>,
}

impl IndexInfo {
    /// Write all the discovered index into a [IndexCreateStatement]
    pub fn write(&self) -> IndexCreateStatement {
        let mut new_index = Index::create();
        // The name is only correct if the index was created by a CREATE INDEX statement. Otherwise it's autogenerated, so we drop it.
        if self.origin.as_str() == "c" {
            new_index.name(&self.index_name);
        }
        new_index.table(Alias::new(&self.table_name));

        if self.unique {
            new_index.unique();
        }

        self.columns.iter().for_each(|column| {
            new_index.col(Alias::new(column));
        });

        new_index
    }
}

/// Maps the index all columns as a result of using query
/// `PRAGMA index_list(table_name)`
#[allow(dead_code)]
#[derive(Debug, Default, Clone)]
pub(crate) struct PartialIndexInfo {
    pub(crate) seq: i64,
    pub(crate) name: String,
    pub(crate) unique: bool,
    pub(crate) origin: String,
    pub(crate) partial: i64,
}

#[cfg(feature = "rusqlite")]
impl From<RusqliteRow> for PartialIndexInfo {
    fn from(row: RusqliteRow) -> Self {
        use crate::rusqlite_types::Row;
        let row = row.sqlite();
        let is_unique: i8 = row.get(2);
        Self {
            seq: row.get(0),
            name: row.get(1),
            unique: is_unique != 0,
            origin: row.get(3),
            partial: row.get(4),
        }
    }
}

#[cfg(all(not(feature = "rusqlite"), not(feature = "sqlx-sqlite")))]
impl From<RusqliteRow> for PartialIndexInfo {
    fn from(_: RusqliteRow) -> Self {
        Self::default()
    }
}

/// Maps all the columns in an index as a result of using query
/// `SELECT * FROM sqlite_master where name = 'index_name'`
#[allow(dead_code)]
#[derive(Debug, Default, Clone)]
pub(crate) struct IndexedColumns {
    pub(crate) r#type: String,
    pub(crate) name: String,
    pub(crate) table: String,
    pub(crate) root_page: i64,
    pub(crate) indexed_columns: Vec<String>,
}

#[cfg(feature = "rusqlite")]
impl From<(RusqliteRow, Vec<RusqliteRow>)> for IndexedColumns {
    fn from((row, rows): (RusqliteRow, Vec<RusqliteRow>)) -> Self {
        use crate::rusqlite_types::Row;

        let row = row.sqlite();
        let columns_to_index = rows
            .into_iter()
            .map(|r| r.sqlite().get(2))
            .collect::<Vec<String>>();

        Self {
            r#type: row.get(0),
            name: row.get(1),
            table: row.get(2),
            root_page: row.get(3),
            indexed_columns: columns_to_index,
        }
    }
}

#[cfg(all(not(feature = "rusqlite"), not(feature = "sqlx-sqlite")))]
impl From<(RusqliteRow, Vec<RusqliteRow>)> for IndexedColumns {
    fn from(_: (RusqliteRow, Vec<RusqliteRow>)) -> Self {
        Self::default()
    }
}

/// Confirms if a table's primary key is set to autoincrement as a result of using query
/// `SELECT COUNT(*) from sqlite_sequence where name = 'table_name';
#[allow(dead_code)]
#[derive(Debug, Default, Clone)]
pub(crate) struct PrimaryKeyAutoincrement(pub(crate) u8);

#[cfg(feature = "rusqlite")]
impl From<RusqliteRow> for PrimaryKeyAutoincrement {
    fn from(row: RusqliteRow) -> Self {
        use crate::rusqlite_types::Row;
        Self(row.sqlite().get(0))
    }
}

#[cfg(all(not(feature = "rusqlite"), not(feature = "sqlx-sqlite")))]
impl From<RusqliteRow> for PrimaryKeyAutoincrement {
    fn from(_: RusqliteRow) -> Self {
        Self::default()
    }
}

/// Indexes the foreign keys
#[derive(Debug, Default, Clone, PartialEq)]
#[non_exhaustive]
pub struct ForeignKeysInfo {
    pub id: i64,
    pub seq: i64,
    pub table: String,
    pub from: Vec<String>,
    pub to: Vec<String>,
    pub on_update: ForeignKeyAction,
    pub on_delete: ForeignKeyAction,
    pub r#match: MatchAction,
}

#[cfg(feature = "rusqlite")]
impl From<RusqliteRow> for ForeignKeysInfo {
    fn from(row: RusqliteRow) -> Self {
        use crate::rusqlite_types::Row;
        let row = row.sqlite();
        Self {
            id: row.get(0),
            seq: row.get(1),
            table: row.get(2),
            from: vec![row.get(3)],
            to: vec![row.get(4)],
            on_update: {
                let op: String = row.get(5);
                op.as_str().into()
            },
            on_delete: {
                let op: String = row.get(6);
                op.as_str().into()
            },
            r#match: {
                let op: String = row.get(7);
                op.as_str().into()
            },
        }
    }
}

#[cfg(all(not(feature = "rusqlite"), not(feature = "sqlx-sqlite")))]
impl From<RusqliteRow> for ForeignKeysInfo {
    fn from(_: RusqliteRow) -> Self {
        Self::default()
    }
}

/// Indexes the actions performed on the foreign keys of a table
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub enum ForeignKeyAction {
    #[default]
    NoAction,
    Restrict,
    SetNull,
    SetDefault,
    Cascade,
}

impl From<&str> for ForeignKeyAction {
    fn from(action: &str) -> Self {
        match action {
            "NO ACTION" => Self::NoAction,
            "RESTRICT" => Self::Restrict,
            "SET NULL" => Self::SetNull,
            "SET DEFAULT" => Self::SetDefault,
            "CASCADE" => Self::Cascade,
            _ => Self::NoAction,
        }
    }
}

impl ForeignKeyAction {
    pub(crate) fn to_seaquery_foreign_key_action(&self) -> SeaQueryForeignKeyAction {
        match self {
            Self::NoAction => SeaQueryForeignKeyAction::NoAction,
            Self::Restrict => SeaQueryForeignKeyAction::Restrict,
            Self::SetNull => SeaQueryForeignKeyAction::SetNull,
            Self::SetDefault => SeaQueryForeignKeyAction::SetDefault,
            Self::Cascade => SeaQueryForeignKeyAction::Cascade,
        }
    }
}

/// Maps to the SQLite `MATCH` actions
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub enum MatchAction {
    Simple,
    Partial,
    Full,
    #[default]
    None,
}

impl From<&str> for MatchAction {
    fn from(action: &str) -> Self {
        match action {
            "MATCH SIMPLE" => Self::Simple,
            "MATCH PARTIAL" => Self::Partial,
            "MATCH FULL" => Self::Full,
            "MATCH NONE" => Self::None,
            _ => Self::None,
        }
    }
}