/** * Database Validation and Health Checks * * Comprehensive validation of database schema, data integrity, * and readiness for the new tRPC and batch processing architecture. */ import { PrismaClient } from "@prisma/client"; import { migrationLogger } from "./migration-logger"; interface ValidationResult { success: boolean; errors: string[]; warnings: string[]; metrics: Record; } export class DatabaseValidator { private prisma: PrismaClient; constructor() { this.prisma = new PrismaClient(); } /** * Run comprehensive database validation */ async validateDatabase(): Promise { const result: ValidationResult = { success: true, errors: [], warnings: [], metrics: {}, }; try { migrationLogger.startStep( "DATABASE_VALIDATION", "Running comprehensive database validation" ); // Test database connection await this.validateConnection(result); // Validate schema integrity await this.validateSchemaIntegrity(result); // Validate data integrity await this.validateDataIntegrity(result); // Validate indexes and performance await this.validateIndexes(result); // Validate batch processing readiness await this.validateBatchProcessingReadiness(result); // Validate tRPC readiness await this.validateTRPCReadiness(result); // Collect metrics await this.collectMetrics(result); result.success = result.errors.length === 0; if (result.success) { migrationLogger.completeStep("DATABASE_VALIDATION"); } else { migrationLogger.failStep( "DATABASE_VALIDATION", new Error(`Validation failed with ${result.errors.length} errors`) ); } } catch (error) { result.success = false; result.errors.push( `Database validation failed: ${(error as Error).message}` ); migrationLogger.error( "DATABASE_VALIDATION", "Critical validation error", error as Error ); } finally { await this.prisma.$disconnect(); } return result; } private async validateConnection(result: ValidationResult): Promise { try { migrationLogger.info("DB_CONNECTION", "Testing database connection"); await this.prisma.$queryRaw`SELECT 1`; migrationLogger.info("DB_CONNECTION", "Database connection successful"); } catch (error) { result.errors.push( `Database connection failed: ${(error as Error).message}` ); } } private async validateSchemaIntegrity( result: ValidationResult ): Promise { migrationLogger.info("SCHEMA_VALIDATION", "Validating schema integrity"); try { // Check if all required tables exist const requiredTables = [ "Company", "User", "Session", "SessionImport", "Message", "SessionProcessingStatus", "Question", "SessionQuestion", "AIBatchRequest", "AIProcessingRequest", "AIModel", "AIModelPricing", "CompanyAIModel", "PlatformUser", ]; for (const table of requiredTables) { try { await this.prisma.$queryRawUnsafe(`SELECT 1 FROM "${table}" LIMIT 1`); } catch (error) { result.errors.push( `Required table missing or inaccessible: ${table}` ); } } // Check for required enums const requiredEnums = [ "ProcessingStage", "ProcessingStatus", "AIBatchRequestStatus", "AIRequestStatus", "SentimentCategory", "SessionCategory", ]; for (const enumName of requiredEnums) { try { const enumValues = await this.prisma.$queryRawUnsafe( `SELECT unnest(enum_range(NULL::${enumName})) as value` ); if (Array.isArray(enumValues) && enumValues.length === 0) { result.warnings.push(`Enum ${enumName} has no values`); } } catch (error) { result.errors.push(`Required enum missing: ${enumName}`); } } } catch (error) { result.errors.push( `Schema validation failed: ${(error as Error).message}` ); } } private async validateDataIntegrity(result: ValidationResult): Promise { migrationLogger.info("DATA_INTEGRITY", "Validating data integrity"); try { // Check for orphaned records const orphanedSessions = await this.prisma.$queryRaw<{ count: bigint }[]>` SELECT COUNT(*) as count FROM "Session" s LEFT JOIN "Company" c ON s."companyId" = c.id WHERE c.id IS NULL `; if (orphanedSessions[0]?.count > 0) { result.errors.push( `Found ${orphanedSessions[0].count} orphaned sessions` ); } // Check for sessions without processing status const sessionsWithoutStatus = await this.prisma.$queryRaw< { count: bigint }[] >` SELECT COUNT(*) as count FROM "Session" s LEFT JOIN "SessionProcessingStatus" sps ON s.id = sps."sessionId" WHERE sps."sessionId" IS NULL `; if (sessionsWithoutStatus[0]?.count > 0) { result.warnings.push( `Found ${sessionsWithoutStatus[0].count} sessions without processing status` ); } // Check for inconsistent batch processing states const inconsistentBatchStates = await this.prisma.$queryRaw< { count: bigint }[] >` SELECT COUNT(*) as count FROM "AIProcessingRequest" apr WHERE apr."batchId" IS NOT NULL AND apr."processingStatus" = 'PENDING_BATCHING' `; if (inconsistentBatchStates[0]?.count > 0) { result.warnings.push( `Found ${inconsistentBatchStates[0].count} requests with inconsistent batch states` ); } } catch (error) { result.errors.push( `Data integrity validation failed: ${(error as Error).message}` ); } } private async validateIndexes(result: ValidationResult): Promise { migrationLogger.info("INDEX_VALIDATION", "Validating database indexes"); try { // Check for missing critical indexes const criticalIndexes = [ { table: "Session", columns: ["companyId", "startTime"] }, { table: "SessionProcessingStatus", columns: ["stage", "status"] }, { table: "AIProcessingRequest", columns: ["processingStatus"] }, { table: "AIBatchRequest", columns: ["companyId", "status"] }, ]; for (const indexInfo of criticalIndexes) { const indexExists = (await this.prisma.$queryRawUnsafe(` SELECT COUNT(*) as count FROM pg_indexes WHERE tablename = '${indexInfo.table}' AND indexdef LIKE '%${indexInfo.columns.join("%")}%' `)) as { count: string }[]; if (parseInt(indexExists[0]?.count || "0") === 0) { result.warnings.push( `Missing recommended index on ${indexInfo.table}(${indexInfo.columns.join(", ")})` ); } } } catch (error) { result.warnings.push( `Index validation failed: ${(error as Error).message}` ); } } private async validateBatchProcessingReadiness( result: ValidationResult ): Promise { migrationLogger.info( "BATCH_READINESS", "Validating batch processing readiness" ); try { // Check if AIBatchRequest table is properly configured const batchTableCheck = await this.prisma.$queryRaw<{ count: bigint }[]>` SELECT COUNT(*) as count FROM "AIBatchRequest" `; // Check if AIProcessingRequest has batch-related fields const batchFieldsCheck = (await this.prisma.$queryRawUnsafe(` SELECT column_name FROM information_schema.columns WHERE table_name = 'AIProcessingRequest' AND column_name IN ('processingStatus', 'batchId') `)) as { column_name: string }[]; if (batchFieldsCheck.length < 2) { result.errors.push( "AIProcessingRequest table missing batch processing fields" ); } // Check if batch status enum values are correct const batchStatusValues = (await this.prisma.$queryRawUnsafe(` SELECT unnest(enum_range(NULL::AIBatchRequestStatus)) as value `)) as { value: string }[]; const requiredBatchStatuses = [ "PENDING", "UPLOADING", "VALIDATING", "IN_PROGRESS", "FINALIZING", "COMPLETED", "PROCESSED", "FAILED", "CANCELLED", ]; const missingStatuses = requiredBatchStatuses.filter( (status) => !batchStatusValues.some((v) => v.value === status) ); if (missingStatuses.length > 0) { result.errors.push( `Missing batch status values: ${missingStatuses.join(", ")}` ); } } catch (error) { result.errors.push( `Batch processing readiness validation failed: ${(error as Error).message}` ); } } private async validateTRPCReadiness(result: ValidationResult): Promise { migrationLogger.info("TRPC_READINESS", "Validating tRPC readiness"); try { // Check if all required models are accessible const modelTests = [ () => this.prisma.company.findFirst(), () => this.prisma.user.findFirst(), () => this.prisma.session.findFirst(), () => this.prisma.aIProcessingRequest.findFirst(), ]; for (const test of modelTests) { try { await test(); } catch (error) { result.warnings.push( `Prisma model access issue: ${(error as Error).message}` ); } } // Test complex queries that tRPC will use try { await this.prisma.session.findMany({ where: { companyId: "test" }, include: { messages: true, processingStatus: true, }, take: 1, }); } catch (error) { // This is expected to fail with the test companyId, but should not error on structure if (!(error as Error).message.includes("test")) { result.warnings.push( `Complex query structure issue: ${(error as Error).message}` ); } } } catch (error) { result.warnings.push( `tRPC readiness validation failed: ${(error as Error).message}` ); } } private async collectMetrics(result: ValidationResult): Promise { migrationLogger.info("METRICS_COLLECTION", "Collecting database metrics"); try { // Count records in key tables const companiesCount = await this.prisma.company.count(); const usersCount = await this.prisma.user.count(); const sessionsCount = await this.prisma.session.count(); const messagesCount = await this.prisma.message.count(); const batchRequestsCount = await this.prisma.aIBatchRequest.count(); const processingRequestsCount = await this.prisma.aIProcessingRequest.count(); result.metrics = { companies: companiesCount, users: usersCount, sessions: sessionsCount, messages: messagesCount, batchRequests: batchRequestsCount, processingRequests: processingRequestsCount, }; // Check processing status distribution const processingStatusCounts = await this.prisma.sessionProcessingStatus.groupBy({ by: ["status"], _count: { status: true }, }); for (const statusCount of processingStatusCounts) { result.metrics[`processing_${statusCount.status.toLowerCase()}`] = statusCount._count.status; } // Check batch request status distribution const batchStatusCounts = await this.prisma.aIBatchRequest.groupBy({ by: ["status"], _count: { status: true }, }); for (const statusCount of batchStatusCounts) { result.metrics[`batch_${statusCount.status.toLowerCase()}`] = statusCount._count.status; } } catch (error) { result.warnings.push( `Metrics collection failed: ${(error as Error).message}` ); } } } // CLI interface if (import.meta.url === `file://${process.argv[1]}`) { const validator = new DatabaseValidator(); validator .validateDatabase() .then((result) => { console.log("\n=== DATABASE VALIDATION RESULTS ==="); console.log(`Success: ${result.success ? "✅" : "❌"}`); if (result.errors.length > 0) { console.log("\n❌ ERRORS:"); result.errors.forEach((error) => console.log(` - ${error}`)); } if (result.warnings.length > 0) { console.log("\n⚠️ WARNINGS:"); result.warnings.forEach((warning) => console.log(` - ${warning}`)); } console.log("\n📊 METRICS:"); Object.entries(result.metrics).forEach(([key, value]) => { console.log(` ${key}: ${value}`); }); process.exit(result.success ? 0 : 1); }) .catch((error) => { console.error("Validation failed:", error); process.exit(1); }); }