Migration Guide
This page covers the updates in 5.9.0 and 5.8.0 and gives an overview of
the move from v4 to v5. The full v4 to v5 cookbook (every renamed call,
cargo-feature swap, and error-handling update) and the notes for earlier
5.x releases live in MIGRATION.md
at the repo root, which is authoritative.
Upgrading to 5.9
5.9.0 is backwards-compatible with 5.8.x:
- Rust MSRV is now 1.98 (Edition 2024).
- New metered report APIs (
budgetfeature):Engine::evaluate_metered_reportandSession::eval_metered_reportreturnMetered<Result<...>>so callers can readopseven when evaluation fails with an ordinary error (Thrown,VariableNotFound,TypeError). See Operation Budget.
Upgrading to 5.8
5.8.0 adds API (rule checking, rule facts, the operator catalogue,
per-compile templating, MissingVar, Roots, SharedSession and more)
without breaking source compatibility. A rule’s result can change in the
cases below, most of them bug fixes. The
CHANGELOG
has every item with examples.
Errors
Error::operator(), and"operator"in a serialized error, name the innermost failing operator, custom operators included. Before 5.8 it was the root operator unless a deeper site set one, and traced runs always reported the root.format_datewith a format specifier chrono does not know ("%Q") or a trailing%is anInvalidArgumentserror instead of a panic.
Data and comparison
- Only a single-key
{"datetime": ..}or{"timestamp": ..}object is a datetime. A record that has such a field next to others is ordinary data for==,===,in,distinctandtype. - Integers above 2^53 compare, divide and sort exactly in
==,===,<and the other comparisons,/,%andsort; they are no longer rounded or judged equal. sortby a field reads a missing field asnull, so it ties with anullfield.filterwith===/!==againstnulltreats a missing field asnull, as the other iterators did.
Arithmetic and strings
- The strings
"NaN","inf"and"infinity", and numeric literals that overflow, no longer coerce to non-finite numbers: arithmetic applies NaN handling to them,==and<raise an error, andabs,ceilandfloorraiseInvalidArguments. NanHandling::CoerceToZerosubstitutes 0 instead of skipping the value:{"*": [2, "x", 3]}is0, not6.- Variadic
+and*coerce numeric strings as the two-argument forms do. ceilandfloorreturn a float for a result outside thei64range instead of saturating.lowerandupperapply context-sensitive Unicode case rules ("ΟΔΟΣ"lowers to"οδος").typeclassifies strings with the datetime and duration parsers:"password1"is"string"(it was"duration").- A rule that passes an expression where an operator distinguishes a
literal argument (a single array for
+,*,maxandmin, avarpath built withcat) gives the same result with or without constant folding:{"+": [{"if": [true, [1, 2], 0]}]}is3with folding on or off. - A rule compiled on one engine and evaluated on an engine with different number coercion, NaN, division, loose-equality or truthiness settings follows the evaluating engine’s settings; before 5.8, values folded at compile time kept the compiling engine’s.
Logic::to_json()writes a rule that reads back as the same rule: a single array argument stays wrapped ({"max": [[1, 2]]}), a dottedvalpath stays invalform, and strings are escaped.
Limits
- A
reduceaccumulator may nest at most 1,024 levels; deeper is anInvalidArgumentserror instead of a stack overflow. - Tensor constructors (
zeros,full,scatter,rle_expand,one_hot,pad) refuse more than 2^28 elements. fractionalclamps bucket weights toi32::MAX, as flagd does.- Operation counts from
evaluate_meteredrise for iterator shapes that took a fast path, to match the general path.
Deprecations (removed in 6.0)
- WASM: the
CompiledRuleclass and the freeevaluateandevaluateWithTracefunctions. Build anEngineand usecompile,evalStrandevaluateWithTraceon it. - Node and WASM:
evaluateNumber. UseevaluateFloat(orevaluateInt).
If you install the Node package: 5.1.1 through 5.7.1 published
@goplasmatic/datalogic-node without its index.js loader, so
require('@goplasmatic/datalogic-node') failed. 5.8.0 ships it again.
v4 to v5 Migration
v5 is a hard cliff
v5 has no compatibility shim. The pre-release compat feature and
the LegacyApi trait are gone. Plan a single cutover: update Cargo.toml,
run a find-and-replace pass, and re-run your test suite.
Rules and data keep the same JSONLogic format; the changes are in the Rust API.
What changed at a glance
- Type renames.
DataLogic→Engine,CompiledLogic→Logic,Operator→CustomOperator,ArenaValue→DataValue,ArenaContextStack→operator::EvalContext.Evaluatoris gone (args arrive pre-evaluated). - Method renames. The
evaluate_*methods becomeeval_*.evaluate_str→eval_str,evaluate_borrowed→eval_borrowed. Theserde_json::Value-shaped variants (evaluate_json_value,evaluate_owned,evaluate_ref, …) collapse into one typed entry point:engine.eval_into::<T, _, _>(rule, data)(ordatalogic_rs::eval_into::<T, _, _>(...)at the module level), gated onfeature = "serde_json". - Builder construction.
DataLogic::with_config(c)/with_preserve_structure()/with_config_and_structure(c, s)all collapse intoEngine::builder()with.with_config(c)and.with_templating(s)setters. - Compilation accepts more shapes.
engine.compile(rule)takes anyIntoLogic:&str,&String,&OwnedDataValue,OwnedDataValue,&serde_json::Value(gated onserde_json). - Module-level helpers for one-shot calls.
datalogic_rs::eval,datalogic_rs::eval_str,datalogic_rs::eval_intoanddatalogic_rs::compileuse a shared default engine, so a one-shot call needs noEngine. - Sessions are explicit. Reusable arenas live on
Session(engine.session()); the session never auto-resets, so you callsession.reset()between batches. - Trace surface is a session.
engine.trace().eval_str(rule, data)returns aTracedRun<R>withresult: Result<R, Error>plusstepsandexpression_tree. Available onfeature = "trace". The oldTracedResulttype is gone; successful and failed runs share the sameTracedRun<R>shape. - Custom operators take pre-evaluated args. Implementations get
args: &[&'a DataValue<'a>], a&mut EvalContext<'_, 'a>, and a&'a bumpalo::Bump; they return&'a DataValue<'a>. - Operator registration is builder-only.
Engineis immutable afterbuild(). Register every custom operator on theEngineBuilderbefore calling.build(). - Error is structured.
Erroris a struct withkind,operator(),node_ids(),tag()andcode(), plus a stable JSON wire format. Construct viaError::invalid_arguments(...),Error::type_error(...),Error::custom_message(...),Error::wrap(...). preserveoperator removed. Literal scalars and arrays pass through inline; templated objects belong in templating mode (Engine::compile_templatefor one rule, orEngine::builder().with_templating(true).build()for the engine; both needfeature = "templating").- Edition 2024 +
#![forbid(unsafe_code)].
Feature-flag rename
The pre-release compat feature is gone. The replacement is
purpose-named:
| v4 / pre-release feature | v5 feature | What it enables |
|---|---|---|
compat (mixed interop + shims) | serde_json | &serde_json::Value interop and the typed eval_into::<T> paths |
preserve | templating | Templating mode and Engine::builder().with_templating(true) |
trace | trace | engine.trace() (transitively enables serde_json) |
Quick before/after sketch
// v4
use datalogic_rs::DataLogic;
let mut engine = DataLogic::with_config(my_config);
engine.add_operator("double".to_string(), Box::new(MyOp));
let compiled = engine.compile(&rule_value)?;
let result: Value = engine.evaluate_owned(&compiled, data)?;
// v5
use datalogic_rs::Engine;
let engine = Engine::builder()
.with_config(my_config)
.add_operator("double", MyOp)
.build();
let compiled = engine.compile(&rule_value)?; // accepts &Value via `serde_json`
let mut session = engine.session(); // reuse one arena for the compiled logic
let result = session.eval(&compiled, &data_value)?; // OwnedDataValue
let result_str = session.eval_str(&compiled, data_str)?; // String (JSON)
let v: serde_json::Value = session.eval_into(&compiled, &data_value)?; // typed
Custom operators
// v5 (final)
use datalogic_rs::{CustomOperator, DataValue, Engine, Result};
use datalogic_rs::operator::EvalContext;
use bumpalo::Bump;
struct DoubleOperator;
impl CustomOperator for DoubleOperator {
fn evaluate<'a>(
&self,
args: &[&'a DataValue<'a>],
_ctx: &mut EvalContext<'_, 'a>,
arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
// args are already evaluated; no Evaluator call.
let n = args.first()
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
Ok(arena.alloc(DataValue::from_f64(n * 2.0)))
}
}
let engine = Engine::builder()
.add_operator("double", DoubleOperator)
.build();
Where to look next
- The repo-root
MIGRATION.mdhas the per-call cookbook. - The API Reference covers the v5 methods.
- The Quick Start shows the module-level helpers.
v3 to v4 Migration
If you are moving from v3 straight to v5, skip the v3 to v4 step: nothing
in it applies to this codebase. Read the
v4 to v5 section above and the repo-root
MIGRATION.md.
Getting Help
If you hit a problem during migration:
- Check the API Reference
- Review the FAQ and Troubleshooting Guide
- Review the examples
- Open an issue on GitHub