# Kanject marker indexes

A Kanject **marker index** is not a GSI. It is a model-declared access path materialized as **reserved rows in the base table**. Markers can enforce uniqueness, group related items, order values for range reads, or support search. Kanject generates both the index-aware write methods that maintain those rows and the finder methods that read through them.

## The model is the definition

```csharp
[Searchable(template: "Marketplace#{ItemCountryId}")]
public string ItemName { get; set; }

[Range(name: "Price", template: "Marketplace#{ItemCountryId}")]
public decimal ItemPrice { get; set; }

[Collection(name: "SellerMarketplaceItems")]
public Guid SellerUserId { get; set; }

[Unique("UniqueBundleRef")]
public string BundleReference { get; set; }
```

The annotation carries more than a property name. A marker key can depend on its name, a `template` or `rangeTemplate`, and several properties from the entity. Composite unique markers also share a named group. That model metadata — together with the matching generator contract — is the canonical definition.

- **`[Unique]` / `[CompositeUnique]`** reserve a key for a value or named value group so competing owners cannot claim the same marker.
- **`[Collection]`** materializes a membership access path, optionally composing other properties into its ordering key.
- **`[Range]`** materializes a value-ordered access path for generated range finders.
- **`[Searchable]`** materializes the tokens and references used by generated search methods.

> **Why a column-only CREATE INDEX is insufficient:** `CREATE RANGE INDEX ON "Items" (ItemPrice)` does not capture `template: "Marketplace#{ItemCountryId}"`, the marker name, generated key encoding, reference fields, or generator version. DynoStudio does not create, alter, refresh, or drop Kanject markers from PartiQL DDL.

## Writes maintain the base item and markers together

```csharp
await Repository.InsertWithIndexAsync(entity);
await Repository.CommitAsync();

await Repository.UpdateWithIndexAsync(updatedEntity, previousEntity);
await Repository.CommitAsync();

await Repository.RemoveWithIndexAsync(entity);
await Repository.CommitAsync();
```

The generated `*WithIndexAsync` methods stage the base-item operation and its marker operations in the same transaction. `CommitAsync()` is the point that submits that unit of work. Updates receive the previous entity because changing a participating property may require deleting old marker keys before inserting the new ones.

> **Generated does not mean invisible:** The annotations generate the index-aware API; they do not intercept every write to DynamoDB. Direct SDK writes, imports, and repository paths that do not delegate to `*WithIndexAsync` can bypass marker maintenance. Use those methods consistently and commit the transaction.

## Generated finders turn markers into access paths

```csharp
var (bundles, page) =
    await Repository.FindBuyerBundlesAsync(buyerUserId, pagination);

var bundle =
    await Repository.FindBundleByUniqueBundleRefAsync(reference);
```

A generated finder usually reads marker rows first and then resolves their base-item references. This is why markers are not only constraints: collection, range, and search markers are application query paths. DynoStudio can expose the definition, composed key shape, reserved rows, owner references, generated finder, and the extra read phase involved.

A generated `Is…UniqueAsync` method is useful for friendly validation, but it is not the concurrency boundary by itself. Correctness comes from the transactional index-aware write; another writer can race a standalone precheck.

## Existing data needs a migration

Adding or changing an annotation governs index-aware writes made after that code ships; it does not populate marker rows for items already stored. Existing-data backfill, reconciliation, or removal must run as a separate durable migration that consumes the same Kanject model metadata and marker composer as the application.

- Pin the Kanject generator or marker-contract version and record a definition hash.
- Checkpoint progress so a long run can resume safely, and make retries idempotent.
- Compose every marker type from complete entities, including templates, search tokens, reference fields, and composite groups.
- Detect unique collisions without choosing a winner, and report enough owner information to resolve them.
- Account for concurrent application writes, throttling, cancellation, audit history, and a controlled rollback or cleanup path.

> **DynoStudio’s boundary:** DynoStudio is the place to discover marker definitions, inspect reserved rows and owner references, exercise generated access paths, and diagnose drift. A production backfill or reconcile is a durable, model-driven migration job — not a desktop scan and not a freehand SQL statement.

Related: [Tables, indexes & replicas](https://dynostudio.dev/docs/dynostudio-partiql-ddl/) for supported control-plane DDL · [CREATE GLOBAL INDEX](https://dynostudio.dev/docs/dynostudio-create-gsi/) for native DynamoDB secondary indexes · [Kanject.Core.NoSqlDatabase](https://dynostudio.dev/docs/core-nosql/) for the annotations and generated repositories.

---
_Source: https://dynostudio.dev/docs/dynostudio-marker-indexes/ · DynoStudio Docs_
