use crate::formats::DdlDialect;
use oxrdf::{Term as OxTerm, Triple as OxTriple};
use std::collections::{BTreeMap, BTreeSet, HashMap};
pub(crate) const RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
#[derive(Debug, Clone)]
pub(crate) struct PgNodeTable {
#[allow(dead_code)]
pub type_iri: String,
pub name: String,
pub properties: BTreeSet<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct PgRelTable {
pub pred_iri: String,
pub name: String,
pub from_table: String,
pub to_table: String,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct PgSchemaModel {
pub node_tables: BTreeMap<String, PgNodeTable>,
pub rel_tables: BTreeMap<String, PgRelTable>,
}
impl PgSchemaModel {
pub fn node_table_count(&self) -> usize {
self.node_tables.len()
}
pub fn rel_table_count(&self) -> usize {
self.rel_tables.len()
}
}
pub(crate) fn derive_pg_schema(triples: &[OxTriple]) -> PgSchemaModel {
let mut model = PgSchemaModel::default();
let mut subject_types: HashMap<String, BTreeSet<String>> = HashMap::new();
for triple in triples {
if triple.predicate.as_str() == RDF_TYPE
&& let OxTerm::NamedNode(_) | OxTerm::BlankNode(_) = &triple.object
{
let class = object_str(&triple.object);
subject_types
.entry(subject_str(&triple.subject))
.or_default()
.insert(class.clone());
model.node_tables.entry(class.clone()).or_insert_with(|| PgNodeTable {
name: sanitize_name(&class),
type_iri: class,
properties: BTreeSet::new(),
});
}
}
for triple in triples {
let pred = triple.predicate.as_str();
if pred == RDF_TYPE {
continue;
}
let subj = subject_str(&triple.subject);
if let Some(types) = subject_types.get(&subj) {
let prop = sanitize_prop_name(pred);
for class in types {
if let Some(table) = model.node_tables.get_mut(class) {
table.properties.insert(prop.clone());
}
}
}
if let OxTerm::NamedNode(_) = &triple.object {
let obj = object_str(&triple.object);
let from = subject_types.get(&subj).and_then(|ts| ts.iter().next());
let to = subject_types.get(&obj).and_then(|ts| ts.iter().next());
if let (Some(from_class), Some(to_class)) = (from, to) {
model.rel_tables.entry(pred.to_string()).or_insert_with(|| PgRelTable {
pred_iri: pred.to_string(),
name: sanitize_name(pred),
from_table: sanitize_name(from_class),
to_table: sanitize_name(to_class),
});
}
}
}
model
}
pub(crate) fn emit_ddl(model: &PgSchemaModel, dialect: DdlDialect, graph_type_name: &str) -> String {
match dialect {
DdlDialect::Cypher => emit_cypher_ddl(model),
DdlDialect::Gql => emit_gql_ddl(model, graph_type_name),
}
}
fn emit_cypher_ddl(model: &PgSchemaModel) -> String {
let mut stmts: Vec<String> = Vec::new();
for table in model.node_tables.values() {
stmts.push(format!(
"CREATE NODE TABLE {} ({}, PRIMARY KEY(id));",
table.name,
node_columns(table).join(", ")
));
}
for rel in model.rel_tables.values() {
stmts.push(format!(
"CREATE REL TABLE {} (FROM {} TO {});",
rel.name, rel.from_table, rel.to_table
));
}
stmts.join("\n")
}
fn emit_gql_ddl(model: &PgSchemaModel, graph_type_name: &str) -> String {
let mut decls: Vec<String> = Vec::new();
for table in model.node_tables.values() {
decls.push(format!(
" NODE TYPE {} ({})",
table.name,
node_columns(table).join(", ")
));
}
for rel in model.rel_tables.values() {
decls.push(format!(
" EDGE TYPE {} (FROM {} TO {})",
rel.name, rel.from_table, rel.to_table
));
}
format!("CREATE GRAPH TYPE {} (\n{}\n);", graph_type_name, decls.join(",\n"))
}
pub(crate) fn node_columns(table: &PgNodeTable) -> Vec<String> {
let mut cols = vec!["id STRING".to_string()];
cols.extend(table.properties.iter().map(|p| format!("{p} STRING")));
cols
}
pub(crate) fn sanitize_name(iri: &str) -> String {
let name = local_name(iri);
let sanitized: String = name
.chars()
.map(|c| if c.is_alphanumeric() || c == '_' { c } else { '_' })
.collect();
if sanitized.is_empty() || sanitized.starts_with(|c: char| c.is_ascii_digit()) {
format!("T_{sanitized}")
} else {
sanitized
}
}
pub(crate) fn sanitize_prop_name(iri: &str) -> String {
let name = local_name(iri);
let sanitized: String = name
.chars()
.map(|c| if c.is_alphanumeric() || c == '_' { c } else { '_' })
.collect();
if sanitized.is_empty() || sanitized.starts_with(|c: char| c.is_ascii_digit()) {
format!("p_{sanitized}")
} else {
sanitized
}
}
fn local_name(iri: &str) -> &str {
iri.rsplit(['#', '/']).next().unwrap_or(iri)
}
pub(crate) fn esc(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('\'', "\\'")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub(crate) fn term_value(t: &OxTerm) -> String {
match t {
OxTerm::NamedNode(n) => n.as_str().to_string(),
OxTerm::Literal(lit) => lit.value().to_string(),
OxTerm::BlankNode(bn) => format!("_:{}", bn.as_str()),
OxTerm::Triple(triple) => format!("<<{triple}>>"),
}
}
pub(crate) fn subject_str(s: &oxrdf::NamedOrBlankNode) -> String {
match s {
oxrdf::NamedOrBlankNode::NamedNode(n) => n.as_str().to_string(),
oxrdf::NamedOrBlankNode::BlankNode(bn) => format!("_:{}", bn.as_str()),
}
}
pub(crate) fn object_str(o: &OxTerm) -> String {
match o {
OxTerm::NamedNode(n) => n.as_str().to_string(),
OxTerm::Literal(lit) => lit.value().to_string(),
OxTerm::BlankNode(bn) => format!("_:{}", bn.as_str()),
OxTerm::Triple(triple) => format!("<<{triple}>>"),
}
}