From 5fb09b78c337b6becc168c89a0ccd1b257151a07 Mon Sep 17 00:00:00 2001 From: Hein Date: Thu, 5 Feb 2026 14:07:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(relations):=20=F0=9F=8E=89=20add=20flatten?= =?UTF-8?q?=20schema=20option=20for=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduce `--flatten-schema` flag to convert, merge, and split commands. * Modify database writing functions to support flattened schema names. * Update template functions to handle schema.table naming convention. * Enhance PostgreSQL writer to utilize flattened schema in generated SQL. * Update tests to ensure compatibility with new flattening feature. * Dependencies updated for improved functionality. --- cmd/relspec/convert.go | 25 ++++++---- cmd/relspec/merge.go | 30 +++++------ cmd/relspec/split.go | 3 +- pkg/writers/bun/template_data.go | 7 +-- pkg/writers/bun/writer.go | 4 +- pkg/writers/gorm/template_data.go | 7 +-- pkg/writers/gorm/writer.go | 4 +- pkg/writers/pgsql/migration_writer.go | 2 +- pkg/writers/pgsql/migration_writer_test.go | 4 +- pkg/writers/pgsql/template_functions_test.go | 2 +- pkg/writers/pgsql/templates.go | 29 ++++++++++- pkg/writers/pgsql/templates/add_column.tmpl | 2 +- .../templates/add_column_with_check.tmpl | 2 +- .../pgsql/templates/alter_column_default.tmpl | 4 +- .../pgsql/templates/alter_column_type.tmpl | 2 +- .../pgsql/templates/audit_function.tmpl | 4 +- .../pgsql/templates/audit_trigger.tmpl | 6 +-- .../pgsql/templates/base_constraint.tmpl | 4 +- pkg/writers/pgsql/templates/base_ddl.tmpl | 4 +- .../pgsql/templates/comment_column.tmpl | 2 +- .../pgsql/templates/comment_table.tmpl | 2 +- .../templates/create_check_constraint.tmpl | 2 +- .../pgsql/templates/create_foreign_key.tmpl | 6 +-- .../create_foreign_key_with_check.tmpl | 4 +- pkg/writers/pgsql/templates/create_index.tmpl | 2 +- .../pgsql/templates/create_primary_key.tmpl | 2 +- ...create_primary_key_with_autogen_check.tmpl | 4 +- .../pgsql/templates/create_sequence.tmpl | 2 +- pkg/writers/pgsql/templates/create_table.tmpl | 2 +- .../templates/create_unique_constraint.tmpl | 2 +- .../pgsql/templates/drop_constraint.tmpl | 2 +- pkg/writers/pgsql/templates/drop_index.tmpl | 2 +- pkg/writers/pgsql/templates/fragments.tmpl | 2 +- .../pgsql/templates/set_sequence_value.tmpl | 4 +- pkg/writers/pgsql/writer.go | 50 ++++++++++--------- pkg/writers/writer.go | 19 +++++++ 36 files changed, 151 insertions(+), 103 deletions(-) diff --git a/cmd/relspec/convert.go b/cmd/relspec/convert.go index a9814c9..9921f97 100644 --- a/cmd/relspec/convert.go +++ b/cmd/relspec/convert.go @@ -38,13 +38,14 @@ import ( ) var ( - convertSourceType string - convertSourcePath string - convertSourceConn string - convertTargetType string - convertTargetPath string - convertPackageName string - convertSchemaFilter string + convertSourceType string + convertSourcePath string + convertSourceConn string + convertTargetType string + convertTargetPath string + convertPackageName string + convertSchemaFilter string + convertFlattenSchema bool ) var convertCmd = &cobra.Command{ @@ -148,6 +149,7 @@ func init() { convertCmd.Flags().StringVar(&convertTargetPath, "to-path", "", "Target output path (file or directory)") convertCmd.Flags().StringVar(&convertPackageName, "package", "", "Package name (for code generation formats like gorm/bun)") convertCmd.Flags().StringVar(&convertSchemaFilter, "schema", "", "Filter to a specific schema by name (required for formats like dctx that only support single schemas)") + convertCmd.Flags().BoolVar(&convertFlattenSchema, "flatten-schema", false, "Flatten schema.table names to schema_table (useful for databases like SQLite that do not support schemas)") err := convertCmd.MarkFlagRequired("from") if err != nil { @@ -202,7 +204,7 @@ func runConvert(cmd *cobra.Command, args []string) error { fmt.Fprintf(os.Stderr, " Schema: %s\n", convertSchemaFilter) } - if err := writeDatabase(db, convertTargetType, convertTargetPath, convertPackageName, convertSchemaFilter); err != nil { + if err := writeDatabase(db, convertTargetType, convertTargetPath, convertPackageName, convertSchemaFilter, convertFlattenSchema); err != nil { return fmt.Errorf("failed to write target: %w", err) } @@ -301,12 +303,13 @@ func readDatabaseForConvert(dbType, filePath, connString string) (*models.Databa return db, nil } -func writeDatabase(db *models.Database, dbType, outputPath, packageName, schemaFilter string) error { +func writeDatabase(db *models.Database, dbType, outputPath, packageName, schemaFilter string, flattenSchema bool) error { var writer writers.Writer writerOpts := &writers.WriterOptions{ - OutputPath: outputPath, - PackageName: packageName, + OutputPath: outputPath, + PackageName: packageName, + FlattenSchema: flattenSchema, } switch strings.ToLower(dbType) { diff --git a/cmd/relspec/merge.go b/cmd/relspec/merge.go index f494fad..e560fd1 100644 --- a/cmd/relspec/merge.go +++ b/cmd/relspec/merge.go @@ -56,6 +56,7 @@ var ( mergeSkipTables string // Comma-separated table names to skip mergeVerbose bool mergeReportPath string // Path to write merge report + mergeFlattenSchema bool ) var mergeCmd = &cobra.Command{ @@ -123,6 +124,7 @@ func init() { mergeCmd.Flags().StringVar(&mergeSkipTables, "skip-tables", "", "Comma-separated list of table names to skip during merge") mergeCmd.Flags().BoolVar(&mergeVerbose, "verbose", false, "Show verbose output") mergeCmd.Flags().StringVar(&mergeReportPath, "merge-report", "", "Path to write merge report (JSON format)") + mergeCmd.Flags().BoolVar(&mergeFlattenSchema, "flatten-schema", false, "Flatten schema.table names to schema_table (useful for databases like SQLite that do not support schemas)") } func runMerge(cmd *cobra.Command, args []string) error { @@ -237,7 +239,7 @@ func runMerge(cmd *cobra.Command, args []string) error { fmt.Fprintf(os.Stderr, " Path: %s\n", mergeOutputPath) } - err = writeDatabaseForMerge(mergeOutputType, mergeOutputPath, mergeOutputConn, targetDB, "Output") + err = writeDatabaseForMerge(mergeOutputType, mergeOutputPath, mergeOutputConn, targetDB, "Output", mergeFlattenSchema) if err != nil { return fmt.Errorf("failed to write output: %w", err) } @@ -324,7 +326,7 @@ func readDatabaseForMerge(dbType, filePath, connString, label string) (*models.D return db, nil } -func writeDatabaseForMerge(dbType, filePath, connString string, db *models.Database, label string) error { +func writeDatabaseForMerge(dbType, filePath, connString string, db *models.Database, label string, flattenSchema bool) error { var writer writers.Writer switch strings.ToLower(dbType) { @@ -332,59 +334,59 @@ func writeDatabaseForMerge(dbType, filePath, connString string, db *models.Datab if filePath == "" { return fmt.Errorf("%s: file path is required for DBML format", label) } - writer = wdbml.NewWriter(&writers.WriterOptions{OutputPath: filePath}) + writer = wdbml.NewWriter(&writers.WriterOptions{OutputPath: filePath, FlattenSchema: flattenSchema}) case "dctx": if filePath == "" { return fmt.Errorf("%s: file path is required for DCTX format", label) } - writer = wdctx.NewWriter(&writers.WriterOptions{OutputPath: filePath}) + writer = wdctx.NewWriter(&writers.WriterOptions{OutputPath: filePath, FlattenSchema: flattenSchema}) case "drawdb": if filePath == "" { return fmt.Errorf("%s: file path is required for DrawDB format", label) } - writer = wdrawdb.NewWriter(&writers.WriterOptions{OutputPath: filePath}) + writer = wdrawdb.NewWriter(&writers.WriterOptions{OutputPath: filePath, FlattenSchema: flattenSchema}) case "graphql": if filePath == "" { return fmt.Errorf("%s: file path is required for GraphQL format", label) } - writer = wgraphql.NewWriter(&writers.WriterOptions{OutputPath: filePath}) + writer = wgraphql.NewWriter(&writers.WriterOptions{OutputPath: filePath, FlattenSchema: flattenSchema}) case "json": if filePath == "" { return fmt.Errorf("%s: file path is required for JSON format", label) } - writer = wjson.NewWriter(&writers.WriterOptions{OutputPath: filePath}) + writer = wjson.NewWriter(&writers.WriterOptions{OutputPath: filePath, FlattenSchema: flattenSchema}) case "yaml": if filePath == "" { return fmt.Errorf("%s: file path is required for YAML format", label) } - writer = wyaml.NewWriter(&writers.WriterOptions{OutputPath: filePath}) + writer = wyaml.NewWriter(&writers.WriterOptions{OutputPath: filePath, FlattenSchema: flattenSchema}) case "gorm": if filePath == "" { return fmt.Errorf("%s: file path is required for GORM format", label) } - writer = wgorm.NewWriter(&writers.WriterOptions{OutputPath: filePath}) + writer = wgorm.NewWriter(&writers.WriterOptions{OutputPath: filePath, FlattenSchema: flattenSchema}) case "bun": if filePath == "" { return fmt.Errorf("%s: file path is required for Bun format", label) } - writer = wbun.NewWriter(&writers.WriterOptions{OutputPath: filePath}) + writer = wbun.NewWriter(&writers.WriterOptions{OutputPath: filePath, FlattenSchema: flattenSchema}) case "drizzle": if filePath == "" { return fmt.Errorf("%s: file path is required for Drizzle format", label) } - writer = wdrizzle.NewWriter(&writers.WriterOptions{OutputPath: filePath}) + writer = wdrizzle.NewWriter(&writers.WriterOptions{OutputPath: filePath, FlattenSchema: flattenSchema}) case "prisma": if filePath == "" { return fmt.Errorf("%s: file path is required for Prisma format", label) } - writer = wprisma.NewWriter(&writers.WriterOptions{OutputPath: filePath}) + writer = wprisma.NewWriter(&writers.WriterOptions{OutputPath: filePath, FlattenSchema: flattenSchema}) case "typeorm": if filePath == "" { return fmt.Errorf("%s: file path is required for TypeORM format", label) } - writer = wtypeorm.NewWriter(&writers.WriterOptions{OutputPath: filePath}) + writer = wtypeorm.NewWriter(&writers.WriterOptions{OutputPath: filePath, FlattenSchema: flattenSchema}) case "pgsql": - writerOpts := &writers.WriterOptions{OutputPath: filePath} + writerOpts := &writers.WriterOptions{OutputPath: filePath, FlattenSchema: flattenSchema} if connString != "" { writerOpts.Metadata = map[string]interface{}{ "connection_string": connString, diff --git a/cmd/relspec/split.go b/cmd/relspec/split.go index 65d32d2..2101768 100644 --- a/cmd/relspec/split.go +++ b/cmd/relspec/split.go @@ -183,7 +183,8 @@ func runSplit(cmd *cobra.Command, args []string) error { splitTargetType, splitTargetPath, splitPackageName, - "", // no schema filter for split + "", // no schema filter for split + false, // no flatten-schema for split ) if err != nil { return fmt.Errorf("failed to write output: %w", err) diff --git a/pkg/writers/bun/template_data.go b/pkg/writers/bun/template_data.go index 5d0a1be..74e46e7 100644 --- a/pkg/writers/bun/template_data.go +++ b/pkg/writers/bun/template_data.go @@ -106,11 +106,8 @@ func (td *TemplateData) FinalizeImports() { } // NewModelData creates a new ModelData from a models.Table -func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper) *ModelData { - tableName := table.Name - if schema != "" { - tableName = schema + "." + table.Name - } +func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper, flattenSchema bool) *ModelData { + tableName := writers.QualifiedTableName(schema, table.Name, flattenSchema) // Generate model name: Model + Schema + Table (all PascalCase) singularTable := Singularize(table.Name) diff --git a/pkg/writers/bun/writer.go b/pkg/writers/bun/writer.go index 647bf16..b036a1d 100644 --- a/pkg/writers/bun/writer.go +++ b/pkg/writers/bun/writer.go @@ -86,7 +86,7 @@ func (w *Writer) writeSingleFile(db *models.Database) error { // Collect all models for _, schema := range db.Schemas { for _, table := range schema.Tables { - modelData := NewModelData(table, schema.Name, w.typeMapper) + modelData := NewModelData(table, schema.Name, w.typeMapper, w.options.FlattenSchema) // Add relationship fields w.addRelationshipFields(modelData, table, schema, db) @@ -181,7 +181,7 @@ func (w *Writer) writeMultiFile(db *models.Database) error { templateData.AddImport(fmt.Sprintf("resolvespec_common \"%s\"", w.typeMapper.GetSQLTypesImport())) // Create model data - modelData := NewModelData(table, schema.Name, w.typeMapper) + modelData := NewModelData(table, schema.Name, w.typeMapper, w.options.FlattenSchema) // Add relationship fields w.addRelationshipFields(modelData, table, schema, db) diff --git a/pkg/writers/gorm/template_data.go b/pkg/writers/gorm/template_data.go index 84fc3a0..f269b52 100644 --- a/pkg/writers/gorm/template_data.go +++ b/pkg/writers/gorm/template_data.go @@ -105,11 +105,8 @@ func (td *TemplateData) FinalizeImports() { } // NewModelData creates a new ModelData from a models.Table -func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper) *ModelData { - tableName := table.Name - if schema != "" { - tableName = schema + "." + table.Name - } +func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper, flattenSchema bool) *ModelData { + tableName := writers.QualifiedTableName(schema, table.Name, flattenSchema) // Generate model name: Model + Schema + Table (all PascalCase) singularTable := Singularize(table.Name) diff --git a/pkg/writers/gorm/writer.go b/pkg/writers/gorm/writer.go index db7c14a..a35f368 100644 --- a/pkg/writers/gorm/writer.go +++ b/pkg/writers/gorm/writer.go @@ -83,7 +83,7 @@ func (w *Writer) writeSingleFile(db *models.Database) error { // Collect all models for _, schema := range db.Schemas { for _, table := range schema.Tables { - modelData := NewModelData(table, schema.Name, w.typeMapper) + modelData := NewModelData(table, schema.Name, w.typeMapper, w.options.FlattenSchema) // Add relationship fields w.addRelationshipFields(modelData, table, schema, db) @@ -175,7 +175,7 @@ func (w *Writer) writeMultiFile(db *models.Database) error { templateData.AddImport(fmt.Sprintf("sql_types \"%s\"", w.typeMapper.GetSQLTypesImport())) // Create model data - modelData := NewModelData(table, schema.Name, w.typeMapper) + modelData := NewModelData(table, schema.Name, w.typeMapper, w.options.FlattenSchema) // Add relationship fields w.addRelationshipFields(modelData, table, schema, db) diff --git a/pkg/writers/pgsql/migration_writer.go b/pkg/writers/pgsql/migration_writer.go index e1e8ded..fe1f23e 100644 --- a/pkg/writers/pgsql/migration_writer.go +++ b/pkg/writers/pgsql/migration_writer.go @@ -31,7 +31,7 @@ type MigrationWriter struct { // NewMigrationWriter creates a new templated migration writer func NewMigrationWriter(options *writers.WriterOptions) (*MigrationWriter, error) { - executor, err := NewTemplateExecutor() + executor, err := NewTemplateExecutor(options.FlattenSchema) if err != nil { return nil, fmt.Errorf("failed to create template executor: %w", err) } diff --git a/pkg/writers/pgsql/migration_writer_test.go b/pkg/writers/pgsql/migration_writer_test.go index 51d8f76..da976b9 100644 --- a/pkg/writers/pgsql/migration_writer_test.go +++ b/pkg/writers/pgsql/migration_writer_test.go @@ -137,7 +137,7 @@ func TestWriteMigration_WithAudit(t *testing.T) { } func TestTemplateExecutor_CreateTable(t *testing.T) { - executor, err := NewTemplateExecutor() + executor, err := NewTemplateExecutor(false) if err != nil { t.Fatalf("Failed to create executor: %v", err) } @@ -170,7 +170,7 @@ func TestTemplateExecutor_CreateTable(t *testing.T) { } func TestTemplateExecutor_AuditFunction(t *testing.T) { - executor, err := NewTemplateExecutor() + executor, err := NewTemplateExecutor(false) if err != nil { t.Fatalf("Failed to create executor: %v", err) } diff --git a/pkg/writers/pgsql/template_functions_test.go b/pkg/writers/pgsql/template_functions_test.go index d1034ad..656a401 100644 --- a/pkg/writers/pgsql/template_functions_test.go +++ b/pkg/writers/pgsql/template_functions_test.go @@ -314,7 +314,7 @@ func TestFormatType(t *testing.T) { // Test that template functions work in actual templates func TestTemplateFunctionsInTemplate(t *testing.T) { - executor, err := NewTemplateExecutor() + executor, err := NewTemplateExecutor(false) if err != nil { t.Fatalf("Failed to create executor: %v", err) } diff --git a/pkg/writers/pgsql/templates.go b/pkg/writers/pgsql/templates.go index eb73f3a..38cf485 100644 --- a/pkg/writers/pgsql/templates.go +++ b/pkg/writers/pgsql/templates.go @@ -18,14 +18,39 @@ type TemplateExecutor struct { templates *template.Template } -// NewTemplateExecutor creates a new template executor -func NewTemplateExecutor() (*TemplateExecutor, error) { +// NewTemplateExecutor creates a new template executor. +// flattenSchema controls whether schema.table identifiers use dot or underscore separation. +func NewTemplateExecutor(flattenSchema bool) (*TemplateExecutor, error) { // Create template with custom functions funcMap := make(template.FuncMap) for k, v := range TemplateFunctions() { funcMap[k] = v } + // qual_table returns a quoted, schema-qualified identifier. + // With flatten=false: "schema"."table" (or unquoted equivalents). + // With flatten=true: "schema_table". + funcMap["qual_table"] = func(schema, name string) string { + if schema == "" { + return quoteIdent(name) + } + if flattenSchema { + return quoteIdent(schema + "_" + name) + } + return quoteIdent(schema) + "." + quoteIdent(name) + } + + // qual_table_raw is the same as qual_table but without identifier quoting. + funcMap["qual_table_raw"] = func(schema, name string) string { + if schema == "" { + return name + } + if flattenSchema { + return schema + "_" + name + } + return schema + "." + name + } + tmpl, err := template.New("").Funcs(funcMap).ParseFS(templateFS, "templates/*.tmpl") if err != nil { return nil, fmt.Errorf("failed to parse templates: %w", err) diff --git a/pkg/writers/pgsql/templates/add_column.tmpl b/pkg/writers/pgsql/templates/add_column.tmpl index f52f803..6e3e1e3 100644 --- a/pkg/writers/pgsql/templates/add_column.tmpl +++ b/pkg/writers/pgsql/templates/add_column.tmpl @@ -1,4 +1,4 @@ -ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} +ALTER TABLE {{qual_table .SchemaName .TableName}} ADD COLUMN IF NOT EXISTS {{quote_ident .ColumnName}} {{.ColumnType}} {{- if .Default}} DEFAULT {{.Default}}{{end}} {{- if .NotNull}} NOT NULL{{end}}; \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/add_column_with_check.tmpl b/pkg/writers/pgsql/templates/add_column_with_check.tmpl index 0d86d07..8be50f1 100644 --- a/pkg/writers/pgsql/templates/add_column_with_check.tmpl +++ b/pkg/writers/pgsql/templates/add_column_with_check.tmpl @@ -6,7 +6,7 @@ BEGIN AND table_name = '{{.TableName}}' AND column_name = '{{.ColumnName}}' ) THEN - ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} ADD COLUMN {{.ColumnDefinition}}; + ALTER TABLE {{qual_table .SchemaName .TableName}} ADD COLUMN {{.ColumnDefinition}}; END IF; END; $$; diff --git a/pkg/writers/pgsql/templates/alter_column_default.tmpl b/pkg/writers/pgsql/templates/alter_column_default.tmpl index 0543a06..f6a19a6 100644 --- a/pkg/writers/pgsql/templates/alter_column_default.tmpl +++ b/pkg/writers/pgsql/templates/alter_column_default.tmpl @@ -1,7 +1,7 @@ {{- if .SetDefault -}} -ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} +ALTER TABLE {{qual_table .SchemaName .TableName}} ALTER COLUMN {{quote_ident .ColumnName}} SET DEFAULT {{.DefaultValue}}; {{- else -}} -ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} +ALTER TABLE {{qual_table .SchemaName .TableName}} ALTER COLUMN {{quote_ident .ColumnName}} DROP DEFAULT; {{- end -}} \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/alter_column_type.tmpl b/pkg/writers/pgsql/templates/alter_column_type.tmpl index 02d9168..2d5e572 100644 --- a/pkg/writers/pgsql/templates/alter_column_type.tmpl +++ b/pkg/writers/pgsql/templates/alter_column_type.tmpl @@ -1,2 +1,2 @@ -ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} +ALTER TABLE {{qual_table .SchemaName .TableName}} ALTER COLUMN {{quote_ident .ColumnName}} TYPE {{.NewType}}; \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/audit_function.tmpl b/pkg/writers/pgsql/templates/audit_function.tmpl index 3358084..7954d69 100644 --- a/pkg/writers/pgsql/templates/audit_function.tmpl +++ b/pkg/writers/pgsql/templates/audit_function.tmpl @@ -1,4 +1,4 @@ -CREATE OR REPLACE FUNCTION {{.SchemaName}}.{{.FunctionName}}() +CREATE OR REPLACE FUNCTION {{qual_table_raw .SchemaName .FunctionName}}() RETURNS trigger AS $body$ DECLARE @@ -81,4 +81,4 @@ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; -COMMENT ON FUNCTION {{.SchemaName}}.{{.FunctionName}}() IS 'Audit trigger function for table {{.SchemaName}}.{{.TableName}}'; \ No newline at end of file +COMMENT ON FUNCTION {{qual_table_raw .SchemaName .FunctionName}}() IS 'Audit trigger function for table {{qual_table_raw .SchemaName .TableName}}'; \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/audit_trigger.tmpl b/pkg/writers/pgsql/templates/audit_trigger.tmpl index 0c825c8..119c020 100644 --- a/pkg/writers/pgsql/templates/audit_trigger.tmpl +++ b/pkg/writers/pgsql/templates/audit_trigger.tmpl @@ -4,13 +4,13 @@ BEGIN SELECT 1 FROM pg_trigger WHERE tgname = '{{.TriggerName}}' - AND tgrelid = '{{.SchemaName}}.{{.TableName}}'::regclass + AND tgrelid = '{{qual_table_raw .SchemaName .TableName}}'::regclass ) THEN CREATE TRIGGER {{.TriggerName}} AFTER {{.Events}} - ON {{.SchemaName}}.{{.TableName}} + ON {{qual_table_raw .SchemaName .TableName}} FOR EACH ROW - EXECUTE FUNCTION {{.SchemaName}}.{{.FunctionName}}(); + EXECUTE FUNCTION {{qual_table_raw .SchemaName .FunctionName}}(); END IF; END; $$; \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/base_constraint.tmpl b/pkg/writers/pgsql/templates/base_constraint.tmpl index c0edea5..97dbf07 100644 --- a/pkg/writers/pgsql/templates/base_constraint.tmpl +++ b/pkg/writers/pgsql/templates/base_constraint.tmpl @@ -1,6 +1,6 @@ {{/* Base constraint template */}} {{- define "constraint_base" -}} -ALTER TABLE {{.SchemaName}}.{{.TableName}} +ALTER TABLE {{qual_table_raw .SchemaName .TableName}} ADD CONSTRAINT {{.ConstraintName}} {{block "constraint_definition" .}}{{end}}; {{- end -}} @@ -15,7 +15,7 @@ BEGIN AND table_name = '{{.TableName}}' AND constraint_name = '{{.ConstraintName}}' ) THEN - ALTER TABLE {{.SchemaName}}.{{.TableName}} + ALTER TABLE {{qual_table_raw .SchemaName .TableName}} DROP CONSTRAINT {{.ConstraintName}}; END IF; END; diff --git a/pkg/writers/pgsql/templates/base_ddl.tmpl b/pkg/writers/pgsql/templates/base_ddl.tmpl index a212e15..c5d5014 100644 --- a/pkg/writers/pgsql/templates/base_ddl.tmpl +++ b/pkg/writers/pgsql/templates/base_ddl.tmpl @@ -11,7 +11,7 @@ {{/* Base ALTER TABLE structure */}} {{- define "alter_table_base" -}} -ALTER TABLE {{.SchemaName}}.{{.TableName}} +ALTER TABLE {{qual_table_raw .SchemaName .TableName}} {{block "alter_operation" .}}{{end}}; {{- end -}} @@ -30,5 +30,5 @@ $$; {{/* Common drop pattern */}} {{- define "drop_if_exists" -}} -{{block "drop_type" .}}{{end}} IF EXISTS {{.SchemaName}}.{{.ObjectName}}; +{{block "drop_type" .}}{{end}} IF EXISTS {{qual_table_raw .SchemaName .ObjectName}}; {{- end -}} \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/comment_column.tmpl b/pkg/writers/pgsql/templates/comment_column.tmpl index 9bfcef9..8ac61ac 100644 --- a/pkg/writers/pgsql/templates/comment_column.tmpl +++ b/pkg/writers/pgsql/templates/comment_column.tmpl @@ -1 +1 @@ -COMMENT ON COLUMN {{quote_ident .SchemaName}}.{{quote_ident .TableName}}.{{quote_ident .ColumnName}} IS '{{.Comment}}'; \ No newline at end of file +COMMENT ON COLUMN {{qual_table .SchemaName .TableName}}.{{quote_ident .ColumnName}} IS '{{.Comment}}'; \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/comment_table.tmpl b/pkg/writers/pgsql/templates/comment_table.tmpl index 43a5b8b..816002e 100644 --- a/pkg/writers/pgsql/templates/comment_table.tmpl +++ b/pkg/writers/pgsql/templates/comment_table.tmpl @@ -1 +1 @@ -COMMENT ON TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} IS '{{.Comment}}'; \ No newline at end of file +COMMENT ON TABLE {{qual_table .SchemaName .TableName}} IS '{{.Comment}}'; \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/create_check_constraint.tmpl b/pkg/writers/pgsql/templates/create_check_constraint.tmpl index abc15d3..dd86186 100644 --- a/pkg/writers/pgsql/templates/create_check_constraint.tmpl +++ b/pkg/writers/pgsql/templates/create_check_constraint.tmpl @@ -6,7 +6,7 @@ BEGIN AND table_name = '{{.TableName}}' AND constraint_name = '{{.ConstraintName}}' ) THEN - ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} ADD CONSTRAINT {{quote_ident .ConstraintName}} CHECK ({{.Expression}}); + ALTER TABLE {{qual_table .SchemaName .TableName}} ADD CONSTRAINT {{quote_ident .ConstraintName}} CHECK ({{.Expression}}); END IF; END; $$; \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/create_foreign_key.tmpl b/pkg/writers/pgsql/templates/create_foreign_key.tmpl index 06bc250..34e47dc 100644 --- a/pkg/writers/pgsql/templates/create_foreign_key.tmpl +++ b/pkg/writers/pgsql/templates/create_foreign_key.tmpl @@ -1,10 +1,10 @@ -ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} +ALTER TABLE {{qual_table .SchemaName .TableName}} DROP CONSTRAINT IF EXISTS {{quote_ident .ConstraintName}}; -ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} +ALTER TABLE {{qual_table .SchemaName .TableName}} ADD CONSTRAINT {{quote_ident .ConstraintName}} FOREIGN KEY ({{.SourceColumns}}) - REFERENCES {{quote_ident .TargetSchema}}.{{quote_ident .TargetTable}} ({{.TargetColumns}}) + REFERENCES {{qual_table .TargetSchema .TargetTable}} ({{.TargetColumns}}) ON DELETE {{.OnDelete}} ON UPDATE {{.OnUpdate}} DEFERRABLE; \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/create_foreign_key_with_check.tmpl b/pkg/writers/pgsql/templates/create_foreign_key_with_check.tmpl index d3fde82..8dbfcbe 100644 --- a/pkg/writers/pgsql/templates/create_foreign_key_with_check.tmpl +++ b/pkg/writers/pgsql/templates/create_foreign_key_with_check.tmpl @@ -6,10 +6,10 @@ BEGIN AND table_name = '{{.TableName}}' AND constraint_name = '{{.ConstraintName}}' ) THEN - ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} + ALTER TABLE {{qual_table .SchemaName .TableName}} ADD CONSTRAINT {{quote_ident .ConstraintName}} FOREIGN KEY ({{.SourceColumns}}) - REFERENCES {{quote_ident .TargetSchema}}.{{quote_ident .TargetTable}} ({{.TargetColumns}}) + REFERENCES {{qual_table .TargetSchema .TargetTable}} ({{.TargetColumns}}) ON DELETE {{.OnDelete}} ON UPDATE {{.OnUpdate}}{{if .Deferrable}} DEFERRABLE{{end}}; diff --git a/pkg/writers/pgsql/templates/create_index.tmpl b/pkg/writers/pgsql/templates/create_index.tmpl index 8cb55fb..55fe127 100644 --- a/pkg/writers/pgsql/templates/create_index.tmpl +++ b/pkg/writers/pgsql/templates/create_index.tmpl @@ -1,2 +1,2 @@ CREATE {{if .Unique}}UNIQUE {{end}}INDEX IF NOT EXISTS {{quote_ident .IndexName}} - ON {{quote_ident .SchemaName}}.{{quote_ident .TableName}} USING {{.IndexType}} ({{.Columns}}); \ No newline at end of file + ON {{qual_table .SchemaName .TableName}} USING {{.IndexType}} ({{.Columns}}); \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/create_primary_key.tmpl b/pkg/writers/pgsql/templates/create_primary_key.tmpl index bc5fec0..864e345 100644 --- a/pkg/writers/pgsql/templates/create_primary_key.tmpl +++ b/pkg/writers/pgsql/templates/create_primary_key.tmpl @@ -6,7 +6,7 @@ BEGIN AND table_name = '{{.TableName}}' AND constraint_name = '{{.ConstraintName}}' ) THEN - ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} + ALTER TABLE {{qual_table .SchemaName .TableName}} ADD CONSTRAINT {{quote_ident .ConstraintName}} PRIMARY KEY ({{.Columns}}); END IF; END; diff --git a/pkg/writers/pgsql/templates/create_primary_key_with_autogen_check.tmpl b/pkg/writers/pgsql/templates/create_primary_key_with_autogen_check.tmpl index 6789a80..90cb5d1 100644 --- a/pkg/writers/pgsql/templates/create_primary_key_with_autogen_check.tmpl +++ b/pkg/writers/pgsql/templates/create_primary_key_with_autogen_check.tmpl @@ -11,7 +11,7 @@ BEGIN AND constraint_name IN ({{.AutoGenNames}}); IF auto_pk_name IS NOT NULL THEN - EXECUTE 'ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} DROP CONSTRAINT ' || quote_ident(auto_pk_name); + EXECUTE 'ALTER TABLE {{qual_table .SchemaName .TableName}} DROP CONSTRAINT ' || quote_ident(auto_pk_name); END IF; -- Add named primary key if it doesn't exist @@ -21,7 +21,7 @@ BEGIN AND table_name = '{{.TableName}}' AND constraint_name = '{{.ConstraintName}}' ) THEN - ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} ADD CONSTRAINT {{quote_ident .ConstraintName}} PRIMARY KEY ({{.Columns}}); + ALTER TABLE {{qual_table .SchemaName .TableName}} ADD CONSTRAINT {{quote_ident .ConstraintName}} PRIMARY KEY ({{.Columns}}); END IF; END; $$; diff --git a/pkg/writers/pgsql/templates/create_sequence.tmpl b/pkg/writers/pgsql/templates/create_sequence.tmpl index 0b74e90..35b8e5a 100644 --- a/pkg/writers/pgsql/templates/create_sequence.tmpl +++ b/pkg/writers/pgsql/templates/create_sequence.tmpl @@ -1,4 +1,4 @@ -CREATE SEQUENCE IF NOT EXISTS {{quote_ident .SchemaName}}.{{quote_ident .SequenceName}} +CREATE SEQUENCE IF NOT EXISTS {{qual_table .SchemaName .SequenceName}} INCREMENT {{.Increment}} MINVALUE {{.MinValue}} MAXVALUE {{.MaxValue}} diff --git a/pkg/writers/pgsql/templates/create_table.tmpl b/pkg/writers/pgsql/templates/create_table.tmpl index 3f471ba..56053c3 100644 --- a/pkg/writers/pgsql/templates/create_table.tmpl +++ b/pkg/writers/pgsql/templates/create_table.tmpl @@ -1,4 +1,4 @@ -CREATE TABLE IF NOT EXISTS {{quote_ident .SchemaName}}.{{quote_ident .TableName}} ( +CREATE TABLE IF NOT EXISTS {{qual_table .SchemaName .TableName}} ( {{- range $i, $col := .Columns}} {{- if $i}},{{end}} {{quote_ident $col.Name}} {{$col.Type}} diff --git a/pkg/writers/pgsql/templates/create_unique_constraint.tmpl b/pkg/writers/pgsql/templates/create_unique_constraint.tmpl index 478edbb..96684b7 100644 --- a/pkg/writers/pgsql/templates/create_unique_constraint.tmpl +++ b/pkg/writers/pgsql/templates/create_unique_constraint.tmpl @@ -6,7 +6,7 @@ BEGIN AND table_name = '{{.TableName}}' AND constraint_name = '{{.ConstraintName}}' ) THEN - ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} ADD CONSTRAINT {{quote_ident .ConstraintName}} UNIQUE ({{.Columns}}); + ALTER TABLE {{qual_table .SchemaName .TableName}} ADD CONSTRAINT {{quote_ident .ConstraintName}} UNIQUE ({{.Columns}}); END IF; END; $$; \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/drop_constraint.tmpl b/pkg/writers/pgsql/templates/drop_constraint.tmpl index 439103f..54aa55c 100644 --- a/pkg/writers/pgsql/templates/drop_constraint.tmpl +++ b/pkg/writers/pgsql/templates/drop_constraint.tmpl @@ -1 +1 @@ -ALTER TABLE {{quote_ident .SchemaName}}.{{quote_ident .TableName}} DROP CONSTRAINT IF EXISTS {{quote_ident .ConstraintName}}; \ No newline at end of file +ALTER TABLE {{qual_table .SchemaName .TableName}} DROP CONSTRAINT IF EXISTS {{quote_ident .ConstraintName}}; \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/drop_index.tmpl b/pkg/writers/pgsql/templates/drop_index.tmpl index 938af10..cfbad87 100644 --- a/pkg/writers/pgsql/templates/drop_index.tmpl +++ b/pkg/writers/pgsql/templates/drop_index.tmpl @@ -1 +1 @@ -DROP INDEX IF EXISTS {{quote_ident .SchemaName}}.{{quote_ident .IndexName}} CASCADE; \ No newline at end of file +DROP INDEX IF EXISTS {{qual_table .SchemaName .IndexName}} CASCADE; \ No newline at end of file diff --git a/pkg/writers/pgsql/templates/fragments.tmpl b/pkg/writers/pgsql/templates/fragments.tmpl index d46b0e8..664422a 100644 --- a/pkg/writers/pgsql/templates/fragments.tmpl +++ b/pkg/writers/pgsql/templates/fragments.tmpl @@ -16,7 +16,7 @@ {{/* Qualified table name */}} {{- define "qualified_table" -}} -{{.SchemaName}}.{{.TableName}} +{{qual_table_raw .SchemaName .TableName}} {{- end -}} {{/* Index method clause */}} diff --git a/pkg/writers/pgsql/templates/set_sequence_value.tmpl b/pkg/writers/pgsql/templates/set_sequence_value.tmpl index e4f8cd2..c94f79e 100644 --- a/pkg/writers/pgsql/templates/set_sequence_value.tmpl +++ b/pkg/writers/pgsql/templates/set_sequence_value.tmpl @@ -10,10 +10,10 @@ BEGIN AND c.relkind = 'S' ) THEN SELECT COALESCE(MAX({{quote_ident .ColumnName}}), 0) + 1 - FROM {{quote_ident .SchemaName}}.{{quote_ident .TableName}} + FROM {{qual_table .SchemaName .TableName}} INTO m_cnt; - PERFORM setval('{{quote_ident .SchemaName}}.{{quote_ident .SequenceName}}'::regclass, m_cnt); + PERFORM setval('{{qual_table_raw .SchemaName .SequenceName}}'::regclass, m_cnt); END IF; END; $$; \ No newline at end of file diff --git a/pkg/writers/pgsql/writer.go b/pkg/writers/pgsql/writer.go index 6e40786..726be72 100644 --- a/pkg/writers/pgsql/writer.go +++ b/pkg/writers/pgsql/writer.go @@ -58,13 +58,18 @@ type ExecutionError struct { // NewWriter creates a new PostgreSQL SQL writer func NewWriter(options *writers.WriterOptions) *Writer { - executor, _ := NewTemplateExecutor() + executor, _ := NewTemplateExecutor(options.FlattenSchema) return &Writer{ options: options, executor: executor, } } +// qualTable returns a schema-qualified name using the writer's FlattenSchema setting. +func (w *Writer) qualTable(schema, name string) string { + return writers.QualifiedTableName(schema, name, w.options.FlattenSchema) +} + // WriteDatabase writes the entire database schema as SQL func (w *Writer) WriteDatabase(db *models.Database) error { // Check if we should execute SQL directly on a database @@ -134,8 +139,8 @@ func (w *Writer) GenerateDatabaseStatements(db *models.Database) ([]string, erro func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, error) { statements := []string{} - // Phase 1: Create schema - if schema.Name != "public" { + // Phase 1: Create schema (skip entirely when flattening) + if schema.Name != "public" && !w.options.FlattenSchema { statements = append(statements, fmt.Sprintf("-- Schema: %s", schema.Name)) statements = append(statements, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schema.SQLName())) } @@ -157,8 +162,8 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro continue } - stmt := fmt.Sprintf("CREATE SEQUENCE IF NOT EXISTS %s.%s\n INCREMENT 1\n MINVALUE 1\n MAXVALUE 9223372036854775807\n START 1\n CACHE 1", - schema.SQLName(), seqName) + stmt := fmt.Sprintf("CREATE SEQUENCE IF NOT EXISTS %s\n INCREMENT 1\n MINVALUE 1\n MAXVALUE 9223372036854775807\n START 1\n CACHE 1", + w.qualTable(schema.SQLName(), seqName)) statements = append(statements, stmt) } @@ -275,8 +280,8 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro whereClause = fmt.Sprintf(" WHERE %s", index.Where) } - stmt := fmt.Sprintf("CREATE %sINDEX IF NOT EXISTS %s ON %s.%s USING %s (%s)%s", - uniqueStr, quoteIdentifier(index.Name), schema.SQLName(), table.SQLName(), indexType, strings.Join(columnExprs, ", "), whereClause) + stmt := fmt.Sprintf("CREATE %sINDEX IF NOT EXISTS %s ON %s USING %s (%s)%s", + uniqueStr, quoteIdentifier(index.Name), w.qualTable(schema.SQLName(), table.SQLName()), indexType, strings.Join(columnExprs, ", "), whereClause) statements = append(statements, stmt) } } @@ -374,15 +379,15 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro // Phase 7: Comments for _, table := range schema.Tables { if table.Comment != "" { - stmt := fmt.Sprintf("COMMENT ON TABLE %s.%s IS '%s'", - schema.SQLName(), table.SQLName(), escapeQuote(table.Comment)) + stmt := fmt.Sprintf("COMMENT ON TABLE %s IS '%s'", + w.qualTable(schema.SQLName(), table.SQLName()), escapeQuote(table.Comment)) statements = append(statements, stmt) } for _, column := range table.Columns { if column.Comment != "" { - stmt := fmt.Sprintf("COMMENT ON COLUMN %s.%s.%s IS '%s'", - schema.SQLName(), table.SQLName(), column.SQLName(), escapeQuote(column.Comment)) + stmt := fmt.Sprintf("COMMENT ON COLUMN %s.%s IS '%s'", + w.qualTable(schema.SQLName(), table.SQLName()), column.SQLName(), escapeQuote(column.Comment)) statements = append(statements, stmt) } } @@ -474,8 +479,8 @@ func (w *Writer) generateCreateTableStatement(schema *models.Schema, table *mode columnDefs = append(columnDefs, " "+def) } - stmt := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (\n%s\n)", - schema.SQLName(), table.SQLName(), strings.Join(columnDefs, ",\n")) + stmt := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (\n%s\n)", + w.qualTable(schema.SQLName(), table.SQLName()), strings.Join(columnDefs, ",\n")) statements = append(statements, stmt) return statements, nil @@ -655,8 +660,7 @@ func (w *Writer) WriteAddColumnStatements(db *models.Database) error { // writeCreateSchema generates CREATE SCHEMA statement func (w *Writer) writeCreateSchema(schema *models.Schema) error { - if schema.Name == "public" { - // public schema exists by default + if schema.Name == "public" || w.options.FlattenSchema { return nil } @@ -708,8 +712,8 @@ func (w *Writer) writeCreateTables(schema *models.Schema) error { fmt.Fprintf(w.writer, "-- Tables for schema: %s\n", schema.Name) for _, table := range schema.Tables { - fmt.Fprintf(w.writer, "CREATE TABLE IF NOT EXISTS %s.%s (\n", - schema.SQLName(), table.SQLName()) + fmt.Fprintf(w.writer, "CREATE TABLE IF NOT EXISTS %s (\n", + w.qualTable(schema.SQLName(), table.SQLName())) // Write columns columns := getSortedColumns(table.Columns) @@ -893,8 +897,8 @@ func (w *Writer) writeIndexes(schema *models.Schema) error { fmt.Fprintf(w.writer, "CREATE %sINDEX IF NOT EXISTS %s\n", unique, indexName) - fmt.Fprintf(w.writer, " ON %s.%s USING %s (%s)%s;\n\n", - schema.SQLName(), table.SQLName(), indexType, strings.Join(columnExprs, ", "), whereClause) + fmt.Fprintf(w.writer, " ON %s USING %s (%s)%s;\n\n", + w.qualTable(schema.SQLName(), table.SQLName()), indexType, strings.Join(columnExprs, ", "), whereClause) } } @@ -1203,16 +1207,16 @@ func (w *Writer) writeComments(schema *models.Schema) error { for _, table := range schema.Tables { // Table comment if table.Description != "" { - fmt.Fprintf(w.writer, "COMMENT ON TABLE %s.%s IS '%s';\n", - schema.SQLName(), table.SQLName(), + fmt.Fprintf(w.writer, "COMMENT ON TABLE %s IS '%s';\n", + w.qualTable(schema.SQLName(), table.SQLName()), escapeQuote(table.Description)) } // Column comments for _, col := range getSortedColumns(table.Columns) { if col.Description != "" { - fmt.Fprintf(w.writer, "COMMENT ON COLUMN %s.%s.%s IS '%s';\n", - schema.SQLName(), table.SQLName(), col.SQLName(), + fmt.Fprintf(w.writer, "COMMENT ON COLUMN %s.%s IS '%s';\n", + w.qualTable(schema.SQLName(), table.SQLName()), col.SQLName(), escapeQuote(col.Description)) } } diff --git a/pkg/writers/writer.go b/pkg/writers/writer.go index 5f78d2f..c29eb9c 100644 --- a/pkg/writers/writer.go +++ b/pkg/writers/writer.go @@ -28,10 +28,29 @@ type WriterOptions struct { // PackageName is the Go package name (for code generation) PackageName string + // FlattenSchema disables schema.table dot notation and instead joins + // schema and table with an underscore (e.g., "public_users"). + // Useful for databases like SQLite that do not support schemas. + FlattenSchema bool + // Additional options can be added here as needed Metadata map[string]interface{} } +// QualifiedTableName returns a schema-qualified table name. +// When flatten is true, schema and table are joined with underscore (e.g., "schema_table"). +// When flatten is false, they are dot-separated (e.g., "schema.table"). +// If schema is empty, just the table name is returned regardless of flatten. +func QualifiedTableName(schema, table string, flatten bool) string { + if schema == "" { + return table + } + if flatten { + return schema + "_" + table + } + return schema + "." + table +} + // SanitizeFilename removes quotes, comments, and invalid characters from identifiers // to make them safe for use in filenames. This handles: // - Double and single quotes: "table_name" or 'table_name' -> table_name