package inspector import ( "regexp" "strings" "git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/pgsql" ) // validatePrimaryKeyNaming checks that primary key column names match a pattern func validatePrimaryKeyNaming(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} pattern, err := regexp.Compile(rule.Pattern) if err != nil { return results } for _, schema := range db.Schemas { for _, table := range schema.Tables { for _, col := range table.Columns { if col.IsPrimaryKey { location := formatLocation(schema.Name, table.Name, col.Name) passed := pattern.MatchString(col.Name) results = append(results, createResult( ruleName, passed, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "column": col.Name, "expected_pattern": rule.Pattern, }, )) } } } } return results } // validatePrimaryKeyDatatype checks that primary keys use approved data types func validatePrimaryKeyDatatype(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} for _, schema := range db.Schemas { for _, table := range schema.Tables { for _, col := range table.Columns { if col.IsPrimaryKey { location := formatLocation(schema.Name, table.Name, col.Name) // Normalize type (remove size/precision) normalizedType := normalizeDataType(col.Type) passed := contains(rule.AllowedTypes, normalizedType) results = append(results, createResult( ruleName, passed, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "column": col.Name, "current_type": col.Type, "allowed_types": rule.AllowedTypes, }, )) } } } } return results } // validatePrimaryKeyAutoIncrement checks primary key auto-increment settings func validatePrimaryKeyAutoIncrement(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} for _, schema := range db.Schemas { for _, table := range schema.Tables { for _, col := range table.Columns { if col.IsPrimaryKey { location := formatLocation(schema.Name, table.Name, col.Name) // Check if auto-increment matches requirement passed := col.AutoIncrement == rule.RequireAutoIncrement if !passed { results = append(results, createResult( ruleName, false, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "column": col.Name, "has_auto_increment": col.AutoIncrement, "require_auto_increment": rule.RequireAutoIncrement, }, )) } } } } } return results } // validateForeignKeyColumnNaming checks that foreign key column names match a pattern func validateForeignKeyColumnNaming(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} pattern, err := regexp.Compile(rule.Pattern) if err != nil { return results } for _, schema := range db.Schemas { for _, table := range schema.Tables { // Check foreign key constraints for _, constraint := range table.Constraints { if constraint.Type == models.ForeignKeyConstraint { for _, colName := range constraint.Columns { location := formatLocation(schema.Name, table.Name, colName) passed := pattern.MatchString(colName) results = append(results, createResult( ruleName, passed, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "column": colName, "constraint": constraint.Name, "expected_pattern": rule.Pattern, }, )) } } } } } return results } // validateForeignKeyConstraintNaming checks that foreign key constraint names match a pattern func validateForeignKeyConstraintNaming(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} pattern, err := regexp.Compile(rule.Pattern) if err != nil { return results } for _, schema := range db.Schemas { for _, table := range schema.Tables { for _, constraint := range table.Constraints { if constraint.Type == models.ForeignKeyConstraint { location := formatLocation(schema.Name, table.Name, "") passed := pattern.MatchString(constraint.Name) results = append(results, createResult( ruleName, passed, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "constraint": constraint.Name, "expected_pattern": rule.Pattern, }, )) } } } } return results } // validateForeignKeyIndex checks that foreign key columns have indexes func validateForeignKeyIndex(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} if !rule.RequireIndex { return results } for _, schema := range db.Schemas { for _, table := range schema.Tables { // Get all foreign key columns fkColumns := make(map[string]bool) for _, constraint := range table.Constraints { if constraint.Type == models.ForeignKeyConstraint { for _, col := range constraint.Columns { fkColumns[col] = true } } } // Check if each FK column has an index for fkCol := range fkColumns { hasIndex := false // Check table indexes for _, index := range table.Indexes { // Index is good if FK column is the first column if len(index.Columns) > 0 && index.Columns[0] == fkCol { hasIndex = true break } } location := formatLocation(schema.Name, table.Name, fkCol) results = append(results, createResult( ruleName, hasIndex, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "column": fkCol, "has_index": hasIndex, }, )) } } } return results } // validateTableNamingCase checks table name casing func validateTableNamingCase(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} pattern, err := regexp.Compile(rule.Pattern) if err != nil { return results } for _, schema := range db.Schemas { for _, table := range schema.Tables { location := formatLocation(schema.Name, table.Name, "") passed := pattern.MatchString(table.Name) results = append(results, createResult( ruleName, passed, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "expected_case": rule.Case, "expected_pattern": rule.Pattern, }, )) } } return results } // validateColumnNamingCase checks column name casing func validateColumnNamingCase(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} pattern, err := regexp.Compile(rule.Pattern) if err != nil { return results } for _, schema := range db.Schemas { for _, table := range schema.Tables { for _, col := range table.Columns { location := formatLocation(schema.Name, table.Name, col.Name) passed := pattern.MatchString(col.Name) results = append(results, createResult( ruleName, passed, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "column": col.Name, "expected_case": rule.Case, "expected_pattern": rule.Pattern, }, )) } } } return results } // validateTableNameLength checks table name length func validateTableNameLength(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} for _, schema := range db.Schemas { for _, table := range schema.Tables { location := formatLocation(schema.Name, table.Name, "") passed := len(table.Name) <= rule.MaxLength results = append(results, createResult( ruleName, passed, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "length": len(table.Name), "max_length": rule.MaxLength, }, )) } } return results } // validateColumnNameLength checks column name length func validateColumnNameLength(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} for _, schema := range db.Schemas { for _, table := range schema.Tables { for _, col := range table.Columns { location := formatLocation(schema.Name, table.Name, col.Name) passed := len(col.Name) <= rule.MaxLength results = append(results, createResult( ruleName, passed, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "column": col.Name, "length": len(col.Name), "max_length": rule.MaxLength, }, )) } } } return results } // validateReservedKeywords checks for reserved SQL keywords func validateReservedKeywords(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} // Build keyword map from PostgreSQL keywords keywordSlice := pgsql.GetPostgresKeywords() keywords := make(map[string]bool) for _, kw := range keywordSlice { keywords[strings.ToUpper(kw)] = true } for _, schema := range db.Schemas { for _, table := range schema.Tables { // Check table name if rule.CheckTables { location := formatLocation(schema.Name, table.Name, "") passed := !keywords[strings.ToUpper(table.Name)] results = append(results, createResult( ruleName, passed, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "object_type": "table", }, )) } // Check column names if rule.CheckColumns { for _, col := range table.Columns { location := formatLocation(schema.Name, table.Name, col.Name) passed := !keywords[strings.ToUpper(col.Name)] results = append(results, createResult( ruleName, passed, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "column": col.Name, "object_type": "column", }, )) } } } } return results } // validateMissingPrimaryKey checks for tables without primary keys func validateMissingPrimaryKey(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} for _, schema := range db.Schemas { for _, table := range schema.Tables { hasPrimaryKey := false // Check columns for primary key for _, col := range table.Columns { if col.IsPrimaryKey { hasPrimaryKey = true break } } // Also check constraints if !hasPrimaryKey { for _, constraint := range table.Constraints { if constraint.Type == models.PrimaryKeyConstraint { hasPrimaryKey = true break } } } location := formatLocation(schema.Name, table.Name, "") results = append(results, createResult( ruleName, hasPrimaryKey, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, }, )) } } return results } // validateOrphanedForeignKey checks for foreign keys referencing non-existent tables func validateOrphanedForeignKey(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} // Build a map of existing tables for quick lookup tableExists := make(map[string]bool) for _, schema := range db.Schemas { for _, table := range schema.Tables { key := schema.Name + "." + table.Name tableExists[key] = true } } // Check all foreign key constraints for _, schema := range db.Schemas { for _, table := range schema.Tables { for _, constraint := range table.Constraints { if constraint.Type == models.ForeignKeyConstraint { // Build referenced table key refSchema := constraint.ReferencedSchema if refSchema == "" { refSchema = schema.Name } refKey := refSchema + "." + constraint.ReferencedTable location := formatLocation(schema.Name, table.Name, "") passed := tableExists[refKey] results = append(results, createResult( ruleName, passed, rule.Message, location, map[string]interface{}{ "schema": schema.Name, "table": table.Name, "constraint": constraint.Name, "referenced_schema": refSchema, "referenced_table": constraint.ReferencedTable, }, )) } } } } return results } // validateCircularDependency checks for circular foreign key dependencies func validateCircularDependency(db *models.Database, rule Rule, ruleName string) []ValidationResult { results := []ValidationResult{} // Build dependency graph dependencies := make(map[string][]string) for _, schema := range db.Schemas { for _, table := range schema.Tables { tableKey := schema.Name + "." + table.Name for _, constraint := range table.Constraints { if constraint.Type == models.ForeignKeyConstraint { refSchema := constraint.ReferencedSchema if refSchema == "" { refSchema = schema.Name } refKey := refSchema + "." + constraint.ReferencedTable dependencies[tableKey] = append(dependencies[tableKey], refKey) } } } } // Check for cycles using DFS for tableKey := range dependencies { visited := make(map[string]bool) recStack := make(map[string]bool) if hasCycle(tableKey, dependencies, visited, recStack) { parts := strings.Split(tableKey, ".") location := formatLocation(parts[0], parts[1], "") results = append(results, createResult( ruleName, false, rule.Message, location, map[string]interface{}{ "schema": parts[0], "table": parts[1], }, )) } } return results } // Helper functions // hasCycle performs DFS to detect cycles in dependency graph func hasCycle(node string, graph map[string][]string, visited, recStack map[string]bool) bool { visited[node] = true recStack[node] = true for _, neighbor := range graph[node] { if !visited[neighbor] { if hasCycle(neighbor, graph, visited, recStack) { return true } } else if recStack[neighbor] { return true } } recStack[node] = false return false } // normalizeDataType removes size/precision from data type func normalizeDataType(dataType string) string { // Remove everything in parentheses idx := strings.Index(dataType, "(") if idx > 0 { dataType = dataType[:idx] } return strings.ToLower(strings.TrimSpace(dataType)) } // contains checks if a string slice contains a value func contains(slice []string, value string) bool { for _, item := range slice { if strings.EqualFold(item, value) { return true } } return false }