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​