jsonatapy 2.2.12__tar.gz → 2.2.14__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.
Files changed (82) hide show
  1. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/CHANGELOG.md +94 -0
  2. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/Cargo.lock +6 -26
  3. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/Cargo.toml +4 -3
  4. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/PKG-INFO +7 -7
  5. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/README.md +4 -4
  6. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/pyproject.toml +6 -4
  7. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/python/jsonatapy/__main__.py +1 -1
  8. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/builtins.rs +63 -2
  9. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/capi.rs +1 -0
  10. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/evaluator.rs +114 -9
  11. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/functions.rs +448 -2
  12. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/lib.rs +2 -0
  13. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/value.rs +67 -3
  14. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/vm.rs +22 -1
  15. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/integration_test.rs +314 -1
  16. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/.gitignore +0 -0
  17. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/.gitmodules +0 -0
  18. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/.serena/.gitignore +0 -0
  19. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/.serena/memories/architecture.md +0 -0
  20. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/.serena/memories/conventions.md +0 -0
  21. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/.serena/memories/core.md +0 -0
  22. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/.serena/memories/memory_maintenance.md +0 -0
  23. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/.serena/memories/suggested_commands.md +0 -0
  24. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/.serena/memories/task_completion.md +0 -0
  25. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/.serena/memories/tech_stack.md +0 -0
  26. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/CONTRIBUTORS.md +0 -0
  27. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/LICENSE +0 -0
  28. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/SECURITY.md +0 -0
  29. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/benches/evaluator_bench.rs +0 -0
  30. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/bindings/c/README.md +0 -0
  31. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/bindings/c/examples/smoke.c +0 -0
  32. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/bindings/c/jsonata.h +0 -0
  33. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/examples/evaluator_demo.rs +0 -0
  34. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/examples/host_functions.rs +0 -0
  35. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/examples/parser_demo.rs +0 -0
  36. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/examples/simd_json_bench.rs +0 -0
  37. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/python/jsonatapy/__init__.py +0 -0
  38. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/python/jsonatapy/_cli/__init__.py +0 -0
  39. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/python/jsonatapy/_cli/bindings.py +0 -0
  40. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/python/jsonatapy/_cli/error_format.py +0 -0
  41. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/python/jsonatapy/_cli/mcp_server.py +0 -0
  42. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/python/jsonatapy/_cli/resolve.py +0 -0
  43. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/python/jsonatapy/_cli/run.py +0 -0
  44. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/python/jsonatapy/py.typed +0 -0
  45. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/allocator.rs +0 -0
  46. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/ast.rs +0 -0
  47. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/ast_transform.rs +0 -0
  48. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/bin/jsonata/bindings.rs +0 -0
  49. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/bin/jsonata/error_format.rs +0 -0
  50. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/bin/jsonata/main.rs +0 -0
  51. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/bin/jsonata/resolve.rs +0 -0
  52. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/compiler.rs +0 -0
  53. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/datetime.rs +0 -0
  54. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/expression.rs +0 -0
  55. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/lazy.rs +0 -0
  56. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/parser.rs +0 -0
  57. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/pystack.rs +0 -0
  58. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/src/signature.rs +0 -0
  59. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/study/cli_fixtures.json +0 -0
  60. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/study/cli_fixtures_testdata.json +0 -0
  61. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/study/cli_spec.md +0 -0
  62. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/array_flags_test.rs +0 -0
  63. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/cli_fixtures_test.rs +0 -0
  64. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/cli_test.rs +0 -0
  65. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/common/mod.rs +0 -0
  66. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/datetime_picture_suite.rs +0 -0
  67. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/deep_value_drop_test.rs +0 -0
  68. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/evaluator_reuse_test.rs +0 -0
  69. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/fixtures/array_flags_differential.json +0 -0
  70. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/fixtures/builtin_arity.json +0 -0
  71. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/fixtures/builtin_differential.json +0 -0
  72. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/fixtures/builtin_signatures.json +0 -0
  73. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/fixtures/fastpath_differential.json +0 -0
  74. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/fixtures/fastpath_known_divergences.json +0 -0
  75. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/fixtures/path_semantics_combinations.json +0 -0
  76. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/fixtures/path_semantics_combinations_known.json +0 -0
  77. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/fixtures/path_semantics_differential.json +0 -0
  78. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/host_functions_test.rs +0 -0
  79. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/lambda_closure_suite.rs +0 -0
  80. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/parent_and_focus_binding_suite.rs +0 -0
  81. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/path_semantics_test.rs +0 -0
  82. {jsonatapy-2.2.12 → jsonatapy-2.2.14}/tests/step_cache_test.rs +0 -0
@@ -19,6 +19,100 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
19
 
20
20
  ### Security
21
21
 
22
+ ## [2.2.14] - 2026-11-04
23
+
24
+ ### Added
25
+
26
+ ### Changed
27
+
28
+ - **Compiled regexes are reused.** The same regex literal evaluated again, such as once per element
29
+ under `$map`, no longer compiles again; each thread keeps up to 256.
30
+ - Updated Rust dependencies: `simd-json` 0.18.1 → 0.19.0, used by the default `simd` feature. (#274)
31
+ - **The `mcp` extra now needs Python 3.11 or later.** On Python 3.10, `pip install
32
+ 'jsonatapy[mcp]'` installs no `fastmcp`, and `jsonatapy mcp` says the extra needs 3.11. Its
33
+ `dnspython` dependency (through `email-validator`) has no release for 3.10 with the fix for
34
+ GHSA-45c3-73f7-pv4m. The rest of `jsonatapy` still supports 3.10.
35
+
36
+ ### Deprecated
37
+
38
+ ### Removed
39
+
40
+ ### Fixed
41
+
42
+ - **Object keys are in JavaScript's order**: integer-like keys first in ascending order, then the
43
+ rest in insertion order, so `$keys({"b":1,"2":2})` is `["2","b"]` as in jsonata-js. Python
44
+ callers see it too: a dict an expression builds comes back in that order, while a dict passed
45
+ through untouched is the caller's own object and keeps its order. (#273) Thanks to
46
+ @Sawmonabo for finding this and for the fix.
47
+ - **A cancel or a timeout now stops `$distinct` and `$keys` part way.** Both compare each element
48
+ with every one kept so far, and over a large input that ran to the end before U1003 or D1012 was
49
+ seen. (#272) Thanks to @Sawmonabo for the cancel check; the timeout check followed from it.
50
+ - **`\d`, `\w`, `\s` and `\b` in a regex now mean what they mean in jsonata-js**: ASCII digits and
51
+ word characters and ECMAScript's whitespace, so `$contains("٣", /\d/)` is `false`. (#271)
52
+ Thanks to @Sawmonabo for finding and fixing this.
53
+ - **A regex with the `i` flag folds case as jsonata-js does**, by upper case rather than Unicode
54
+ case folding. The Kelvin sign (U+212A) and the long s (U+017F) match only themselves, so
55
+ `$contains("\u212A", /k/i)` and `/[\w]/i` against it are `false`; `ß` and `ẞ` no longer match,
56
+ nor the Ohm and Angstrom signs with `ω` and `å`; and characters outside the BMP no longer fold.
57
+
58
+ ### Security
59
+
60
+ - `uv.lock`: `authlib` 1.7.2 → 1.9.1 (seven advisories), `joserfc` 1.7.3 → 1.7.7
61
+ (GHSA-jpxq-7h8p-2q76, GHSA-9pg5-8c7x-5hfx), and `dnspython` 2.8.0 → 2.9.0
62
+ (GHSA-45c3-73f7-pv4m), which no longer resolves for Python 3.10 now that the `mcp` extra needs
63
+ 3.11 (see Changed). All three are transitive dependencies of the optional `mcp` extra, not
64
+ of the published crate or of the wheel, which has no runtime dependencies. Users of the `mcp`
65
+ extra resolve their own versions and should upgrade these. (#269, #270)
66
+
67
+ ## [2.2.13] - 2026-10-24
68
+
69
+ **Upgrade notes**
70
+
71
+ - **Breaking for Rust code that builds `EvaluatorOptions` field by field.** `EvaluatorOptions`
72
+ has a new public `cancel` field (see Added), so a struct literal that lists every field
73
+ without `..Default::default()` no longer compiles. Add `cancel: None`, or end the literal with
74
+ `..Default::default()`. Strict semver would call this a major change. It ships in a patch
75
+ release because jsonata-core's version tracks the jsonata-js version it targets (2.2), and
76
+ code that uses `..Default::default()` is unaffected. The Python and C APIs are unchanged.
77
+ - **`jsonatapy[mcp]` now needs fastmcp 4.1.2 or later**, which brings `mcp` 2.x (see Changed).
78
+
79
+ ### Added
80
+
81
+ - **An evaluation can be cancelled from another thread.** `EvaluatorOptions` has a new
82
+ `cancel` field that takes a `CancelToken`. Calling `cancel()` on any clone of the token
83
+ stops the evaluation with `U1003: Evaluation cancelled` wherever `timeout_ms` is checked:
84
+ each node evaluated, each element a higher-order builtin hands its callback, each match
85
+ `$replace` hands its replacement function, and each pass of a tail-call loop. A single
86
+ builtin call over a large input finishes first, as it does under `timeout_ms`, and if it is
87
+ the last step the evaluation returns its result. jsonata-js has no cancellation, so the
88
+ code is in the `U` series. This is a Rust API only. See the upgrade notes above for the
89
+ `EvaluatorOptions` source break. (#264) Thanks to @jonking-ajar for the design, the
90
+ implementation and its tests.
91
+
92
+ ### Changed
93
+
94
+ - **The `mcp` extra now needs fastmcp 4.1.2 or later** (was 3.0). fastmcp 4 brings `mcp` 2.x
95
+ and replaces `httpx` with `httpx2`, so an environment that pins `mcp<2` or depends on
96
+ `httpx` alongside `jsonatapy[mcp]` may need updating. The `jsonatapy mcp` server itself is
97
+ unchanged. 4.1.2 is also the first 4.x release with the fix for GHSA-7xgj-8392-5q4r.
98
+
99
+ ### Deprecated
100
+
101
+ ### Removed
102
+
103
+ ### Fixed
104
+
105
+ - **`timeout_ms` now stops `$map`, `$filter`, `$reduce`, `$single`, `$sift`, `$each` and
106
+ `$sort` when their callback is a builtin, host or partial function, and `$replace` when its
107
+ replacement function is one.** Their loops over such a function had no checkpoint, because
108
+ the call evaluates no node, so `$map(items, $string)` over a large array ran to the end past
109
+ its timeout. A lambda callback was already stopped, since its body is checked like any node.
110
+ An expression with `timeout_ms` set that used to finish late in one of these loops and
111
+ return a result now fails with D1012 instead. (#264) Thanks to @jonking-ajar, who found and
112
+ fixed this alongside the cancellation work.
113
+
114
+ ### Security
115
+
22
116
  ## [2.2.12] "Gymkata" - 2026-10-20
23
117
 
24
118
  ### Added
@@ -620,7 +620,7 @@ dependencies = [
620
620
 
621
621
  [[package]]
622
622
  name = "jsonata-core"
623
- version = "2.2.12"
623
+ version = "2.2.14"
624
624
  dependencies = [
625
625
  "assert_cmd",
626
626
  "base64",
@@ -637,6 +637,7 @@ dependencies = [
637
637
  "pyo3",
638
638
  "rand",
639
639
  "regex",
640
+ "regex-syntax",
640
641
  "serde",
641
642
  "serde_json",
642
643
  "simd-json",
@@ -986,26 +987,6 @@ dependencies = [
986
987
  "crossbeam-utils",
987
988
  ]
988
989
 
989
- [[package]]
990
- name = "ref-cast"
991
- version = "1.0.25"
992
- source = "registry+https://github.com/rust-lang/crates.io-index"
993
- checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
994
- dependencies = [
995
- "ref-cast-impl",
996
- ]
997
-
998
- [[package]]
999
- name = "ref-cast-impl"
1000
- version = "1.0.25"
1001
- source = "registry+https://github.com/rust-lang/crates.io-index"
1002
- checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
1003
- dependencies = [
1004
- "proc-macro2",
1005
- "quote",
1006
- "syn 2.0.118",
1007
- ]
1008
-
1009
990
  [[package]]
1010
991
  name = "regex"
1011
992
  version = "1.13.1"
@@ -1134,12 +1115,11 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
1134
1115
 
1135
1116
  [[package]]
1136
1117
  name = "simd-json"
1137
- version = "0.18.1"
1118
+ version = "0.19.0"
1138
1119
  source = "registry+https://github.com/rust-lang/crates.io-index"
1139
- checksum = "6ebe141eb4a23ecc4d5de93fa5b139ac65dc07c38037a2fe9a8fb7c9d36bd832"
1120
+ checksum = "e8f2e4d523646f7da6dde3983bd18ede0babc21c258ab708b3dd93ea0259719d"
1140
1121
  dependencies = [
1141
1122
  "halfbrown",
1142
- "ref-cast",
1143
1123
  "serde",
1144
1124
  "serde_json",
1145
1125
  "simdutf8",
@@ -1304,9 +1284,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1304
1284
 
1305
1285
  [[package]]
1306
1286
  name = "value-trait"
1307
- version = "0.12.1"
1287
+ version = "0.12.3"
1308
1288
  source = "registry+https://github.com/rust-lang/crates.io-index"
1309
- checksum = "8e80f0c733af0720a501b3905d22e2f97662d8eacfe082a75ed7ffb5ab08cb59"
1289
+ checksum = "545a3d7089f11f1741d12963c057adf9443fc24325110a6a6daf582e825fd51b"
1310
1290
  dependencies = [
1311
1291
  "float-cmp",
1312
1292
  "halfbrown",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonata-core"
3
- version = "2.2.12"
3
+ version = "2.2.14"
4
4
  edition = "2021"
5
5
  authors = ["txjmb <txjmb@users.noreply.github.com>"]
6
6
  description = "High-performance Rust implementation of JSONata query and transformation language"
@@ -46,14 +46,15 @@ serde = { version = "1.0", features = ["derive"] }
46
46
  serde_json = { version = "1.0", features = ["preserve_order"] }
47
47
  thiserror = "2.0"
48
48
  regex = "1.12"
49
+ regex-syntax = "0.8"
49
50
  chrono = "0.4"
50
51
  num-traits = "0.2"
51
52
  base64 = "0.23"
52
- indexmap = "2.2"
53
+ indexmap = "2.2.3"
53
54
  percent-encoding = "2.3"
54
55
  rand = "0.10"
55
56
  stacker = "0.1"
56
- simd-json = { version = "0.18", optional = true }
57
+ simd-json = { version = "0.19", optional = true }
57
58
  clap = { version = "4.6", features = ["derive"], optional = true }
58
59
  mimalloc = { version = "0.1.52", optional = true }
59
60
  libmimalloc-sys = { version = "0.1.49", optional = true }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonatapy
3
- Version: 2.2.12
3
+ Version: 2.2.14
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -24,12 +24,12 @@ Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
24
24
  Requires-Dist: ruff>=0.3.0 ; extra == 'dev'
25
25
  Requires-Dist: mypy>=1.0 ; extra == 'dev'
26
26
  Requires-Dist: maturin>=1.0 ; extra == 'dev'
27
- Requires-Dist: fastmcp>=3.0 ; extra == 'dev'
27
+ Requires-Dist: fastmcp>=4.1.2 ; python_full_version >= '3.11' and extra == 'dev'
28
28
  Requires-Dist: mkdocs>=1.5 ; extra == 'docs'
29
29
  Requires-Dist: mkdocs-material>=9.0 ; extra == 'docs'
30
30
  Requires-Dist: mkdocstrings[python]>=0.24 ; extra == 'docs'
31
31
  Requires-Dist: pymdown-extensions>=10.0 ; extra == 'docs'
32
- Requires-Dist: fastmcp>=3.0 ; extra == 'mcp'
32
+ Requires-Dist: fastmcp>=4.1.2 ; python_full_version >= '3.11' and extra == 'mcp'
33
33
  Provides-Extra: bench
34
34
  Provides-Extra: dev
35
35
  Provides-Extra: docs
@@ -75,13 +75,13 @@ Project-URL: Repository, https://github.com/txjmb/jsonata-core
75
75
  <source media="(prefers-color-scheme: dark)"
76
76
  srcset="https://raw.githubusercontent.com/txjmb/jsonata-core/main/docs/assets/realistic-workload-dark.svg">
77
77
  <img width="880"
78
- alt="Realistic-workload benchmark on a 100-product dataset, lower is better: jsonatapy is 8.7x faster than the jsonata-js reference on the geometric mean of 5 e-commerce queries, and the pure-Rust jsonata-core engine is faster still."
78
+ alt="Realistic-workload benchmark on a 100-product dataset, lower is better: jsonatapy is 8.2x faster than the jsonata-js reference on the geometric mean of 5 e-commerce queries, and the pure-Rust jsonata-core engine is faster still."
79
79
  src="https://raw.githubusercontent.com/txjmb/jsonata-core/main/docs/assets/realistic-workload-light.svg">
80
80
  </picture>
81
81
  </a>
82
82
 
83
83
  *Click the chart for the full category-by-category tables, including jsonata-python
84
- and jsonata-rs, which are left off the chart because at 68 ms–9 s they would
84
+ and jsonata-rs, which are left off the chart because at 67 ms–9 s they would
85
85
  flatten everything else into a sliver.*
86
86
  <!-- END generated performance chart -->
87
87
 
@@ -156,10 +156,10 @@ let result = Evaluator::new().evaluate(&ast, &data)?;
156
156
  ```toml
157
157
  # Cargo.toml
158
158
  [dependencies]
159
- jsonata-core = "2.2.12" # pure Rust, no Python dependency
159
+ jsonata-core = "2.2.14" # pure Rust, no Python dependency
160
160
 
161
161
  # Optional: disable SIMD for constrained targets
162
- jsonata-core = { version = "2.2.12", default-features = false }
162
+ jsonata-core = { version = "2.2.14", default-features = false }
163
163
  ```
164
164
 
165
165
  ---
@@ -26,13 +26,13 @@
26
26
  <source media="(prefers-color-scheme: dark)"
27
27
  srcset="https://raw.githubusercontent.com/txjmb/jsonata-core/main/docs/assets/realistic-workload-dark.svg">
28
28
  <img width="880"
29
- alt="Realistic-workload benchmark on a 100-product dataset, lower is better: jsonatapy is 8.7x faster than the jsonata-js reference on the geometric mean of 5 e-commerce queries, and the pure-Rust jsonata-core engine is faster still."
29
+ alt="Realistic-workload benchmark on a 100-product dataset, lower is better: jsonatapy is 8.2x faster than the jsonata-js reference on the geometric mean of 5 e-commerce queries, and the pure-Rust jsonata-core engine is faster still."
30
30
  src="https://raw.githubusercontent.com/txjmb/jsonata-core/main/docs/assets/realistic-workload-light.svg">
31
31
  </picture>
32
32
  </a>
33
33
 
34
34
  *Click the chart for the full category-by-category tables, including jsonata-python
35
- and jsonata-rs, which are left off the chart because at 68 ms–9 s they would
35
+ and jsonata-rs, which are left off the chart because at 67 ms–9 s they would
36
36
  flatten everything else into a sliver.*
37
37
  <!-- END generated performance chart -->
38
38
 
@@ -107,10 +107,10 @@ let result = Evaluator::new().evaluate(&ast, &data)?;
107
107
  ```toml
108
108
  # Cargo.toml
109
109
  [dependencies]
110
- jsonata-core = "2.2.12" # pure Rust, no Python dependency
110
+ jsonata-core = "2.2.14" # pure Rust, no Python dependency
111
111
 
112
112
  # Optional: disable SIMD for constrained targets
113
- jsonata-core = { version = "2.2.12", default-features = false }
113
+ jsonata-core = { version = "2.2.14", default-features = false }
114
114
  ```
115
115
 
116
116
  ---
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "jsonatapy"
7
- version = "2.2.12"
7
+ version = "2.2.14"
8
8
  description = "High-performance Python/Rust implementation of JSONata query and transformation language"
9
9
  authors = [
10
10
  {name = "txjmb", email = "txjmb@users.noreply.github.com"}
@@ -42,10 +42,12 @@ dev = [
42
42
  "ruff>=0.3.0", # Linting and formatting
43
43
  "mypy>=1.0",
44
44
  "maturin>=1.0",
45
- "fastmcp>=3.0", # So `dev` installs can run the full test suite (Task 9)
45
+ "fastmcp>=4.1.2; python_version >= '3.11'", # So `dev` installs can run the full test suite
46
46
  ]
47
47
  mcp = [
48
- "fastmcp>=3.0",
48
+ # 3.11+ only: fastmcp pulls in dnspython through email-validator, and dnspython 2.9.0,
49
+ # the first release with the fix for GHSA-45c3-73f7-pv4m, dropped 3.10.
50
+ "fastmcp>=4.1.2; python_version >= '3.11'",
49
51
  ]
50
52
  bench = [
51
53
  "jsonata>=0.2.0", # jsonata-python for benchmarking
@@ -82,7 +84,7 @@ dev = [
82
84
  "ruff>=0.3.0",
83
85
  "mypy>=1.0",
84
86
  "maturin>=1.0",
85
- "fastmcp>=3.0",
87
+ "fastmcp>=4.1.2; python_version >= '3.11'",
86
88
  ]
87
89
 
88
90
  [tool.ruff]
@@ -24,7 +24,7 @@ def _run_mcp(argv: list[str]) -> int:
24
24
  from ._cli.mcp_server import serve
25
25
  except ImportError:
26
26
  print(
27
- "error: the 'mcp' extra is not installed. Run:\n"
27
+ "error: the 'mcp' extra is not installed (it needs Python 3.11 or later). Run:\n"
28
28
  ' uvx --from "jsonatapy[mcp]" jsonatapy mcp\n'
29
29
  "or: pip install jsonatapy[mcp]",
30
30
  file=sys.stderr,
@@ -241,11 +241,17 @@ fn describe_operand(v: &JValue) -> String {
241
241
  /// boundary collapses it, so a builtin passed as a callback (`$map(arr,
242
242
  /// $keys)`) is unwrapped once, by the higher-order function's own result,
243
243
  /// exactly as in jsonata-js.
244
+ ///
245
+ /// `start_time` is when the evaluation started, if it has a timeout; the
246
+ /// builtins whose walk is quadratic (`$distinct`, `$keys` over an array)
247
+ /// check it with the cancel token per element, so neither runs on past
248
+ /// D1012 or U1003.
244
249
  pub(crate) fn dispatch_pure(
245
250
  name: &str,
246
251
  args: &[JValue],
247
252
  context: &JValue,
248
253
  options: &EvaluatorOptions,
254
+ start_time: Option<std::time::Instant>,
249
255
  ) -> Result<JValue, EvaluatorError> {
250
256
  // 1. Implicit context insertion. The union of what the two paths used to
251
257
  // do separately: only the tree-walker had `fromMillis` here and
@@ -916,7 +922,9 @@ pub(crate) fn dispatch_pure(
916
922
  // genuinely absent argument) moves to Undefined.
917
923
  None | Some(JValue::Undefined) => Ok(JValue::Undefined),
918
924
  Some(JValue::Array(arr)) if arr.len() > 1 => {
919
- let mut result = functions::array::distinct(arr)?;
925
+ let mut result = functions::array::distinct_checked(arr, || {
926
+ crate::evaluator::check_loop_timeout(options, start_time)
927
+ })?;
920
928
  // jsonata-js builds a sequence only when the input was one.
921
929
  if arr.sequence {
922
930
  result.mark_sequence();
@@ -1035,6 +1043,7 @@ pub(crate) fn dispatch_pure(
1035
1043
  let normalized_item = crate::evaluator::normalize_lazy(item)?;
1036
1044
  if let JValue::Object(obj) = &normalized_item {
1037
1045
  for key in obj.keys() {
1046
+ crate::evaluator::check_loop_timeout(options, start_time)?;
1038
1047
  let k = JValue::string(key.clone());
1039
1048
  if !all_keys.contains(&k) {
1040
1049
  all_keys.push(k);
@@ -1608,6 +1617,7 @@ mod tests {
1608
1617
  &[JValue::Undefined],
1609
1618
  &JValue::Number(5.0),
1610
1619
  &opts,
1620
+ None,
1611
1621
  )
1612
1622
  .expect_err("a numeric context cannot satisfy substring's first parameter");
1613
1623
  assert!(
@@ -1625,11 +1635,62 @@ mod tests {
1625
1635
  // assert on the member only one list had.
1626
1636
  let opts = EvaluatorOptions::default();
1627
1637
  // $fromMillis() with a numeric context formats that context.
1628
- let got = dispatch_pure("fromMillis", &[], &JValue::Number(0.0), &opts)
1638
+ let got = dispatch_pure("fromMillis", &[], &JValue::Number(0.0), &opts, None)
1629
1639
  .expect("fromMillis should read the context");
1630
1640
  assert!(
1631
1641
  matches!(&got, JValue::String(s) if s.starts_with("1970-01-01")),
1632
1642
  "expected an epoch timestamp, got {got:?}"
1633
1643
  );
1634
1644
  }
1645
+
1646
+ #[test]
1647
+ fn distinct_and_keys_stop_on_cancel() {
1648
+ // Both walk their input once per element against everything kept so
1649
+ // far, so a cancel has to be seen inside the walk, not only after it.
1650
+ let token = crate::evaluator::CancelToken::new();
1651
+ token.cancel();
1652
+ let opts = EvaluatorOptions {
1653
+ cancel: Some(token),
1654
+ ..Default::default()
1655
+ };
1656
+ for (name, arg) in [
1657
+ ("distinct", serde_json::json!([1, 2, 1])),
1658
+ ("keys", serde_json::json!([{"a": 1}, {"b": 2}])),
1659
+ ] {
1660
+ let err = dispatch_pure(name, &[JValue::from(arg)], &JValue::Undefined, &opts, None)
1661
+ .expect_err("a cancelled evaluation must not finish the walk");
1662
+ assert!(
1663
+ err.to_string().contains("U1003"),
1664
+ "{name}: expected U1003, got {err}"
1665
+ );
1666
+ }
1667
+ }
1668
+
1669
+ #[test]
1670
+ fn distinct_and_keys_stop_on_timeout() {
1671
+ // The same walks honour the evaluation timeout, not only a cancel.
1672
+ let opts = EvaluatorOptions {
1673
+ timeout_ms: Some(1),
1674
+ ..Default::default()
1675
+ };
1676
+ let started = std::time::Instant::now();
1677
+ std::thread::sleep(std::time::Duration::from_millis(5));
1678
+ for (name, arg) in [
1679
+ ("distinct", serde_json::json!([1, 2, 1])),
1680
+ ("keys", serde_json::json!([{"a": 1}, {"b": 2}])),
1681
+ ] {
1682
+ let err = dispatch_pure(
1683
+ name,
1684
+ &[JValue::from(arg)],
1685
+ &JValue::Undefined,
1686
+ &opts,
1687
+ Some(started),
1688
+ )
1689
+ .expect_err("a timed-out evaluation must not finish the walk");
1690
+ assert!(
1691
+ err.to_string().contains("D1012"),
1692
+ "{name}: expected D1012, got {err}"
1693
+ );
1694
+ }
1695
+ }
1635
1696
  }
@@ -491,6 +491,7 @@ pub unsafe extern "C" fn jsonata_set_limits(
491
491
  timeout_ms: (timeout_ms > 0).then_some(timeout_ms),
492
492
  max_stack_depth: (max_stack_depth > 0).then_some(max_stack_depth as usize),
493
493
  max_sequence_length: (max_sequence_length > 0).then_some(max_sequence_length as usize),
494
+ cancel: None,
494
495
  };
495
496
  clear_error();
496
497
  0