Skip to content
Alvo is pre-v0.1: the image runs from its edge tag, and no NuGet package or release is published yet.Pre-v0.1: no release yet.Roadmap and status

Indexes and uniqueness

Declare composite indexes for the queries you run most, make a combination of fields unique, and know which indexes Alvo already creates.

  • The stack from Run your own descriptor, run from its alvo-help-desk directory with COMPOSE_FILE and that page’s secrets exported in this shell. This page uses the agent key (roles agent, authenticated).
  • Entities and fields, for fields and unique.

Declare an index only beyond these, which every entity already has:

  • the primary key id;
  • every field with "unique": true;
  • every ref to a declared entity.

A ref to the built-in users entity is not indexed by itself.

  1. Start the stack over this page’s descriptor. The first command deletes the stack’s database:

    Terminal
    docker compose down --volumes
    curl -fsSL -o help-desk.alvo.json https://raw.githubusercontent.com/Burgyn/MMLib.Alvo/main/website/src/snippets/indexes/01-indexes.alvo.json
    docker compose up --wait --wait-timeout 90
  2. The descriptor declares three indexes:

    help-desk.alvo.json
    {
    "$schema": "https://alvo.dev/schema/v1/project.json",
    "apiVersion": "alvo.dev/v1",
    "name": "help-desk",
    "auth": {
    "providers": ["local"],
    "roles": ["admin", "agent"]
    },
    "entities": {
    "tickets": {
    "fields": {
    "queue": {
    "type": "enum", "values": ["billing", "hardware", "software"],
    "required": true
    },
    "number": { "type": "integer", "required": true },
    "title": { "type": "string", "required": true, "maxLength": 120 },
    "status": {
    "type": "enum", "values": ["open", "closed"], "default": "open"
    },
    "assignee_id": { "type": "ref", "entity": "users", "index": true }
    },
    "indexes": [
    { "fields": ["queue", "status"] },
    { "fields": ["queue", "number"], "unique": true }
    ],
    "rules": {
    "list": "'authenticated' in @user.roles",
    "get": "'authenticated' in @user.roles",
    "create": "'agent' in @user.roles || 'admin' in @user.roles",
    "update": "'agent' in @user.roles || 'admin' in @user.roles"
    }
    }
    }
    }
    • "index": true on assignee_id is the one-field form. Use it for a single field, here a ref to users, which gets no index otherwise.
    • {"fields": ["queue", "status"]} is a composite index. List the fields in query order: callers filter on queue first, then on status.
    • {"fields": ["queue", "number"], "unique": true} makes the combination unique: one ticket per number within each queue.

A plain index never refuses a write; it only makes reads faster. When the schema assistant (or any JSON Patch) adds the first index, it must add the whole indexes list: an add at /indexes/- on a list that does not exist yet is refused.

  1. Ticket 1 in billing, then ticket 1 in hardware. The same number in two queues is allowed:

    Request
    curl -sS -X POST http://localhost:8080/api/tickets \
    -H "X-Alvo-Api-Key: agent.$ALVO_AGENT_KEY_SECRET" \
    -H "Content-Type: application/json" \
    -d '{"queue":"billing","number":1,"title":"Invoice sent twice"}'

    201 Created

    Response
    HTTP/1.1 201 Created
    Content-Type: application/json; charset=utf-8
    Location: /api/tickets/66f74c91-8e33-4460-b971-5d6142011be6
    {
    "id": "66f74c91-8e33-4460-b971-5d6142011be6",
    "queue": "billing",
    "number": 1,
    "title": "Invoice sent twice"
    }
    Request
    curl -sS -X POST http://localhost:8080/api/tickets \
    -H "X-Alvo-Api-Key: agent.$ALVO_AGENT_KEY_SECRET" \
    -H "Content-Type: application/json" \
    -d '{"queue":"hardware","number":1,"title":"Laptop will not charge"}'

    201 Created

    Response
    HTTP/1.1 201 Created
    Content-Type: application/json; charset=utf-8
    Location: /api/tickets/fb953b71-a719-4363-8596-5ac4cfb7eab0
    {
    "id": "fb953b71-a719-4363-8596-5ac4cfb7eab0",
    "queue": "hardware",
    "number": 1,
    "title": "Laptop will not charge"
    }
  2. A second ticket 1 in billing collides with the first:

    Request
    curl -sS -X POST http://localhost:8080/api/tickets \
    -H "X-Alvo-Api-Key: agent.$ALVO_AGENT_KEY_SECRET" \
    -H "Content-Type: application/json" \
    -d '{"queue":"billing","number":1,"title":"Refund for March"}'

    409 Conflict

    Response
    HTTP/1.1 409 Conflict
    Content-Type: application/problem+json
    {
    "type": "https://alvo.dev/errors/conflict",
    "title": "Conflict",
    "status": 409,
    "detail": "This field is declared unique and another record already holds the value sent for it.",
    "violations": [
    {
    "pointer": "/queue",
    "code": "unique",
    "message": "This field is declared unique and another record already holds the value sent for it.",
    "fixSuggestion": "Send a value no other record holds, or change the record that holds it."
    },
    {
    "pointer": "/number",
    "code": "unique",
    "message": "This field is declared unique and another record already holds the value sent for it.",
    "fixSuggestion": "Send a value no other record holds, or change the record that holds it."
    }
    ]
    }

    Each field of the combination gets a violation with the code unique. The message is the one a single unique field gets; the pointers name the combination.

A declared index becomes a database index when the descriptor is applied, and a unique one makes the database refuse a duplicate, which Alvo answers as 409 conflict. On a tenant-scoped entity, Alvo puts tenant_id first in every unique index, both a field’s unique and a unique combination, so two tenants may hold the same value and neither learns that the other does. Multi-tenancy covers tenant-scoped entities.

You wantDeclare
One field unique across the entitythe field’s unique
A combination unique, such as one number per queuean entry in indexes with unique
A faster filter on one fieldthe field’s index
A faster filter on several fields togetheran entry in indexes with its fields in query order

A field’s unique and a unique combination are different rules: unique on both queue and number would allow one ticket per queue in total.

StatusProblem typeWhenFixReturned by
409conflictA write would give two rows the same value of a unique field, or the same combination of a unique index (code unique).Send a value, or a combination, that no other row holds.every host

A new unique index cannot be created while duplicate rows exist. Before you add one to an entity that already holds data, remove or change the rows that share a value.

Apply and evolve your descriptor: change a running backend safely, with dry runs, revisions and rollback.