Appearance
Data Layer
Overview
The Oktuple data layer is built on PostgreSQL with Prisma ORM, providing a robust foundation for project management data. The system uses logical replication for real-time synchronization with search engines and implements comprehensive data validation and integrity constraints.
Database Architecture
Primary Database (PostgreSQL)
Purpose: Main data persistence for all project management operations
Key Features:
- ACID compliance for data integrity
- JSON support for flexible metadata
- Full-text search capabilities
- Logical replication for data sync
- Connection pooling for performance
Database Schema Overview
Core Tables
ENode (Project Structure)
sql
CREATE TABLE enode (
id VARCHAR PRIMARY KEY,
owner_id VARCHAR,
title VARCHAR NOT NULL,
description TEXT,
domain VARCHAR,
prefix VARCHAR,
type VARCHAR NOT NULL,
parent_id VARCHAR REFERENCES enode(id),
template_id VARCHAR REFERENCES template(id),
template_it_id VARCHAR,
template_ip_id VARCHAR,
list_type VARCHAR,
materialized_path VARCHAR,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
start_at TIMESTAMP,
end_at TIMESTAMP,
complete_at TIMESTAMP,
deleted_at TIMESTAMP,
meta JSONB,
archived BOOLEAN DEFAULT FALSE,
index INTEGER DEFAULT 0,
position FLOAT,
old_id VARCHAR,
has_sprint BOOLEAN DEFAULT FALSE,
has_limited BOOLEAN DEFAULT TRUE,
inherit_id VARCHAR,
count_project INTEGER DEFAULT 0,
pids VARCHAR[]
);Issue (Work Items)
sql
CREATE TABLE issue (
id VARCHAR PRIMARY KEY,
owner_id VARCHAR NOT NULL,
title VARCHAR NOT NULL,
hierarchy_path VARCHAR,
enode_id VARCHAR REFERENCES enode(id),
parent_id VARCHAR REFERENCES issue(id),
effort INTEGER,
type VARCHAR NOT NULL,
tags JSONB DEFAULT '[]',
viewer VARCHAR[],
people JSONB,
meta JSONB,
cfs JSONB,
position FLOAT,
addons JSONB,
stats JSONB,
template_id VARCHAR REFERENCES template(id),
old_id VARCHAR,
abbreviation VARCHAR,
cover VARCHAR,
pined BOOLEAN DEFAULT FALSE,
is_closed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP,
completed_at TIMESTAMP,
due_at TIMESTAMP,
closed_at TIMESTAMP,
start_at TIMESTAMP
);People (Team Members)
sql
CREATE TABLE people (
id VARCHAR PRIMARY KEY,
name VARCHAR,
pl INTEGER,
parent_id VARCHAR,
mpath VARCHAR,
type VARCHAR NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
enode_id VARCHAR NOT NULL REFERENCES enode(id),
config JSONB,
old_id VARCHAR
);Template (Workflow Templates)
sql
CREATE TABLE template (
id VARCHAR PRIMARY KEY,
owner_id VARCHAR NOT NULL,
title VARCHAR,
origin_id VARCHAR,
framework VARCHAR,
rules JSONB,
enode_id VARCHAR REFERENCES enode(id),
is_default VARCHAR,
template_type VARCHAR,
old_id VARCHAR,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP
);Addon (Extended Functionality)
sql
CREATE TABLE addon (
id VARCHAR,
type VARCHAR,
owner_id VARCHAR,
title VARCHAR NOT NULL,
parent_id INTEGER,
metadata JSONB,
mpath VARCHAR,
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
enode_id VARCHAR NOT NULL REFERENCES enode(id),
is_abstract BOOLEAN DEFAULT FALSE,
old_id VARCHAR,
deleted_at TIMESTAMP,
UNIQUE(enode_id, id)
);Supporting Tables
Attachment (File Management)
sql
CREATE TABLE attachment (
id VARCHAR PRIMARY KEY,
issue_id VARCHAR NOT NULL REFERENCES issue(id),
key VARCHAR NOT NULL,
owner_id VARCHAR,
value JSONB,
old_id VARCHAR,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP
);Links (Dependencies)
sql
CREATE TABLE links (
id VARCHAR PRIMARY KEY,
type VARCHAR NOT NULL,
from VARCHAR NOT NULL REFERENCES issue(id),
to VARCHAR NOT NULL REFERENCES issue(id),
resolve BOOLEAN NOT NULL,
owner_id VARCHAR NOT NULL,
old_id VARCHAR,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);Role (Permission Management)
sql
CREATE TABLE role (
id VARCHAR PRIMARY KEY,
name VARCHAR NOT NULL,
enode_id VARCHAR NOT NULL REFERENCES enode(id),
hierarchy_path VARCHAR,
is_default BOOLEAN DEFAULT FALSE,
parent_role_id VARCHAR REFERENCES role(id)
);History (Audit Trail)
sql
CREATE TABLE history (
id VARCHAR PRIMARY KEY,
issue_id VARCHAR NOT NULL REFERENCES issue(id),
owner_id VARCHAR NOT NULL,
patch JSONB,
type VARCHAR NOT NULL,
type_event VARCHAR,
created_at TIMESTAMP DEFAULT NOW()
);Data Relationships
Hierarchical Structure
ENode Hierarchy
Root Project
├── Collection 1
│ │ ├── Issue 1
│ │ └── Issue 2
└── Collection 2
└── Issue 4
└── Sub Issue 5Data Validation
Prisma Schema Validation
typescript
// Example Prisma model with validation
model enode {
id String @id
owner_id String?
title String
description String?
domain String?
prefix String?
type String
parent_id String?
template_id String?
meta Json?
archived Boolean @default(false)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
// Relationships
template template? @relation(fields: [template_id], references: [id])
addons addon[] @relation("EnodeToAddon")
issues issue[]
peoples people[]
@@index([parent_id])
@@index([template_id])
@@index([type])
}Zod Schema Validation
typescript
// Request validation schemas
const createEnodeSchema = z.object({
title: z.string().min(1, "Title is required").max(255),
description: z.string().optional(),
type: z.enum(["project", "sprint", "epic", "story", "task"]),
parent_id: z.string().uuid().optional(),
template_id: z.string().uuid().optional(),
domain: z.string().optional(),
prefix: z.string().max(10).optional(),
meta: z.record(z.any()).optional(),
});
const updateEnodeSchema = createEnodeSchema.partial();Data Synchronization
Logical Replication Setup
PostgreSQL Configuration
sql
-- Enable logical replication
ALTER SYSTEM SET wal_level = logical;
ALTER SYSTEM SET max_replication_slots = 10;
ALTER SYSTEM SET max_wal_senders = 10;
-- Create publication
CREATE PUBLICATION oktuple_pub FOR ALL TABLES;
-- Create replication slot
SELECT pg_create_logical_replication_slot('oktuple_syncer_slot', 'pgoutput');Replication Slot Management
sql
-- Monitor replication slots
SELECT slot_name, active, restart_lsn, confirmed_flush_lsn
FROM pg_replication_slots;
-- Drop replication slot if needed
SELECT pg_drop_replication_slot('oktuple_syncer_slot');Sync Process Flow
1. Database Change (INSERT/UPDATE/DELETE)
↓
2. WAL (Write-Ahead Log) Generation
↓
3. Logical Replication Capture
↓
4. Syncer Service Processing
↓
5. Data Transformation
↓
6. Multi-Engine Sync
├── Elasticsearch Indexing
├── Typesense Indexing
└── TimescaleDB Indexing
↓
7. Fallback Queue (Redis)Performance Optimization
Database Indexing
Primary Indexes
sql
-- Primary key indexes (automatic)
CREATE INDEX idx_enode_id ON enode(id);
CREATE INDEX idx_issue_id ON issue(id);
CREATE INDEX idx_people_id ON people(id);Performance Indexes
sql
-- Hierarchical queries
CREATE INDEX idx_enode_parent_id ON enode(parent_id);
CREATE INDEX idx_issue_parent_id ON issue(parent_id);
CREATE INDEX idx_issue_enode_id ON issue(enode_id);
-- Search and filtering
CREATE INDEX idx_enode_type ON enode(type);
CREATE INDEX idx_issue_type ON issue(type);
CREATE INDEX idx_issue_status ON issue(is_closed);
-- Temporal queries
CREATE INDEX idx_enode_created_at ON enode(created_at);
CREATE INDEX idx_issue_created_at ON issue(created_at);
CREATE INDEX idx_issue_due_at ON issue(due_at);
-- JSON field indexing
CREATE INDEX idx_enode_meta ON enode USING GIN (meta);
CREATE INDEX idx_issue_meta ON issue USING GIN (meta);
CREATE INDEX idx_issue_tags ON issue USING GIN (tags);Composite Indexes
sql
-- Multi-field queries
CREATE INDEX idx_enode_type_parent ON enode(type, parent_id);
CREATE INDEX idx_issue_enode_type ON issue(enode_id, type);
CREATE INDEX idx_people_enode_type ON people(enode_id, type);Query Optimization
Efficient Hierarchical Queries
sql
-- Using materialized paths
SELECT * FROM enode
WHERE materialized_path LIKE '1.2.%'
ORDER BY materialized_path;
-- Using recursive CTEs
WITH RECURSIVE enode_tree AS (
SELECT id, title, parent_id, 1 as level
FROM enode WHERE id = 'root_id'
UNION ALL
SELECT e.id, e.title, e.parent_id, et.level + 1
FROM enode e
JOIN enode_tree et ON e.parent_id = et.id
)
SELECT * FROM enode_tree ORDER BY level, title;JSON Field Queries
sql
-- Query JSON metadata
SELECT * FROM enode
WHERE meta->>'priority' = 'high';
-- Array field queries
SELECT * FROM issue
WHERE 'bug' = ANY(tags);
-- Complex JSON queries
SELECT * FROM enode
WHERE meta->>'status' = 'active'
AND meta->>'priority' IN ('high', 'critical');Data Integrity
Constraints and Validation
Database Constraints
sql
-- Not null constraints
ALTER TABLE enode ALTER COLUMN title SET NOT NULL;
ALTER TABLE enode ALTER COLUMN type SET NOT NULL;
ALTER TABLE issue ALTER COLUMN title SET NOT NULL;
ALTER TABLE issue ALTER COLUMN owner_id SET NOT NULL;
-- Check constraints
ALTER TABLE issue ADD CONSTRAINT chk_effort_positive
CHECK (effort IS NULL OR effort > 0);
ALTER TABLE enode ADD CONSTRAINT chk_dates_valid
CHECK (start_at IS NULL OR end_at IS NULL OR start_at <= end_at);
-- Unique constraints
ALTER TABLE people ADD CONSTRAINT uk_people_enode_user
UNIQUE (enode_id, id);Application-Level Validation
typescript
// Business rule validation
export class EnodeService {
async validateEnodeHierarchy(
parentId: string,
type: string
): Promise<boolean> {
if (!parentId) return true;
const parent = await this.prisma.enode.findUnique({
where: { id: parentId },
});
// Validate hierarchy rules
if (type === "sprint" && parent.type !== "project") {
throw new Error("Sprints can only be created under projects");
}
if (type === "epic" && !["project", "sprint"].includes(parent.type)) {
throw new Error("Epics can only be created under projects or sprints");
}
return true;
}
}Transaction Management
ACID Operations
typescript
// Example transaction
export class IssueService {
async createIssueWithDependencies(data: CreateIssueData): Promise<Issue> {
return await this.prisma.$transaction(async (tx) => {
// Create issue
const issue = await tx.issue.create({
data: {
title: data.title,
type: data.type,
enode_id: data.enode_id,
owner_id: data.owner_id,
},
});
// Create dependencies
if (data.dependencies?.length) {
await tx.links.createMany({
data: data.dependencies.map((dep) => ({
type: dep.type,
from: issue.id,
to: dep.issue_id,
resolve: false,
owner_id: data.owner_id,
})),
});
}
// Update parent issue stats
if (data.parent_id) {
await tx.issue.update({
where: { id: data.parent_id },
data: {
stats: {
increment: { children_count: 1 },
},
},
});
}
return issue;
});
}
}Data Migration
Schema Evolution
Migration Strategy
typescript
// Prisma migration example
export async function migrateEnodeStructure() {
await prisma.$executeRaw`
-- Add new columns
ALTER TABLE enode ADD COLUMN IF NOT EXISTS has_sprint BOOLEAN DEFAULT FALSE;
ALTER TABLE enode ADD COLUMN IF NOT EXISTS has_limited BOOLEAN DEFAULT TRUE;
-- Update existing data
UPDATE enode SET has_sprint = TRUE WHERE type IN ('project', 'sprint');
UPDATE enode SET has_limited = FALSE WHERE type = 'project';
-- Add indexes
CREATE INDEX IF NOT EXISTS idx_enode_has_sprint ON enode(has_sprint);
CREATE INDEX IF NOT EXISTS idx_enode_has_limited ON enode(has_limited);
`;
}Data Transformation
typescript
// Data cleanup and transformation
export async function cleanupOrphanedData() {
// Remove orphaned issues
await prisma.issue.deleteMany({
where: {
enode_id: null,
deleted_at: { not: null },
},
});
// Remove orphaned attachments
await prisma.attachment.deleteMany({
where: {
issue_id: null,
},
});
// Update materialized paths
await updateMaterializedPaths();
}Monitoring and Maintenance
Database Health Checks
Connection Monitoring
typescript
export class DatabaseHealthService {
async checkHealth(): Promise<HealthStatus> {
try {
// Test connection
await this.prisma.$queryRaw`SELECT 1`;
// Check replication slots
const slots = await this.prisma.$queryRaw`
SELECT slot_name, active, restart_lsn
FROM pg_replication_slots
`;
// Check table sizes
const sizes = await this.prisma.$queryRaw`
SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
`;
return {
status: "healthy",
database: "connected",
replication: slots.length > 0 ? "active" : "inactive",
tables: sizes.length,
};
} catch (error) {
return {
status: "unhealthy",
error: error.message,
};
}
}
}Performance Monitoring
sql
-- Slow query analysis
SELECT query, calls, total_time, mean_time, rows
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
-- Table access statistics
SELECT schemaname, tablename, seq_scan, seq_tup_read, idx_scan, idx_tup_fetch
FROM pg_stat_user_tables
ORDER BY seq_scan DESC;
-- Index usage statistics
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;Backup and Recovery
Backup Strategy
bash
#!/bin/bash
# Automated backup script
# Database backup
pg_dump -h localhost -U postgres -d oktuple > backup_$(date +%Y%m%d_%H%M%S).sql
# Schema backup
pg_dump -h localhost -U postgres -d oktuple --schema-only > schema_$(date +%Y%m%d_%H%M%S).sql
# Data backup (excluding large tables)
pg_dump -h localhost -U postgres -d oktuple \
--exclude-table=attachment \
--exclude-table=history \
> data_$(date +%Y%m%d_%H%M%S).sqlRecovery Procedures
sql
-- Point-in-time recovery
RESTORE DATABASE oktuple FROM 'backup_file'
WITH RECOVERY, STOPAT = '2025-01-01 12:00:00';
-- Restore specific tables
pg_restore -h localhost -U postgres -d oktuple -t enode backup_file.dump
pg_restore -h localhost -U postgres -d oktuple -t issue backup_file.dumpFuture Considerations
Planned Enhancements
Database Features
- TimescaleDB: Time-series data for metrics and logs
- Partitioning: Table partitioning for large datasets
- Sharding: Horizontal scaling across multiple databases
- Read Replicas: Load distribution for read operations
Performance Improvements
- Connection Pooling: Advanced connection management
- Query Caching: Result set caching
- Materialized Views: Pre-computed aggregations
- Parallel Queries: Concurrent query execution
Data Management
- Data Archiving: Automatic data lifecycle management
- Compression: Data compression for storage optimization
- Encryption: Field-level encryption for sensitive data
- Audit Logging: Enhanced change tracking