Ptah is a schema management tool for relational databases, inspired by the ancient Egyptian god of creation. In mythology, Ptah brought the world into existence through thought and speech—shaping order from chaos. This tool follows a similar philosophy: it turns structured Go code into coherent, executable database schemas, ensuring consistency between code and data.
The name Ptah is also an acronym:
P.T.A.H. — Parse, Transform, Apply, Harmonize
- Parse – extract schema definitions from annotated Go structs
- Transform – generate SQL DDL and schema diffs
- Apply – execute up/down migrations with version tracking
- Harmonize – synchronize code-defined schema with actual database state
ptah provides a unified workflow to define, evolve, and apply database schemas based on Go code annotations. Its main
capabilities include:
-
📘 Go Struct Parsing Extracts tables, columns, indexes, foreign keys, and constraints from structured comments in Go code.
-
🧱 Schema Generation (DDL) Builds platform-specific
CREATE TABLE,CREATE INDEX, and other DDL statements. -
🔐 Row-Level Security (PostgreSQL) Supports PostgreSQL RLS policies and custom functions for multi-tenant data isolation.
-
🔍 Database Introspection Reads the current schema directly from Postgres or MySQL for comparison and analysis.
-
🧮 Schema Diffing Compares code-based schema with the live database schema using AST representations.
-
🪄 Migration Generation Automatically generates
upanddownSQL migrations to bring the database in sync. -
🚀 Migration Execution Applies versioned migrations in both directions, tracking state via a migrations table.
-
💥 Database Cleaning Drops all user-defined schema objects—useful for testing or re-initialization.
Ptah is organized into several key packages that work together to provide comprehensive database schema management:
The core package contains all fundamental components for parsing, transforming, and representing database schemas:
-
ast/- Abstract Syntax Tree representation for SQL DDL statements- Provides database-agnostic AST nodes for CREATE TABLE, ALTER TABLE, CREATE INDEX, etc.
- Implements visitor pattern for dialect-specific SQL generation
- Core node types:
CreateTableNode,AlterTableNode,ColumnNode,ConstraintNode,IndexNode,EnumNode
-
astbuilder/- Fluent API for building SQL DDL AST nodes- Provides convenient builder pattern for constructing complex schemas
- SchemaBuilder, TableBuilder, ColumnBuilder, IndexBuilder interfaces
- Integrates seamlessly with the core AST package
-
goschema/- Go package parsing and entity extraction- Recursively parses Go source files to discover entity definitions
- Extracts table directives, field mappings, indexes, enums, and embedded fields
- Handles dependency analysis and topological sorting for proper table creation order
-
parser/- SQL DDL token-to-AST parser- Converts SQL DDL tokens into Abstract Syntax Tree nodes
- Supports CREATE TABLE, ALTER TABLE, CREATE INDEX, CREATE TYPE statements
- Provides comprehensive error handling and position information
-
lexer/- SQL tokenization and lexical analysis- Tokenizes SQL input for parser consumption
- Handles strings, comments, identifiers, operators, and whitespace
- Provides position tracking for error reporting
-
renderer/- Dialect-specific SQL generation from AST- Converts AST nodes to database-specific SQL statements
- Supports PostgreSQL, MySQL, MariaDB dialects
- Implements visitor pattern for extensible rendering
-
platform/- Database platform constants and identifiers- Defines platform-specific constants used throughout the system
- Provides standardized platform identification
-
sqlutil/- SQL utility functions- SQL statement splitting and comment removal
- AST-based parsing for proper handling of strings and comments
-
convert/- Schema conversion utilities- Converts between different schema representations
- Handles transformations between goschema and database schema formats
Provides comprehensive database migration functionality:
-
generator/- Dynamic migration file generation- Generates up/down migration files from schema differences
- Compares Go entities with current database state
- Creates timestamped migration files with proper SQL
-
migrator/- Migration execution engine- Applies and rolls back database migrations
- Tracks migration history and versions
- Provides dry-run capabilities and transaction safety
-
planner/- Migration planning and SQL generation- Converts schema differences into executable SQL statements
- Dialect-specific planners for PostgreSQL, MySQL, MariaDB
- Handles dependency ordering and safety checks
-
schemadiff/- Schema comparison and difference analysis- Compares generated schemas with live database schemas
- Identifies tables, columns, indexes, and enum differences
- Provides detailed change analysis for migration planning
Handles all database interactions and schema operations:
- Connection management for PostgreSQL, MySQL, MariaDB
- Schema reading and introspection from live databases
- Schema writing and migration execution with transaction support
- Database cleaning and schema dropping capabilities
- Type definitions for database schema representation
Provides command-line interface for all Ptah operations:
generate- Generate SQL schema from Go entities without touching databaseread-db- Read and display current database schemacompare- Compare Go entities with current database schemamigrate- Generate migration SQL for schema differencesmigrate-up- Apply migrations to bring database up to latest versionmigrate-down- Roll back migrations to previous versionsmigrate-status- Show current migration status and historydrop-all- Drop ALL tables and enums in database (VERY DANGEROUS!) (supports--dry-run)integration-test- Run comprehensive integration tests across database platforms
ast_demo/- Demonstrates AST-based SQL generationmigrator_parser/- Shows parsing and generation workflow
framework.go- Core test framework with TestRunner and DatabaseHelperreporter.go- Report generation in multiple formats (TXT, JSON, HTML)scenarios.go- Basic test scenarios implementationscenarios_advanced.go- Advanced test scenarios (concurrency, idempotency)scenarios_misc.go- Miscellaneous test scenarios (timestamps, permissions)fixtures/- Test data including migrations and entity definitions
Contains sample Go structs with schema annotations for testing and demonstration:
product.go,category.go- Real-world entity examples- Various test files showing different annotation patterns and features
system_design.md- Comprehensive system architecture documentationdiagrams/- System architecture diagrams and visual documentation
Ptah uses structured comments to define database schema information directly in Go structs. Here's the annotation format:
//migrator:schema:table name="products" platform.mysql.engine="InnoDB" platform.mysql.comment="Product catalog"
type Product struct {
// fields...
}//migrator:schema:field name="id" type="SERIAL" primary="true" platform.mysql.type="INT AUTO_INCREMENT"
ID int64
//migrator:schema:field name="name" type="VARCHAR(255)" not_null="true"
Name string
//migrator:schema:field name="price" type="DECIMAL(10,2)" not_null="true" check="price > 0"
Price float64
//migrator:schema:field name="status" type="ENUM" enum="active,inactive,discontinued" not_null="true" default="active"
Status string
//migrator:schema:field name="category_id" type="INT" not_null="true" foreign="categories(id)" foreign_key_name="fk_product_category"
CategoryID int64//migrator:schema:index name="idx_products_category" fields="category_id"
_ int//migrator:schema:extension name="pg_trgm" if_not_exists="true" comment="Trigram similarity for text search"
//migrator:schema:extension name="btree_gin" if_not_exists="true" comment="GIN indexes for btree operations"
//migrator:schema:extension name="postgis" version="3.0" comment="Geographic data support"
type DatabaseExtensions struct{}//migrator:schema:function name="set_tenant_context" params="tenant_id_param TEXT" returns="VOID" language="plpgsql" security="DEFINER" body="BEGIN PERFORM set_config('app.current_tenant_id', tenant_id_param, false); END;" comment="Sets the current tenant context for RLS"
//migrator:schema:function name="get_current_tenant_id" returns="TEXT" language="plpgsql" volatility="STABLE" body="BEGIN RETURN current_setting('app.current_tenant_id', true); END;" comment="Gets the current tenant ID from session"
type User struct {
// fields...
}// Enable RLS on a table
//migrator:schema:rls:enable table="users" comment="Enable RLS for multi-tenant isolation"
// Create RLS policy
//migrator:schema:rls:policy name="user_tenant_isolation" table="users" for="ALL" to="inventario_app" using="tenant_id = get_current_tenant_id()" comment="Ensures users can only access their tenant's data"
// Policy with WITH CHECK clause for INSERT/UPDATE
//migrator:schema:rls:policy name="product_tenant_isolation" table="products" for="ALL" to="inventario_app" using="tenant_id = get_current_tenant_id()" with_check="tenant_id = get_current_tenant_id()" comment="Ensures products are isolated by tenant"
//migrator:schema:table name="users"
type User struct {
//migrator:schema:field name="tenant_id" type="TEXT" not_null="true"
TenantID string
// other fields...
}name- Database column/table nametype- SQL data typeprimary- Primary key constraintnot_null- NOT NULL constraintunique- UNIQUE constraintdefault- Default valuedefault_fn- Default function (e.g., "NOW()")check- CHECK constraintforeign- Foreign key reference (table(column))foreign_key_name- Custom foreign key constraint nameenum- Enum values (comma-separated)platform.{dialect}.{attribute}- Platform-specific overrides
name- Extension name (e.g., "pg_trgm", "postgis")version- Specific version to install (optional)if_not_exists- Skip if extension already exists ("true" or "false")comment- Extension description
name- Function nameparams- Function parameters (e.g., "tenant_id_param TEXT, user_id INTEGER")returns- Return type (e.g., "VOID", "TEXT", "INTEGER")language- Function language (e.g., "plpgsql", "sql")security- Security context ("DEFINER" or "INVOKER")volatility- Function volatility ("STABLE", "IMMUTABLE", "VOLATILE")body- Function implementation codecomment- Function description
name- Policy nametable- Target table namefor- Operations policy applies to ("ALL", "SELECT", "INSERT", "UPDATE", "DELETE")to- Target database roles (e.g., "app_user", "PUBLIC")using- USING clause expression for row filteringwith_check- WITH CHECK clause expression for INSERT/UPDATE validationcomment- Policy description
name- Constraint nametable- Target table nametype- Constraint type ("CHECK", "UNIQUE", "FOREIGN KEY", "EXCLUDE")columns- Column names for UNIQUE and FOREIGN KEY constraints (comma-separated)expression- CHECK constraint expressionforeign_table- Referenced table for FOREIGN KEY constraintsforeign_column- Referenced column for FOREIGN KEY constraintson_delete- Foreign key ON DELETE action ("CASCADE", "SET NULL", "RESTRICT", "NO ACTION")on_update- Foreign key ON UPDATE action ("CASCADE", "SET NULL", "RESTRICT", "NO ACTION")using- Index method for EXCLUDE constraints (e.g., "gist", "btree")elements- EXCLUDE constraint elements (e.g., "room_id WITH =, during WITH &&")condition- WHERE condition for EXCLUDE constraints
# Clone the repository
git clone https://github.com/stokaro/ptah.git
cd ptah
# Build the CLI tool
go build -o package-migrator ./cmd- Define your entities with schema annotations:
//migrator:schema:table name="users"
type User struct {
//migrator:schema:field name="id" type="SERIAL" primary="true"
ID int64
//migrator:schema:field name="email" type="VARCHAR(255)" not_null="true" unique="true"
Email string
//migrator:schema:field name="created_at" type="TIMESTAMP" not_null="true" default_fn="NOW()"
CreatedAt time.Time
}
// Example with EXCLUDE constraint (PostgreSQL only)
//migrator:schema:table name="bookings"
//migrator:schema:constraint name="no_overlapping_bookings" type="EXCLUDE" table="bookings" using="gist" elements="room_id WITH =, during WITH &&"
type Booking struct {
//migrator:schema:field name="id" type="SERIAL" primary="true"
ID int64
//migrator:schema:field name="room_id" type="INTEGER" not_null="true"
RoomID int
//migrator:schema:field name="during" type="TSRANGE" not_null="true"
During string // PostgreSQL range type
}- Generate SQL schema:
# Generate for PostgreSQL
./package-migrator generate --root-dir ./models --dialect postgres
# Generate for MySQL
./package-migrator generate --root-dir ./models --dialect mysql- Compare and migrate:
# Compare current database with Go entities
./package-migrator compare --root-dir ./models --db-url postgres://user:pass@localhost/db
# Generate migration SQL
./package-migrator migrate --root-dir ./models --db-url postgres://user:pass@localhost/db
# Apply migrations to database
./package-migrator migrate-up --db-url postgres://user:pass@localhost/db --migrations-dir ./migrationsGenerate SQL DDL statements from Go entities without touching the database:
# Generate for all supported dialects
./package-migrator generate --root-dir ./models
# Generate for specific dialect
./package-migrator generate --root-dir ./models --dialect postgres
./package-migrator generate --root-dir ./models --dialect mysql
./package-migrator generate --root-dir ./models --dialect mariadbRead and display the current database schema:
./package-migrator read-db --db-url postgres://user:pass@localhost:5432/databaseOutput: Complete schema information including tables, columns, constraints, indexes, and enums
Compare your Go entities with the current database schema:
./package-migrator compare --root-dir ./models --db-url postgres://user:pass@localhost:5432/databaseOutput: Detailed differences showing what needs to be added, removed, or modified
Generate SQL migration statements to synchronize schemas:
./package-migrator migrate --root-dir ./models --db-url postgres://user:pass@localhost:5432/databaseOutput: SQL statements to bring the database in sync with Go entities
Ptah provides a comprehensive migration system with versioning, rollback capabilities, and transaction safety.
Apply all pending migrations to bring database up to latest version:
./package-migrator migrate-up --db-url postgres://user:pass@localhost:5432/database --migrations-dir ./migrations
# Dry run to preview what would be applied
./package-migrator migrate-up --db-url postgres://user:pass@localhost:5432/database --migrations-dir ./migrations --dry-runFeatures:
- ✅ Applies migrations in correct order based on version numbers
- ✅ Each migration runs in its own transaction
- ✅ Automatic rollback on failure
- ✅ Tracks applied migrations in
schema_migrationstable - ✅ Supports dry-run mode for preview
Roll back migrations to a specific version:
./package-migrator migrate-down --db-url postgres://user:pass@localhost:5432/database --migrations-dir ./migrations --target 5
# Dry run to preview rollback
./package-migrator migrate-down --db-url postgres://user:pass@localhost:5432/database --migrations-dir ./migrations --target 5 --dry-runFeatures:
- ✅ Rolls back to any previous version
- ✅ Executes down migrations in reverse order
- ✅ Transaction safety with automatic rollback on failure
- ✅ Updates migration tracking table
Show current migration status and pending migrations:
./package-migrator migrate-status --db-url postgres://user:pass@localhost:5432/database --migrations-dir ./migrations
# JSON output for automation
./package-migrator migrate-status --db-url postgres://user:pass@localhost:5432/database --migrations-dir ./migrations --jsonOutput includes:
- Current database version
- Total available migrations
- List of pending migrations
- Migration history and timestamps
Generate timestamped migration files from schema differences using the migration generator package:
// Example using the migration generator package
package main
import (
"fmt"
"log"
"github.com/stokaro/ptah/migration/generator"
)
func main() {
opts := generator.GenerateMigrationOptions{
GoEntitiesDir: "./models",
DatabaseURL: "postgres://user:pass@localhost:5432/database",
MigrationName: "add_user_table",
OutputDir: "./migrations",
}
files, err := generator.GenerateMigration(opts)
if err != nil {
log.Fatal(err)
}
// Check if any migration was generated (nil means no changes detected)
if files == nil {
fmt.Println("No schema changes detected - no migration needed")
return
}
fmt.Printf("Generated: %s and %s\n", files.UpFile, files.DownFile)
}Generated files:
YYYYMMDDHHMMSS_add_user_table.up.sql- Forward migrationYYYYMMDDHHMMSS_add_user_table.down.sql- Rollback migration
Advanced Usage with Embedded Filesystem:
//go:embed entities
var entitiesFS embed.FS
opts := generator.GenerateMigrationOptions{
GoEntitiesDir: ".",
GoEntitiesFS: entitiesFS, // Use embedded filesystem
DatabaseURL: "postgres://user:pass@localhost:5432/database",
MigrationName: "add_user_table",
OutputDir: "./migrations",
}
files, err := generator.GenerateMigration(opts)Using Existing Database Connection:
// Reuse existing connection
conn, err := dbschema.ConnectToDatabase(dbURL)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
opts := generator.GenerateMigrationOptions{
GoEntitiesDir: "./models",
DBConn: conn, // Reuse connection instead of creating new one
MigrationName: "add_user_table",
OutputDir: "./migrations",
}Features:
- ✅ Automatic timestamp-based versioning
- ✅ Generates both up and down migrations
- ✅ Compares Go entities with live database
- ✅ Handles table, column, index, and constraint changes
- ✅ Database-specific SQL generation
- ✅ Embedded filesystem support for Go modules
- ✅ Connection reuse for better performance
- ✅ No-op detection (returns nil when no changes needed)
Drop ALL tables and enums in the database (VERY DANGEROUS!):
./package-migrator drop-all --db-url postgres://user:pass@localhost:5432/database
# Dry run to see what would be dropped
./package-migrator drop-all --db-url postgres://user:pass@localhost:5432/database --dry-runRun comprehensive integration tests across multiple database platforms:
# Run all integration tests across all databases
./package-migrator integration-test
# Run tests for specific databases
./package-migrator integration-test --databases postgres,mysql
# Run specific test scenarios
./package-migrator integration-test --scenarios apply_incremental_migrations,rollback_migrations
# Generate detailed HTML report
./package-migrator integration-test --report html
# Verbose output with detailed logging
./package-migrator integration-test --verboseFeatures:
- ✅ Tests across PostgreSQL, MySQL, and MariaDB
- ✅ Comprehensive scenario coverage (basic, concurrency, idempotency, failure recovery)
- ✅ Multiple report formats (TXT, JSON, HTML)
- ✅ Automated database setup and cleanup
package main
import (
"fmt"
"github.com/stokaro/ptah/core/ast"
"github.com/stokaro/ptah/core/renderer"
)
func main() {
// Create a table using the AST API
table := ast.NewCreateTable("users").
AddColumn(
ast.NewColumn("id", "SERIAL").
SetPrimary().
SetAutoIncrement(),
).
AddColumn(
ast.NewColumn("email", "VARCHAR(255)").
SetNotNull().
SetUnique(),
).
AddColumn(
ast.NewColumn("created_at", "TIMESTAMP").
SetDefaultFunction("CURRENT_TIMESTAMP"),
).
AddConstraint(ast.NewUniqueConstraint("uk_users_email", "email"))
// Render for PostgreSQL
pgSQL, err := renderer.RenderSQL("postgresql", table)
if err != nil {
panic(err)
}
fmt.Println("PostgreSQL:")
fmt.Println(pgSQL)
// Render for MySQL
mysqlSQL, err := renderer.RenderSQL("mysql", table)
if err != nil {
panic(err)
}
fmt.Println("MySQL:")
fmt.Println(mysqlSQL)
}package main
import (
"fmt"
"github.com/stokaro/ptah/core/goschema"
"github.com/stokaro/ptah/core/renderer"
)
func main() {
// Parse Go entities from a directory
result, err := goschema.ParseDir("./models")
if err != nil {
panic(err)
}
// Generate ordered CREATE TABLE statements
statements := renderer.GetOrderedCreateStatements(result, "postgresql")
for _, stmt := range statements {
fmt.Println(stmt)
}
}package main
import (
"fmt"
"github.com/stokaro/ptah/core/goschema"
"github.com/stokaro/ptah/migration/schemadiff"
"github.com/stokaro/ptah/migration/planner"
"github.com/stokaro/ptah/dbschema"
)
func main() {
// Parse Go entities
generated, err := goschema.ParseDir("./models")
if err != nil {
panic(err)
}
// Connect to database and read schema
dbURL := "postgres://user:pass@localhost:5432/database"
conn, err := dbschema.ConnectToDatabase(dbURL)
if err != nil {
panic(err)
}
defer conn.Close()
database, err := conn.Reader().ReadSchema()
if err != nil {
panic(err)
}
// Compare schemas
diff := schemadiff.Compare(generated, database)
// Generate migration SQL
migrationSQL := planner.GenerateSchemaDiffSQLStatements(diff, generated, "postgres")
for _, stmt := range migrationSQL {
fmt.Println(stmt)
}
}Ptah includes a comprehensive testing framework with both unit tests and integration tests across multiple database platforms.
# Run all unit tests (no database required)
go test ./...
# Run tests with verbose output
go test -v ./...
# Run specific package tests
go test -v ./core/...
go test -v ./migration/...Ptah includes a comprehensive integration testing framework that validates migration functionality across PostgreSQL, MySQL, and MariaDB.
# Run all integration tests across all databases
./package-migrator integration-test
# Run tests for specific databases
./package-migrator integration-test --databases postgres,mysql
# Run specific test scenarios
./package-migrator integration-test --scenarios apply_incremental_migrations,rollback_migrations
# Generate detailed HTML report
./package-migrator integration-test --report html
# Verbose output with detailed logging
./package-migrator integration-test --verboseThe integration test suite covers:
🧱 Basic Functionality
- Apply incremental migrations
- Roll back migrations
- Upgrade to specific version
- Check current version
- Generate desired schema
- Read actual DB schema
- Dry-run support
- Operation planning
- Schema diff validation
- Failure diagnostics
🔁 Idempotency
- Re-apply already applied migrations
- Run migrate up when database is already up-to-date
🔀 Concurrency
- Launch parallel migrate up processes
- Ensure locking prevents double-apply
🧪 Partial Failure Recovery
- Handle multi-step migrations with intentional failures
- Validate recovery and rollback capabilities
⏱ Timestamp Verification
- Check that
applied_attimestamps are stored correctly
📂 Manual Patch Detection
- Detect manual schema changes via schema diff
🔒 Permission Restrictions
- Test behavior with limited database privileges
🧹 Cleanup Support
- Drop all tables and re-run from empty state
For integration tests, you can use Docker to set up test databases:
# Start PostgreSQL container
docker run --name test-postgres \
-e POSTGRES_PASSWORD=testpass \
-e POSTGRES_DB=testdb \
-p 5432:5432 -d postgres:15
# Run tests
go test -v ./executor/... -tags=integration
# Test with real database
./package-migrator read-db --db-url postgres://postgres:testpass@localhost:5432/testdb# Start MySQL container
docker run --name test-mysql \
-e MYSQL_ROOT_PASSWORD=testpass \
-e MYSQL_DATABASE=testdb \
-p 3306:3306 -d mysql:8.0
# Test with real database
./package-migrator read-db --db-url mysql://root:testpass@tcp(localhost:3306)/testdb- Parse - Go source files are parsed to extract schema annotations
- Transform - Annotations are converted to internal schema representations
- Generate - Schema representations are converted to AST nodes
- Render - AST nodes are rendered to dialect-specific SQL
- Execute - SQL is executed against the target database
- Database Agnostic: Core logic works with any supported database
- AST-Based: Uses Abstract Syntax Trees for type-safe SQL generation
- Visitor Pattern: Enables dialect-specific rendering without modifying core AST
- Dependency Aware: Automatically handles table creation order based on foreign keys
- Transaction Safe: All operations are wrapped in transactions for consistency
- PostgreSQL - Full support including enums, constraints (CHECK, UNIQUE, FOREIGN KEY, EXCLUDE), indexes, RLS policies, and extensions
- MySQL - Full support with MySQL-specific optimizations
- MariaDB - Full support with MariaDB-specific features
You can specify platform-specific attributes in your annotations:
//migrator:schema:table name="products" platform.mysql.engine="InnoDB" platform.mysql.comment="Product catalog"
type Product struct {
//migrator:schema:field name="id" type="SERIAL" platform.mysql.type="INT AUTO_INCREMENT" platform.mariadb.type="INT AUTO_INCREMENT"
ID int64
}Ptah supports embedded fields with different relation modes:
type Address struct {
Street string
City string
}
//migrator:schema:table name="users"
type User struct {
ID int64
// Embedded as separate columns
//migrator:schema:embedded mode="columns"
Address Address
// Embedded as JSON
//migrator:schema:embedded mode="json" name="address_data" type="JSONB"
Metadata Address
}Define enums for type safety:
//migrator:schema:field name="status" type="ENUM" enum="active,inactive,pending" not_null="true" default="active"
Status stringFor PostgreSQL, this creates a proper ENUM type. For MySQL/MariaDB, it uses the ENUM column type.
Ptah supports PostgreSQL's Row-Level Security (RLS) for implementing multi-tenant data isolation at the database level:
package main
// Define helper functions for tenant context management
//migrator:schema:function name="set_tenant_context" params="tenant_id_param TEXT" returns="VOID" language="plpgsql" security="DEFINER" body="BEGIN PERFORM set_config('app.current_tenant_id', tenant_id_param, false); END;" comment="Sets the current tenant context for RLS"
//migrator:schema:function name="get_current_tenant_id" returns="TEXT" language="plpgsql" volatility="STABLE" body="BEGIN RETURN current_setting('app.current_tenant_id', true); END;" comment="Gets the current tenant ID from session"
// Enable RLS and create policies for users table
//migrator:schema:rls:enable table="users" comment="Enable RLS for multi-tenant isolation"
//migrator:schema:rls:policy name="user_tenant_isolation" table="users" for="ALL" to="app_role" using="tenant_id = get_current_tenant_id()" comment="Ensures users can only access their tenant's data"
//migrator:schema:table name="users" comment="User accounts table"
type User struct {
//migrator:schema:field name="id" type="SERIAL" primary="true"
ID int64 `json:"id" db:"id"`
//migrator:schema:field name="tenant_id" type="TEXT" not_null="true"
TenantID string `json:"tenant_id" db:"tenant_id"`
//migrator:schema:field name="email" type="VARCHAR(255)" not_null="true" unique="true"
Email string `json:"email" db:"email"`
//migrator:schema:field name="name" type="VARCHAR(255)" not_null="true"
Name string `json:"name" db:"name"`
}
// Enable RLS and create policies for products table with INSERT/UPDATE checks
//migrator:schema:rls:enable table="products" comment="Enable RLS for product isolation"
//migrator:schema:rls:policy name="product_tenant_isolation" table="products" for="ALL" to="app_role" using="tenant_id = get_current_tenant_id()" with_check="tenant_id = get_current_tenant_id()" comment="Ensures products are isolated by tenant"
//migrator:schema:table name="products" comment="Product catalog table"
type Product struct {
//migrator:schema:field name="id" type="SERIAL" primary="true"
ID int64 `json:"id" db:"id"`
//migrator:schema:field name="tenant_id" type="TEXT" not_null="true"
TenantID string `json:"tenant_id" db:"tenant_id"`
//migrator:schema:field name="name" type="VARCHAR(255)" not_null="true"
Name string `json:"name" db:"name"`
//migrator:schema:field name="user_id" type="INTEGER" not_null="true" foreign="users(id)"
UserID int64 `json:"user_id" db:"user_id"`
}This generates the following PostgreSQL SQL:
-- Create helper functions
CREATE OR REPLACE FUNCTION set_tenant_context(tenant_id_param TEXT) RETURNS VOID AS $$
BEGIN PERFORM set_config('app.current_tenant_id', tenant_id_param, false); END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE OR REPLACE FUNCTION get_current_tenant_id() RETURNS TEXT AS $$
BEGIN RETURN current_setting('app.current_tenant_id', true); END;
$$ LANGUAGE plpgsql STABLE;
-- Enable RLS on tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
-- Create RLS policies
CREATE POLICY user_tenant_isolation ON users FOR ALL TO app_role
USING (tenant_id = get_current_tenant_id());
CREATE POLICY product_tenant_isolation ON products FOR ALL TO app_role
USING (tenant_id = get_current_tenant_id())
WITH CHECK (tenant_id = get_current_tenant_id());Note: RLS and custom functions are PostgreSQL-specific features. For other databases, these annotations are ignored during SQL generation.
Ptah supports PostgreSQL's EXCLUDE constraints for preventing overlapping or conflicting data:
package main
// Prevent overlapping room bookings
//migrator:schema:table name="bookings"
//migrator:schema:constraint name="no_overlapping_bookings" type="EXCLUDE" table="bookings" using="gist" elements="room_id WITH =, during WITH &&"
type Booking struct {
//migrator:schema:field name="id" type="SERIAL" primary="true"
ID int64
//migrator:schema:field name="room_id" type="INTEGER" not_null="true"
RoomID int
//migrator:schema:field name="during" type="TSRANGE" not_null="true"
During string // PostgreSQL range type
}
// EXCLUDE constraint with WHERE condition
//migrator:schema:table name="user_sessions"
//migrator:schema:constraint name="one_active_session_per_user" type="EXCLUDE" table="user_sessions" using="gist" elements="user_id WITH =" condition="is_active = true"
type UserSession struct {
//migrator:schema:field name="id" type="SERIAL" primary="true"
ID int64
//migrator:schema:field name="user_id" type="INTEGER" not_null="true"
UserID int
//migrator:schema:field name="is_active" type="BOOLEAN" not_null="true" default="false"
IsActive bool
}Generated SQL:
-- For bookings table
ALTER TABLE bookings ADD CONSTRAINT no_overlapping_bookings
EXCLUDE USING gist (room_id WITH =, during WITH &&);
-- For user_sessions table
ALTER TABLE user_sessions ADD CONSTRAINT one_active_session_per_user
EXCLUDE USING gist (user_id WITH =) WHERE (is_active = true);EXCLUDE Constraint Features:
- Index Methods: Supports
gist,btree, and other PostgreSQL index methods - Multiple Elements: Can exclude on multiple columns with different operators
- WHERE Conditions: Optional WHERE clause for conditional exclusion
- Operator Classes: Supports PostgreSQL operator classes (=, &&, <>, etc.)
Common Use Cases:
- Room Booking Systems: Prevent overlapping time slots for the same resource
- Session Management: Ensure only one active session per user
- Spatial Data: Prevent overlapping geographic regions
- Scheduling: Avoid conflicting appointments or events
Note: EXCLUDE constraints are PostgreSQL-specific. For other databases, these annotations generate warnings during migration planning.
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Run the test suite
- Submit a pull request
# Clone the repository
git clone https://github.com/stokaro/ptah.git
cd ptah
# Install dependencies
go mod download
# Run tests
go test ./...
# Build the CLI
go build -o package-migrator ./cmdThis project is part of the Inventario system and follows the same licensing terms.
- ✅ Migration versioning and rollback capabilities - Full migration system with up/down migrations, version tracking, and rollback support
- ✅ Comprehensive integration testing - Multi-database testing framework with PostgreSQL, MySQL, and MariaDB support
- ✅ PostgreSQL extensions support - Support for PostgreSQL extensions in schema definitions
- ✅ PostgreSQL EXCLUDE constraints - Full support for EXCLUDE constraints with USING methods, elements, and WHERE conditions
- ✅ Migration file generation - Automatic generation of timestamped migration files from schema differences
- ✅ Dry-run capabilities - Preview operations before execution across all commands
- ✅ Transaction safety - All operations wrapped in transactions for consistency
- Enhanced schema validation - Advanced validation and linting capabilities
- Performance optimizations - Optimizations for large schemas and complex migrations
- Additional database dialects - SQLite, SQL Server support
- Web UI for schema visualization - Interactive schema browser and migration management
- Import from existing databases - Reverse engineering existing schemas to Go entities
- Export capabilities - Export to GraphQL schemas, OpenAPI specs, and other formats
- Runtime performance monitoring - Migration performance tracking and optimization
- Schema versioning - Git-like versioning for schema definitions
- Advanced conflict resolution - Smart handling of schema conflicts and merges