257 lines
6.3 KiB
Markdown
257 lines
6.3 KiB
Markdown
# Database schema
|
|
|
|
This document defines the intended relational shape. EF Core migrations are the
|
|
executable source once implemented. Names may be refined in M0/M1 through an ADR, but
|
|
constraints and invariants must remain.
|
|
|
|
## Conventions
|
|
|
|
- PostgreSQL `uuid` primary keys generated by the application or database.
|
|
- `timestamptz` for instants.
|
|
- UTC application semantics.
|
|
- lowercase snake_case database names.
|
|
- explicit foreign keys and delete behavior.
|
|
- `citext` or normalized columns for case-insensitive unique values.
|
|
- `jsonb` only for versioned, non-core metadata and import diagnostics.
|
|
- integer/bigint monotonic revision numbers where scoped.
|
|
- byte/string concurrency token exposed as ETag.
|
|
- no database credentials or secrets in data migrations.
|
|
|
|
## Identity and workspace
|
|
|
|
### installation
|
|
|
|
- `id` singleton key
|
|
- `instance_id` uuid unique
|
|
- `product_version`
|
|
- `schema_version`
|
|
- `primary_locale`
|
|
- `time_zone`
|
|
- `setup_completed_at`
|
|
- timestamps / concurrency version
|
|
|
|
### users
|
|
|
|
Use ASP.NET Core Identity-compatible tables with application extensions:
|
|
|
|
- display name
|
|
- enabled state
|
|
- last login
|
|
- security stamp
|
|
- created/updated timestamps
|
|
|
|
### workspaces
|
|
|
|
- `id`
|
|
- `name`
|
|
- normalized `slug`
|
|
- settings version
|
|
- timestamps
|
|
- archived timestamp
|
|
|
|
Unique: normalized slug.
|
|
|
|
### workspace_memberships
|
|
|
|
- workspace/user composite uniqueness
|
|
- role
|
|
- timestamps
|
|
|
|
Constraint: valid role enum. Application invariant: final admin cannot be removed.
|
|
|
|
## Knowledge tables
|
|
|
|
### pages
|
|
|
|
Important columns:
|
|
|
|
```text
|
|
id uuid primary key
|
|
workspace_id uuid not null
|
|
parent_page_id uuid null
|
|
sort_key text not null
|
|
title varchar(300) not null
|
|
normalized_title varchar(300) not null
|
|
slug varchar(320) not null
|
|
markdown text not null
|
|
document_type smallint not null
|
|
document_status smallint not null
|
|
current_revision bigint not null
|
|
content_hash char(64) not null
|
|
concurrency_version bigint not null
|
|
created_by / updated_by uuid
|
|
created_at / updated_at / published_at timestamptz
|
|
trashed_at / purge_after timestamptz null
|
|
search_vector tsvector generated or maintained
|
|
```
|
|
|
|
Indexes:
|
|
|
|
- `(workspace_id, parent_page_id, sort_key, id)`
|
|
- `(workspace_id, slug)`
|
|
- `(workspace_id, updated_at desc, id)`
|
|
- partial active-page indexes where `trashed_at is null`
|
|
- GIN `search_vector`
|
|
- GIN/GiST trigram on normalized title
|
|
- optional path/ancestor closure only if measured need justifies it
|
|
|
|
Cycle prevention is primarily application-controlled and integration-tested. A recursive
|
|
query validates moves transactionally.
|
|
|
|
### page_revisions
|
|
|
|
Unique `(page_id, revision_number)`. Store immutable snapshots and content hash.
|
|
|
|
Indexes:
|
|
|
|
- page/revision descending
|
|
- workspace/created timestamp
|
|
- author/time where audit UI needs it
|
|
|
|
No cascade delete during ordinary content lifecycle.
|
|
|
|
### drafts
|
|
|
|
Unique `(workspace_id, user_id, draft_key)`. Draft content is private to the author.
|
|
|
|
### page_links
|
|
|
|
Unique identity includes source page, occurrence key and source revision. Index target
|
|
page for backlinks and unresolved text for maintenance.
|
|
|
|
### page_relations
|
|
|
|
Unique `(source_page_id, target_page_id, relation_type)` unless multiple labels are
|
|
explicitly supported.
|
|
|
|
### tags / page_tags
|
|
|
|
- normalized tag unique per workspace
|
|
- composite join primary key
|
|
- indexes for tag-filter search
|
|
|
|
### favorites / recent_visits
|
|
|
|
Composite user/content uniqueness. Recent visits use upserted last-view time and bounded
|
|
retention.
|
|
|
|
## Attachments
|
|
|
|
### attachment_blobs
|
|
|
|
Primary key is 64-character lowercase SHA-256.
|
|
|
|
Columns include:
|
|
|
|
- length
|
|
- detected media type
|
|
- storage/preview/integrity states
|
|
- created/verified timestamps
|
|
- reference count cache only if transactionally maintained
|
|
|
|
### attachment_references
|
|
|
|
Stable logical ID, workspace, blob hash, safe original filename and descriptive metadata.
|
|
|
|
### attachment_usages
|
|
|
|
Polymorphic ownership must not use an unconstrained arbitrary string alone. Prefer
|
|
separate nullable constrained columns or typed join tables for page/project update usage.
|
|
The selected design requires an ADR.
|
|
|
|
Indexes support owner lookup and orphan discovery.
|
|
|
|
## Projects
|
|
|
|
### projects
|
|
|
|
Unique normalized project slug per workspace. Index lifecycle status and last update.
|
|
|
|
### project_documents
|
|
|
|
Unique `(project_id, page_id, role)`.
|
|
|
|
### project_updates
|
|
|
|
Ordered by `(occurred_at desc, id desc)`. Immutable content with correction/supersession
|
|
links.
|
|
|
|
### project_milestones
|
|
|
|
Unique name or sequence per project; stable sort key.
|
|
|
|
### technology_tags / project_technology_tags
|
|
|
|
Normalized unique technology name per workspace.
|
|
|
|
## Decisions and runbooks
|
|
|
|
### decision_records
|
|
|
|
- unique page profile
|
|
- unique accepted decision number per project
|
|
- self-referencing supersession constraints
|
|
- indexes by project/status/date
|
|
|
|
### runbook_profiles
|
|
|
|
- unique page profile
|
|
- risk/validation metadata
|
|
- review/validation indexes
|
|
|
|
## Imports
|
|
|
|
### import_jobs
|
|
|
|
Unique source fingerprint plus invocation identity does not automatically prevent a new
|
|
deliberate import, but idempotency keys prevent duplicate execution.
|
|
|
|
### import_items
|
|
|
|
Unique `(import_job_id, source_identity)`. Index target identities and status.
|
|
|
|
### import_provenance
|
|
|
|
Links imported target rows to job/item. This table is the authoritative rollback scope.
|
|
|
|
## Audit, jobs and outbox
|
|
|
|
### audit_events
|
|
|
|
Append-only, indexed by workspace/time, actor/time, target/time and correlation ID.
|
|
Partitioning is deferred until measured volume warrants it.
|
|
|
|
### background_jobs
|
|
|
|
Durable state, lease owner/expiry, attempt count, schedule, progress and safe diagnostics.
|
|
|
|
### outbox_messages
|
|
|
|
Index unprocessed `(available_at, id)`. Retention after successful dispatch is
|
|
configurable.
|
|
|
|
## Search implementation
|
|
|
|
Use the PostgreSQL `simple` text-search configuration initially because documents mix
|
|
Dutch, English, code, commands and product names. Maintain normalized searchable text
|
|
from:
|
|
|
|
- title;
|
|
- stripped Markdown text;
|
|
- tags;
|
|
- selected structured metadata.
|
|
|
|
Use trigram matching for titles/slugs/tags and explicit ranking weights. Permission
|
|
filters must be part of the query, not a post-filter after result selection.
|
|
|
|
## Migration requirements
|
|
|
|
Every migration must:
|
|
|
|
- be deterministic;
|
|
- be tested from the previous released schema;
|
|
- avoid silent data truncation;
|
|
- include data backfill verification where needed;
|
|
- document locks/expected duration for large tables;
|
|
- have a tested application rollback strategy even when SQL downgrade is unsafe.
|