-- Create polymorphic associations for commentsCREATETABLEcommentable_types(idSERIALPRIMARYKEY,nameVARCHAR(50)UNIQUENOTNULL);INSERTINTOcommentable_types(name)VALUES('post'),('project'),('user');CREATETABLEcomments(idSERIALPRIMARYKEY,contentTEXTNOTNULL,commentable_typeVARCHAR(50)NOTNULL,commentable_idINTEGERNOTNULL,author_idINTEGERNOTNULLREFERENCESusers(id)ONDELETECASCADE,parent_idINTEGERREFERENCEScomments(id)ONDELETECASCADE,is_deletedBOOLEANDEFAULTFALSE,created_atTIMESTAMPTZDEFAULTCURRENT_TIMESTAMP,updated_atTIMESTAMPTZDEFAULTCURRENT_TIMESTAMP,UNIQUE(commentable_type,commentable_id,author_id,created_at));-- Create function to validate polymorphic associationsCREATEORREPLACEFUNCTIONvalidate_commentable_reference()RETURNSTRIGGERAS$$BEGIN-- Validate that the referenced record existsIFNEW.commentable_type='post'THENIFNOTEXISTS(SELECT1FROMpostsWHEREid=NEW.commentable_id)THENRAISEEXCEPTION'Referenced post does not exist';ENDIF;ELSIFNEW.commentable_type='project'THENIFNOTEXISTS(SELECT1FROMprojectsWHEREid=NEW.commentable_id)THENRAISEEXCEPTION'Referenced project does not exist';ENDIF;ELSIFNEW.commentable_type='user'THENIFNOTEXISTS(SELECT1FROMusersWHEREid=NEW.commentable_id)THENRAISEEXCEPTION'Referenced user does not exist';ENDIF;ELSERAISEEXCEPTION'Invalid commentable_type: %',NEW.commentable_type;ENDIF;RETURNNEW;END;$$LANGUAGEplpgsql;CREATETRIGGERvalidate_commentable_reference_triggerBEFOREINSERTORUPDATEONcommentsFOREACHROWEXECUTEFUNCTIONvalidate_commentable_reference();
-- Create denormalized views for performanceCREATEMATERIALIZEDVIEWuser_project_summaryASSELECTu.idasuser_id,u.username,u.email,COUNT(p.id)asproject_count,COUNT(pm.id)asmember_count,MAX(p.created_at)aslatest_project_created,ARRAY_AGG(DISTINCTo.name)asorganization_namesFROMusersuLEFTJOINprojectspONu.id=p.owner_idLEFTJOINproject_memberspmONu.id=pm.user_idLEFTJOINorganizationsoONp.organization_id=o.idGROUPBYu.id,u.username,u.email;-- Create index on materialized viewCREATEINDEXidx_user_project_summary_usernameONuser_project_summary(username);-- Create function to refresh materialized viewCREATEORREPLACEFUNCTIONrefresh_user_project_summary()RETURNSVOIDAS$$BEGINREFRESHMATERIALIZEDVIEWCONCURRENTLYuser_project_summary;END;$$LANGUAGEplpgsql;-- Create trigger to auto-refresh materialized viewCREATEORREPLACEFUNCTIONtrigger_refresh_user_project_summary()RETURNSTRIGGERAS$$BEGINPERFORMrefresh_user_project_summary();RETURNNULL;END;$$LANGUAGEplpgsql;CREATETRIGGERrefresh_user_project_summary_triggerAFTERINSERTORUPDATEORDELETEONprojectsFOREACHSTATEMENTEXECUTEFUNCTIONtrigger_refresh_user_project_summary();
-- Create partitioned table for eventsCREATETABLEevents(idBIGSERIAL,event_typeVARCHAR(50)NOTNULL,user_idINTEGERNOTNULL,dataJSONB,created_atTIMESTAMPTZNOTNULL,PRIMARYKEY(id,created_at))PARTITIONBYRANGE(created_at);-- Create monthly partitionsCREATETABLEevents_2024_01PARTITIONOFeventsFORVALUESFROM('2024-01-01')TO('2024-02-01');CREATETABLEevents_2024_02PARTITIONOFeventsFORVALUESFROM('2024-02-01')TO('2024-03-01');-- Create indexes on partitionsCREATEINDEXidx_events_2024_01_user_idONevents_2024_01(user_id);CREATEINDEXidx_events_2024_01_event_typeONevents_2024_01(event_type);CREATEINDEXidx_events_2024_01_created_atONevents_2024_01(created_at);-- Create function to auto-create partitionsCREATEORREPLACEFUNCTIONcreate_monthly_partition(table_nameTEXT,start_dateDATE)RETURNSVOIDAS$$DECLAREpartition_nameTEXT;end_dateDATE;BEGINpartition_name:=table_name||'_'||to_char(start_date,'YYYY_MM');end_date:=start_date+interval'1 month';EXECUTEformat('CREATE TABLE %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',partition_name,table_name,start_date,end_date);-- Create indexes on new partitionEXECUTEformat('CREATE INDEX %I ON %I (user_id)','idx_'||partition_name||'_user_id',partition_name);EXECUTEformat('CREATE INDEX %I ON %I (event_type)','idx_'||partition_name||'_event_type',partition_name);EXECUTEformat('CREATE INDEX %I ON %I (created_at)','idx_'||partition_name||'_created_at',partition_name);END;$$LANGUAGEplpgsql;
-- Create comprehensive constraintsALTERTABLEusersADDCONSTRAINTusers_email_formatCHECK(email~*'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');ALTERTABLEusersADDCONSTRAINTusers_username_lengthCHECK(LENGTH(username)>=3ANDLENGTH(username)<=50);ALTERTABLEprojectsADDCONSTRAINTprojects_status_validCHECK(statusIN('active','inactive','archived','deleted'));ALTERTABLEproject_membersADDCONSTRAINTproject_members_role_validCHECK(roleIN('owner','admin','contributor','viewer'));-- Create custom constraint for business rulesCREATEORREPLACEFUNCTIONvalidate_project_owner()RETURNSTRIGGERAS$$BEGIN-- Ensure project owner is a member of the organizationIFNOTEXISTS(SELECT1FROMuser_organizationsuoWHEREuo.user_id=NEW.owner_idANDuo.organization_id=NEW.organization_idANDuo.is_active=TRUE)THENRAISEEXCEPTION'Project owner must be a member of the organization';ENDIF;RETURNNEW;END;$$LANGUAGEplpgsql;CREATETRIGGERvalidate_project_owner_triggerBEFOREINSERTORUPDATEONprojectsFOREACHROWEXECUTEFUNCTIONvalidate_project_owner();
-- Create soft delete patternCREATETABLEsoft_deletable(idSERIALPRIMARYKEY,nameVARCHAR(100)NOTNULL,deleted_atTIMESTAMPTZ,deleted_byINTEGERREFERENCESusers(id),is_deletedBOOLEANGENERATEDALWAYSAS(deleted_atISNOTNULL)STORED);-- Create function for soft deleteCREATEORREPLACEFUNCTIONsoft_delete_record(table_nameTEXT,record_idINTEGER,deleted_by_user_idINTEGER)RETURNSBOOLEANAS$$BEGINEXECUTEformat('UPDATE %I SET deleted_at = CURRENT_TIMESTAMP, deleted_by = %s WHERE id = %s',table_name,deleted_by_user_id,record_id);RETURNFOUND;END;$$LANGUAGEplpgsql;-- Create view to exclude soft-deleted recordsCREATEVIEWactive_projectsASSELECT*FROMprojectsWHEREdeleted_atISNULL;-- Create function to restore soft-deleted recordsCREATEORREPLACEFUNCTIONrestore_record(table_nameTEXT,record_idINTEGER)RETURNSBOOLEANAS$$BEGINEXECUTEformat('UPDATE %I SET deleted_at = NULL, deleted_by = NULL WHERE id = %s',table_name,record_id);RETURNFOUND;END;$$LANGUAGEplpgsql;
-- Create schema versioning tableCREATETABLEschema_migrations(versionVARCHAR(50)PRIMARYKEY,applied_atTIMESTAMPTZDEFAULTCURRENT_TIMESTAMP,descriptionTEXT);-- Create function to track schema changesCREATEORREPLACEFUNCTIONtrack_schema_change(change_descriptionTEXT)RETURNSVOIDAS$$DECLAREcurrent_versionVARCHAR(50);BEGINcurrent_version:=to_char(CURRENT_TIMESTAMP,'YYYYMMDD_HH24MISS');INSERTINTOschema_migrations(version,description)VALUES(current_version,change_description);RAISENOTICE'Schema change tracked: % - %',current_version,change_description;END;$$LANGUAGEplpgsql;-- Example schema migrationDO$$BEGIN-- Add new columnALTERTABLEusersADDCOLUMNphoneVARCHAR(20);-- Track the changePERFORMtrack_schema_change('Add phone column to users table');END$$;
-- Create function to handle schema evolution gracefullyCREATEORREPLACEFUNCTIONmigrate_user_data()RETURNSVOIDAS$$BEGIN-- Add new columns with defaultsIFNOTEXISTS(SELECT1FROMinformation_schema.columnsWHEREtable_name='users'ANDcolumn_name='phone')THENALTERTABLEusersADDCOLUMNphoneVARCHAR(20);ENDIF;IFNOTEXISTS(SELECT1FROMinformation_schema.columnsWHEREtable_name='users'ANDcolumn_name='preferences')THENALTERTABLEusersADDCOLUMNpreferencesJSONBDEFAULT'{}';ENDIF;-- Migrate existing dataUPDATEusersSETpreferences='{"theme": "light", "notifications": true}'::jsonbWHEREpreferencesISNULL;RAISENOTICE'User data migration completed';END;$$LANGUAGEplpgsql;
# validation/schema_validator.pyimportpsycopg2importjsonfromtypingimportDict,List,AnyclassDatabaseSchemaValidator:def__init__(self,connection_params):self.conn_params=connection_paramsdefvalidate_schema_design(self)->Dict[str,Any]:"""Validate database schema design."""conn=psycopg2.connect(**self.conn_params)try:withconn.cursor()ascur:# Check for missing indexesmissing_indexes=self.check_missing_indexes(cur)# Check for foreign key constraintsfk_constraints=self.check_foreign_keys(cur)# Check for data typesdata_types=self.check_data_types(cur)# Check for naming conventionsnaming_issues=self.check_naming_conventions(cur)return{'missing_indexes':missing_indexes,'foreign_keys':fk_constraints,'data_types':data_types,'naming_issues':naming_issues,'validation_timestamp':'2024-01-15T10:30:00Z'}exceptExceptionase:print(f"Error validating schema: {e}")return{}finally:conn.close()defcheck_missing_indexes(self,cursor)->List[Dict[str,Any]]:"""Check for missing indexes on foreign keys."""cursor.execute(""" SELECT tc.table_name, kcu.column_name, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name WHERE tc.constraint_type = 'FOREIGN KEY' AND NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE tablename = tc.table_name AND indexdef LIKE '%' || kcu.column_name || '%' ) """)missing_indexes=[]forrowincursor.fetchall():missing_indexes.append({'table':row[0],'column':row[1],'foreign_table':row[2],'foreign_column':row[3]})returnmissing_indexesdefcheck_foreign_keys(self,cursor)->List[Dict[str,Any]]:"""Check foreign key constraints."""cursor.execute(""" SELECT tc.table_name, tc.constraint_name, kcu.column_name, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name, rc.delete_rule, rc.update_rule FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name JOIN information_schema.referential_constraints AS rc ON tc.constraint_name = rc.constraint_name WHERE tc.constraint_type = 'FOREIGN KEY' """)fk_constraints=[]forrowincursor.fetchall():fk_constraints.append({'table':row[0],'constraint':row[1],'column':row[2],'foreign_table':row[3],'foreign_column':row[4],'delete_rule':row[5],'update_rule':row[6]})returnfk_constraintsdefcheck_data_types(self,cursor)->List[Dict[str,Any]]:"""Check data type usage."""cursor.execute(""" SELECT table_name, column_name, data_type, character_maximum_length, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, ordinal_position """)data_types=[]forrowincursor.fetchall():data_types.append({'table':row[0],'column':row[1],'type':row[2],'max_length':row[3],'nullable':row[4],'default':row[5]})returndata_typesdefcheck_naming_conventions(self,cursor)->List[Dict[str,Any]]:"""Check naming convention violations."""cursor.execute(""" SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_schema = 'public' AND ( column_name LIKE '% %' OR column_name LIKE '%-%' OR column_name ~ '[A-Z]' ) """)naming_issues=[]forrowincursor.fetchall():naming_issues.append({'table':row[0],'column':row[1],'type':row[2],'issue':'Naming convention violation'})returnnaming_issues# Usageif__name__=="__main__":validator=DatabaseSchemaValidator({'host':'localhost','database':'production','user':'validator_user','password':'validator_password'})validation_results=validator.validate_schema_design()print(json.dumps(validation_results,indent=2))