jsonl-diff 0.1.1__tar.gz → 0.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonl-diff
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A lightweight, pure-Python tool for comparing large JSONL/NDJSON datasets without loading them entirely into memory.
5
5
  Author-email: rmoralespp <rmoralespp@gmail.com>
6
6
  License: MIT License
@@ -51,6 +51,8 @@ Description-Content-Type: text/markdown
51
51
  License-File: LICENSE
52
52
  Requires-Dist: py-jsonl==1.5.2
53
53
  Requires-Dist: jmespath>=1.0.0
54
+ Provides-Extra: speedups
55
+ Requires-Dist: msgspec>=0.18; python_version >= "3.10" and extra == "speedups"
54
56
  Dynamic: license-file
55
57
 
56
58
  # jsonl-diff
@@ -63,15 +65,11 @@ Dynamic: license-file
63
65
  <img src="https://img.shields.io/github/license/rmoralespp/jsonl-diff.svg" alt="License">
64
66
  </p>
65
67
 
66
- Compare large JSONL/NDJSON datasets **by record identity instead of line
68
+ A lightweight, pure-Python tool for comparing large [JSONL](https://jsonlines.org/)/[NDJSON](https://github.com/ndjson/ndjson-spec) datasets **by record identity instead of line
67
69
  position**, without loading the complete inputs into memory.
68
70
 
69
71
  `jsonl-diff` matches records using one or more top-level fields and reports:
70
-
71
- * `equal`
72
- * `added`
73
- * `deleted`
74
- * `modified`
72
+ `equal`, `added`, `deleted`, `modified`, and tolerated duplicates in each source.
75
73
 
76
74
  It is useful for snapshots, ETL validation, migrations, exports, and CI checks.
77
75
 
@@ -79,12 +77,23 @@ It is useful for snapshots, ETL validation, migrations, exports, and CI checks.
79
77
 
80
78
  ## Installation
81
79
 
82
- Python 3.8–3.14.
80
+ ```bash
81
+ pip install jsonl-diff
82
+ ```
83
+
84
+ ### Optional speedups
85
+
86
+ For large datasets, install the optional [`msgspec`](https://jcristharif.com/msgspec/)
87
+ accelerator (Python 3.10+), which uses a C parser and encoder for JSON decoding
88
+ and record canonicalization, speeding up large diffs by roughly **2x**:
83
89
 
84
90
  ```bash
85
- python -m pip install jsonl-diff
91
+ pip install "jsonl-diff[speedups]"
86
92
  ```
87
93
 
94
+ It is used automatically when available — no configuration or code changes. When
95
+ `msgspec` is absent, the pure-Python path runs instead and produces identical results.
96
+
88
97
  ## Quick start
89
98
 
90
99
  Given:
@@ -114,6 +123,8 @@ Records:
114
123
  added: 1
115
124
  deleted: 0
116
125
  modified: 1
126
+ OLD duplicates: 0
127
+ NEW duplicates: 0
117
128
  ```
118
129
 
119
130
  Record order does not matter.
@@ -121,14 +132,16 @@ Record order does not matter.
121
132
  ## Features
122
133
 
123
134
  * Single or composite top-level identities.
124
- * Disk-backed comparison for large datasets ([architecture](docs/architecture.md)).
125
- * Optional JMESPath filtering with `--where`.
126
- * Configurable duplicate handling: `error`, `first`, or `last`.
127
- * Exact RFC 6901 JSON Pointer ignores.
135
+ * Disk-backed comparison for large datasets ([architecture](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/architecture.md)).
136
+ * Optional **[JMESPath](https://jmespath.org/)** filtering with `--where`.
137
+ * Configurable duplicate handling with counts and diagnostics: `error`, `first`, or `last`.
138
+ * Exact **[RFC 6901](https://www.rfc-editor.org/info/rfc6901/)** JSON Pointer ignores.
139
+ * Optional disk-backed observed-schema diff for fields, types, nullability, and requiredness.
128
140
  * Semantic number comparison using `Decimal`.
141
+ * Optional `msgspec` accelerator for ~2x faster large diffs (`pip install "jsonl-diff[speedups]"`).
129
142
  * Deterministic summaries and change iteration.
130
143
  * Original OLD/NEW physical line numbers.
131
- * Machine-readable JSONL change log with `--details` ([format](docs/details-format.md)).
144
+ * Machine-readable JSONL change log with `--details` ([format](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/details-format.md)).
132
145
  * Local, HTTP/HTTPS, file-like, and supported compressed sources.
133
146
  * CLI and Python API using the same comparison engine.
134
147
 
@@ -137,7 +150,7 @@ Record order does not matter.
137
150
  ```text
138
151
  jsonl-diff [-h] --key KEY [--ignore IGNORE] [--where EXPRESSION]
139
152
  [--duplicates {error,first,last}] [--details FILE] [--quiet]
140
- [--max-temp MAX_TEMP]
153
+ [--schema-diff] [--schema-ignore POINTER] [--max-temp MAX_TEMP]
141
154
  old new
142
155
  ```
143
156
 
@@ -147,10 +160,12 @@ jsonl-diff [-h] --key KEY [--ignore IGNORE] [--where EXPRESSION]
147
160
  | `--key KEY` | Required top-level identity field; repeat or comma-separate for composite keys |
148
161
  | `--ignore POINTER` | RFC 6901 pointer to exclude from content comparison |
149
162
  | `--where EXPRESSION` | JMESPath filter applied to each record |
150
- | `--duplicates POLICY` | `error` (default), `first`, or `last` |
163
+ | `--duplicates POLICY` | `error` (default), or select and report duplicates with `first`/`last` |
151
164
  | `--details FILE` | Write deterministic machine-readable JSONL changes |
165
+ | `--schema-diff` | Compare observed fields, types, nullability, and requiredness |
166
+ | `--schema-ignore` | RFC 6901 pointer to exclude from observed-schema profiling |
152
167
  | `--quiet` | Suppress the normal summary |
153
- | `--max-temp BYTES` | Limit `jsonl-diff` temporary storage |
168
+ | `--max-temp BYTES` | Best-effort budget for `jsonl-diff` workspace temporary storage |
154
169
 
155
170
  Examples:
156
171
 
@@ -169,18 +184,24 @@ jsonl-diff old.jsonl new.jsonl \
169
184
  --key id \
170
185
  --where 'deleted_at == `null`'
171
186
 
187
+ # Detect observed schema drift
188
+ jsonl-diff old.jsonl new.jsonl \
189
+ --key id \
190
+ --schema-diff \
191
+ --schema-ignore /metadata
192
+
172
193
  # Compressed input
173
194
  jsonl-diff old.jsonl.xz new.jsonl.gz --key id
174
195
  ```
175
196
 
176
197
  ### Exit codes
177
198
 
178
- | Code | Meaning |
179
- | ---: | ----------------------------------------- |
180
- | `0` | Inputs are equal |
181
- | `1` | Added, deleted, or modified records found |
182
- | `2` | Input, resource, output, or runtime error |
183
- | `3` | Invalid CLI configuration or usage |
199
+ | Code | Meaning |
200
+ | ---: | ---------------------------------------------------------- |
201
+ | `0` | No record, duplicate, or requested observed-schema issues |
202
+ | `1` | Record differences, tolerated duplicates, or schema changes |
203
+ | `2` | Input, resource, output, or runtime error |
204
+ | `3` | Invalid CLI configuration or usage |
184
205
 
185
206
  ## Python API
186
207
 
@@ -193,11 +214,15 @@ with diff(
193
214
  key=("country", "customerId"),
194
215
  ignore=("/updated_at",),
195
216
  where='country == `"ES"`',
217
+ schema_diff=True,
196
218
  ) as result:
197
219
  print(result.summary)
198
220
 
199
221
  for change in result.changes(ChangeOperation.MODIFIED):
200
222
  print(change.key, change.old_line, change.new_line)
223
+
224
+ for change in result.schema_changes():
225
+ print(change.operation, change.path)
201
226
  ```
202
227
 
203
228
  `diff()` returns a disk-backed `DiffResult`, used as a context manager.
@@ -220,7 +245,7 @@ The public exception hierarchy is rooted at `JsonlDiffError`:
220
245
  * `DuplicateKeyError`
221
246
  * `ResourceError`
222
247
 
223
- See the [Python API reference](docs/python-api.md) for the full callable
248
+ See the [Python API reference](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/python-api.md) for the full callable
224
249
  signature, result models, `Decimal` key semantics, and error hierarchy.
225
250
 
226
251
  ## Comparison semantics
@@ -228,18 +253,22 @@ signature, result models, `Decimal` key semantics, and error hierarchy.
228
253
  `jsonl-diff` is intentionally strict:
229
254
 
230
255
  * Every input line must contain exactly one valid top-level JSON object.
231
- * Blank lines, malformed JSON, duplicate properties, `NaN`, and infinities are rejected.
256
+ * Blank lines, malformed JSON, `NaN`, and infinities are rejected.
257
+ * Duplicate object property names within a record follow JSON's last-wins semantics: the last occurrence is kept.
232
258
  * Identity fields must be top-level scalar values (`null` allowed only as one component of a composite key).
233
259
  * Identity types remain significant: `"1"` ≠ `1`, `true` ≠ `1`.
234
- * Duplicate identities fail by default; `--duplicates first`/`last` tolerate them.
260
+ * Duplicate identities fail by default; `--duplicates first`/`last` select one
261
+ occurrence, report every discarded occurrence, and return exit code `1`.
235
262
  * Object property order is ignored; array order is significant.
236
263
  * Numbers are compared by mathematical value: `1`, `1.0`, and `1e0` are equal.
237
264
  * Unicode strings are compared without normalization.
238
265
  * Changes are reported in deterministic identity order.
239
266
 
240
267
  `--where` selects which records participate; `--key` defines identity;
241
- `--ignore` removes fields from content comparison. See
242
- [Comparison semantics](docs/comparison-semantics.md) for the full rules,
268
+ `--ignore` removes fields from content comparison. With `--schema-diff`,
269
+ `--schema-ignore` independently removes fields from observed-schema profiling.
270
+ See
271
+ [Comparison semantics](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/comparison-semantics.md) for the full rules,
243
272
  including duplicate handling, ignore-pointer edge cases, `--where` evaluation
244
273
  order, and canonical number formatting.
245
274
 
@@ -255,7 +284,7 @@ Supported compression:
255
284
  * xz
256
285
  * Zstandard on Python 3.14
257
286
 
258
- See [architecture](docs/architecture.md) for the full source/compression
287
+ See [architecture](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/architecture.md) for the full source/compression
259
288
  support matrix and how sources are delegated to `py-jsonl`.
260
289
 
261
290
  ## Limitations
@@ -268,7 +297,7 @@ support matrix and how sources are delegated to `py-jsonl`.
268
297
  * nested identities or automatic key detection
269
298
  * fuzzy matching or numeric tolerances
270
299
  * unordered-array comparison
271
- * schema validation or input repair
300
+ * validation against a declared schema or input repair
272
301
  * ZIP, database, or cloud-provider inputs
273
302
  * GUI or HTML reports
274
303
 
@@ -276,10 +305,10 @@ It is a **dataset reconciliation tool**, not a general-purpose visual JSON diff.
276
305
 
277
306
  ## Further reading
278
307
 
279
- * [Comparison semantics](docs/comparison-semantics.md) — identity, duplicates, ignores, `--where`, canonical numbers, determinism.
280
- * [Disk-backed architecture](docs/architecture.md) — SQLite index, `max_temp`, cleanup, sources and compression.
281
- * [Details JSONL format](docs/details-format.md) — machine-readable `--details` output schema.
282
- * [Python API reference](docs/python-api.md) — full signature, result models, and error hierarchy.
308
+ * [Comparison semantics](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/comparison-semantics.md) — identity, duplicates, ignores, `--where`, canonical numbers, determinism.
309
+ * [Disk-backed architecture](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/architecture.md) — SQLite index, `max_temp`, cleanup, sources and compression.
310
+ * [Details JSONL format](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/details-format.md) — machine-readable `--details` output schema.
311
+ * [Python API reference](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/python-api.md) — full signature, result models, and error hierarchy.
283
312
 
284
313
  ## Development
285
314
 
@@ -8,15 +8,11 @@
8
8
  <img src="https://img.shields.io/github/license/rmoralespp/jsonl-diff.svg" alt="License">
9
9
  </p>
10
10
 
11
- Compare large JSONL/NDJSON datasets **by record identity instead of line
11
+ A lightweight, pure-Python tool for comparing large [JSONL](https://jsonlines.org/)/[NDJSON](https://github.com/ndjson/ndjson-spec) datasets **by record identity instead of line
12
12
  position**, without loading the complete inputs into memory.
13
13
 
14
14
  `jsonl-diff` matches records using one or more top-level fields and reports:
15
-
16
- * `equal`
17
- * `added`
18
- * `deleted`
19
- * `modified`
15
+ `equal`, `added`, `deleted`, `modified`, and tolerated duplicates in each source.
20
16
 
21
17
  It is useful for snapshots, ETL validation, migrations, exports, and CI checks.
22
18
 
@@ -24,12 +20,23 @@ It is useful for snapshots, ETL validation, migrations, exports, and CI checks.
24
20
 
25
21
  ## Installation
26
22
 
27
- Python 3.8–3.14.
23
+ ```bash
24
+ pip install jsonl-diff
25
+ ```
26
+
27
+ ### Optional speedups
28
+
29
+ For large datasets, install the optional [`msgspec`](https://jcristharif.com/msgspec/)
30
+ accelerator (Python 3.10+), which uses a C parser and encoder for JSON decoding
31
+ and record canonicalization, speeding up large diffs by roughly **2x**:
28
32
 
29
33
  ```bash
30
- python -m pip install jsonl-diff
34
+ pip install "jsonl-diff[speedups]"
31
35
  ```
32
36
 
37
+ It is used automatically when available — no configuration or code changes. When
38
+ `msgspec` is absent, the pure-Python path runs instead and produces identical results.
39
+
33
40
  ## Quick start
34
41
 
35
42
  Given:
@@ -59,6 +66,8 @@ Records:
59
66
  added: 1
60
67
  deleted: 0
61
68
  modified: 1
69
+ OLD duplicates: 0
70
+ NEW duplicates: 0
62
71
  ```
63
72
 
64
73
  Record order does not matter.
@@ -66,14 +75,16 @@ Record order does not matter.
66
75
  ## Features
67
76
 
68
77
  * Single or composite top-level identities.
69
- * Disk-backed comparison for large datasets ([architecture](docs/architecture.md)).
70
- * Optional JMESPath filtering with `--where`.
71
- * Configurable duplicate handling: `error`, `first`, or `last`.
72
- * Exact RFC 6901 JSON Pointer ignores.
78
+ * Disk-backed comparison for large datasets ([architecture](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/architecture.md)).
79
+ * Optional **[JMESPath](https://jmespath.org/)** filtering with `--where`.
80
+ * Configurable duplicate handling with counts and diagnostics: `error`, `first`, or `last`.
81
+ * Exact **[RFC 6901](https://www.rfc-editor.org/info/rfc6901/)** JSON Pointer ignores.
82
+ * Optional disk-backed observed-schema diff for fields, types, nullability, and requiredness.
73
83
  * Semantic number comparison using `Decimal`.
84
+ * Optional `msgspec` accelerator for ~2x faster large diffs (`pip install "jsonl-diff[speedups]"`).
74
85
  * Deterministic summaries and change iteration.
75
86
  * Original OLD/NEW physical line numbers.
76
- * Machine-readable JSONL change log with `--details` ([format](docs/details-format.md)).
87
+ * Machine-readable JSONL change log with `--details` ([format](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/details-format.md)).
77
88
  * Local, HTTP/HTTPS, file-like, and supported compressed sources.
78
89
  * CLI and Python API using the same comparison engine.
79
90
 
@@ -82,7 +93,7 @@ Record order does not matter.
82
93
  ```text
83
94
  jsonl-diff [-h] --key KEY [--ignore IGNORE] [--where EXPRESSION]
84
95
  [--duplicates {error,first,last}] [--details FILE] [--quiet]
85
- [--max-temp MAX_TEMP]
96
+ [--schema-diff] [--schema-ignore POINTER] [--max-temp MAX_TEMP]
86
97
  old new
87
98
  ```
88
99
 
@@ -92,10 +103,12 @@ jsonl-diff [-h] --key KEY [--ignore IGNORE] [--where EXPRESSION]
92
103
  | `--key KEY` | Required top-level identity field; repeat or comma-separate for composite keys |
93
104
  | `--ignore POINTER` | RFC 6901 pointer to exclude from content comparison |
94
105
  | `--where EXPRESSION` | JMESPath filter applied to each record |
95
- | `--duplicates POLICY` | `error` (default), `first`, or `last` |
106
+ | `--duplicates POLICY` | `error` (default), or select and report duplicates with `first`/`last` |
96
107
  | `--details FILE` | Write deterministic machine-readable JSONL changes |
108
+ | `--schema-diff` | Compare observed fields, types, nullability, and requiredness |
109
+ | `--schema-ignore` | RFC 6901 pointer to exclude from observed-schema profiling |
97
110
  | `--quiet` | Suppress the normal summary |
98
- | `--max-temp BYTES` | Limit `jsonl-diff` temporary storage |
111
+ | `--max-temp BYTES` | Best-effort budget for `jsonl-diff` workspace temporary storage |
99
112
 
100
113
  Examples:
101
114
 
@@ -114,18 +127,24 @@ jsonl-diff old.jsonl new.jsonl \
114
127
  --key id \
115
128
  --where 'deleted_at == `null`'
116
129
 
130
+ # Detect observed schema drift
131
+ jsonl-diff old.jsonl new.jsonl \
132
+ --key id \
133
+ --schema-diff \
134
+ --schema-ignore /metadata
135
+
117
136
  # Compressed input
118
137
  jsonl-diff old.jsonl.xz new.jsonl.gz --key id
119
138
  ```
120
139
 
121
140
  ### Exit codes
122
141
 
123
- | Code | Meaning |
124
- | ---: | ----------------------------------------- |
125
- | `0` | Inputs are equal |
126
- | `1` | Added, deleted, or modified records found |
127
- | `2` | Input, resource, output, or runtime error |
128
- | `3` | Invalid CLI configuration or usage |
142
+ | Code | Meaning |
143
+ | ---: | ---------------------------------------------------------- |
144
+ | `0` | No record, duplicate, or requested observed-schema issues |
145
+ | `1` | Record differences, tolerated duplicates, or schema changes |
146
+ | `2` | Input, resource, output, or runtime error |
147
+ | `3` | Invalid CLI configuration or usage |
129
148
 
130
149
  ## Python API
131
150
 
@@ -138,11 +157,15 @@ with diff(
138
157
  key=("country", "customerId"),
139
158
  ignore=("/updated_at",),
140
159
  where='country == `"ES"`',
160
+ schema_diff=True,
141
161
  ) as result:
142
162
  print(result.summary)
143
163
 
144
164
  for change in result.changes(ChangeOperation.MODIFIED):
145
165
  print(change.key, change.old_line, change.new_line)
166
+
167
+ for change in result.schema_changes():
168
+ print(change.operation, change.path)
146
169
  ```
147
170
 
148
171
  `diff()` returns a disk-backed `DiffResult`, used as a context manager.
@@ -165,7 +188,7 @@ The public exception hierarchy is rooted at `JsonlDiffError`:
165
188
  * `DuplicateKeyError`
166
189
  * `ResourceError`
167
190
 
168
- See the [Python API reference](docs/python-api.md) for the full callable
191
+ See the [Python API reference](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/python-api.md) for the full callable
169
192
  signature, result models, `Decimal` key semantics, and error hierarchy.
170
193
 
171
194
  ## Comparison semantics
@@ -173,18 +196,22 @@ signature, result models, `Decimal` key semantics, and error hierarchy.
173
196
  `jsonl-diff` is intentionally strict:
174
197
 
175
198
  * Every input line must contain exactly one valid top-level JSON object.
176
- * Blank lines, malformed JSON, duplicate properties, `NaN`, and infinities are rejected.
199
+ * Blank lines, malformed JSON, `NaN`, and infinities are rejected.
200
+ * Duplicate object property names within a record follow JSON's last-wins semantics: the last occurrence is kept.
177
201
  * Identity fields must be top-level scalar values (`null` allowed only as one component of a composite key).
178
202
  * Identity types remain significant: `"1"` ≠ `1`, `true` ≠ `1`.
179
- * Duplicate identities fail by default; `--duplicates first`/`last` tolerate them.
203
+ * Duplicate identities fail by default; `--duplicates first`/`last` select one
204
+ occurrence, report every discarded occurrence, and return exit code `1`.
180
205
  * Object property order is ignored; array order is significant.
181
206
  * Numbers are compared by mathematical value: `1`, `1.0`, and `1e0` are equal.
182
207
  * Unicode strings are compared without normalization.
183
208
  * Changes are reported in deterministic identity order.
184
209
 
185
210
  `--where` selects which records participate; `--key` defines identity;
186
- `--ignore` removes fields from content comparison. See
187
- [Comparison semantics](docs/comparison-semantics.md) for the full rules,
211
+ `--ignore` removes fields from content comparison. With `--schema-diff`,
212
+ `--schema-ignore` independently removes fields from observed-schema profiling.
213
+ See
214
+ [Comparison semantics](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/comparison-semantics.md) for the full rules,
188
215
  including duplicate handling, ignore-pointer edge cases, `--where` evaluation
189
216
  order, and canonical number formatting.
190
217
 
@@ -200,7 +227,7 @@ Supported compression:
200
227
  * xz
201
228
  * Zstandard on Python 3.14
202
229
 
203
- See [architecture](docs/architecture.md) for the full source/compression
230
+ See [architecture](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/architecture.md) for the full source/compression
204
231
  support matrix and how sources are delegated to `py-jsonl`.
205
232
 
206
233
  ## Limitations
@@ -213,7 +240,7 @@ support matrix and how sources are delegated to `py-jsonl`.
213
240
  * nested identities or automatic key detection
214
241
  * fuzzy matching or numeric tolerances
215
242
  * unordered-array comparison
216
- * schema validation or input repair
243
+ * validation against a declared schema or input repair
217
244
  * ZIP, database, or cloud-provider inputs
218
245
  * GUI or HTML reports
219
246
 
@@ -221,10 +248,10 @@ It is a **dataset reconciliation tool**, not a general-purpose visual JSON diff.
221
248
 
222
249
  ## Further reading
223
250
 
224
- * [Comparison semantics](docs/comparison-semantics.md) — identity, duplicates, ignores, `--where`, canonical numbers, determinism.
225
- * [Disk-backed architecture](docs/architecture.md) — SQLite index, `max_temp`, cleanup, sources and compression.
226
- * [Details JSONL format](docs/details-format.md) — machine-readable `--details` output schema.
227
- * [Python API reference](docs/python-api.md) — full signature, result models, and error hierarchy.
251
+ * [Comparison semantics](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/comparison-semantics.md) — identity, duplicates, ignores, `--where`, canonical numbers, determinism.
252
+ * [Disk-backed architecture](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/architecture.md) — SQLite index, `max_temp`, cleanup, sources and compression.
253
+ * [Details JSONL format](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/details-format.md) — machine-readable `--details` output schema.
254
+ * [Python API reference](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/python-api.md) — full signature, result models, and error hierarchy.
228
255
 
229
256
  ## Development
230
257
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonl-diff
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A lightweight, pure-Python tool for comparing large JSONL/NDJSON datasets without loading them entirely into memory.
5
5
  Author-email: rmoralespp <rmoralespp@gmail.com>
6
6
  License: MIT License
@@ -51,6 +51,8 @@ Description-Content-Type: text/markdown
51
51
  License-File: LICENSE
52
52
  Requires-Dist: py-jsonl==1.5.2
53
53
  Requires-Dist: jmespath>=1.0.0
54
+ Provides-Extra: speedups
55
+ Requires-Dist: msgspec>=0.18; python_version >= "3.10" and extra == "speedups"
54
56
  Dynamic: license-file
55
57
 
56
58
  # jsonl-diff
@@ -63,15 +65,11 @@ Dynamic: license-file
63
65
  <img src="https://img.shields.io/github/license/rmoralespp/jsonl-diff.svg" alt="License">
64
66
  </p>
65
67
 
66
- Compare large JSONL/NDJSON datasets **by record identity instead of line
68
+ A lightweight, pure-Python tool for comparing large [JSONL](https://jsonlines.org/)/[NDJSON](https://github.com/ndjson/ndjson-spec) datasets **by record identity instead of line
67
69
  position**, without loading the complete inputs into memory.
68
70
 
69
71
  `jsonl-diff` matches records using one or more top-level fields and reports:
70
-
71
- * `equal`
72
- * `added`
73
- * `deleted`
74
- * `modified`
72
+ `equal`, `added`, `deleted`, `modified`, and tolerated duplicates in each source.
75
73
 
76
74
  It is useful for snapshots, ETL validation, migrations, exports, and CI checks.
77
75
 
@@ -79,12 +77,23 @@ It is useful for snapshots, ETL validation, migrations, exports, and CI checks.
79
77
 
80
78
  ## Installation
81
79
 
82
- Python 3.8–3.14.
80
+ ```bash
81
+ pip install jsonl-diff
82
+ ```
83
+
84
+ ### Optional speedups
85
+
86
+ For large datasets, install the optional [`msgspec`](https://jcristharif.com/msgspec/)
87
+ accelerator (Python 3.10+), which uses a C parser and encoder for JSON decoding
88
+ and record canonicalization, speeding up large diffs by roughly **2x**:
83
89
 
84
90
  ```bash
85
- python -m pip install jsonl-diff
91
+ pip install "jsonl-diff[speedups]"
86
92
  ```
87
93
 
94
+ It is used automatically when available — no configuration or code changes. When
95
+ `msgspec` is absent, the pure-Python path runs instead and produces identical results.
96
+
88
97
  ## Quick start
89
98
 
90
99
  Given:
@@ -114,6 +123,8 @@ Records:
114
123
  added: 1
115
124
  deleted: 0
116
125
  modified: 1
126
+ OLD duplicates: 0
127
+ NEW duplicates: 0
117
128
  ```
118
129
 
119
130
  Record order does not matter.
@@ -121,14 +132,16 @@ Record order does not matter.
121
132
  ## Features
122
133
 
123
134
  * Single or composite top-level identities.
124
- * Disk-backed comparison for large datasets ([architecture](docs/architecture.md)).
125
- * Optional JMESPath filtering with `--where`.
126
- * Configurable duplicate handling: `error`, `first`, or `last`.
127
- * Exact RFC 6901 JSON Pointer ignores.
135
+ * Disk-backed comparison for large datasets ([architecture](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/architecture.md)).
136
+ * Optional **[JMESPath](https://jmespath.org/)** filtering with `--where`.
137
+ * Configurable duplicate handling with counts and diagnostics: `error`, `first`, or `last`.
138
+ * Exact **[RFC 6901](https://www.rfc-editor.org/info/rfc6901/)** JSON Pointer ignores.
139
+ * Optional disk-backed observed-schema diff for fields, types, nullability, and requiredness.
128
140
  * Semantic number comparison using `Decimal`.
141
+ * Optional `msgspec` accelerator for ~2x faster large diffs (`pip install "jsonl-diff[speedups]"`).
129
142
  * Deterministic summaries and change iteration.
130
143
  * Original OLD/NEW physical line numbers.
131
- * Machine-readable JSONL change log with `--details` ([format](docs/details-format.md)).
144
+ * Machine-readable JSONL change log with `--details` ([format](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/details-format.md)).
132
145
  * Local, HTTP/HTTPS, file-like, and supported compressed sources.
133
146
  * CLI and Python API using the same comparison engine.
134
147
 
@@ -137,7 +150,7 @@ Record order does not matter.
137
150
  ```text
138
151
  jsonl-diff [-h] --key KEY [--ignore IGNORE] [--where EXPRESSION]
139
152
  [--duplicates {error,first,last}] [--details FILE] [--quiet]
140
- [--max-temp MAX_TEMP]
153
+ [--schema-diff] [--schema-ignore POINTER] [--max-temp MAX_TEMP]
141
154
  old new
142
155
  ```
143
156
 
@@ -147,10 +160,12 @@ jsonl-diff [-h] --key KEY [--ignore IGNORE] [--where EXPRESSION]
147
160
  | `--key KEY` | Required top-level identity field; repeat or comma-separate for composite keys |
148
161
  | `--ignore POINTER` | RFC 6901 pointer to exclude from content comparison |
149
162
  | `--where EXPRESSION` | JMESPath filter applied to each record |
150
- | `--duplicates POLICY` | `error` (default), `first`, or `last` |
163
+ | `--duplicates POLICY` | `error` (default), or select and report duplicates with `first`/`last` |
151
164
  | `--details FILE` | Write deterministic machine-readable JSONL changes |
165
+ | `--schema-diff` | Compare observed fields, types, nullability, and requiredness |
166
+ | `--schema-ignore` | RFC 6901 pointer to exclude from observed-schema profiling |
152
167
  | `--quiet` | Suppress the normal summary |
153
- | `--max-temp BYTES` | Limit `jsonl-diff` temporary storage |
168
+ | `--max-temp BYTES` | Best-effort budget for `jsonl-diff` workspace temporary storage |
154
169
 
155
170
  Examples:
156
171
 
@@ -169,18 +184,24 @@ jsonl-diff old.jsonl new.jsonl \
169
184
  --key id \
170
185
  --where 'deleted_at == `null`'
171
186
 
187
+ # Detect observed schema drift
188
+ jsonl-diff old.jsonl new.jsonl \
189
+ --key id \
190
+ --schema-diff \
191
+ --schema-ignore /metadata
192
+
172
193
  # Compressed input
173
194
  jsonl-diff old.jsonl.xz new.jsonl.gz --key id
174
195
  ```
175
196
 
176
197
  ### Exit codes
177
198
 
178
- | Code | Meaning |
179
- | ---: | ----------------------------------------- |
180
- | `0` | Inputs are equal |
181
- | `1` | Added, deleted, or modified records found |
182
- | `2` | Input, resource, output, or runtime error |
183
- | `3` | Invalid CLI configuration or usage |
199
+ | Code | Meaning |
200
+ | ---: | ---------------------------------------------------------- |
201
+ | `0` | No record, duplicate, or requested observed-schema issues |
202
+ | `1` | Record differences, tolerated duplicates, or schema changes |
203
+ | `2` | Input, resource, output, or runtime error |
204
+ | `3` | Invalid CLI configuration or usage |
184
205
 
185
206
  ## Python API
186
207
 
@@ -193,11 +214,15 @@ with diff(
193
214
  key=("country", "customerId"),
194
215
  ignore=("/updated_at",),
195
216
  where='country == `"ES"`',
217
+ schema_diff=True,
196
218
  ) as result:
197
219
  print(result.summary)
198
220
 
199
221
  for change in result.changes(ChangeOperation.MODIFIED):
200
222
  print(change.key, change.old_line, change.new_line)
223
+
224
+ for change in result.schema_changes():
225
+ print(change.operation, change.path)
201
226
  ```
202
227
 
203
228
  `diff()` returns a disk-backed `DiffResult`, used as a context manager.
@@ -220,7 +245,7 @@ The public exception hierarchy is rooted at `JsonlDiffError`:
220
245
  * `DuplicateKeyError`
221
246
  * `ResourceError`
222
247
 
223
- See the [Python API reference](docs/python-api.md) for the full callable
248
+ See the [Python API reference](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/python-api.md) for the full callable
224
249
  signature, result models, `Decimal` key semantics, and error hierarchy.
225
250
 
226
251
  ## Comparison semantics
@@ -228,18 +253,22 @@ signature, result models, `Decimal` key semantics, and error hierarchy.
228
253
  `jsonl-diff` is intentionally strict:
229
254
 
230
255
  * Every input line must contain exactly one valid top-level JSON object.
231
- * Blank lines, malformed JSON, duplicate properties, `NaN`, and infinities are rejected.
256
+ * Blank lines, malformed JSON, `NaN`, and infinities are rejected.
257
+ * Duplicate object property names within a record follow JSON's last-wins semantics: the last occurrence is kept.
232
258
  * Identity fields must be top-level scalar values (`null` allowed only as one component of a composite key).
233
259
  * Identity types remain significant: `"1"` ≠ `1`, `true` ≠ `1`.
234
- * Duplicate identities fail by default; `--duplicates first`/`last` tolerate them.
260
+ * Duplicate identities fail by default; `--duplicates first`/`last` select one
261
+ occurrence, report every discarded occurrence, and return exit code `1`.
235
262
  * Object property order is ignored; array order is significant.
236
263
  * Numbers are compared by mathematical value: `1`, `1.0`, and `1e0` are equal.
237
264
  * Unicode strings are compared without normalization.
238
265
  * Changes are reported in deterministic identity order.
239
266
 
240
267
  `--where` selects which records participate; `--key` defines identity;
241
- `--ignore` removes fields from content comparison. See
242
- [Comparison semantics](docs/comparison-semantics.md) for the full rules,
268
+ `--ignore` removes fields from content comparison. With `--schema-diff`,
269
+ `--schema-ignore` independently removes fields from observed-schema profiling.
270
+ See
271
+ [Comparison semantics](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/comparison-semantics.md) for the full rules,
243
272
  including duplicate handling, ignore-pointer edge cases, `--where` evaluation
244
273
  order, and canonical number formatting.
245
274
 
@@ -255,7 +284,7 @@ Supported compression:
255
284
  * xz
256
285
  * Zstandard on Python 3.14
257
286
 
258
- See [architecture](docs/architecture.md) for the full source/compression
287
+ See [architecture](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/architecture.md) for the full source/compression
259
288
  support matrix and how sources are delegated to `py-jsonl`.
260
289
 
261
290
  ## Limitations
@@ -268,7 +297,7 @@ support matrix and how sources are delegated to `py-jsonl`.
268
297
  * nested identities or automatic key detection
269
298
  * fuzzy matching or numeric tolerances
270
299
  * unordered-array comparison
271
- * schema validation or input repair
300
+ * validation against a declared schema or input repair
272
301
  * ZIP, database, or cloud-provider inputs
273
302
  * GUI or HTML reports
274
303
 
@@ -276,10 +305,10 @@ It is a **dataset reconciliation tool**, not a general-purpose visual JSON diff.
276
305
 
277
306
  ## Further reading
278
307
 
279
- * [Comparison semantics](docs/comparison-semantics.md) — identity, duplicates, ignores, `--where`, canonical numbers, determinism.
280
- * [Disk-backed architecture](docs/architecture.md) — SQLite index, `max_temp`, cleanup, sources and compression.
281
- * [Details JSONL format](docs/details-format.md) — machine-readable `--details` output schema.
282
- * [Python API reference](docs/python-api.md) — full signature, result models, and error hierarchy.
308
+ * [Comparison semantics](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/comparison-semantics.md) — identity, duplicates, ignores, `--where`, canonical numbers, determinism.
309
+ * [Disk-backed architecture](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/architecture.md) — SQLite index, `max_temp`, cleanup, sources and compression.
310
+ * [Details JSONL format](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/details-format.md) — machine-readable `--details` output schema.
311
+ * [Python API reference](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/python-api.md) — full signature, result models, and error hierarchy.
283
312
 
284
313
  ## Development
285
314