Skip to main content

link_cli/
link_reference_validator.rs

1//! Checking that every link a query refers to exists, and creating the ones
2//! that do not when the caller asked for that.
3//!
4//! Ported from the C# `LinkReferenceValidator`, and public for the same reason
5//! the query processor is: a custom front end that resolves references its own
6//! way needs to be able to reuse, or replace, exactly this step.
7
8use anyhow::Result;
9use std::collections::HashSet;
10
11use crate::error::LinkError;
12use crate::link::Link;
13use crate::lino_link::LinoLink;
14use crate::named_type_links::NamedTypeLinks;
15use crate::query_processor::Changes;
16
17pub struct LinkReferenceValidator {
18    trace: bool,
19    auto_create_missing_references: bool,
20}
21
22#[derive(Debug, Default)]
23struct LinkReferencePlan {
24    numeric_ids_to_be_created: HashSet<u32>,
25    names_to_be_created: HashSet<String>,
26    /// `(source, target)` pairs the substitution itself defines.
27    ///
28    /// A missing numeric reference whose own point pair `(id, id)` appears
29    /// here is left as a `(id: 0 0)` placeholder instead of being turned into
30    /// a point link, so that the substitution which is about to write that
31    /// exact pair does not collide with it under uniqueness resolution.
32    composite_pairs_to_be_created: HashSet<(u32, u32)>,
33    missing_references: Vec<MissingLinkReference>,
34    missing_reference_keys: HashSet<String>,
35}
36
37impl LinkReferencePlan {
38    fn add_missing_reference(&mut self, reference: MissingLinkReference) {
39        let key = reference.key();
40        if self.missing_reference_keys.insert(key) {
41            self.missing_references.push(reference);
42        }
43    }
44}
45
46#[derive(Debug, Clone)]
47struct MissingLinkReference {
48    identifier: String,
49    pattern_type: &'static str,
50    numeric_id: Option<u32>,
51}
52
53impl MissingLinkReference {
54    fn key(&self) -> String {
55        self.numeric_id
56            .map(|id| format!("id:{id}"))
57            .unwrap_or_else(|| format!("name:{}", self.identifier))
58    }
59}
60
61impl LinkReferenceValidator {
62    pub fn new(trace: bool, auto_create_missing_references: bool) -> Self {
63        Self {
64            trace,
65            auto_create_missing_references,
66        }
67    }
68
69    pub fn validate_links_exist_or_will_be_created(
70        &self,
71        storage: &mut impl NamedTypeLinks,
72        restriction_patterns: &[LinoLink],
73        substitution_patterns: &[LinoLink],
74    ) -> Result<Changes> {
75        self.trace_msg("[ValidateLinksExistOrWillBeCreated] Starting validation");
76
77        let mut created = Vec::new();
78        loop {
79            let plan =
80                self.plan_references(storage, restriction_patterns, substitution_patterns)?;
81            if plan.missing_references.is_empty() {
82                self.trace_msg("[ValidateLinksExistOrWillBeCreated] Validation completed");
83                return Ok(created);
84            }
85
86            if !self.auto_create_missing_references {
87                let missing = &plan.missing_references[0];
88                return Err(LinkError::QueryError(format!(
89                    "Invalid reference to non-existent link '{}' in {} pattern. Link '{}' does not exist and will not be created by this operation. Use --auto-create-missing-references to create missing references as point links.",
90                    missing.identifier, missing.pattern_type, missing.identifier
91                ))
92                .into());
93            }
94
95            // Creating a reference frees addresses on the way, which changes
96            // the ones the query's new links get, so the plan is made again
97            // until every reference is accounted for.
98            created.extend(self.auto_create_missing_references(storage, &plan)?);
99        }
100    }
101
102    fn plan_references(
103        &self,
104        storage: &mut impl NamedTypeLinks,
105        restriction_patterns: &[LinoLink],
106        substitution_patterns: &[LinoLink],
107    ) -> Result<LinkReferencePlan> {
108        let mut plan = self.build_link_reference_plan(storage, substitution_patterns)?;
109        self.trace_msg(&format!(
110            "[ValidateLinksExistOrWillBeCreated] Numeric links to be created: {:?}",
111            plan.numeric_ids_to_be_created
112        ));
113        self.trace_msg(&format!(
114            "[ValidateLinksExistOrWillBeCreated] Named links to be created: {:?}",
115            plan.names_to_be_created
116        ));
117
118        self.collect_missing_references(
119            storage,
120            &mut plan,
121            restriction_patterns,
122            false,
123            "restriction",
124        )?;
125        self.collect_missing_references(
126            storage,
127            &mut plan,
128            substitution_patterns,
129            true,
130            "substitution",
131        )?;
132        Ok(plan)
133    }
134
135    fn build_link_reference_plan(
136        &self,
137        storage: &mut impl NamedTypeLinks,
138        substitution_patterns: &[LinoLink],
139    ) -> Result<LinkReferencePlan> {
140        let mut plan = LinkReferencePlan::default();
141        let mut anonymous_links = 0;
142        for pattern in substitution_patterns {
143            Self::collect_definitions(pattern, &mut plan, &mut anonymous_links);
144        }
145        plan.numeric_ids_to_be_created
146            .extend(Self::next_created_addresses(storage, anonymous_links)?);
147        Ok(plan)
148    }
149
150    /// Collects the ids and names the substitution defines, the `(source,
151    /// target)` pairs it writes under them, and how many links it writes
152    /// without an id.
153    fn collect_definitions(
154        pattern: &LinoLink,
155        plan: &mut LinkReferencePlan,
156        anonymous_links: &mut usize,
157    ) {
158        if let Some(values) = &pattern.values {
159            for sub_pattern in values {
160                Self::collect_definitions(sub_pattern, plan, anonymous_links);
161            }
162        }
163        if !Self::is_composite_lino(pattern) {
164            return;
165        }
166        let Some(identifier) = Self::concrete_identifier(pattern.id.as_deref()) else {
167            *anonymous_links += 1;
168            return;
169        };
170        match identifier.parse::<u32>() {
171            Ok(link_id) => plan.numeric_ids_to_be_created.insert(link_id),
172            Err(_) => plan.names_to_be_created.insert(identifier),
173        };
174        let values = pattern.values.as_deref().unwrap_or_default();
175        if let (Some(source), Some(target)) = (
176            Self::concrete_numeric_identifier(values[0].id.as_deref()),
177            Self::concrete_numeric_identifier(values[1].id.as_deref()),
178        ) {
179            plan.composite_pairs_to_be_created.insert((source, target));
180        }
181    }
182
183    /// The addresses the next `count` links created in `storage` get.
184    ///
185    /// A store reuses the address freed last first, so the lowest free one is
186    /// not necessarily next. Creating the links and deleting them again in
187    /// reverse leaves the store exactly as it was, and is the one way to ask
188    /// any store, a remote one included.
189    fn next_created_addresses(storage: &mut impl NamedTypeLinks, count: usize) -> Result<Vec<u32>> {
190        let addresses: Vec<u32> = (0..count).map(|_| storage.create(0, 0)).collect();
191        for &address in addresses.iter().rev() {
192            storage.delete(address)?;
193        }
194        Ok(addresses)
195    }
196
197    fn collect_missing_references(
198        &self,
199        storage: &mut impl NamedTypeLinks,
200        plan: &mut LinkReferencePlan,
201        patterns: &[LinoLink],
202        is_substitution: bool,
203        pattern_type: &'static str,
204    ) -> Result<()> {
205        for pattern in patterns {
206            self.collect_missing_references_in_pattern(
207                storage,
208                plan,
209                pattern,
210                is_substitution,
211                pattern_type,
212            )?;
213        }
214        Ok(())
215    }
216
217    fn collect_missing_references_in_pattern(
218        &self,
219        storage: &mut impl NamedTypeLinks,
220        plan: &mut LinkReferencePlan,
221        pattern: &LinoLink,
222        is_substitution: bool,
223        pattern_type: &'static str,
224    ) -> Result<()> {
225        let pattern_id_is_definition = is_substitution
226            && Self::is_composite_lino(pattern)
227            && Self::concrete_identifier(pattern.id.as_deref()).is_some();
228
229        if !pattern_id_is_definition {
230            if let Some(identifier) = Self::concrete_identifier(pattern.id.as_deref()) {
231                self.validate_reference_identifier(storage, plan, &identifier, pattern_type)?;
232            }
233        }
234
235        if let Some(values) = &pattern.values {
236            for sub_pattern in values {
237                self.collect_missing_references_in_pattern(
238                    storage,
239                    plan,
240                    sub_pattern,
241                    is_substitution,
242                    pattern_type,
243                )?;
244            }
245        }
246        Ok(())
247    }
248
249    fn validate_reference_identifier(
250        &self,
251        storage: &mut impl NamedTypeLinks,
252        plan: &mut LinkReferencePlan,
253        identifier: &str,
254        pattern_type: &'static str,
255    ) -> Result<()> {
256        if let Ok(link_id) = identifier.parse::<u32>() {
257            if !storage.exists(link_id) && !plan.numeric_ids_to_be_created.contains(&link_id) {
258                plan.add_missing_reference(MissingLinkReference {
259                    identifier: identifier.to_string(),
260                    pattern_type,
261                    numeric_id: Some(link_id),
262                });
263                return Ok(());
264            }
265            self.trace_msg(&format!(
266                "[ValidateReferencesInPattern] Link {link_id} reference validated in {pattern_type} pattern"
267            ));
268            return Ok(());
269        }
270
271        if storage.get_by_name(identifier)?.is_none()
272            && !plan.names_to_be_created.contains(identifier)
273        {
274            plan.add_missing_reference(MissingLinkReference {
275                identifier: identifier.to_string(),
276                pattern_type,
277                numeric_id: None,
278            });
279            return Ok(());
280        }
281
282        self.trace_msg(&format!(
283            "[ValidateReferencesInPattern] Named link '{identifier}' reference validated in {pattern_type} pattern"
284        ));
285        Ok(())
286    }
287
288    /// Creates every missing reference as a point link and reports each
289    /// creation the way it happens: a numeric reference is first filled in as
290    /// the empty `(id: 0 0)` and then pointed at itself, a named one is created
291    /// as `(name: name name)` directly. The simplified `--changes` report of
292    /// both is the creation of the point link, `() ((2: 2 2))`.
293    ///
294    /// A numeric reference the substitution defines as `(id: id id)` itself
295    /// is only filled in: the query writes the point link.
296    fn auto_create_missing_references(
297        &self,
298        storage: &mut impl NamedTypeLinks,
299        plan: &LinkReferencePlan,
300    ) -> Result<Changes> {
301        let missing_references = &plan.missing_references;
302        let mut created = Vec::new();
303        let mut numeric_references = missing_references
304            .iter()
305            .filter_map(|reference| reference.numeric_id)
306            .collect::<Vec<_>>();
307        numeric_references.sort_unstable();
308        numeric_references.dedup();
309
310        for link_id in numeric_references {
311            if storage.exists(link_id) {
312                continue;
313            }
314
315            self.trace_msg(&format!(
316                "[ValidateLinksExistOrWillBeCreated] Auto-creating missing numeric reference {link_id}."
317            ));
318            storage.try_ensure_created(link_id)?;
319            let placeholder = Link::new(link_id, 0, 0);
320            created.push((None, Some(placeholder)));
321            if plan
322                .composite_pairs_to_be_created
323                .contains(&(link_id, link_id))
324            {
325                self.trace_msg(&format!(
326                    "[ValidateLinksExistOrWillBeCreated] Link {link_id} exists as a placeholder because ({link_id}, {link_id}) is defined by the substitution."
327                ));
328                continue;
329            }
330            storage.update(link_id, link_id, link_id)?;
331            created.push((Some(placeholder), storage.get_link(link_id)));
332        }
333
334        let mut named_references = missing_references
335            .iter()
336            .filter(|reference| reference.numeric_id.is_none())
337            .map(|reference| reference.identifier.clone())
338            .collect::<Vec<_>>();
339        named_references.sort();
340        named_references.dedup();
341
342        for name in named_references {
343            if storage.get_by_name(&name)?.is_some() {
344                continue;
345            }
346
347            self.trace_msg(&format!(
348                "[ValidateLinksExistOrWillBeCreated] Auto-creating missing named reference '{name}' as point link."
349            ));
350            let link_id = storage.get_or_create_named(&name)?;
351            created.push((None, storage.get_link(link_id)));
352        }
353
354        Ok(created)
355    }
356
357    fn is_composite_lino(lino_link: &LinoLink) -> bool {
358        lino_link.values_count() == 2
359    }
360
361    fn concrete_numeric_identifier(id: Option<&str>) -> Option<u32> {
362        Self::concrete_identifier(id).and_then(|identifier| identifier.parse::<u32>().ok())
363    }
364
365    fn concrete_identifier(id: Option<&str>) -> Option<String> {
366        let identifier = id?.trim_end_matches(':');
367        if identifier.is_empty() || identifier == "*" || identifier.starts_with('$') {
368            None
369        } else {
370            Some(identifier.to_string())
371        }
372    }
373
374    fn trace_msg(&self, msg: &str) {
375        if self.trace {
376            eprintln!("{}", msg);
377        }
378    }
379}