use mysql::prelude::Queryable as _;
use mysql::{Conn, Opts};
use crate::error::{GenError, Result};
use crate::schema::{ColumnDef, ForeignKey, Schema, TableDef, TableKind};
fn err(e: impl std::fmt::Display) -> GenError {
GenError::Introspect(format!("mysql: {e}"))
}
pub(crate) fn introspect(url: &str) -> Result<Schema> {
let opts = Opts::from_url(url).map_err(err)?;
let mut conn = Conn::new(opts).map_err(err)?;
let rels: Vec<(String, String, Option<String>)> = conn
.query_map(
"SELECT t.TABLE_NAME, t.TABLE_TYPE, v.IS_UPDATABLE \
FROM information_schema.TABLES t \
LEFT JOIN information_schema.VIEWS v \
ON v.TABLE_SCHEMA = t.TABLE_SCHEMA AND v.TABLE_NAME = t.TABLE_NAME \
WHERE t.TABLE_SCHEMA = DATABASE() \
AND t.TABLE_TYPE IN ('BASE TABLE', 'VIEW') \
ORDER BY t.TABLE_NAME",
|row: (String, String, Option<String>)| row,
)
.map_err(err)?;
let mut tables = Vec::with_capacity(rels.len());
for (name, table_type, is_updatable) in rels {
let kind = if table_type != "VIEW" {
TableKind::Table
} else if is_updatable.is_some_and(|u| u.eq_ignore_ascii_case("YES")) {
TableKind::UpdatableView
} else {
TableKind::View
};
tables.push(table_def(&mut conn, &name, kind)?);
}
let mut schema = Schema { tables };
super::canonicalise(&mut schema);
Ok(schema)
}
fn table_def(conn: &mut Conn, name: &str, kind: TableKind) -> Result<TableDef> {
let columns = conn
.exec_map(
"SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT \
FROM information_schema.COLUMNS \
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? \
ORDER BY ORDINAL_POSITION",
(name,),
|(col, col_type, nullable, default, extra, comment): (
String,
String,
String,
Option<String>,
String,
String,
)| {
ColumnDef {
name: col,
db_type: col_type,
nullable: nullable.eq_ignore_ascii_case("YES"),
default,
autoincrement: extra.to_ascii_lowercase().contains("auto_increment"),
comment: (!comment.is_empty()).then_some(comment),
}
},
)
.map_err(err)?;
let primary_key: Vec<String> = conn
.exec_map(
"SELECT COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE \
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? \
AND CONSTRAINT_NAME = 'PRIMARY' \
ORDER BY ORDINAL_POSITION",
(name,),
|col: String| col,
)
.map_err(err)?;
let fk_rows: Vec<(String, String, String, String)> = conn
.exec_map(
"SELECT CONSTRAINT_NAME, REFERENCED_TABLE_NAME, COLUMN_NAME, REFERENCED_COLUMN_NAME \
FROM information_schema.KEY_COLUMN_USAGE \
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? \
AND REFERENCED_TABLE_NAME IS NOT NULL \
ORDER BY CONSTRAINT_NAME, ORDINAL_POSITION",
(name,),
|row: (String, String, String, String)| row,
)
.map_err(err)?;
let mut foreign_keys: Vec<ForeignKey> = Vec::new();
let mut last: Option<String> = None;
for (constraint, ref_table, column, ref_column) in fk_rows {
if last.as_deref() != Some(&constraint) {
foreign_keys.push(ForeignKey {
columns: vec![],
ref_table,
ref_columns: vec![],
});
last = Some(constraint);
}
let fk = foreign_keys.last_mut().expect("just pushed");
fk.columns.push(column);
fk.ref_columns.push(ref_column);
}
let uniq_rows: Vec<(String, String)> = conn
.exec_map(
"SELECT k.CONSTRAINT_NAME, k.COLUMN_NAME \
FROM information_schema.KEY_COLUMN_USAGE k \
JOIN information_schema.TABLE_CONSTRAINTS c \
ON c.CONSTRAINT_SCHEMA = k.CONSTRAINT_SCHEMA \
AND c.CONSTRAINT_NAME = k.CONSTRAINT_NAME \
AND c.TABLE_NAME = k.TABLE_NAME \
WHERE k.TABLE_SCHEMA = DATABASE() AND k.TABLE_NAME = ? \
AND c.CONSTRAINT_TYPE = 'UNIQUE' \
ORDER BY k.CONSTRAINT_NAME, k.ORDINAL_POSITION",
(name,),
|row: (String, String)| row,
)
.map_err(err)?;
let mut unique_keys: Vec<Vec<String>> = Vec::new();
let mut last: Option<String> = None;
for (constraint, column) in uniq_rows {
if last.as_deref() != Some(&constraint) {
unique_keys.push(vec![]);
last = Some(constraint);
}
unique_keys.last_mut().expect("just pushed").push(column);
}
Ok(TableDef {
name: name.to_owned(),
kind,
columns,
primary_key,
foreign_keys,
unique_keys,
})
}