Integrated Migration Workflow
This guide demonstrates CQL's integrated migration system that automatically maintains schema files in sync with your database changes, providing a seamless development experience when using the Active Record pattern.
Overview
CQL's integrated workflow combines three powerful components:
- Migrations: Version-controlled schema changes 
- Schema Synchronization: Automatic schema file updates 
- Active Record Models: Type-safe model definitions 
This integration ensures your schema files always reflect the current database state, enabling compile-time type safety and seamless team collaboration.
Quick Start Example
Here's a complete example showing the integrated workflow:
# 1. Configure the integrated migrator
config = CQL::MigratorConfig.new(
  schema_file_path: "src/schemas/app_schema.cr",
  schema_name: :AppSchema,
  schema_symbol: :app_schema,
  auto_sync: true  # Automatically update schema file after migrations
)
# 2. Initialize your base schema connection
AppDB = CQL::Schema.define(
  :app_database,
  adapter: CQL::Adapter::Postgres,
  uri: ENV["DATABASE_URL"]
) do
  # Tables will be managed by migrations
end
migrator = AppDB.migrator(config)
# 3. Create migrations
class CreateUsersTable < CQL::Migration(1)
  def up
    schema.table :users do
      primary :id, Int32
      column :name, String
      column :email, String
      timestamps
    end
    schema.users.create!
  end
  def down
    schema.users.drop!
  end
end
# 4. Run migrations - schema file automatically updated!
migrator.up
# 5. Use the auto-generated schema in your Active Record models
require "./src/schemas/app_schema"
class User
  include CQL::ActiveRecord::Model(Int32)
  db_context AppSchema, :users
  property id : Int32?
  property name : String
  property email : String
  property created_at : Time?
  property updated_at : Time?
end
# 6. Now you have type-safe Active Record operations
user = User.create!(name: "Alice", email: "alice@example.com")
puts user.id  # Compile-time type safety guaranteed!Configuration Setup
Basic Configuration
# Configure for automatic schema synchronization
config = CQL::MigratorConfig.new(
  schema_file_path: "src/schemas/app_schema.cr",  # Where to save schema file
  schema_name: :AppSchema,                        # Constant name in schema file
  schema_symbol: :app_schema,                     # Symbol name for schema
  auto_sync: true                                 # Auto-update after migrations
)
migrator = MyDB.migrator(config)Environment-Specific Configurations
# Development configuration
dev_config = CQL::MigratorConfig.new(
  schema_file_path: "src/schemas/app_schema.cr",
  schema_name: :AppSchema,
  schema_symbol: :app_schema,
  auto_sync: true  # Rapid iteration with automatic updates
)
# Production configuration
prod_config = CQL::MigratorConfig.new(
  schema_file_path: "src/schemas/production_schema.cr",
  schema_name: :ProductionSchema,
  schema_symbol: :production_schema,
  auto_sync: false  # Manual control for safety
)
# Test configuration
test_config = CQL::MigratorConfig.new(
  schema_file_path: "src/schemas/test_schema.cr",
  schema_name: :TestSchema,
  schema_symbol: :test_schema,
  auto_sync: true
)Migration Development Workflow
1. Initial Bootstrap (Existing Database)
Start with an existing database by bootstrapping your schema file:
# Bootstrap from existing database
migrator.bootstrap_schema
# This generates src/schemas/app_schema.cr with current database structureGenerated AppSchema.cr:
AppSchema = CQL::Schema.define(
  :app_schema,
  adapter: CQL::Adapter::Postgres,
  uri: ENV["DATABASE_URL"]) do
  table :users do
    primary :id, Int32
    text :name
    text :email
    timestamps
  end
end2. Creating Models from Generated Schema
Once you have the schema file, create Active Record models:
# models/user.cr
require "../src/schemas/app_schema"
class User
  include CQL::ActiveRecord::Model(Int32)
  db_context AppSchema, :users
  # Properties match the generated schema
  property id : Int32?
  property name : String
  property email : String
  property created_at : Time?
  property updated_at : Time?
  # Validations
  validate :name, presence: true, size: 2..50
  validate :email, presence: true, match: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
end3. Adding New Features with Migrations
Create migrations to add new features:
# migrations/002_add_user_profile.cr
class AddUserProfile < CQL::Migration(2)
  def up
    # Add new columns to existing table
    schema.alter :users do
      add_column :bio, String, null: true
      add_column :avatar_url, String, null: true
      add_column :active, Bool, default: true
    end
    # Create new related table
    schema.table :user_preferences do
      primary :id, Int32
      column :user_id, Int32
      column :notifications, Bool, default: true
      column :theme, String, default: "light"
      timestamps
      foreign_key [:user_id], references: :users, references_columns: [:id], on_delete: :cascade
    end
    schema.user_preferences.create!
  end
  def down
    schema.user_preferences.drop!
    schema.alter :users do
      drop_column :bio
      drop_column :avatar_url
      drop_column :active
    end
  end
end
# Apply migration - schema file automatically updated!
migrator.up4. Updated Models After Migration
After running the migration, update your models to use the new schema:
# models/user.cr - Updated after migration
class User
  include CQL::ActiveRecord::Model(Int32)
  db_context AppSchema, :users
  # Original properties
  property id : Int32?
  property name : String
  property email : String
  # New properties from migration
  property bio : String?
  property avatar_url : String?
  property active : Bool = true
  property created_at : Time?
  property updated_at : Time?
  # Relationships
  has_one :preferences, UserPreferences, dependent: :destroy
  # Validations
  validate :name, presence: true, size: 2..50
  validate :email, presence: true, match: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
end
# models/user_preferences.cr - New model
class UserPreferences
  include CQL::ActiveRecord::Model(Int32)
  db_context AppSchema, :user_preferences
  property id : Int32?
  property user_id : Int32
  property notifications : Bool = true
  property theme : String = "light"
  property created_at : Time?
  property updated_at : Time?
  belongs_to :user, User
  validate :theme, in: ["light", "dark"]
endAdvanced Migration Patterns
Complex Schema Changes
class RefactorUserSystem < CQL::Migration(3)
  def up
    # Create new tables first
    schema.table :profiles do
      primary :id, Int32
      column :user_id, Int32
      column :first_name, String
      column :last_name, String
      column :bio, String, null: true
      timestamps
      foreign_key [:user_id], references: :users, references_columns: [:id]
    end
    schema.profiles.create!
    # Migrate data (in production, do this carefully)
    schema.exec_query do |conn|
      conn.query_each("SELECT id, name, bio FROM users") do |rs|
        user_id = rs.read(Int32)
        name_parts = rs.read(String).split(" ", 2)
        bio = rs.read(String?)
        first_name = name_parts[0]? || ""
        last_name = name_parts[1]? || ""
        conn.exec(
          "INSERT INTO profiles (user_id, first_name, last_name, bio, created_at, updated_at) VALUES ($1, $2, $3, $4, NOW(), NOW())",
          user_id, first_name, last_name, bio
        )
      end
    end
    # Remove old columns
    schema.alter :users do
      drop_column :name
      drop_column :bio
    end
  end
  def down
    # Reverse the changes
    schema.alter :users do
      add_column :name, String
      add_column :bio, String, null: true
    end
    # Migrate data back
    schema.exec_query do |conn|
      conn.query_each("SELECT user_id, first_name, last_name, bio FROM profiles") do |rs|
        user_id = rs.read(Int32)
        first_name = rs.read(String)
        last_name = rs.read(String)
        bio = rs.read(String?)
        full_name = "#{first_name} #{last_name}".strip
        conn.exec(
          "UPDATE users SET name = $1, bio = $2 WHERE id = $3",
          full_name, bio, user_id
        )
      end
    end
    schema.profiles.drop!
  end
endAdding Indexes for Performance
class AddPerformanceIndexes < CQL::Migration(4)
  def up
    schema.alter :users do
      create_index :idx_users_email, [:email], unique: true
      create_index :idx_users_active, [:active]
    end
    schema.alter :user_preferences do
      create_index :idx_preferences_user_id, [:user_id]
      create_index :idx_preferences_theme, [:theme]
    end
  end
  def down
    schema.alter :user_preferences do
      drop_index :idx_preferences_user_id
      drop_index :idx_preferences_theme
    end
    schema.alter :users do
      drop_index :idx_users_email
      drop_index :idx_users_active
    end
  end
endTeam Collaboration Workflow
1. Developer Workflow
# When starting work on a feature
git pull origin main
migrator.up  # Apply any new migrations
migrator.verify_schema_consistency  # Ensure schema file is current
# Create your migration
class AddUserRoles < CQL::Migration(5)
  def up
    schema.alter :users do
      add_column :role, String, default: "user"
    end
  end
  def down
    schema.alter :users do
      drop_column :role
    end
  end
end
# Apply and test your migration
migrator.up  # Schema file automatically updated
# Update your models
class User
  # ... existing properties ...
  property role : String = "user"
  validate :role, in: ["user", "admin", "moderator"]
end
# Commit both migration and updated schema file
git add migrations/005_add_user_roles.cr
git add src/schemas/app_schema.cr
git commit -m "Add user roles system"2. Code Review Process
# Reviewer can verify schema consistency
migrator.verify_schema_consistency
# => true (schema file matches database after migrations)
# Check what migrations will be applied
migrator.print_pending_migrations
# Apply and verify
migrator.up
puts "Schema file represents database: #{migrator.verify_schema_consistency}"3. Deployment Workflow
# Production deployment configuration
prod_config = CQL::MigratorConfig.new(
  schema_file_path: "src/schemas/production_schema.cr",
  schema_name: :ProductionSchema,
  auto_sync: false  # Manual control in production
)
prod_migrator = ProductionDB.migrator(prod_config)
# 1. Apply migrations
prod_migrator.up
# 2. Verify consistency
unless prod_migrator.verify_schema_consistency
  puts "WARNING: Schema file out of sync!"
  # Manual update if needed
  prod_migrator.update_schema_file
end
# 3. Deploy application with updated schema fileIntegration with Active Record Features
Relationships with Generated Schema
# The integrated workflow makes relationships easy
class User
  include CQL::ActiveRecord::Model(Int32)
  db_context AppSchema, :users  # Uses auto-generated schema
  has_many :posts, Post, dependent: :destroy
  has_one :profile, Profile, dependent: :destroy
  has_many :comments, Comment, dependent: :destroy
  # The schema file ensures all foreign keys exist
end
class Post
  include CQL::ActiveRecord::Model(Int32)
  db_context AppSchema, :posts
  belongs_to :user, User
  has_many :comments, Comment, dependent: :destroy
  # Schema file guarantees foreign key constraints
endValidations Based on Schema
# Schema file provides compile-time guarantees
class User
  # Schema: column :email, String (not null)
  validate :email, presence: true  # Matches schema constraint
  # Schema: column :age, Int32, null: true
  validate :age, gt: 0, lt: 120, allow_nil: true  # Matches nullable
  # Schema: column :role, String, default: "user"
  validate :role, in: ["user", "admin"], allow_blank: false
endScopes with Schema Awareness
class User
  include CQL::ActiveRecord::Model(Int32)
  db_context AppSchema, :users
  # Schema file ensures these columns exist
  scope :active, -> { where(active: true) }
  scope :by_role, ->(role : String) { where(role: role) }
  scope :recent, -> { order(:created_at, :desc) }
  # Compile-time verification of column names
endManual Schema Operations
Manual Schema Updates
# Force update schema file to match current database
migrator.update_schema_file
# Verify schema file matches database
consistent = migrator.verify_schema_consistency
unless consistent
  puts "Schema file is out of sync with database"
  puts "Run: migrator.update_schema_file"
endSchema Inspection
# Get current schema content
schema_dumper = AppDB.schema_dumper
current_schema = schema_dumper.generate_schema_content(:AppSchema, :app_schema)
puts current_schema
# Compare with existing file
if File.exists?("src/schemas/app_schema.cr")
  existing_schema = File.read("src/schemas/app_schema.cr")
  if current_schema == existing_schema
    puts "✅ Schema file is up to date"
  else
    puts "❌ Schema file needs updating"
  end
endBest Practices
1. Version Control Integration
# Always commit both migrations and schema files together
git add migrations/
git add src/schemas/app_schema.cr
git commit -m "Add user authentication system
- Add users table with email/password
- Add sessions table for authentication
- Update schema file automatically"2. CI/CD Pipeline Integration
# In your CI/CD pipeline
migrator = AppDB.migrator(config)
# Ensure migrations are applied
migrator.up
# Verify schema consistency
unless migrator.verify_schema_consistency
  puts "❌ DEPLOYMENT FAILED: Schema file out of sync"
  exit(1)
end
puts "✅ Database and schema file are synchronized"3. Development Environment Setup
# db/setup.cr - New developer setup script
require "../src/config/database"
def setup_development_database
  puts "Setting up development database..."
  # Apply all migrations
  migrator = AppDB.migrator
  migrator.up
  # Verify everything is consistent
  if migrator.verify_schema_consistency
    puts "✅ Development database ready!"
  else
    puts "❌ Schema inconsistency detected"
    migrator.update_schema_file
    puts "Schema file updated"
  end
end
setup_development_database4. Model Generation
# After running migrations, generate model templates
def generate_model_template(table_name : Symbol)
  schema_file = File.read("src/schemas/app_schema.cr")
  # Parse schema and generate model template
  puts "class #{table_name.to_s.camelcase}"
  puts "  include CQL::ActiveRecord::Model(Int32)"
  puts "  db_context AppSchema, :#{table_name}"
  puts ""
  puts "  # Properties based on current schema"
  puts "  # Add your properties here based on the generated schema"
  puts "end"
end
# Generate model for new table
generate_model_template(:user_preferences)Troubleshooting
Schema File Out of Sync
# Check if schema file matches database
consistent = migrator.verify_schema_consistency
if !consistent
  puts "Schema file is out of sync with database"
  # Option 1: Update schema file to match database
  migrator.update_schema_file
  # Option 2: Check what migrations are pending
  migrator.print_pending_migrations
  # Option 3: Apply pending migrations
  migrator.up
endMigration Conflicts
# When migration conflicts occur during team development
begin
  migrator.up
rescue ex : Exception
  puts "Migration failed: #{ex.message}"
  # Check current state
  migrator.print_applied_migrations
  migrator.print_pending_migrations
  # Resolve conflicts and retry
  puts "Resolve conflicts and run again"
endModel Compilation Errors
# When models don't match the schema
class User
  include CQL::ActiveRecord::Model(Int32)
  db_context AppSchema, :users
  # This will cause compilation error if column doesn't exist in schema
  property non_existent_column : String  # ❌ Compile-time error
  # Solution: Check generated schema file and update model
  property name : String  # ✅ Matches schema
endConclusion
The integrated migration workflow provides:
- Automatic Schema Synchronization: No manual schema file maintenance 
- Type Safety: Compile-time guarantees for Active Record models 
- Team Coordination: Consistent schema files across team members 
- Production Safety: Manual control options for deployment 
- Developer Experience: Seamless workflow from migration to model usage 
This workflow eliminates schema drift, reduces manual errors, and provides a smooth development experience when building applications with the Active Record pattern in CQL.
Last updated
Was this helpful?
