Skip to main content

Database Design

Overview

Checkmate uses MySQL 8.0 as its primary database, designed to efficiently manage organizations, projects, test cases, test runs, and associated metadata.

Design Principles

  • Normalized Structure - Minimize data redundancy
  • Referential Integrity - Foreign keys enforce relationships
  • Performance Optimized - Strategic indexes on frequently queried columns
  • Audit Trail - CreatedBy, updatedBy, timestamps on key entities
  • Soft Deletes - Status flags instead of hard deletes where appropriate

Schema Visualization

You can visualize the complete schema by importing _schema.sql into dbdiagram.io.

Schema File Location: /sql/_schema.sql


Database Structure

Table Categories

The database consists of 16 tables organized into four categories:

1. Core Entities (5 tables)

  • users - System users
  • organisations - Organization entities
  • projects - Test projects
  • tests - Test cases
  • runs - Test runs

2. Test Organization (3 tables)

  • sections - Hierarchical test organization
  • squads - Team ownership
  • labels - Flexible tagging

3. Test Metadata (5 tables)

  • type - Test types
  • priority - Priority levels
  • platform - Target platforms
  • automationStatus - Automation state
  • testCoveredBy - Coverage type

4. Relationship Tables (3 tables)

  • labelTestMap - Many-to-many (labels ↔ tests)
  • testRunMap - Many-to-many (tests ↔ runs)
  • testRunsStatusHistory - Audit trail for test results

Entity Relationships

organisations (1) ──→ (N) projects
projects (1) ──→ (N) tests
projects (1) ──→ (N) runs
projects (1) ──→ (N) sections (hierarchical)
projects (1) ──→ (N) squads
projects (1) ──→ (N) labels

tests (N) ←──→ (N) labels (via labelTestMap)
tests (N) ←──→ (N) runs (via testRunMap)
tests (N) ──→ (1) section
tests (N) ──→ (1) squad
tests (N) ──→ (1) priority
tests (N) ──→ (1) platform
tests (N) ──→ (1) automationStatus
tests (N) ──→ (1) testCoveredBy

testRunMap (N) ──→ (N) testRunsStatusHistory

Core Entities

Users

System users with authentication and role-based access.

ColumnTypeConstraintsDescription
userIdintPK, AUTO_INCREMENTUnique identifier
userNamevarchar(100)NOT NULLDisplay name
emailvarchar(100)NOT NULL, UNIQUEEmail address
ssoIdvarchar(200)UNIQUEGoogle OAuth SSO ID
profileUrltextProfile image URL
roleenumNOT NULL, DEFAULT 'user'admin, user, reader
tokenvarchar(500)UNIQUEAPI authentication token
updatedByintFK → users.userIdLast modifier
statusenumDEFAULT 'active'active, archive, delete
note

Users are never hard-deleted. The status field marks inactive accounts.

Organizations

Top-level entities that group projects and users.

ColumnTypeConstraintsDescription
orgIdintPK, AUTO_INCREMENTUnique identifier
orgNamevarchar(20)NOT NULL, UNIQUEOrganization name
createdByintFK → users.userIdCreator
createdOntimestampDEFAULT CURRENT_TIMESTAMPCreation time

Projects

Containers for test cases and test runs.

ColumnTypeConstraintsDescription
projectIdintPK, AUTO_INCREMENTUnique identifier
projectNamevarchar(50)NOT NULLProject name
projectDescriptionvarchar(255)Description
createdByintFK → users.userIdCreator
createdOntimestampDEFAULT CURRENT_TIMESTAMPCreation time
orgIdintNOT NULL, FK → organisations.orgIdOwner organization
testsCountintDEFAULT 0Cached test count
runsCountintDEFAULT 0Cached run count
statusenumDEFAULT 'Active'Active, Archived, Deleted
updatedOntimestampON UPDATE CURRENT_TIMESTAMPLast update
updatedByintFK → users.userIdLast modifier

Indexes:

  • projectsNameIndex on projectName
  • projectsProjectIdIndex on projectId
  • projectOrgUniqueIndex (UNIQUE) on (projectName, orgId)
tip

The unique index on (projectName, orgId) ensures project names are unique within an organization but can repeat across organizations.

Tests

Test cases with detailed specifications and metadata.

ColumnTypeConstraintsDescription
testIdintPK, AUTO_INCREMENTUnique identifier
sectionIdintFK → sections.sectionIdHierarchical section
projectIdintNOT NULL, FK → projects.projectIdOwner project
titlevarchar(750)NOT NULLTest title
squadIdintFK → squads.squadIdResponsible squad
priorityIdintFK → priority.priorityIdPriority level
typeIdintFK → type.typeIdTest type
automationStatusIdintFK → automationStatusAutomation status
platformIdintFK → platform.platformIdTarget platform
testCoveredByIdintFK → testCoveredByCoverage type
preconditionstextSetup requirements
stepstextExecution steps
expectedResulttextExpected outcome
descriptiontextTest description
jiraTickettextAssociated Jira ticket
additionalGroupstextCustom grouping
defectstextRelated defects
automationIdtextAutomation script ID
createdByintFK → users.userIdCreator
createdOntimestampDEFAULT CURRENT_TIMESTAMPCreation time
updatedByintFK → users.userIdLast modifier
updatedOntimestampON UPDATE CURRENT_TIMESTAMPLast update

Indexes:

  • testIdIndex on testId
  • projectIdIndex on projectId

Runs

Test execution runs containing selected tests.

ColumnTypeConstraintsDescription
runIdintPK, AUTO_INCREMENTUnique identifier
runNamevarchar(255)NOT NULLRun name
runDescriptiontextDescription
projectIdintNOT NULL, FK → projects.projectIdOwner project
testsCountintDEFAULT 0Number of tests
passedintDEFAULT 0Passed count
failedintDEFAULT 0Failed count
retestintDEFAULT 0Retest count
blockedintDEFAULT 0Blocked count
untestedintDEFAULT 0Untested count
skippedintDEFAULT 0Skipped count
createdByintFK → users.userIdCreator
createdOntimestampDEFAULT CURRENT_TIMESTAMPCreation time
updatedOntimestampON UPDATE CURRENT_TIMESTAMPLast update
updatedByintFK → users.userIdLast modifier
statusenumDEFAULT 'Active'Active, Locked
lockedByintFK → users.userIdUser who locked
lockedOntimestampLock time

Indexes:

  • runIdIndex on runId
  • projectIdIndex on projectId
note

Status counts (passed, failed, etc.) are denormalized for performance. They're updated when test statuses change.


Test Organization

Sections

Hierarchical organization of tests within projects.

ColumnTypeConstraintsDescription
sectionIdintPK, AUTO_INCREMENTUnique identifier
sectionNamevarchar(255)NOT NULLSection name
sectionDescriptiontextDescription
projectIdintNOT NULL, FK → projects.projectIdOwner project
parentSectionIdintFK → sections.sectionIdParent section (null = root)
createdByintFK → users.userIdCreator
createdOntimestampDEFAULT CURRENT_TIMESTAMPCreation time

Hierarchy: Self-referencing via parentSectionId allows unlimited nesting.

Example:

Login & Auth (parentSectionId = null)
├── Email Login (parentSectionId = 1)
└── Social Login (parentSectionId = 1)

Squads

Teams or groups responsible for tests.

ColumnTypeConstraintsDescription
squadIdintPK, AUTO_INCREMENTUnique identifier
squadNamevarchar(100)NOT NULLSquad name
projectIdintNOT NULL, FK → projects.projectIdOwner project
createdByintFK → users.userIdCreator
createdOntimestampDEFAULT CURRENT_TIMESTAMPCreation time

Labels

Flexible tagging system for tests.

ColumnTypeConstraintsDescription
labelIdintPK, AUTO_INCREMENTUnique identifier
labelNamevarchar(100)NOT NULLLabel name
projectIdintNOT NULL, FK → projects.projectIdOwner project
createdByintFK → users.userIdCreator
createdOntimestampDEFAULT CURRENT_TIMESTAMPCreation time

Test Metadata

These tables store predefined values for test attributes.

Priority

ColumnTypeConstraints
priorityIdintPK, AUTO_INCREMENT
priorityNamevarchar(20)NOT NULL, UNIQUE

Values: Critical, High, Medium, Low

Platform

ColumnTypeConstraints
platformIdintPK, AUTO_INCREMENT
platformNamevarchar(50)NOT NULL, UNIQUE

Values: All Platforms, Android Only, iOS Only, Web Only, Android Web and Playstore, etc.

Automation Status

ColumnTypeConstraints
automationStatusIdintPK, AUTO_INCREMENT
automationStatusNamevarchar(20)NOT NULL, UNIQUE

Values: Not Automatable, Automated, Automatable

Test Covered By

ColumnTypeConstraints
testCoveredByIdintPK, AUTO_INCREMENT
testCoveredByNamevarchar(15)NOT NULL, UNIQUE

Values: Manual, E2E, Integration, Unit, Contract

Type

ColumnTypeConstraints
typeIdintPK, AUTO_INCREMENT
typeNamevarchar(20)NOT NULL, UNIQUE

Values: (Project-specific test types)


Relationship Tables

labelTestMap

Many-to-many relationship between labels and tests.

ColumnTypeConstraints
idintPK, AUTO_INCREMENT
labelIdintNOT NULL, FK → labels.labelId
testIdintNOT NULL, FK → tests.testId

Unique Constraint: (labelId, testId) - prevents duplicate mappings

testRunMap

Many-to-many relationship between tests and runs.

ColumnTypeConstraints
idintPK, AUTO_INCREMENT
testIdintNOT NULL, FK → tests.testId
runIdintNOT NULL, FK → runs.runId
statusenumDEFAULT 'Untested'
updatedByintFK → users.userId
updatedOntimestampON UPDATE CURRENT_TIMESTAMP

Status Values: Passed, Failed, Blocked, Retest, Skipped, Untested

Unique Constraint: (testId, runId) - prevents duplicate test assignments

testRunsStatusHistory

Audit trail for test status changes in runs.

ColumnTypeConstraints
historyIdintPK, AUTO_INCREMENT
testRunMapIdintNOT NULL, FK → testRunMap.id
statusenumNOT NULL
commenttext
updatedByintFK → users.userId
updatedOntimestampDEFAULT CURRENT_TIMESTAMP

Purpose: Track complete history of status changes for audit and reporting.


Design Decisions

Why Denormalized Counts?

Tables like projects and runs store counts (testsCount, passed, failed, etc.) for performance:

Benefits:

  • Fast dashboard queries (no expensive COUNT operations)
  • Reduced database load
  • Better user experience with instant stats

Trade-off:

  • Requires careful update logic
  • Potential for inconsistency (mitigated by transactions)

Why Soft Deletes?

Using status flags instead of hard deletes:

Benefits:

  • Preserve audit trail
  • Enable undo functionality
  • Maintain referential integrity
  • Support historical reporting

Implementation:

  • projects.status: Active, Archived, Deleted
  • users.status: active, archive, delete

Why Separate Metadata Tables?

Priority, platform, automationStatus, etc. are separate tables instead of enums:

Benefits:

  • Centralized management
  • Easy to extend without schema changes
  • Consistent across projects
  • Supports future customization per organization

Performance Optimizations

Strategic Indexes

Projects:

  • projectName - Fast search by name
  • (projectName, orgId) - Enforce uniqueness, fast lookups

Tests:

  • projectId - Filter tests by project
  • testId - Primary key lookups

Runs:

  • projectId - Filter runs by project
  • runId - Primary key lookups

Query Patterns

Optimized Queries:

  • Paginated test lists use LIMIT and OFFSET
  • Filters use indexed columns (projectId, sectionId, squadId)
  • Status counts avoid real-time aggregation

Avoid:

  • Full table scans
  • Unindexed WHERE clauses
  • N+1 query problems (use JOINs)

Schema Migrations

Checkmate uses Drizzle ORM for schema management.

Migration Files: sql/_migration*.sql

Process:

  1. Define schema in TypeScript (app/db/schema/)
  2. Generate migration with Drizzle
  3. Apply migration to database
  4. Seed initial data if needed

Seed Data: app/db/seed/ contains initial values for metadata tables.


Database Access

Connection Configuration

// drizzle.config.ts
export default {
schema: "./app/db/schema/*",
out: "./sql",
driver: "mysql2",
dbCredentials: {
uri: process.env.DATABASE_URL
}
}

ORM Layer

Drizzle ORM provides:

  • Type-safe queries
  • SQL-like syntax
  • Automatic migrations
  • Connection pooling

Example Query:

// Get all tests for a project
const tests = await db
.select()
.from(testsTable)
.where(eq(testsTable.projectId, projectId))
.limit(10)
.offset(0);

Best Practices

For Developers

  • Always use transactions for multi-table updates
  • Update counts when modifying tests/runs
  • Check foreign keys before deleting
  • Use prepared statements (ORM handles this)
  • Index new query columns if adding filters

For Database Admins

  • Regular backups - Daily snapshots recommended
  • Monitor query performance - Check slow query log
  • Index maintenance - Rebuild indexes periodically
  • Connection pooling - Configure max connections appropriately

Additional Resources


Next Steps