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:
- 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';
- 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
- Create template file in
pkg/writers/pgsql/templates/:
// templates/custom_operation.tmpl
-- Custom operation for {{.TableName}}
ALTER TABLE {{.SchemaName}}.{{.TableName}}
{{.CustomOperation}};
- Define data structure in
templates.go:
type CustomOperationData struct {
SchemaName string
TableName string
CustomOperation string
}
- 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
}
- 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-basedTemplatedMigrationWriter- 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:
- Clean build cache:
go clean -cache - Rebuild:
go build ./cmd/relspec - 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 conversionsnake_case,camelCase- Naming convention conversion- Usage:
{{upper .TableName}}→USERS
SQL Formatting
indent(spaces, text)- Indent textquote(string)- Quote for SQL with escapingescape(string)- Escape special characterssafe_identifier(string)- Make SQL-safe identifier- Usage:
{{quote "O'Brien"}}→'O''Brien'
Type Conversion
goTypeToSQL(type)- Convert Go type to PostgreSQL typesqlTypeToGo(type)- Convert PostgreSQL type to Go typeisNumeric(type),isText(type)- Type checking- Usage:
{{goTypeToSQL "int64"}}→bigint
Collection Helpers
first(slice),last(slice)- Get elementsjoin_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 patternsbase_constraint.tmpl- Constraint operationsfragments.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:
- Place in
pkg/writers/pgsql/templates/ - Use
.tmplextension - Document data structure in
templates.go - Add executor method
- Write tests
- Update this documentation