Files
relspecgo/pkg/writers/pgsql/TEMPLATES.md
Hein 5e1448dcdb
Some checks are pending
CI / Test (1.23) (push) Waiting to run
CI / Test (1.24) (push) Waiting to run
CI / Test (1.25) (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Build (push) Waiting to run
sql writer
2025-12-17 20:44:02 +02:00

15 KiB

PostgreSQL Migration Templates

Overview

The PostgreSQL migration writer uses Go text templates to generate SQL, making the code much more maintainable and customizable than hardcoded string concatenation.

Architecture

pkg/writers/pgsql/
├── templates/                    # Template files
│   ├── create_table.tmpl        # CREATE TABLE
│   ├── add_column.tmpl          # ALTER TABLE ADD COLUMN
│   ├── alter_column_type.tmpl   # ALTER TABLE ALTER COLUMN TYPE
│   ├── alter_column_default.tmpl # ALTER TABLE ALTER COLUMN DEFAULT
│   ├── create_primary_key.tmpl  # ADD CONSTRAINT PRIMARY KEY
│   ├── create_index.tmpl        # CREATE INDEX
│   ├── create_foreign_key.tmpl  # ADD CONSTRAINT FOREIGN KEY
│   ├── drop_constraint.tmpl     # DROP CONSTRAINT
│   ├── drop_index.tmpl          # DROP INDEX
│   ├── comment_table.tmpl       # COMMENT ON TABLE
│   ├── comment_column.tmpl      # COMMENT ON COLUMN
│   ├── audit_tables.tmpl        # CREATE audit tables
│   ├── audit_function.tmpl      # CREATE audit function
│   └── audit_trigger.tmpl       # CREATE audit trigger
├── templates.go                 # Template executor and data structures
└── migration_writer_templated.go # Templated migration writer

Using Templates

Basic Usage

// Create template executor
executor, err := pgsql.NewTemplateExecutor()
if err != nil {
    log.Fatal(err)
}

// Prepare data
data := pgsql.CreateTableData{
    SchemaName: "public",
    TableName:  "users",
    Columns: []pgsql.ColumnData{
        {Name: "id", Type: "integer", NotNull: true},
        {Name: "name", Type: "text"},
    },
}

// Execute template
sql, err := executor.ExecuteCreateTable(data)
if err != nil {
    log.Fatal(err)
}

fmt.Println(sql)

Using Templated Migration Writer

// Create templated migration writer
writer, err := pgsql.NewTemplatedMigrationWriter(&writers.WriterOptions{
    OutputPath: "migration.sql",
})
if err != nil {
    log.Fatal(err)
}

// Generate migration (uses templates internally)
err = writer.WriteMigration(modelDB, currentDB)
if err != nil {
    log.Fatal(err)
}

Template Data Structures

CreateTableData

For create_table.tmpl:

type CreateTableData struct {
    SchemaName string
    TableName  string
    Columns    []ColumnData
}

type ColumnData struct {
    Name    string
    Type    string
    Default string
    NotNull bool
}

Example:

data := CreateTableData{
    SchemaName: "public",
    TableName:  "products",
    Columns: []ColumnData{
        {Name: "id", Type: "serial", NotNull: true},
        {Name: "name", Type: "text", NotNull: true},
        {Name: "price", Type: "numeric(10,2)", Default: "0.00"},
    },
}

AddColumnData

For add_column.tmpl:

type AddColumnData struct {
    SchemaName string
    TableName  string
    ColumnName string
    ColumnType string
    Default    string
    NotNull    bool
}

CreateIndexData

For create_index.tmpl:

type CreateIndexData struct {
    SchemaName string
    TableName  string
    IndexName  string
    IndexType  string  // btree, hash, gin, gist
    Columns    string  // comma-separated
    Unique     bool
}

CreateForeignKeyData

For create_foreign_key.tmpl:

type CreateForeignKeyData struct {
    SchemaName      string
    TableName       string
    ConstraintName  string
    SourceColumns   string  // comma-separated
    TargetSchema    string
    TargetTable     string
    TargetColumns   string  // comma-separated
    OnDelete        string  // CASCADE, SET NULL, etc.
    OnUpdate        string
}

AuditFunctionData

For audit_function.tmpl:

type AuditFunctionData struct {
    SchemaName      string
    FunctionName    string
    TableName       string
    TablePrefix     string
    PrimaryKey      string
    AuditSchema     string
    UserFunction    string
    AuditInsert     bool
    AuditUpdate     bool
    AuditDelete     bool
    UpdateCondition string
    UpdateColumns   []AuditColumnData
    DeleteColumns   []AuditColumnData
}

type AuditColumnData struct {
    Name     string
    OldValue string  // SQL expression for old value
    NewValue string  // SQL expression for new value
}

Customizing Templates

Modifying Existing Templates

Templates are embedded in the binary but can be modified at compile time:

  1. Edit template file in pkg/writers/pgsql/templates/:
// templates/create_table.tmpl
CREATE TABLE IF NOT EXISTS {{.SchemaName}}.{{.TableName}} (
{{- range $i, $col := .Columns}}
{{- if $i}},{{end}}
  {{$col.Name}} {{$col.Type}}
{{- if $col.Default}} DEFAULT {{$col.Default}}{{end}}
{{- if $col.NotNull}} NOT NULL{{end}}
{{- end}}
);

-- Custom comment
COMMENT ON TABLE {{.SchemaName}}.{{.TableName}} IS 'Auto-generated by RelSpec';
  1. Rebuild the application:
go build ./cmd/relspec

The new template is automatically embedded.

Template Syntax Reference

Variables

{{.FieldName}}           // Access field
{{.SchemaName}}          // String field
{{.NotNull}}             // Boolean field

Conditionals

{{if .NotNull}}
  NOT NULL
{{end}}

{{if .Default}}
  DEFAULT {{.Default}}
{{else}}
  -- No default
{{end}}

Loops

{{range $i, $col := .Columns}}
  Column: {{$col.Name}} Type: {{$col.Type}}
{{end}}

Functions

{{if eq .Type "CASCADE"}}
  ON DELETE CASCADE
{{end}}

{{join .Columns ", "}}   // Join string slice

Creating New Templates

  1. Create template file in pkg/writers/pgsql/templates/:
// templates/custom_operation.tmpl
-- Custom operation for {{.TableName}}
ALTER TABLE {{.SchemaName}}.{{.TableName}}
  {{.CustomOperation}};
  1. Define data structure in templates.go:
type CustomOperationData struct {
    SchemaName      string
    TableName       string
    CustomOperation string
}
  1. Add executor method in templates.go:
func (te *TemplateExecutor) ExecuteCustomOperation(data CustomOperationData) (string, error) {
    var buf bytes.Buffer
    err := te.templates.ExecuteTemplate(&buf, "custom_operation.tmpl", data)
    if err != nil {
        return "", fmt.Errorf("failed to execute custom_operation template: %w", err)
    }
    return buf.String(), nil
}
  1. Use in migration writer:
sql, err := w.executor.ExecuteCustomOperation(CustomOperationData{
    SchemaName:      "public",
    TableName:       "users",
    CustomOperation: "ADD COLUMN custom_field text",
})

Template Examples

Example 1: Custom Table Creation

Modify create_table.tmpl to add table options:

CREATE TABLE IF NOT EXISTS {{.SchemaName}}.{{.TableName}} (
{{- range $i, $col := .Columns}}
{{- if $i}},{{end}}
  {{$col.Name}} {{$col.Type}}
{{- if $col.Default}} DEFAULT {{$col.Default}}{{end}}
{{- if $col.NotNull}} NOT NULL{{end}}
{{- end}}
) WITH (fillfactor = 90);

-- Add automatic comment
COMMENT ON TABLE {{.SchemaName}}.{{.TableName}}
IS 'Created: {{.CreatedDate}} | Version: {{.Version}}';

Example 2: Custom Index with WHERE Clause

Add to create_index.tmpl:

CREATE {{if .Unique}}UNIQUE {{end}}INDEX IF NOT EXISTS {{.IndexName}}
  ON {{.SchemaName}}.{{.TableName}}
  USING {{.IndexType}} ({{.Columns}})
{{- if .Where}}
  WHERE {{.Where}}
{{- end}}
{{- if .Include}}
  INCLUDE ({{.Include}})
{{- end}};

Update data structure:

type CreateIndexData struct {
    SchemaName string
    TableName  string
    IndexName  string
    IndexType  string
    Columns    string
    Unique     bool
    Where      string  // New field for partial indexes
    Include    string  // New field for covering indexes
}

Example 3: Enhanced Audit Function

Modify audit_function.tmpl to add custom logging:

CREATE OR REPLACE FUNCTION {{.SchemaName}}.{{.FunctionName}}()
RETURNS trigger AS
$body$
DECLARE
    m_funcname text = '{{.FunctionName}}';
    m_user text;
    m_atevent integer;
    m_application_name text;
BEGIN
    -- Get current user and application
    m_user := {{.UserFunction}}::text;
    m_application_name := current_setting('application_name', true);

    -- Custom logging
    RAISE NOTICE 'Audit: % on %.% by % from %',
        TG_OP, TG_TABLE_SCHEMA, TG_TABLE_NAME, m_user, m_application_name;

    -- Rest of function...
    ...

Best Practices

1. Keep Templates Simple

Templates should focus on SQL generation. Complex logic belongs in Go code:

Good:

// In Go code
columns := buildColumnList(table)

// In template
{{range .Columns}}
  {{.Name}} {{.Type}}
{{end}}

Bad:

// Don't do complex transformations in templates
{{range .Columns}}
  {{if eq .Type "integer"}}
    {{.Name}} serial
  {{else}}
    {{.Name}} {{.Type}}
  {{end}}
{{end}}

2. Use Descriptive Field Names

// Good
type CreateTableData struct {
    SchemaName string
    TableName  string
}

// Bad
type CreateTableData struct {
    S string  // What is S?
    T string  // What is T?
}

3. Document Template Data

Always document what data a template expects:

// CreateTableData contains data for create table template.
// Used by templates/create_table.tmpl
type CreateTableData struct {
    SchemaName string       // Schema where table will be created
    TableName  string       // Name of the table
    Columns    []ColumnData // List of columns to create
}

4. Handle SQL Injection

Always escape user input:

// In Go code - escape before passing to template
data := CommentTableData{
    SchemaName: schema,
    TableName:  table,
    Comment:    escapeQuote(userComment),  // Escape quotes
}

5. Test Templates Thoroughly

func TestTemplate_CreateTable(t *testing.T) {
    executor, _ := NewTemplateExecutor()

    data := CreateTableData{
        SchemaName: "public",
        TableName:  "test",
        Columns:    []ColumnData{{Name: "id", Type: "integer"}},
    }

    sql, err := executor.ExecuteCreateTable(data)
    if err != nil {
        t.Fatal(err)
    }

    // Verify expected SQL patterns
    if !strings.Contains(sql, "CREATE TABLE") {
        t.Error("Missing CREATE TABLE")
    }
}

Benefits of Template-Based Approach

Maintainability

Before (string concatenation):

sql := fmt.Sprintf(`CREATE TABLE %s.%s (
  %s %s%s%s
);`, schema, table, col, typ,
    func() string {
        if def != "" {
            return " DEFAULT " + def
        }
        return ""
    }(),
    func() string {
        if notNull {
            return " NOT NULL"
        }
        return ""
    }(),
)

After (templates):

sql, _ := executor.ExecuteCreateTable(CreateTableData{
    SchemaName: schema,
    TableName:  table,
    Columns:    columns,
})

Customization

Users can modify templates without changing Go code:

  • Edit template file
  • Rebuild application
  • New SQL generation logic active

Testing

Templates can be tested independently:

func TestAuditTemplate(t *testing.T) {
    executor, _ := NewTemplateExecutor()

    // Test with various data
    for _, testCase := range testCases {
        sql, err := executor.ExecuteAuditFunction(testCase.data)
        // Verify output
    }
}

Readability

SQL templates are easier to read and review than Go string building code.

Migration from Old Writer

To migrate from the old string-based writer to templates:

Option 1: Use TemplatedMigrationWriter

// Old
writer := pgsql.NewMigrationWriter(options)

// New
writer, err := pgsql.NewTemplatedMigrationWriter(options)
if err != nil {
    log.Fatal(err)
}

// Same interface
writer.WriteMigration(model, current)

Option 2: Keep Both

Both writers are available:

  • MigrationWriter - Original string-based
  • TemplatedMigrationWriter - New template-based

Choose based on your needs.

Troubleshooting

Template Not Found

Error: template: "my_template.tmpl" not defined

Solution: Ensure template file exists in templates/ directory and rebuild.

Template Execution Error

Error: template: create_table.tmpl:5:10: executing "create_table.tmpl"
at <.InvalidField>: can't evaluate field InvalidField

Solution: Check data structure has all fields used in template.

Embedded Files Not Updating

If template changes aren't reflected:

  1. Clean build cache: go clean -cache
  2. Rebuild: go build ./cmd/relspec
  3. Verify template file is in templates/ directory

Custom Template Functions

RelSpec provides a comprehensive library of template functions for SQL generation:

String Manipulation

  • upper, lower - Case conversion
  • snake_case, camelCase - Naming convention conversion
  • Usage: {{upper .TableName}}USERS

SQL Formatting

  • indent(spaces, text) - Indent text
  • quote(string) - Quote for SQL with escaping
  • escape(string) - Escape special characters
  • safe_identifier(string) - Make SQL-safe identifier
  • Usage: {{quote "O'Brien"}}'O''Brien'

Type Conversion

  • goTypeToSQL(type) - Convert Go type to PostgreSQL type
  • sqlTypeToGo(type) - Convert PostgreSQL type to Go type
  • isNumeric(type), isText(type) - Type checking
  • Usage: {{goTypeToSQL "int64"}}bigint

Collection Helpers

  • first(slice), last(slice) - Get elements
  • join_with(slice, sep) - Join with custom separator
  • Usage: {{join_with .Columns ", "}}id, name, email

See template_functions.go for full documentation.

Template Inheritance and Composition

RelSpec supports Go template inheritance using {{template}} and {{block}}:

Base Templates

  • base_ddl.tmpl - Common DDL patterns
  • base_constraint.tmpl - Constraint operations
  • fragments.tmpl - Reusable fragments

Using Fragments

{{/* Use predefined fragments */}}
CREATE TABLE {{template "qualified_table" .}} (
{{range .Columns}}
  {{template "column_definition" .}}
{{end}}
);

Template Blocks

{{/* Define with override capability */}}
{{define "table_options"}}
) {{block "storage_options" .}}WITH (fillfactor = 90){{end}};
{{end}}

See TEMPLATE_INHERITANCE.md for detailed guide.

Visual Template Editor

A VS Code extension is available for visual template editing:

Features

  • Live Preview - See rendered SQL as you type
  • IntelliSense - Auto-completion for functions
  • Validation - Syntax checking and error highlighting
  • Scaffolding - Quick template creation
  • Function Browser - Browse available functions

Installation

cd vscode-extension
npm install
npm run compile
code .
# Press F5 to launch

See vscode-extension/README.md for full documentation.

Future Enhancements

Completed:

  • Template inheritance/composition
  • Custom template functions library
  • Visual template editor (VS Code)

Potential future improvements:

  • Parameterized templates (load from config)
  • Template validation CLI tool
  • Template library/marketplace
  • Template versioning
  • Hot-reload during development

Contributing Templates

When contributing new templates:

  1. Place in pkg/writers/pgsql/templates/
  2. Use .tmpl extension
  3. Document data structure in templates.go
  4. Add executor method
  5. Write tests
  6. Update this documentation