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