1use super::{
2 BTreeMap, BTreeSet, DataViewSpec, DataViewUpdate, Error, ErrorKind, Map, MutationPlan, Path,
3 Result, Serialize, Transport, Value, data_views, invalid_plan, json, normalize, validate,
4};
5
6#[derive(Debug, Clone, Default, PartialEq, Serialize)]
7pub struct DataViewPatch {
8 pub base: Option<DataViewUpdate>,
9 pub field_metadata: Map<String, Value>,
10}
11
12#[derive(Debug, Clone, PartialEq)]
13pub struct DataViewImportPlan {
14 pub preview: MutationPlan,
15 pub specs: Vec<DataViewSpec>,
16 pub before: BTreeMap<String, Option<DataViewSpec>>,
17 pub patches: BTreeMap<String, DataViewPatch>,
18 pub skipped: Vec<Value>,
19 pub total: usize,
20 pub overwrite: bool,
21}
22
23#[derive(Debug, Clone, PartialEq, Serialize)]
24pub struct DataViewImportReport {
25 pub applied: bool,
26 pub succeeded: Vec<Value>,
27 pub skipped: Vec<Value>,
28 pub failed: Vec<Value>,
29 pub total: usize,
30}
31pub fn build_patch(current: &DataViewSpec, desired: &DataViewSpec) -> Result<DataViewPatch> {
32 current.validate()?;
33 desired.validate()?;
34 if current.id != desired.id {
35 return unsupported("changing data view id is not supported by the data-view update API");
36 }
37 if current.allow_hidden != desired.allow_hidden {
38 return unsupported("changing allowHidden is not supported by the data-view update API");
39 }
40 if current.name.is_some() && desired.name.is_none() {
41 return unsupported("removing name is not supported by the data-view update API");
42 }
43 if current.time_field_name.is_some() && desired.time_field_name.is_none() {
44 return unsupported("removing timeFieldName is not supported by the data-view update API");
45 }
46 if current.view_type.is_some() && desired.view_type.is_none() {
47 return unsupported("removing type is not supported by the data-view update API");
48 }
49 if current.type_meta.is_some() && desired.type_meta.is_none() {
50 return unsupported("removing typeMeta is not supported by the data-view update API");
51 }
52
53 let base = DataViewUpdate {
54 allow_no_index: changed(¤t.allow_no_index, &desired.allow_no_index)
55 .then_some(desired.allow_no_index),
56 field_formats: changed(¤t.field_formats, &desired.field_formats)
57 .then(|| desired.field_formats.clone()),
58 fields: changed(¤t.fields, &desired.fields).then(|| desired.fields.clone()),
59 name: changed(¤t.name, &desired.name)
60 .then(|| desired.name.clone())
61 .flatten(),
62 runtime_field_map: changed(¤t.runtime_field_map, &desired.runtime_field_map)
63 .then(|| desired.runtime_field_map.clone()),
64 source_filters: changed(¤t.source_filters, &desired.source_filters)
65 .then(|| desired.source_filters.clone()),
66 time_field_name: changed(¤t.time_field_name, &desired.time_field_name)
67 .then(|| desired.time_field_name.clone())
68 .flatten(),
69 title: changed(¤t.title, &desired.title).then(|| desired.title.clone()),
70 view_type: changed(¤t.view_type, &desired.view_type)
71 .then(|| desired.view_type.clone())
72 .flatten(),
73 type_meta: changed(¤t.type_meta, &desired.type_meta)
74 .then(|| desired.type_meta.clone())
75 .flatten(),
76 };
77 let base = (!is_empty_update(&base)).then_some(base);
78
79 let mut field_metadata = Map::new();
80 let names: BTreeSet<_> = current
81 .field_attrs
82 .keys()
83 .chain(desired.field_attrs.keys())
84 .collect();
85 for name in names {
86 let current_values = current.field_attrs.get(name).and_then(Value::as_object);
87 let desired_values = desired.field_attrs.get(name).and_then(Value::as_object);
88 let keys: BTreeSet<_> = current_values
89 .into_iter()
90 .flat_map(|values| values.keys())
91 .chain(desired_values.into_iter().flat_map(|values| values.keys()))
92 .collect();
93 let mut delta = Map::new();
94 for key in keys {
95 let before = current_values.and_then(|values| values.get(key));
96 let after = desired_values.and_then(|values| values.get(key));
97 if before != after {
98 delta.insert(key.clone(), after.cloned().unwrap_or(Value::Null));
99 }
100 }
101 if !delta.is_empty() {
102 field_metadata.insert(name.clone(), Value::Object(delta));
103 }
104 }
105 Ok(DataViewPatch {
106 base,
107 field_metadata,
108 })
109}
110
111pub async fn plan_import(
112 transport: Option<&Transport>,
113 path: &Path,
114 overwrite: bool,
115 skip_existing: bool,
116) -> Result<DataViewImportPlan> {
117 let mut specs = validate(path)?;
118 if specs.is_empty() {
119 return Err(Error::new(
120 ErrorKind::Error,
121 "data-view import needs at least one data view",
122 ));
123 }
124 if overwrite && skip_existing {
125 return Err(Error::new(
126 ErrorKind::Error,
127 "--overwrite and --skip-existing cannot be used together",
128 ));
129 }
130 let total = specs.len();
131 let requires_server = overwrite || skip_existing || transport.is_some();
132 let transport = if requires_server { transport } else { None };
133 if (overwrite || skip_existing) && transport.is_none() {
134 return Err(Error::new(
135 ErrorKind::Error,
136 "data-view import conflict mode needs a transport",
137 ));
138 }
139
140 let mut before = BTreeMap::new();
141 let mut skipped = Vec::new();
142 let mut conflicts = Vec::new();
143 if let Some(transport) = transport {
144 for spec in &specs {
145 match read_spec(transport, &spec.id).await {
146 Ok(current) => {
147 if !overwrite && !skip_existing {
148 conflicts.push(spec.id.clone());
149 }
150 before.insert(spec.id.clone(), Some(current));
151 }
152 Err(error) if error.kind == ErrorKind::NotFound => {
153 before.insert(spec.id.clone(), None);
154 }
155 Err(error) => return Err(error),
156 }
157 }
158 } else {
159 before.extend(specs.iter().map(|spec| (spec.id.clone(), None)));
160 }
161 if !conflicts.is_empty() {
162 return Err(Error::new(
163 ErrorKind::Conflict,
164 format!("data views already exist: {}", conflicts.join(", ")),
165 ));
166 }
167 if skip_existing {
168 specs.retain(|spec| match before.get(&spec.id) {
169 Some(Some(_)) => {
170 skipped.push(json!({"id": spec.id, "reason": "exists"}));
171 false
172 }
173 _ => true,
174 });
175 before.retain(|id, _| specs.iter().any(|spec| spec.id == *id));
176 }
177
178 let mut patches = BTreeMap::new();
179 for spec in &specs {
180 match before.get(&spec.id).and_then(Option::as_ref) {
181 Some(current) if current == spec => {
182 patches.insert(spec.id.clone(), DataViewPatch::default());
183 }
184 Some(current) => {
185 let patch = build_patch(current, spec)?;
186 patches.insert(spec.id.clone(), patch);
187 }
188 None => {}
189 }
190 }
191 let preview = MutationPlan {
192 preview_action: format!(
193 "Import {} data view(s) from {}",
194 specs.len(),
195 path.display()
196 ),
197 preview_details: preview_details(&specs, &before),
198 targets: specs.iter().map(|spec| spec.id.clone()).collect(),
199 };
200 Ok(DataViewImportPlan {
201 preview,
202 specs,
203 before,
204 patches,
205 skipped,
206 total,
207 overwrite,
208 })
209}
210
211pub async fn apply_import(
212 transport: &Transport,
213 plan: &DataViewImportPlan,
214) -> Result<DataViewImportReport> {
215 validate_import_plan(plan)?;
216 let mut succeeded = Vec::new();
217 let mut failed = Vec::new();
218
219 for desired in &plan.specs {
220 let before = match plan.before.get(&desired.id) {
221 Some(before) => before,
222 None => {
223 failed.push(failed_row(&desired.id, false, "missing preflight snapshot"));
224 continue;
225 }
226 };
227 let current = match read_spec(transport, &desired.id).await {
228 Ok(current) => Some(current),
229 Err(error) if error.kind == ErrorKind::NotFound => None,
230 Err(error) => {
231 failed.push(failed_row(&desired.id, false, error.message));
232 continue;
233 }
234 };
235
236 match (before, current) {
237 (None, Some(_)) => {
238 failed.push(failed_row(
239 &desired.id,
240 false,
241 "data view appeared since preview",
242 ));
243 continue;
244 }
245 (Some(_), None) => {
246 failed.push(failed_row(
247 &desired.id,
248 false,
249 "data view disappeared since preview",
250 ));
251 continue;
252 }
253 (Some(before), Some(current)) if before != ¤t => {
254 failed.push(failed_row(
255 &desired.id,
256 false,
257 "data view changed since preview",
258 ));
259 continue;
260 }
261 (None, None) => {
262 let mut applied = false;
263 match data_views::create(transport, desired).await {
264 Ok(_) => applied = true,
265 Err(error) => {
266 failed.push(failed_row(&desired.id, applied, error.message));
267 continue;
268 }
269 }
270 match read_spec(transport, &desired.id).await {
271 Ok(stored) if stored == *desired => {
272 succeeded.push(json!({"id": desired.id, "action": "created"}));
273 }
274 Ok(_) => failed.push(failed_row(
275 &desired.id,
276 applied,
277 "server stored a different data-view spec",
278 )),
279 Err(error) => failed.push(failed_row(&desired.id, applied, error.message)),
280 }
281 }
282 (Some(_), Some(_)) => {
283 let patch = match plan.patches.get(&desired.id) {
284 Some(patch) => patch,
285 None => {
286 failed.push(failed_row(&desired.id, false, "missing replacement patch"));
287 continue;
288 }
289 };
290 if patch.base.is_none() && patch.field_metadata.is_empty() {
291 succeeded.push(json!({"id": desired.id, "action": "unchanged"}));
292 continue;
293 }
294
295 let mut applied = false;
296 if let Some(base) = &patch.base {
297 if let Err(error) = data_views::update(transport, &desired.id, base).await {
298 failed.push(failed_row(
299 &desired.id,
300 applied,
301 format!("base update failed: {}", error.message),
302 ));
303 continue;
304 }
305 applied = true;
306 }
307 if !patch.field_metadata.is_empty()
308 && let Err(error) = data_views::update_fields_metadata(
309 transport,
310 &desired.id,
311 &patch.field_metadata,
312 )
313 .await
314 {
315 let message = if applied {
316 format!("base updated; field metadata failed: {}", error.message)
317 } else {
318 format!("field metadata failed: {}", error.message)
319 };
320 failed.push(failed_row(&desired.id, applied, message));
321 continue;
322 }
323 if !patch.field_metadata.is_empty() {
324 applied = true;
325 }
326 match read_spec(transport, &desired.id).await {
327 Ok(stored) if stored == *desired => {
328 succeeded.push(json!({"id": desired.id, "action": "replaced"}));
329 }
330 Ok(_) => failed.push(failed_row(
331 &desired.id,
332 applied,
333 "server stored a different data-view spec",
334 )),
335 Err(error) => failed.push(failed_row(&desired.id, applied, error.message)),
336 }
337 }
338 }
339 }
340
341 Ok(DataViewImportReport {
342 applied: true,
343 succeeded,
344 skipped: plan.skipped.clone(),
345 failed,
346 total: plan.total,
347 })
348}
349
350fn changed<T: PartialEq>(current: &T, desired: &T) -> bool {
351 current != desired
352}
353
354fn is_empty_update(update: &DataViewUpdate) -> bool {
355 update.allow_no_index.is_none()
356 && update.field_formats.is_none()
357 && update.fields.is_none()
358 && update.name.is_none()
359 && update.runtime_field_map.is_none()
360 && update.source_filters.is_none()
361 && update.time_field_name.is_none()
362 && update.title.is_none()
363 && update.view_type.is_none()
364 && update.type_meta.is_none()
365}
366
367fn unsupported(message: impl Into<String>) -> Result<DataViewPatch> {
368 Err(Error::new(ErrorKind::Unsupported, message))
369}
370
371fn failed_row(id: &str, applied: bool, error: impl Into<String>) -> Value {
372 json!({"id": id, "applied": applied, "error": error.into()})
373}
374
375async fn read_spec(transport: &Transport, id: &str) -> Result<DataViewSpec> {
376 let spec = normalize(&Value::Object(
377 data_views::get(transport, id).await?.data_view,
378 ))?;
379 if spec.id != id {
380 return Err(Error::new(
381 ErrorKind::Http,
382 format!("decoding data view: expected id '{id}', got '{}'", spec.id),
383 ));
384 }
385 Ok(spec)
386}
387
388fn validate_import_plan(plan: &DataViewImportPlan) -> Result<()> {
389 if plan.total == 0 {
390 return invalid_plan("total must be greater than zero");
391 }
392 if plan.total != plan.specs.len() + plan.skipped.len() {
393 return invalid_plan("total does not equal pending and skipped data views");
394 }
395
396 let mut pending_ids = Vec::with_capacity(plan.specs.len());
397 for spec in &plan.specs {
398 validate_canonical_spec(spec)?;
399 if pending_ids
400 .last()
401 .is_some_and(|previous| previous >= &spec.id)
402 {
403 return invalid_plan("pending data views must be unique and sorted by id");
404 }
405 pending_ids.push(spec.id.clone());
406 }
407 let pending: BTreeSet<_> = pending_ids.iter().cloned().collect();
408 if !plan.skipped.is_empty() && plan.overwrite {
409 return invalid_plan("skipped data views require skip-existing mode");
410 }
411 let mut skipped_ids = BTreeSet::new();
412 let mut previous_skipped = None;
413 for row in &plan.skipped {
414 let object = row
415 .as_object()
416 .filter(|object| object.len() == 2)
417 .ok_or_else(|| Error::new(ErrorKind::Error, "invalid data-view import skipped row"))?;
418 if !object.keys().map(String::as_str).eq(["id", "reason"]) {
419 return invalid_plan("invalid data-view import skipped row order");
420 }
421 let id = object
422 .get("id")
423 .and_then(Value::as_str)
424 .filter(|id| !id.trim().is_empty())
425 .ok_or_else(|| Error::new(ErrorKind::Error, "invalid data-view import skipped row"))?;
426 if object.get("reason").and_then(Value::as_str) != Some("exists")
427 || previous_skipped.is_some_and(|previous: &str| previous >= id)
428 || !skipped_ids.insert(id.to_owned())
429 || pending.contains(id)
430 {
431 return invalid_plan("invalid data-view import skipped rows");
432 }
433 previous_skipped = Some(id);
434 }
435 if plan.preview.targets != pending_ids {
436 return invalid_plan("preview targets do not match pending data views");
437 }
438 let prefix = format!("Import {} data view(s) from ", plan.specs.len());
439 if !plan.preview.preview_action.starts_with(&prefix)
440 || plan.preview.preview_action[prefix.len()..].is_empty()
441 {
442 return invalid_plan("preview action does not match pending data views");
443 }
444 if plan.preview.preview_details != preview_details(&plan.specs, &plan.before) {
445 return invalid_plan("preview details do not match pending data views");
446 }
447
448 let before_ids: BTreeSet<_> = plan.before.keys().cloned().collect();
449 if before_ids != pending {
450 return invalid_plan("preflight snapshots do not match pending data views");
451 }
452 let mut patch_ids = BTreeSet::new();
453 for spec in &plan.specs {
454 let before = plan.before.get(&spec.id).ok_or_else(|| {
455 Error::new(
456 ErrorKind::Error,
457 "preflight snapshots do not match pending data views",
458 )
459 })?;
460 match before {
461 None => {
462 if plan.patches.contains_key(&spec.id) {
463 return invalid_plan("planned creates must not carry a replacement patch");
464 }
465 }
466 Some(snapshot) => {
467 validate_canonical_spec(snapshot)?;
468 if snapshot.id != spec.id {
469 return invalid_plan("preflight snapshot id does not match its target");
470 }
471 if !plan.overwrite {
472 return invalid_plan("replacement plan requires overwrite");
473 }
474 let expected = build_patch(snapshot, spec)?;
475 let patch = plan.patches.get(&spec.id).ok_or_else(|| {
476 Error::new(ErrorKind::Error, "planned replacement is missing its patch")
477 })?;
478 if patch != &expected {
479 return invalid_plan("planned replacement patch does not match its snapshots");
480 }
481 patch_ids.insert(spec.id.clone());
482 }
483 }
484 }
485 if plan.patches.keys().cloned().collect::<BTreeSet<_>>() != patch_ids {
486 return invalid_plan("replacement patches do not match preflight snapshots");
487 }
488 Ok(())
489}
490
491fn validate_canonical_spec(spec: &DataViewSpec) -> Result<()> {
492 spec.validate()?;
493 if matches!(spec.type_meta.as_ref(), Some(values) if values.is_empty()) {
494 return Err(Error::new(
495 ErrorKind::Error,
496 "data view typeMeta must not be empty",
497 ));
498 }
499 Ok(())
500}
501
502fn preview_details(
503 specs: &[DataViewSpec],
504 before: &BTreeMap<String, Option<DataViewSpec>>,
505) -> Vec<String> {
506 specs
507 .iter()
508 .filter_map(|spec| match before.get(&spec.id) {
509 Some(None) => Some(format!("{} create {}", spec.id, spec.title)),
510 Some(Some(current)) if current == spec => {
511 Some(format!("{} no-op {}", spec.id, spec.title))
512 }
513 Some(Some(current)) => Some(format!(
514 "{} replace {} -> {}",
515 spec.id, current.title, spec.title
516 )),
517 None => None,
518 })
519 .collect()
520}