use crate::encoding::DEFAULT_GRAPH_ID;
use crate::error::Result;
use crate::sql::{col, sql_str, Capabilities, Request, Response, Statement};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
#[repr(i64)]
pub enum SchemaRole {
Ontology = 1,
Shacl = 2,
Shex = 3,
}
impl SchemaRole {
pub fn from_i64(v: i64) -> Option<Self> {
Some(match v {
1 => Self::Ontology,
2 => Self::Shacl,
3 => Self::Shex,
_ => return None,
})
}
pub const fn as_i64(self) -> i64 {
self as i64
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct SchemaGraph {
pub graph: i64,
pub role: SchemaRole,
pub iri: Option<String>,
pub version: Option<String>,
pub sha256: Option<String>,
pub imports: Vec<String>,
pub active: bool,
pub loaded_at: f64,
}
impl SchemaGraph {
pub fn new(graph: i64, role: SchemaRole) -> Self {
Self {
graph,
role,
iri: None,
version: None,
sha256: None,
imports: Vec::new(),
active: true,
loaded_at: 0.0,
}
}
}
fn opt_str(v: Option<&String>) -> String {
v.map_or_else(|| "NULL".into(), |s| sql_str(s))
}
pub fn register_statements(entry: &SchemaGraph) -> Vec<Statement> {
let imports = if entry.imports.is_empty() {
"NULL".to_string()
} else {
sql_str(&entry.imports.join("\n"))
};
vec![Statement::new(format!(
"INSERT OR REPLACE INTO schema_graphs(g, role, iri, version, sha256, imports, active, loaded_at) \
VALUES ({g}, {role}, {iri}, {version}, {sha}, {imports}, {active}, {loaded_at})",
g = entry.graph,
role = entry.role.as_i64(),
iri = opt_str(entry.iri.as_ref()),
version = opt_str(entry.version.as_ref()),
sha = opt_str(entry.sha256.as_ref()),
active = i64::from(entry.active),
loaded_at = entry.loaded_at,
))]
}
pub fn unregister_statements(graph: i64) -> Vec<Statement> {
vec![Statement::new(format!(
"DELETE FROM schema_graphs WHERE g = {graph}"
))]
}
pub fn set_active_statements(graph: i64, active: bool) -> Vec<Statement> {
vec![Statement::new(format!(
"UPDATE schema_graphs SET active = {} WHERE g = {graph}",
i64::from(active)
))]
}
pub fn drop_statements(graph: i64) -> Vec<Statement> {
let mut s = vec![Statement::new(format!(
"DELETE FROM quads WHERE g = {graph}"
))];
if graph != DEFAULT_GRAPH_ID {
s.push(Statement::new(format!(
"DELETE FROM graphs WHERE id = {graph}"
)));
}
s.extend(unregister_statements(graph));
s
}
pub fn load_request(caps: &Capabilities) -> Request {
let g = if caps.int64_as_text {
"CAST(g AS TEXT)"
} else {
"g"
};
Request::read(vec![Statement::new(format!(
"SELECT {g}, role, iri, version, sha256, imports, active, loaded_at FROM schema_graphs ORDER BY role, g"
))])
}
pub fn from_response(response: &Response) -> Result<Vec<SchemaGraph>> {
let Some(rs) = response.first() else {
return Ok(Vec::new());
};
let mut out = Vec::with_capacity(rs.rows.len());
for row in &rs.rows {
let (Some(graph), Some(role)) = (col(row, 0)?.as_i64(), col(row, 1)?.as_i64()) else {
continue;
};
let Some(role) = SchemaRole::from_i64(role) else {
continue;
};
let text = |i: usize| -> Result<Option<String>> {
Ok(col(row, i)?.clone().into_string().filter(|s| !s.is_empty()))
};
out.push(SchemaGraph {
graph,
role,
iri: text(2)?,
version: text(3)?,
sha256: text(4)?,
imports: text(5)?
.map(|s| s.split('\n').map(str::to_owned).collect())
.unwrap_or_default(),
active: col(row, 6)?.as_i64().unwrap_or(1) != 0,
loaded_at: col(row, 7)?.as_f64().unwrap_or(0.0),
});
}
Ok(out)
}
pub fn scope(role: SchemaRole, column: &str) -> String {
let r = role.as_i64();
format!(
"(NOT EXISTS (SELECT 1 FROM schema_graphs WHERE role = {r} AND active = 1) \
OR {column} IN (SELECT g FROM schema_graphs WHERE role = {r} AND active = 1))"
)
}
pub fn scoped_quads(role: SchemaRole) -> String {
format!(
"(SELECT s, p, o, g FROM quads WHERE {})",
scope(role, "g")
)
}
pub fn quads_without_schema_graphs() -> &'static str {
"(SELECT s, p, o, g FROM quads WHERE g NOT IN (SELECT g FROM schema_graphs))"
}