deepdiff-rs 0.8.2__tar.gz → 0.9.1__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.8.2 → deepdiff_rs-0.9.1}/Cargo.lock +8 -8
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/Cargo.toml +1 -1
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/PKG-INFO +15 -15
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/README.md +14 -14
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/error.rs +49 -0
- deepdiff_rs-0.9.1/crates/onix-arrow/src/json_rows.rs +141 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/lib.rs +2 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/row_diff.rs +20 -12
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/table_diff.rs +97 -5
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/scalar.rs +7 -4
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/distance.rs +1 -1
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/fxhash.rs +8 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/hash.rs +42 -10
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/memo.rs +8 -6
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/tests.rs +48 -6
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/lcs.rs +33 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/lcs_tests.rs +61 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/path.rs +13 -2
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/test_support.rs +1 -1
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/value.rs +112 -31
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/value_tests.rs +83 -16
- deepdiff_rs-0.9.1/crates/onix-py/deepdiff_rs.pyi +205 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/src/arrow.rs +43 -41
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/src/convert.rs +8 -12
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/src/deepdiff.rs +8 -5
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/src/guard.rs +107 -6
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_conversions.py +13 -13
- deepdiff_rs-0.9.1/crates/onix-py/tests/test_non_finite.py +318 -0
- deepdiff_rs-0.9.1/crates/onix-py/tests/test_stub_mypy.py +67 -0
- deepdiff_rs-0.9.1/crates/onix-py/tests/test_stub_signatures.py +144 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_table_diff.py +186 -40
- deepdiff_rs-0.9.1/crates/onix-py/tests/test_wheel_contents.py +50 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/pyproject.toml +12 -6
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-arrow/Cargo.toml +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-arrow/examples/row_diff_rss.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-arrow/examples/type_stack_cost.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/options.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/schema.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/Cargo.toml +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/examples/stack_frame_cost.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/datetime.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/datetime_tests.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/array.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/dispatch.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/mod.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/object.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/options.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/set.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/tests.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/error.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/mod.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/pairing.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/lib.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/report.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/report_tests.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/unified_diff.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/src/unified_diff_tests.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/tests/golden.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/tests/ignore_order_memory.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/tests/memory_footprint.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/tests/proptest_diff.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-core/tests/proptest_ignore_order.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/.python-version +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/Cargo.toml +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/benchmarks/bench_bindings.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/src/errors.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/src/fast_path.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/src/lib.rs +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/conftest.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_bindings_memory.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_datetimes.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_depth_guard.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_differential_fuzz.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_golden_parity.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_sets.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_signed_zero.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_smoke.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_suite_hygiene.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_table_row_diff.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_timedeltas.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_times.py +0 -0
- {deepdiff_rs-0.8.2 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_tuples.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.9.1"
|
|
633
633
|
dependencies = [
|
|
634
634
|
"arrow-array",
|
|
635
635
|
"arrow-buffer",
|
|
@@ -646,7 +646,7 @@ dependencies = [
|
|
|
646
646
|
|
|
647
647
|
[[package]]
|
|
648
648
|
name = "onix-cli"
|
|
649
|
-
version = "0.
|
|
649
|
+
version = "0.9.1"
|
|
650
650
|
dependencies = [
|
|
651
651
|
"onix-core",
|
|
652
652
|
"serde_json",
|
|
@@ -654,7 +654,7 @@ dependencies = [
|
|
|
654
654
|
|
|
655
655
|
[[package]]
|
|
656
656
|
name = "onix-core"
|
|
657
|
-
version = "0.
|
|
657
|
+
version = "0.9.1"
|
|
658
658
|
dependencies = [
|
|
659
659
|
"proptest",
|
|
660
660
|
"serde",
|
|
@@ -665,7 +665,7 @@ dependencies = [
|
|
|
665
665
|
|
|
666
666
|
[[package]]
|
|
667
667
|
name = "onix-py"
|
|
668
|
-
version = "0.
|
|
668
|
+
version = "0.9.1"
|
|
669
669
|
dependencies = [
|
|
670
670
|
"arrow-array",
|
|
671
671
|
"arrow-ipc",
|
|
@@ -997,7 +997,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|
|
997
997
|
dependencies = [
|
|
998
998
|
"proc-macro2",
|
|
999
999
|
"quote",
|
|
1000
|
-
"syn 3.0.
|
|
1000
|
+
"syn 3.0.5",
|
|
1001
1001
|
]
|
|
1002
1002
|
|
|
1003
1003
|
[[package]]
|
|
@@ -1050,9 +1050,9 @@ dependencies = [
|
|
|
1050
1050
|
|
|
1051
1051
|
[[package]]
|
|
1052
1052
|
name = "syn"
|
|
1053
|
-
version = "3.0.
|
|
1053
|
+
version = "3.0.5"
|
|
1054
1054
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1055
|
-
checksum = "
|
|
1055
|
+
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
|
1056
1056
|
dependencies = [
|
|
1057
1057
|
"proc-macro2",
|
|
1058
1058
|
"quote",
|
|
@@ -1199,7 +1199,7 @@ dependencies = [
|
|
|
1199
1199
|
"bumpalo",
|
|
1200
1200
|
"proc-macro2",
|
|
1201
1201
|
"quote",
|
|
1202
|
-
"syn 3.0.
|
|
1202
|
+
"syn 3.0.5",
|
|
1203
1203
|
"wasm-bindgen-shared",
|
|
1204
1204
|
]
|
|
1205
1205
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepdiff-rs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Rust
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -56,7 +56,7 @@ From source:
|
|
|
56
56
|
```sh
|
|
57
57
|
cd crates/onix-py
|
|
58
58
|
uv tool install maturin # the build tool (skip if already installed)
|
|
59
|
-
uv sync --group test # creates .venv, installs pytest, pinned deepdiff, and pyarrow/polars/duckdb for the table-diff tests
|
|
59
|
+
uv sync --group test # creates .venv, installs pytest, pinned deepdiff, and pyarrow/polars/duckdb/pandas for the table-diff tests
|
|
60
60
|
uv run --group test maturin develop --release
|
|
61
61
|
```
|
|
62
62
|
|
|
@@ -118,15 +118,15 @@ Pass `--ignore-order` to compare every list by value instead of by position, mir
|
|
|
118
118
|
|
|
119
119
|
`diff_tables` compares two tables the way `DeepDiff` compares two objects. It takes any object implementing the [Arrow PyCapsule interface](https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html) — a pyarrow `Table` or `RecordBatch`, a polars `DataFrame`, a DuckDB relation — and imports it with no Python round trip. The two tables are matched on a required, non-empty set of key columns (the table's primary key).
|
|
120
120
|
|
|
121
|
-
It reports the **schema** diff (which columns were added, removed, or changed type), the keyed **row** diff (which rows were added, removed, or changed, and which keys are duplicated), and the per-cell diff (`cells_changed`): one row per changed cell
|
|
121
|
+
It reports the **schema** diff (which columns were added, removed, or changed type), the keyed **row** diff (which rows were added, removed, or changed, and which keys are duplicated), and the per-cell diff (`cells_changed`): one row per changed cell, carrying the key columns, `column`, `old_value`/`new_value`, and `change` (`became_null`/`became_non_null`, `type_changed`, or `value_changed`), ordered by the canonical string rendering of the key columns (nulls first), then left-schema column order — the exact rendering and change-classification rules are in the module doc of [`crates/onix-arrow/src/row_diff.rs`](crates/onix-arrow/src/row_diff.rs). Rows are matched by the key columns; `rows_added`, `rows_removed`, `cells_changed`, and `duplicate_keys` return Arrow tables, and `summary()` counts each outcome.
|
|
122
122
|
|
|
123
123
|
```python
|
|
124
124
|
import pyarrow as pa
|
|
125
125
|
from deepdiff_rs import diff_tables
|
|
126
126
|
|
|
127
127
|
left = pa.table({
|
|
128
|
-
"id": pa.array([1, 2, 3], pa.int64()),
|
|
129
|
-
"amount": pa.array([10, 20, 30], pa.int32()),
|
|
128
|
+
"id": pa.array([1, 2, 3, 9, 9], pa.int64()), # 9 is a duplicate key
|
|
129
|
+
"amount": pa.array([10, 20, 30, 90, 91], pa.int32()),
|
|
130
130
|
})
|
|
131
131
|
right = pa.table({
|
|
132
132
|
"id": pa.array([2, 3, 4], pa.int64()),
|
|
@@ -135,22 +135,20 @@ right = pa.table({
|
|
|
135
135
|
})
|
|
136
136
|
|
|
137
137
|
diff = diff_tables(left, right, key=["id"])
|
|
138
|
-
print(diff.summary())
|
|
139
|
-
print("
|
|
140
|
-
print("cells changed:", pa.table(diff.cells_changed()).to_pylist())
|
|
138
|
+
print(diff.summary(), "added ids:", pa.table(diff.rows_added()).column("id").to_pylist(), "removed ids:", pa.table(diff.rows_removed()).column("id").to_pylist())
|
|
139
|
+
print("cells changed:", pa.table(diff.cells_changed()).to_pylist(), "duplicate keys:", pa.table(diff.duplicate_keys()).to_pylist())
|
|
141
140
|
```
|
|
142
141
|
|
|
143
142
|
```
|
|
144
|
-
{'columns_added': 1, 'columns_removed': 0, 'columns_type_changed': 1, 'rows_added': 1, 'rows_removed': 1, 'rows_changed': 1, 'duplicate_keys':
|
|
145
|
-
|
|
146
|
-
cells changed: [{'id': 3, 'column': 'amount', 'old_value': '30', 'new_value': '31', 'change': 'value_changed'}]
|
|
143
|
+
{'columns_added': 1, 'columns_removed': 0, 'columns_type_changed': 1, 'rows_added': 1, 'rows_removed': 1, 'rows_changed': 1, 'duplicate_keys': 1, 'null_keys': 0, 'cells_changed': 1} added ids: [4] removed ids: [1]
|
|
144
|
+
cells changed: [{'id': 3, 'column': 'amount', 'old_value': '30', 'new_value': '31', 'change': 'value_changed'}] duplicate keys: [{'id': 9, 'left_count': 2, 'right_count': 0}]
|
|
147
145
|
```
|
|
148
146
|
|
|
149
147
|
A key appearing more than once on either side is reported in `duplicate_keys` (with `left_count` and `right_count`) and excluded from the added/removed/changed sets; a null key matches its counterpart and is counted in `null_keys`. Rows are compared by the non-key columns present on *both* sides, with onix's value semantics (integers and integral floats fold together, all NaNs compare equal, `1.00` equals `1.0000`, a timestamp compares by its instant and a time or duration by its value across units, dictionary-encoded values equal their plain form, and null equals null); a nested non-key column is out of scope and is skipped rather than compared. The exact value-comparison rules are documented on the hashing functions in [`crates/onix-arrow/src/row_diff.rs`](crates/onix-arrow/src/row_diff.rs).
|
|
150
148
|
|
|
151
|
-
Type comparison uses the full logical Arrow type (timestamp unit and timezone, decimal precision and scale, and so on), but physical encodings that carry the same logical type compare equal — a dictionary-encoded string equals a plain string, polars' `Utf8View` equals pyarrow's `Utf8`, the list variants normalize together, and a map compares equal however a library spells it — so the same table read through pyarrow, polars, or DuckDB reports no spurious type changes. The full normalization rules are documented on `normalized_type` (and `map_entries`) in [`crates/onix-arrow/src/schema.rs`](crates/onix-arrow/src/schema.rs); nullability is ignored but reported in each record. Column names must be unique on each side; a repeated name raises `ValueError`. `diff.schema_arrow` is the same result as an Arrow table: it implements `__arrow_c_stream__`, so `polars.DataFrame(diff.schema_arrow)`
|
|
149
|
+
Type comparison uses the full logical Arrow type (timestamp unit and timezone, decimal precision and scale, and so on), but physical encodings that carry the same logical type compare equal — a dictionary-encoded string equals a plain string, polars' `Utf8View` equals pyarrow's `Utf8`, the list variants normalize together, and a map compares equal however a library spells it — so the same table read through pyarrow, polars, or DuckDB reports no spurious type changes. The full normalization rules are documented on `normalized_type` (and `map_entries`) in [`crates/onix-arrow/src/schema.rs`](crates/onix-arrow/src/schema.rs); nullability is ignored but reported in each record. Column names must be unique on each side; a repeated name raises `ValueError`. `diff.schema_arrow` is the same result as an Arrow table: it implements `__arrow_c_stream__`, so `polars.DataFrame(diff.schema_arrow)` consumes it with no pyarrow needed, and `diff.schema_arrow.to_pyarrow()` returns a `pyarrow.Table`; `pandas.api.interchange.from_dataframe(diff.schema_arrow)` also works, but pandas' own implementation of that protocol needs pyarrow installed regardless of which path you take.
|
|
152
150
|
|
|
153
|
-
`pyarrow` is optional: install it with `pip install deepdiff-rs[arrow]`. It is needed only for `to_pyarrow()` and for passing pyarrow objects in — importing `deepdiff_rs` and diffing polars or DuckDB tables need it not at all. Passing an object that implements neither Arrow protocol raises `TypeError`; calling `to_pyarrow()` without pyarrow installed raises `ImportError` naming the extra.
|
|
151
|
+
`pyarrow` is optional: install it with `pip install deepdiff-rs[arrow]`. It is needed only for `to_pyarrow()` and for passing pyarrow objects in — importing `deepdiff_rs` and diffing polars or DuckDB tables need it not at all. Passing an object that implements neither Arrow protocol raises `TypeError`; calling `to_pyarrow()` without pyarrow installed raises `ImportError` naming the extra. `diff.to_json()` gives the whole diff — schema, summary, and `rows_added`/`rows_removed`/`cells_changed`/`duplicate_keys` in full, one JSON object per row — as a single string with no pyarrow, polars, or pandas needed at all; see [Known limitations](#known-limitations) for its row cap.
|
|
154
152
|
|
|
155
153
|
## Performance
|
|
156
154
|
|
|
@@ -206,7 +204,7 @@ Both reports carry their full methodology, fairness rules, and the reproduce com
|
|
|
206
204
|
- `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` (see [Known limitations](#known-limitations) for the exact restrictions and exclusions, including which types a `dict` key may be); `.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 — 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)]`).
|
|
207
205
|
- `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.
|
|
208
206
|
- `MaxDepthError` (a `ValueError` subclass) is raised when input exceeds `max_depth`; `MAX_DEPTH_CEILING` (20,000) is the hard upper bound on `max_depth`.
|
|
209
|
-
- `diff_tables(left, right, key=[...]) -> TableDiff`: diffs two Arrow tables (see [Diffing tables](#diffing-tables)). `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
|
|
207
|
+
- `diff_tables(left, right, key=[...]) -> TableDiff`: diffs two Arrow tables (see [Diffing tables](#diffing-tables)). `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.
|
|
210
208
|
|
|
211
209
|
**CLI.** `onix diff <a.json> <b.json> [--max-depth N] [--ignore-order] [--timing]` reads both files as JSON and prints a compact, single-line DeepDiff-compatible report to stdout (`{}` when there is no difference).
|
|
212
210
|
|
|
@@ -238,10 +236,11 @@ perf/ # cross-language benchmark harness and RESULTS.md
|
|
|
238
236
|
## Known limitations
|
|
239
237
|
|
|
240
238
|
- Only the core diff is implemented: `exclude_paths`, `significant_digits`, custom operators, `verbose_level != 2`, and delta/patch are not (yet) supported.
|
|
241
|
-
- Supported value types are `None`, `bool`, `int`, `float
|
|
239
|
+
- 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). `int`s must fit in `i64`/`u64`, and anything else — a custom object or an arbitrary-precision `int` — raises `TypeError`/`ValueError` naming the exact path it was found at. 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).
|
|
242
240
|
- A subclass of a supported type (a `tuple`, `set` or `frozenset` subclass including `namedtuple`, a `datetime`/`date`/`time`/`timedelta` subclass such as pandas' `Timestamp`) raises `TypeError` rather than being diffed as its base type, because DeepDiff reports each value's own type name. A `type_changes` entry's `old_type`/`new_type` are type *names* in `to_dict()`, where DeepDiff returns the type objects. Both are described in [`tests/golden/README.md`](tests/golden/README.md).
|
|
243
241
|
- **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.
|
|
244
242
|
- **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.
|
|
243
|
+
- **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 no Python object identity, so two independently-obtained `NaN`s 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.
|
|
245
244
|
- A `str` containing a lone (unpaired) surrogate code point (e.g. `'\udc80'`, legal in Python but not encodable as UTF-8) raises `ValueError` naming the exact path on either side, before the two values are ever compared — including a pair DeepDiff would call equal and report as no change, since DeepDiff's scalar equality is plain Python `==` and never hits the encoding problem; DeepDiff does report a plain change for a *differing* pair, and crashes with an unhandled `UnicodeEncodeError` if such a string is ever hashed (a `set`/`frozenset` member). See [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
246
245
|
- 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.
|
|
247
246
|
- 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).
|
|
@@ -252,6 +251,7 @@ perf/ # cross-language benchmark harness and RESULTS.md
|
|
|
252
251
|
- 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.
|
|
253
252
|
- `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.
|
|
254
253
|
- `diff_tables`'s row diff costs, per call: RAM for a 32-byte hash per row per side (sorted), so peak memory is linear in row count (about 75 MB at 1M rows/side, 660 MB at 10M) and time is `N·log N` (about 2 s for a 10M-row pair), plus a second term for the duplicate-key report, which holds the key values of every *distinct duplicated* key — an all-duplicate 200k-rows/side table (100k distinct duplicated keys) peaks at about 37 MB with 16-byte keys and 1.05 GB with 1 KB keys, 1M rows/side (500k distinct) at about 165 MB with 16-byte keys; a third term for the per-cell diff, which re-reads both inputs, holds the changed rows of both sides, and renders every changed cell to a string held once in the output — so its cost is the number of changed cells times the cell width, not the changed-row count alone (every value is rendered in full). Measured, same method: with two narrow `int64` columns, 85 MB at 1M rows/side with 2% changed and 629 MB with every row changed; with a 1 KB `string` cell changed on every row, 1.18 GB at 100k rows/side and 2.34 GB at 200k (about 11.6 KB per changed row, linear, so on the order of 11.6 GB at 1M). Then temp disk, because each input is re-read several times and so is spooled to an anonymous file (`tempfile`: unlinked at once, mode 0600, no predictable name — nothing is left on disk even on abnormal exit), both spools resident at once, peaking at the decoded size of both inputs (about 315 MB — 161 + 154 MB uncompressed Arrow IPC — for the 1M-row fixture pair; on the order of 10 GB for the full 5 GB-per-side fixture pair, which on Linux may be a RAM-backed `tmpfs`), a full temp filesystem raising `ValueError` naming `TMPDIR`. None of these has a built-in cap: for untrusted input, bound the row count, the changed fraction, and the column widths (key columns for duplicate-heavy data, changed cells — rendered in full — for wide value columns). Figures are the peak resident set of `cargo run -p onix-arrow --release --example row_diff_rss`, single run, macOS on an Apple M-series laptop, 2026-09-05, same method as [Performance](#performance). See [`crates/onix-arrow/src/row_diff.rs`](crates/onix-arrow/src/row_diff.rs).
|
|
254
|
+
- `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 row-diff cost 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).
|
|
255
255
|
- `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`.
|
|
256
256
|
- 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.
|
|
257
257
|
|
|
@@ -38,7 +38,7 @@ From source:
|
|
|
38
38
|
```sh
|
|
39
39
|
cd crates/onix-py
|
|
40
40
|
uv tool install maturin # the build tool (skip if already installed)
|
|
41
|
-
uv sync --group test # creates .venv, installs pytest, pinned deepdiff, and pyarrow/polars/duckdb for the table-diff tests
|
|
41
|
+
uv sync --group test # creates .venv, installs pytest, pinned deepdiff, and pyarrow/polars/duckdb/pandas for the table-diff tests
|
|
42
42
|
uv run --group test maturin develop --release
|
|
43
43
|
```
|
|
44
44
|
|
|
@@ -100,15 +100,15 @@ Pass `--ignore-order` to compare every list by value instead of by position, mir
|
|
|
100
100
|
|
|
101
101
|
`diff_tables` compares two tables the way `DeepDiff` compares two objects. It takes any object implementing the [Arrow PyCapsule interface](https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html) — a pyarrow `Table` or `RecordBatch`, a polars `DataFrame`, a DuckDB relation — and imports it with no Python round trip. The two tables are matched on a required, non-empty set of key columns (the table's primary key).
|
|
102
102
|
|
|
103
|
-
It reports the **schema** diff (which columns were added, removed, or changed type), the keyed **row** diff (which rows were added, removed, or changed, and which keys are duplicated), and the per-cell diff (`cells_changed`): one row per changed cell
|
|
103
|
+
It reports the **schema** diff (which columns were added, removed, or changed type), the keyed **row** diff (which rows were added, removed, or changed, and which keys are duplicated), and the per-cell diff (`cells_changed`): one row per changed cell, carrying the key columns, `column`, `old_value`/`new_value`, and `change` (`became_null`/`became_non_null`, `type_changed`, or `value_changed`), ordered by the canonical string rendering of the key columns (nulls first), then left-schema column order — the exact rendering and change-classification rules are in the module doc of [`crates/onix-arrow/src/row_diff.rs`](crates/onix-arrow/src/row_diff.rs). Rows are matched by the key columns; `rows_added`, `rows_removed`, `cells_changed`, and `duplicate_keys` return Arrow tables, and `summary()` counts each outcome.
|
|
104
104
|
|
|
105
105
|
```python
|
|
106
106
|
import pyarrow as pa
|
|
107
107
|
from deepdiff_rs import diff_tables
|
|
108
108
|
|
|
109
109
|
left = pa.table({
|
|
110
|
-
"id": pa.array([1, 2, 3], pa.int64()),
|
|
111
|
-
"amount": pa.array([10, 20, 30], pa.int32()),
|
|
110
|
+
"id": pa.array([1, 2, 3, 9, 9], pa.int64()), # 9 is a duplicate key
|
|
111
|
+
"amount": pa.array([10, 20, 30, 90, 91], pa.int32()),
|
|
112
112
|
})
|
|
113
113
|
right = pa.table({
|
|
114
114
|
"id": pa.array([2, 3, 4], pa.int64()),
|
|
@@ -117,22 +117,20 @@ right = pa.table({
|
|
|
117
117
|
})
|
|
118
118
|
|
|
119
119
|
diff = diff_tables(left, right, key=["id"])
|
|
120
|
-
print(diff.summary())
|
|
121
|
-
print("
|
|
122
|
-
print("cells changed:", pa.table(diff.cells_changed()).to_pylist())
|
|
120
|
+
print(diff.summary(), "added ids:", pa.table(diff.rows_added()).column("id").to_pylist(), "removed ids:", pa.table(diff.rows_removed()).column("id").to_pylist())
|
|
121
|
+
print("cells changed:", pa.table(diff.cells_changed()).to_pylist(), "duplicate keys:", pa.table(diff.duplicate_keys()).to_pylist())
|
|
123
122
|
```
|
|
124
123
|
|
|
125
124
|
```
|
|
126
|
-
{'columns_added': 1, 'columns_removed': 0, 'columns_type_changed': 1, 'rows_added': 1, 'rows_removed': 1, 'rows_changed': 1, 'duplicate_keys':
|
|
127
|
-
|
|
128
|
-
cells changed: [{'id': 3, 'column': 'amount', 'old_value': '30', 'new_value': '31', 'change': 'value_changed'}]
|
|
125
|
+
{'columns_added': 1, 'columns_removed': 0, 'columns_type_changed': 1, 'rows_added': 1, 'rows_removed': 1, 'rows_changed': 1, 'duplicate_keys': 1, 'null_keys': 0, 'cells_changed': 1} added ids: [4] removed ids: [1]
|
|
126
|
+
cells changed: [{'id': 3, 'column': 'amount', 'old_value': '30', 'new_value': '31', 'change': 'value_changed'}] duplicate keys: [{'id': 9, 'left_count': 2, 'right_count': 0}]
|
|
129
127
|
```
|
|
130
128
|
|
|
131
129
|
A key appearing more than once on either side is reported in `duplicate_keys` (with `left_count` and `right_count`) and excluded from the added/removed/changed sets; a null key matches its counterpart and is counted in `null_keys`. Rows are compared by the non-key columns present on *both* sides, with onix's value semantics (integers and integral floats fold together, all NaNs compare equal, `1.00` equals `1.0000`, a timestamp compares by its instant and a time or duration by its value across units, dictionary-encoded values equal their plain form, and null equals null); a nested non-key column is out of scope and is skipped rather than compared. The exact value-comparison rules are documented on the hashing functions in [`crates/onix-arrow/src/row_diff.rs`](crates/onix-arrow/src/row_diff.rs).
|
|
132
130
|
|
|
133
|
-
Type comparison uses the full logical Arrow type (timestamp unit and timezone, decimal precision and scale, and so on), but physical encodings that carry the same logical type compare equal — a dictionary-encoded string equals a plain string, polars' `Utf8View` equals pyarrow's `Utf8`, the list variants normalize together, and a map compares equal however a library spells it — so the same table read through pyarrow, polars, or DuckDB reports no spurious type changes. The full normalization rules are documented on `normalized_type` (and `map_entries`) in [`crates/onix-arrow/src/schema.rs`](crates/onix-arrow/src/schema.rs); nullability is ignored but reported in each record. Column names must be unique on each side; a repeated name raises `ValueError`. `diff.schema_arrow` is the same result as an Arrow table: it implements `__arrow_c_stream__`, so `polars.DataFrame(diff.schema_arrow)`
|
|
131
|
+
Type comparison uses the full logical Arrow type (timestamp unit and timezone, decimal precision and scale, and so on), but physical encodings that carry the same logical type compare equal — a dictionary-encoded string equals a plain string, polars' `Utf8View` equals pyarrow's `Utf8`, the list variants normalize together, and a map compares equal however a library spells it — so the same table read through pyarrow, polars, or DuckDB reports no spurious type changes. The full normalization rules are documented on `normalized_type` (and `map_entries`) in [`crates/onix-arrow/src/schema.rs`](crates/onix-arrow/src/schema.rs); nullability is ignored but reported in each record. Column names must be unique on each side; a repeated name raises `ValueError`. `diff.schema_arrow` is the same result as an Arrow table: it implements `__arrow_c_stream__`, so `polars.DataFrame(diff.schema_arrow)` consumes it with no pyarrow needed, and `diff.schema_arrow.to_pyarrow()` returns a `pyarrow.Table`; `pandas.api.interchange.from_dataframe(diff.schema_arrow)` also works, but pandas' own implementation of that protocol needs pyarrow installed regardless of which path you take.
|
|
134
132
|
|
|
135
|
-
`pyarrow` is optional: install it with `pip install deepdiff-rs[arrow]`. It is needed only for `to_pyarrow()` and for passing pyarrow objects in — importing `deepdiff_rs` and diffing polars or DuckDB tables need it not at all. Passing an object that implements neither Arrow protocol raises `TypeError`; calling `to_pyarrow()` without pyarrow installed raises `ImportError` naming the extra.
|
|
133
|
+
`pyarrow` is optional: install it with `pip install deepdiff-rs[arrow]`. It is needed only for `to_pyarrow()` and for passing pyarrow objects in — importing `deepdiff_rs` and diffing polars or DuckDB tables need it not at all. Passing an object that implements neither Arrow protocol raises `TypeError`; calling `to_pyarrow()` without pyarrow installed raises `ImportError` naming the extra. `diff.to_json()` gives the whole diff — schema, summary, and `rows_added`/`rows_removed`/`cells_changed`/`duplicate_keys` in full, one JSON object per row — as a single string with no pyarrow, polars, or pandas needed at all; see [Known limitations](#known-limitations) for its row cap.
|
|
136
134
|
|
|
137
135
|
## Performance
|
|
138
136
|
|
|
@@ -188,7 +186,7 @@ Both reports carry their full methodology, fairness rules, and the reproduce com
|
|
|
188
186
|
- `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` (see [Known limitations](#known-limitations) for the exact restrictions and exclusions, including which types a `dict` key may be); `.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 — 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)]`).
|
|
189
187
|
- `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.
|
|
190
188
|
- `MaxDepthError` (a `ValueError` subclass) is raised when input exceeds `max_depth`; `MAX_DEPTH_CEILING` (20,000) is the hard upper bound on `max_depth`.
|
|
191
|
-
- `diff_tables(left, right, key=[...]) -> TableDiff`: diffs two Arrow tables (see [Diffing tables](#diffing-tables)). `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
|
|
189
|
+
- `diff_tables(left, right, key=[...]) -> TableDiff`: diffs two Arrow tables (see [Diffing tables](#diffing-tables)). `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.
|
|
192
190
|
|
|
193
191
|
**CLI.** `onix diff <a.json> <b.json> [--max-depth N] [--ignore-order] [--timing]` reads both files as JSON and prints a compact, single-line DeepDiff-compatible report to stdout (`{}` when there is no difference).
|
|
194
192
|
|
|
@@ -220,10 +218,11 @@ perf/ # cross-language benchmark harness and RESULTS.md
|
|
|
220
218
|
## Known limitations
|
|
221
219
|
|
|
222
220
|
- Only the core diff is implemented: `exclude_paths`, `significant_digits`, custom operators, `verbose_level != 2`, and delta/patch are not (yet) supported.
|
|
223
|
-
- Supported value types are `None`, `bool`, `int`, `float
|
|
221
|
+
- 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). `int`s must fit in `i64`/`u64`, and anything else — a custom object or an arbitrary-precision `int` — raises `TypeError`/`ValueError` naming the exact path it was found at. 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).
|
|
224
222
|
- A subclass of a supported type (a `tuple`, `set` or `frozenset` subclass including `namedtuple`, a `datetime`/`date`/`time`/`timedelta` subclass such as pandas' `Timestamp`) raises `TypeError` rather than being diffed as its base type, because DeepDiff reports each value's own type name. A `type_changes` entry's `old_type`/`new_type` are type *names* in `to_dict()`, where DeepDiff returns the type objects. Both are described in [`tests/golden/README.md`](tests/golden/README.md).
|
|
225
223
|
- **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.
|
|
226
224
|
- **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.
|
|
225
|
+
- **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 no Python object identity, so two independently-obtained `NaN`s 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.
|
|
227
226
|
- A `str` containing a lone (unpaired) surrogate code point (e.g. `'\udc80'`, legal in Python but not encodable as UTF-8) raises `ValueError` naming the exact path on either side, before the two values are ever compared — including a pair DeepDiff would call equal and report as no change, since DeepDiff's scalar equality is plain Python `==` and never hits the encoding problem; DeepDiff does report a plain change for a *differing* pair, and crashes with an unhandled `UnicodeEncodeError` if such a string is ever hashed (a `set`/`frozenset` member). See [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
228
227
|
- 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.
|
|
229
228
|
- 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).
|
|
@@ -234,6 +233,7 @@ perf/ # cross-language benchmark harness and RESULTS.md
|
|
|
234
233
|
- 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.
|
|
235
234
|
- `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.
|
|
236
235
|
- `diff_tables`'s row diff costs, per call: RAM for a 32-byte hash per row per side (sorted), so peak memory is linear in row count (about 75 MB at 1M rows/side, 660 MB at 10M) and time is `N·log N` (about 2 s for a 10M-row pair), plus a second term for the duplicate-key report, which holds the key values of every *distinct duplicated* key — an all-duplicate 200k-rows/side table (100k distinct duplicated keys) peaks at about 37 MB with 16-byte keys and 1.05 GB with 1 KB keys, 1M rows/side (500k distinct) at about 165 MB with 16-byte keys; a third term for the per-cell diff, which re-reads both inputs, holds the changed rows of both sides, and renders every changed cell to a string held once in the output — so its cost is the number of changed cells times the cell width, not the changed-row count alone (every value is rendered in full). Measured, same method: with two narrow `int64` columns, 85 MB at 1M rows/side with 2% changed and 629 MB with every row changed; with a 1 KB `string` cell changed on every row, 1.18 GB at 100k rows/side and 2.34 GB at 200k (about 11.6 KB per changed row, linear, so on the order of 11.6 GB at 1M). Then temp disk, because each input is re-read several times and so is spooled to an anonymous file (`tempfile`: unlinked at once, mode 0600, no predictable name — nothing is left on disk even on abnormal exit), both spools resident at once, peaking at the decoded size of both inputs (about 315 MB — 161 + 154 MB uncompressed Arrow IPC — for the 1M-row fixture pair; on the order of 10 GB for the full 5 GB-per-side fixture pair, which on Linux may be a RAM-backed `tmpfs`), a full temp filesystem raising `ValueError` naming `TMPDIR`. None of these has a built-in cap: for untrusted input, bound the row count, the changed fraction, and the column widths (key columns for duplicate-heavy data, changed cells — rendered in full — for wide value columns). Figures are the peak resident set of `cargo run -p onix-arrow --release --example row_diff_rss`, single run, macOS on an Apple M-series laptop, 2026-09-05, same method as [Performance](#performance). See [`crates/onix-arrow/src/row_diff.rs`](crates/onix-arrow/src/row_diff.rs).
|
|
236
|
+
- `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 row-diff cost 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).
|
|
237
237
|
- `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`.
|
|
238
238
|
- 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.
|
|
239
239
|
|
|
@@ -121,6 +121,25 @@ pub enum TableDiffError {
|
|
|
121
121
|
/// The column whose two renderings were equal.
|
|
122
122
|
column: String,
|
|
123
123
|
},
|
|
124
|
+
/// [`crate::TableDiff::to_json`] would embed more row objects than
|
|
125
|
+
/// [`crate::MAX_JSON_ROWS`] allows (see its own doc for what that caps
|
|
126
|
+
/// and why). Use the Arrow-returning members instead, or export the
|
|
127
|
+
/// batches directly, for a diff this large.
|
|
128
|
+
TooManyJsonRows {
|
|
129
|
+
/// The number of row objects `to_json()` would have embedded.
|
|
130
|
+
rows: usize,
|
|
131
|
+
/// The cap that was exceeded.
|
|
132
|
+
max: usize,
|
|
133
|
+
},
|
|
134
|
+
/// [`crate::TableDiff::to_json`]'s `serde_json` serialization failed.
|
|
135
|
+
/// Its input is a fixed set of already-rendered strings, numbers, and
|
|
136
|
+
/// nested objects/arrays, so this does not happen in practice; it is a
|
|
137
|
+
/// typed error rather than a panic because a public API must return,
|
|
138
|
+
/// not abort, on an unexpected failure.
|
|
139
|
+
Json {
|
|
140
|
+
/// The underlying `serde_json` error's message.
|
|
141
|
+
message: String,
|
|
142
|
+
},
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
impl fmt::Display for TableDiffError {
|
|
@@ -170,6 +189,15 @@ impl fmt::Display for TableDiffError {
|
|
|
170
189
|
"internal invariant: a value change in column {column:?} rendered identically \
|
|
171
190
|
on both sides, which the common-form rendering is designed to prevent"
|
|
172
191
|
),
|
|
192
|
+
TableDiffError::TooManyJsonRows { rows, max } => write!(
|
|
193
|
+
f,
|
|
194
|
+
"to_json() would embed {rows} row objects, more than the {max}-row cap; use \
|
|
195
|
+
the Arrow-returning members (rows_added(), rows_removed(), cells_changed(), \
|
|
196
|
+
duplicate_keys()) or export the batches directly for a diff this large"
|
|
197
|
+
),
|
|
198
|
+
TableDiffError::Json { message } => {
|
|
199
|
+
write!(f, "failed to serialize the table diff to JSON: {message}")
|
|
200
|
+
}
|
|
173
201
|
}
|
|
174
202
|
}
|
|
175
203
|
}
|
|
@@ -278,6 +306,27 @@ mod tests {
|
|
|
278
306
|
assert!(message.contains("changed-row count"));
|
|
279
307
|
}
|
|
280
308
|
|
|
309
|
+
#[test]
|
|
310
|
+
fn too_many_json_rows_message_names_count_and_cap() {
|
|
311
|
+
let error = TableDiffError::TooManyJsonRows {
|
|
312
|
+
rows: 20_000,
|
|
313
|
+
max: 10_000,
|
|
314
|
+
};
|
|
315
|
+
let message = error.to_string();
|
|
316
|
+
assert!(message.contains("20000"));
|
|
317
|
+
assert!(message.contains("10000-row cap"));
|
|
318
|
+
assert!(message.contains("rows_added()"));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[test]
|
|
322
|
+
fn json_message_names_the_underlying_error() {
|
|
323
|
+
let error = TableDiffError::Json {
|
|
324
|
+
message: "unexpected end of input".to_string(),
|
|
325
|
+
};
|
|
326
|
+
let message = error.to_string();
|
|
327
|
+
assert!(message.contains("unexpected end of input"));
|
|
328
|
+
}
|
|
329
|
+
|
|
281
330
|
#[test]
|
|
282
331
|
fn error_implements_std_error() {
|
|
283
332
|
let error = TableDiffError::EmptyKey;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
//! Renders an Arrow [`RecordBatch`] to a JSON array of row objects, for
|
|
2
|
+
//! [`crate::TableDiff::to_json`]. Reuses [`crate::row_diff::SideRenderer`],
|
|
3
|
+
//! the same renderer [`crate::TableDiff::cells_changed`]'s `old_value`/
|
|
4
|
+
//! `new_value` use, so no cell renders two different ways in the crate.
|
|
5
|
+
//!
|
|
6
|
+
//! [`MAX_JSON_ROWS`] bounds the row count: the JSON holds one object per row
|
|
7
|
+
//! and one owned string per non-null cell, so memory is proportional to
|
|
8
|
+
//! rows × columns × cell width, uncapped past that row count otherwise. A
|
|
9
|
+
//! batch over the cap is refused with [`crate::TableDiffError::TooManyJsonRows`].
|
|
10
|
+
|
|
11
|
+
use arrow_array::RecordBatch;
|
|
12
|
+
use arrow_cast::display::FormatOptions;
|
|
13
|
+
use serde_json::{Map, Value as JsonValue};
|
|
14
|
+
|
|
15
|
+
use crate::error::TableDiffError;
|
|
16
|
+
use crate::row_diff::{SideRenderer, cell_is_null};
|
|
17
|
+
|
|
18
|
+
/// The maximum number of row objects this module will render for one
|
|
19
|
+
/// [`RecordBatch`]; see the module doc for what this bounds.
|
|
20
|
+
pub const MAX_JSON_ROWS: usize = 10_000;
|
|
21
|
+
|
|
22
|
+
/// Renders every row of `batch` to a JSON object keyed by column name, or
|
|
23
|
+
/// [`TableDiffError::TooManyJsonRows`] if `batch` has more than
|
|
24
|
+
/// [`MAX_JSON_ROWS`] rows. A null cell renders as JSON `null`; every other
|
|
25
|
+
/// cell renders through [`SideRenderer`], the same renderer
|
|
26
|
+
/// [`crate::TableDiff::cells_changed`] uses.
|
|
27
|
+
///
|
|
28
|
+
/// # Errors
|
|
29
|
+
///
|
|
30
|
+
/// - [`TableDiffError::TooManyJsonRows`] if `batch.num_rows()` exceeds
|
|
31
|
+
/// [`MAX_JSON_ROWS`].
|
|
32
|
+
/// - [`TableDiffError::Render`] if a cell cannot be rendered to its
|
|
33
|
+
/// canonical string (naming the column), the same failure
|
|
34
|
+
/// [`crate::TableDiff::cells_changed`] can raise.
|
|
35
|
+
pub(crate) fn rows_to_json(batch: &RecordBatch) -> Result<Vec<JsonValue>, TableDiffError> {
|
|
36
|
+
if batch.num_rows() > MAX_JSON_ROWS {
|
|
37
|
+
return Err(TableDiffError::TooManyJsonRows {
|
|
38
|
+
rows: batch.num_rows(),
|
|
39
|
+
max: MAX_JSON_ROWS,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let format_options = FormatOptions::default();
|
|
44
|
+
let schema = batch.schema();
|
|
45
|
+
let columns = schema
|
|
46
|
+
.fields()
|
|
47
|
+
.iter()
|
|
48
|
+
.zip(batch.columns())
|
|
49
|
+
.map(|(field, array)| {
|
|
50
|
+
SideRenderer::new(array, &format_options)
|
|
51
|
+
.map(|renderer| (field.name(), array, renderer))
|
|
52
|
+
})
|
|
53
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
54
|
+
|
|
55
|
+
let mut rows = Vec::with_capacity(batch.num_rows());
|
|
56
|
+
for row in 0..batch.num_rows() {
|
|
57
|
+
let mut object = Map::with_capacity(columns.len());
|
|
58
|
+
for (name, array, renderer) in &columns {
|
|
59
|
+
let value = if cell_is_null(array, row) {
|
|
60
|
+
JsonValue::Null
|
|
61
|
+
} else {
|
|
62
|
+
JsonValue::String(renderer.render(row, name)?)
|
|
63
|
+
};
|
|
64
|
+
object.insert((*name).clone(), value);
|
|
65
|
+
}
|
|
66
|
+
rows.push(JsonValue::Object(object));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Ok(rows)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[cfg(test)]
|
|
73
|
+
mod tests {
|
|
74
|
+
use std::sync::Arc;
|
|
75
|
+
|
|
76
|
+
use arrow_array::{Int64Array, RecordBatch, StringArray};
|
|
77
|
+
use arrow_schema::{DataType, Field, Schema};
|
|
78
|
+
|
|
79
|
+
use super::{MAX_JSON_ROWS, rows_to_json};
|
|
80
|
+
use crate::error::TableDiffError;
|
|
81
|
+
|
|
82
|
+
fn batch(rows: usize) -> RecordBatch {
|
|
83
|
+
let schema = Arc::new(Schema::new(vec![
|
|
84
|
+
Field::new("id", DataType::Int64, false),
|
|
85
|
+
Field::new("name", DataType::Utf8, true),
|
|
86
|
+
]));
|
|
87
|
+
let ids: Vec<i64> = (0..rows).map(|i| i64::try_from(i).unwrap()).collect();
|
|
88
|
+
let names: Vec<Option<String>> = (0..rows)
|
|
89
|
+
.map(|i| {
|
|
90
|
+
if i % 2 == 0 {
|
|
91
|
+
None
|
|
92
|
+
} else {
|
|
93
|
+
Some(format!("n{i}"))
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
.collect();
|
|
97
|
+
RecordBatch::try_new(
|
|
98
|
+
schema,
|
|
99
|
+
vec![
|
|
100
|
+
Arc::new(Int64Array::from(ids)),
|
|
101
|
+
Arc::new(StringArray::from(names)),
|
|
102
|
+
],
|
|
103
|
+
)
|
|
104
|
+
.unwrap()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn renders_one_object_per_row_with_column_keys() {
|
|
109
|
+
let rows = rows_to_json(&batch(2)).unwrap();
|
|
110
|
+
assert_eq!(rows.len(), 2);
|
|
111
|
+
assert_eq!(rows[0]["id"], serde_json::json!("0"));
|
|
112
|
+
assert_eq!(rows[0]["name"], serde_json::Value::Null);
|
|
113
|
+
assert_eq!(rows[1]["id"], serde_json::json!("1"));
|
|
114
|
+
assert_eq!(rows[1]["name"], serde_json::json!("n1"));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[test]
|
|
118
|
+
fn empty_batch_renders_no_rows() {
|
|
119
|
+
assert_eq!(
|
|
120
|
+
rows_to_json(&batch(0)).unwrap(),
|
|
121
|
+
Vec::<serde_json::Value>::new()
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[test]
|
|
126
|
+
fn batch_over_the_cap_is_refused() {
|
|
127
|
+
let error = rows_to_json(&batch(MAX_JSON_ROWS + 1)).unwrap_err();
|
|
128
|
+
assert_eq!(
|
|
129
|
+
error,
|
|
130
|
+
TableDiffError::TooManyJsonRows {
|
|
131
|
+
rows: MAX_JSON_ROWS + 1,
|
|
132
|
+
max: MAX_JSON_ROWS,
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[test]
|
|
138
|
+
fn batch_at_the_cap_is_accepted() {
|
|
139
|
+
assert!(rows_to_json(&batch(MAX_JSON_ROWS)).is_ok());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -102,12 +102,14 @@
|
|
|
102
102
|
//! ```
|
|
103
103
|
|
|
104
104
|
mod error;
|
|
105
|
+
mod json_rows;
|
|
105
106
|
mod options;
|
|
106
107
|
mod row_diff;
|
|
107
108
|
mod schema;
|
|
108
109
|
mod table_diff;
|
|
109
110
|
|
|
110
111
|
pub use error::{Side, TableDiffError};
|
|
112
|
+
pub use json_rows::MAX_JSON_ROWS;
|
|
111
113
|
pub use options::TableDiffOptions;
|
|
112
114
|
pub use row_diff::{MemoryInput, TableInput};
|
|
113
115
|
pub use schema::{ChangeKind, SchemaChange, diff_schemas};
|