# Postgres Specialist Report — Enforce unique Name on 5 entity tables
## Revision context
The human reviewer requested: *"I don't need any API yet, just change the
data model in postgres, revert other changes."*
The previous iteration enforced uniqueness in **application logic** (an
`EXISTS` pre-check inside `usp_Insert*`/`usp_Update*` procedures that
`RAISE`d a `unique_violation`) plus a one-off dedupe **patch**. None of it
was an actual data-model constraint.
This revision replaces that with a pure **data-model** change — DB-level
partial/total unique indexes — and reverts everything else (the procedures
and the patch), per the reviewer's instruction.
## Files modified
- `Database/xyz/Constraints/999_indexes.sql` — appended five
`CREATE UNIQUE INDEX IF NOT EXISTS` statements (one per entity), grouped
by table with the standard `-- xyz.
` header comments. No new index
file was created (per convention, all indexes live in `999_indexes.sql`).
## Files reverted (deleted)
These were the API-facing / one-off changes the reviewer asked to drop:
- `Database/xyz/Procedures/usp_InsertAsset.sql`
- `Database/xyz/Procedures/usp_InsertAssetType.sql`
- `Database/xyz/Procedures/usp_InsertSystemType.sql`
- `Database/xyz/Procedures/usp_InsertCommissioningSystem.sql`
- `Database/xyz/Procedures/usp_InsertCommissioningWorkflow.sql`
- `Database/xyz/Procedures/usp_UpdateAsset.sql`
- `Database/xyz/Procedures/usp_UpdateCommissioningSystem.sql`
- `Database/xyz/Procedures/usp_UpdateCommissioningWorkflow.sql`
- `Database/Patch/018_patch_dedupe_entity_names.sql`
Working tree is now clean apart from the single `999_indexes.sql` edit.
## The data-model change (unique indexes)
All five are expressed as unique **indexes** (not table `CONSTRAINT`s)
because two of them are partial (`WHERE "IsDeleted" = FALSE`), and a partial
unique key can only be an index. The other three could have been
`UNIQUE` constraints, but they are kept as indexes so all five live together
in `999_indexes.sql` and read consistently.
| Table | Unique scope | Notes |
| --- | --- | --- |
| `AssetType` | `(ProjectShardId, CommissioningWorkflowId, Name)` | total |
| `SystemType` | `(ProjectShardId, CommissioningWorkflowId, Name)` | total |
| `Asset` | `(ProjectShardId, ParentAssetId, Name)` | partial `WHERE IsDeleted = FALSE`, `NULLS NOT DISTINCT` |
| `CommissioningSystem` | `(ProjectShardId, SystemTypeId, Name)` | partial `WHERE IsDeleted = FALSE` |
| `CommissioningWorkflow` | `(ProjectShardId, Name)` | total |
Scope columns were confirmed against the table create files
(`118_xyz_asset_type.sql`, `119_xyz_system_type.sql`, `129_xyz_asset.sql`,
`130_xyz_commissioning_system.sql`, `116_xyz_commissioning_workflow.sql`).
Only `Asset.ParentAssetId` is nullable — hence `NULLS NOT DISTINCT` there so
root-level assets (NULL parent) are also constrained to unique names. The
soft-delete tables (`Asset`, `CommissioningSystem`) use a partial predicate
so a soft-deleted name can be reused. `NULLS NOT DISTINCT` is already used
elsewhere in the repo (`011_xyz_categorytype_constrains.sql`) and the
deployment image is `postgres` (latest, PG16+), so the syntax is supported.
These statements are AI-generated. The unique-index lines carry no
`--changeset` of their own — they were appended to the existing
`davewebb:999_indexes` changeset, which already has `runOnChange:true` and
re-runs on change, exactly as the conventions require for index additions.
## Deviation from `_PLAN.yaml` (intentional, driven by reviewer)
`_PLAN.yaml` listed `index_changes: none` and routed enforcement through
amended procedures. The reviewer's revision explicitly asked for a
data-model change with the API/procedure work reverted, which can only be
satisfied with DB-level unique indexes. I followed the reviewer's
instruction over the stale plan field. This supersedes the plan's
proc-based approach and its dedupe patch.
## Build / test
- `./build` — **not available** in this repo (no such script; the brief's
reference to it does not match this checkout).
- `./test` / `./test --no-docker` — **could not run**: Docker is not
available in this environment and no Postgres is running on 5490.
Runtime verification was therefore not performed. Static review done:
column names and nullability verified against the table files; index syntax
(`NULLS NOT DISTINCT` before `WHERE`) matches the Postgres `CREATE INDEX`
grammar; the edit is confined to the existing index changeset.
## Open questions for the human reviewer
1. **Existing duplicate data / deployment ordering.** These tables are
newly added (June 2026) so there is most likely no production data to
dedupe, and a fresh `./build` deploys clean. But if any environment
already holds colliding names, creating these unique indexes will FAIL.
The previous dedupe patch did not actually solve this — `Patch/` runs
*after* `Constraints/999_indexes.sql`, so it would run too late. If you
want a safety net, the dedupe must be a **pre-deploy data migration run
ahead of this change**, not a `Patch/` file. I reverted the patch as
"other changes"; let me know if you want it reinstated as a pre-deploy
step.
2. **Case sensitivity.** These indexes are case-**sensitive** (`'Pump'` vs
`'pump'` are distinct), matching the plan's default assumption. If
case-insensitive uniqueness is wanted, the indexes need to be on
`LOWER("Name")`.
3. **Uniqueness scope.** Per-project plus natural parent scope, as the plan
assumed (AssetType/SystemType → CommissioningWorkflowId, Asset →
ParentAssetId, CommissioningSystem → SystemTypeId, CommissioningWorkflow
→ project only). Confirm this matches intent.