pushkin-compiler 0.2.1

Schema compiler for pushkin: canonical JSON Schema emission and generated bindings
Documentation
//! SQL DDL emission, contract-as-truth direction (spec §5.2 table).

use std::fmt::Write as _;

use crate::schema::{ArrayElement, ContractSchema, PropertyKind};

pub fn emit(schema: &ContractSchema, epoch: u32) -> String {
    let mut columns = Vec::new();
    for property in &schema.properties {
        let sql = sql_column(&property.kind);
        let mut column = format!("    {} {}", identifier(&property.name), sql.base_type);
        if property.required || sql.default_literal.is_some() {
            column.push_str(" NOT NULL");
        }
        if let Some(literal) = &sql.default_literal {
            let _ = write!(column, " DEFAULT {literal}");
        }
        if let Some(values) = &sql.check_values {
            let list = values
                .iter()
                .map(|value| string_literal(value))
                .collect::<Vec<_>>()
                .join(", ");
            let _ = write!(
                column,
                " CHECK ({} IN ({list}))",
                identifier(&property.name)
            );
        }
        columns.push(column);
    }

    format!(
        "-- GENERATED by pushkin compile from contract '{name}' — do not hand-edit.\n\
         -- pushkin-epoch: {epoch}\n\
         CREATE TABLE {table} (\n{columns}\n);\n",
        name = schema.contract_name,
        table = identifier(&schema.contract_name),
        columns = columns.join(",\n"),
    )
}

struct SqlColumn {
    base_type: String,
    default_literal: Option<String>,
    check_values: Option<Vec<String>>,
}

/// Column type, spelled default literal, and CHECK values per kind. SQL
/// spells booleans `TRUE`/`FALSE`.
fn sql_column(kind: &PropertyKind) -> SqlColumn {
    match kind {
        PropertyKind::String {
            max_length,
            enum_values,
            default,
            ..
        } => SqlColumn {
            base_type: match max_length {
                Some(max) => format!("VARCHAR({max})"),
                None => "TEXT".to_owned(),
            },
            default_literal: default.as_deref().map(string_literal),
            check_values: enum_values.clone(),
        },
        PropertyKind::Integer { default } => SqlColumn {
            base_type: "BIGINT".to_owned(),
            default_literal: default.map(|value| value.to_string()),
            check_values: None,
        },
        PropertyKind::Number { default } => SqlColumn {
            base_type: "DOUBLE PRECISION".to_owned(),
            default_literal: default.map(crate::targets::number_literal),
            check_values: None,
        },
        PropertyKind::Boolean { default } => SqlColumn {
            base_type: "BOOLEAN".to_owned(),
            default_literal: default.map(|value| if value { "TRUE" } else { "FALSE" }.to_owned()),
            check_values: None,
        },
        // D2: native Postgres array types — the element spellings this
        // same function already produces, suffixed. JSONB was rejected
        // because it erases the element type, which is the one thing the
        // contract is asserting.
        PropertyKind::Array { element } => SqlColumn {
            base_type: format!(
                "{}[]",
                match element {
                    ArrayElement::String => "TEXT",
                    ArrayElement::Integer => "BIGINT",
                    ArrayElement::Number => "DOUBLE PRECISION",
                    ArrayElement::Boolean => "BOOLEAN",
                }
            ),
            default_literal: None,
            check_values: None,
        },
    }
}

/// Every table/column identifier is double-quoted (S3 human decision, option
/// a): S2-valid names that collide with SQL reserved words (`user`, `order`)
/// stay executable, and quoting is semantics-preserving for lowercase names
/// Postgres would fold anyway. `"` doubling mirrors `string_literal`'s `''`.
fn identifier(name: &str) -> String {
    format!("\"{}\"", name.replace('"', "\"\""))
}

fn string_literal(value: &str) -> String {
    format!("'{}'", value.replace('\'', "''"))
}