first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,423 @@
// Package main provides a tool to validate migration table creation order
// by checking that dependent tables (with foreign keys) are created after
// the tables they reference.
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// TableDependency represents a foreign key relationship where
// Table has a FK column that references DependsOn
type TableDependency struct {
Table string // Table that has the FK column
DependsOn string // Table being referenced (must be created first)
Field string // Field name with the FK
SourceFile string // Source file where this is defined
}
// MigrationAction represents a table creation or column addition in migrations
type MigrationAction struct {
MigrationID string
ActionType string // "CreateTable" or "AddColumn"
Table string
Column string // Only for AddColumn
Order int // Order within migrations.go
Line int // Line number in file
}
func main() {
// Default paths relative to where the script is run from
migrationsPath := "framework/configstore/migrations.go"
tablesDir := "framework/configstore/tables"
// Allow overriding via command line args
if len(os.Args) > 1 {
migrationsPath = os.Args[1]
}
if len(os.Args) > 2 {
tablesDir = os.Args[2]
}
// Parse table definitions to get FK dependencies
dependencies, err := parseTableDefinitions(tablesDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing table definitions: %v\n", err)
os.Exit(1)
}
// Parse migrations to get table creation order
actions, err := parseMigrationOrder(migrationsPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing migrations: %v\n", err)
os.Exit(1)
}
// Validate dependencies
violations := validateDependencies(dependencies, actions)
// Report results
if len(violations) == 0 {
fmt.Println("✓ All migration dependencies are satisfied!")
fmt.Printf(" Checked %d table dependencies across %d migration actions\n", len(dependencies), len(actions))
os.Exit(0)
}
fmt.Printf("✗ Found %d dependency violation(s):\n\n", len(violations))
for _, v := range violations {
fmt.Println(v)
}
os.Exit(1)
}
// parseTableDefinitions parses all Go files in the tables directory
// and extracts foreign key relationships from GORM struct tags
//
// GORM FK relationships:
// 1. Belongs-to: `Budget *TableBudget gorm:"foreignKey:BudgetID"`
// - FK column (BudgetID) is on THIS table
// - Referenced table (TableBudget) must be created FIRST
//
// 2. Has-many: `Keys []TableKey gorm:"foreignKey:ProviderID"`
// - FK column (ProviderID) is on the CHILD table (TableKey)
// - THIS table (parent) must be created FIRST
// - We don't track this as a dependency because the parent comes first naturally
func parseTableDefinitions(tablesDir string) ([]TableDependency, error) {
var dependencies []TableDependency
// Table struct name to table name mapping
tableNames := make(map[string]string)
// First pass: collect all table name mappings
files, err := filepath.Glob(filepath.Join(tablesDir, "*.go"))
if err != nil {
return nil, fmt.Errorf("failed to glob tables dir: %w", err)
}
fset := token.NewFileSet()
for _, file := range files {
node, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", file, err)
}
// Find struct types and their TableName methods
ast.Inspect(node, func(n ast.Node) bool {
// Look for TableName() methods
if funcDecl, ok := n.(*ast.FuncDecl); ok {
if funcDecl.Name.Name == "TableName" && funcDecl.Recv != nil {
// Get the receiver type name
if len(funcDecl.Recv.List) > 0 {
if ident, ok := funcDecl.Recv.List[0].Type.(*ast.Ident); ok {
structName := ident.Name
// Extract the table name from the return statement
if funcDecl.Body != nil {
for _, stmt := range funcDecl.Body.List {
if ret, ok := stmt.(*ast.ReturnStmt); ok {
if len(ret.Results) > 0 {
if lit, ok := ret.Results[0].(*ast.BasicLit); ok {
tableName := strings.Trim(lit.Value, `"`)
tableNames[structName] = tableName
}
}
}
}
}
}
}
}
}
return true
})
}
// Second pass: find foreign key relationships
// Pattern to match GORM foreignKey tags
fkPattern := regexp.MustCompile(`foreignKey:(\w+)`)
for _, file := range files {
node, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
if err != nil {
continue
}
ast.Inspect(node, func(n ast.Node) bool {
typeSpec, ok := n.(*ast.TypeSpec)
if !ok {
return true
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
return true
}
structName := typeSpec.Name.Name
if !strings.HasPrefix(structName, "Table") && structName != "SessionsTable" {
return true
}
currentTableName := tableNames[structName]
if currentTableName == "" {
return true
}
// Check each field for FK relationships
for _, field := range structType.Fields.List {
if field.Tag == nil {
continue
}
tag := field.Tag.Value
// Skip fields that are not stored in DB
if strings.Contains(tag, `gorm:"-"`) {
continue
}
// Look for foreignKey in gorm tag
fkMatches := fkPattern.FindStringSubmatch(tag)
if len(fkMatches) < 2 {
continue
}
fkColumn := fkMatches[1]
// Get field name
fieldName := ""
if len(field.Names) > 0 {
fieldName = field.Names[0].Name
}
// Determine the type of relationship based on field type
var refTableStruct string
isBelongsTo := false
switch t := field.Type.(type) {
case *ast.StarExpr:
// Pointer type: *TableBudget - this is a "belongs-to" relationship
// The FK column is on THIS table, referencing the other table
if ident, ok := t.X.(*ast.Ident); ok {
refTableStruct = ident.Name
isBelongsTo = true
}
case *ast.ArrayType, *ast.SliceExpr:
// Slice type: []TableKey - this is a "has-many" relationship
// The FK column is on the CHILD table, not on this table
// Parent must be created first, which is natural order
// We don't need to track this as a dependency
continue
case *ast.Ident:
// Direct type reference
refTableStruct = t.Name
isBelongsTo = true
}
// Only track belongs-to relationships where THIS table has the FK column
if !isBelongsTo || refTableStruct == "" || !strings.HasPrefix(refTableStruct, "Table") {
continue
}
// Verify the FK column exists on this struct
hasFKColumn := false
for _, f := range structType.Fields.List {
if len(f.Names) > 0 && f.Names[0].Name == fkColumn {
hasFKColumn = true
break
}
}
// If the FK column is on this table, it's a belongs-to relationship
// The referenced table must be created before this table
if hasFKColumn {
refTableName := tableNames[refTableStruct]
if refTableName != "" {
dependencies = append(dependencies, TableDependency{
Table: currentTableName,
DependsOn: refTableName,
Field: fieldName,
SourceFile: filepath.Base(file),
})
}
}
}
return true
})
}
return dependencies, nil
}
// parseMigrationOrder parses migrations.go and extracts the order of table creations
func parseMigrationOrder(migrationsPath string) ([]MigrationAction, error) {
var actions []MigrationAction
content, err := os.ReadFile(migrationsPath)
if err != nil {
return nil, fmt.Errorf("failed to read migrations file: %w", err)
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, migrationsPath, content, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("failed to parse migrations file: %w", err)
}
// Track the current migration function being processed
currentMigration := ""
order := 0
// Table struct to table name mapping (simplified)
tableMapping := map[string]string{
"TableConfigHash": "config_hashes",
"TableBudget": "governance_budgets",
"TableRateLimit": "governance_rate_limits",
"TableProvider": "config_providers",
"TableKey": "config_keys",
"TableModel": "config_models",
"TableOauthConfig": "oauth_configs",
"TableOauthToken": "oauth_tokens",
"TableMCPClient": "config_mcp_clients",
"TableClientConfig": "config_client",
"TableEnvKey": "config_env_keys",
"TableVectorStoreConfig": "config_vector_stores",
"TableLogStoreConfig": "config_log_stores",
"TableCustomer": "governance_customers",
"TableTeam": "governance_teams",
"TableVirtualKey": "governance_virtual_keys",
"TableGovernanceConfig": "governance_configs",
"TableModelPricing": "model_pricing",
"TablePlugin": "plugins",
"TableFrameworkConfig": "framework_configs",
"TableVirtualKeyProviderConfig": "governance_virtual_key_provider_configs",
"TableVirtualKeyMCPConfig": "governance_virtual_key_mcp_configs",
"SessionsTable": "sessions",
"TableDistributedLock": "distributed_locks",
"TableModelConfig": "model_configs",
"TableRoutingRule": "routing_rules",
}
ast.Inspect(node, func(n ast.Node) bool {
// Track function declarations for migration IDs
if funcDecl, ok := n.(*ast.FuncDecl); ok {
if strings.HasPrefix(funcDecl.Name.Name, "migration") {
currentMigration = funcDecl.Name.Name
}
}
// Look for CreateTable calls
if call, ok := n.(*ast.CallExpr); ok {
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
if sel.Sel.Name == "CreateTable" {
// Extract the table type
if len(call.Args) > 0 {
if unary, ok := call.Args[0].(*ast.UnaryExpr); ok {
if comp, ok := unary.X.(*ast.CompositeLit); ok {
if sel, ok := comp.Type.(*ast.SelectorExpr); ok {
structName := sel.Sel.Name
if tableName, exists := tableMapping[structName]; exists {
pos := fset.Position(call.Pos())
actions = append(actions, MigrationAction{
MigrationID: currentMigration,
ActionType: "CreateTable",
Table: tableName,
Order: order,
Line: pos.Line,
})
order++
}
}
}
}
}
} else if sel.Sel.Name == "AddColumn" {
// Extract column additions that might have FK constraints
if len(call.Args) >= 2 {
// First arg is table, second is column name
if unary, ok := call.Args[0].(*ast.UnaryExpr); ok {
if comp, ok := unary.X.(*ast.CompositeLit); ok {
if sel, ok := comp.Type.(*ast.SelectorExpr); ok {
structName := sel.Sel.Name
if tableName, exists := tableMapping[structName]; exists {
colName := ""
if lit, ok := call.Args[1].(*ast.BasicLit); ok {
colName = strings.Trim(lit.Value, `"`)
}
pos := fset.Position(call.Pos())
actions = append(actions, MigrationAction{
MigrationID: currentMigration,
ActionType: "AddColumn",
Table: tableName,
Column: colName,
Order: order,
Line: pos.Line,
})
order++
}
}
}
}
}
}
}
}
return true
})
return actions, nil
}
// validateDependencies checks if tables with FK dependencies are created after their referenced tables
func validateDependencies(deps []TableDependency, actions []MigrationAction) []string {
var violations []string
// Build a map of table -> first creation order
tableCreationOrder := make(map[string]int)
tableCreationLine := make(map[string]int)
tableCreationMigration := make(map[string]string)
for _, action := range actions {
if action.ActionType == "CreateTable" {
if _, exists := tableCreationOrder[action.Table]; !exists {
tableCreationOrder[action.Table] = action.Order
tableCreationLine[action.Table] = action.Line
tableCreationMigration[action.Table] = action.MigrationID
}
}
}
// Check each dependency
for _, dep := range deps {
depOrder, depExists := tableCreationOrder[dep.Table]
refOrder, refExists := tableCreationOrder[dep.DependsOn]
// Skip if either table isn't created via migrations (might be handled elsewhere)
if !depExists || !refExists {
continue
}
// The table with the FK (dep.Table) should be created AFTER the referenced table (dep.DependsOn)
// Violation: dep.Table is created before dep.DependsOn
if depOrder < refOrder {
violations = append(violations, fmt.Sprintf(
" - Table '%s' (line %d, %s) is created before '%s' (line %d, %s)\n"+
" but '%s' has a FK column referencing '%s' via field '%s' (defined in %s)",
dep.Table, tableCreationLine[dep.Table], tableCreationMigration[dep.Table],
dep.DependsOn, tableCreationLine[dep.DependsOn], tableCreationMigration[dep.DependsOn],
dep.Table, dep.DependsOn, dep.Field, dep.SourceFile,
))
}
}
// Sort violations for consistent output
sort.Strings(violations)
return violations
}