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>>,
}
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,
},
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,
},
}
}
fn identifier(name: &str) -> String {
format!("\"{}\"", name.replace('"', "\"\""))
}
fn string_literal(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}