zeppelin_embed/lifecycle/
cascade.rs1use super::namespace_batch::{
3 body, durable_write, envelope, invalid, io, name_valid, read_optional, sync_dir,
4};
5use super::{
6 CancelToken, DocumentFields, DocumentScanRequest, NamespaceMutation, QueryControl, Store,
7 StoreError,
8};
9use crate::ingest::DocId;
10use crate::meta::{ColumnId, ColumnType, PredicateValue};
11use crate::vfs::{StdVfs, Vfs};
12use std::collections::{BTreeMap, BTreeSet, VecDeque};
13use std::path::Path;
14
15const RECORD: &str = ".ze-cascades";
16const VERSION: &str = "ZECASCADE1\n";
17
18#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
20pub struct CascadeRule {
21 pub parent: String,
23 pub child: String,
25 pub attribute: ColumnId,
27}
28
29fn encode(rules: &BTreeSet<CascadeRule>) -> Vec<u8> {
30 let mut text = VERSION.to_owned();
31 for rule in rules {
32 text.push_str(&format!(
33 "{}\t{}\t{}\n",
34 rule.parent,
35 rule.child,
36 rule.attribute.get()
37 ));
38 }
39 envelope(text.as_bytes())
40}
41fn read(root: &Path) -> Result<BTreeSet<CascadeRule>, StoreError> {
42 let path = root.join(RECORD);
43 let Some(bytes) = read_optional(&path)? else {
44 return Ok(BTreeSet::new());
45 };
46 let text =
47 std::str::from_utf8(body(&path, &bytes)?).map_err(|_| invalid(&path, "cascade UTF-8"))?;
48 let text = text
49 .strip_prefix(VERSION)
50 .ok_or_else(|| invalid(&path, "cascade version"))?;
51 let mut rules = BTreeSet::new();
52 for line in text.lines() {
53 let mut fields = line.split('\t');
54 let parent = fields
55 .next()
56 .ok_or_else(|| invalid(&path, "cascade parent"))?;
57 let child = fields
58 .next()
59 .ok_or_else(|| invalid(&path, "cascade child"))?;
60 let attribute = fields
61 .next()
62 .and_then(|s| s.parse::<u32>().ok())
63 .ok_or_else(|| invalid(&path, "cascade attribute"))?;
64 if fields.next().is_some() || !name_valid(parent) || !name_valid(child) {
65 return Err(invalid(&path, "invalid cascade rule"));
66 }
67 let rule = CascadeRule {
68 parent: parent.into(),
69 child: child.into(),
70 attribute: ColumnId::new(attribute),
71 };
72 reject_cycle(&rules, &rule)?;
73 if !rules.insert(rule) {
74 return Err(invalid(&path, "duplicate cascade rule"));
75 }
76 }
77 if encode(&rules) != bytes {
78 return Err(invalid(&path, "noncanonical cascade record"));
79 }
80 Ok(rules)
81}
82fn reject_cycle(rules: &BTreeSet<CascadeRule>, new: &CascadeRule) -> Result<(), StoreError> {
83 let mut queue = VecDeque::from([(
84 new.child.clone(),
85 vec![new.parent.clone(), new.child.clone()],
86 )]);
87 let mut visited = BTreeSet::new();
88 while let Some((name, path)) = queue.pop_front() {
89 if name == new.parent {
90 return Err(StoreError::CascadeCycle { cycle: path });
91 }
92 if !visited.insert(name.clone()) {
93 continue;
94 }
95 for rule in rules.iter().filter(|r| r.parent == name) {
96 let mut path = path.clone();
97 path.push(rule.child.clone());
98 queue.push_back((rule.child.clone(), path));
99 }
100 }
101 Ok(())
102}
103fn validate(
104 root: &Path,
105 sources: &BTreeMap<String, Store>,
106 rule: &CascadeRule,
107) -> Result<(), StoreError> {
108 if !sources.contains_key(&rule.parent) {
109 return Err(invalid(
110 root,
111 &format!("missing cascade participant {}", rule.parent),
112 ));
113 }
114 let child = sources
115 .get(&rule.child)
116 .ok_or_else(|| invalid(root, &format!("missing cascade participant {}", rule.child)))?;
117 if child
118 .schema()
119 .column(rule.attribute)
120 .map(|c| c.column_type())
121 != Some(ColumnType::Id128)
122 {
123 return Err(invalid(
124 root,
125 "cascade attribute must be a declared id128 column",
126 ));
127 }
128 Ok(())
129}
130
131pub(super) fn declare(
132 root: &Path,
133 sources: &BTreeMap<String, Store>,
134 rule: CascadeRule,
135 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
136) -> Result<(), StoreError> {
137 validate(root, sources, &rule)?;
138 let mut rules = read(root)?;
139 reject_cycle(&rules, &rule)?;
140 if !rules.insert(rule) {
141 return Ok(());
142 }
143 let bytes = encode(&rules);
144 if bytes.len() > 1024 * 1024 {
145 return Err(invalid(root, "cascade record too large"));
146 }
147 let temporary = root.join(".ze-cascades.tmp");
148 durable_write(&StdVfs, &temporary, &bytes, step)?;
149 StdVfs
150 .rename(&temporary, &root.join(RECORD))
151 .map_err(|e| io(root, e))?;
152 step("cascade declaration rename").map_err(|e| io(root, e))?;
153 sync_dir(&StdVfs, root, step)
154}
155
156pub(super) fn expand(
157 root: &Path,
158 sources: &BTreeMap<String, Store>,
159 mutations: &mut [NamespaceMutation],
160) -> Result<(), StoreError> {
161 let rules = read(root)?;
162 let mut reachable: BTreeSet<String> = sources.keys().cloned().collect();
165 loop {
166 let before = reachable.len();
167 for rule in &rules {
168 if reachable.contains(&rule.parent) {
169 reachable.insert(rule.child.clone());
170 }
171 }
172 if before == reachable.len() {
173 break;
174 }
175 }
176 for name in &reachable {
177 if !sources.contains_key(name) {
178 return Err(invalid(
179 root,
180 &format!("missing cascade participant {name}"),
181 ));
182 }
183 }
184 let active: Vec<_> = rules
185 .iter()
186 .filter(|r| reachable.contains(&r.parent))
187 .collect();
188 for rule in &active {
189 validate(root, sources, rule)?;
190 }
191 let mut ids: BTreeMap<String, BTreeSet<DocId>> = mutations
192 .iter()
193 .map(|m| (m.name.clone(), m.deletes.iter().copied().collect()))
194 .collect();
195 let mut references = Vec::new();
198 for (name, source) in sources {
199 let child_rules: Vec<_> = active.iter().filter(|r| r.child == *name).collect();
200 if child_rules.is_empty() {
201 continue;
202 }
203 let mut cursor = None;
204 loop {
205 let mut request = DocumentScanRequest::new(
206 1024,
207 DocumentFields::ATTRIBUTES,
208 QueryControl::Cancel(CancelToken::new()),
209 );
210 if let Some(cursor) = cursor {
211 request = request.with_cursor(cursor);
212 }
213 let page = source
214 .scan_documents(request)
215 .map_err(|e| invalid(root, &e.to_string()))?;
216 for document in page.documents {
217 for (column, value) in document.attributes.into_iter().flatten() {
218 for rule in child_rules.iter().filter(|r| r.attribute == column) {
219 let PredicateValue::Id128(parent_id) = value else {
220 return Err(invalid(root, "cascade reference is not id128"));
221 };
222 references.push((
223 rule.parent.clone(),
224 parent_id,
225 name.clone(),
226 document.doc_id,
227 ));
228 }
229 }
230 }
231 cursor = page.continuation;
232 if cursor.is_none() {
233 break;
234 }
235 }
236 }
237 loop {
238 let mut changed = false;
239 for (parent, parent_id, child, child_id) in &references {
240 if ids.get(parent).is_some_and(|set| set.contains(parent_id)) {
241 let set = ids
242 .get_mut(child)
243 .ok_or_else(|| invalid(root, "missing child ID set"))?;
244 changed |= set.insert(*child_id);
245 }
246 }
247 if !changed {
248 break;
249 }
250 }
251 for mutation in mutations {
252 mutation.deletes = ids
253 .remove(&mutation.name)
254 .ok_or_else(|| invalid(root, "missing cascade ID set"))?
255 .into_iter()
256 .collect();
257 }
258 Ok(())
259}