use crate::error::{Result, SqliteError};
use crate::record::SqliteValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectKind {
Table,
Index,
View,
Trigger,
}
impl ObjectKind {
fn parse(text: &str) -> Result<ObjectKind> {
Ok(match text {
"table" => ObjectKind::Table,
"index" => ObjectKind::Index,
"view" => ObjectKind::View,
"trigger" => ObjectKind::Trigger,
other => {
return Err(SqliteError::malformed(
1,
format!("the schema holds an object of unknown kind {other:?}"),
))
}
})
}
pub fn as_str(self) -> &'static str {
match self {
ObjectKind::Table => "table",
ObjectKind::Index => "index",
ObjectKind::View => "view",
ObjectKind::Trigger => "trigger",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SchemaObject {
pub kind: ObjectKind,
pub name: String,
pub table_name: String,
pub root_page: Option<u32>,
pub sql: Option<String>,
}
impl SchemaObject {
pub fn is_internal(&self) -> bool {
let name = self.name.as_bytes();
name.len() >= 7 && name[..7].eq_ignore_ascii_case(b"sqlite_")
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Schema {
objects: Vec<SchemaObject>,
}
impl Schema {
pub(crate) fn from_rows(
rows: impl IntoIterator<Item = Vec<SqliteValue>>,
page_count: u32,
) -> Result<Schema> {
let mut objects = Vec::new();
for values in rows {
if values.len() != 5 {
return Err(SqliteError::malformed(
1,
format!(
"a schema row has {} columns, the schema table has five",
values.len()
),
));
}
let kind = match &values[0] {
SqliteValue::Text(t) => ObjectKind::parse(t)?,
other => {
return Err(SqliteError::malformed(
1,
format!("a schema row's kind is {}, not text", other.type_name()),
))
}
};
let name = text_column(&values[1], "name")?;
let table_name = text_column(&values[2], "tbl_name")?;
let root_page = match &values[3] {
SqliteValue::Null => None,
SqliteValue::Integer(0) => None,
SqliteValue::Integer(n) if *n > 0 && *n <= page_count as i64 => Some(*n as u32),
other => {
return Err(SqliteError::malformed(
1,
format!(
"{name} has root page {other:?}, outside the file's 1..={page_count}"
),
))
}
};
if root_page.is_none() && matches!(kind, ObjectKind::Table | ObjectKind::Index) {
return Err(SqliteError::malformed(
1,
format!("the {} {name} has no root page", kind.as_str()),
));
}
let sql = match &values[4] {
SqliteValue::Null => None,
SqliteValue::Text(t) => Some(t.clone()),
other => {
return Err(SqliteError::malformed(
1,
format!("{name} has a {} statement, not text", other.type_name()),
))
}
};
objects.push(SchemaObject {
kind,
name,
table_name,
root_page,
sql,
});
}
Ok(Schema { objects })
}
pub fn objects(&self) -> &[SchemaObject] {
&self.objects
}
pub fn tables(&self) -> impl Iterator<Item = &SchemaObject> {
self.objects.iter().filter(|o| o.kind == ObjectKind::Table)
}
pub fn user_tables(&self) -> impl Iterator<Item = &SchemaObject> {
self.tables().filter(|o| !o.is_internal())
}
pub fn indexes(&self) -> impl Iterator<Item = &SchemaObject> {
self.objects.iter().filter(|o| o.kind == ObjectKind::Index)
}
pub fn object(&self, name: &str) -> Option<&SchemaObject> {
self.objects
.iter()
.find(|o| o.name.eq_ignore_ascii_case(name))
}
pub fn indexes_for(&self, table: &str) -> impl Iterator<Item = &SchemaObject> {
let table = table.to_string();
self.objects.iter().filter(move |o| {
o.kind == ObjectKind::Index && o.table_name.eq_ignore_ascii_case(&table)
})
}
}
fn text_column(value: &SqliteValue, column: &str) -> Result<String> {
match value {
SqliteValue::Text(t) => Ok(t.clone()),
other => Err(SqliteError::malformed(
1,
format!("a schema row's {column} is {}, not text", other.type_name()),
)),
}
}
impl SchemaObject {
pub fn table_def(&self) -> Result<crate::ddl::TableDef> {
if self.kind != ObjectKind::Table {
return Err(SqliteError::unsupported(format!(
"{} is a {}, not a table",
self.name,
self.kind.as_str()
)));
}
match &self.sql {
Some(sql) => crate::ddl::parse_create_table(sql),
None => Err(SqliteError::malformed(
1,
format!("the table {} has no create statement", self.name),
)),
}
}
}