raisfast 0.2.23

The last backend you'll ever need. Rust-powered headless CMS with built-in blog, ecommerce, wallet, payment and 4 plugin engines.
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
}}
"#
    )
}