Skip to content
Settings

predicates — search & criteria condition semantics

cyoda-go version 0.8.4

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.

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.

AND, OR, and NOT combine conditions (GroupCondition, see cyoda help search). NOT takes exactly one child condition — NOT(c) is true exactly when c is false — and is never rewritten by De Morgan into a leaf’s negative twin: NOT(EQUALS) is not NOT_EQUAL, and NOT(IS_NULL) is not NOT_NULL. Over a wildcard path NOT is a universal quantifier where the leaf beneath it is existential — NOT($.tags[*] EQUALS "red") means no element equals "red", a different question from $.tags[*] NOT_EQUAL "red" (some element differs). See cyoda help search for the full contract, including the one-condition rule and the absent-field/presence-test asymmetries.

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). String and pattern operators apply to text fields only.

An unsatisfiable comparison follows operator polarity. This applies only once an operand has been accepted — it parses into at least one of the field’s declared types (see errors.CONDITION_TYPE_MISMATCH). Evaluation still runs per stored-value type family: when a given family’s declared type produced no surviving sub-condition for that operand, a positive operator (EQUALS, GREATER_THAN, the string operators, …) answers non-match for an entity in that family, and a negative operator (NOT_EQUAL, NOT_CONTAINS, INOT_*, …) answers match instead. $.n NOT_EQUAL 12.5 on an INTEGER field matches every entity holding a number there, because no integer equals 12.5 — the same answer PostgreSQL gives for 5::int <> 12.5. This is a determinate answer about the entity, not a rejection, and null/absent values are unaffected (see NULL SEMANTICS below). An operand that parses into none of a known field’s declared types at all is rejected before evaluation instead (400 CONDITION_TYPE_MISMATCH). A field carrying no declared type at all is a different failure with a different code: there is no declared-type check to run, so the operand is accepted here and the leaf instead fails at preparation with 400 INVALID_CONDITION.

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.

NOT is not a binary operator and is not subject to this guard. NOT sits outside the operator it wraps and inverts whatever two-valued answer that operator gives, including a null-guarded non-match: on an entity missing x, NOT($.x EQUALS "A") matches even though both $.x EQUALS "A" and $.x NOT_EQUAL "A" do not. See cyoda help search for the full NOT contract.

LIKE is a glob matched directly — not translated into a regular expression, so no regex metacharacter has any meaning in a LIKE operand. Only these three characters are special:

  • % — matches any sequence of characters, including empty and including newlines.
  • _ — matches exactly one character (one UTF-8 rune, not one byte), including a newline.
  • \ — escapes the character after it to its literal form. It escapes any character, not only %, _ and \: \a matches a literal a, and \\ matches a single backslash.

The match is whole-string anchored (the entire stored value must match, not a substring) and case-sensitive. Everything outside the three characters above is literal text compared bytewise, so an operand carrying invalid UTF-8 matches the byte-identical stored value rather than being transcoded.

A pattern that ends with an unpaired \ has nothing to escape and is invalid. The request is rejected — see VALIDATION below. Use \\ for a literal trailing backslash.

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.

The operand must parse on its own as well as compile anchored. Anchoring wraps it as \A(?:operand)\z, and that wrapper’s own parentheses can rebalance a body whose parens are unmatched: )|( would become an alternation matching every stored value. Requiring a standalone parse keeps that family unrepresentable, so an accepted pattern never matches more than it says.

Validation is parse-based, evaluated at request time against the target model. The codes below are search’s; a workflow or transition criterion is validated identically at import and surfaces the same rejections as 400 VALIDATION_FAILED (see workflows).

  • 400 CONDITION_TYPE_MISMATCH — the operand parses into none of the field’s declared types, or the operator does not apply to the field’s type: string and pattern operators require a text field; ordering and range operators require an ordered type (number, text, timestamp). IS_NULL/NOT_NULL carry no operand-type constraint.
  • 400 INVALID_FIELD_PATH — the field path is unknown to the model, or it names a container path with no scalar form: a bare object path, or a [*] path over an array whose elements are only ever objects. A scalar operator cannot compare against structure — navigate to a scalar leaf sub-path instead. (A path observed as both a container and a scalar across entities — a field, or an array’s elements, holding a plain value on some entities and an object on others — remains searchable via its scalar branch; see cyoda help search.) IS_NULL/NOT_NULL are exempt from the container-path rejection since they test presence, not a value.
  • 400 INVALID_CONDITIONoperatorType is missing or names no operator in the catalog above, the operand is null on a binary/range operator, a range operator’s value is not a two-element array, the operand is an object/complex value, or a LIKE/MATCHES_PATTERN operand is not a valid pattern. Pattern operands are checked against the same derivation the evaluator uses, so anything accepted here evaluates everywhere — on both the sync and async paths, and on every backend. This is the code on every surface that carries a condition, workflow import included — there is no surface where an unknown operator falls through to the coarser 400 BAD_REQUEST.
  • search
  • workflows
  • errors.CONDITION_TYPE_MISMATCH
  • errors.INVALID_FIELD_PATH
  • errors.INVALID_CONDITION
  • cyoda help search — Search operates against a specific entity model (entityName, modelVersion). Two modes are supported:
  • cyoda 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 — 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 — Three checks emit this code.
  • cyoda help errors INVALID_CONDITION — Endpoints that accept a search-style condition in the request body — sync and async search, 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 LIKE or MATCHES_PATTERN operand is not a valid pattern, a BETWEEN/BETWEEN_INCLUSIVE operator’s value is not a two-element array, a group clause’s operator is not AND, OR, or NOT, or a NOT group’s conditions does not hold exactly one entry (zero, or two or more).