﻿# predicates — search & criteria condition semantics

predicates — operator catalog and evaluation semantics for the `Condition` DSL used by search (`cyoda help search`) and workflow/transition criteria (`cyo…

<em>cyoda-go version <a href="https://github.com/Cyoda/cyoda-go/releases/tag/v0.8.3">0.8.3</a></em>

# predicates

## NAME

predicates — operator catalog and evaluation semantics for the `Condition` DSL used by search (`cyoda help search`) and workflow/transition criteria (`cyoda help workflows`). Both consume the same kernel, so everything here applies identically to both.

## OPERATORS

**Unary** (test presence, no operand type constraint):

- `IS_NULL` — the leaf is absent or JSON `null`.
- `NOT_NULL` — the leaf is present and non-null.

**Range** (operand is a two-element `[low, high]` array):

- `BETWEEN` — exclusive bounds.
- `BETWEEN_INCLUSIVE` — inclusive bounds.

**Binary — comparison** (same-type `compareTo`):

- `EQUALS`, `NOT_EQUAL`, `GREATER_THAN`, `GREATER_OR_EQUAL`, `LESS_THAN`, `LESS_OR_EQUAL`.
- `IEQUALS` (case-insensitive equals), `INOT_EQUAL` (case-insensitive not-equal).

**Binary — string** (case-sensitive; `I*`/`INOT_*` fold both sides):

- `CONTAINS`, `STARTS_WITH`, `ENDS_WITH`, and their `NOT_*` inverses.
- `ICONTAINS`/`INOT_CONTAINS`, `ISTARTS_WITH`/`INOT_STARTS_WITH`, `IENDS_WITH`/`INOT_ENDS_WITH`.

**Binary — pattern:**

- `LIKE` — anchored glob, see LIKE GRAMMAR below.
- `MATCHES_PATTERN` — anchored regular expression, see MATCHES_PATTERN below.

**Not supported:** `IS_CHANGED`/`IS_UNCHANGED` are change-generation operators, not search predicates — cyoda-go does not implement them.

## TYPE-DIRECTED COMPARISON

A condition's operand is compared against the target field's **declared type(s)** (a leaf may carry more than one, e.g. a field seen as both an integer and a string across entities). The operand is treated as a string and parse-tested against every declared type — a numeric-looking string operand (`"30"`) and a JSON number operand (`30`) are parsed identically and are **treated the same**. There is no cross-type coincidental matching: an operand that parses as a number only compares against numerically-stored values; an operand that parses only as a string compares against string-stored values. Numbers compare precisely (arbitrary-precision, not `float64` — correct beyond 2^53). Comparing against `MATCHES_PATTERN`/`LIKE` and string ops evaluate against textual stored values only; a string op against a non-textual stored value is a non-match, not an error.

## NULL SEMANTICS

A missing (absent) or JSON-`null` leaf **never matches any binary operator — including negatives**. `NOT_EQUAL`, `NOT_CONTAINS`, `INOT_*`, and every other negated op are **null-guarded to non-match**, not `!positive` — a null/absent field does not satisfy a negative condition just because it fails the positive one. `IS_NULL` / `NOT_NULL` are the only operators that test presence directly.

## LIKE GRAMMAR

`LIKE` compiles its operand to an anchored regular expression:

- `%` — matches any sequence of characters (including empty).
- `_` — matches exactly one character.
- `\` — escapes a following `%`, `_`, or `\` to its literal form.
- The match is **whole-string anchored** (the entire stored value must match, not a substring) and **case-sensitive**.

## MATCHES_PATTERN

`MATCHES_PATTERN` compiles the operand as a Go RE2 regular expression, whole-string anchored (equivalent to Java's `Pattern.matches`), case-sensitive. RE2 and the Java regex dialect diverge on some constructs (e.g. backreferences, some lookaround); this is an accepted, bounded divergence — not reconciled.

## VALIDATION

Validation is **parse-based**, evaluated at request time against the target model:

- `400 CONDITION_TYPE_MISMATCH` — the operand parses into **none** of the field's declared types (comparison and range operators only; string operators and unary presence tests carry no operand-type constraint and are always accepted).
- `400 INVALID_FIELD_PATH` — the field path is unknown to the model, **or** it names a pure-container (object) path: a scalar operator cannot compare against structure — navigate to a scalar leaf sub-path instead. (A path observed as both an object and a scalar across entities remains searchable via its scalar type — see `cyoda help search`.) `IS_NULL`/`NOT_NULL` are exempt from the container-path rejection since they test presence, not a value.
- `400 INVALID_CONDITION` — the operand is `null` on a binary/range operator, a range operator's value is not a two-element array, or the operand is an object/complex value.

There is no operator-versus-field-type rejection: `CONTAINS` on a numeric field or `GREATER_THAN` on a boolean field are accepted requests — they parse and simply evaluate to a (non-)match.

## SEE ALSO

- search
- workflows
- errors.CONDITION_TYPE_MISMATCH
- errors.INVALID_FIELD_PATH
- errors.INVALID_CONDITION

## See also

- [`cyoda help search`](/help/search/) — Search operates against a specific entity model `(entityName, modelVersion)`. Two modes are supported:
- [`cyoda help workflows`](/help/workflows/) — A workflow definition is a named finite state machine attached to an entity model. Workflows are stored per model reference `(entityName, modelVersion)`. A model may have multiple workflow definitions; the engine selects the matching one per entity using the workflow-level `criterion` field evaluated at entity creation time. When no `criterion` matches, the engine uses the default built-in workflow.
- [`cyoda help errors CONDITION_TYPE_MISMATCH`](/help/errors/condition_type_mismatch/) — Validation is parse-based: a comparison or range operand is rejected only when it parses into none of the field's declared DataTypes. For example `"abc"` against a DOUBLE field is rejected — it is not a number. A numeric-looking string against a polymorphic `[INTEGER, STRING]` field is accepted (it parses as STRING).
- [`cyoda help errors INVALID_FIELD_PATH`](/help/errors/invalid_field_path/) — Before executing a search, the server validates that every data-field path referenced by the condition (e.g. `$.price`, `$.profile.email`) resolves against the target model's locked schema. Lifecycle paths (`state`, `previousTransition`, etc.) and meta paths (`$._meta.*`) bypass this check.
- [`cyoda help errors INVALID_CONDITION`](/help/errors/invalid_condition/) — Endpoints that accept a search-style condition in the request body — grouped statistics and the conditional form of delete-by-model — reject a body whose condition cannot be parsed or is otherwise structurally invalid. The condition type is unrecognised, a nested clause is malformed, the JSON does not match the expected condition envelope, an `operatorType` is not one of the canonical operators, a `MATCHES_PATTERN` regex is malformed, or a `BETWEEN`/`BETWEEN_INCLUSIVE` operator's value is not a two-element array.

## Raw formats

- [`/help/predicates.json`](/help/predicates.json) — full descriptor (matches `GET /help/{topic}` envelope)
- [`/help/predicates.md`](/help/predicates.md) — body only