Database Review
This document records findings from a schema review of the BonardaHR PostgreSQL database, including identified issues, their severity, recommendations, and an action plan.
Summary of Findings
| Severity | Count |
|---|---|
| 🔴 Critical | 4 |
| 🟡 Medium | 6 |
| 🟢 Low | 5 |
🔴 Critical Issues
1. Missing Foreign Key: employees.position
Location: employees table
Issue: The position column stores a plain text string (VARCHAR(100)) rather than a foreign key to a positions table.
Problems:
- Inconsistent position names across employees (e.g. "Software Engineer" vs. "SW Engineer")
- No referential integrity — positions can't be renamed or managed centrally
- Cannot query employees by position without string matching
Recommendation: Add a positions table and a position_id FK:
CREATE TABLE positions (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
...
);
ALTER TABLE employees
ADD COLUMN position_id BIGINT REFERENCES positions(id) ON DELETE SET NULL;
2. Missing Primary Key on section_fields
Location: section_fields table
Issue: The table lacks a clearly defined primary key in its current schema definition.
Recommendation: Verify the migration and ensure a BIGSERIAL PK is defined.
3. Denormalised visible_to_roles Column
Location: employee_sections.visible_to_roles
Issue: Roles are stored as a comma-separated string (e.g. 'ADMIN,HR_MANAGER').
Current state:
visible_to_roles VARCHAR(500) -- e.g., 'ADMIN,HR_MANAGER'
Problems:
- Violates First Normal Form (1NF)
- Requires string parsing in queries
- Cannot use FK constraints
- If a role is renamed, all sections using that role string need manual updates
Recommendation: Replace with a junction table:
CREATE TABLE section_role_visibility (
section_id BIGINT NOT NULL REFERENCES employee_sections(id) ON DELETE CASCADE,
role_id BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (section_id, role_id)
);
4. Document Deletion Cascades When Uploader Leaves
Location: documents.uploaded_by_id FK constraint
Issue: ON DELETE CASCADE means if the employee who uploaded a document is deleted, all their documents are also deleted.
Current state:
CONSTRAINT fk_documents_uploaded_by
FOREIGN KEY (uploaded_by_id) REFERENCES employees(id) ON DELETE CASCADE
Problems:
- Company policy documents and contracts are lost when an HR person leaves
- Document shares and signatures also cascade-delete
- Historical compliance documents could be permanently lost
Recommendation: Change to ON DELETE SET NULL and make uploaded_by_id nullable (or use soft-delete for employees).
🟡 Medium Severity Issues
5. No Foreign Key on Audit Columns
Location: created_by, updated_by columns on all tables
Issue: These columns use BIGINT (matching employees.id) but have no actual FK constraints.
Problems: Orphaned references when employees are deleted; cannot verify who made changes if the auditor no longer exists.
Recommendation:
ALTER TABLE employees
ADD CONSTRAINT fk_employees_created_by
FOREIGN KEY (created_by) REFERENCES employees(id) ON DELETE SET NULL;
6. Timestamps Without Timezone
Location: All TIMESTAMP columns across all tables
Issue: All timestamps use TIMESTAMP without timezone information.
Problems: Timezone ambiguity when deploying across regions; daylight saving time edge cases; API consumers don't know the timezone context.
Recommendation: Use TIMESTAMPTZ:
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
7. Document Share Cascade Deletes When Sharer Leaves
Location: document_shares.shared_by_id
Issue: ON DELETE CASCADE means all document shares are deleted when the sharing employee leaves.
Problems: Other employees lose access to shared documents; audit trail of who shared what is lost.
Recommendation: Change to ON DELETE SET NULL and make nullable.
8. No Soft Delete Pattern for Critical Data
Location: documents, time_off_requests, and other tables
Issue: Hard deletes are used throughout — no deleted_at or is_deleted column.
Problems: Lost historical data; cannot restore accidentally deleted records; potential compliance issues.
Recommendation:
ALTER TABLE documents ADD COLUMN deleted_at TIMESTAMP;
CREATE INDEX idx_documents_not_deleted ON documents(deleted_at) WHERE deleted_at IS NULL;
9. Project Time Log Cannot Record Zero Hours
Location: project_time_logs.hours CHECK constraint
Issue: CHECK (hours > 0) prevents logging zero hours.
Problems: Cannot "zero out" an existing entry; must delete rather than update to 0.
Recommendation: Change to hours >= 0 or handle via application logic.
10. employee_roles Lacks Full Audit Trail
Location: employee_roles table
Issue: Has assigned_at and assigned_by but:
assigned_byhas no FK constraint- No tracking of when roles are removed
Recommendation: Add FK on assigned_by and consider an employee_role_audit table to track role removals.
🟢 Low Severity Issues
11. Inconsistent Unique Constraint Patterns
Some tables use inline UNIQUE constraints; others use separate CREATE UNIQUE INDEX. Standardise on CREATE UNIQUE INDEX for better control (partial indexes, concurrent creation).
12. Missing Composite Indexes for Common Query Patterns
Queries that could benefit from composite indexes:
time_off_requests:(employee_id, status)document_shares:(employee_id, viewed_at)for unread document queriestimesheet_entries:(timesheet_id, entry_date)
13. Redundant Index on employee_sections.name
employee_sections.name already has a UNIQUE constraint (which creates an implicit unique index). The explicit CREATE INDEX idx_employee_sections_name is redundant and should be removed.
14. FieldValueAudit Denormalised Fields (Intentional)
employee_field_value_audit stores field_name and section_name denormalised. If a field or section is later renamed, audit records retain the original names. This is intentional and correct for audit purposes — audit records must preserve the state at the time of the change.
15. No Index on company_events.event_type
If filtering by event_type is common, add:
CREATE INDEX idx_company_events_event_type ON company_events(event_type);
✅ Positive Observations
The schema demonstrates several strong practices:
- Consistent
public_id UUIDfor all API-facing identifiers - Good use of CHECK constraints for enum column validation
- Proper cascade behaviours on most FK relationships
updated_attriggers consistently applied viaupdate_updated_at_column()function- GIN indexes on JSONB columns for efficient dynamic field queries
- Unique constraints preventing duplicates in junction tables
versioncolumn for optimistic locking support throughout
Recommended Action Plan
Phase 1 — Critical
- Add
position_idFK to employees table (createpositionstable) - Replace
visible_to_rolesVARCHAR with asection_role_visibilityjunction table - Change
documents.uploaded_by_idtoON DELETE SET NULL
Phase 2 — Important
- Add FK constraints on audit columns (
created_by,updated_by) withON DELETE SET NULL - Migrate all
TIMESTAMPcolumns toTIMESTAMPTZ - Add soft-delete pattern to critical tables (
documents,time_off_requests)
Phase 3 — Cleanup
- Remove redundant index on
employee_sections.name - Add missing composite indexes for common query patterns
- Standardise unique constraint patterns to use
CREATE UNIQUE INDEX - Add FK and removal tracking to
employee_roles