deepdiff-rs 0.11.2__tar.gz → 0.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/Cargo.lock +4 -4
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/Cargo.toml +1 -1
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/PKG-INFO +51 -28
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/README.md +50 -27
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/src/lib.rs +25 -95
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/diff/dispatch.rs +49 -14
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/diff/mod.rs +6 -3
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/diff/object.rs +53 -16
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/diff/options.rs +51 -4
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/diff/tests.rs +423 -4
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/ignore_order/distance.rs +46 -7
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/ignore_order/fxhash.rs +6 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/ignore_order/hash.rs +39 -9
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/ignore_order/memo.rs +77 -7
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/ignore_order/tests.rs +487 -1
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/path.rs +53 -3
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/report.rs +96 -3
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/report_tests.rs +7 -1
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/test_support.rs +31 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/value.rs +454 -27
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/tests/golden.rs +54 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/deepdiff_rs.pyi +11 -2
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/src/convert.rs +963 -45
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/src/deepdiff.rs +78 -11
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/src/fast_path.rs +1 -1
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/src/guard.rs +20 -15
- deepdiff_rs-0.12.0/crates/onix-py/tests/test_conversions.py +1859 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_depth_guard.py +45 -3
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_differential_fuzz.py +214 -6
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/pyproject.toml +3 -1
- deepdiff_rs-0.11.2/crates/onix-py/tests/test_conversions.py +0 -720
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/Cargo.toml +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/examples/row_diff_profile.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/examples/row_diff_rss.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/examples/shared/gen_shapes.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/examples/type_stack_cost.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/src/error.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/src/json_rows.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/src/options.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/src/profile.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/src/row_diff.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/src/schema.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/src/spool.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/src/table_diff.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-arrow/tests/profile_passes.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/Cargo.toml +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/examples/stack_frame_cost.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/datetime.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/datetime_tests.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/diff/array.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/diff/scalar.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/diff/set.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/error.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/ignore_order/mod.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/ignore_order/pairing.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/lcs.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/lcs_tests.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/lib.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/unified_diff.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/unified_diff_tests.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/src/value_tests.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/tests/ignore_order_memory.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/tests/memory_footprint.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/tests/proptest_diff.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-core/tests/proptest_ignore_order.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/.python-version +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/Cargo.toml +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/benchmarks/bench_bindings.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/src/arrow.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/src/errors.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/src/lib.rs +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/conftest.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_bindings_memory.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_datetimes.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_golden_parity.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_non_finite.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_sets.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_signed_zero.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_smoke.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_stub_mypy.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_stub_signatures.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_suite_hygiene.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_table_diff.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_table_row_diff.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_timedeltas.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_times.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_tuples.py +0 -0
- {deepdiff_rs-0.11.2 → deepdiff_rs-0.12.0}/crates/onix-py/tests/test_wheel_contents.py +0 -0
|
@@ -629,7 +629,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
629
629
|
|
|
630
630
|
[[package]]
|
|
631
631
|
name = "onix-arrow"
|
|
632
|
-
version = "0.
|
|
632
|
+
version = "0.12.0"
|
|
633
633
|
dependencies = [
|
|
634
634
|
"arrow-array",
|
|
635
635
|
"arrow-buffer",
|
|
@@ -648,7 +648,7 @@ dependencies = [
|
|
|
648
648
|
|
|
649
649
|
[[package]]
|
|
650
650
|
name = "onix-cli"
|
|
651
|
-
version = "0.
|
|
651
|
+
version = "0.12.0"
|
|
652
652
|
dependencies = [
|
|
653
653
|
"onix-core",
|
|
654
654
|
"serde_json",
|
|
@@ -656,7 +656,7 @@ dependencies = [
|
|
|
656
656
|
|
|
657
657
|
[[package]]
|
|
658
658
|
name = "onix-core"
|
|
659
|
-
version = "0.
|
|
659
|
+
version = "0.12.0"
|
|
660
660
|
dependencies = [
|
|
661
661
|
"num-bigint",
|
|
662
662
|
"num-traits",
|
|
@@ -669,7 +669,7 @@ dependencies = [
|
|
|
669
669
|
|
|
670
670
|
[[package]]
|
|
671
671
|
name = "onix-py"
|
|
672
|
-
version = "0.
|
|
672
|
+
version = "0.12.0"
|
|
673
673
|
dependencies = [
|
|
674
674
|
"arrow-array",
|
|
675
675
|
"arrow-schema",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepdiff-rs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Rust
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -91,6 +91,24 @@ if diff:
|
|
|
91
91
|
{'values_changed': {"root['a']": {'new_value': 2, 'old_value': 1}}}
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
A custom object diffs by its attributes:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from dataclasses import dataclass
|
|
98
|
+
from deepdiff_rs import DeepDiff
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class Point:
|
|
102
|
+
x: int
|
|
103
|
+
y: int
|
|
104
|
+
|
|
105
|
+
print(DeepDiff(Point(1, 2), Point(1, 3)).to_json())
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
{"values_changed":{"root.y":{"new_value":3,"old_value":2}}}
|
|
110
|
+
```
|
|
111
|
+
|
|
94
112
|
`diff_json`, the fast path when you already have JSON text (it parses, diffs, and serializes entirely in Rust, with no Python-object conversion):
|
|
95
113
|
|
|
96
114
|
```python
|
|
@@ -151,31 +169,31 @@ Beyond a join-based diff, `diff_tables` reports duplicate and null keys rather t
|
|
|
151
169
|
|
|
152
170
|
Two committed, regenerable reports back the numbers below; every figure here is copied verbatim from them.
|
|
153
171
|
|
|
154
|
-
The Python bindings against real `deepdiff` on **live Python objects**, the number a real caller pays (source: [`crates/onix-py/benchmarks/bench_bindings.py`](crates/onix-py/benchmarks/bench_bindings.py), macOS 26.5.1, Apple M5 Max, median of 11 isolated subprocess runs per side, run on 2026-09-
|
|
172
|
+
The Python bindings against real `deepdiff` on **live Python objects**, the number a real caller pays (source: [`crates/onix-py/benchmarks/bench_bindings.py`](crates/onix-py/benchmarks/bench_bindings.py), macOS 26.5.1, Apple M5 Max, median of 11 isolated subprocess runs per side, run on 2026-09-23):
|
|
155
173
|
|
|
156
174
|
| Shape | deepdiff | deepdiff_rs | Speedup |
|
|
157
175
|
| --- | --- | --- | --- |
|
|
158
|
-
| `ignore_order`, 10k shuffled ints, ~5% mutated (live objects) |
|
|
159
|
-
| peak RSS |
|
|
160
|
-
| CPU seconds |
|
|
161
|
-
| Heterogeneous API-payload records, n=20,000 (live objects) |
|
|
162
|
-
| peak RSS |
|
|
163
|
-
| CPU seconds | 3.
|
|
164
|
-
| Typed records (datetime/tuple/set fields), n=10,000 (live objects) | 795.
|
|
165
|
-
| peak RSS |
|
|
166
|
-
| CPU seconds | 0.
|
|
167
|
-
| Same typed-records shape, `ignore_order` (live objects) |
|
|
168
|
-
| peak RSS |
|
|
169
|
-
| CPU seconds | 60.
|
|
170
|
-
| Same `ignore_order` shape, via `diff_json` (JSON-string path) |
|
|
171
|
-
| peak RSS |
|
|
172
|
-
| CPU seconds |
|
|
173
|
-
| Same API-payload shape, via `diff_json` (JSON-string path) |
|
|
174
|
-
| peak RSS |
|
|
175
|
-
| CPU seconds | 4.
|
|
176
|
-
| Same API-payload shape, both tools reading two JSON files from disk |
|
|
177
|
-
| peak RSS |
|
|
178
|
-
| CPU seconds | 4.
|
|
176
|
+
| `ignore_order`, 10k shuffled ints, ~5% mutated (live objects) | 699.68ms | 66.23ms | **10.56x** |
|
|
177
|
+
| peak RSS | 401.8 MB | 203.4 MB | **1.98x** |
|
|
178
|
+
| CPU seconds | 0.699 s | 0.066 s | **10.56x** |
|
|
179
|
+
| Heterogeneous API-payload records, n=20,000 (live objects) | 3486.50ms | 141.42ms | **24.65x** |
|
|
180
|
+
| peak RSS | 228.1 MB | 274.6 MB | **0.83x** |
|
|
181
|
+
| CPU seconds | 3.484 s | 0.141 s | **24.64x** |
|
|
182
|
+
| Typed records (datetime/tuple/set fields), n=10,000 (live objects) | 795.03ms | 46.45ms | **17.11x** |
|
|
183
|
+
| peak RSS | 169.8 MB | 175.6 MB | **0.97x** |
|
|
184
|
+
| CPU seconds | 0.794 s | 0.046 s | **17.11x** |
|
|
185
|
+
| Same typed-records shape, `ignore_order` (live objects) | 60834.60ms | 864.82ms | **70.34x** |
|
|
186
|
+
| peak RSS | 225.6 MB | 257.6 MB | **0.88x** |
|
|
187
|
+
| CPU seconds | 60.786 s | 0.863 s | **70.47x** |
|
|
188
|
+
| Same `ignore_order` shape, via `diff_json` (JSON-string path) | 731.31ms | 74.54ms | **9.81x** |
|
|
189
|
+
| peak RSS | 402.3 MB | 203.9 MB | **1.97x** |
|
|
190
|
+
| CPU seconds | 0.730 s | 0.074 s | **9.81x** |
|
|
191
|
+
| Same API-payload shape, via `diff_json` (JSON-string path) | 5125.41ms | 95.33ms | **53.76x** |
|
|
192
|
+
| peak RSS | 249.1 MB | 269.7 MB | **0.92x** |
|
|
193
|
+
| CPU seconds | 4.971 s | 0.095 s | **52.22x** |
|
|
194
|
+
| Same API-payload shape, both tools reading two JSON files from disk | 4774.33ms | 97.33ms | **49.05x** |
|
|
195
|
+
| peak RSS | 249.0 MB | 269.7 MB | **0.92x** |
|
|
196
|
+
| CPU seconds | 4.771 s | 0.097 s | **49.12x** |
|
|
179
197
|
|
|
180
198
|
The engine's own diff-only time and peak resident memory against pinned `deepdiff` 9.1.0 (source: [`perf/RESULTS.md`](perf/RESULTS.md), same machine, median over tier-appropriate runs, diff time excluding process startup and JSON parsing on both sides):
|
|
181
199
|
|
|
@@ -198,7 +216,7 @@ Both reports carry their full methodology, fairness rules, and the reproduce com
|
|
|
198
216
|
|
|
199
217
|
**Python API.** The public surface is `DeepDiff`, `diff_json`, `diff_tables` (returning a `TableDiff`), `MaxDepthError`, `MAX_DEPTH_CEILING`, and `__version__` (a `str` matching the installed `deepdiff-rs` distribution version).
|
|
200
218
|
|
|
201
|
-
- `DeepDiff(t1, t2, ignore_order=False, max_depth=None)`: diffs two live Python objects of supported value types — `None`, `bool`, `int`, `float`, `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta
|
|
219
|
+
- `DeepDiff(t1, t2, ignore_order=False, max_depth=None)`: diffs two live Python objects of supported value types — `None`, `bool`, `int`, `float`, `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta`, plus any custom object (diffed by its attributes; see [Known limitations](#known-limitations) for the exact restrictions and exclusions, including which types a `dict` key may be, and the types that raise `TypeError` instead); `.to_json()` returns the DeepDiff-compatible JSON string, `.to_dict()` the same report as a dict — with Python types preserved, so a value the diff found in a `tuple`, `set` or `frozenset` comes back as one and a `datetime`/`date`/`time`/`timedelta` comes back as a real one of those (a custom object, though, comes back as a plain `dict` of its attributes, since onix cannot reconstruct the instance — see [Known limitations](#known-limitations)) — and the instance is falsy when there is no difference. The `set_item_added`/`set_item_removed` categories are lists of path strings, each ending in the item itself (`root['a'][2]`, `root['x']`, `root[(1, 2)]`).
|
|
202
220
|
- `diff_json(a, b, ignore_order=False, max_depth=None) -> str`: diffs two JSON strings entirely in Rust and returns the report as a JSON string.
|
|
203
221
|
- `MaxDepthError` (a `ValueError` subclass) is raised when input exceeds `max_depth`; `MAX_DEPTH_CEILING` (20,000) is the hard upper bound on `max_depth`.
|
|
204
222
|
- `diff_tables(left, right, key=[...], threads=None) -> TableDiff`: diffs two Arrow tables (see [Diffing tables](#diffing-tables)). `threads` sets the row diff's worker count — `None` uses the machine's available parallelism, `1` runs single-threaded, and a value below 1 or above 1024 raises `ValueError` (the diff spawns one worker per thread); the result is byte-identical at any value. `TableDiff.schema` is the list of changed columns, `.schema_arrow` the same as an Arrow table, `.summary()` the schema and row change counts, `.to_json()` the full diff (schema, summary, and every row-level member, capped at 10,000 embedded rows); `.rows_added()`, `.rows_removed()`, `.cells_changed()`, and `.duplicate_keys()` return Arrow tables.
|
|
@@ -233,11 +251,12 @@ perf/ # cross-language benchmark harness and RESULTS.md
|
|
|
233
251
|
## Known limitations
|
|
234
252
|
|
|
235
253
|
- Only the core diff is implemented: `exclude_paths`, `significant_digits`, custom operators, `verbose_level != 2`, and delta/patch are not (yet) supported.
|
|
236
|
-
- Supported value types are `None`, `bool`, `int`, `float` (`NaN`/`Infinity`/`-Infinity` included), `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta`; a `set`/`frozenset` member may be any of these except a `list`, `dict` or `set`, matching Python's own hashability rule, transitively through whatever the member nests, and a `dict` key may be `str`, `None`, `bool`, `int`, `float`, `datetime.datetime`, `datetime.date`, or a `tuple` of those (never nested), or a `tuple`/`datetime`/`date` subclass key (`namedtuple` included), accepted and matched against its base-type value the same way — see [Known limitations](#known-limitations)'s subclass bullet. An `int` of any magnitude converts and keeps its exact value (an arbitrary-precision integer beyond `i64`/`u64` is compared, ordered, hashed and rendered by its full digits), while a custom object
|
|
254
|
+
- Supported value types are `None`, `bool`, `int`, `float` (`NaN`/`Infinity`/`-Infinity` included), `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta`; a `set`/`frozenset` member may be any of these except a `list`, `dict` or `set`, matching Python's own hashability rule, transitively through whatever the member nests, and a `dict` key may be `str`, `None`, `bool`, `int`, `float`, `datetime.datetime`, `datetime.date`, or a `tuple` of those (never nested), or a `tuple`/`datetime`/`date` subclass key (`namedtuple` included), accepted and matched against its base-type value the same way — see [Known limitations](#known-limitations)'s subclass bullet. An `int` of any magnitude converts and keeps its exact value (an arbitrary-precision integer beyond `i64`/`u64` is compared, ordered, hashed and rendered by its full digits), while a custom object is diffed by its attributes (see the **Custom objects** bullet below). Under `ignore_order`, comparing an integer beyond `f64::MAX` (about `2**1024`) reports the change deterministically where real DeepDiff raises `OverflowError` (its distance function calls `float()` on it unguarded), a documented crash-class divergence like the datetime one below (see [`tests/golden/README.md`](tests/golden/README.md)). Reading JSON *text* (`diff_json` and the CLI, not the Python-object path), an integer beyond `i64`/`u64` parses as the nearest `float`, so two documents whose integers differ only past that range compare **equal** and diff to `{}` — a limitation of the JSON number reader, not the value model, tracked in [#92](https://github.com/ksco92/onix/issues/92). A non-`str` key's path renders via Python's own `repr()`, except a `tuple` key, which splits into one bracket group per element (`root[1][2]` for `(1, 2)`, never `root[(1, 2)]`, with one deliberate exception for a real DeepDiff bug on the empty tuple, and another on a non-finite-float key — see [`tests/golden/README.md`](tests/golden/README.md)). A nested dict value's own `bool`/`None`/`int`/`float` key stringifies the way `json.dumps` does, but its `datetime`/`date`/`tuple` key renders that same `repr()` text where DeepDiff's own `to_json()` raises `TypeError` on one — a superset, not a difference in the findings (see [`tests/golden/README.md`](tests/golden/README.md)). `1`/`1.0`/`True` match as the same key between two dicts (Python `dict`/`set` equality). The **Datetimes**, **Sets** and **Non-finite floats** bullets below cover the deliberate divergences for those types. See [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs) and [`tests/golden/README.md`](tests/golden/README.md).
|
|
255
|
+
- **Custom objects** diff by their attributes, matching DeepDiff's `_diff_obj` (`attribute_added`/`attribute_removed`, `root.attr` paths, `type_changes` between two different class objects), and an `Enum` member by its `name` and `value`, matching `_diff_enum`. A type DeepDiff routes to a handler onix lacks — `bytes`, `bytearray`, a generator or any other iterable, `complex`, `Decimal`, `Fraction`, a `numpy` scalar, `uuid`, `ipaddress`, a class object, a module, or a bare `object()` — raises `TypeError` at the root; below it, such a value is equal only to the same object, as DeepDiff's `t1 is t2` check makes it, and raises `TypeError` naming its path wherever the report would have to show it. Two identical Python objects at one position are never walked, a class attribute converts only when a report compares it with a shadowing value, and a class attribute is left out of a whole object in a report, as in DeepDiff (see [`tests/golden/README.md`](tests/golden/README.md#types-deepdiff-routes-to-a-handler-onix-lacks)). A `pydantic` model raises `TypeError` where DeepDiff diffs it (see [`tests/golden/README.md`](tests/golden/README.md#pydantic-models)). A custom non-`dict` `Mapping`, and a `re.Pattern` pair whose named groups differ, raise `TypeError` where DeepDiff diffs them (see [`tests/golden/README.md`](tests/golden/README.md#refused-mappings)). A `@property` that raises `AttributeError`, or an unset slot beside a `__dict__`, raises `TypeError` where DeepDiff reports the object as `unprocessed` (see [`tests/golden/README.md`](tests/golden/README.md#a-property-that-raises)). A child that points back at an object on its path reports nothing when it is on the first side; on the second side only it is compared as the object it points back at, as DeepDiff's `parents_ids` tracks only the first side's ancestry (see [`tests/golden/README.md`](tests/golden/README.md#a-recursive-object)). Three attribute-view divergences (`ignore_order` hashing, and the whole-object values in `to_json()` and `to_dict()`) are tracked in [#99](https://github.com/ksco92/onix/issues/99), see [`tests/golden/README.md`](tests/golden/README.md#custom-objects-where-onix-is-deliberately-different).
|
|
237
256
|
- A subclass of a supported `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, or `datetime.timedelta` (including `namedtuple` and pandas' `Timestamp`) converts and compares as its base type but carries its own class name into a `type_changes` entry, matching DeepDiff's `type(obj).__name__` reporting, with three exceptions: a calendar-type (`datetime`/`date`/`time`/`timedelta`) subclass held as a `set`/`frozenset` member compares as its base with no `type_changes`, matching DeepDiff, since set membership has no pairwise comparison to report one against; a `tuple`/`frozenset` subclass, including a `namedtuple`, is not accepted as a `set`/`frozenset` member at all and raises `TypeError` where DeepDiff accepts and compares it by value (a documented divergence); and `namedtuple` diffs positionally rather than by field (also documented). A `type_changes` entry's `old_type`/`new_type` are type *names* in `to_dict()`, where DeepDiff returns the type objects. See [`tests/golden/README.md`](tests/golden/README.md).
|
|
238
257
|
- **Datetimes** compare by instant, with a naive value read as UTC, matching DeepDiff. A changed pair is reported normalized to UTC (`to_json()` renders `...+00:00`, `to_dict()` returns UTC-aware `datetime`s); everywhere else a datetime keeps its raw value. Three deliberate departures: `to_json()` renders a `date` as `YYYY-MM-DD` where DeepDiff's own `to_json()` raises `TypeError` (a documented superset); a `zoneinfo`/`pytz` tzinfo comes back from `to_dict()` as a fixed-offset `datetime.timezone` carrying the offset it was in force at, not the original zone object; and a set holding both a naive and an aware value at one instant reports both as members, where DeepDiff's own digest cache can report only one (see [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs)). Comparing two datetimes whose UTC form would leave year 1..=9999 raises `ValueError` naming the path, where DeepDiff raises `OverflowError`; under `ignore_order` DeepDiff's hasher normalizes every datetime and so raises for such a value even when it is only added, removed, or shuffled, where onix hashes by instant and reports it normally (see [`tests/golden/README.md`](tests/golden/README.md)). `truncate_datetime` is not supported; the normalized-versus-raw split is documented there too. A `time`/`timedelta`, unlike a datetime, is never normalized for report (DeepDiff compares `time`/`date`/`timedelta` with a plain `!=`), and a naive `time` is never equal to an aware one; `to_json()` renders a `time` as `time.isoformat()`'s bytes and a `timedelta` as `str(timedelta)`'s, both supersets. Under `ignore_order`, `DeepHash` hashes a `time` by whole seconds-of-day only — dropping the microsecond and any offset, a confirmed upstream quirk — while a `timedelta` hashes exactly; see [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
239
258
|
- **Sets** are diffed deterministically, where DeepDiff's own answers depend on the order the running process happens to iterate a set in (hash order, and `PYTHONHASHSEED`-dependent for `str` members) or on how its digest cache/computation handles a tuple, frozenset, or calendar member independently of Python's own `==`. Each consequence — entry order, which member of an equality class is reported, set-versus-sequence coercion, and a tuple/frozenset member's own (positional, not order-/repetition-insensitive) matching rule — is shown with both tools' output in [`tests/golden/README.md`](tests/golden/README.md)'s "Set iteration order" section. A report holding a `frozenset` value also serializes to JSON here, where DeepDiff's own `to_json()` raises `TypeError` — a superset, not a difference in the findings.
|
|
240
|
-
- **Non-finite floats** (`NaN`, `Infinity`, `-Infinity`) compare and hash like real Python: two `NaN`s are never equal, `Infinity == Infinity`, `to_json()` renders the same bare `NaN`/`Infinity`/`-Infinity` tokens Python's `json.dumps` does, and `ignore_order` matching treats every `NaN` as one shared item, matching `DeepHash`. The one divergence, always deterministic: this crate's value model carries
|
|
259
|
+
- **Non-finite floats** (`NaN`, `Infinity`, `-Infinity`) compare and hash like real Python: two `NaN`s are never equal, `Infinity == Infinity`, `to_json()` renders the same bare `NaN`/`Infinity`/`-Infinity` tokens Python's `json.dumps` does, and `ignore_order` matching treats every `NaN` as one shared item, matching `DeepHash`. The one divergence, always deterministic: this crate's value model carries Python object identity for custom objects only, so two `NaN`s outside one shared custom object always compare unequal here, where DeepDiff sometimes reports no difference (`t1 is t2`) or lets one collapse into another (a `set` member, an ordered-list match) when the two objects, or their containers, happen to be the same one. See [`tests/golden/README.md`](tests/golden/README.md)'s "Non-finite floats" section.
|
|
241
260
|
- A `str` (or `dict` key) containing a lone (unpaired) surrogate code point (e.g. `'\udc80'`, legal in Python but not encodable as UTF-8) is compared and reported exactly like any other `str`, matching DeepDiff's plain `==`; `to_json()` renders it with `json.dumps`'s own single-backslash `\uXXXX` escape. The one accepted divergence, always deterministic: hashing one — a `set`/`frozenset` member, or *any* value at all once `ignore_order=True` (`DeepHash` hashes every value there, not just a set's members) — crashes real DeepDiff with an unhandled `UnicodeEncodeError`, where onix hashes by code point and reports normally. See [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
242
261
|
- A `str` inside a `tuple` or `frozenset` set item is escaped exactly as Python's `repr()` escapes it, against Unicode 16.0.0; on a Python older than 3.14 (an older `unicodedata` table), a code point assigned to Unicode after that Python's own version is escaped by DeepDiff and rendered literally by onix. See [`tests/golden/README.md`](tests/golden/README.md)'s "Pinned versions" section.
|
|
243
262
|
- Adversarially deep input raises `MaxDepthError` instead of crashing: the default `max_depth` is 512 and the hard ceiling is `MAX_DEPTH_CEILING` (20,000). See [`crates/onix-py/src/guard.rs`](crates/onix-py/src/guard.rs).
|
|
@@ -247,8 +266,12 @@ perf/ # cross-language benchmark harness and RESULTS.md
|
|
|
247
266
|
- `diff_tables` inherits some Arrow-interchange quirks: a column name with an embedded NUL byte (`\0`) arrives truncated at the NUL through the C Data Interface (the report shows the truncated name; rare in practice); a list of structs named exactly `key`/`value` with a nullable key is not distinguished from a real map, so a migration between the two is not reported as a type change (polars exports both as the same Arrow type — see `map_entries` in [`crates/onix-arrow/src/schema.rs`](crates/onix-arrow/src/schema.rs)); and a polars all-null (`Null`-typed) column fails at Arrow C import with a `ValueError` (`the datatype "Null" doesn't expect buffer at index 0`), so give such a column a concrete type first (a pyarrow all-`None` column, inferred as `null`, works and compares as all-null).
|
|
248
267
|
- In `diff_tables`, DuckDB labels a `TIMESTAMP WITH TIME ZONE` column with the connection's *session* time zone when it exports to Arrow (a UTC session as `Timestamp(µs, "UTC")`, an `America/New_York` session as `Timestamp(µs, "America/New_York")`), so on a non-UTC machine such a column can be reported as a type change against a UTC column from another library. Run `SET TimeZone='UTC'` on the DuckDB connection first for a deterministic, machine-independent result.
|
|
249
268
|
- `diff_tables` refuses a column whose Arrow type is nested deeper than `MAX_NESTING_DEPTH` (128) with a `MaxDepthError`, because comparing arbitrarily deep nesting would overflow the native stack; 128 is far beyond any real schema. Importing a schema nested many thousands of levels deep is also slow regardless, a cost of the Arrow C Data Interface itself.
|
|
250
|
-
- `diff_tables`
|
|
251
|
-
-
|
|
269
|
+
- `diff_tables` row diff: per-row hash (32 B/row/side): 75 MB at 1M, 660 MB at 10M, linear in row count; wall time `N·log N` across workers. Parallel overhead ~10-15 B/row/side: 629/857 MB at 8M (1/18 threads), 2.9/3.6 GB at 37M; `threads=1` sequential. Duplicate-key report (every distinct duplicated key): 37 MB (16 B keys) to 1.05 GB (1 KB keys) at 200k (100k distinct), 165 MB at 1M (500k distinct, 16 B keys). Cell-pass resident (spilled rows-x-column-width, both sides + 2x `cells_changed` output + hash vectors) at 18 threads: 0.92/1.31/4.81 GB at 100k/200k/1M output-dominated (single-threaded 8.30 GB); eight 512 B columns, one differing (150k changed cells, ~160 MB output): 2.09 GB at 150k spill-dominated vs 0.84 GB one column; 0.38 GB at 1M two int64.
|
|
270
|
+
- Temp disk, same call: each input is spooled to an anonymous file (`tempfile`: unlinked at once, mode 0600, no name, nothing left on abnormal exit). The cell pass spills both sides' changed rows to anonymous per-partition files: byte-view columns cast to a large-offset type (buffers can exceed i32's ~2 GB), dictionaries decoded to value type, keeping the spill compact, independent of partition count (~97 GB at 64 threads undecoded vs ~34 GB decoded, wide pair). Spool and spill resident together, whole-process peaks (34-column fixture, 16.875M rows/side): ~33 GB at 18 threads, 45 GB at 2, 34 GB at 64, down from ~67 GB pre-streaming (Linux spill may be a RAM-backed `tmpfs`). A full temp filesystem raises `ValueError` naming `TMPDIR`.
|
|
271
|
+
- The size gate peeks up to 50,000 rows or 64 MB decoded per side, holding one whole producer batch per side beyond it: a 49,999-row/side, 8 KB-cell, 100-row-batch pair peaks ~78 MB at 18 threads, 14 MB single-threaded, ~1.6 GB for one whole-side batch.
|
|
272
|
+
- None of the row-diff memory, temp-disk or size-gate-peek costs above has a built-in cap. Bound: row count; changed fraction; column widths (key-column width, for duplicate-heavy data; for the cell pass, the total width of all common value columns, since every one spills in full per changed row whether it changed or not, plus the rendered changed cells; any column's width for the size-gate peek, which buffers decoded cells whether or not they change); the producer's batch size; thread count, lowest at 18 threads (measured default), higher at 2 threads (two partitions each holding half the rows), slightly higher at 64: spill-dominated 3.03 GB at 2 threads, 2.09 GB at 64; output-dominated at 200k rows, 1.98 GB at 2, 1.49 GB at 64.
|
|
273
|
+
- Figures are the peak resident set of `cargo run -p onix-arrow --release --example row_diff_rss` (worker count from `ROW_DIFF_THREADS`, rows per batch from `ROW_DIFF_BATCH`), single run, macOS on an Apple M-series laptop, 2026-09-06, same method as [Performance](#performance); the single-threaded hash-term figures carry run-to-run slack up to 2.9-4.2 GB at 37M rows/side, and the two spill/output cell-pass shapes are medians over 5 runs. See [`perf/arrow/RESULTS.md`](perf/arrow/RESULTS.md) for the size-gate peek table and the decode-vs-undecoded partition figures, and [`crates/onix-arrow/src/row_diff.rs`](crates/onix-arrow/src/row_diff.rs) for the implementation.
|
|
274
|
+
- `TableDiff.to_json()` is the one member with a built-in cap: it embeds `rows_added`, `rows_removed`, `cells_changed`, and `duplicate_keys` in full, one JSON object per row, so it refuses with `ValueError` — naming the row count and the cap — once those four together hold more than 10,000 rows. The cap bounds row count only, the same way the first row-diff bullet above states its per-cell term as changed cells times cell width, not column count or cell width — so a table under the row cap but with wide or large cells can still be large; use the Arrow-returning accessors instead. See [`crates/onix-arrow/src/json_rows.rs`](crates/onix-arrow/src/json_rows.rs).
|
|
252
275
|
- `diff_tables` compares scalar columns by value (hashed: null, booleans, every integer, float and decimal width, strings and binary in every encoding, timestamps, dates, times, durations, intervals, and dictionaries of these; refused with `ValueError`: run-end encoded columns and any type-and-unit combination Arrow itself cannot build; nested non-key columns skipped, nested key columns refused), with the exact enumeration in the module doc of [`crates/onix-arrow/src/row_diff.rs`](crates/onix-arrow/src/row_diff.rs). It also refuses a key column whose type differs across the two inputs after encoding normalization (the conservative choice: a primary key that changed type is refused rather than guessed, not coerced). A row whose only difference is a lossless type change — an `Int32` widened to `Int64`, a timestamp unit change at the same instant — hashes equal on both sides, so it is in neither `rows_changed` nor `cells_changed`; the column's type change is still reported in `schema`.
|
|
253
276
|
- Output is byte-identical to DeepDiff except for the cases listed above and two path-rendering quirks; [`tests/golden/README.md`](tests/golden/README.md) enumerates every accepted exception, including integers past `2^53` (the limit of exact `f64` representation) inside ordered scalar lists and `ignore_order` pairing among naive datetimes, which DeepDiff ranks using the *process's local timezone* while onix reads a naive value as UTC everywhere.
|
|
254
277
|
|
|
@@ -73,6 +73,24 @@ if diff:
|
|
|
73
73
|
{'values_changed': {"root['a']": {'new_value': 2, 'old_value': 1}}}
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
A custom object diffs by its attributes:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from dataclasses import dataclass
|
|
80
|
+
from deepdiff_rs import DeepDiff
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class Point:
|
|
84
|
+
x: int
|
|
85
|
+
y: int
|
|
86
|
+
|
|
87
|
+
print(DeepDiff(Point(1, 2), Point(1, 3)).to_json())
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
{"values_changed":{"root.y":{"new_value":3,"old_value":2}}}
|
|
92
|
+
```
|
|
93
|
+
|
|
76
94
|
`diff_json`, the fast path when you already have JSON text (it parses, diffs, and serializes entirely in Rust, with no Python-object conversion):
|
|
77
95
|
|
|
78
96
|
```python
|
|
@@ -133,31 +151,31 @@ Beyond a join-based diff, `diff_tables` reports duplicate and null keys rather t
|
|
|
133
151
|
|
|
134
152
|
Two committed, regenerable reports back the numbers below; every figure here is copied verbatim from them.
|
|
135
153
|
|
|
136
|
-
The Python bindings against real `deepdiff` on **live Python objects**, the number a real caller pays (source: [`crates/onix-py/benchmarks/bench_bindings.py`](crates/onix-py/benchmarks/bench_bindings.py), macOS 26.5.1, Apple M5 Max, median of 11 isolated subprocess runs per side, run on 2026-09-
|
|
154
|
+
The Python bindings against real `deepdiff` on **live Python objects**, the number a real caller pays (source: [`crates/onix-py/benchmarks/bench_bindings.py`](crates/onix-py/benchmarks/bench_bindings.py), macOS 26.5.1, Apple M5 Max, median of 11 isolated subprocess runs per side, run on 2026-09-23):
|
|
137
155
|
|
|
138
156
|
| Shape | deepdiff | deepdiff_rs | Speedup |
|
|
139
157
|
| --- | --- | --- | --- |
|
|
140
|
-
| `ignore_order`, 10k shuffled ints, ~5% mutated (live objects) |
|
|
141
|
-
| peak RSS |
|
|
142
|
-
| CPU seconds |
|
|
143
|
-
| Heterogeneous API-payload records, n=20,000 (live objects) |
|
|
144
|
-
| peak RSS |
|
|
145
|
-
| CPU seconds | 3.
|
|
146
|
-
| Typed records (datetime/tuple/set fields), n=10,000 (live objects) | 795.
|
|
147
|
-
| peak RSS |
|
|
148
|
-
| CPU seconds | 0.
|
|
149
|
-
| Same typed-records shape, `ignore_order` (live objects) |
|
|
150
|
-
| peak RSS |
|
|
151
|
-
| CPU seconds | 60.
|
|
152
|
-
| Same `ignore_order` shape, via `diff_json` (JSON-string path) |
|
|
153
|
-
| peak RSS |
|
|
154
|
-
| CPU seconds |
|
|
155
|
-
| Same API-payload shape, via `diff_json` (JSON-string path) |
|
|
156
|
-
| peak RSS |
|
|
157
|
-
| CPU seconds | 4.
|
|
158
|
-
| Same API-payload shape, both tools reading two JSON files from disk |
|
|
159
|
-
| peak RSS |
|
|
160
|
-
| CPU seconds | 4.
|
|
158
|
+
| `ignore_order`, 10k shuffled ints, ~5% mutated (live objects) | 699.68ms | 66.23ms | **10.56x** |
|
|
159
|
+
| peak RSS | 401.8 MB | 203.4 MB | **1.98x** |
|
|
160
|
+
| CPU seconds | 0.699 s | 0.066 s | **10.56x** |
|
|
161
|
+
| Heterogeneous API-payload records, n=20,000 (live objects) | 3486.50ms | 141.42ms | **24.65x** |
|
|
162
|
+
| peak RSS | 228.1 MB | 274.6 MB | **0.83x** |
|
|
163
|
+
| CPU seconds | 3.484 s | 0.141 s | **24.64x** |
|
|
164
|
+
| Typed records (datetime/tuple/set fields), n=10,000 (live objects) | 795.03ms | 46.45ms | **17.11x** |
|
|
165
|
+
| peak RSS | 169.8 MB | 175.6 MB | **0.97x** |
|
|
166
|
+
| CPU seconds | 0.794 s | 0.046 s | **17.11x** |
|
|
167
|
+
| Same typed-records shape, `ignore_order` (live objects) | 60834.60ms | 864.82ms | **70.34x** |
|
|
168
|
+
| peak RSS | 225.6 MB | 257.6 MB | **0.88x** |
|
|
169
|
+
| CPU seconds | 60.786 s | 0.863 s | **70.47x** |
|
|
170
|
+
| Same `ignore_order` shape, via `diff_json` (JSON-string path) | 731.31ms | 74.54ms | **9.81x** |
|
|
171
|
+
| peak RSS | 402.3 MB | 203.9 MB | **1.97x** |
|
|
172
|
+
| CPU seconds | 0.730 s | 0.074 s | **9.81x** |
|
|
173
|
+
| Same API-payload shape, via `diff_json` (JSON-string path) | 5125.41ms | 95.33ms | **53.76x** |
|
|
174
|
+
| peak RSS | 249.1 MB | 269.7 MB | **0.92x** |
|
|
175
|
+
| CPU seconds | 4.971 s | 0.095 s | **52.22x** |
|
|
176
|
+
| Same API-payload shape, both tools reading two JSON files from disk | 4774.33ms | 97.33ms | **49.05x** |
|
|
177
|
+
| peak RSS | 249.0 MB | 269.7 MB | **0.92x** |
|
|
178
|
+
| CPU seconds | 4.771 s | 0.097 s | **49.12x** |
|
|
161
179
|
|
|
162
180
|
The engine's own diff-only time and peak resident memory against pinned `deepdiff` 9.1.0 (source: [`perf/RESULTS.md`](perf/RESULTS.md), same machine, median over tier-appropriate runs, diff time excluding process startup and JSON parsing on both sides):
|
|
163
181
|
|
|
@@ -180,7 +198,7 @@ Both reports carry their full methodology, fairness rules, and the reproduce com
|
|
|
180
198
|
|
|
181
199
|
**Python API.** The public surface is `DeepDiff`, `diff_json`, `diff_tables` (returning a `TableDiff`), `MaxDepthError`, `MAX_DEPTH_CEILING`, and `__version__` (a `str` matching the installed `deepdiff-rs` distribution version).
|
|
182
200
|
|
|
183
|
-
- `DeepDiff(t1, t2, ignore_order=False, max_depth=None)`: diffs two live Python objects of supported value types — `None`, `bool`, `int`, `float`, `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta
|
|
201
|
+
- `DeepDiff(t1, t2, ignore_order=False, max_depth=None)`: diffs two live Python objects of supported value types — `None`, `bool`, `int`, `float`, `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta`, plus any custom object (diffed by its attributes; see [Known limitations](#known-limitations) for the exact restrictions and exclusions, including which types a `dict` key may be, and the types that raise `TypeError` instead); `.to_json()` returns the DeepDiff-compatible JSON string, `.to_dict()` the same report as a dict — with Python types preserved, so a value the diff found in a `tuple`, `set` or `frozenset` comes back as one and a `datetime`/`date`/`time`/`timedelta` comes back as a real one of those (a custom object, though, comes back as a plain `dict` of its attributes, since onix cannot reconstruct the instance — see [Known limitations](#known-limitations)) — and the instance is falsy when there is no difference. The `set_item_added`/`set_item_removed` categories are lists of path strings, each ending in the item itself (`root['a'][2]`, `root['x']`, `root[(1, 2)]`).
|
|
184
202
|
- `diff_json(a, b, ignore_order=False, max_depth=None) -> str`: diffs two JSON strings entirely in Rust and returns the report as a JSON string.
|
|
185
203
|
- `MaxDepthError` (a `ValueError` subclass) is raised when input exceeds `max_depth`; `MAX_DEPTH_CEILING` (20,000) is the hard upper bound on `max_depth`.
|
|
186
204
|
- `diff_tables(left, right, key=[...], threads=None) -> TableDiff`: diffs two Arrow tables (see [Diffing tables](#diffing-tables)). `threads` sets the row diff's worker count — `None` uses the machine's available parallelism, `1` runs single-threaded, and a value below 1 or above 1024 raises `ValueError` (the diff spawns one worker per thread); the result is byte-identical at any value. `TableDiff.schema` is the list of changed columns, `.schema_arrow` the same as an Arrow table, `.summary()` the schema and row change counts, `.to_json()` the full diff (schema, summary, and every row-level member, capped at 10,000 embedded rows); `.rows_added()`, `.rows_removed()`, `.cells_changed()`, and `.duplicate_keys()` return Arrow tables.
|
|
@@ -215,11 +233,12 @@ perf/ # cross-language benchmark harness and RESULTS.md
|
|
|
215
233
|
## Known limitations
|
|
216
234
|
|
|
217
235
|
- Only the core diff is implemented: `exclude_paths`, `significant_digits`, custom operators, `verbose_level != 2`, and delta/patch are not (yet) supported.
|
|
218
|
-
- Supported value types are `None`, `bool`, `int`, `float` (`NaN`/`Infinity`/`-Infinity` included), `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta`; a `set`/`frozenset` member may be any of these except a `list`, `dict` or `set`, matching Python's own hashability rule, transitively through whatever the member nests, and a `dict` key may be `str`, `None`, `bool`, `int`, `float`, `datetime.datetime`, `datetime.date`, or a `tuple` of those (never nested), or a `tuple`/`datetime`/`date` subclass key (`namedtuple` included), accepted and matched against its base-type value the same way — see [Known limitations](#known-limitations)'s subclass bullet. An `int` of any magnitude converts and keeps its exact value (an arbitrary-precision integer beyond `i64`/`u64` is compared, ordered, hashed and rendered by its full digits), while a custom object
|
|
236
|
+
- Supported value types are `None`, `bool`, `int`, `float` (`NaN`/`Infinity`/`-Infinity` included), `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta`; a `set`/`frozenset` member may be any of these except a `list`, `dict` or `set`, matching Python's own hashability rule, transitively through whatever the member nests, and a `dict` key may be `str`, `None`, `bool`, `int`, `float`, `datetime.datetime`, `datetime.date`, or a `tuple` of those (never nested), or a `tuple`/`datetime`/`date` subclass key (`namedtuple` included), accepted and matched against its base-type value the same way — see [Known limitations](#known-limitations)'s subclass bullet. An `int` of any magnitude converts and keeps its exact value (an arbitrary-precision integer beyond `i64`/`u64` is compared, ordered, hashed and rendered by its full digits), while a custom object is diffed by its attributes (see the **Custom objects** bullet below). Under `ignore_order`, comparing an integer beyond `f64::MAX` (about `2**1024`) reports the change deterministically where real DeepDiff raises `OverflowError` (its distance function calls `float()` on it unguarded), a documented crash-class divergence like the datetime one below (see [`tests/golden/README.md`](tests/golden/README.md)). Reading JSON *text* (`diff_json` and the CLI, not the Python-object path), an integer beyond `i64`/`u64` parses as the nearest `float`, so two documents whose integers differ only past that range compare **equal** and diff to `{}` — a limitation of the JSON number reader, not the value model, tracked in [#92](https://github.com/ksco92/onix/issues/92). A non-`str` key's path renders via Python's own `repr()`, except a `tuple` key, which splits into one bracket group per element (`root[1][2]` for `(1, 2)`, never `root[(1, 2)]`, with one deliberate exception for a real DeepDiff bug on the empty tuple, and another on a non-finite-float key — see [`tests/golden/README.md`](tests/golden/README.md)). A nested dict value's own `bool`/`None`/`int`/`float` key stringifies the way `json.dumps` does, but its `datetime`/`date`/`tuple` key renders that same `repr()` text where DeepDiff's own `to_json()` raises `TypeError` on one — a superset, not a difference in the findings (see [`tests/golden/README.md`](tests/golden/README.md)). `1`/`1.0`/`True` match as the same key between two dicts (Python `dict`/`set` equality). The **Datetimes**, **Sets** and **Non-finite floats** bullets below cover the deliberate divergences for those types. See [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs) and [`tests/golden/README.md`](tests/golden/README.md).
|
|
237
|
+
- **Custom objects** diff by their attributes, matching DeepDiff's `_diff_obj` (`attribute_added`/`attribute_removed`, `root.attr` paths, `type_changes` between two different class objects), and an `Enum` member by its `name` and `value`, matching `_diff_enum`. A type DeepDiff routes to a handler onix lacks — `bytes`, `bytearray`, a generator or any other iterable, `complex`, `Decimal`, `Fraction`, a `numpy` scalar, `uuid`, `ipaddress`, a class object, a module, or a bare `object()` — raises `TypeError` at the root; below it, such a value is equal only to the same object, as DeepDiff's `t1 is t2` check makes it, and raises `TypeError` naming its path wherever the report would have to show it. Two identical Python objects at one position are never walked, a class attribute converts only when a report compares it with a shadowing value, and a class attribute is left out of a whole object in a report, as in DeepDiff (see [`tests/golden/README.md`](tests/golden/README.md#types-deepdiff-routes-to-a-handler-onix-lacks)). A `pydantic` model raises `TypeError` where DeepDiff diffs it (see [`tests/golden/README.md`](tests/golden/README.md#pydantic-models)). A custom non-`dict` `Mapping`, and a `re.Pattern` pair whose named groups differ, raise `TypeError` where DeepDiff diffs them (see [`tests/golden/README.md`](tests/golden/README.md#refused-mappings)). A `@property` that raises `AttributeError`, or an unset slot beside a `__dict__`, raises `TypeError` where DeepDiff reports the object as `unprocessed` (see [`tests/golden/README.md`](tests/golden/README.md#a-property-that-raises)). A child that points back at an object on its path reports nothing when it is on the first side; on the second side only it is compared as the object it points back at, as DeepDiff's `parents_ids` tracks only the first side's ancestry (see [`tests/golden/README.md`](tests/golden/README.md#a-recursive-object)). Three attribute-view divergences (`ignore_order` hashing, and the whole-object values in `to_json()` and `to_dict()`) are tracked in [#99](https://github.com/ksco92/onix/issues/99), see [`tests/golden/README.md`](tests/golden/README.md#custom-objects-where-onix-is-deliberately-different).
|
|
219
238
|
- A subclass of a supported `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, or `datetime.timedelta` (including `namedtuple` and pandas' `Timestamp`) converts and compares as its base type but carries its own class name into a `type_changes` entry, matching DeepDiff's `type(obj).__name__` reporting, with three exceptions: a calendar-type (`datetime`/`date`/`time`/`timedelta`) subclass held as a `set`/`frozenset` member compares as its base with no `type_changes`, matching DeepDiff, since set membership has no pairwise comparison to report one against; a `tuple`/`frozenset` subclass, including a `namedtuple`, is not accepted as a `set`/`frozenset` member at all and raises `TypeError` where DeepDiff accepts and compares it by value (a documented divergence); and `namedtuple` diffs positionally rather than by field (also documented). A `type_changes` entry's `old_type`/`new_type` are type *names* in `to_dict()`, where DeepDiff returns the type objects. See [`tests/golden/README.md`](tests/golden/README.md).
|
|
220
239
|
- **Datetimes** compare by instant, with a naive value read as UTC, matching DeepDiff. A changed pair is reported normalized to UTC (`to_json()` renders `...+00:00`, `to_dict()` returns UTC-aware `datetime`s); everywhere else a datetime keeps its raw value. Three deliberate departures: `to_json()` renders a `date` as `YYYY-MM-DD` where DeepDiff's own `to_json()` raises `TypeError` (a documented superset); a `zoneinfo`/`pytz` tzinfo comes back from `to_dict()` as a fixed-offset `datetime.timezone` carrying the offset it was in force at, not the original zone object; and a set holding both a naive and an aware value at one instant reports both as members, where DeepDiff's own digest cache can report only one (see [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs)). Comparing two datetimes whose UTC form would leave year 1..=9999 raises `ValueError` naming the path, where DeepDiff raises `OverflowError`; under `ignore_order` DeepDiff's hasher normalizes every datetime and so raises for such a value even when it is only added, removed, or shuffled, where onix hashes by instant and reports it normally (see [`tests/golden/README.md`](tests/golden/README.md)). `truncate_datetime` is not supported; the normalized-versus-raw split is documented there too. A `time`/`timedelta`, unlike a datetime, is never normalized for report (DeepDiff compares `time`/`date`/`timedelta` with a plain `!=`), and a naive `time` is never equal to an aware one; `to_json()` renders a `time` as `time.isoformat()`'s bytes and a `timedelta` as `str(timedelta)`'s, both supersets. Under `ignore_order`, `DeepHash` hashes a `time` by whole seconds-of-day only — dropping the microsecond and any offset, a confirmed upstream quirk — while a `timedelta` hashes exactly; see [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
221
240
|
- **Sets** are diffed deterministically, where DeepDiff's own answers depend on the order the running process happens to iterate a set in (hash order, and `PYTHONHASHSEED`-dependent for `str` members) or on how its digest cache/computation handles a tuple, frozenset, or calendar member independently of Python's own `==`. Each consequence — entry order, which member of an equality class is reported, set-versus-sequence coercion, and a tuple/frozenset member's own (positional, not order-/repetition-insensitive) matching rule — is shown with both tools' output in [`tests/golden/README.md`](tests/golden/README.md)'s "Set iteration order" section. A report holding a `frozenset` value also serializes to JSON here, where DeepDiff's own `to_json()` raises `TypeError` — a superset, not a difference in the findings.
|
|
222
|
-
- **Non-finite floats** (`NaN`, `Infinity`, `-Infinity`) compare and hash like real Python: two `NaN`s are never equal, `Infinity == Infinity`, `to_json()` renders the same bare `NaN`/`Infinity`/`-Infinity` tokens Python's `json.dumps` does, and `ignore_order` matching treats every `NaN` as one shared item, matching `DeepHash`. The one divergence, always deterministic: this crate's value model carries
|
|
241
|
+
- **Non-finite floats** (`NaN`, `Infinity`, `-Infinity`) compare and hash like real Python: two `NaN`s are never equal, `Infinity == Infinity`, `to_json()` renders the same bare `NaN`/`Infinity`/`-Infinity` tokens Python's `json.dumps` does, and `ignore_order` matching treats every `NaN` as one shared item, matching `DeepHash`. The one divergence, always deterministic: this crate's value model carries Python object identity for custom objects only, so two `NaN`s outside one shared custom object always compare unequal here, where DeepDiff sometimes reports no difference (`t1 is t2`) or lets one collapse into another (a `set` member, an ordered-list match) when the two objects, or their containers, happen to be the same one. See [`tests/golden/README.md`](tests/golden/README.md)'s "Non-finite floats" section.
|
|
223
242
|
- A `str` (or `dict` key) containing a lone (unpaired) surrogate code point (e.g. `'\udc80'`, legal in Python but not encodable as UTF-8) is compared and reported exactly like any other `str`, matching DeepDiff's plain `==`; `to_json()` renders it with `json.dumps`'s own single-backslash `\uXXXX` escape. The one accepted divergence, always deterministic: hashing one — a `set`/`frozenset` member, or *any* value at all once `ignore_order=True` (`DeepHash` hashes every value there, not just a set's members) — crashes real DeepDiff with an unhandled `UnicodeEncodeError`, where onix hashes by code point and reports normally. See [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
224
243
|
- A `str` inside a `tuple` or `frozenset` set item is escaped exactly as Python's `repr()` escapes it, against Unicode 16.0.0; on a Python older than 3.14 (an older `unicodedata` table), a code point assigned to Unicode after that Python's own version is escaped by DeepDiff and rendered literally by onix. See [`tests/golden/README.md`](tests/golden/README.md)'s "Pinned versions" section.
|
|
225
244
|
- Adversarially deep input raises `MaxDepthError` instead of crashing: the default `max_depth` is 512 and the hard ceiling is `MAX_DEPTH_CEILING` (20,000). See [`crates/onix-py/src/guard.rs`](crates/onix-py/src/guard.rs).
|
|
@@ -229,8 +248,12 @@ perf/ # cross-language benchmark harness and RESULTS.md
|
|
|
229
248
|
- `diff_tables` inherits some Arrow-interchange quirks: a column name with an embedded NUL byte (`\0`) arrives truncated at the NUL through the C Data Interface (the report shows the truncated name; rare in practice); a list of structs named exactly `key`/`value` with a nullable key is not distinguished from a real map, so a migration between the two is not reported as a type change (polars exports both as the same Arrow type — see `map_entries` in [`crates/onix-arrow/src/schema.rs`](crates/onix-arrow/src/schema.rs)); and a polars all-null (`Null`-typed) column fails at Arrow C import with a `ValueError` (`the datatype "Null" doesn't expect buffer at index 0`), so give such a column a concrete type first (a pyarrow all-`None` column, inferred as `null`, works and compares as all-null).
|
|
230
249
|
- In `diff_tables`, DuckDB labels a `TIMESTAMP WITH TIME ZONE` column with the connection's *session* time zone when it exports to Arrow (a UTC session as `Timestamp(µs, "UTC")`, an `America/New_York` session as `Timestamp(µs, "America/New_York")`), so on a non-UTC machine such a column can be reported as a type change against a UTC column from another library. Run `SET TimeZone='UTC'` on the DuckDB connection first for a deterministic, machine-independent result.
|
|
231
250
|
- `diff_tables` refuses a column whose Arrow type is nested deeper than `MAX_NESTING_DEPTH` (128) with a `MaxDepthError`, because comparing arbitrarily deep nesting would overflow the native stack; 128 is far beyond any real schema. Importing a schema nested many thousands of levels deep is also slow regardless, a cost of the Arrow C Data Interface itself.
|
|
232
|
-
- `diff_tables`
|
|
233
|
-
-
|
|
251
|
+
- `diff_tables` row diff: per-row hash (32 B/row/side): 75 MB at 1M, 660 MB at 10M, linear in row count; wall time `N·log N` across workers. Parallel overhead ~10-15 B/row/side: 629/857 MB at 8M (1/18 threads), 2.9/3.6 GB at 37M; `threads=1` sequential. Duplicate-key report (every distinct duplicated key): 37 MB (16 B keys) to 1.05 GB (1 KB keys) at 200k (100k distinct), 165 MB at 1M (500k distinct, 16 B keys). Cell-pass resident (spilled rows-x-column-width, both sides + 2x `cells_changed` output + hash vectors) at 18 threads: 0.92/1.31/4.81 GB at 100k/200k/1M output-dominated (single-threaded 8.30 GB); eight 512 B columns, one differing (150k changed cells, ~160 MB output): 2.09 GB at 150k spill-dominated vs 0.84 GB one column; 0.38 GB at 1M two int64.
|
|
252
|
+
- Temp disk, same call: each input is spooled to an anonymous file (`tempfile`: unlinked at once, mode 0600, no name, nothing left on abnormal exit). The cell pass spills both sides' changed rows to anonymous per-partition files: byte-view columns cast to a large-offset type (buffers can exceed i32's ~2 GB), dictionaries decoded to value type, keeping the spill compact, independent of partition count (~97 GB at 64 threads undecoded vs ~34 GB decoded, wide pair). Spool and spill resident together, whole-process peaks (34-column fixture, 16.875M rows/side): ~33 GB at 18 threads, 45 GB at 2, 34 GB at 64, down from ~67 GB pre-streaming (Linux spill may be a RAM-backed `tmpfs`). A full temp filesystem raises `ValueError` naming `TMPDIR`.
|
|
253
|
+
- The size gate peeks up to 50,000 rows or 64 MB decoded per side, holding one whole producer batch per side beyond it: a 49,999-row/side, 8 KB-cell, 100-row-batch pair peaks ~78 MB at 18 threads, 14 MB single-threaded, ~1.6 GB for one whole-side batch.
|
|
254
|
+
- None of the row-diff memory, temp-disk or size-gate-peek costs above has a built-in cap. Bound: row count; changed fraction; column widths (key-column width, for duplicate-heavy data; for the cell pass, the total width of all common value columns, since every one spills in full per changed row whether it changed or not, plus the rendered changed cells; any column's width for the size-gate peek, which buffers decoded cells whether or not they change); the producer's batch size; thread count, lowest at 18 threads (measured default), higher at 2 threads (two partitions each holding half the rows), slightly higher at 64: spill-dominated 3.03 GB at 2 threads, 2.09 GB at 64; output-dominated at 200k rows, 1.98 GB at 2, 1.49 GB at 64.
|
|
255
|
+
- Figures are the peak resident set of `cargo run -p onix-arrow --release --example row_diff_rss` (worker count from `ROW_DIFF_THREADS`, rows per batch from `ROW_DIFF_BATCH`), single run, macOS on an Apple M-series laptop, 2026-09-06, same method as [Performance](#performance); the single-threaded hash-term figures carry run-to-run slack up to 2.9-4.2 GB at 37M rows/side, and the two spill/output cell-pass shapes are medians over 5 runs. See [`perf/arrow/RESULTS.md`](perf/arrow/RESULTS.md) for the size-gate peek table and the decode-vs-undecoded partition figures, and [`crates/onix-arrow/src/row_diff.rs`](crates/onix-arrow/src/row_diff.rs) for the implementation.
|
|
256
|
+
- `TableDiff.to_json()` is the one member with a built-in cap: it embeds `rows_added`, `rows_removed`, `cells_changed`, and `duplicate_keys` in full, one JSON object per row, so it refuses with `ValueError` — naming the row count and the cap — once those four together hold more than 10,000 rows. The cap bounds row count only, the same way the first row-diff bullet above states its per-cell term as changed cells times cell width, not column count or cell width — so a table under the row cap but with wide or large cells can still be large; use the Arrow-returning accessors instead. See [`crates/onix-arrow/src/json_rows.rs`](crates/onix-arrow/src/json_rows.rs).
|
|
234
257
|
- `diff_tables` compares scalar columns by value (hashed: null, booleans, every integer, float and decimal width, strings and binary in every encoding, timestamps, dates, times, durations, intervals, and dictionaries of these; refused with `ValueError`: run-end encoded columns and any type-and-unit combination Arrow itself cannot build; nested non-key columns skipped, nested key columns refused), with the exact enumeration in the module doc of [`crates/onix-arrow/src/row_diff.rs`](crates/onix-arrow/src/row_diff.rs). It also refuses a key column whose type differs across the two inputs after encoding normalization (the conservative choice: a primary key that changed type is refused rather than guessed, not coerced). A row whose only difference is a lossless type change — an `Int32` widened to `Int64`, a timestamp unit change at the same instant — hashes equal on both sides, so it is in neither `rows_changed` nor `cells_changed`; the column's type change is still reported in `schema`.
|
|
235
258
|
- Output is byte-identical to DeepDiff except for the cases listed above and two path-rendering quirks; [`tests/golden/README.md`](tests/golden/README.md) enumerates every accepted exception, including integers past `2^53` (the limit of exact `f64` representation) inside ordered scalar lists and `ignore_order` pairing among naive datetimes, which DeepDiff ranks using the *process's local timezone* while onix reads a naive value as UTC everywhere.
|
|
236
259
|
|
|
@@ -1,58 +1,16 @@
|
|
|
1
1
|
//! Table diffing for onix, over Apache Arrow.
|
|
2
2
|
//!
|
|
3
|
-
//! [`diff_tables`] compares two tables presented as [`TableInput`]s and
|
|
4
|
-
//! a [`TableDiff`] carrying the
|
|
5
|
-
//!
|
|
6
|
-
//!
|
|
7
|
-
//!
|
|
8
|
-
//!
|
|
3
|
+
//! [`diff_tables`] compares two tables presented as [`TableInput`]s and
|
|
4
|
+
//! returns a [`TableDiff`] carrying the schema diff, the keyed row diff, and
|
|
5
|
+
//! the per-cell diff. The two tables are matched on a required, non-empty
|
|
6
|
+
//! set of key columns, carried in [`TableDiffOptions`]. The row diff reads
|
|
7
|
+
//! each side more than once, so [`diff_tables`] takes a re-openable
|
|
8
|
+
//! [`TableInput`] rather than a single-use `RecordBatchReader`. In-memory
|
|
9
|
+
//! tables use [`MemoryInput`]; a one-shot stream spools to a temporary file
|
|
10
|
+
//! and implements [`TableInput`] over it, as the Python bindings do.
|
|
9
11
|
//!
|
|
10
|
-
//!
|
|
11
|
-
//!
|
|
12
|
-
//! exist on both sides — a missing one is a [`TableDiffError::KeyColumnMissing`].
|
|
13
|
-
//! Column names must be unique on each side; a repeated name is a
|
|
14
|
-
//! [`TableDiffError::DuplicateColumn`].
|
|
15
|
-
//!
|
|
16
|
-
//! # Inputs
|
|
17
|
-
//!
|
|
18
|
-
//! The row diff reads each side more than once (to hash every row, to
|
|
19
|
-
//! materialize the added/removed rows, and to materialize the changed rows for
|
|
20
|
-
//! the per-cell diff), so [`diff_tables`] takes a re-openable [`TableInput`]
|
|
21
|
-
//! rather than a single-use `RecordBatchReader`.
|
|
22
|
-
//! In-memory tables use [`MemoryInput`]; a caller whose data is a one-shot
|
|
23
|
-
//! stream spools it to a temporary Arrow IPC file first and implements
|
|
24
|
-
//! [`TableInput`] over that file (as the Python bindings do).
|
|
25
|
-
//!
|
|
26
|
-
//! # Row diff
|
|
27
|
-
//!
|
|
28
|
-
//! Rows are matched by a keyed 128-bit hash of the key columns; a row present
|
|
29
|
-
//! only on one side is added or removed, a row on both sides whose non-key
|
|
30
|
-
//! columns differ is changed, and a key appearing more than once on either side
|
|
31
|
-
//! is a duplicate — reported with its per-side counts and excluded from the
|
|
32
|
-
//! added/removed/changed sets. Only the non-key columns present on *both* sides
|
|
33
|
-
//! take part in change detection (a column on one side only is a schema change,
|
|
34
|
-
//! not a cell change), and only scalar columns are compared; a nested non-key
|
|
35
|
-
//! column is skipped, while a nested key column is a
|
|
36
|
-
//! [`TableDiffError::UnsupportedRowType`]. See `src/row_diff.rs` for the exact
|
|
37
|
-
//! value semantics.
|
|
38
|
-
//!
|
|
39
|
-
//! # Type comparison
|
|
40
|
-
//!
|
|
41
|
-
//! Two columns of the same name are "changed type" when their Arrow
|
|
42
|
-
//! [`arrow_schema::DataType`]s differ, comparing every logical parameter
|
|
43
|
-
//! (timestamp unit and timezone, decimal precision and scale). Nullability is
|
|
44
|
-
//! ignored (but reported). Physical encodings that carry the same logical type
|
|
45
|
-
//! are treated as equal — recursively — so a column keeps the same type when a
|
|
46
|
-
//! producer picks a different encoding of it: dictionary encoding, string and
|
|
47
|
-
//! binary views, and the several list variants all normalize together
|
|
48
|
-
//! (`diff_tables(pl.DataFrame, pa.Table)` does not flag every string column, for
|
|
49
|
-
//! instance); a `FixedSizeList` keeps its width; and a list of structs named
|
|
50
|
-
//! exactly `key`/`value` with a nullable key is read as a map, on every list
|
|
51
|
-
//! variant, so a real map and such a list are not distinguished. See
|
|
52
|
-
//! `normalized_type` in `src/schema.rs` for the exact, enumerated rule list.
|
|
53
|
-
//!
|
|
54
|
-
//! The reported `left_type`/`right_type` strings show the actual (un-normalized)
|
|
55
|
-
//! Arrow type, so a real change reports exactly what each side holds.
|
|
12
|
+
//! See `src/row_diff.rs` for the row-matching and value-comparison rules, and
|
|
13
|
+
//! `src/schema.rs` for the column type-normalization rules.
|
|
56
14
|
//!
|
|
57
15
|
//! # Example
|
|
58
16
|
//!
|
|
@@ -118,33 +76,10 @@ pub use row_diff::{MemoryInput, TableInput};
|
|
|
118
76
|
pub use schema::{ChangeKind, SchemaChange, diff_schemas};
|
|
119
77
|
pub use table_diff::{TableDiff, TableDiffSummary};
|
|
120
78
|
|
|
121
|
-
/// The maximum column-type nesting depth [`diff_tables`] will compare;
|
|
122
|
-
///
|
|
123
|
-
///
|
|
124
|
-
///
|
|
125
|
-
/// every recursive walk over one — the comparison, the type's `Display` (which
|
|
126
|
-
/// renders the report), and its own `Clone`/`Drop` — is a native-stack sink
|
|
127
|
-
/// that would abort the process with an uncatchable overflow, not a Python
|
|
128
|
-
/// exception (pyarrow's own recursive `str()` survives depths where these
|
|
129
|
-
/// die, so the producer is not a backstop). This bound is checked iteratively,
|
|
130
|
-
/// before any recursive walk runs, and converts that hazard into a recoverable
|
|
131
|
-
/// error.
|
|
132
|
-
///
|
|
133
|
-
/// The value, 128, is far above any real Arrow schema (nesting beyond a
|
|
134
|
-
/// handful of levels is exotic; a hundred is unheard of). The per-level native
|
|
135
|
-
/// stack cost of those recursive walks is measured by a committed example,
|
|
136
|
-
/// `crates/onix-arrow/examples/type_stack_cost.rs` (`cargo run -p onix-arrow
|
|
137
|
-
/// --example type_stack_cost`, and `--release`), which builds a type nested to
|
|
138
|
-
/// a given depth and binary-searches the deepest one that survives a
|
|
139
|
-
/// clone + `Display` + drop on a fixed-size stack. The worst case is nested
|
|
140
|
-
/// structs in a debug build (the profile `cargo test` uses) at roughly
|
|
141
|
-
/// 5.0 KiB per level; release is roughly 0.7 KiB. So 128 levels costs on the
|
|
142
|
-
/// order of 640 KiB debug / 90 KiB release. The Python bindings run the whole
|
|
143
|
-
/// operation — the recursive FFI import and the drop of the imported types
|
|
144
|
-
/// included — on the large stack-sized worker thread `crate::guard` sizes for
|
|
145
|
-
/// the JSON path (hundreds of MiB), which clears the debug worst case by
|
|
146
|
-
/// roughly 500x, so this bound is the clean-error ceiling rather than the sole
|
|
147
|
-
/// backstop.
|
|
79
|
+
/// The maximum column-type nesting depth [`diff_tables`] will compare; deeper is refused
|
|
80
|
+
/// with [`TableDiffError::MaxDepthExceeded`], turning a native-stack overflow (recursive
|
|
81
|
+
/// comparison, `Display`, `Clone`/`Drop`) into an error before it can run.
|
|
82
|
+
/// Per-level cost is measured by `crates/onix-arrow/examples/type_stack_cost.rs`.
|
|
148
83
|
pub const MAX_NESTING_DEPTH: usize = 128;
|
|
149
84
|
|
|
150
85
|
/// The maximum worker-thread count for the row diff. A larger `threads` is
|
|
@@ -153,8 +88,9 @@ pub const MAX_THREADS: usize = 1024;
|
|
|
153
88
|
|
|
154
89
|
/// Diffs two tables presented as re-openable [`TableInput`]s.
|
|
155
90
|
///
|
|
156
|
-
/// See the [crate-level docs](crate) for the
|
|
157
|
-
///
|
|
91
|
+
/// See the [crate-level docs](crate) for the key-column contract; the
|
|
92
|
+
/// row-diff rules are in `row_diff.rs` and the type-comparison rules in
|
|
93
|
+
/// `schema.rs`.
|
|
158
94
|
///
|
|
159
95
|
/// # Errors
|
|
160
96
|
///
|
|
@@ -164,24 +100,18 @@ pub const MAX_THREADS: usize = 1024;
|
|
|
164
100
|
/// - [`TableDiffError::DuplicateColumn`] if either input has two columns with
|
|
165
101
|
/// the same name.
|
|
166
102
|
/// - [`TableDiffError::KeyColumnMissing`] if a key column is absent from
|
|
167
|
-
/// either input's schema
|
|
103
|
+
/// either input's schema.
|
|
168
104
|
/// - [`TableDiffError::KeyTypeMismatch`] if a key column's normalized type
|
|
169
|
-
/// differs across the two inputs
|
|
170
|
-
/// not coerced).
|
|
105
|
+
/// differs across the two inputs.
|
|
171
106
|
/// - [`TableDiffError::UnsupportedRowType`] if a key column's type, or any
|
|
172
|
-
/// non-nested column's type on either side, cannot be hashed by value
|
|
173
|
-
/// nested key, a run-end-encoded column, or a type combination Arrow cannot
|
|
174
|
-
/// build.
|
|
107
|
+
/// non-nested column's type on either side, cannot be hashed by value.
|
|
175
108
|
/// - [`TableDiffError::Read`] if a batch cannot be read from either input.
|
|
176
|
-
/// - [`TableDiffError::Render`] if a changed cell's value cannot be rendered
|
|
177
|
-
/// its canonical string
|
|
178
|
-
/// formatter cannot format.
|
|
109
|
+
/// - [`TableDiffError::Render`] if a changed cell's value cannot be rendered
|
|
110
|
+
/// to its canonical string.
|
|
179
111
|
/// - [`TableDiffError::TooManyChangedRows`] if one side has more than
|
|
180
|
-
/// `u32::MAX` changed rows
|
|
181
|
-
///
|
|
182
|
-
///
|
|
183
|
-
/// the internal invariant that a `value_changed` cell always renders two
|
|
184
|
-
/// different strings.
|
|
112
|
+
/// `u32::MAX` changed rows.
|
|
113
|
+
/// - [`TableDiffError::EqualRenderings`] never fires for real input; it
|
|
114
|
+
/// guards an internal invariant.
|
|
185
115
|
pub fn diff_tables(
|
|
186
116
|
left: &impl TableInput,
|
|
187
117
|
right: &impl TableInput,
|