use std::collections::HashMap;
use crate::{
db::Transaction,
query::{Insertable, Queryable},
schema::{
collect::{EntityStateContainer, PartType},
entity::{Entity, EntityPart, EntityPartList, EntityPartVisitor},
meta, DatabaseItem, DatabaseItemVisitor,
},
DBResult,
};
#[cfg(feature = "track_typesig")]
use crate::schema::typesig;
use sha2::Digest;
#[derive(Debug)]
struct ColumnInfo {
name: &'static str,
ty: String,
fkey: Option<String>,
unique: bool,
}
#[derive(Debug)]
struct TableInfo {
table_name: String,
columns: Vec<ColumnInfo>,
constraints: Vec<String>,
}
impl TableInfo {
fn new(name: String) -> Self {
TableInfo {
table_name: name,
columns: vec![],
constraints: vec![],
}
}
fn build_creation_query(&self) -> String {
let columns = self.columns.iter().map(|col| {
format!(
", `{}` {}{}",
col.name,
col.ty,
if col.unique { " unique" } else { "" }
)
});
let fkeys = self.columns.iter().filter_map(|col| {
Some(format!(
", foreign key(`{}`) references {} on delete cascade",
col.name,
col.fkey.as_ref()?
))
});
format!(
"CREATE TABLE \"{}\" (`id` integer primary key{}{}{})",
self.table_name,
columns.collect::<String>(),
fkeys.collect::<String>(),
self.constraints
.iter()
.fold(String::new(), |a, b| format!("{}, {}", a, b))
)
}
}
#[derive(Debug, Hash)]
struct IndexInfo {
table_name: String,
columns: Vec<String>,
unique: bool,
}
impl IndexInfo {
fn build_full_name(&self) -> String {
format!(
"index_{}_{}",
self.table_name,
self.columns
.iter()
.cloned()
.reduce(|a, b| format!("{a}_{b}"))
.unwrap()
)
}
fn build_creation_query(&self) -> String {
format!(
"CREATE {unique}INDEX `{index_name}` on `{table_name}`({cols})",
index_name = self.build_full_name(),
table_name = self.table_name,
unique = if self.unique { "UNIQUE " } else { "" },
cols = self.columns.join(",")
)
}
}
#[derive(Clone, Debug)]
pub(crate) struct GeneratedSchema {
signature: String,
table_queries: HashMap<String, String>,
index_queries: HashMap<String, String>,
related_indices: HashMap<String, Vec<String>>,
#[cfg(feature = "track_typesig")]
type_signatures: typesig::SignatureMap,
}
impl GeneratedSchema {
pub(crate) const SCHEMA_SIGNATURE_KEY: &'static str = "schema_signature";
#[allow(unused)]
pub(crate) fn signature(&self) -> &str {
self.signature.as_str()
}
#[allow(unused)]
pub(crate) fn table_queries(&self) -> &HashMap<String, String> {
&self.table_queries
}
#[allow(unused)]
pub(crate) fn index_queries(&self) -> &HashMap<String, String> {
&self.index_queries
}
#[allow(unused)]
pub(crate) fn related_indices(&self) -> &HashMap<String, Vec<String>> {
&self.related_indices
}
pub fn check(&self, txn: &mut Transaction) -> Option<bool> {
let metadb = meta::MetadataDB::build(super::BuildSeal::new());
metadb
.metastore
.keyed(Self::SCHEMA_SIGNATURE_KEY)
.get(txn)
.ok()
.flatten()
.map(|kv| kv.value == self.signature)
}
pub fn create(&self, txn: &mut Transaction) -> DBResult<()> {
for query in self.table_queries.iter() {
log::trace!("Running creation query {}", query.1);
txn.lease().execute_raw_sql(query.1)?;
}
for query in self.index_queries.iter() {
log::trace!("Running creation query {}", query.1);
txn.lease().execute_raw_sql(query.1)?;
}
for query in generate_from_schema::<meta::MetadataDB>()
.table_queries
.iter()
{
txn.lease().execute_raw_sql(query.1)?;
}
self.update_metadata(txn)?;
Ok(())
}
pub fn update_metadata(&self, txn: &mut Transaction) -> DBResult<()> {
let metadb = meta::MetadataDB::build(super::BuildSeal::new());
metadb
.metastore
.keyed(Self::SCHEMA_SIGNATURE_KEY)
.delete(txn)?;
metadb.metastore.insert(
txn,
meta::MicrormMeta {
key: Self::SCHEMA_SIGNATURE_KEY.into(),
value: self.signature.clone(),
},
)?;
#[cfg(feature = "track_typesig")]
metadb
.metastore
.keyed(typesig::TYPE_SIGNATURES_KEY)
.delete(txn)?;
#[cfg(feature = "track_typesig")]
metadb.metastore.insert(
txn,
meta::MicrormMeta {
key: typesig::TYPE_SIGNATURES_KEY.into(),
value: serde_json::to_string(&self.type_signatures).unwrap(),
},
)?;
Ok(())
}
}
fn process_state(tables: &mut HashMap<String, TableInfo>, state: &super::collect::EntityState) {
let table_name = state.name.to_string();
if tables.contains_key(&table_name) {
return;
}
let mut table = TableInfo::new(table_name.clone());
for part in state.parts.iter() {
match &part.ty {
PartType::Datum(dtype) => table.columns.push(ColumnInfo {
name: part.name,
ty: dtype.to_string(),
fkey: None,
unique: part.unique,
}),
PartType::IDReference(entity_name) => table.columns.push(ColumnInfo {
name: part.name,
ty: "int".into(),
fkey: Some(format!("`{}`(`id`)", entity_name)),
unique: part.unique,
}),
PartType::RelationDomain {
table_name: relation_table_name,
range_name,
injective,
} => {
let mut relation_table = TableInfo::new(relation_table_name.clone());
relation_table.columns.push(ColumnInfo {
name: "domain",
ty: "int".into(),
fkey: Some(format!("`{}`(`id`)", table_name)),
unique: false,
});
relation_table.columns.push(ColumnInfo {
name: "range",
ty: "int".into(),
fkey: Some(format!("`{}`(`id`)", range_name)),
unique: *injective,
});
relation_table
.constraints
.push("unique(`range`, `domain`)".to_string());
tables.insert(relation_table_name.clone(), relation_table);
},
PartType::RelationRange {
table_name: relation_table_name,
domain_name,
injective,
} => {
let mut relation_table = TableInfo::new(relation_table_name.clone());
relation_table.columns.push(ColumnInfo {
name: "domain",
ty: "int".into(),
fkey: Some(format!("`{}`(`id`)", domain_name)),
unique: false,
});
relation_table.columns.push(ColumnInfo {
name: "range",
ty: "int".into(),
fkey: Some(format!("`{}`(`id`)", table_name)),
unique: *injective,
});
relation_table
.constraints
.push("unique(`range`, `domain`)".to_string());
tables.insert(relation_table_name.clone(), relation_table);
},
}
}
let key = state.parts.iter().filter(|p| p.key).collect::<Vec<_>>();
if !key.is_empty() {
table.constraints.push(format!(
"/* keying index */ unique({})",
key.into_iter()
.map(|s| format!("`{}`", s.name))
.reduce(|a, b| format!("{},{}", a, b))
.unwrap()
));
}
tables.insert(table_name, table);
}
pub(crate) fn generate_single_entity_table<E: Entity>() -> String {
let mut esc = EntityStateContainer::default();
esc.visit_idmap::<E>();
let mut tables = std::collections::HashMap::new();
esc.iter_states()
.for_each(|state| process_state(&mut tables, state));
tables[E::entity_name()].build_creation_query()
}
pub(crate) fn generate_from_schema<S: super::Schema>() -> GeneratedSchema {
struct IV(EntityStateContainer, Vec<IndexInfo>);
impl DatabaseItemVisitor for IV {
fn visit_idmap<E: Entity>(&mut self)
where
Self: Sized,
{
self.0.visit_idmap::<E>();
}
fn visit_index<const UNIQUE: bool, T: Entity, PL: EntityPartList<Entity = T>>(&mut self)
where
Self: Sized,
{
struct PV<E: Entity>(Vec<String>, std::marker::PhantomData<E>);
impl<E: Entity> EntityPartVisitor for PV<E> {
type Entity = E;
fn visit<EP: EntityPart<Entity = Self::Entity>>(&mut self) {
self.0.push(EP::part_name().to_string());
}
}
let mut pv = PV::<T>(Default::default(), Default::default());
PL::accept_part_visitor(&mut pv);
self.1.push(IndexInfo {
table_name: T::entity_name().to_string(),
columns: pv.0,
unique: UNIQUE,
});
}
}
let mut iv = IV(EntityStateContainer::default(), Default::default());
S::accept_item_visitor(&mut iv);
let mut tables = std::collections::HashMap::new();
iv.0.iter_states()
.for_each(|state| process_state(&mut tables, state));
let mut hasher = sha2::Sha256::new();
if let Some(name) = S::NAME {
hasher.update(name);
}
let mut table_queries = vec![];
let mut index_queries = vec![];
let mut related_indices = HashMap::<String, Vec<String>>::new();
let mut sorted_tables: Vec<_> = tables.into_iter().collect();
sorted_tables.sort_by(|a, b| a.0.cmp(&b.0));
for (table_name, table) in sorted_tables {
let create_sql = table.build_creation_query();
hasher.update(&table_name);
hasher.update(&create_sql);
table_queries.push((table_name, create_sql));
}
for iinfo in iv.1.into_iter() {
hasher.update(&iinfo.table_name);
let query = iinfo.build_creation_query();
hasher.update(&query);
let index_name = iinfo.build_full_name();
index_queries.push((index_name.clone(), query));
related_indices
.entry(iinfo.table_name)
.or_default()
.push(index_name);
}
let digest = hasher.finalize();
#[cfg(feature = "track_typesig")]
let type_signatures = typesig::collect_type_info::<S>();
GeneratedSchema {
signature: digest.into_iter().fold(String::new(), |mut a, v| {
a += &format!("{:02x}", v);
a
}),
table_queries: HashMap::from_iter(table_queries),
index_queries: HashMap::from_iter(index_queries),
related_indices,
#[cfg(feature = "track_typesig")]
type_signatures,
}
}