use {
good_ormning_core::sqlite::{
schema::{
constraint::{
Constraint,
ConstraintType,
ForeignKeyDef,
PrimaryKeyDef,
},
field::{
Field,
FieldType,
},
index::Index,
table::Table,
},
types::{
SimpleSimpleType,
SimpleType,
Type,
},
Version,
},
loga::ResultContext,
rusqlite::Connection,
std::collections::{
BTreeMap,
HashMap,
},
};
fn map_type(type_str: &str) -> Result<SimpleSimpleType, loga::Error> {
let t = type_str.to_lowercase();
if t.contains("real") || t.contains("floa") || t.contains("doub") {
return Ok(SimpleSimpleType::F64);
} else if t.contains("bool") {
return Ok(SimpleSimpleType::Bool);
} else if t.contains("blob") || t.is_empty() {
return Ok(SimpleSimpleType::Bytes);
} else if t.contains("char") || t.contains("clob") || t.contains("text") {
return Ok(SimpleSimpleType::String);
} else if t.contains("int") {
return Ok(SimpleSimpleType::I64);
} else {
return Err(loga::err(format!("Unknown SQLite type: {:?}", type_str)));
}
}
fn make_field_type(sst: SimpleSimpleType, opt: bool) -> FieldType {
return FieldType {
type_: Type {
type_: SimpleType {
type_: sst,
custom: None,
},
opt: opt,
arr: false,
},
migration_default: None,
};
}
pub fn read_schema(conn: &Connection) -> Result<Version, loga::Error> {
let table_names: Vec<String> = {
let mut stmt = conn.prepare(
r#"SELECT
name
FROM
sqlite_master
WHERE
type = 'table'
AND name NOT LIKE '__good_%'
AND name NOT LIKE 'sqlite_%'
ORDER BY
name
"#,
).context("Preparing table list query")?;
let mut out = vec![];
let rows = stmt.query_map([], |r| r.get::<_, String>(0)).context("Querying table list")?;
for row in rows {
out.push(row.map_err(loga::err).context("Reading row from table list")?);
}
out
};
struct RawFk {
fk_id: i64,
seq: i64,
remote_table: String,
local_col: String,
remote_col: String,
}
struct TableRaw {
table: Table,
sql_to_field_id: HashMap<String, String>,
raw_fks: Vec<RawFk>,
}
let mut table_raws: Vec<(String, TableRaw)> = vec![];
for table_name in &table_names {
struct ColInfo {
name: String,
type_str: String,
notnull: bool,
pk_pos: i64,
}
let cols: Vec<ColInfo> = {
let mut stmt =
conn.prepare(&format!("PRAGMA table_info(\"{}\")", table_name)).context("Preparing table_info")?;
let mut out = vec![];
let rows = stmt.query_map([], |r| {
Ok(ColInfo {
name: r.get(1)?,
type_str: r.get::<_, String>(2).unwrap_or_default(),
notnull: r.get::<_, i64>(3).unwrap_or(0) != 0,
pk_pos: r.get::<_, i64>(5).unwrap_or(0),
})
}).context("Querying table_info")?;
for row in rows {
out.push(row.map_err(loga::err).context("Reading row from table_info")?);
}
out
};
let pk_cols: Vec<&ColInfo> = cols.iter().filter(|c| c.pk_pos > 0).collect();
let rowid_alias: Option<&ColInfo> = if pk_cols.len() == 1 && pk_cols[0].type_str.to_lowercase() == "integer" {
Some(pk_cols[0])
} else {
None
};
let mut fields: BTreeMap<String, Field> = BTreeMap::new();
let mut sql_to_field_id: HashMap<String, String> = HashMap::new();
let mut constraints: BTreeMap<String, Constraint> = BTreeMap::new();
for col in &cols {
let is_rowid = rowid_alias.map(|r| r.name == col.name).unwrap_or(false);
if is_rowid {
fields.insert("rowid".to_string(), Field {
id: col.name.clone(),
renamed_from: None,
type_: make_field_type(SimpleSimpleType::Auto, false),
});
sql_to_field_id.insert(col.name.clone(), "rowid".to_string());
constraints.insert(format!("{}_pkey", table_name), Constraint {
id: format!("{}_pkey", table_name),
renamed_from: None,
type_: ConstraintType::PrimaryKey(PrimaryKeyDef { fields: vec!["rowid".to_string()] }),
});
} else {
let field_id = col.name.clone();
let sst =
map_type(
&col.type_str,
).context(format!("Mapping type for column {:?} of table {:?}", col.name, table_name))?;
fields.insert(field_id.clone(), Field {
id: col.name.clone(),
renamed_from: None,
type_: make_field_type(sst, !col.notnull),
});
sql_to_field_id.insert(col.name.clone(), field_id);
}
}
if rowid_alias.is_none() && pk_cols.len() > 1 {
let mut sorted_pk: Vec<(i64, &ColInfo)> = pk_cols.iter().map(|c| (c.pk_pos, *c)).collect();
sorted_pk.sort_by_key(|(pos, _)| *pos);
constraints.insert(format!("{}_pkey", table_name), Constraint {
id: format!("{}_pkey", table_name),
renamed_from: None,
type_: ConstraintType::PrimaryKey(
PrimaryKeyDef {
fields: sorted_pk.into_iter().map(|(_, c)| sql_to_field_id[&c.name].clone()).collect(),
},
),
});
}
let raw_fks: Vec<RawFk> = {
let mut stmt =
conn
.prepare(&format!("PRAGMA foreign_key_list(\"{}\")", table_name))
.context("Preparing foreign_key_list")?;
let mut out = vec![];
let rows = stmt.query_map([], |r| {
Ok(RawFk {
fk_id: r.get(0)?,
seq: r.get(1)?,
remote_table: r.get(2)?,
local_col: r.get(3)?,
remote_col: r.get(4)?,
})
}).context("Querying foreign_key_list")?;
for row in rows {
out.push(row.map_err(loga::err).context("Reading row from foreign_key_list")?);
}
out
};
let mut indices: BTreeMap<String, Index> = BTreeMap::new();
{
struct IdxEntry {
name: String,
unique: bool,
}
let idx_entries: Vec<IdxEntry> = {
let mut stmt =
conn
.prepare(&format!("PRAGMA index_list(\"{}\")", table_name))
.context("Preparing index_list")?;
let mut out = vec![];
let rows = stmt.query_map([], |r| {
Ok((r.get::<_, String>(1)?, r.get::<_, i64>(2)? != 0, r.get::<_, String>(3)?))
}).context("Querying index_list")?;
for row in rows {
let (name, unique, origin) = row.map_err(loga::err).context("Reading row from index_list")?;
if origin != "pk" {
out.push(IdxEntry {
name: name,
unique: unique,
});
}
}
out
};
for entry in idx_entries {
let col_names: Vec<String> = {
let mut stmt =
conn
.prepare(&format!("PRAGMA index_info(\"{}\")", entry.name))
.context("Preparing index_info")?;
let mut out = vec![];
let rows = stmt.query_map([], |r| r.get::<_, String>(2)).context("Querying index_info")?;
for row in rows {
out.push(row.map_err(loga::err).context("Reading row from index_info")?);
}
out
};
let field_ids = col_names.iter().map(|col| {
sql_to_field_id
.get(col)
.cloned()
.ok_or_else(
|| loga::err(
format!(
"Index {:?} references unknown column {:?} in table {:?}",
entry.name,
col,
table_name
),
),
)
}).collect::<Result<Vec<String>, _>>()?;
indices.insert(entry.name.clone(), Index {
id: entry.name,
renamed_from: None,
fields: field_ids,
unique: entry.unique,
});
}
}
table_raws.push((table_name.clone(), TableRaw {
table: Table {
id: table_name.clone(),
renamed_from: None,
fields: fields,
indices: indices,
constraints: constraints,
},
sql_to_field_id: sql_to_field_id,
raw_fks: raw_fks,
}));
}
let sql_to_field_id_by_table: HashMap<String, HashMap<String, String>> =
table_raws.iter().map(|(name, raw)| (name.clone(), raw.sql_to_field_id.clone())).collect();
let mut tables: BTreeMap<String, Table> = BTreeMap::new();
for (table_name, mut raw) in table_raws {
let mut fk_map: HashMap<i64, (String, Vec<(i64, String, String)>)> = HashMap::new();
for fk in raw.raw_fks {
let entry = fk_map.entry(fk.fk_id).or_insert_with(|| (fk.remote_table.clone(), vec![]));
entry.1.push((fk.seq, fk.local_col, fk.remote_col));
}
let mut fk_ids: Vec<i64> = fk_map.keys().copied().collect();
fk_ids.sort();
for fk_id in fk_ids {
let (remote_table, mut pairs) = fk_map.remove(&fk_id).unwrap();
pairs.sort_by_key(|(seq, _, _)| *seq);
let remote_map = sql_to_field_id_by_table.get(&remote_table);
let field_pairs =
pairs.into_iter().map(|(_, local_col, remote_col)| -> Result<(String, String), loga::Error> {
let local_fid =
raw
.sql_to_field_id
.get(&local_col)
.cloned()
.ok_or_else(
|| loga::err(
format!(
"FK {:?} in table {:?} references unknown local column {:?}",
fk_id,
table_name,
local_col
),
),
)?;
let remote_fid =
remote_map
.and_then(|m| m.get(&remote_col))
.cloned()
.ok_or_else(
|| loga::err(
format!(
"FK {:?} in table {:?} references unknown column {:?} in remote table {:?}",
fk_id,
table_name,
remote_col,
remote_table
),
),
)?;
return Ok((local_fid, remote_fid));
}).collect::<Result<Vec<(String, String)>, _>>()?;
let constraint_name = format!("{}_{}_fkey", table_name, fk_id);
raw.table.constraints.insert(constraint_name.clone(), Constraint {
id: constraint_name,
renamed_from: None,
type_: ConstraintType::ForeignKey(ForeignKeyDef {
remote_table: remote_table,
fields: field_pairs,
}),
});
}
tables.insert(table_name, raw.table);
}
return Ok(Version {
tables: tables,
custom_types: BTreeMap::new(),
});
}