Skip to content

storage unique index excludes player_id, blocking per-player game.storage usage #122

@Taure

Description

@Taure

Bug

The `storage` table has `UNIQUE (collection, key)` (per migration `m20260329095708_update_schema.erl`). Per-player rows therefore collide: two players writing to the same `(collection, key)` with different `player_id` values can't both insert.

`asobi_lua`'s `game.storage.player_set/4` upserts via `storage_set → storage_get → insert-or-update`. `storage_get` filters by `player_id` (via `maybe_filter_player`) so for a new player it returns `{error, not_found}`, and the subsequent `storage_insert` hits the unique constraint and silently fails.

Repro

```
docker compose up (asobi_lua + postgres)
register two players → both write game.storage.player_set(pid, "foo", "bar", #{n => 1})
SELECT * FROM storage; → only the first player's row exists
```

Confirmed against `ghcr.io/widgrensit/asobi_lua:latest` (built post widgrensit/asobi_lua#44) using barrow's smoke test.

Fix

Widen the unique index to `(collection, key, COALESCE(player_id, '00000000-0000-0000-0000-000000000000'))` or split into two indexes (one for global rows where `player_id IS NULL`, one for player-scoped where it isn't). Either way, both routes through `game.storage.player_set` and `game.storage.set` must remain unique within their own scope.

A new migration adding the corrected index (and dropping the old one) is the safe path; in-flight rows won't violate the new constraint.

Workaround (currently used by barrow)

Embed `player_id` in the storage `key`: `game.storage.player_set(pid, "barrow", "run_loot:" .. pid, ...)`. Ugly but unblocks per-player state until the constraint is widened.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions