# MARKER INDEXES — constraints DynamoDB doesn't have

A Kanject **marker** index is *not* a GSI — it's a set of **reserved rows in the base table itself** that the Kanject Core library reads to enforce and resolve a constraint DynamoDB has no native concept of: uniqueness, membership buckets, value-sorted ranges. Creating one backfills those rows from existing data; the matching annotation (`[Unique]`, `[Collection]`, …) is what *enforces* the constraint on every future write.

## Syntax

```sql
CREATE UNIQUE     INDEX ["name"] ON "table" (col[, col…]) [WHERE …]
CREATE COLLECTION INDEX ON "table" (col)
CREATE RANGE      INDEX ON "table" (col)

ALTER  <kind> INDEX ON "table" (col) REFRESH        -- reconcile to current data
ALTER  <kind> INDEX ON "table" (col) SET WHERE …    -- re-scope a partial marker
DROP   <kind> INDEX ON "table" (col)                -- delete the reserved rows
```

Runs from the PartiQL console and Browse's PartiQL mode for **bounded tables** (the in-app path is scan-capped); larger tables route to an AWS Glue job. Guide: [Tables, indexes & replicas](https://www.kanject.com/docs/dynostudio-partiql-ddl/#kanject-marker-indexes).

## Use cases

### Enforce unique emails

One reserved row per distinct value. The backfill scans, groups by value — and **if a value repeats it refuses and lists the collisions**: it never picks a winner; you resolve them and re-run.

```sql
CREATE UNIQUE INDEX ON "stage.Users" (email)
```

_Executes as:_ Backfill: scan → group by email → one reserved row per distinct value (refuses on collisions)

- Pair it with the `[Unique]` annotation in Kanject Core — the backfill alone is point-in-time.

_Quote this example:_ https://www.kanject.com/docs/dynostudio-marker-indexes/#unique-email

### Unique within a tenant (composite)

The same email may exist across tenants but not within one — a multi-column unique, named to match the entity's `[CompositeUnique("uxTenantEmail")]` group.

```sql
CREATE UNIQUE INDEX "uxTenantEmail" ON "stage.Users" (tenantId, email)
```

_Executes as:_ Backfill: one reserved row per distinct (tenantId, email) pair

_Quote this example:_ https://www.kanject.com/docs/dynostudio-marker-indexes/#composite-unique

### Scope a rule to active rows

Active users need unique emails; deactivated accounts may share one. A trailing `WHERE` makes the marker **partial** — it only conflicts *within* that scope.

```sql
CREATE UNIQUE INDEX ON "stage.Users" (email)
WHERE status = 'active'
```

_Executes as:_ Backfill applies the predicate per scanned item · an unparseable predicate refuses rather than index everything

_Quote this example:_ https://www.kanject.com/docs/dynostudio-marker-indexes/#partial-unique

### Bucket items by a value

A membership marker — one reserved row **per item**, bucketed by status, no uniqueness check. Needs the base table to have a sort key.

```sql
CREATE COLLECTION INDEX ON "stage.Orders" (status)
```

_Executes as:_ Backfill: one reserved row per item, bucketed by status

_Quote this example:_ https://www.kanject.com/docs/dynostudio-marker-indexes/#collection-marker

### Reconcile a marker to current data

Data changed outside the annotations? `REFRESH` recomputes the reserved rows in one scan and diffs: adds the missing, removes the stale, reports the diff — and a `UNIQUE` refresh can surface *new* conflicts and refuse, just like create.

```sql
ALTER UNIQUE INDEX ON "stage.Users" (email) REFRESH
```

_Executes as:_ Scan → recompute reserved rows → diff against stored (add missing · remove stale · report)

- `DROP … INDEX` deletes the reserved rows — but if the entity is still annotated, the SDK re-creates them on the next write, so drop the annotation too.

_Quote this example:_ https://www.kanject.com/docs/dynostudio-marker-indexes/#refresh-marker

## Try it

**Try it · UNIQUE marker**

```sql
CREATE UNIQUE INDEX ON "stage.Users" (email)
WHERE status = 'active'
```

_Executes as:_ Marker backfill · stage.Users (email, partial) — Refuses with the colliding items listed if any active email repeats — you resolve, then re-run. Reserved rows live in the base table; this is not a GSI.

_Result:_ Backfill complete · 4,918 reserved rows written · 0 collisions · add [Unique] to the entity to enforce future writes

_Run it live in DynoStudio:_ https://www.kanject.com/dynostudio/

> **The backfill is point-in-time:** DynoStudio writes the reserved rows; the Kanject Core annotation **enforces** the constraint on every future write. A backfill without the matching `[Unique]` / `[Collection]` annotation drifts the moment the app writes again — the run nudges you to add it for exactly this reason.

Related: [Tables, indexes & replicas](https://www.kanject.com/docs/dynostudio-partiql-ddl/) · [CREATE GSI](https://www.kanject.com/docs/dynostudio-create-gsi/) for read paths (markers are for *constraints*) · [Kanject.Core.NoSqlDatabase](https://www.kanject.com/docs/core-nosql/) for the annotations.

---
_Source: https://www.kanject.com/docs/dynostudio-marker-indexes/ · Kanject Docs_
