fix: contain generated files and assets and harden SQL rendering

This commit is contained in:
2026-10-12 20:59:46 +02:00
parent 2fa48335d3
commit dd97413b8e
68 changed files with 2328 additions and 798 deletions
+186 -123
View File
@@ -37,6 +37,9 @@ type ExecutionError = writers.ExecutionError
// NewWriter creates a new PostgreSQL SQL writer
func NewWriter(options *writers.WriterOptions) *Writer {
if options == nil {
options = &writers.WriterOptions{}
}
executor, _ := NewTemplateExecutor(options.FlattenSchema)
return &Writer{
options: options,
@@ -46,11 +49,22 @@ func NewWriter(options *writers.WriterOptions) *Writer {
// 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)
if schema == "" {
return quoteIdent(name)
}
if w.options.FlattenSchema {
return quoteIdent(schema + "_" + name)
}
return quoteIdent(schema) + "." + quoteIdent(name)
}
// WriteDatabase writes the entire database schema as SQL
func (w *Writer) WriteDatabase(db *models.Database) error {
var preparationErr error
db, preparationErr = prepareModel(db, w.options.LowercaseIdentifiers)
if preparationErr != nil {
return preparationErr
}
// Check if we should execute SQL directly on a database
if connString, ok := w.options.Metadata["connection_string"].(string); ok && connString != "" {
return w.executeDatabaseSQL(db, connString)
@@ -79,7 +93,7 @@ func (w *Writer) WriteDatabase(db *models.Database) error {
// Write header comment
fmt.Fprintf(w.writer, "-- PostgreSQL Database Schema\n")
fmt.Fprintf(w.writer, "-- Database: %s\n", db.Name)
fmt.Fprintf(w.writer, "-- Database: %s\n", commentText(db.Name))
fmt.Fprintf(w.writer, "-- Generated by RelSpec\n")
if w.options.ContinueOnError {
fmt.Fprintf(w.writer, "\\set ON_ERROR_STOP off\n")
@@ -110,11 +124,16 @@ func (w *Writer) WriteDatabase(db *models.Database) error {
// GenerateDatabaseStatements generates SQL statements as a list for the entire database
// Returns a slice of SQL statements that can be executed independently
func (w *Writer) GenerateDatabaseStatements(db *models.Database) ([]string, error) {
var preparationErr error
db, preparationErr = prepareModel(db, w.options.LowercaseIdentifiers)
if preparationErr != nil {
return nil, preparationErr
}
statements := []string{}
// Add header comment
statements = append(statements, "-- PostgreSQL Database Schema")
statements = append(statements, fmt.Sprintf("-- Database: %s", db.Name))
statements = append(statements, fmt.Sprintf("-- Database: %s", commentText(db.Name)))
statements = append(statements, "-- Generated by RelSpec")
for _, schema := range db.Schemas {
@@ -161,7 +180,7 @@ func (w *Writer) primaryKeySequenceStatement(schema *models.Schema, table *model
}
return 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))
w.qualTable(schema.Name, seqName))
}
// primaryKeySetvalStatement returns the statement that moves a table's primary key
@@ -182,6 +201,11 @@ func (w *Writer) primaryKeySetvalStatement(schema *models.Schema, table *models.
// GenerateSchemaStatements generates SQL statements as a list for a single schema
func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, error) {
var err error
schema, err = w.prepareSchema(schema)
if err != nil {
return nil, err
}
preflights, err := w.generateTypePreflights(schema)
if err != nil {
return nil, err
@@ -199,8 +223,8 @@ func (w *Writer) generateSchemaStatements(schema *models.Schema) ([]string, erro
// 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()))
statements = append(statements, fmt.Sprintf("-- Schema: %s", commentText(schema.Name)))
statements = append(statements, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", quoteIdent(schema.Name)))
}
for _, extension := range requiredExtensions(schema) {
@@ -258,14 +282,14 @@ func (w *Writer) generateSchemaStatements(schema *models.Schema) ([]string, erro
pkCols := []string{}
for _, col := range table.Columns {
if col.IsPrimaryKey {
pkCols = append(pkCols, col.SQLName())
pkCols = append(pkCols, col.Name)
}
}
if len(pkCols) > 0 {
// Sort for consistent output
sort.Strings(pkCols)
pkColumns = pkCols
pkName = fmt.Sprintf("pk_%s_%s", schema.SQLName(), table.SQLName())
pkName = fmt.Sprintf("pk_%s_%s", schema.Name, table.Name)
}
}
@@ -282,7 +306,7 @@ func (w *Writer) generateSchemaStatements(schema *models.Schema) ([]string, erro
TableName: table.Name,
ConstraintName: pkName,
AutoGenNames: formatStringList(autoGenPKNames),
Columns: strings.Join(pkColumns, ", "),
Columns: quotedIdentifiers(pkColumns),
ColumnNames: formatStringList(pkColumns),
}
@@ -322,7 +346,7 @@ func (w *Writer) generateSchemaStatements(schema *models.Schema) ([]string, erro
tablespaceClause := ""
if ts := pgIndexDirectiveTablespace(index); ts != "" {
tablespaceClause = fmt.Sprintf(" TABLESPACE %s", ts)
tablespaceClause = fmt.Sprintf(" TABLESPACE %s", quoteIdent(ts))
}
whereClause := ""
@@ -331,7 +355,7 @@ func (w *Writer) generateSchemaStatements(schema *models.Schema) ([]string, erro
}
stmt := fmt.Sprintf("CREATE %sINDEX IF NOT EXISTS %s ON %s USING %s (%s)%s%s%s",
uniqueStr, quoteIdentifier(index.Name), w.qualTable(schema.SQLName(), table.SQLName()), indexType, strings.Join(columnExprs, ", "), withClause, tablespaceClause, whereClause)
uniqueStr, quoteIdentifier(index.Name), w.qualTable(schema.Name, table.Name), indexType, strings.Join(columnExprs, ", "), withClause, tablespaceClause, whereClause)
statements = append(statements, stmt)
}
}
@@ -348,7 +372,7 @@ func (w *Writer) generateSchemaStatements(schema *models.Schema) ([]string, erro
SchemaName: schema.Name,
TableName: table.Name,
ConstraintName: constraint.Name,
Columns: strings.Join(constraint.Columns, ", "),
Columns: quotedIdentifiers(constraint.Columns),
UsingIndex: adoptableUniqueIndex(table, constraint),
}
@@ -410,10 +434,10 @@ func (w *Writer) generateSchemaStatements(schema *models.Schema) ([]string, erro
SchemaName: schema.Name,
TableName: table.Name,
ConstraintName: constraint.Name,
SourceColumns: strings.Join(constraint.Columns, ", "),
SourceColumns: quotedIdentifiers(constraint.Columns),
TargetSchema: refSchema,
TargetTable: constraint.ReferencedTable,
TargetColumns: strings.Join(constraint.ReferencedColumns, ", "),
TargetColumns: quotedIdentifiers(constraint.ReferencedColumns),
OnDelete: onDelete,
OnUpdate: onUpdate,
Deferral: constraint.DeferralClause(),
@@ -441,15 +465,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 IS '%s'",
w.qualTable(schema.SQLName(), table.SQLName()), escapeQuote(table.Comment))
stmt := fmt.Sprintf("COMMENT ON TABLE %s IS %s",
w.qualTable(schema.Name, table.Name), quote(table.Comment))
statements = append(statements, stmt)
}
for _, column := range sortColumns(table.Columns) {
if column.Comment != "" {
stmt := fmt.Sprintf("COMMENT ON COLUMN %s.%s IS '%s'",
w.qualTable(schema.SQLName(), table.SQLName()), column.SQLName(), escapeQuote(column.Comment))
stmt := fmt.Sprintf("COMMENT ON COLUMN %s.%s IS %s",
w.qualTable(schema.Name, table.Name), quoteIdent(column.Name), quote(column.Comment))
statements = append(statements, stmt)
}
}
@@ -461,9 +485,14 @@ func (w *Writer) generateSchemaStatements(schema *models.Schema) ([]string, erro
// GenerateAddColumnStatements generates ALTER TABLE ADD COLUMN statements for existing tables
// This is useful for schema evolution when new columns are added to existing tables
func (w *Writer) GenerateAddColumnStatements(schema *models.Schema) ([]string, error) {
var err error
schema, err = w.prepareSchema(schema)
if err != nil {
return nil, err
}
statements := []string{}
statements = append(statements, fmt.Sprintf("-- Add missing columns for schema: %s", schema.Name))
statements = append(statements, fmt.Sprintf("-- Add missing columns for schema: %s", commentText(schema.Name)))
for _, table := range schema.Tables {
// Sort columns by sequence or name for consistent output
@@ -501,9 +530,14 @@ func (w *Writer) GenerateAddColumnStatements(schema *models.Schema) ([]string, e
}
func (w *Writer) GenerateAlterColumnTypeStatements(schema *models.Schema) ([]string, error) {
var err error
schema, err = w.prepareSchema(schema)
if err != nil {
return nil, err
}
statements := []string{}
statements = append(statements, fmt.Sprintf("-- Alter column types for schema: %s", schema.Name))
statements = append(statements, fmt.Sprintf("-- Alter column types for schema: %s", commentText(schema.Name)))
for _, table := range schema.Tables {
columns := getSortedColumns(table.Columns)
@@ -533,9 +567,14 @@ func (w *Writer) GenerateAlterColumnTypeStatements(schema *models.Schema) ([]str
// statements to bring existing columns' DEFAULT clause in line with the
// model, safe to run against a database that already has the columns.
func (w *Writer) GenerateAlterColumnDefaultStatements(schema *models.Schema) ([]string, error) {
var err error
schema, err = w.prepareSchema(schema)
if err != nil {
return nil, err
}
statements := []string{}
statements = append(statements, fmt.Sprintf("-- Alter column defaults for schema: %s", schema.Name))
statements = append(statements, fmt.Sprintf("-- Alter column defaults for schema: %s", commentText(schema.Name)))
for _, table := range schema.Tables {
columns := getSortedColumns(table.Columns)
@@ -592,7 +631,7 @@ func formatColumnDefaultSQL(col *models.Column) (setDefault bool, defaultVal str
return false, ""
}
if value, ok := col.Default.(string); ok {
return true, writers.QuoteDefaultValue(stripBackticks(value), col.Type)
return true, quoteDefaultValue(stripBackticks(value), col.Type)
}
return true, fmt.Sprintf("%v", col.Default)
}
@@ -601,9 +640,14 @@ func formatColumnDefaultSQL(col *models.Column) (setDefault bool, defaultVal str
// statements to bring existing columns' NOT NULL state in line with the
// model, safe to run against a database that already has the columns.
func (w *Writer) GenerateAlterColumnNullabilityStatements(schema *models.Schema) ([]string, error) {
var err error
schema, err = w.prepareSchema(schema)
if err != nil {
return nil, err
}
statements := []string{}
statements = append(statements, fmt.Sprintf("-- Alter column nullability for schema: %s", schema.Name))
statements = append(statements, fmt.Sprintf("-- Alter column nullability for schema: %s", commentText(schema.Name)))
for _, table := range schema.Tables {
columns := getSortedColumns(table.Columns)
@@ -626,10 +670,15 @@ func (w *Writer) GenerateAlterColumnNullabilityStatements(schema *models.Schema)
// GenerateAddColumnsForDatabase generates ALTER TABLE ADD COLUMN statements for the entire database
func (w *Writer) GenerateAddColumnsForDatabase(db *models.Database) ([]string, error) {
prepared, preparationErr := prepareModel(db, w.options.LowercaseIdentifiers)
if preparationErr != nil {
return nil, preparationErr
}
db = prepared
statements := []string{}
statements = append(statements, "-- Add missing columns to existing tables")
statements = append(statements, fmt.Sprintf("-- Database: %s", db.Name))
statements = append(statements, fmt.Sprintf("-- Database: %s", commentText(db.Name)))
statements = append(statements, "-- Generated by RelSpec")
for _, schema := range db.Schemas {
@@ -666,7 +715,7 @@ func (w *Writer) generateCreateTableStatement(schema *models.Schema, table *mode
}
stmt := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (\n%s\n)%s",
w.qualTable(schema.SQLName(), table.SQLName()), strings.Join(columnDefs, ",\n"),
w.qualTable(schema.Name, table.Name), strings.Join(columnDefs, ",\n"),
pgTableDirectiveSuffix(table))
statements = append(statements, stmt)
@@ -675,7 +724,7 @@ func (w *Writer) generateCreateTableStatement(schema *models.Schema, table *mode
// generateColumnDefinition generates column definition
func (w *Writer) generateColumnDefinition(col *models.Column) string {
parts := []string{col.SQLName()}
parts := []string{quoteIdent(col.Name)}
parts = append(parts, effectiveColumnSQLType(col))
@@ -700,7 +749,7 @@ func (w *Writer) generateColumnDefinition(col *models.Column) string {
if !col.Generated && !col.Identity && col.Default != nil {
switch v := col.Default.(type) {
case string:
parts = append(parts, fmt.Sprintf("DEFAULT %s", writers.QuoteDefaultValue(stripBackticks(v), col.Type)))
parts = append(parts, fmt.Sprintf("DEFAULT %s", quoteDefaultValue(stripBackticks(v), col.Type)))
case bool:
parts = append(parts, fmt.Sprintf("DEFAULT %v", v))
default:
@@ -764,13 +813,18 @@ func equivalentTypeListSQL(sqlType string) string {
variants := pgsql.EquivalentSQLTypeVariants(sqlType)
quoted := make([]string, 0, len(variants))
for _, variant := range variants {
quoted = append(quoted, fmt.Sprintf("'%s'", escapeQuote(variant)))
quoted = append(quoted, quote(variant))
}
return strings.Join(quoted, ", ")
}
// WriteSchema writes a single schema and all its tables
func (w *Writer) WriteSchema(schema *models.Schema) error {
var err error
schema, err = w.prepareSchema(schema)
if err != nil {
return err
}
if w.writer == nil {
w.writer = os.Stdout
}
@@ -885,6 +939,11 @@ func (w *Writer) WriteTable(table *models.Table) error {
// WriteAddColumnStatements writes ALTER TABLE ADD COLUMN statements for a database
// This is used for schema evolution/migration when new columns are added
func (w *Writer) WriteAddColumnStatements(db *models.Database) error {
prepared, preparationErr := prepareModel(db, w.options.LowercaseIdentifiers)
if preparationErr != nil {
return preparationErr
}
db = prepared
var writer io.Writer
var file *os.File
var err error
@@ -926,8 +985,8 @@ func (w *Writer) writeCreateSchema(schema *models.Schema) error {
return nil
}
fmt.Fprintf(w.writer, "-- Schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "CREATE SCHEMA IF NOT EXISTS %s;\n\n", schema.SQLName())
fmt.Fprintf(w.writer, "-- Schema: %s\n", commentText(schema.Name))
fmt.Fprintf(w.writer, "CREATE SCHEMA IF NOT EXISTS %s;\n\n", quoteIdent(schema.Name))
return nil
}
@@ -946,7 +1005,7 @@ func (w *Writer) writeRequiredExtensions(schema *models.Schema) error {
// writeSequences generates CREATE SEQUENCE statements for identity columns
func (w *Writer) writeSequences(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Sequences for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Sequences for schema: %s\n", commentText(schema.Name))
for _, table := range schema.Tables {
// Only create the sequence the primary key's nextval() default uses
@@ -978,11 +1037,11 @@ func (w *Writer) writeSequences(schema *models.Schema) error {
// writeCreateTables generates CREATE TABLE statements
func (w *Writer) writeCreateTables(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Tables for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Tables for schema: %s\n", commentText(schema.Name))
for _, table := range schema.Tables {
fmt.Fprintf(w.writer, "CREATE TABLE IF NOT EXISTS %s (\n",
w.qualTable(schema.SQLName(), table.SQLName()))
w.qualTable(schema.Name, table.Name))
// Write columns
columns := getSortedColumns(table.Columns)
@@ -1003,7 +1062,7 @@ func (w *Writer) writeCreateTables(schema *models.Schema) error {
// writeAddColumns generates ALTER TABLE ADD COLUMN statements for missing columns
func (w *Writer) writeAddColumns(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Add missing columns for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Add missing columns for schema: %s\n", commentText(schema.Name))
for _, table := range schema.Tables {
// Sort columns by sequence or name for consistent output
@@ -1032,7 +1091,7 @@ func (w *Writer) writeAddColumns(schema *models.Schema) error {
}
func (w *Writer) writeAlterColumnTypes(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Alter column types for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Alter column types for schema: %s\n", commentText(schema.Name))
statements, err := w.GenerateAlterColumnTypeStatements(schema)
if err != nil {
@@ -1047,7 +1106,7 @@ func (w *Writer) writeAlterColumnTypes(schema *models.Schema) error {
}
func (w *Writer) writeAlterColumnDefaults(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Alter column defaults for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Alter column defaults for schema: %s\n", commentText(schema.Name))
statements, err := w.GenerateAlterColumnDefaultStatements(schema)
if err != nil {
@@ -1062,7 +1121,7 @@ func (w *Writer) writeAlterColumnDefaults(schema *models.Schema) error {
}
func (w *Writer) writeAlterColumnNullability(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Alter column nullability for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Alter column nullability for schema: %s\n", commentText(schema.Name))
statements, err := w.GenerateAlterColumnNullabilityStatements(schema)
if err != nil {
@@ -1078,7 +1137,7 @@ func (w *Writer) writeAlterColumnNullability(schema *models.Schema) error {
// writePrimaryKeys generates ALTER TABLE statements for primary keys
func (w *Writer) writePrimaryKeys(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Primary keys for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Primary keys for schema: %s\n", commentText(schema.Name))
for _, table := range schema.Tables {
// Find primary key constraint
@@ -1091,21 +1150,21 @@ func (w *Writer) writePrimaryKeys(schema *models.Schema) error {
}
var columnNames []string
pkName := fmt.Sprintf("pk_%s_%s", schema.SQLName(), table.SQLName())
pkName := fmt.Sprintf("pk_%s_%s", schema.Name, table.Name)
if pkConstraint != nil {
// Build column list from explicit constraint
columnNames = make([]string, 0, len(pkConstraint.Columns))
for _, colName := range pkConstraint.Columns {
if col, ok := table.Columns[colName]; ok {
columnNames = append(columnNames, col.SQLName())
columnNames = append(columnNames, col.Name)
}
}
} else {
// No explicit PK constraint, check for columns with IsPrimaryKey = true
for _, col := range table.Columns {
if col.IsPrimaryKey {
columnNames = append(columnNames, col.SQLName())
columnNames = append(columnNames, col.Name)
}
}
// Sort for consistent output
@@ -1127,7 +1186,7 @@ func (w *Writer) writePrimaryKeys(schema *models.Schema) error {
TableName: table.Name,
ConstraintName: pkName,
AutoGenNames: formatStringList(autoGenPKNames),
Columns: strings.Join(columnNames, ", "),
Columns: quotedIdentifiers(columnNames),
ColumnNames: formatStringList(columnNames),
}
@@ -1144,7 +1203,7 @@ func (w *Writer) writePrimaryKeys(schema *models.Schema) error {
// writeIndexes generates CREATE INDEX statements
func (w *Writer) writeIndexes(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Indexes for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Indexes for schema: %s\n", commentText(schema.Name))
for _, table := range schema.Tables {
// Sort indexes by name for consistent output
@@ -1170,7 +1229,7 @@ func (w *Writer) writeIndexes(schema *models.Schema) error {
indexType = "ukey"
}
columnSuffix := strings.Join(index.Columns, "_")
indexName = fmt.Sprintf("%s_%s_%s", indexType, table.SQLName(), strings.ToLower(columnSuffix))
indexName = fmt.Sprintf("%s_%s_%s", indexType, table.Name, strings.ToLower(columnSuffix))
}
indexType := index.Type
@@ -1196,7 +1255,7 @@ func (w *Writer) writeIndexes(schema *models.Schema) error {
tablespaceClause := ""
if ts := pgIndexDirectiveTablespace(index); ts != "" {
tablespaceClause = fmt.Sprintf(" TABLESPACE %s", ts)
tablespaceClause = fmt.Sprintf(" TABLESPACE %s", quoteIdent(ts))
}
whereClause := ""
@@ -1210,9 +1269,9 @@ func (w *Writer) writeIndexes(schema *models.Schema) error {
}
fmt.Fprintf(w.writer, "CREATE %sINDEX %sIF NOT EXISTS %s\n",
unique, concurrently, indexName)
unique, concurrently, quoteIdent(indexName))
fmt.Fprintf(w.writer, " ON %s USING %s (%s)%s%s%s;\n\n",
w.qualTable(schema.SQLName(), table.SQLName()), indexType, strings.Join(columnExprs, ", "), withClause, tablespaceClause, whereClause)
w.qualTable(schema.Name, table.Name), indexType, strings.Join(columnExprs, ", "), withClause, tablespaceClause, whereClause)
}
}
@@ -1221,7 +1280,7 @@ func (w *Writer) writeIndexes(schema *models.Schema) error {
// writeUniqueConstraints generates ALTER TABLE statements for unique constraints
func (w *Writer) writeUniqueConstraints(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Unique constraints for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Unique constraints for schema: %s\n", commentText(schema.Name))
for _, table := range schema.Tables {
// Sort constraints by name for consistent output
@@ -1240,7 +1299,7 @@ func (w *Writer) writeUniqueConstraints(schema *models.Schema) error {
columnExprs := make([]string, 0, len(constraint.Columns))
for _, colName := range constraint.Columns {
if col, ok := table.Columns[colName]; ok {
columnExprs = append(columnExprs, col.SQLName())
columnExprs = append(columnExprs, quoteIdent(col.Name))
}
}
@@ -1268,7 +1327,7 @@ func (w *Writer) writeUniqueConstraints(schema *models.Schema) error {
// writeCheckConstraints generates ALTER TABLE statements for check constraints
func (w *Writer) writeCheckConstraints(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Check constraints for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Check constraints for schema: %s\n", commentText(schema.Name))
for _, table := range schema.Tables {
// Sort constraints by name for consistent output
@@ -1307,7 +1366,7 @@ func (w *Writer) writeCheckConstraints(schema *models.Schema) error {
// writeForeignKeys generates ALTER TABLE statements for foreign keys
func (w *Writer) writeForeignKeys(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Foreign keys for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Foreign keys for schema: %s\n", commentText(schema.Name))
for _, table := range schema.Tables {
// Sort relationships by name for consistent output
@@ -1327,7 +1386,7 @@ func (w *Writer) writeForeignKeys(schema *models.Schema) error {
fkName = name
}
if fkName == "" {
fkName = fmt.Sprintf("fk_%s_%s", table.SQLName(), rel.ToTable)
fkName = fmt.Sprintf("fk_%s_%s", table.Name, rel.ToTable)
}
// Find the foreign key constraint that matches this relationship.
@@ -1354,13 +1413,13 @@ func (w *Writer) writeForeignKeys(schema *models.Schema) error {
sourceColumns := make([]string, 0, len(fkConstraint.Columns))
for _, colName := range fkConstraint.Columns {
if col, ok := table.Columns[colName]; ok {
sourceColumns = append(sourceColumns, col.SQLName())
sourceColumns = append(sourceColumns, quoteIdent(col.Name))
}
}
targetColumns := make([]string, 0, len(fkConstraint.ReferencedColumns))
for _, colName := range fkConstraint.ReferencedColumns {
targetColumns = append(targetColumns, strings.ToLower(colName))
targetColumns = append(targetColumns, quoteIdent(colName))
}
if len(sourceColumns) == 0 || len(targetColumns) == 0 {
@@ -1435,15 +1494,15 @@ func (w *Writer) writeForeignKeys(schema *models.Schema) error {
sourceColumns := make([]string, 0, len(constraint.Columns))
for _, colName := range constraint.Columns {
if col, ok := table.Columns[colName]; ok {
sourceColumns = append(sourceColumns, col.SQLName())
sourceColumns = append(sourceColumns, quoteIdent(col.Name))
} else {
sourceColumns = append(sourceColumns, colName)
sourceColumns = append(sourceColumns, quoteIdent(colName))
}
}
targetColumns := make([]string, 0, len(constraint.ReferencedColumns))
for _, colName := range constraint.ReferencedColumns {
targetColumns = append(targetColumns, strings.ToLower(colName))
targetColumns = append(targetColumns, quoteIdent(colName))
}
if len(sourceColumns) == 0 || len(targetColumns) == 0 {
@@ -1492,7 +1551,7 @@ func (w *Writer) writeForeignKeys(schema *models.Schema) error {
// writeSetSequenceValues generates statements to set sequence current values
func (w *Writer) writeSetSequenceValues(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Set sequence values for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Set sequence values for schema: %s\n", commentText(schema.Name))
for _, table := range schema.Tables {
seqName := primaryKeySequenceName(table)
@@ -1521,22 +1580,22 @@ func (w *Writer) writeSetSequenceValues(schema *models.Schema) error {
// writeComments generates COMMENT statements for tables and columns
func (w *Writer) writeComments(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Comments for schema: %s\n", schema.Name)
fmt.Fprintf(w.writer, "-- Comments for schema: %s\n", commentText(schema.Name))
for _, table := range schema.Tables {
// Table comment
if table.Description != "" {
fmt.Fprintf(w.writer, "COMMENT ON TABLE %s IS '%s';\n",
w.qualTable(schema.SQLName(), table.SQLName()),
escapeQuote(table.Description))
fmt.Fprintf(w.writer, "COMMENT ON TABLE %s IS %s;\n",
w.qualTable(schema.Name, table.Name),
quote(table.Description))
}
// Column comments
for _, col := range getSortedColumns(table.Columns) {
if col.Description != "" {
fmt.Fprintf(w.writer, "COMMENT ON COLUMN %s.%s IS '%s';\n",
w.qualTable(schema.SQLName(), table.SQLName()), col.SQLName(),
escapeQuote(col.Description))
fmt.Fprintf(w.writer, "COMMENT ON COLUMN %s.%s IS %s;\n",
w.qualTable(schema.Name, table.Name), quoteIdent(col.Name),
quote(col.Description))
}
}
@@ -1897,6 +1956,9 @@ func nativeGistBaseType(baseType string) bool {
}
func resolveIndexColumn(table *models.Table, colName string) (*models.Column, bool) {
if len(colName) >= 2 && colName[0] == '"' && colName[len(colName)-1] == '"' {
colName = strings.ReplaceAll(colName[1:len(colName)-1], `""`, `"`)
}
if table == nil {
return nil, false
}
@@ -1904,18 +1966,8 @@ func resolveIndexColumn(table *models.Table, colName string) (*models.Column, bo
return col, true
}
normalized := strings.ToLower(strings.Trim(colName, `"`))
for key, col := range table.Columns {
if col == nil {
continue
}
if strings.ToLower(strings.Trim(key, `"`)) == normalized {
return col, true
}
if strings.ToLower(strings.Trim(col.Name, `"`)) == normalized {
return col, true
}
if strings.ToLower(strings.Trim(col.SQLName(), `"`)) == normalized {
for _, col := range table.Columns {
if col != nil && col.Name == colName {
return col, true
}
}
@@ -1972,7 +2024,7 @@ func sortIndexes(indexes map[string]*models.Index) []*models.Index {
func formatStringList(items []string) string {
quoted := make([]string, len(items))
for i, item := range items {
quoted[i] = fmt.Sprintf("'%s'", escapeQuote(item))
quoted[i] = quote(item)
}
return strings.Join(quoted, ", ")
}
@@ -2049,19 +2101,20 @@ func extractSequenceName(defaultExpr string) string {
if start == -1 {
return ""
}
end := strings.Index(defaultExpr[start+1:], "'")
if end == -1 {
return ""
var name strings.Builder
for i := start + 1; i < len(defaultExpr); i++ {
if defaultExpr[i] == '\'' {
if i+1 < len(defaultExpr) && defaultExpr[i+1] == '\'' {
name.WriteByte('\'')
i++
continue
}
return regclassLastName(name.String())
}
name.WriteByte(defaultExpr[i])
}
return ""
fullName := defaultExpr[start+1 : start+1+end]
// Remove schema prefix if present
parts := strings.Split(fullName, ".")
if len(parts) > 1 {
return parts[len(parts)-1]
}
return fullName
}
// executeDatabaseSQL applies db to a PostgreSQL database. By default it reads the live schema
@@ -2126,7 +2179,7 @@ func (w *Writer) diffStatements(model, current *models.Database) ([]string, erro
currentSchemas := make(map[string]*models.Schema)
for _, cs := range current.Schemas {
if cs != nil {
currentSchemas[strings.ToLower(cs.Name)] = cs
currentSchemas[cs.Name] = cs
}
}
@@ -2142,25 +2195,25 @@ func (w *Writer) diffStatements(model, current *models.Database) ([]string, erro
return nil, err
}
cs := currentSchemas[strings.ToLower(schema.Name)]
cs := currentSchemas[schema.Name]
if cs == nil && schema.Name != "public" {
statements = append(statements,
fmt.Sprintf("-- Schema: %s", schema.Name),
fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schema.SQLName()))
fmt.Sprintf("-- Schema: %s", commentText(schema.Name)),
fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", quoteIdent(schema.Name)))
}
existing := make(map[string]*models.Table)
knownSequences := make(map[string]bool)
if cs != nil {
for _, t := range cs.Tables {
existing[strings.ToLower(t.Name)] = t
existing[t.Name] = t
}
for _, seq := range cs.Sequences {
knownSequences[strings.ToLower(seq.Name)] = true
knownSequences[seq.Name] = true
}
}
for _, table := range schema.Tables {
currentTable, ok := existing[strings.ToLower(table.Name)]
currentTable, ok := existing[table.Name]
if !ok {
// New table: no rows yet, so the sequence only needs creating.
if stmt := w.primaryKeySequenceStatement(schema, table); stmt != "" {
@@ -2173,7 +2226,7 @@ func (w *Writer) diffStatements(model, current *models.Database) ([]string, erro
// database does not have yet: create it and move it past the data,
// otherwise it restarts at 1 and collides with existing rows.
seqName := primaryKeySequenceName(table)
if seqName == "" || knownSequences[strings.ToLower(seqName)] {
if seqName == "" || knownSequences[seqName] {
continue
}
if cpk := currentTable.GetPrimaryKey(); cpk != nil && columnDefaultsEqual(table.GetPrimaryKey().Default, cpk.Default) {
@@ -2209,7 +2262,7 @@ func (w *Writer) diffStatements(model, current *models.Database) ([]string, erro
continue
}
if script.Schema != "" && script.Schema != lastSchema {
statements = append(statements, fmt.Sprintf("-- Schema: %s", script.Schema))
statements = append(statements, fmt.Sprintf("-- Schema: %s", commentText(script.Schema)))
lastSchema = script.Schema
}
statements = append(statements, body)
@@ -2311,23 +2364,8 @@ func extractTableNameFromCreate(stmt string) string {
rest = strings.TrimSpace(rest[13:])
}
// Get the table name (first token before '(' or whitespace)
tokens := strings.FieldsFunc(rest, func(r rune) bool {
return r == '(' || r == ' ' || r == '\n' || r == '\t'
})
if len(tokens) == 0 {
return ""
}
// Handle schema.table format
fullName := tokens[0]
parts := strings.Split(fullName, ".")
if len(parts) > 1 {
return parts[len(parts)-1]
}
return fullName
_, name := parseQualifiedIdent(rest)
return name
}
// truncateStatement truncates long SQL statements for error messages
@@ -2445,11 +2483,21 @@ func extractSQLStringValue(stmt, key string) string {
// parseQualifiedIdent extracts (schema, name) from the start of s, handling optional "schema"."table" quoting.
func parseQualifiedIdent(s string) (schema, name string) {
token := firstBareIdent(s)
parts := strings.SplitN(token, ".", 2)
if len(parts) == 2 {
return stripQuotes(parts[0]), stripQuotes(parts[1])
quoted := false
for i := 0; i < len(token); i++ {
if token[i] == '"' {
if quoted && i+1 < len(token) && token[i+1] == '"' {
i++
continue
}
quoted = !quoted
}
if token[i] == '.' && !quoted {
return stripQuotes(token[:i]), stripQuotes(token[i+1:])
}
}
return "", stripQuotes(token)
}
// firstBareIdent returns the first whitespace/delimiter-terminated token, stripping outer quotes.
@@ -2458,11 +2506,21 @@ func firstBareIdent(s string) string {
if s == "" {
return ""
}
end := strings.IndexAny(s, " \t\n(,;")
if end == -1 {
return s
quoted := false
for i := 0; i < len(s); i++ {
if s[i] == '"' {
if quoted && i+1 < len(s) && s[i+1] == '"' {
i++
continue
}
quoted = !quoted
}
if !quoted && strings.ContainsRune(" \t\r\n(,;", rune(s[i])) {
return s[:i]
}
}
return s[:end]
return s
}
// firstIdentAfterKeyword returns the first identifier token after keyword (matched case-insensitively via upperStmt).
@@ -2476,7 +2534,10 @@ func firstIdentAfterKeyword(stmt, upperStmt, keyword string) string {
// stripQuotes removes surrounding double-quotes from an identifier.
func stripQuotes(s string) string {
return strings.Trim(s, "\"")
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
return strings.ReplaceAll(s[1:len(s)-1], `""`, `"`)
}
return s
}
// buildStmtContext assembles a display string from available identifiers.
@@ -2598,3 +2659,5 @@ func detectStatementType(stmt string) string {
func quoteIdentifier(s string) string {
return quoteIdent(s)
}
func commentText(s string) string { return strings.NewReplacer("\r", " ", "\n", " ").Replace(s) }