use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
fn main() {
println!("cargo:rerun-if-changed=migrations");
let out_dir = env::var("OUT_DIR").unwrap();
let dest = Path::new(&out_dir).join("schema_meta.rs");
let schema = parse_schema();
let code = generate_code(&schema);
fs::write(&dest, code).unwrap();
}
fn parse_schema() -> HashMap<String, Vec<String>> {
let mut tables: HashMap<String, Vec<String>> = HashMap::new();
for path in &[
"migrations/sqlite/schema.sqlite.sql",
"migrations/sqlite/tenantable.sqlite.sql",
] {
if let Ok(content) = fs::read_to_string(path) {
parse_sql(&content, &mut tables);
}
}
tables
}
fn parse_sql(content: &str, tables: &mut HashMap<String, Vec<String>>) {
let mut in_create = false;
let mut current_table = String::new();
let mut columns: Vec<String> = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if !in_create {
if let Some(rest) = trimmed.strip_prefix("CREATE TABLE")
&& let Some(name) = extract_table_name(rest)
{
current_table = name;
columns = Vec::new();
in_create = true;
}
continue;
}
if trimmed.starts_with(')')
&& !trimmed.contains("DEFAULT")
&& !trimmed.contains("REFERENCES")
{
if !current_table.is_empty() {
tables.insert(current_table.clone(), columns.clone());
}
in_create = false;
continue;
}
if let Some(col) = extract_column_name(trimmed) {
columns.push(col);
}
}
}
fn extract_table_name(s: &str) -> Option<String> {
let s = s.trim();
let s = s.strip_prefix("IF NOT EXISTS").unwrap_or(s).trim();
let s = s.strip_prefix("IF EXISTS").unwrap_or(s).trim();
let name = if s.starts_with('"') {
s.split('"').nth(1)?.to_string()
} else if s.starts_with('`') {
s.split('`').nth(1)?.to_string()
} else {
s.split(|c: char| c.is_whitespace() || c == '(')
.next()?
.to_string()
};
if name.is_empty() { None } else { Some(name) }
}
fn extract_column_name(line: &str) -> Option<String> {
let line = line.trim_end_matches(',');
let line = line.trim();
if line.is_empty() || line.starts_with("--") {
return None;
}
let upper = line.to_uppercase();
if upper.starts_with("PRIMARY KEY")
|| upper.starts_with("UNIQUE(")
|| upper.starts_with("UNIQUE (")
|| upper.starts_with("CHECK(")
|| upper.starts_with("CHECK (")
|| upper.starts_with("FOREIGN KEY")
|| upper.starts_with("CONSTRAINT")
{
return None;
}
let name = if line.starts_with('"') {
line.split('"').nth(1)?.to_string()
} else if line.starts_with('`') {
line.split('`').nth(1)?.to_string()
} else {
line.split_whitespace().next()?.to_string()
};
if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return None;
}
Some(name)
}
fn generate_code(tables: &HashMap<String, Vec<String>>) -> String {
let mut entries: Vec<String> = Vec::new();
for (table, columns) in tables {
for col in columns {
if col == "id" || col == "rowid" || col == "tenant_id" {
continue;
}
entries.push(format!(" (\"{table}\", \"{col}\"),"));
}
}
let mut table_list: Vec<String> = Vec::new();
for (table, columns) in tables {
let cols: String = columns
.iter()
.map(|c| format!("\"{c}\""))
.collect::<Vec<_>>()
.join(", ");
table_list.push(format!(" (\"{table}\", &[{cols}]),"));
}
let e = entries.join("\n");
let t = table_list.join("\n");
format!(
r#"// Auto-generated by build.rs — DO NOT EDIT
const COLUMNS: &[(&str, &str)] = &[
{e}
];
const fn str_eq(a: &str, b: &str) -> bool {{
let a = a.as_bytes();
let b = b.as_bytes();
if a.len() != b.len() {{
return false;
}}
let mut i = 0;
while i < a.len() {{
if a[i] != b[i] {{
return false;
}}
i += 1;
}}
true
}}
/// Returns `true` if the given table has the given column.
/// `tenant_id`, `id`, and `rowid` are always accepted.
pub const fn column_exists(table: &str, column: &str) -> bool {{
if str_eq(column, "tenant_id") || str_eq(column, "id") || str_eq(column, "rowid") {{
return true;
}}
let mut i = 0;
while i < COLUMNS.len() {{
if str_eq(COLUMNS[i].0, table) && str_eq(COLUMNS[i].1, column) {{
return true;
}}
i += 1;
}}
false
}}
/// All tables and their columns.
pub const ALL_TABLES: &[(&str, &[&str])] = &[
{t}
];
/// Returns `true` if the given table exists in the schema.
pub const fn table_exists(table: &str) -> bool {{
let mut i = 0;
while i < ALL_TABLES.len() {{
if str_eq(ALL_TABLES[i].0, table) {{
return true;
}}
i += 1;
}}
false
}}
"#
)
}