diff --git a/cmd/generate.go b/cmd/generate.go index 368d6da..ad0f001 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -487,7 +487,7 @@ var ( modelContent = "" ) -//nolint:cyclop +//nolint:cyclop,gocognit func generateMigrationStatements( ctx context.Context, config *configuration.Config, @@ -501,25 +501,41 @@ func generateMigrationStatements( ) (string, error) { log.Println("Generating migration statements") - err := internal.PgModelerExportToFile( - ctx, - filepath.Join(wd, fmt.Sprintf("%s.dbm", config.ModelName)), - filepath.Join(wd, fmt.Sprintf("%s.sql", config.ModelName)), - ) + dbmPath := filepath.Join(wd, fmt.Sprintf("%s.dbm", config.ModelName)) + // Generate SQL file in tmpDir for internal use during migration generation + tmpSQLPath := filepath.Join(tmpDir, fmt.Sprintf("%s.sql", config.ModelName)) + + err := internal.PgmodelerExportSQL(ctx, dbmPath, tmpSQLPath) if err != nil { return "", fmt.Errorf("failed to export model: %w", err) } - go func() { - err = internal.PgModelerExportToPng( - ctx, - filepath.Join(wd, fmt.Sprintf("%s.dbm", config.ModelName)), - filepath.Join(wd, fmt.Sprintf("%s.png", config.ModelName)), - ) + // Copy SQL to output path if enabled + if sqlPath := config.GetOutputPath("sql"); sqlPath != "" { + var sqlContent []byte + sqlContent, err = os.ReadFile(tmpSQLPath) + if err != nil { + return "", fmt.Errorf("failed to read sql file: %w", err) + } + err = os.WriteFile(filepath.Join(wd, sqlPath), sqlContent, 0o644) //nolint:gosec + if err != nil { + return "", fmt.Errorf("failed to write sql output file: %w", err) + } + } + + if pngPath := config.GetOutputPath("png"); pngPath != "" { + err = internal.PgmodelerExportPNG(ctx, dbmPath, filepath.Join(wd, pngPath)) + if err != nil { + return "", fmt.Errorf("failed to export png: %w", err) + } + } + + if svgPath := config.GetOutputPath("svg"); svgPath != "" { + err = internal.PgmodelerExportSVG(ctx, dbmPath, filepath.Join(wd, svgPath)) if err != nil { - log.Printf("Failed to export png: %v\n", err) + return "", fmt.Errorf("failed to export svg: %w", err) } - }() + } for _, role := range config.Roles { _, err = targetConn.Exec(ctx, fmt.Sprintf("CREATE ROLE %q WITH LOGIN;", role.Name)) @@ -528,28 +544,20 @@ func generateMigrationStatements( } } - err = executeTargetSQL(ctx, config, wd, targetConn) + err = executeTargetSQL(ctx, tmpSQLPath, targetConn) if err != nil { return "", fmt.Errorf("failed to execute target sql: %w", err) } - if initial { - // If we are developing the schema initially, there will be no diffs, - // and we want to copy over the schema file to the initial migration file - var input []byte - input, err = os.ReadFile(filepath.Join(wd, fmt.Sprintf("%s.sql", config.ModelName))) + // Apply existing migrations to the migrate database (skip if no migrations exist yet) + if !initial { + err = executeMigrateSQL(migrationsDir, migrateConn) if err != nil { - return "", fmt.Errorf("failed to read sql file: %w", err) + return "", fmt.Errorf("failed to execute migrate sql: %w", err) } - - return string(input), nil - } - - err = executeMigrateSQL(migrationsDir, migrateConn) - if err != nil { - return "", fmt.Errorf("failed to execute migrate sql: %w", err) } + // Generate diff between migrate database (with existing migrations) and target database (with full schema) statements, err := internal.Diff( ctx, postgresConn, @@ -595,8 +603,8 @@ func executeMigrateSQL(migrationsDir string, migrateConn *pgx.Conn) error { return nil } -func executeTargetSQL(ctx context.Context, config *configuration.Config, wd string, targetConn *pgx.Conn) error { - targetSQL, err := os.ReadFile(filepath.Join(wd, fmt.Sprintf("%s.sql", config.ModelName))) +func executeTargetSQL(ctx context.Context, sqlPath string, targetConn *pgx.Conn) error { + targetSQL, err := os.ReadFile(sqlPath) if err != nil { return fmt.Errorf("failed to read target sql: %w", err) } diff --git a/example/foo.sql b/example/foo.gen.sql similarity index 100% rename from example/foo.sql rename to example/foo.gen.sql diff --git a/example/foo.gen.svg b/example/foo.gen.svg new file mode 100644 index 0000000..9690861 --- /dev/null +++ b/example/foo.gen.svg @@ -0,0 +1,52 @@ + + +SVG representation of database model +SVG file generated by pgModeler + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/foo.png b/example/foo.png deleted file mode 100644 index 54c13c7..0000000 Binary files a/example/foo.png and /dev/null differ diff --git a/example/migrations/001_init.up.sql b/example/migrations/001_init.up.sql index f46bd93..e69de29 100644 --- a/example/migrations/001_init.up.sql +++ b/example/migrations/001_init.up.sql @@ -1,10 +0,0 @@ --- ** Database generated with pgModeler (PostgreSQL Database Modeler). --- ** pgModeler version: 1.2.2 --- ** PostgreSQL version: 18.0 --- ** Project Site: pgmodeler.io --- ** Model Author: --- - -SET search_path TO pg_catalog,public; --- ddl-end -- - - diff --git a/example/trek.yaml b/example/trek.yaml index 22a1c9d..96b7bc2 100644 --- a/example/trek.yaml +++ b/example/trek.yaml @@ -3,3 +3,6 @@ db_name: bar roles: - name: alice - name: bob +output: + sql: {} + svg: {} diff --git a/internal/configuration/config.go b/internal/configuration/config.go index 394b60b..54eea68 100644 --- a/internal/configuration/config.go +++ b/internal/configuration/config.go @@ -24,7 +24,9 @@ type Config struct { //nolint:tagliatelle Roles []Role `yaml:"roles"` Templates []Template `yaml:"templates"` + Output *Output `yaml:"output"` } + type Role struct { Name string `yaml:"name"` } @@ -34,6 +36,16 @@ type Template struct { Content string `yaml:"content"` } +type OutputFile struct { + Path string `yaml:"path"` +} + +type Output struct { + SQL *OutputFile `yaml:"sql"` + PNG *OutputFile `yaml:"png"` + SVG *OutputFile `yaml:"svg"` +} + func ReadConfig(wd string) (*Config, error) { var config *Config file, err := os.ReadFile(filepath.Join(wd, "trek.yaml")) @@ -85,6 +97,38 @@ func (c *Config) validate() (problems []string) { return problems } +// GetOutputPath returns the output path for the given type if enabled, or empty string if not. +// The outputType must be one of: "sql", "png", "svg". Panics if an invalid outputType is provided. +func (c *Config) GetOutputPath(outputType string) string { + if c.Output == nil { + return "" + } + + var outputFile *OutputFile + + switch outputType { + case "sql": + outputFile = c.Output.SQL + case "png": + outputFile = c.Output.PNG + case "svg": + outputFile = c.Output.SVG + default: + panic(fmt.Sprintf("invalid output type: %q", outputType)) + } + + if outputFile == nil { + return "" + } + + if outputFile.Path != "" { + return outputFile.Path + } + + // Default path: {model_name}.gen.{ext} + return fmt.Sprintf("%s.gen.%s", c.ModelName, outputType) +} + func ValidateIdentifier(identifier string) bool { return regexpValidIdentifier.MatchString(identifier) } diff --git a/internal/pgmodeler.go b/internal/pgmodeler.go index e70cd7f..db2f3b8 100644 --- a/internal/pgmodeler.go +++ b/internal/pgmodeler.go @@ -7,7 +7,7 @@ import ( "os/exec" ) -func PgModelerExportToFile(ctx context.Context, input, output string) error { +func PgmodelerExportSQL(ctx context.Context, input, output string) error { //nolint:gosec err := os.WriteFile(output, []byte{}, 0o644) if err != nil { @@ -35,7 +35,7 @@ func PgModelerExportToFile(ctx context.Context, input, output string) error { return nil } -func PgModelerExportToPng(ctx context.Context, input, output string) error { +func PgmodelerExportPNG(ctx context.Context, input, output string) error { //nolint:gosec err := os.WriteFile(output, []byte{}, 0o644) if err != nil { @@ -60,3 +60,29 @@ func PgModelerExportToPng(ctx context.Context, input, output string) error { return nil } + +func PgmodelerExportSVG(ctx context.Context, input, output string) error { + //nolint:gosec + err := os.WriteFile(output, []byte{}, 0o644) + if err != nil { + return fmt.Errorf("failed to create output svg: %w", err) + } + //nolint:gosec + cmdPgModeler := exec.CommandContext( + ctx, + "pgmodeler-cli", + "--input", + input, + "--export-to-svg", + "--output", + output, + ) + cmdPgModeler.Stderr = os.Stderr + + out, err := cmdPgModeler.Output() + if err != nil { + return fmt.Errorf("failed to run pgmodeler: %w %s", err, string(out)) + } + + return nil +} diff --git a/internal/templates/trek.yaml.tmpl b/internal/templates/trek.yaml.tmpl index 8e59a24..a9ac737 100755 --- a/internal/templates/trek.yaml.tmpl +++ b/internal/templates/trek.yaml.tmpl @@ -2,3 +2,6 @@ model_name: {{.model_name}} db_name: {{.db_name}} roles:{{range .roleNames}} - name: {{.}}{{end}} +output: + sql: {} + svg: {} diff --git a/tests/output/migrations/001_init.up.sql b/tests/output/migrations/001_init.up.sql index f46bd93..e69de29 100644 --- a/tests/output/migrations/001_init.up.sql +++ b/tests/output/migrations/001_init.up.sql @@ -1,10 +0,0 @@ --- ** Database generated with pgModeler (PostgreSQL Database Modeler). --- ** pgModeler version: 1.2.2 --- ** PostgreSQL version: 18.0 --- ** Project Site: pgmodeler.io --- ** Model Author: --- - -SET search_path TO pg_catalog,public; --- ddl-end -- - - diff --git a/tests/output/santas_warehouse.sql b/tests/output/santas_warehouse.gen.sql similarity index 100% rename from tests/output/santas_warehouse.sql rename to tests/output/santas_warehouse.gen.sql diff --git a/tests/output/santas_warehouse.gen.svg b/tests/output/santas_warehouse.gen.svg new file mode 100644 index 0000000..8f3448c --- /dev/null +++ b/tests/output/santas_warehouse.gen.svg @@ -0,0 +1,1408 @@ + + +SVG representation of database model +SVG file generated by pgModeler + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +warehouse + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +factory + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +id + + + + + + + bigint + + + + + + +« pk » + + + + + + + + + + + + + + + + + + + +name + + + + + + + text + + + + + + +« nn » + + + + + + + + + + + + + + + + + + + +toys_produced + + + + + + + bigint + + + + + + +« nn » + + + + + + + + + + + + + + + + + + + + + + + + + +machines_pk + + + + + + + constraint + + + + + + +« pk » + + + + + + + + + + + + + + + + + + + +toys_produced_increase + + + + + + + trigger + + + + + + +« b u » + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +machines + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +id + + + + + + + bigint + + + + + + +« pk » + + + + + + + + + + + + + + + + + + + +shelf + + + + + + + bigint + + + + + + +« nn » + + + + + + + + + + + + + + + + + + + +total_capacity + + + + + + + bigint + + + + + + +« nn » + + + + + + + + + + + + + + + + + + + +used_capacity + + + + + + + bigint + + + + + + +« nn » + + + + + + + + + + + + + + + + + + + +current_toy_type + + + + + + + text + + + + + + +« nn » + + + + + + + + + + + + + + + + + + + + + + + + + +storage_locations_pk + + + + + + + constraint + + + + + + +« pk » + + + + + + + + + + + + + + + + + + + +ck_capacity + + + + + + + constraint + + + + + + +« ck » + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +storage_locations + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/output/santas_warehouse.png b/tests/output/santas_warehouse.png deleted file mode 100644 index 4cd0644..0000000 Binary files a/tests/output/santas_warehouse.png and /dev/null differ diff --git a/tests/output/trek.yaml b/tests/output/trek.yaml index 2d3f4b9..0c0c874 100644 --- a/tests/output/trek.yaml +++ b/tests/output/trek.yaml @@ -3,3 +3,6 @@ db_name: north_pole roles: - name: santa - name: worker +output: + sql: {} + svg: {}