Plans and executes implementation of a new evitaDB query constraint (filter, order, or require). Produces a task checklist covering all layers — query model, EvitaQL grammar, engine translator, Kryo serializer, external APIs, tests, and documentation — with exact file paths. Use when adding new filter/order/require constraints or variants of existing ones.
Installs into .claude/skills of the current project.
Are you the author of New Constraint?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/fgforrest-new-constraint)
---
name: new-constraint
description: Plans and executes implementation of a new evitaDB query constraint (filter, order, or require). Produces a task checklist covering all layers — query model, EvitaQL grammar, engine translator, Kryo serializer, external APIs, tests, and documentation — with exact file paths. Use when adding new filter/order/require constraints or variants of existing ones.
allowed-tools: Read, Glob, Grep, Bash(rg *), Bash(find *), Bash(ls *), Bash(wc *), Write, Agent
---
# Planning New Constraint
Produces an implementation plan for adding a new query constraint to evitaDB.
## Procedure
### Step 1: Gather requirements
Ask the user (if not already clear):
1. **Constraint type**: filter, order, or require
2. **What it does**: behavior, parameters, return semantics
3. **Supported domains**: ENTITY, REFERENCE, INLINE_REFERENCE, FACET, etc.
4. **Does it need a new formula?** Or reuses existing infrastructure
### Step 2: Find the closest existing constraint
Search the codebase for the most similar existing constraint to use as a template:
| If the new constraint is... | Study this template |
|-----------------------------------|--------------------------------------------------------|
| Filter leaf, entity property | `EntityPrimaryKeyInSet`, `EntityPrimaryKeyGreaterThan` |
| Filter leaf, attribute comparison | `AttributeGreaterThan`, `AttributeBetween` |
| Filter leaf, price | `PriceBetween`, `PriceInCurrency` |
| Filter container | `And`, `ReferenceHaving`, `HierarchyWithin` |
| Order leaf | `EntityPrimaryKeyNatural`, `AttributeNatural` |
| Require leaf | `Page`, `PriceContent` |
| Require container | `EntityFetch`, `ReferenceContent` |
### Step 3: Trace the template through all layers
For the chosen template, search for every file that references it. This reveals the exact set of files the new constraint must touch:
```bash
rg "TemplateConstraintName" --type java -l
```
Cross-reference the results against the full checklist in [LAYERS.md](LAYERS.md) to ensure nothing is missed.
### Step 4: Produce the plan
For each item in the checklist from [LAYERS.md](LAYERS.md):
- Provide the **exact file path** to create or modify
- Specify **which template file** to mimic (the one from Step 2)
- For modifications, note **where in the file** to add (e.g., "after the `EntityPrimaryKeyInSet` entry")
- Include brief notes on any deviations from the template pattern
### Step 5: Save the plan
Write to `docs/plans/YYYY-MM-DD-{constraint-name}.md` with this structure:
```markdown
# [Constraint Name] Implementation Plan
**Goal:** [One sentence]
**Template constraint:** [Name of template being followed]
**Architecture:** [2-3 sentences about approach]
---
## Task N: [Layer/component name]
**Files:**
- Create: `exact/path/to/NewConstraint.java` (template: `exact/path/to/TemplateConstraint.java`)
- Modify: `exact/path/to/Registry.java` (add after `TemplateConstraint.class`)
**Key differences from template:**
- [What's different about the new constraint vs the template]
```
## Critical rules
- **Kryo registration**: ALWAYS append at the END of `QuerySerializationKryoConfigurer.java`. Inserting in the middle shifts class IDs and breaks deserialization of existing stored data.
- **Grammar regeneration**: Run `cd evita_query && ./generate_grammar.sh evitaql` after modifying `EvitaQL.g4`. The generated files in `evita_query/.../parser/grammar/` are checked into version control.
- **Formula framework**: New formulas MUST follow `documentation/developer/formula/formula_framework.md` — lazy evaluation, unique CLASS_ID, benchmarked `getOperationCost()` via `FormulaCostMeasurement.java` (COST 1 = 1M ops/s).
- **GraphQL/REST**: Usually automatic from constraint descriptors. Manual API code only needed for container constraints with additional children.
- **gRPC**: No changes needed — passes EvitaQL strings.
- **Tests are domain-grouped**: E2E tests live in `evita_test/.../functional/{entity,attribute,price,hierarchy,facet,fetch}/` — place the test in the matching domain package.
- **Constraint descriptor annotations**: See `documentation/developer/query/query_constraint_description_framework.md` for the full annotation framework (`@ConstraintDefinition`, `@Creator`, `@Classifier`, `@Value`, `@Child`, `@AdditionalChild`).
- **`@ConstraintSupportedValues` vs Creator parameter type**: The `supportedTypes` in the annotation controls which types the external APIs (GraphQL/REST) advertise in their schemas. The `@Creator` constructor parameter type is what `ConstraintProcessor.resolveValueParameter()` reads via `parameter.getType()` and stores as the `ValueParameterDescriptor` type — the GraphQL schema builder then maps this to a GraphQL scalar via `DataTypesConverter.getGraphQLScalarType`. **The Creator parameter must be a concrete type** (e.g. `Integer`, `String`), never a generic like `<T extends Number & Serializable>`, because Java type erasure reduces the generic to its first bound (`Number.class`), which has no mapping in `DataTypesConverter.SCALAR_MAPPINGS` and crashes the entire GraphQL schema build. To accept broader numeric types, keep the `supportedTypes` list broad (e.g. `{ Byte.class, Short.class, Integer.class, Long.class, BigDecimal.class }`) — the external API layer handles type coercion via `EvitaDataTypes.toTargetType` before the constraint constructor is called.
- **Integer overflow in strict comparison translators**: When translating strict inequality constraints (e.g. `greaterThan` → `[threshold + 1, MAX_VALUE]`), always guard against overflow at `Integer.MAX_VALUE` / `Integer.MIN_VALUE`. Return `EmptyFormula.INSTANCE` when no value can satisfy the constraint (e.g. `greaterThan(Integer.MAX_VALUE)` → empty).