This document describes PlainNAS tag behavior, requirements, and the current implementation.
- Tag: a named label, stored as a record with fields like
id,name,type, andcount. - Tag relation: a link between a tag and an item key.
- For media, the key is the media UUID.
- Relations are stored as key presence in Pebble (value is a constant
1), not as JSON payloads.
-
Tag record:
- Key:
tag:<tagID> - Value: JSON of
db.Tag
- Key:
-
Tag relation primary record:
- Key:
tag_relation:<tagID>:<key> - Value:
1
- Key:
-
Tag relation secondary index (by item key):
- Key:
tag_relation_key:<key>:<tagID> - Value:
1
- Key:
Notes:
- The secondary index enables a fast lookup for “what tags are on this item key?”
EnsureTagRelationKeyIndex()can backfill thetag_relation_key:index from existingtag_relation:keys.
tag.count must reflect the number of existing relations for that tag:
tag.count == number_of(tag_relation:<tagID>:...)
It must be updated when:
- Adding tag relations.
- Removing tag relations.
- Running a reindex/rebuild operation.
Repeated “add the same relation” operations must not inflate tag.count. The relation key is unique (tag_relation:<tagID>:<key>), so a repeated save overwrites the same record and the derived count remains stable.
If a file/media item is deleted or moved to trash, its tag relations must be removed (so tags never point at trash/missing items).
- GraphQL mutations call into DB helpers:
UpdateTagRelationsadds/removes relations for a single item.- Bulk operations like
addToTags/removeFromTagsadd/remove relations for a query result.
The actual persistence happens in the DB layer:
SaveTagRelation/SaveTagRelationsDeleteTagRelationsByTagIDDeleteTagRelationsByKeys...
The authoritative source of truth is the relation records, not the stored Tag.Count value.
-
On every relation add (save):
- Persist
tag_relation:andtag_relation_key: - Recompute count for that tag and update the tag record.
- Persist
-
On relation remove (delete):
- Delete
tag_relation:andtag_relation_key: - Recompute count for every affected tag and update their tag records.
- Delete
Helpers:
-
RecomputeTagCount(tagID):- Iterates
tag_relation:<tagID>:prefix and counts entries - Writes the result back to the tag record
- Iterates
-
RebuildAllTagCounts():- Scans all
tag_relation:keys once to buildtagID -> count - Iterates all tags and updates each
Tag.Countaccordingly
- Scans all
Media reindexing ("rebuild media index") resets media data and indexes. Tag relations are independent from media records, so a reset can leave behind stale relations (UUIDs that no longer exist after the rebuild).
After the reindex scan completes, PlainNAS performs a cleanup pass:
- Iterates
tag_relation_key:*and deletes relations for UUIDs that are missing from the rebuilt media store - Runs
RebuildAllTagCounts()to repair all storedtag.countvalues
Implementation notes:
- Media trash: when a media item is moved to trash, its UUID relations are removed.
- Media delete: when a media item is deleted from the media store, its UUID relations are removed.
- Reindex cleanup is only triggered after
ResetAllMediaData()(full rebuild).
The following regressions are covered:
- Add relation =>
tag.countincrements. - Add the same relation again =>
tag.countremains correct. - Remove relation =>
tag.countdecrements. - Rebuild counts => fixes intentionally incorrect stored
tag.count.
If tag counts look wrong:
- Ensure relations exist as expected:
tag_relation:<tagID>:...keys should match the desired tag usage.
- Run a count rebuild (development/admin flow):
- Trigger a media index rebuild (which also rebuilds tag counts), or call the backend helper if you have an internal admin route.
- If your DB is old and missing
tag_relation_key:keys:- Run
EnsureTagRelationKeyIndex()once to backfill the by-key index.
- Run
- Avoid maintaining
tag.countvia incremental +1/-1 updates unless you can guarantee transactional correctness across all write paths. - Prefer deriving
tag.countfrom relation keys to keep invariants simple and resilient to unexpected or legacy data states.