diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 471105e..146e2f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: go-version: '1.24' cache-dependency-path: backend/go.sum - name: Start Docker Databases - run: docker compose up -d + run: cd frontend && docker compose up -d --wait - name: Run Backend Tests run: cd backend && go test -v ./... @@ -69,6 +69,6 @@ jobs: - name: Install Playwright Browsers run: cd frontend && npx playwright install --with-deps - name: Start Docker Databases - run: docker compose up -d + run: cd frontend && docker compose up -d --wait - name: Run E2E Tests run: cd frontend && xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" npm run test:e2e diff --git a/backend/internal/ast/compiler_mysql.go b/backend/internal/ast/compiler_mysql.go index 85f721f..0bcbbf6 100644 --- a/backend/internal/ast/compiler_mysql.go +++ b/backend/internal/ast/compiler_mysql.go @@ -54,6 +54,60 @@ func (c *MysqlCompiler) formatComment(col ColumnDefinition) string { return fmt.Sprintf("COMMENT '%s'", strings.ReplaceAll(col.Comment, "'", "''")) } +func (c *MysqlCompiler) GenerateCreateTableSql(req DiffRequest) []string { + var colDefs []string + var pkCols []string + safeTable := fmt.Sprintf("`%s`", req.TableName) + + for _, col := range req.Columns { + autoInc := "" + if col.IsAutoIncrement { + autoInc = "AUTO_INCREMENT" + } + colDefs = append(colDefs, strings.TrimSpace(fmt.Sprintf("`%s` %s %s %s %s %s", col.Name, c.formatType(col), c.formatNullable(col), c.formatDefault(col), autoInc, c.formatComment(col)))) + if col.IsPrimaryKey { + pkCols = append(pkCols, fmt.Sprintf("`%s`", col.Name)) + } + } + if len(pkCols) > 0 { + colDefs = append(colDefs, fmt.Sprintf("PRIMARY KEY (%s)", strings.Join(pkCols, ", "))) + } + + for _, idx := range req.Indexes { + uniqueStr := "" + if idx.IsUnique { + uniqueStr = "UNIQUE " + } + var cols []string + for _, col := range idx.Columns { + cols = append(cols, fmt.Sprintf("`%s`", col)) + } + colDefs = append(colDefs, fmt.Sprintf("%sKEY `%s` (%s)", uniqueStr, idx.Name, strings.Join(cols, ", "))) + } + + engine := "InnoDB" + charset := "utf8mb4" + collation := "utf8mb4_unicode_ci" + if req.Config != nil { + if req.Config.Engine != "" { + engine = req.Config.Engine + } + if req.Config.Charset != "" { + charset = req.Config.Charset + } + if req.Config.Collation != "" { + collation = req.Config.Collation + } + } + + sql := fmt.Sprintf("CREATE TABLE %s (\n %s\n) ENGINE=%s DEFAULT CHARSET=%s COLLATE=%s;", safeTable, strings.Join(colDefs, ",\n "), engine, charset, collation) + return []string{sql} +} + +func (c *MysqlCompiler) GenerateDropTableSql(req DiffRequest) []string { + return []string{fmt.Sprintf("DROP TABLE IF EXISTS `%s`;", req.TableName)} +} + func (c *MysqlCompiler) GenerateAlterTableSql(req DiffRequest) []string { var sqls []string safeTable := fmt.Sprintf("`%s`", req.TableName) @@ -72,7 +126,6 @@ func (c *MysqlCompiler) GenerateAlterTableSql(req DiffRequest) []string { allOldCols = append(allOldCols, col) } } - // Sort by OriginalIndex for i := 0; i < len(allOldCols); i++ { for j := i + 1; j < len(allOldCols); j++ { if allOldCols[i].OriginalIndex > allOldCols[j].OriginalIndex { @@ -89,7 +142,7 @@ func (c *MysqlCompiler) GenerateAlterTableSql(req DiffRequest) []string { } if req.OldTableName != "" && req.OldTableName != req.TableName { - sqls = append(sqls, fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`;", req.OldTableName, req.TableName)) + sqls = append(sqls, fmt.Sprintf("RENAME TABLE `%s` TO `%s`;", req.OldTableName, req.TableName)) } for _, col := range req.DeletedColumns { sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s DROP COLUMN `%s`;", safeTable, col.Name)) @@ -105,8 +158,8 @@ func (c *MysqlCompiler) GenerateAlterTableSql(req DiffRequest) []string { colType := c.formatType(col) if col.Original == nil { - sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s ADD COLUMN `%s` %s %s %s %s %s;", - safeTable, col.Name, colType, c.formatNullable(col), c.formatDefault(col), c.formatComment(col), position)) + sqls = append(sqls, strings.TrimSpace(fmt.Sprintf("ALTER TABLE %s ADD COLUMN `%s` %s %s %s %s %s;", + safeTable, col.Name, colType, c.formatNullable(col), c.formatDefault(col), c.formatComment(col), position))) } else { origType := c.formatType(*col.Original) propChanged := col.Name != col.Original.Name || colType != origType || col.Nullable != col.Original.Nullable || @@ -120,30 +173,146 @@ func (c *MysqlCompiler) GenerateAlterTableSql(req DiffRequest) []string { posChanged := actualPrev != oldPredecessor[col.Original.Name] if propChanged || posChanged { - sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s CHANGE `%s` `%s` %s %s %s %s %s;", - safeTable, col.Original.Name, col.Name, colType, c.formatNullable(col), c.formatDefault(col), c.formatComment(col), position)) + sqls = append(sqls, strings.TrimSpace(fmt.Sprintf("ALTER TABLE %s CHANGE COLUMN `%s` `%s` %s %s %s %s %s;", + safeTable, col.Original.Name, col.Name, colType, c.formatNullable(col), c.formatDefault(col), c.formatComment(col), position))) + } + } + } + + // Foreign Keys + for _, fk := range req.DeletedForeignKeys { + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s DROP FOREIGN KEY `%s`;", safeTable, fk.Name)) + } + for _, fk := range req.ForeignKeys { + if fk.Original == nil { + var cols, refCols []string + for _, c := range fk.Columns { + cols = append(cols, fmt.Sprintf("`%s`", c)) + } + for _, c := range fk.ReferencedColumns { + refCols = append(refCols, fmt.Sprintf("`%s`", c)) + } + onDel := "" + if fk.OnDelete != "" { + onDel = " ON DELETE " + fk.OnDelete + } + onUpd := "" + if fk.OnUpdate != "" { + onUpd = " ON UPDATE " + fk.OnUpdate + } + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT `%s` FOREIGN KEY (%s) REFERENCES `%s` (%s)%s%s;", + safeTable, fk.Name, strings.Join(cols, ", "), fk.ReferencedTable, strings.Join(refCols, ", "), onDel, onUpd)) + } + } + + // Check Constraints (MySQL >= 8.0) + for _, chk := range req.DeletedChecks { + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s DROP CHECK `%s`;", safeTable, chk.Name)) + } + for _, chk := range req.CheckConstraints { + if chk.Original == nil { + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT `%s` CHECK (%s);", safeTable, chk.Name, chk.Expression)) + } + } + + // Indexes + for _, idx := range req.DeletedIndexes { + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s DROP INDEX `%s`;", safeTable, idx.Name)) + } + for _, idx := range req.Indexes { + if idx.Original == nil { + uniqueStr := "" + if idx.IsUnique { + uniqueStr = "UNIQUE " + } + var cols []string + for _, col := range idx.Columns { + cols = append(cols, fmt.Sprintf("`%s`", col)) } + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s ADD %sINDEX `%s` (%s);", safeTable, uniqueStr, idx.Name, strings.Join(cols, ", "))) } } + + // Config + if req.Config != nil { + var configOpts []string + if req.Config.Engine != "" { + configOpts = append(configOpts, "ENGINE="+req.Config.Engine) + } + if req.Config.Charset != "" { + configOpts = append(configOpts, "DEFAULT CHARSET="+req.Config.Charset) + } + if req.Config.Collation != "" { + configOpts = append(configOpts, "COLLATE="+req.Config.Collation) + } + if req.Config.AutoIncrementOffset > 0 { + configOpts = append(configOpts, fmt.Sprintf("AUTO_INCREMENT=%d", req.Config.AutoIncrementOffset)) + } + if len(configOpts) > 0 { + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s %s;", safeTable, strings.Join(configOpts, " "))) + } + } + return sqls } -func (c *MysqlCompiler) GenerateCreateTableSql(req DiffRequest) []string { - var colDefs []string - var pkCols []string - for _, col := range req.Columns { - autoInc := "" - if col.IsAutoIncrement { - autoInc = "AUTO_INCREMENT" +func (c *MysqlCompiler) GenerateCreateViewSql(req DiffRequest) []string { + var sqls []string + for _, view := range req.Views { + if view.Original == nil || view.Definition != view.Original.Definition { + sqls = append(sqls, fmt.Sprintf("CREATE OR REPLACE VIEW `%s` AS %s;", view.Name, view.Definition)) } - colDefs = append(colDefs, fmt.Sprintf("`%s` %s %s %s %s %s", col.Name, c.formatType(col), c.formatNullable(col), c.formatDefault(col), autoInc, c.formatComment(col))) - if col.IsPrimaryKey { - pkCols = append(pkCols, fmt.Sprintf("`%s`", col.Name)) + } + return sqls +} + +func (c *MysqlCompiler) GenerateDropViewSql(req DiffRequest) []string { + var sqls []string + for _, view := range req.DeletedViews { + sqls = append(sqls, fmt.Sprintf("DROP VIEW IF EXISTS `%s`;", view.Name)) + } + return sqls +} + +func (c *MysqlCompiler) GenerateCreateTriggerSql(req DiffRequest) []string { + var sqls []string + for _, trig := range req.Triggers { + if trig.Original == nil || trig.Definition != trig.Original.Definition { + if trig.Original != nil { + sqls = append(sqls, fmt.Sprintf("DROP TRIGGER IF EXISTS `%s`;", trig.Name)) + } + sqls = append(sqls, fmt.Sprintf("CREATE TRIGGER `%s` %s %s ON `%s` FOR EACH ROW %s", trig.Name, trig.Timing, trig.Event, trig.TableName, trig.Definition)) } } - if len(pkCols) > 0 { - colDefs = append(colDefs, fmt.Sprintf("PRIMARY KEY (%s)", strings.Join(pkCols, ", "))) + return sqls +} + +func (c *MysqlCompiler) GenerateDropTriggerSql(req DiffRequest) []string { + var sqls []string + for _, trig := range req.DeletedTriggers { + sqls = append(sqls, fmt.Sprintf("DROP TRIGGER IF EXISTS `%s`;", trig.Name)) } - sql := fmt.Sprintf("CREATE TABLE `%s` (\n %s\n);", req.TableName, strings.Join(colDefs, ",\n ")) - return []string{sql} + return sqls +} + +func (c *MysqlCompiler) GenerateCreateRoutineSql(req DiffRequest) []string { + var sqls []string + for _, rout := range req.Routines { + if rout.Original == nil || rout.Definition != rout.Original.Definition { + if rout.Original != nil { + sqls = append(sqls, fmt.Sprintf("DROP %s IF EXISTS `%s`;", rout.Type, rout.Name)) + } + // Routine definition should include CREATE PROCEDURE/FUNCTION... + sqls = append(sqls, rout.Definition) + } + } + return sqls +} + +func (c *MysqlCompiler) GenerateDropRoutineSql(req DiffRequest) []string { + var sqls []string + for _, rout := range req.DeletedRoutines { + sqls = append(sqls, fmt.Sprintf("DROP %s IF EXISTS `%s`;", rout.Type, rout.Name)) + } + return sqls } diff --git a/backend/internal/ast/compiler_pg.go b/backend/internal/ast/compiler_pg.go index ba60bfa..5e3eb63 100644 --- a/backend/internal/ast/compiler_pg.go +++ b/backend/internal/ast/compiler_pg.go @@ -38,7 +38,6 @@ func (c *PGCompiler) formatDefault(col ColumnDefinition) string { if col.IsDefaultExpression { return fmt.Sprintf("DEFAULT %s", val) } - // Simple escaping for strings return fmt.Sprintf("DEFAULT '%s'", strings.ReplaceAll(val, "'", "''")) } @@ -49,6 +48,57 @@ func (c *PGCompiler) formatTableName(schema, table string) string { return fmt.Sprintf("\"%s\"", table) } +func (c *PGCompiler) formatNullable(col ColumnDefinition) string { + if col.Nullable { + return "NULL" + } + return "NOT NULL" +} + +func (c *PGCompiler) GenerateCreateTableSql(req DiffRequest) []string { + var colDefs []string + var comments []string + var pkCols []string + fullTableName := c.formatTableName(req.Schema, req.TableName) + + for _, col := range req.Columns { + colDefs = append(colDefs, strings.TrimSpace(fmt.Sprintf("\"%s\" %s %s %s", col.Name, c.formatType(col), c.formatNullable(col), c.formatDefault(col)))) + if col.Comment != "" { + comments = append(comments, fmt.Sprintf("COMMENT ON COLUMN %s.\"%s\" IS '%s';", fullTableName, col.Name, strings.ReplaceAll(col.Comment, "'", "''"))) + } + if col.IsPrimaryKey { + pkCols = append(pkCols, fmt.Sprintf("\"%s\"", col.Name)) + } + } + + if len(pkCols) > 0 { + colDefs = append(colDefs, fmt.Sprintf("PRIMARY KEY (%s)", strings.Join(pkCols, ", "))) + } + + sql := fmt.Sprintf("CREATE TABLE %s (\n %s\n);", fullTableName, strings.Join(colDefs, ",\n ")) + res := []string{sql} + res = append(res, comments...) + + // Create Indexes + for _, idx := range req.Indexes { + uniqueStr := "" + if idx.IsUnique { + uniqueStr = "UNIQUE " + } + var cols []string + for _, c := range idx.Columns { + cols = append(cols, fmt.Sprintf("\"%s\"", c)) + } + res = append(res, fmt.Sprintf("CREATE %sINDEX \"%s\" ON %s (%s);", uniqueStr, idx.Name, fullTableName, strings.Join(cols, ", "))) + } + return res +} + +func (c *PGCompiler) GenerateDropTableSql(req DiffRequest) []string { + fullTableName := c.formatTableName(req.Schema, req.TableName) + return []string{fmt.Sprintf("DROP TABLE IF EXISTS %s CASCADE;", fullTableName)} +} + func (c *PGCompiler) GenerateAlterTableSql(req DiffRequest) []string { var sqls []string fullTableName := c.formatTableName(req.Schema, req.TableName) @@ -65,7 +115,6 @@ func (c *PGCompiler) GenerateAlterTableSql(req DiffRequest) []string { for _, col := range req.Columns { colType := c.formatType(col) if col.Original == nil { - // New column sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s ADD COLUMN \"%s\" %s %s %s;", fullTableName, col.Name, colType, c.formatNullable(col), c.formatDefault(col))) if col.Comment != "" { @@ -77,7 +126,7 @@ func (c *PGCompiler) GenerateAlterTableSql(req DiffRequest) []string { fullTableName, col.Original.Name, col.Name)) } if colType != c.formatType(*col.Original) { - sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s ALTER COLUMN \"%s\" TYPE %s;", fullTableName, col.Name, colType)) + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s ALTER COLUMN \"%s\" TYPE %s USING \"%s\"::%s;", fullTableName, col.Name, colType, col.Name, colType)) } if col.Nullable != col.Original.Nullable { if col.Nullable { @@ -86,7 +135,6 @@ func (c *PGCompiler) GenerateAlterTableSql(req DiffRequest) []string { sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s ALTER COLUMN \"%s\" SET NOT NULL;", fullTableName, col.Name)) } } - // Default change if (col.DefaultValue == nil) != (col.Original.DefaultValue == nil) || (col.DefaultValue != nil && *col.DefaultValue != *col.Original.DefaultValue) { if col.DefaultValue == nil { sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s ALTER COLUMN \"%s\" DROP DEFAULT;", fullTableName, col.Name)) @@ -95,44 +143,131 @@ func (c *PGCompiler) GenerateAlterTableSql(req DiffRequest) []string { sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s ALTER COLUMN \"%s\" SET %s;", fullTableName, col.Name, def)) } } - // Comment change if col.Comment != col.Original.Comment { sqls = append(sqls, fmt.Sprintf("COMMENT ON COLUMN %s.\"%s\" IS '%s';", fullTableName, col.Name, strings.ReplaceAll(col.Comment, "'", "''"))) } } } + + // Constraints + for _, fk := range req.DeletedForeignKeys { + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s DROP CONSTRAINT \"%s\";", fullTableName, fk.Name)) + } + for _, fk := range req.ForeignKeys { + if fk.Original == nil { + var cols, refCols []string + for _, c := range fk.Columns { + cols = append(cols, fmt.Sprintf("\"%s\"", c)) + } + for _, c := range fk.ReferencedColumns { + refCols = append(refCols, fmt.Sprintf("\"%s\"", c)) + } + onDel := "" + if fk.OnDelete != "" { + onDel = " ON DELETE " + fk.OnDelete + } + onUpd := "" + if fk.OnUpdate != "" { + onUpd = " ON UPDATE " + fk.OnUpdate + } + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT \"%s\" FOREIGN KEY (%s) REFERENCES %s (%s)%s%s;", + fullTableName, fk.Name, strings.Join(cols, ", "), c.formatTableName(req.Schema, fk.ReferencedTable), strings.Join(refCols, ", "), onDel, onUpd)) + } + } + + for _, chk := range req.DeletedChecks { + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s DROP CONSTRAINT \"%s\";", fullTableName, chk.Name)) + } + for _, chk := range req.CheckConstraints { + if chk.Original == nil { + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT \"%s\" CHECK (%s);", fullTableName, chk.Name, chk.Expression)) + } + } + + // Indexes + for _, idx := range req.DeletedIndexes { + idxName := idx.Name + schemaPrefix := "" + if req.Schema != "" { + schemaPrefix = fmt.Sprintf("\"%s\".", req.Schema) + } + sqls = append(sqls, fmt.Sprintf("DROP INDEX %s\"%s\";", schemaPrefix, idxName)) + } + for _, idx := range req.Indexes { + if idx.Original == nil { + uniqueStr := "" + if idx.IsUnique { + uniqueStr = "UNIQUE " + } + var cols []string + for _, col := range idx.Columns { + cols = append(cols, fmt.Sprintf("\"%s\"", col)) + } + sqls = append(sqls, fmt.Sprintf("CREATE %sINDEX \"%s\" ON %s (%s);", uniqueStr, idx.Name, fullTableName, strings.Join(cols, ", "))) + } + } return sqls } -func (c *PGCompiler) formatNullable(col ColumnDefinition) string { - if col.Nullable { - return "NULL" +func (c *PGCompiler) GenerateCreateViewSql(req DiffRequest) []string { + var sqls []string + for _, view := range req.Views { + if view.Original == nil || view.Definition != view.Original.Definition { + sqls = append(sqls, fmt.Sprintf("CREATE OR REPLACE VIEW %s AS %s;", c.formatTableName(req.Schema, view.Name), view.Definition)) + } } - return "NOT NULL" + return sqls } -func (c *PGCompiler) GenerateCreateTableSql(req DiffRequest) []string { - var colDefs []string - var comments []string - var pkCols []string - fullTableName := c.formatTableName(req.Schema, req.TableName) +func (c *PGCompiler) GenerateDropViewSql(req DiffRequest) []string { + var sqls []string + for _, view := range req.DeletedViews { + sqls = append(sqls, fmt.Sprintf("DROP VIEW IF EXISTS %s;", c.formatTableName(req.Schema, view.Name))) + } + return sqls +} - for _, col := range req.Columns { - colDefs = append(colDefs, strings.TrimSpace(fmt.Sprintf("\"%s\" %s %s %s", col.Name, c.formatType(col), c.formatNullable(col), c.formatDefault(col)))) - if col.Comment != "" { - comments = append(comments, fmt.Sprintf("COMMENT ON COLUMN %s.\"%s\" IS '%s';", fullTableName, col.Name, strings.ReplaceAll(col.Comment, "'", "''"))) - } - if col.IsPrimaryKey { - pkCols = append(pkCols, fmt.Sprintf("\"%s\"", col.Name)) +func (c *PGCompiler) GenerateCreateTriggerSql(req DiffRequest) []string { + var sqls []string + for _, trig := range req.Triggers { + if trig.Original == nil { + sqls = append(sqls, fmt.Sprintf("CREATE TRIGGER \"%s\" %s %s ON %s %s;", trig.Name, trig.Timing, trig.Event, c.formatTableName(req.Schema, trig.TableName), trig.Definition)) + } else { + if trig.Enabled != trig.Original.Enabled { + state := "ENABLE" + if !trig.Enabled { + state = "DISABLE" + } + sqls = append(sqls, fmt.Sprintf("ALTER TABLE %s %s TRIGGER \"%s\";", c.formatTableName(req.Schema, trig.TableName), state, trig.Name)) + } } } + return sqls +} - if len(pkCols) > 0 { - colDefs = append(colDefs, fmt.Sprintf("PRIMARY KEY (%s)", strings.Join(pkCols, ", "))) +func (c *PGCompiler) GenerateDropTriggerSql(req DiffRequest) []string { + var sqls []string + for _, trig := range req.DeletedTriggers { + sqls = append(sqls, fmt.Sprintf("DROP TRIGGER IF EXISTS \"%s\" ON %s;", trig.Name, c.formatTableName(req.Schema, trig.TableName))) + } + return sqls +} + +func (c *PGCompiler) GenerateCreateRoutineSql(req DiffRequest) []string { + var sqls []string + for _, rout := range req.Routines { + if rout.Original == nil || rout.Definition != rout.Original.Definition { + sqls = append(sqls, rout.Definition) // For PG, Definition usually contains full CREATE OR REPLACE FUNCTION ... + } } + return sqls +} - sql := fmt.Sprintf("CREATE TABLE %s (\n %s\n);", fullTableName, strings.Join(colDefs, ",\n ")) - res := []string{sql} - res = append(res, comments...) - return res +func (c *PGCompiler) GenerateDropRoutineSql(req DiffRequest) []string { + var sqls []string + for _, rout := range req.DeletedRoutines { + // Basic drop, requires parameter types in real PG, simplified here for testing. + sqls = append(sqls, fmt.Sprintf("DROP %s IF EXISTS \"%s\";", rout.Type, rout.Name)) + } + return sqls } diff --git a/backend/internal/ast/diff.go b/backend/internal/ast/diff.go index b1e6db8..139de30 100644 --- a/backend/internal/ast/diff.go +++ b/backend/internal/ast/diff.go @@ -5,6 +5,13 @@ import "fmt" type Compiler interface { GenerateAlterTableSql(req DiffRequest) []string GenerateCreateTableSql(req DiffRequest) []string + GenerateDropTableSql(req DiffRequest) []string + GenerateCreateViewSql(req DiffRequest) []string + GenerateDropViewSql(req DiffRequest) []string + GenerateCreateTriggerSql(req DiffRequest) []string + GenerateDropTriggerSql(req DiffRequest) []string + GenerateCreateRoutineSql(req DiffRequest) []string + GenerateDropRoutineSql(req DiffRequest) []string } func GetCompiler(dialect string) (Compiler, error) { diff --git a/backend/internal/ast/diff_integration_mysql_test.go b/backend/internal/ast/diff_integration_mysql_test.go new file mode 100644 index 0000000..43cb97d --- /dev/null +++ b/backend/internal/ast/diff_integration_mysql_test.go @@ -0,0 +1,215 @@ +package ast + +import ( + "context" + "fmt" + "os" + "testing" + "time" + "vstable-engine/internal/db" +) + +func TestMysqlSyncIntegration(t *testing.T) { + dsn := os.Getenv("MYSQL_DSN") + if dsn == "" { + dsn = "root:password@tcp(localhost:3307)/vstable_test?charset=utf8mb4&parseTime=True&loc=Local" + } + + dialect := "mysql" + compiler := &MysqlCompiler{} + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + dbManager := db.NewManager() + err := dbManager.Connect(ctx, "test-session", dialect, dsn) + if err != nil { + t.Fatalf("[%s] Connection failed: %v", dialect, err) + } + defer dbManager.Disconnect("test-session") + driver, _ := dbManager.Get("test-session") + + tableName := fmt.Sprintf("sync_test_%s", dialect) + refTableName := fmt.Sprintf("ref_test_%s", dialect) + viewName := fmt.Sprintf("view_test_%s", dialect) + schema := "vstable_test" + + // 1. Cleanup + cleanup := []string{ + fmt.Sprintf("DROP VIEW IF EXISTS %s;", viewName), + fmt.Sprintf("DROP TABLE IF EXISTS %s;", tableName), + fmt.Sprintf("DROP TABLE IF EXISTS %s;", refTableName), + "DROP PROCEDURE IF EXISTS test_proc;", + } + + for _, sql := range cleanup { + driver.Query(ctx, sql, nil) + } + + // 2. CREATE Reference Table (for FK) + refReq := DiffRequest{ + Dialect: dialect, + Schema: schema, + TableName: refTableName, + Columns: []ColumnDefinition{ + {ID: "r1", Name: "id", Type: "int", Nullable: false, IsPrimaryKey: true, OriginalIndex: 0}, + }, + } + for _, sql := range compiler.GenerateCreateTableSql(refReq) { + if _, err := driver.Query(ctx, sql, nil); err != nil { + t.Fatalf("[%s] Create ref table failed: %v (SQL: %s)", dialect, err, sql) + } + } + + // 3. CREATE Main Table + defActive := "active" + createReq := DiffRequest{ + Dialect: dialect, + Schema: schema, + TableName: tableName, + Columns: []ColumnDefinition{ + {ID: "c1", Name: "id", Type: "int", Nullable: false, IsPrimaryKey: true, OriginalIndex: 0}, + {ID: "c2", Name: "name", Type: "varchar", Length: 100.0, Nullable: false, OriginalIndex: 1, Comment: "Initial NOT NULL"}, + {ID: "c3", Name: "status", Type: "varchar", Length: 20.0, Nullable: true, OriginalIndex: 2, DefaultValue: &defActive}, + }, + } + for _, sql := range compiler.GenerateCreateTableSql(createReq) { + if _, err := driver.Query(ctx, sql, nil); err != nil { + t.Fatalf("[%s] Create table failed: %v (SQL: %s)", dialect, err, sql) + } + } + + // 4. ALTER TABLE: Add Column, Change Column, Rename Column, Drop Column, FK, Check, Index, Config + alterReq := DiffRequest{ + Dialect: dialect, + Schema: schema, + TableName: tableName, + OldTableName: tableName, + Columns: []ColumnDefinition{ + {ID: "c1", Name: "id", Type: "int", Nullable: false, OriginalIndex: 0, Original: &ColumnDefinition{Name: "id", Type: "int", Nullable: false}}, + {ID: "c2", Name: "user_name", Type: "varchar", Length: 150.0, Nullable: true, OriginalIndex: 1, Original: &ColumnDefinition{Name: "name", Type: "varchar", Length: 100.0, Nullable: false}}, + {ID: "c3", Name: "status", Type: "varchar", Length: 20.0, Nullable: false, OriginalIndex: 2, DefaultValue: &defActive, Original: &ColumnDefinition{Name: "status", Type: "varchar", Length: 20.0, Nullable: true, DefaultValue: &defActive}}, + {ID: "c4", Name: "age", Type: "int", Nullable: true, OriginalIndex: 3}, + {ID: "c5", Name: "ref_id", Type: "int", Nullable: true, OriginalIndex: 4}, + }, + ForeignKeys: []ForeignKeyDefinition{ + {Name: "fk_ref_id", Columns: []string{"ref_id"}, ReferencedTable: refTableName, ReferencedColumns: []string{"id"}, OnDelete: "CASCADE"}, + }, + CheckConstraints: []CheckConstraintDefinition{ + {Name: "chk_age", Expression: "age >= 0"}, + }, + Indexes: []IndexDefinition{ + {Name: "idx_status", Columns: []string{"status"}, IsUnique: false}, + }, + Config: &DatabaseConfig{ + Engine: "InnoDB", + Charset: "utf8mb4", + }, + } + + sqls := compiler.GenerateAlterTableSql(alterReq) + for _, sql := range sqls { + fmt.Printf("[%s Test] Executing Alter: %s\n", dialect, sql) + if _, err := driver.Query(ctx, sql, nil); err != nil { + t.Errorf("[%s] Alter failed: %v (SQL: %s)", dialect, err, sql) + } + } + + // --- VERIFY ALTER RESULTS --- + verifyColSql := fmt.Sprintf("SELECT COLUMN_NAME, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '%s' AND TABLE_SCHEMA = '%s';", tableName, schema) + res, err := driver.Query(ctx, verifyColSql, nil) + if err != nil { + t.Errorf("[%s] Verify columns failed: %v", dialect, err) + } else { + foundCols := make(map[string]bool) + for _, row := range res.Rows { + var colName, isNullable string + for k, v := range row { + lowerK := fmt.Sprintf("%v", k) + val := "" + if s, ok := v.(string); ok { + val = s + } else if v != nil { + val = fmt.Sprintf("%v", v) + } + if lowerK == "COLUMN_NAME" || lowerK == "column_name" { + colName = val + } + if lowerK == "IS_NULLABLE" || lowerK == "is_nullable" { + isNullable = val + } + } + if colName != "" { + foundCols[colName] = true + if colName == "user_name" && isNullable != "YES" { + t.Errorf("[%s] Column user_name should be nullable", dialect) + } + if colName == "status" && isNullable != "NO" { + t.Errorf("[%s] Column status should be NOT NULL", dialect) + } + } + } + expectedCols := []string{"id", "user_name", "status", "age", "ref_id"} + for _, ec := range expectedCols { + if !foundCols[ec] { + t.Errorf("[%s] Expected column %s not found", dialect, ec) + } + } + } + + // 2. Verify Data Insertion + insertSql := fmt.Sprintf("INSERT INTO %s (id, user_name, status, age) VALUES (1, 'test', 'active', 25);", tableName) + if _, err := driver.Query(ctx, insertSql, nil); err != nil { + t.Errorf("[%s] Valid insert failed: %v", dialect, err) + } + + invalidInsertSql := fmt.Sprintf("INSERT INTO %s (id, user_name, status, age) VALUES (2, 'fail', 'active', -1);", tableName) + if _, err := driver.Query(ctx, invalidInsertSql, nil); err == nil { + t.Errorf("[%s] Check constraint 'chk_age' failed to block negative age", dialect) + } + + // 5. CREATE VIEW + viewReq := DiffRequest{ + Dialect: dialect, Schema: schema, + Views: []ViewDefinition{ + {Name: viewName, Definition: fmt.Sprintf("SELECT id, user_name FROM %s", tableName)}, + }, + } + for _, sql := range compiler.GenerateCreateViewSql(viewReq) { + if _, err := driver.Query(ctx, sql, nil); err != nil { + t.Errorf("[%s] Create View failed: %v", dialect, err) + } + } + verifyViewSql := fmt.Sprintf("SELECT * FROM %s WHERE id = 1;", viewName) + viewRes, err := driver.Query(ctx, verifyViewSql, nil) + if err != nil || len(viewRes.Rows) == 0 { + t.Errorf("[%s] View verification failed: %v", dialect, err) + } + + // 6. CREATE TRIGGER + trigReq := DiffRequest{ + Dialect: dialect, Schema: schema, + Triggers: []TriggerDefinition{ + {Name: "trig_test", Timing: "BEFORE", Event: "INSERT", TableName: tableName, Definition: "BEGIN SET @x = 1; END"}, + }, + } + for _, sql := range compiler.GenerateCreateTriggerSql(trigReq) { + if _, err := driver.Query(ctx, sql, nil); err != nil { + t.Errorf("[%s] Create Trigger failed: %v (SQL: %s)", dialect, err, sql) + } + } + + // 7. ROUTINES + routReq := DiffRequest{ + Dialect: dialect, Schema: schema, + Routines: []RoutineDefinition{ + {Name: "test_proc", Type: "PROCEDURE", Definition: "CREATE PROCEDURE test_proc() BEGIN SELECT 1; END"}, + }, + } + for _, sql := range compiler.GenerateCreateRoutineSql(routReq) { + if _, err := driver.Query(ctx, sql, nil); err != nil { + t.Errorf("[%s] Create Routine failed: %v (SQL: %s)", dialect, err, sql) + } + } + + t.Logf("[%s] All scenarios passed and verified", dialect) +} diff --git a/backend/internal/ast/diff_integration_pg_test.go b/backend/internal/ast/diff_integration_pg_test.go new file mode 100644 index 0000000..1e24d07 --- /dev/null +++ b/backend/internal/ast/diff_integration_pg_test.go @@ -0,0 +1,205 @@ +package ast + +import ( + "context" + "fmt" + "os" + "testing" + "time" + "vstable-engine/internal/db" +) + +func TestPostgresSyncIntegration(t *testing.T) { + dsn := os.Getenv("PG_DSN") + if dsn == "" { + dsn = "postgres://root:password@localhost:5433/vstable_test?sslmode=disable" + } + + dialect := "pg" + compiler := &PGCompiler{} + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + dbManager := db.NewManager() + err := dbManager.Connect(ctx, "test-session", dialect, dsn) + if err != nil { + t.Fatalf("[%s] Connection failed: %v", dialect, err) + } + defer dbManager.Disconnect("test-session") + driver, _ := dbManager.Get("test-session") + + tableName := fmt.Sprintf("sync_test_%s", dialect) + refTableName := fmt.Sprintf("ref_test_%s", dialect) + viewName := fmt.Sprintf("view_test_%s", dialect) + schema := "public" + + // 1. Cleanup + cleanup := []string{ + fmt.Sprintf("DROP VIEW IF EXISTS %s.%s CASCADE;", schema, viewName), + fmt.Sprintf("DROP TABLE IF EXISTS %s.%s CASCADE;", schema, tableName), + fmt.Sprintf("DROP TABLE IF EXISTS %s.%s CASCADE;", schema, refTableName), + fmt.Sprintf("DROP FUNCTION IF EXISTS test_func();"), + fmt.Sprintf("DROP FUNCTION IF EXISTS test_func2();"), + } + + for _, sql := range cleanup { + driver.Query(ctx, sql, nil) + } + + // 2. CREATE Reference Table (for FK) + refReq := DiffRequest{ + Dialect: dialect, + Schema: schema, + TableName: refTableName, + Columns: []ColumnDefinition{ + {ID: "r1", Name: "id", Type: "int", Nullable: false, IsPrimaryKey: true, OriginalIndex: 0}, + }, + } + for _, sql := range compiler.GenerateCreateTableSql(refReq) { + if _, err := driver.Query(ctx, sql, nil); err != nil { + t.Fatalf("[%s] Create ref table failed: %v (SQL: %s)", dialect, err, sql) + } + } + + // 3. CREATE Main Table + defActive := "active" + createReq := DiffRequest{ + Dialect: dialect, + Schema: schema, + TableName: tableName, + Columns: []ColumnDefinition{ + {ID: "c1", Name: "id", Type: "int", Nullable: false, IsPrimaryKey: true, OriginalIndex: 0}, + {ID: "c2", Name: "name", Type: "varchar", Length: 100.0, Nullable: false, OriginalIndex: 1, Comment: "Initial NOT NULL"}, + {ID: "c3", Name: "status", Type: "varchar", Length: 20.0, Nullable: true, OriginalIndex: 2, DefaultValue: &defActive}, + }, + } + for _, sql := range compiler.GenerateCreateTableSql(createReq) { + if _, err := driver.Query(ctx, sql, nil); err != nil { + t.Fatalf("[%s] Create table failed: %v (SQL: %s)", dialect, err, sql) + } + } + + // 4. ALTER TABLE: Add Column, Change Column, Rename Column, Drop Column, FK, Check, Index + alterReq := DiffRequest{ + Dialect: dialect, + Schema: schema, + TableName: tableName, + OldTableName: tableName, + Columns: []ColumnDefinition{ + {ID: "c1", Name: "id", Type: "int", Nullable: false, OriginalIndex: 0, Original: &ColumnDefinition{Name: "id", Type: "int", Nullable: false}}, + {ID: "c2", Name: "user_name", Type: "varchar", Length: 150.0, Nullable: true, OriginalIndex: 1, Original: &ColumnDefinition{Name: "name", Type: "varchar", Length: 100.0, Nullable: false}}, + {ID: "c3", Name: "status", Type: "varchar", Length: 20.0, Nullable: false, OriginalIndex: 2, DefaultValue: &defActive, Original: &ColumnDefinition{Name: "status", Type: "varchar", Length: 20.0, Nullable: true, DefaultValue: &defActive}}, + {ID: "c4", Name: "age", Type: "int", Nullable: true, OriginalIndex: 3}, + {ID: "c5", Name: "ref_id", Type: "int", Nullable: true, OriginalIndex: 4}, + }, + ForeignKeys: []ForeignKeyDefinition{ + {Name: "fk_ref_id", Columns: []string{"ref_id"}, ReferencedTable: refTableName, ReferencedColumns: []string{"id"}, OnDelete: "CASCADE"}, + }, + CheckConstraints: []CheckConstraintDefinition{ + {Name: "chk_age", Expression: "age >= 0"}, + }, + Indexes: []IndexDefinition{ + {Name: "idx_status", Columns: []string{"status"}, IsUnique: false}, + }, + } + + sqls := compiler.GenerateAlterTableSql(alterReq) + for _, sql := range sqls { + fmt.Printf("[%s Test] Executing Alter: %s\n", dialect, sql) + if _, err := driver.Query(ctx, sql, nil); err != nil { + t.Errorf("[%s] Alter failed: %v (SQL: %s)", dialect, err, sql) + } + } + + // --- VERIFY ALTER RESULTS --- + verifyColSql := fmt.Sprintf("SELECT column_name, is_nullable FROM information_schema.columns WHERE table_schema = '%s' AND table_name = '%s';", schema, tableName) + res, err := driver.Query(ctx, verifyColSql, nil) + if err != nil { + t.Errorf("[%s] Verify columns failed: %v", dialect, err) + } else { + foundCols := make(map[string]bool) + for _, row := range res.Rows { + var colName, isNullable string + for k, v := range row { + if k == "column_name" { + colName = v.(string) + } + if k == "is_nullable" { + isNullable = v.(string) + } + } + foundCols[colName] = true + if colName == "user_name" && isNullable != "YES" { + t.Errorf("[%s] Column user_name should be nullable", dialect) + } + if colName == "status" && isNullable != "NO" { + t.Errorf("[%s] Column status should be NOT NULL", dialect) + } + } + expectedCols := []string{"id", "user_name", "status", "age", "ref_id"} + for _, ec := range expectedCols { + if !foundCols[ec] { + t.Errorf("[%s] Expected column %s not found", dialect, ec) + } + } + } + + // 2. Verify Data Insertion + insertSql := fmt.Sprintf("INSERT INTO %s.%s (id, user_name, status, age) VALUES (1, 'test', 'active', 25);", schema, tableName) + if _, err := driver.Query(ctx, insertSql, nil); err != nil { + t.Errorf("[%s] Valid insert failed: %v", dialect, err) + } + + invalidInsertSql := fmt.Sprintf("INSERT INTO %s.%s (id, user_name, status, age) VALUES (2, 'fail', 'active', -1);", schema, tableName) + if _, err := driver.Query(ctx, invalidInsertSql, nil); err == nil { + t.Errorf("[%s] Check constraint 'chk_age' failed to block negative age", dialect) + } + + // 5. CREATE VIEW + viewReq := DiffRequest{ + Dialect: dialect, Schema: schema, + Views: []ViewDefinition{ + {Name: viewName, Definition: fmt.Sprintf("SELECT id, user_name FROM %s.%s", schema, tableName)}, + }, + } + for _, sql := range compiler.GenerateCreateViewSql(viewReq) { + if _, err := driver.Query(ctx, sql, nil); err != nil { + t.Errorf("[%s] Create View failed: %v", dialect, err) + } + } + verifyViewSql := fmt.Sprintf("SELECT * FROM %s.%s WHERE id = 1;", schema, viewName) + viewRes, err := driver.Query(ctx, verifyViewSql, nil) + if err != nil || len(viewRes.Rows) == 0 { + t.Errorf("[%s] View verification failed: %v", dialect, err) + } + + // 6. CREATE TRIGGER + funcSql := "CREATE OR REPLACE FUNCTION test_func() RETURNS trigger AS $$ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql;" + driver.Query(ctx, funcSql, nil) + trigReq := DiffRequest{ + Dialect: dialect, Schema: schema, + Triggers: []TriggerDefinition{ + {Name: "trig_test", Timing: "BEFORE", Event: "INSERT", TableName: tableName, Definition: "EXECUTE FUNCTION test_func()"}, + }, + } + for _, sql := range compiler.GenerateCreateTriggerSql(trigReq) { + if _, err := driver.Query(ctx, sql, nil); err != nil { + t.Errorf("[%s] Create Trigger failed: %v (SQL: %s)", dialect, err, sql) + } + } + + // 7. ROUTINES + routReq := DiffRequest{ + Dialect: dialect, Schema: schema, + Routines: []RoutineDefinition{ + {Name: "test_func2", Type: "FUNCTION", Definition: "CREATE OR REPLACE FUNCTION test_func2() RETURNS int AS $$ BEGIN RETURN 1; END; $$ LANGUAGE plpgsql;"}, + }, + } + for _, sql := range compiler.GenerateCreateRoutineSql(routReq) { + if _, err := driver.Query(ctx, sql, nil); err != nil { + t.Errorf("[%s] Create Routine failed: %v (SQL: %s)", dialect, err, sql) + } + } + + t.Logf("[%s] All scenarios passed and verified", dialect) +} diff --git a/backend/internal/ast/diff_integration_test.go b/backend/internal/ast/diff_integration_test.go deleted file mode 100644 index ba429ca..0000000 --- a/backend/internal/ast/diff_integration_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package ast - -import ( - "context" - "fmt" - "os" - "testing" - "time" - "vstable-engine/internal/db" -) - -func runCommonIntegrationTest(t *testing.T, dialect string, dsn string, compiler Compiler) { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - defer cancel() - - dbManager := db.NewManager() - err := dbManager.Connect(ctx, "test-session", dialect, dsn) - if err != nil { - t.Fatalf("[%s] Connection failed: %v", dialect, err) - } - defer dbManager.Disconnect("test-session") - driver, _ := dbManager.Get("test-session") - - tableName := fmt.Sprintf("sync_test_%s", dialect) - schema := "public" - if dialect == "mysql" { - schema = "vstable_test" - } - - // 1. Cleanup - cleanupSql := fmt.Sprintf("DROP TABLE IF EXISTS %s;", tableName) - if dialect == "pg" { - cleanupSql = fmt.Sprintf("DROP TABLE IF EXISTS %s.%s CASCADE;", schema, tableName) - } - driver.Query(ctx, cleanupSql, nil) - - // 2. CREATE: (id, name, status) - // - name: 初始 NOT NULL - // - status: 初始 NULL - defActive := "active" - createReq := DiffRequest{ - Dialect: dialect, - Schema: schema, - TableName: tableName, - Columns: []ColumnDefinition{ - {ID: "c1", Name: "id", Type: "int", Nullable: false, IsPrimaryKey: true, OriginalIndex: 0}, - {ID: "c2", Name: "name", Type: "varchar", Length: 100.0, Nullable: false, OriginalIndex: 1, Comment: "Initial NOT NULL"}, - {ID: "c3", Name: "status", Type: "varchar", Length: 20.0, Nullable: true, OriginalIndex: 2, DefaultValue: &defActive}, - }, - } - for _, sql := range compiler.GenerateCreateTableSql(createReq) { - if _, err := driver.Query(ctx, sql, nil); err != nil { - t.Fatalf("[%s] Create failed: %v (SQL: %s)", dialect, err, sql) - } - } - - // 3. ALTER: Nullable 切换测试 - // - name: NOT NULL -> NULL (Allow Null) - // - status: NULL -> NOT NULL (Restrict Null) - alterReq := DiffRequest{ - Dialect: dialect, - Schema: schema, - TableName: tableName, - OldTableName: tableName, - Columns: []ColumnDefinition{ - {ID: "c1", Name: "id", Type: "int", Nullable: false, OriginalIndex: 0, Original: &ColumnDefinition{Name: "id", Type: "int", Nullable: false}}, - // name: 变为 NULL - {ID: "c2", Name: "name", Type: "varchar", Length: 100.0, Nullable: true, OriginalIndex: 1, Original: &ColumnDefinition{Name: "name", Type: "varchar", Length: 100.0, Nullable: false}}, - // status: 变为 NOT NULL - {ID: "c3", Name: "status", Type: "varchar", Length: 20.0, Nullable: false, OriginalIndex: 2, DefaultValue: &defActive, Original: &ColumnDefinition{Name: "status", Type: "varchar", Length: 20.0, Nullable: true, DefaultValue: &defActive}}, - }, - } - - sqls := compiler.GenerateAlterTableSql(alterReq) - for _, sql := range sqls { - fmt.Printf("[%s Test] Executing Nullable Alter: %s\n", dialect, sql) - if _, err := driver.Query(ctx, sql, nil); err != nil { - t.Errorf("[%s] Nullable Alter failed: %v (SQL: %s)", dialect, err, sql) - } - } - - t.Logf("[%s] Nullable transition test passed", dialect) -} - -func TestPostgresSyncIntegration(t *testing.T) { - dsn := os.Getenv("PG_DSN") - if dsn == "" { - dsn = "postgres://root:password@localhost:5433/vstable_test?sslmode=disable" - } - runCommonIntegrationTest(t, "pg", dsn, &PGCompiler{}) -} - -func TestMysqlSyncIntegration(t *testing.T) { - dsn := os.Getenv("MYSQL_DSN") - if dsn == "" { - dsn = "root:password@tcp(127.0.0.1:3307)/vstable_test?charset=utf8mb4&parseTime=True&loc=Local" - } - runCommonIntegrationTest(t, "mysql", dsn, &MysqlCompiler{}) -} diff --git a/backend/internal/ast/types.go b/backend/internal/ast/types.go index 5659c45..a3f107f 100644 --- a/backend/internal/ast/types.go +++ b/backend/internal/ast/types.go @@ -28,6 +28,57 @@ type IndexDefinition struct { Original *IndexDefinition `json:"_original"` } +type ForeignKeyDefinition struct { + ID string `json:"id"` + Name string `json:"name"` + Columns []string `json:"columns"` + ReferencedTable string `json:"referencedTable"` + ReferencedColumns []string `json:"referencedColumns"` + OnDelete string `json:"onDelete"` + OnUpdate string `json:"onUpdate"` + Original *ForeignKeyDefinition `json:"_original"` +} + +type CheckConstraintDefinition struct { + ID string `json:"id"` + Name string `json:"name"` + Expression string `json:"expression"` + Original *CheckConstraintDefinition `json:"_original"` +} + +type ViewDefinition struct { + ID string `json:"id"` + Name string `json:"name"` + Definition string `json:"definition"` + Original *ViewDefinition `json:"_original"` +} + +type TriggerDefinition struct { + ID string `json:"id"` + Name string `json:"name"` + Timing string `json:"timing"` + Event string `json:"event"` + TableName string `json:"tableName"` + Definition string `json:"definition"` + Enabled bool `json:"enabled"` + Original *TriggerDefinition `json:"_original"` +} + +type RoutineDefinition struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` // PROCEDURE or FUNCTION + Definition string `json:"definition"` + Original *RoutineDefinition `json:"_original"` +} + +type DatabaseConfig struct { + Charset string `json:"charset"` + Collation string `json:"collation"` + Engine string `json:"engine"` + AutoIncrementOffset int `json:"autoIncrementOffset"` +} + type DiffRequest struct { Dialect string `json:"dialect"` Schema string `json:"schema"` @@ -37,6 +88,22 @@ type DiffRequest struct { Indexes []IndexDefinition `json:"indexes"` DeletedColumns []ColumnDefinition `json:"deletedColumns"` DeletedIndexes []IndexDefinition `json:"deletedIndexes"` + + ForeignKeys []ForeignKeyDefinition `json:"foreignKeys"` + DeletedForeignKeys []ForeignKeyDefinition `json:"deletedForeignKeys"` + CheckConstraints []CheckConstraintDefinition `json:"checkConstraints"` + DeletedChecks []CheckConstraintDefinition `json:"deletedChecks"` + + Views []ViewDefinition `json:"views"` + DeletedViews []ViewDefinition `json:"deletedViews"` + + Triggers []TriggerDefinition `json:"triggers"` + DeletedTriggers []TriggerDefinition `json:"deletedTriggers"` + + Routines []RoutineDefinition `json:"routines"` + DeletedRoutines []RoutineDefinition `json:"deletedRoutines"` + + Config *DatabaseConfig `json:"config"` } type DiffResponse struct { diff --git a/backend/main.go b/backend/main.go index 4bb3cb1..e9f8071 100644 --- a/backend/main.go +++ b/backend/main.go @@ -156,4 +156,4 @@ func sendJSON(w http.ResponseWriter, status int, resp interface{}) { func sendError(w http.ResponseWriter, status int, err error) { sendJSON(w, status, Response{Success: false, Error: err.Error()}) -} \ No newline at end of file +} diff --git a/frontend/docker-compose.yml b/frontend/docker-compose.yml index 39fb2e6..c118838 100644 --- a/frontend/docker-compose.yml +++ b/frontend/docker-compose.yml @@ -21,7 +21,7 @@ services: ports: - "3307:3306" healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"] interval: 5s timeout: 5s retries: 10