Constraints

The GigaMap can validate every write operation (add, set, update, …​) against a set of constraints. If any constraint is violated, a ConstraintViolationException is thrown and the operation is rolled back, so the map is never left in an invalid state.

Constraints are accessed through gigaMap.constraints(), which returns a GigaConstraints instance with two categories:

gigaMap.constraints().unique(); // UniqueConstraints<E>
gigaMap.constraints().custom(); // CustomConstraints<E>

Unique Constraints

A unique constraint ensures that the indexed value of a property is unique across all entities in the map. It is backed by a bitmap index, so adding one always implies that a corresponding bitmap index exists.

// use the builder
final GigaMap<Person> gigaMap = GigaMap.<Person>Builder()
    .withBitmapUniqueIndex(PersonIndices.id)
    ...
    .build();

// or register it after creating
GigaMap<Person> gigaMap = GigaMap.New();
gigaMap.index().bitmap().addUniqueConstraint(PersonIndices.id);

Combining With an Identity Index

A unique constraint and an identity index are independent concerns: the identity index defines how entities are looked up, the unique constraint enforces uniqueness at write time. Combine both to get an identifying field that is also guaranteed unique:

// use the builder — the same indexer can be passed to both methods
final GigaMap<Person> gigaMap = GigaMap.<Person>Builder()
    .withBitmapIdentityIndex(PersonIndices.id)
    .withBitmapUniqueIndex(PersonIndices.id) // enforce uniqueness at runtime
    ...
    .build();

// or register it after creating: addUniqueConstraint creates the backing
// bitmap index, then setIdentityIndices promotes it to identity
GigaMap<Person> gigaMap = GigaMap.New();
final BitmapIndices<Person> bitmap = gigaMap.index().bitmap();
bitmap.addUniqueConstraint(PersonIndices.id); // creates index + unique constraint
bitmap.setIdentityIndices(PersonIndices.id);  // promote it to identity

Custom Constraints

Custom constraints let you express arbitrary validation logic — anything from a simple per-entity predicate to a check that consults the rest of the map.

Custom constraints are persisted with the GigaMap: when the map is stored, the registered constraint instances are written along with it and are restored when the map is loaded again.

Because of this, every constraint must be implemented as a named, top-level (or static nested) class. EclipseStore cannot persist:

  • lambdas — their synthetic class names are generated by the JVM and not stable across compilations or runs;

  • anonymous inner classes — their $1, $2, …​ names depend on the declaration order in the enclosing class.

In practice this means: only use CustomConstraint.Wrapper / CustomConstraint.WrapperSimple for short-lived, in-memory GigaMaps that you will never store. For any persisted GigaMap, write a named subclass of CustomConstraint.AbstractSimple or CustomConstraint.Abstract.

A custom constraint implements CustomConstraint<E>. There are four ready-made base types, ordered from simplest to most flexible:

Type When to use

AbstractSimple<E>

Validation depends only on the new entity itself.

Abstract<E>

Validation also needs the entityId and/or the entity being replaced. Throws the exception directly.

WrapperSimple<E>

Same as AbstractSimple but using a Predicate<E> lambda.

Wrapper<E>

Same as Abstract but using a CustomConstraint.Logic<E> lambda. Most flexible — also receives the GigaMap itself, so it can cross-check against other entities.

Adding Constraints

// use the builder
final GigaMap<Person> gigaMap = GigaMap.<Person>Builder()
    .withCustomConstraint(new NoBlankNames())
    ...
    .build();

// or register one or more after creating
gigaMap.constraints().custom().addConstraint(new NoBlankNames());
gigaMap.constraints().custom().addConstraints(constraint1, constraint2);
When a custom constraint is added to a non-empty GigaMap, it is immediately checked against every existing entity. If any current entity already violates it, the call fails with a ConstraintViolationException and the constraint is not registered.

Example: AbstractSimple

The most common case — a per-entity rule:

public class NoBlankNames extends CustomConstraint.AbstractSimple<Person>
{
    @Override
    public boolean isViolated(final Person person)
    {
        return person.getFirstName() == null || person.getFirstName().isBlank();
    }
}

Example: WrapperSimple (lambda)

Same logic, expressed as a predicate:

gigaMap.constraints().custom().addConstraint(
    new CustomConstraint.WrapperSimple<Person>(
        person -> person.getFirstName() == null || person.getFirstName().isBlank()
    )
);
The wrapped Predicate is a lambda and cannot be persisted by EclipseStore. Use this form only for in-memory GigaMaps. For persisted maps, port the logic to a named subclass of AbstractSimple (see above).

Example: Abstract

When the check needs the entity id or the replaced entity, declare a named subclass:

public class NoLevelDowngrade extends CustomConstraint.Abstract<Person>
{
    @Override
    public void check(final long entityId, final Person replaced, final Person entity)
    {
        if(replaced != null && entity.getLevel() < replaced.getLevel())
        {
            throw new ConstraintViolationException(entityId, replaced, entity);
        }
    }
}

gigaMap.constraints().custom().addConstraint(new NoLevelDowngrade());
For persisted GigaMaps, prefer named classes over the new CustomConstraint.Abstract<>() { …​ } anonymous-class shorthand. Anonymous-inner-class names (…​$1, …​$2) depend on declaration order in the enclosing class and are fragile across refactors.

Example: Wrapper (full Logic lambda)

When the check needs to consult the rest of the map:

final CustomConstraint.Wrapper<Person> singleAdmin = new CustomConstraint.Wrapper<>(
    (map, entityId, replaced, entity, createException) ->
    {
        if(!entity.isAdmin())
        {
            return null;
        }
        final long admins = map.query(PersonIndices.admin.is(true)).count();
        if(admins == 0 || (replaced != null && replaced.isAdmin()))
        {
            return null;
        }
        return createException
            ? new ConstraintViolationException(entityId, replaced, entity)
            : org.eclipse.serializer.util.X.BREAK();
    }
);
gigaMap.constraints().custom().addConstraint(singleAdmin);
The wrapped Logic is a lambda and cannot be persisted by EclipseStore. Use this form only for in-memory GigaMaps. For persisted maps, port the logic to a named subclass of Abstract (see above) — the check method has access to the same entityId and replaced parameters; if you also need the surrounding GigaMap, implement CustomConstraint<E> directly.

Constraint Names

Each CustomConstraint has a name() used as its registry key — duplicate names are rejected with an IllegalArgumentException. The default name() in AbstractBase returns the simple class name, which is sufficient for named subclasses (the recommended form, see the persistence note above). Anonymous subclasses fall back to an empty simple name, so registering more than one without overriding name() will fail.

Constraint Violations

All violations are signalled by ConstraintViolationException (or its subclass UniqueConstraintViolationException for unique-index violations). The exception carries:

  • entityId — the id of the offending entity

  • replacedEntity — the previous entity for set / update, null for add

  • violatingEntity — the entity that triggered the violation

What happens to the map on violation depends on the operation:

Operation Effect on the map

add / addAll

The map is unchanged. For batch operations none of the entities in the batch are added.

set / replace

The map is unchanged. Constraints are checked before the entry is mutated, so the previous entity stays in place.

update / apply

The offending entity is removed from the map. The provided logic mutates the entity in place, so a constraint failure cannot be rolled back to the previous state — the only consistent option is to drop the entry. The removed entity and its id are carried by the ConstraintViolationException (violatingEntity, entityId), so the caller can recover or re-add it after fixing the violation.