1use crate::encoding::DEFAULT_GRAPH_ID;
12use crate::error::Result;
13use crate::sql::{col, sql_str, Capabilities, Request, Response, Statement};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
19#[repr(i64)]
20pub enum SchemaRole {
21 Ontology = 1,
23 Shacl = 2,
25 Shex = 3,
27}
28
29impl SchemaRole {
30 pub fn from_i64(v: i64) -> Option<Self> {
31 Some(match v {
32 1 => Self::Ontology,
33 2 => Self::Shacl,
34 3 => Self::Shex,
35 _ => return None,
36 })
37 }
38
39 pub const fn as_i64(self) -> i64 {
40 self as i64
41 }
42}
43
44#[derive(Debug, Clone, PartialEq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
48pub struct SchemaGraph {
49 pub graph: i64,
51 pub role: SchemaRole,
52 pub iri: Option<String>,
54 pub version: Option<String>,
56 pub sha256: Option<String>,
58 pub imports: Vec<String>,
60 pub active: bool,
62 pub loaded_at: f64,
64}
65
66impl SchemaGraph {
67 pub fn new(graph: i64, role: SchemaRole) -> Self {
69 Self {
70 graph,
71 role,
72 iri: None,
73 version: None,
74 sha256: None,
75 imports: Vec::new(),
76 active: true,
77 loaded_at: 0.0,
78 }
79 }
80}
81
82fn opt_str(v: Option<&String>) -> String {
83 v.map_or_else(|| "NULL".into(), |s| sql_str(s))
84}
85
86pub fn register_statements(entry: &SchemaGraph) -> Vec<Statement> {
88 let imports = if entry.imports.is_empty() {
91 "NULL".to_string()
92 } else {
93 sql_str(&entry.imports.join("\n"))
94 };
95 vec![Statement::new(format!(
96 "INSERT OR REPLACE INTO schema_graphs(g, role, iri, version, sha256, imports, active, loaded_at) \
97 VALUES ({g}, {role}, {iri}, {version}, {sha}, {imports}, {active}, {loaded_at})",
98 g = entry.graph,
99 role = entry.role.as_i64(),
100 iri = opt_str(entry.iri.as_ref()),
101 version = opt_str(entry.version.as_ref()),
102 sha = opt_str(entry.sha256.as_ref()),
103 active = i64::from(entry.active),
104 loaded_at = entry.loaded_at,
105 ))]
106}
107
108pub fn unregister_statements(graph: i64) -> Vec<Statement> {
110 vec![Statement::new(format!(
111 "DELETE FROM schema_graphs WHERE g = {graph}"
112 ))]
113}
114
115pub fn set_active_statements(graph: i64, active: bool) -> Vec<Statement> {
117 vec![Statement::new(format!(
118 "UPDATE schema_graphs SET active = {} WHERE g = {graph}",
119 i64::from(active)
120 ))]
121}
122
123pub fn drop_statements(graph: i64) -> Vec<Statement> {
125 let mut s = vec![Statement::new(format!(
126 "DELETE FROM quads WHERE g = {graph}"
127 ))];
128 if graph != DEFAULT_GRAPH_ID {
129 s.push(Statement::new(format!(
130 "DELETE FROM graphs WHERE id = {graph}"
131 )));
132 }
133 s.extend(unregister_statements(graph));
134 s
135}
136
137pub fn load_request(caps: &Capabilities) -> Request {
139 let g = if caps.int64_as_text {
140 "CAST(g AS TEXT)"
141 } else {
142 "g"
143 };
144 Request::read(vec![Statement::new(format!(
145 "SELECT {g}, role, iri, version, sha256, imports, active, loaded_at FROM schema_graphs ORDER BY role, g"
146 ))])
147}
148
149pub fn from_response(response: &Response) -> Result<Vec<SchemaGraph>> {
151 let Some(rs) = response.first() else {
152 return Ok(Vec::new());
153 };
154 let mut out = Vec::with_capacity(rs.rows.len());
155 for row in &rs.rows {
156 let (Some(graph), Some(role)) = (col(row, 0)?.as_i64(), col(row, 1)?.as_i64()) else {
157 continue;
158 };
159 let Some(role) = SchemaRole::from_i64(role) else {
160 continue;
161 };
162 let text = |i: usize| -> Result<Option<String>> {
163 Ok(col(row, i)?.clone().into_string().filter(|s| !s.is_empty()))
164 };
165 out.push(SchemaGraph {
166 graph,
167 role,
168 iri: text(2)?,
169 version: text(3)?,
170 sha256: text(4)?,
171 imports: text(5)?
172 .map(|s| s.split('\n').map(str::to_owned).collect())
173 .unwrap_or_default(),
174 active: col(row, 6)?.as_i64().unwrap_or(1) != 0,
175 loaded_at: col(row, 7)?.as_f64().unwrap_or(0.0),
176 });
177 }
178 Ok(out)
179}
180
181pub fn scope(role: SchemaRole, column: &str) -> String {
186 let r = role.as_i64();
187 format!(
188 "(NOT EXISTS (SELECT 1 FROM schema_graphs WHERE role = {r} AND active = 1) \
189 OR {column} IN (SELECT g FROM schema_graphs WHERE role = {r} AND active = 1))"
190 )
191}
192
193pub fn scoped_quads(role: SchemaRole) -> String {
195 format!("(SELECT s, p, o, g FROM quads WHERE {})", scope(role, "g"))
196}
197
198pub fn quads_without_schema_graphs() -> &'static str {
201 "(SELECT s, p, o, g FROM quads WHERE g NOT IN (SELECT g FROM schema_graphs))"
202}
203
204pub fn without_schema_graphs(source: &str) -> String {
206 format!("(SELECT s, p, o, g FROM {source} WHERE g NOT IN (SELECT g FROM schema_graphs))")
207}