enhancement
major
minor
major
minor
Uniqueness Constraint
Motivation
Applications routinely require that certain values not be entered twice. A project must not have two milestones with the same name; a contact’s email address should be unique; and an order line number must be unique within its order. Today, this must be checked using hand-written constraints or custom code in every application. Since TopLogic is a general-purpose framework, this should be a declarative, model-level feature: the modeler specifies “which combination of attributes must be unique,” and the framework enforces this and provides the user with early feedback.
This is the object-model equivalent of a SQL UNIQUE index, but expressed in terms of the model (types, attributes, references) rather than tables and columns.
Declaration
A uniqueness constraint is a built-in constraint check declared on the primary attribute whose value is expected to be unique, using the general <constraints> annotation:
{{{#!xml <property name="name">
<annotations>
<constraints>
<unique additional-attributes="project"/>
</constraints>
</annotations>
</property> }}}
Additional attributes may be included in the constraint: the value of the annotated attribute must then be unique only among those objects that share the values of all additional attributes. Participating attributes may be data properties or one-to-one references —the two are treated uniformly, just as a SQL index combines data columns and foreign key columns.
This single mechanism covers all the usual cases:
- Single property — <unique/> on name: a simple globally unique value.
- Several properties — <unique additional-attributes="lastName"/> on firstName: a unique combination.
- Property plus reference — <unique additional-attributes="project"/> on name: unique within a scope. The “scope” (e.g., the container) is not a separate concept: it is simply a reference attribute participating in the constraint.
- References only — at most one link of a given type between objects: for a membership type with references "person " and "project," annotate one of the references, e.g. , <unique additional-attributes="project"/> on person.
Violations are reported at the annotated (primary) attribute—in a form, at exactly the field where the user is typing. Because the declaration is a standard constraint check, it can also be declared in-app via the model editor, and a type="warning" variant displays the conflict in the form without preventing the commit.
Application value: early form feedback
The most important aspect for the end user is early validation within the form. When the user enters a value that would violate a uniqueness constraint, the affected field is marked as invalid immediately (as the value is entered or the field is left), with a clear message—long before the change is committed. This is the visible benefit of the feature.
The form check:
- uses the values currently entered (not yet committed);
- excludes the edited object itself when checking an existing object (there is no “self” yet in a creation dialog);
- re-evaluates when a scoping attribute is changed in the same form (e.g., the user first selects the container, then types the name), not only when the constrained field itself changes.
Enforcement at commit
Regardless of the form, the constraint is also enforced for changes that do not go through a form—imports, scripts, the API. The check at commit time is the integrity safeguard: it runs within the commit (before anything is written) and vetoes the transaction if a conflict is detected. Because transactions are sequentialized at commit (to produce a gap-free revision order), the application-level check is race-free without any additional effort: no two conflicting values can be committed concurrently. Objects created or modified within the same transaction are also checked against each other.
The form check and the commit check share the same “find conflicting objects” implementation, so the two layers cannot drift apart.
Semantic Decisions
- NULL counts as a value. `Unset` participates in the constraint like any other value, so at most one object may have the empty (combination of) value(s). This is more intuitive for a model constraint than SQL’s rule that multiple NULLs are distinct.
- To-many references cannot be included. A set-valued attribute has no single value to constrain; “unique over a set” has no clear meaning. Inclusion is restricted to data properties and to-one references.
- Inheritance. A constraint applies to the entire instance set of the type declaring it, including instances of subtypes. A subtype that overrides the constrained attribute inherits the constraint and remains part of the same uniqueness domain—an override made for an unrelated reason (a narrowed type, a different label) does not reduce the domain.
- Declaration on an override. Declaring the constraint on an overriding attribute instead of on its definition narrows the domain to the instances of the overriding type: those are then checked against each other only. Since a local <constraints> annotation replaces the inherited one instead of merging with it, declarations on sibling types constrain independent uniqueness domains.
In-app configuration
The additional attributes participating in a constraint are chosen from the attributes of the type that owns the annotated attribute; to-many attributes are not offered, because they cannot participate.
The constraint types offered in the model editor are those annotated as in-app implementations, as is the case for other configurable algorithms. An implementation that serves only as an internal adapter for its own model annotation (the mandatory, size, and range annotations) is therefore not offered as a separately configurable constraint, and the option list contains only translated entries.
Versioning
Uniqueness applies per branch and for the current state: two objects existing simultaneously in the same branch must not share the constrained values. Historical states are not affected—a value freed by a deletion may be reused later. (At the storage level, this corresponds to including the branch and the revision-range marker in the lookup, exactly as the Knowledge Base’s internal indexes already do.)
Optional: DB-level enforcement (advanced DB mapping)
Application-level checks (form + commit) are sufficient to ensure correctness and are the default. When the constrained attributes are stored in actual columns of the object’s table, the equality test is already pushed down to an indexable database query. For advanced database mapping, the constraint may additionally be supported by a real database unique index for efficiency:
- For monomorphic storage (all constrained instances in a single table), the index can be placed directly on the storage table (over the branch, the revision-range marker, and the participating columns).
- For polymorphic storage (instances spread across multiple tables), a single native index cannot span them. Enforcement at the database level then requires a separate flat auxiliary table containing the relevant values (plus the branch/revision marker and the owning object), updated on every commit, with a unique index on that table.
Such a flat auxiliary table, when present, doubles as a fast lookup path for application-level checks (form and non-form mutations) and is the natural place to explicitly implement the “null counts as a value” semantics. It is, however, an optimization—not required for the feature to function.