Skip to content

Commit 4b34691

Browse files
author
Frank Dekervel
committed
runtime: introduce PatchOptions {ignore_no_ops, treat_missing_as_null} with defaults; make no-op skipping configurable; wire CLI --ignore-noop/--treat-missing-as-null; update Python patch signature; update tests
1 parent 7c58d56 commit 4b34691

File tree

6 files changed

+171
-44
lines changed

6 files changed

+171
-44
lines changed

src/python/src/lib.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -764,22 +764,30 @@ fn py_diff(
764764
Ok(json_value_to_py(py, &JsonValue::Array(vals)))
765765
}
766766

767-
#[pyfunction(name = "patch", signature = (source, deltas, treat_missing_as_null = false))]
767+
#[pyfunction(name = "patch", signature = (source, deltas, treat_missing_as_null = true, ignore_no_ops = true))]
768768
fn py_patch(
769769
py: Python<'_>,
770770
source: &PyLinkMLValue,
771771
deltas: &Bound<'_, PyAny>,
772772
treat_missing_as_null: bool,
773+
ignore_no_ops: bool,
773774
) -> PyResult<PyObject> {
774775
let json_mod = PyModule::import(py, "json")?;
775776
let deltas_str: String = json_mod.call_method1("dumps", (deltas,))?.extract()?;
776777
let deltas_vec: Vec<Delta> =
777778
serde_json::from_str(&deltas_str).map_err(|e| PyException::new_err(e.to_string()))?;
778779
let sv_ref = source.sv.bind(py).borrow();
779780
let rust_sv = sv_ref.as_rust();
780-
let (new_value, trace) =
781-
patch_internal(&source.value, &deltas_vec, rust_sv, treat_missing_as_null)
782-
.map_err(|e| PyException::new_err(e.to_string()))?;
781+
let (new_value, trace) = patch_internal(
782+
&source.value,
783+
&deltas_vec,
784+
rust_sv,
785+
linkml_runtime::diff::PatchOptions {
786+
ignore_no_ops,
787+
treat_missing_as_null,
788+
},
789+
)
790+
.map_err(|e| PyException::new_err(e.to_string()))?;
783791
let trace_json = serde_json::json!({
784792
"added": trace.added,
785793
"deleted": trace.deleted,

src/runtime/src/diff.rs

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -248,23 +248,31 @@ pub struct PatchTrace {
248248
pub updated: Vec<NodeId>,
249249
}
250250

251+
#[derive(Debug, Clone, Copy)]
252+
pub struct PatchOptions {
253+
pub ignore_no_ops: bool,
254+
pub treat_missing_as_null: bool,
255+
}
256+
257+
impl Default for PatchOptions {
258+
fn default() -> Self {
259+
Self {
260+
ignore_no_ops: true,
261+
treat_missing_as_null: true,
262+
}
263+
}
264+
}
265+
251266
pub fn patch(
252267
source: &LinkMLValue,
253268
deltas: &[Delta],
254269
sv: &SchemaView,
255-
treat_missing_as_null: bool,
270+
opts: PatchOptions,
256271
) -> LResult<(LinkMLValue, PatchTrace)> {
257272
let mut out = source.clone();
258273
let mut trace = PatchTrace::default();
259274
for d in deltas {
260-
apply_delta_linkml(
261-
&mut out,
262-
&d.path,
263-
&d.new,
264-
sv,
265-
&mut trace,
266-
treat_missing_as_null,
267-
)?;
275+
apply_delta_linkml(&mut out, &d.path, &d.new, sv, &mut trace, opts)?;
268276
}
269277
Ok((out, trace))
270278
}
@@ -303,7 +311,7 @@ fn apply_delta_linkml(
303311
newv: &Option<serde_json::Value>,
304312
sv: &SchemaView,
305313
trace: &mut PatchTrace,
306-
treat_missing_as_null: bool,
314+
opts: PatchOptions,
307315
) -> LResult<()> {
308316
if path.is_empty() {
309317
if let Some(v) = newv {
@@ -317,7 +325,7 @@ fn apply_delta_linkml(
317325
let conv = sv.converter();
318326
if let Some(cls) = class_opt {
319327
let new_node = LinkMLValue::from_json(v.clone(), cls, slot_opt, sv, &conv, false)?;
320-
if current.equals(&new_node, treat_missing_as_null) {
328+
if opts.ignore_no_ops && current.equals(&new_node, opts.treat_missing_as_null) {
321329
// No-op delta; skip to preserve node IDs
322330
return Ok(());
323331
}
@@ -346,7 +354,9 @@ fn apply_delta_linkml(
346354
false,
347355
)?;
348356
if let Some(old_child) = values.get_mut(key) {
349-
if old_child.equals(&new_child, treat_missing_as_null) {
357+
if opts.ignore_no_ops
358+
&& old_child.equals(&new_child, opts.treat_missing_as_null)
359+
{
350360
// no-op; skip
351361
return Ok(());
352362
}
@@ -368,7 +378,8 @@ fn apply_delta_linkml(
368378
}
369379
} else {
370380
// adding a Null assignment may be a no-op when treating missing as null
371-
if treat_missing_as_null
381+
if opts.ignore_no_ops
382+
&& opts.treat_missing_as_null
372383
&& matches!(new_child, LinkMLValue::Null { .. })
373384
{
374385
return Ok(());
@@ -381,7 +392,8 @@ fn apply_delta_linkml(
381392
}
382393
None => {
383394
if let Some(old_child) = values.get(key) {
384-
if treat_missing_as_null
395+
if opts.ignore_no_ops
396+
&& opts.treat_missing_as_null
385397
&& matches!(old_child, LinkMLValue::Null { .. })
386398
{
387399
// deleting a Null assignment: no-op
@@ -395,7 +407,7 @@ fn apply_delta_linkml(
395407
}
396408
}
397409
} else if let Some(child) = values.get_mut(key) {
398-
apply_delta_linkml(child, &path[1..], newv, sv, trace, treat_missing_as_null)?;
410+
apply_delta_linkml(child, &path[1..], newv, sv, trace, opts)?;
399411
}
400412
}
401413
LinkMLValue::Mapping { values, slot, .. } => {
@@ -412,7 +424,9 @@ fn apply_delta_linkml(
412424
Vec::new(),
413425
)?;
414426
if let Some(old_child) = values.get(key) {
415-
if old_child.equals(&new_child, treat_missing_as_null) {
427+
if opts.ignore_no_ops
428+
&& old_child.equals(&new_child, opts.treat_missing_as_null)
429+
{
416430
return Ok(());
417431
}
418432
mark_deleted_subtree(old_child, trace);
@@ -430,7 +444,7 @@ fn apply_delta_linkml(
430444
}
431445
}
432446
} else if let Some(child) = values.get_mut(key) {
433-
apply_delta_linkml(child, &path[1..], newv, sv, trace, treat_missing_as_null)?;
447+
apply_delta_linkml(child, &path[1..], newv, sv, trace, opts)?;
434448
}
435449
}
436450
LinkMLValue::List {
@@ -455,7 +469,9 @@ fn apply_delta_linkml(
455469
&conv,
456470
Vec::new(),
457471
)?;
458-
if values[idx].equals(&new_child, treat_missing_as_null) {
472+
if opts.ignore_no_ops
473+
&& values[idx].equals(&new_child, opts.treat_missing_as_null)
474+
{
459475
return Ok(());
460476
}
461477
match (&mut values[idx], &new_child) {
@@ -504,14 +520,7 @@ fn apply_delta_linkml(
504520
}
505521
}
506522
} else if idx < values.len() {
507-
apply_delta_linkml(
508-
&mut values[idx],
509-
&path[1..],
510-
newv,
511-
sv,
512-
trace,
513-
treat_missing_as_null,
514-
)?;
523+
apply_delta_linkml(&mut values[idx], &path[1..], newv, sv, trace, opts)?;
515524
}
516525
}
517526
LinkMLValue::Scalar { .. } => {}

src/runtime/tests/diff.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,16 @@ fn diff_and_patch_person() {
5757
}
5858
}
5959

60-
let (patched, _trace) = patch(&src, &deltas, &sv, false).unwrap();
60+
let (patched, _trace) = patch(
61+
&src,
62+
&deltas,
63+
&sv,
64+
linkml_runtime::diff::PatchOptions {
65+
ignore_no_ops: true,
66+
treat_missing_as_null: false,
67+
},
68+
)
69+
.unwrap();
6170
let patched_json = patched.to_json();
6271
let target_json = tgt.to_json();
6372
let src_json = src.to_json();
@@ -93,7 +102,16 @@ fn diff_ignore_missing_target() {
93102

94103
let deltas = diff(&src, &tgt, false);
95104
assert!(deltas.is_empty());
96-
let (patched, _trace) = patch(&src, &deltas, &sv, false).unwrap();
105+
let (patched, _trace) = patch(
106+
&src,
107+
&deltas,
108+
&sv,
109+
linkml_runtime::diff::PatchOptions {
110+
ignore_no_ops: true,
111+
treat_missing_as_null: false,
112+
},
113+
)
114+
.unwrap();
97115
let patched_json = patched.to_json();
98116
let src_json = src.to_json();
99117
assert_eq!(patched_json, src_json);
@@ -135,7 +153,16 @@ fn diff_and_patch_personinfo() {
135153
assert!(tgt.navigate_path(&d.path).is_some());
136154
}
137155
}
138-
let (patched, _trace) = patch(&src, &deltas, &sv, false).unwrap();
156+
let (patched, _trace) = patch(
157+
&src,
158+
&deltas,
159+
&sv,
160+
linkml_runtime::diff::PatchOptions {
161+
ignore_no_ops: true,
162+
treat_missing_as_null: false,
163+
},
164+
)
165+
.unwrap();
139166
assert_eq!(patched.to_json(), tgt.to_json());
140167
}
141168

src/runtime/tests/diff_identifier.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,16 @@ fn single_inlined_object_identifier_change_is_replacement() {
7676
assert!(d.old.is_some() && d.new.is_some());
7777

7878
// Patch should yield target
79-
let (patched, _trace) = patch(&src, &deltas, &sv, false).unwrap();
79+
let (patched, _trace) = patch(
80+
&src,
81+
&deltas,
82+
&sv,
83+
linkml_runtime::diff::PatchOptions {
84+
ignore_no_ops: true,
85+
treat_missing_as_null: false,
86+
},
87+
)
88+
.unwrap();
8089
assert_eq!(patched.to_json(), tgt.to_json());
8190
}
8291

@@ -145,7 +154,16 @@ fn single_inlined_object_non_identifier_change_is_field_delta() {
145154
"diagnosis".to_string()
146155
]));
147156

148-
let (patched, _trace) = patch(&src, &deltas, &sv, false).unwrap();
157+
let (patched, _trace) = patch(
158+
&src,
159+
&deltas,
160+
&sv,
161+
linkml_runtime::diff::PatchOptions {
162+
ignore_no_ops: true,
163+
treat_missing_as_null: false,
164+
},
165+
)
166+
.unwrap();
149167
assert_eq!(patched.to_json(), tgt.to_json());
150168
}
151169

@@ -194,7 +212,16 @@ fn list_inlined_object_identifier_change_is_replacement() {
194212
.iter()
195213
.any(|d| d.path == vec!["objects".to_string(), "2".to_string(), "id".to_string()]));
196214

197-
let (patched, _trace) = patch(&src, &deltas, &sv, false).unwrap();
215+
let (patched, _trace) = patch(
216+
&src,
217+
&deltas,
218+
&sv,
219+
linkml_runtime::diff::PatchOptions {
220+
ignore_no_ops: true,
221+
treat_missing_as_null: false,
222+
},
223+
)
224+
.unwrap();
198225
assert_eq!(patched.to_json(), tgt.to_json());
199226
}
200227

@@ -241,6 +268,15 @@ fn mapping_inlined_identifier_change_is_add_delete() {
241268
.iter()
242269
.any(|d| d.path == vec!["things".to_string(), "alpha".to_string(), "key".to_string()]));
243270

244-
let (patched, _trace) = patch(&src, &deltas, &sv, false).unwrap();
271+
let (patched, _trace) = patch(
272+
&src,
273+
&deltas,
274+
&sv,
275+
linkml_runtime::diff::PatchOptions {
276+
ignore_no_ops: true,
277+
treat_missing_as_null: false,
278+
},
279+
)
280+
.unwrap();
245281
assert_eq!(patched.to_json(), tgt.to_json());
246282
}

src/runtime/tests/trace.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,16 @@ fn node_ids_preserved_scalar_update() {
7070
.unwrap();
7171

7272
let deltas = diff(&src, &tgt, false);
73-
let (patched, trace) = linkml_runtime::patch(&src, &deltas, &sv, false).unwrap();
73+
let (patched, trace) = linkml_runtime::patch(
74+
&src,
75+
&deltas,
76+
&sv,
77+
linkml_runtime::diff::PatchOptions {
78+
ignore_no_ops: true,
79+
treat_missing_as_null: false,
80+
},
81+
)
82+
.unwrap();
7483

7584
assert!(trace.added.is_empty());
7685
assert!(trace.deleted.is_empty());
@@ -123,7 +132,16 @@ fn patch_trace_add_in_list() {
123132
let deltas = diff(&base, &target, false);
124133
let mut pre = Vec::new();
125134
collect_ids(&base, &mut pre);
126-
let (patched, trace) = linkml_runtime::patch(&base, &deltas, &sv, false).unwrap();
135+
let (patched, trace) = linkml_runtime::patch(
136+
&base,
137+
&deltas,
138+
&sv,
139+
linkml_runtime::diff::PatchOptions {
140+
ignore_no_ops: true,
141+
treat_missing_as_null: false,
142+
},
143+
)
144+
.unwrap();
127145
let mut post = Vec::new();
128146
collect_ids(&patched, &mut post);
129147

@@ -164,7 +182,16 @@ fn patch_missing_to_null_semantics() {
164182

165183
// treat_missing_as_null = true => no-op; no trace changes, no node id changes
166184
let pre_id = src.node_id();
167-
let (patched_same, trace_same) = linkml_runtime::patch(&src, &deltas, &sv, true).unwrap();
185+
let (patched_same, trace_same) = linkml_runtime::patch(
186+
&src,
187+
&deltas,
188+
&sv,
189+
linkml_runtime::diff::PatchOptions {
190+
ignore_no_ops: true,
191+
treat_missing_as_null: true,
192+
},
193+
)
194+
.unwrap();
168195
assert!(
169196
trace_same.added.is_empty()
170197
&& trace_same.deleted.is_empty()
@@ -179,7 +206,16 @@ fn patch_missing_to_null_semantics() {
179206
}
180207

181208
// treat_missing_as_null = false => apply explicit null
182-
let (patched_null, trace_applied) = linkml_runtime::patch(&src, &deltas, &sv, false).unwrap();
209+
let (patched_null, trace_applied) = linkml_runtime::patch(
210+
&src,
211+
&deltas,
212+
&sv,
213+
linkml_runtime::diff::PatchOptions {
214+
ignore_no_ops: true,
215+
treat_missing_as_null: false,
216+
},
217+
)
218+
.unwrap();
183219
assert!(trace_applied.updated.contains(&patched_null.node_id()));
184220
// age present as Null
185221
if let LinkMLValue::Object { values, .. } = &patched_null {

0 commit comments

Comments
 (0)