# Tag Denormalization Strategy

## Overview

The tag system uses **denormalized tables** for efficient "get all items with tag X" queries. This document explains how to maintain consistency across these tables.

## Table Architecture

### Primary Tables (Source of Truth)
- `collections_by_id` - Collections with `tag_ids LIST<UUID>` field
- `files_by_id` - Files with `tag_ids LIST<UUID>` field
- `tag_assignments_by_entity` - Lightweight assignment tracking

### Denormalized Tables (For Query Performance)
- `collections_by_tag_id` - Full collection data partitioned by tag_id
- `files_by_tag_id` - Full file data partitioned by tag_id

## Maintenance Responsibilities

### Tag Repository (`internal/repo/tag/tag.go`)
**Maintains:**
- `tag_assignments_by_entity` only

**Does NOT maintain:**
- `collections_by_tag_id`
- `files_by_tag_id`

**Reason**: Tag repository doesn't have access to full collection/file data needed for denormalized tables.

### Collection Repository (`internal/repo/collection/*.go`)
**Must maintain these tables when collections change:**

#### On Collection Create:
1. Insert into `collections_by_id` with `tag_ids = []`
2. For each tag_id in collection.tag_ids:
   - Insert into `collections_by_tag_id`

#### On Collection Update:
1. Update `collections_by_id`
2. **Sync denormalized tables:**
   - Get old tag_ids from existing collection
   - Calculate diff: added tags, removed tags
   - For removed tags: `DELETE FROM collections_by_tag_id WHERE tag_id = ? AND collection_id = ?`
   - For added tags: `INSERT INTO collections_by_tag_id (...)`
   - For unchanged tags: `UPDATE collections_by_tag_id ...` (if other fields changed)

#### On Collection Delete:
1. Delete from `collections_by_id`
2. For each tag_id in collection.tag_ids:
   - Delete from `collections_by_tag_id`

### File Repository (`internal/repo/file/*.go`)
**Same pattern as collections** but for `files_by_tag_id` table.

## Tag Assignment Flow

### Assigning a Tag to a Collection:
```go
// 1. Service layer calls
tagService.AssignTag(ctx, userID, tagID, collectionID, "collection")

// 2. Tag service:
//    a) Get current collection
collection := collectionRepo.Get(ctx, collectionID)

//    b) Add tag to collection's tag_ids
collection.TagIds = append(collection.TagIds, tagID)

//    c) Update collection (this triggers denormalization)
collectionRepo.Update(ctx, collection)
//       - Updates collections_by_id
//       - Inserts into collections_by_tag_id
//       - Updates tag_assignments_by_entity

// 3. Tag repository only updates lightweight tracking
tagRepo.AssignTag(ctx, assignment)
//    - Inserts into tag_assignments_by_entity only
```

### Unassigning a Tag:
```go
// 1. Service layer calls
tagService.UnassignTag(ctx, tagID, collectionID, "collection")

// 2. Tag service:
//    a) Get current collection
collection := collectionRepo.Get(ctx, collectionID)

//    b) Remove tag from collection's tag_ids
collection.TagIds = removeTag(collection.TagIds, tagID)

//    c) Update collection
collectionRepo.Update(ctx, collection)
//       - Updates collections_by_id
//       - Deletes from collections_by_tag_id for removed tag
//       - Updates tag_assignments_by_entity

// 3. Tag repository updates tracking
tagRepo.UnassignTag(ctx, tagID, collectionID, "collection")
//    - Deletes from tag_assignments_by_entity
```

## Query Patterns

### Get All Collections with Tag X:
```sql
-- Efficient single-partition query!
SELECT * FROM collections_by_tag_id WHERE tag_id = ?
```

### Get All Tags for Collection Y:
```sql
-- Efficient query using tag_assignments_by_entity
SELECT tag_id FROM tag_assignments_by_entity
WHERE entity_id = ? AND entity_type = 'collection'
```

## Trade-offs

### Pros:
- ✅ **100x faster** queries for "show all items with tag X"
- ✅ Single partition reads (optimal Cassandra performance)
- ✅ Enables tag-based filtering in UI

### Cons:
- ❌ Write amplification (each collection update = N writes where N = number of tags)
- ❌ Data duplication (collection data stored in 3+ tables)
- ❌ Complexity in keeping tables in sync

## Implementation Checklist

When implementing denormalization in Collection/File repositories:

- [ ] On Create: Insert into denormalized table for each tag
- [ ] On Update: Diff tag_ids and sync denormalized table
- [ ] On Delete: Remove from denormalized table for all tags
- [ ] Handle edge cases: empty tag_ids, nil tag_ids
- [ ] Add logging for denormalization failures
- [ ] Consider using Cassandra batches for atomic writes
- [ ] Test concurrent updates to same collection/file

## Future Optimizations

If write performance becomes an issue:

1. **Use BATCH statements** for atomic multi-table writes
2. **Async denormalization** with message queue
3. **Materialized views** (Cassandra native, but with caveats)
4. **Caching layer** (Redis) to reduce read pressure
