arrow-left

Only this pageAll pages
gitbookPowered by GitBook
1 of 85

Cql

Loading...

Loading...

Tutorials

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

How-to Guides

Loading...

Models

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Relationships

Loading...

Loading...

Loading...

Loading...

Querying

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Data Operations

Loading...

Loading...

Loading...

Loading...

Migrations

Loading...

Loading...

Loading...

Loading...

Loading...

Configuration

Loading...

Loading...

Loading...

Loading...

Caching

Loading...

Loading...

Loading...

Performance

Loading...

Loading...

Loading...

Testing

Loading...

Loading...

Troubleshooting

Loading...

Loading...

Loading...

Reference

Loading...

Loading...

API Reference

Loading...

Loading...

Loading...

Resources

Loading...

Loading...

Explanation

Loading...

Loading...

Loading...

Loading...

Resources

Loading...

Loading...

Loading...

Loading...

Installation

Complete setup guide for CQL with PostgreSQL, MySQL, and SQLite.

hashtag
Prerequisites

Crystal Language: Version 1.12.2 or higher

Database Server: Choose one or more:

  • SQLite - Built into most systems (development/testing)

  • PostgreSQL - brew install postgresql (production recommended)

  • MySQL - brew install mysql (legacy support)

hashtag
Quick Start

hashtag
1. Create Crystal Project

hashtag
2. Add Dependencies

hashtag
3. Install and Test

hashtag
4. Basic Setup

hashtag
Database-Specific Setup

hashtag
PostgreSQL Configuration

Connection Examples:

hashtag
MySQL Configuration

hashtag
SQLite Configuration

hashtag
Environment Configuration

hashtag
Multi-Environment Setup

hashtag
File Structure

hashtag
Connection Testing

hashtag
Docker Development

hashtag
Troubleshooting

Connection Issues:

  • Verify database server is running

  • Check connection string format

  • Confirm database exists

  • Verify user permissions

Dependencies:

Common Errors:

  • Database not found - Create database first

  • Permission denied - Check user privileges

  • Connection refused - Verify server is running

hashtag
Next Steps

  • - Environment-specific setup

  • - Build your first app

  • - Design your database

Tutorials Overview

Learning-oriented guides that take you through a series of steps to complete a project. Tutorials are designed to help you learn CQL by doing.

hashtag
Getting Started

If you're new to CQL, start here:

CQL Documentation

A high-performance, type-safe ORM for Crystal applications.

Build fast, reliable database applications with compile-time safety and exceptional performance.

hashtag
Quick Navigation

Building a Blog

This is the first part of a five-part tutorial series where you'll build a complete blog engine with CQL. In this part, you'll set up your project structure, configure CQL, and prepare the foundation for your blog application.

hashtag
What You'll Learn

  • Setting up a Crystal project for CQL

Part 2: Database Schema

In this part, you'll design and create the database schema for your blog engine using CQL migrations. You'll create tables for users, categories, posts, and comments with proper relationships and indexes.

hashtag
What You'll Learn

  • Designing a relational database schema

Part 5: Adding Features

In this final part, you'll enhance your blog engine with validations, callbacks, transactions, and performance monitoring. You'll also learn how to seed data and prepare your application for production.

hashtag
What You'll Learn

  • Adding model validations

Fix Connection Errors

This guide helps you diagnose and fix database connection errors.

hashtag
Common Error: Connection Refused

Error:

Causes:

Adding CQL to Existing Project

This tutorial walks you through integrating CQL into an existing Crystal application. You'll learn how to add CQL alongside your current code, migrate existing data, and gradually adopt CQL patterns.

hashtag
What You'll Learn

  • Adding CQL as a dependency

Getting Started

This tutorial walks you through building your first application with CQL from scratch. By the end, you'll have a working Crystal application that connects to a database, creates tables through migrations, and performs CRUD operations.

hashtag
What You'll Learn

  • Installing CQL and database drivers

Part 3: Models and Relationships

In this part, you'll create Active Record models for your blog engine and define the relationships between them. You'll learn how to map database tables to Crystal structs and navigate between related records.

hashtag
What You'll Learn

  • Creating Active Record models

crystal --version
Configuration Guidearrow-up-right
Getting Startedarrow-up-right
Schema Designarrow-up-right

Database server not running

  • Wrong host or port

  • Firewall blocking connection

  • Solutions:

    1. Check if database is running:

    1. Start the database:

    1. Verify connection string:

    hashtag
    Common Error: Authentication Failed

    Error:

    Solutions:

    1. Verify credentials:

    1. Reset password (PostgreSQL):

    1. Check pg_hba.conf for authentication method

    hashtag
    Common Error: Database Does Not Exist

    Error:

    Solutions:

    1. Create the database:

    1. For SQLite, ensure directory exists:

    hashtag
    Common Error: Too Many Connections

    Error:

    Solutions:

    1. Close unused connections

    2. Increase connection limit:

    1. Use connection pooling

    hashtag
    Common Error: SSL Required

    Error:

    Solutions:

    Add SSL mode to connection string:

    hashtag
    Debugging Connection Issues

    hashtag
    Test Connection Script

    Run:

    hashtag
    Check Network Connectivity

    hashtag
    Related

    • Configure Database Connection

    • Fix Migration Errors

    • Enable SSL Connections

    Configuring CQL alongside existing database code

  • Creating migrations for existing tables

  • Defining models for existing data

  • Gradually migrating to CQL patterns

  • hashtag
    Prerequisites

    • An existing Crystal project

    • Basic familiarity with CQL concepts

    • Access to your existing database

    hashtag
    Step 1: Add Dependencies

    Add CQL to your existing shard.yml:

    Install:

    hashtag
    Step 2: Create Database Configuration

    Create a separate file for CQL configuration:

    hashtag
    Step 3: Define Existing Tables in Schema

    If you have existing tables, define them in the schema so CQL knows about them:

    hashtag
    Step 4: Create Models for Existing Tables

    Create CQL models that map to your existing tables:

    hashtag
    Step 5: Initialize CQL

    Add CQL initialization to your application startup:

    hashtag
    Step 6: Create New Migrations

    For new features, create CQL migrations:

    hashtag
    Step 7: Run New Migrations

    Create a migration runner:

    Run migrations:

    hashtag
    Step 8: Gradual Migration Strategy

    Adopt CQL incrementally:

    hashtag
    Phase 1: Read Operations

    Start by using CQL for read operations:

    hashtag
    Phase 2: Simple Writes

    Move simple create/update operations:

    hashtag
    Phase 3: Complex Queries

    Migrate complex queries:

    hashtag
    Phase 4: Relationships

    Add relationship navigation:

    hashtag
    Handling Existing Migrations

    If you have existing migrations (e.g., from another ORM), you have options:

    hashtag
    Option A: Start Fresh

    Create a baseline migration that represents your current schema:

    hashtag
    Option B: Import Existing Schema

    Mark existing migrations as applied:

    hashtag
    Best Practices

    1. Don't modify existing tables with CQL initially - just read from them

    2. Test thoroughly before switching write operations

    3. Keep existing code working during transition

    4. Migrate one model at a time to limit risk

    5. Use transactions when doing bulk data updates

    hashtag
    Common Issues

    hashtag
    Column Name Mismatches

    If your existing column names differ from CQL conventions:

    hashtag
    Different Timestamp Format

    If your timestamps use a different format:

    hashtag
    Summary

    You've learned how to:

    1. Add CQL to an existing Crystal project

    2. Define schemas for existing tables

    3. Create models for existing data

    4. Create new migrations for new features

    5. Gradually migrate from raw SQL to CQL patterns

    hashtag
    Next Steps

    • Your First CQL App - Learn CQL fundamentals

    • Define a Model - Advanced model features

    • Run Migrations - Migration management

    crystal init app myapp
    cd myapp
    # shard.yml
    dependencies:
      cql:
        github: azutoolkit/cql
        version: "~> 0.0.435"
    
      # Choose your database driver
      sqlite3:
        github: crystal-lang/crystal-sqlite3
      # OR
      pg:
        github: will/crystal-pg
      # OR
      mysql:
        github: crystal-lang/crystal-mysql
    shards install
    # src/myapp.cr
    require "cql"
    require "sqlite3"
    
    MyDB = CQL::Schema.define(
      :myapp,
      adapter: CQL::Adapter::SQLite,
      uri: "sqlite3://./app.db"
    ) do
      table :users do
        primary :id, Int64, auto_increment: true
        column :name, String
        column :email, String
        timestamps
      end
    end
    
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property created_at : Time?
      property updated_at : Time?
    end
    
    MyDB.build
    user = User.create!(name: "Alice", email: "alice@example.com")
    puts "Created user: #{user.name}"
    crystal run src/myapp.cr
    # Production setup
    AppDB = CQL::Schema.define(
      :production_db,
      adapter: CQL::Adapter::Postgres,
      uri: ENV["DATABASE_URL"]? || "postgresql://username:password@localhost:5432/myapp_production",
      pool_size: 20,
      checkout_timeout: 10.seconds
    ) do
      table :users do
        primary :id, Int64, auto_increment: true
        column :name, String, size: 100, null: false
        column :email, String, size: 255, null: false
        column :active, Bool, default: true
        timestamps
    
        unique_constraint [:email]
        index [:active, :created_at]
      end
    end
    # Production with SSL
    "postgresql://user:pass@prod-server:5432/myapp_prod?sslmode=require"
    
    # Development
    "postgresql://user:pass@localhost:5432/myapp_dev"
    AppDB = CQL::Schema.define(
      :mysql_db,
      adapter: CQL::Adapter::MySql,
      uri: ENV["DATABASE_URL"]? || "mysql://username:password@localhost:3306/myapp_development",
      pool_size: 15,
      charset: "utf8mb4"
    ) do
      table :users do
        primary :id, Int64, auto_increment: true
        column :name, String, size: 100
        column :email, String, size: 320
        timestamps
    
        index [:email], unique: true
      end
    end
    # Multi-environment setup
    database_url = case ENV["CRYSTAL_ENV"]?
    when "test"        then "sqlite3://:memory:"
    when "production"  then "sqlite3://#{ENV["DATA_PATH"]? || "./"}/production.db"
    else                    "sqlite3://./db/development.db"
    end
    
    AppDB = CQL::Schema.define(
      :app_db,
      adapter: CQL::Adapter::SQLite,
      uri: database_url
    ) do
      table :users do
        primary :id, Int64, auto_increment: true
        column :name, String
        column :email, String
        timestamps
    
        unique_constraint [:email]
      end
    end
    # config/database.cr
    module DatabaseConfig
      ENV_NAME = ENV["CRYSTAL_ENV"]? || "development"
    
      DATABASE_URLS = {
        "development" => "sqlite3://./db/development.db",
        "test"        => "sqlite3://:memory:",
        "production"  => ENV["DATABASE_URL"]? || raise("DATABASE_URL required")
      }
    
      def self.adapter_for(url : String)
        case url
        when .starts_with?("postgresql://") then CQL::Adapter::Postgres
        when .starts_with?("mysql://")      then CQL::Adapter::MySql
        when .starts_with?("sqlite3://")    then CQL::Adapter::SQLite
        else raise "Unsupported database URL: #{url}"
        end
      end
    
      POOL_SIZES = {
        "development" => 5,
        "test"        => 1,
        "production"  => 25
      }
    end
    
    database_url = DatabaseConfig::DATABASE_URLS[DatabaseConfig::ENV_NAME]
    adapter = DatabaseConfig.adapter_for(database_url)
    pool_size = DatabaseConfig::POOL_SIZES[DatabaseConfig::ENV_NAME]
    
    AppDB = CQL::Schema.define(
      :app_database,
      adapter: adapter,
      uri: database_url,
      pool_size: pool_size
    )
    src/
    ├── config/
    │   ├── database.cr
    │   └── environments/
    │       ├── development.cr
    │       ├── test.cr
    │       └── production.cr
    ├── models/
    │   ├── user.cr
    │   └── post.cr
    └── myapp.cr
    # test_connection.cr
    require "./config/database"
    
    puts "Testing database connection..."
    
    begin
      AppDB.build
      puts "✅ Database connection successful"
      puts "Tables: #{AppDB.tables.keys}"
    
      if AppDB.tables.has_key?(:users)
        user = User.create!(name: "Test", email: "test@example.com")
        puts "✅ Created test user: #{user.name}"
        user.delete!
        puts "✅ CRUD operations working"
      end
    rescue ex
      puts "❌ Connection failed: #{ex.message}"
      exit 1
    end
    # docker-compose.yml
    version: "3.8"
    services:
      postgres:
        image: postgres:15
        environment:
          POSTGRES_PASSWORD: password
          POSTGRES_DB: myapp_development
        ports:
          - "5432:5432"
        volumes:
          - postgres_data:/var/lib/postgresql/data
    
      mysql:
        image: mysql:8.0
        environment:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: myapp_development
        ports:
          - "3306:3306"
        volumes:
          - mysql_data:/var/lib/mysql
    
    volumes:
      postgres_data:
      mysql_data:
    docker-compose up -d postgres
    # Use: postgresql://postgres:password@localhost:5432/myapp_development
    # Reinstall dependencies
    rm -rf lib/ shard.lock
    shards install
    Connection refused (localhost:5432)
    # PostgreSQL
    pg_isready -h localhost -p 5432
    
    # MySQL
    mysqladmin -h localhost -u root ping
    # PostgreSQL (macOS with Homebrew)
    brew services start postgresql
    
    # PostgreSQL (Linux)
    sudo systemctl start postgresql
    
    # MySQL
    sudo systemctl start mysql
    # Check your URL format
    DATABASE_URL = "postgres://localhost:5432/myapp"  # Port is correct?
    password authentication failed for user "myuser"
    # Check username and password
    DATABASE_URL = "postgres://username:password@localhost/myapp"
    psql -U postgres
    ALTER USER myuser WITH PASSWORD 'newpassword';
    database "myapp_development" does not exist
    # PostgreSQL
    createdb myapp_development
    
    # Or in psql
    psql -U postgres -c "CREATE DATABASE myapp_development"
    Dir.mkdir_p("./db") unless Dir.exists?("./db")
    too many connections for role "myuser"
    ALTER ROLE myuser CONNECTION LIMIT 100;
    SSL connection is required
    DATABASE_URL = "postgres://user:pass@host/db?sslmode=require"
    begin
      MyDB.init
      puts "Connection successful"
    rescue ex
      puts "Connection failed"
      puts "Error type: #{ex.class}"
      puts "Message: #{ex.message}"
    
      # Print sanitized connection string
      puts "URL: #{DATABASE_URL.gsub(/:[^:@]+@/, ":****@")}"
    end
    # scripts/test_connection.cr
    require "cql"
    require "pg"
    
    url = ARGV[0]? || ENV["DATABASE_URL"]? || "postgres://localhost/myapp"
    
    puts "Testing connection to: #{url.gsub(/:[^:@]+@/, ":****@")}"
    
    begin
      db = CQL::Schema.define(:test, adapter: CQL::Adapter::Postgres, uri: url) do
      end
      db.init
      db.exec("SELECT 1")
      puts "Connection successful!"
    rescue ex
      puts "Connection failed: #{ex.message}"
      exit 1
    end
    crystal scripts/test_connection.cr
    # Test if host is reachable
    ping database-host.example.com
    
    # Test if port is open
    nc -zv localhost 5432
    dependencies:
      cql:
        github: azutoolkit/cql
        version: ~> 0.0.435
    
      # Keep your existing database driver or add one
      pg:
        github: will/crystal-pg
        version: "~> 0.26.0"
    shards install
    # src/cql_config.cr
    require "cql"
    require "pg"
    
    # Use your existing database URL
    DATABASE_URL = ENV["DATABASE_URL"]? || "postgres://localhost/myapp_development"
    
    AppDB = CQL::Schema.define(
      :app_db,
      adapter: CQL::Adapter::Postgres,
      uri: DATABASE_URL
    ) do
      # We'll define existing tables here
    end
    
    # Migration configuration
    CQL_MIGRATOR_CONFIG = CQL::MigratorConfig.new(
      schema_file_path: "src/schemas/app_schema.cr",
      schema_name: :AppSchema,
      auto_sync: true
    )
    AppDB = CQL::Schema.define(
      :app_db,
      adapter: CQL::Adapter::Postgres,
      uri: DATABASE_URL
    ) do
      # Define existing tables (read-only, won't create them)
      table :users do
        primary :id, Int64, auto_increment: true
        text :email, null: false
        text :name
        text :password_digest
        timestamps
      end
    
      table :products do
        primary :id, Int64, auto_increment: true
        text :name, null: false
        text :description
        decimal :price, precision: 10, scale: 2
        integer :stock, default: 0
        timestamps
      end
    end
    # src/models/user.cr
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context AppDB, :users
    
      property id : Int64?
      property email : String
      property name : String?
      property password_digest : String?
      property created_at : Time?
      property updated_at : Time?
    
      def initialize(@email : String, @name : String? = nil)
      end
    end
    # src/models/product.cr
    struct Product
      include CQL::ActiveRecord::Model(Int64)
      db_context AppDB, :products
    
      property id : Int64?
      property name : String
      property description : String?
      property price : BigDecimal?
      property stock : Int32 = 0
      property created_at : Time?
      property updated_at : Time?
    
      def initialize(@name : String, @price : BigDecimal? = nil)
      end
    end
    # src/app.cr (or your main file)
    require "./cql_config"
    require "./models/*"
    
    # Initialize CQL connection
    AppDB.init
    
    # Your existing application code continues...
    mkdir -p migrations
    # migrations/001_create_orders.cr
    class CreateOrders < CQL::Migration(1)
      def up
        schema.table :orders do
          primary :id, Int64, auto_increment: true
          column :user_id, Int64, null: false
          column :total, Float64
          column :status, String, default: "pending"
          timestamps
    
          foreign_key [:user_id], references: :users, references_columns: [:id]
          index [:user_id]
          index [:status]
        end
    
        schema.orders.create!
      end
    
      def down
        schema.orders.drop!
      end
    end
    # src/migrate.cr
    require "./cql_config"
    require "../migrations/*"
    
    AppDB.init
    
    migrator = AppDB.migrator(CQL_MIGRATOR_CONFIG)
    
    case ARGV[0]?
    when "up"
      migrator.up
      puts "Migrations applied"
    when "down"
      migrator.down
      puts "Last migration rolled back"
    when "status"
      puts "Applied: #{migrator.applied_migrations.size}"
      puts "Pending: #{migrator.pending_migrations.size}"
    else
      puts "Usage: crystal src/migrate.cr [up|down|status]"
    end
    crystal src/migrate.cr up
    # Before (raw SQL)
    result = db.query("SELECT * FROM users WHERE email = $1", email)
    
    # After (CQL)
    user = User.find_by(email: email)
    # Before
    db.exec("INSERT INTO users (email, name) VALUES ($1, $2)", email, name)
    
    # After
    user = User.create!(email: email, name: name)
    # Before
    results = db.query(<<-SQL
      SELECT u.*, COUNT(o.id) as order_count
      FROM users u
      LEFT JOIN orders o ON o.user_id = u.id
      GROUP BY u.id
      ORDER BY order_count DESC
      LIMIT 10
    SQL
    )
    
    # After
    top_users = User.all.sort_by { |u| -u.orders.count }.first(10)
    # Update User model
    struct User
      # ...existing code...
      has_many :orders, Order, :user_id
    end
    
    # Now you can do
    user = User.find(1)
    user_orders = user.orders.all
    # migrations/000_baseline.cr
    class Baseline < CQL::Migration(0)
      def up
        # This migration assumes tables already exist
        # It just establishes the baseline for CQL
      end
    
      def down
        # Cannot rollback baseline
        raise "Cannot rollback baseline migration"
      end
    end
    # In your migrate script
    migrator = AppDB.migrator(CQL_MIGRATOR_CONFIG)
    
    # Mark baseline as applied without running
    migrator.mark_as_applied(0) unless migrator.applied?(0)
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context AppDB, :users
    
      # Map property to different column name
      @[CQL::Column(name: "user_email")]
      property email : String
    end
    # Handle existing timestamp format
    property created_at : Time?
    
    def created_at_formatted
      created_at.try(&.to_s("%Y-%m-%d %H:%M:%S"))
    end
    Your First CQL App - Build your first application with CQL from scratch
  • Adding CQL to an Existing Project - Integrate CQL into your current Crystal application

  • hashtag
    Building a Blog

    A comprehensive tutorial series that walks you through building a complete blog engine:

    1. Project Setup - Initialize your project and configure dependencies

    2. Database Schema - Design and create your database structure

    3. Models and Relationships - Define Active Record models with associations

    4. - Implement create, read, update, and delete functionality

    5. - Extend your blog with comments, tags, and more

    hashtag
    Real-World Examples

    Additional tutorials demonstrating common patterns:

    • Migration Workflows - Managing database migrations in production

    • Performance Monitoring - Track and optimize query performance


    What's next? After completing a tutorial, check out the How-to Guides for specific task-based instructions.

    Accomplish a specific task

    Look up API details

    Understand concepts

    hashtag
    Installation

    Add CQL to your shard.yml:

    Then run:

    Full installation guide

    hashtag
    Quick Start

    hashtag
    Documentation Structure

    hashtag
    Tutorials

    Learning-oriented - Step-by-step guides for beginners

    • Your First CQL App - Build your first application

    • Building a Blog - Complete multi-part tutorial

    hashtag
    How-to Guides

    Task-oriented - Practical steps to accomplish specific goals

    • Models - Define, validate, and enhance models

    • Relationships - Set up associations

    • Querying - Find and filter data

    • - Manage schema changes

    hashtag
    Reference

    Information-oriented - Technical descriptions and specifications

    • Quick Reference - Common patterns at a glance

    • Glossary - Terminology definitions

    • Error Codes - Error messages explained

    hashtag
    Explanation

    Understanding-oriented - Conceptual discussions

    • What is an ORM? - ORM fundamentals

    • Active Record Pattern - Design pattern overview

    hashtag
    Key Features

    • Type Safety - Catch errors at compile time

    • Multiple Databases - PostgreSQL, MySQL, SQLite

    • Active Record - Familiar patterns for rapid development

    • Migrations - Version-controlled schema changes

    • Validations - Built-in data integrity

    • Relationships - belongs_to, has_one, has_many, many-to-many

    • Soft Deletes - Mark records as deleted

    • Optimistic Locking - Prevent concurrent update conflicts

    hashtag
    Getting Help

    • FAQ - Frequently asked questions

    • Troubleshooting - Common issues

    • Community - Get support

    • - Report bugs

    hashtag
    License

    CQL is available under the MIT license.

    I want to...

    Go to...

    Learn CQL from scratch

    Configuring CQL for different environments

  • Understanding the project structure

  • Preparing for database migrations

  • hashtag
    Prerequisites

    • Crystal 1.0+ installed

    • Basic understanding of Crystal syntax

    • Familiarity with database concepts

    • Completed tutorial (recommended)

    hashtag
    The Blog Engine Architecture

    Before we start coding, let's understand what we're building:

    The relationships:

    • A User has many Posts and Comments

    • A Category has many Posts

    • A Post belongs to a User and a Category, and has many Comments

    • A Comment belongs to a Post and optionally a User

    hashtag
    Step 1: Create the Project

    hashtag
    Step 2: Configure Dependencies

    Edit your shard.yml:

    Install dependencies:

    hashtag
    Step 3: Set Up Project Structure

    Create the necessary directories:

    Your project structure should look like:

    hashtag
    Step 4: Configure the Database

    Create the database configuration file:

    hashtag
    Step 5: Create the Main Entry Point

    hashtag
    Step 6: Create a Setup Script

    Create a script for initializing and migrating the database:

    hashtag
    Step 7: Verify the Setup

    Create a simple verification script:

    Run the verification:

    You should see: Database connection: OK

    hashtag
    Understanding CQL Configuration Options

    The configuration we created uses sensible defaults, but CQL offers many options:

    Option
    Description
    Our Choice

    adapter

    Database type

    SQLite for simplicity

    uri

    Connection string

    Local file-based database

    schema_file_path

    Auto-generated schema location

    src/schemas/blog_schema.cr

    auto_sync

    Automatically update schema file

    For production, you might use PostgreSQL:

    hashtag
    Summary

    In this part, you:

    1. Created a new Crystal project

    2. Added CQL and database driver dependencies

    3. Set up the project directory structure

    4. Configured the database connection

    5. Created main entry point and setup scripts

    6. Verified the database connection works

    hashtag
    Next Steps

    In Part 2: Database Schema, you'll design and create the database tables for users, categories, posts, and comments using CQL migrations.


    Tutorial Navigation:

    • Part 1: Project Setup (current)

    • Part 2: Database Schema

    • Part 3: Models and Relationships

    Creating migrations for multiple tables

  • Defining primary keys, foreign keys, and indexes

  • Using timestamps and default values

  • Running and verifying migrations

  • hashtag
    Prerequisites

    • Completed Part 1: Project Setup

    hashtag
    Database Design

    Here's the schema we'll create:

    hashtag
    Step 1: Create Users Migration

    Create the first migration for the users table:

    Key points:

    • primary :id creates an auto-incrementing primary key

    • null: false makes columns required

    • default: true sets a default value

    • timestamps creates created_at and updated_at columns

    • Unique indexes on email and username prevent duplicates

    hashtag
    Step 2: Create Categories Migration

    The slug column stores URL-friendly versions of category names (e.g., "Web Development" becomes "web-development").

    hashtag
    Step 3: Create Posts Migration

    Key points:

    • Foreign keys establish relationships with users and categories

    • on_delete: :cascade deletes posts when the author is deleted

    • on_delete: :set_null keeps posts when a category is deleted (just nullifies the reference)

    • Indexes on foreign keys improve query performance

    • Index on published helps filter posts efficiently

    hashtag
    Step 4: Create Comments Migration

    Key points:

    • user_id is nullable to allow anonymous comments

    • Cascading delete removes comments when their post is deleted

    • Setting user to NULL when a user is deleted preserves the comment

    hashtag
    Step 5: Run the Migrations

    Update your setup script to require migrations:

    Run the migrations:

    Expected output:

    hashtag
    Step 6: Verify the Schema

    Create a verification script to inspect the created tables:

    Run it:

    hashtag
    Understanding the Schema

    Let's visualize the relationships:

    hashtag
    Migration Best Practices

    1. Number migrations sequentially: Use 001_, 002_, etc. to ensure order

    2. Write reversible migrations: Always implement both up and down

    3. One concern per migration: Don't create multiple unrelated tables in one migration

    4. Index foreign keys: Always index columns used in foreign key relationships

    5. Plan for deletion: Choose appropriate on_delete behavior

    6. Use nullable carefully: Only make columns nullable when truly optional

    hashtag
    Common Column Types

    CQL Type
    SQLite
    PostgreSQL
    MySQL

    primary

    INTEGER

    BIGSERIAL

    BIGINT

    bigint

    INTEGER

    BIGINT

    BIGINT

    integer

    INTEGER

    INTEGER

    hashtag
    Summary

    In this part, you:

    1. Designed a relational database schema for a blog

    2. Created four migrations: users, categories, posts, comments

    3. Defined primary keys, foreign keys, and indexes

    4. Ran migrations to create the database tables

    5. Verified the schema was created correctly

    hashtag
    Next Steps

    In Part 3: Models and Relationships, you'll create Active Record models that map to these tables and define the relationships between them.


    Tutorial Navigation:

    • Part 1: Project Setup

    • Part 2: Database Schema (current)

    • Part 3: Models and Relationships

    Using lifecycle callbacks

  • Implementing transactions

  • Seeding sample data

  • Performance monitoring basics

  • Production considerations

  • hashtag
    Prerequisites

    • Completed Part 4: CRUD Operations

    hashtag
    Step 1: Add Validations

    Validations ensure data integrity before records are saved.

    hashtag
    User Validations

    Update your User model:

    hashtag
    Post Validations

    hashtag
    Using Validations

    hashtag
    Step 2: Add Callbacks

    Callbacks let you run code at specific points in a model's lifecycle.

    hashtag
    User Callbacks

    hashtag
    Post Callbacks

    hashtag
    Category Callbacks

    hashtag
    Step 3: Implement Transactions

    Transactions ensure multiple operations succeed or fail together.

    hashtag
    Creating a Post with Comments

    hashtag
    User Registration with Profile

    hashtag
    Step 4: Create a Seeder

    Create a script to populate your database with sample data:

    Run the seeder:

    hashtag
    Step 5: Add Performance Monitoring

    Track query performance to identify issues:

    hashtag
    Step 6: Create the Complete Demo

    Put everything together:

    Run the demo:

    hashtag
    Production Considerations

    hashtag
    Database Configuration

    For production, switch to PostgreSQL:

    hashtag
    Environment Variables

    hashtag
    Security Checklist

    1. Input Validation: All user input is validated

    2. Parameterized Queries: CQL uses parameterized queries by default

    3. Authentication: Add authentication before deploying (not covered in this tutorial)

    4. Authorization: Ensure users can only modify their own content

    5. Rate Limiting: Add rate limiting for API endpoints

    hashtag
    Performance Checklist

    1. Indexes: Ensure indexes on frequently queried columns

    2. N+1 Queries: Use eager loading for relationships

    3. Pagination: Always paginate large result sets

    4. Caching: Add caching for expensive queries

    5. Connection Pooling: Configure appropriate pool sizes

    hashtag
    Summary

    In this tutorial series, you built a complete blog engine:

    1. Part 1: Set up the project and configured CQL

    2. Part 2: Designed and created the database schema

    3. Part 3: Built models with relationships

    4. Part 4: Implemented CRUD operations

    5. Part 5: Added validations, callbacks, transactions, and monitoring

    hashtag
    What's Next?

    Continue learning with:

    • How-to Guides - Task-specific instructions

    • Reference - API documentation

    • Explanation - Conceptual deep-dives

    hashtag
    Ideas for Extending Your Blog

    • Add user authentication

    • Implement post tagging

    • Add image uploads

    • Create an RSS feed

    • Build a REST API

    • Add full-text search

    • Implement caching


    Tutorial Navigation:

    • Part 1: Project Setup

    • Part 2: Database Schema

    • Part 3: Models and Relationships

    • Part 5: Adding Features (current)


    Congratulations on completing the Building a Blog tutorial series!

    Configuring a database connection

  • Creating and running migrations

  • Defining Active Record models

  • Performing basic CRUD operations

  • Working with associations

  • Adding validations

  • Using transactions

  • hashtag
    Prerequisites

    Before starting, ensure you have:

    • Crystal (latest stable version recommended)

    • A supported database: PostgreSQL, MySQL, or SQLite

    • Basic familiarity with Crystal syntax

    hashtag
    Step 1: Create Your Project

    Create a new Crystal project:

    hashtag
    Step 2: Add Dependencies

    Add CQL and your database driver to shard.yml:

    Install the dependencies:

    hashtag
    Step 3: Set Up Database Connection

    Create a file to configure your database connection. The setup varies slightly by database.

    hashtag
    PostgreSQL

    hashtag
    SQLite

    hashtag
    MySQL

    hashtag
    Step 4: Create Your First Migration

    Create a migrations directory and your first migration file:

    hashtag
    Step 5: Define Your First Model

    Create a model that maps to your users table:

    hashtag
    Step 6: Initialize the Database and Run Migrations

    Create a setup script to initialize everything:

    Run the setup:

    hashtag
    Step 7: Perform CRUD Operations

    Now you can interact with your data. Create a main application file:

    Run your application:

    hashtag
    Step 8: Add a Related Model

    Let's add posts to demonstrate relationships. First, create the migration:

    Create the Post model:

    Update the User model to include the association:

    Run migrations and test:

    hashtag
    Step 9: Add Validations

    Enhance your User model with validations:

    hashtag
    Step 10: Use Transactions

    Ensure data consistency with transactions:

    hashtag
    Project Structure

    Your completed project should look like this:

    hashtag
    Troubleshooting

    hashtag
    Database Connection Issues

    hashtag
    Migration Issues

    hashtag
    Next Steps

    Congratulations! You've built your first CQL application. Continue learning with:

    • Building a Blog - A comprehensive multi-part tutorial

    • Define a Model - Learn advanced model features

    • Build Complex Queries - Master the query interface

    • - Ensure data integrity

    Defining model properties

  • Setting up belongs_to relationships

  • Setting up has_many relationships

  • Adding custom model methods

  • Navigating between related records

  • hashtag
    Prerequisites

    • Completed Part 2: Database Schema

    hashtag
    Step 1: Create the User Model

    Key points:

    • include CQL::ActiveRecord::Model(Int64) - Makes this an Active Record model with Int64 primary key

    • db_context BlogDB, :users - Connects model to the users table in BlogDB

    • property defines model attributes matching database columns

    • has_many defines one-to-many relationships

    • Custom methods add business logic

    hashtag
    Step 2: Create the Category Model

    The generate_slug method automatically creates URL-friendly slugs from category names.

    hashtag
    Step 3: Create the Post Model

    Key points:

    • belongs_to defines many-to-one relationships

    • category_id is nilable (Int64?) because category is optional

    • Custom methods provide useful functionality like word count and excerpts

    hashtag
    Step 4: Create the Comment Model

    hashtag
    Step 5: Update the Main File

    Update blog_engine.cr to require all models:

    Note: Order matters! Models with dependencies must be required after their dependencies.

    hashtag
    Step 6: Test the Models

    Create a test script:

    Run it:

    hashtag
    Understanding Relationships

    hashtag
    belongs_to

    This creates a user method that returns the associated User (or nil):

    hashtag
    has_many

    This creates a posts method that returns a query builder:

    hashtag
    Model Best Practices

    1. Keep models focused: Business logic belongs in models, not controllers

    2. Use meaningful method names: published? is clearer than is_published

    3. Validate early: Add validations to catch errors before database operations

    4. Handle nil safely: Use try(&.method) when accessing optional relationships

    5. Add useful computed properties: Like full_name, word_count, etc.

    hashtag
    Summary

    In this part, you:

    1. Created four Active Record models: User, Category, Post, Comment

    2. Defined properties matching database columns

    3. Set up belongs_to and has_many relationships

    4. Added custom methods for business logic

    5. Tested model creation and relationship navigation

    hashtag
    Next Steps

    In Part 4: CRUD Operations, you'll learn how to create, read, update, and delete records efficiently, including bulk operations and querying patterns.


    Tutorial Navigation:

    • Part 1: Project Setup

    • Part 2: Database Schema

    • Part 3: Models and Relationships (current)

    dependencies:
      cql:
        github: azutoolkit/cql
        version: ~> 0.0.435
    
      # Choose your database driver:
      pg:  # PostgreSQL
        github: will/crystal-pg
        version: "~> 0.26.0"
    shards install
    require "cql"
    require "pg"
    
    # Define your database
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::Postgres,
      uri: "postgres://localhost/myapp"
    ) do
    end
    
    # Define a model
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property email : String
    
      def initialize(@name : String, @email : String)
      end
    end
    
    # Use it
    MyDB.init
    user = User.create!(name: "Alice", email: "alice@example.com")
    puts "Created user #{user.id}: #{user.name}"
    Blog Engine
    ├── Users (authors and commenters)
    ├── Categories (post organization)
    ├── Posts (blog content)
    └── Comments (reader engagement)
    crystal init app blog_engine
    cd blog_engine
    name: blog_engine
    version: 0.1.0
    
    dependencies:
      cql:
        github: azutoolkit/cql
        version: ~> 0.0.435
      sqlite3:
        github: crystal-lang/crystal-sqlite3
        version: "~> 0.18.0"
    
    targets:
      blog_engine:
        main: src/blog_engine.cr
    shards install
    mkdir -p src/models
    mkdir -p src/schemas
    mkdir -p migrations
    mkdir -p db
    blog_engine/
    ├── shard.yml
    ├── src/
    │   ├── blog_engine.cr      # Main entry point
    │   ├── database.cr         # Database configuration
    │   ├── models/             # Active Record models
    │   └── schemas/            # Auto-generated schemas
    ├── migrations/             # Database migrations
    └── db/                     # SQLite database files
    # src/database.cr
    require "cql"
    require "sqlite3"
    
    # Database connection URL - easily switchable via environment
    DATABASE_URL = ENV["DATABASE_URL"]? || "sqlite3://./db/blog_development.db"
    
    # Define the schema connection
    BlogDB = CQL::Schema.define(
      :blog_db,
      adapter: CQL::Adapter::SQLite,
      uri: DATABASE_URL
    ) do
      # Tables will be defined through migrations
    end
    
    # Migration configuration
    MIGRATOR_CONFIG = CQL::MigratorConfig.new(
      schema_file_path: "src/schemas/blog_schema.cr",
      schema_name: :BlogSchema,
      auto_sync: true
    )
    # src/blog_engine.cr
    require "./database"
    require "./models/*"
    
    module BlogEngine
      VERSION = "0.1.0"
    
      def self.setup
        # Initialize database connection
        BlogDB.init
        puts "Database connected"
      end
    
      def self.migrate
        require "../migrations/*"
    
        migrator = BlogDB.migrator(MIGRATOR_CONFIG)
        pending = migrator.pending_migrations.size
    
        if pending > 0
          puts "Running #{pending} pending migration(s)..."
          migrator.up
          puts "Migrations complete"
        else
          puts "Database is up to date"
        end
      end
    end
    
    # Run setup when executed directly
    if PROGRAM_NAME.includes?("blog_engine")
      BlogEngine.setup
    end
    # src/setup.cr
    require "./blog_engine"
    
    puts "Setting up Blog Engine..."
    puts "========================="
    
    BlogEngine.setup
    BlogEngine.migrate
    
    puts ""
    puts "Setup complete!"
    # src/verify.cr
    require "./database"
    
    begin
      BlogDB.init
      puts "Database connection: OK"
    rescue ex
      puts "Database connection: FAILED"
      puts "Error: #{ex.message}"
      exit 1
    end
    crystal src/verify.cr
    # Production configuration example
    BlogDB = CQL::Schema.define(
      :blog_db,
      adapter: CQL::Adapter::Postgres,
      uri: ENV["DATABASE_URL"]
    ) do
    end
    USERS                    CATEGORIES
    ├── id (PK)              ├── id (PK)
    ├── username (unique)    ├── name
    ├── email (unique)       ├── slug (unique)
    ├── first_name           ├── created_at
    ├── last_name            └── updated_at
    ├── active
    ├── created_at
    └── updated_at
    
    POSTS                    COMMENTS
    ├── id (PK)              ├── id (PK)
    ├── title                ├── content
    ├── content              ├── post_id (FK)
    ├── published            ├── user_id (FK, nullable)
    ├── views_count          ├── created_at
    ├── user_id (FK)         └── updated_at
    ├── category_id (FK)
    ├── created_at
    └── updated_at
    # migrations/001_create_users.cr
    class CreateUsers < CQL::Migration(1)
      def up
        schema.table :users do
          primary :id, Int64, auto_increment: true
          column :username, String, null: false
          column :email, String, null: false
          column :first_name, String, null: true
          column :last_name, String, null: true
          column :active, Bool, default: true
          timestamps
    
          index [:email], unique: true
          index [:username], unique: true
        end
    
        schema.users.create!
      end
    
      def down
        schema.users.drop!
      end
    end
    # migrations/002_create_categories.cr
    class CreateCategories < CQL::Migration(2)
      def up
        schema.table :categories do
          primary :id, Int64, auto_increment: true
          column :name, String, null: false
          column :slug, String, null: false
          timestamps
    
          index [:slug], unique: true
        end
    
        schema.categories.create!
      end
    
      def down
        schema.categories.drop!
      end
    end
    # migrations/003_create_posts.cr
    class CreatePosts < CQL::Migration(3)
      def up
        schema.table :posts do
          primary :id, Int64, auto_increment: true
          column :title, String, null: false
          column :content, String, null: false
          column :published, Bool, default: false
          column :views_count, Int64, default: 0_i64
          column :user_id, Int64, null: false
          column :category_id, Int64, null: true
          timestamps
    
          foreign_key [:user_id], references: :users, references_columns: [:id], on_delete: :cascade
          foreign_key [:category_id], references: :categories, references_columns: [:id], on_delete: :set_null
          index [:user_id]
          index [:category_id]
          index [:published]
        end
    
        schema.posts.create!
      end
    
      def down
        schema.posts.drop!
      end
    end
    # migrations/004_create_comments.cr
    class CreateComments < CQL::Migration(4)
      def up
        schema.table :comments do
          primary :id, Int64, auto_increment: true
          column :content, String, null: false
          column :post_id, Int64, null: false
          column :user_id, Int64, null: true  # Allow anonymous comments
          timestamps
    
          foreign_key [:post_id], references: :posts, references_columns: [:id], on_delete: :cascade
          foreign_key [:user_id], references: :users, references_columns: [:id], on_delete: :set_null
          index [:post_id]
          index [:user_id]
        end
    
        schema.comments.create!
      end
    
      def down
        schema.comments.drop!
      end
    end
    # src/setup.cr
    require "./blog_engine"
    
    puts "Setting up Blog Engine..."
    puts "========================="
    
    BlogEngine.setup
    BlogEngine.migrate
    
    puts ""
    puts "Setup complete!"
    crystal src/setup.cr
    Setting up Blog Engine...
    =========================
    Database connected
    Running 4 pending migration(s)...
    Migrations complete
    
    Setup complete!
    # src/verify_schema.cr
    require "./database"
    require "../migrations/*"
    
    BlogDB.init
    
    migrator = BlogDB.migrator(MIGRATOR_CONFIG)
    
    puts "Migration Status"
    puts "================"
    puts "Applied: #{migrator.applied_migrations.size}"
    puts "Pending: #{migrator.pending_migrations.size}"
    puts ""
    puts "Applied migrations:"
    migrator.applied_migrations.each do |m|
      puts "  - Migration #{m}"
    end
    crystal src/verify_schema.cr
    Users ─────────┬─── has many ───> Posts
                   │
                   └─── has many ───> Comments
    
    Categories ────────── has many ───> Posts
    
    Posts ─────────┬─── belongs to ──> User
                   ├─── belongs to ──> Category (optional)
                   └─── has many ────> Comments
    
    Comments ──────┬─── belongs to ──> Post
                   └─── belongs to ──> User (optional)
    # src/models/user.cr
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context BlogDB, :users
    
      # ... existing properties ...
    
      # Validations
      validate :username, presence: true, size: (3..50)
      validate :email, presence: true, match: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    
      # Custom validation
      def validate
        super
        validate_username_uniqueness
        validate_email_uniqueness
      end
    
      private def validate_username_uniqueness
        existing = User.find_by(username: @username)
        if existing && existing.id != @id
          errors.add(:username, "has already been taken")
        end
      end
    
      private def validate_email_uniqueness
        existing = User.find_by(email: @email)
        if existing && existing.id != @id
          errors.add(:email, "has already been taken")
        end
      end
    end
    # src/models/post.cr
    struct Post
      include CQL::ActiveRecord::Model(Int64)
      db_context BlogDB, :posts
    
      # ... existing properties ...
    
      # Validations
      validate :title, presence: true, size: (1..200)
      validate :content, presence: true
    
      def validate
        super
        validate_title_length
      end
    
      private def validate_title_length
        if @title.size > 200
          errors.add(:title, "is too long (maximum 200 characters)")
        end
      end
    end
    # Test validations
    user = User.new(username: "ab", email: "invalid")
    
    if user.valid?
      user.save
    else
      puts "Validation errors:"
      user.errors.each do |field, messages|
        messages.each do |message|
          puts "  #{field}: #{message}"
        end
      end
    end
    # src/models/user.cr
    struct User
      # ... existing code ...
    
      # Callbacks
      before_save :normalize_email
      after_create :send_welcome_notification
    
      private def normalize_email
        @email = @email.downcase.strip
      end
    
      private def send_welcome_notification
        # In a real app, this would send an email or create a notification
        puts "Welcome, #{display_name}! Your account has been created."
      end
    end
    # src/models/post.cr
    struct Post
      # ... existing code ...
    
      # Callbacks
      before_save :update_timestamps
      after_save :notify_subscribers
    
      private def update_timestamps
        now = Time.utc
        @created_at ||= now
        @updated_at = now
      end
    
      private def notify_subscribers
        if @published && changes.includes?(:published)
          # Notify subscribers about new post
          puts "Notifying subscribers about: #{@title}"
        end
      end
    end
    # src/models/category.cr
    struct Category
      # ... existing code ...
    
      before_save :generate_slug_if_blank
    
      private def generate_slug_if_blank
        if @slug.blank?
          @slug = generate_slug(@name)
        end
      end
    end
    def create_post_with_comments(author : User, title : String, content : String, comment_texts : Array(String))
      Post.transaction do
        post = Post.create!(
          title: title,
          content: content,
          user_id: author.id.not_nil!,
          published: true
        )
    
        comment_texts.each do |text|
          Comment.create!(
            content: text,
            post_id: post.id.not_nil!,
            user_id: author.id
          )
        end
    
        puts "Created post with #{comment_texts.size} comments"
        post
      end
    end
    def register_user(username : String, email : String, bio : String?)
      User.transaction do
        user = User.create!(
          username: username,
          email: email
        )
    
        # Create welcome post
        Post.create!(
          title: "Welcome to my blog!",
          content: "Hi, I'm #{user.display_name}. #{bio || "Welcome to my blog!"}",
          user_id: user.id.not_nil!,
          published: true
        )
    
        user
      end
    rescue ex
      puts "Registration failed: #{ex.message}"
      nil
    end
    # src/seeders.cr
    require "./blog_engine"
    
    module Seeders
      def self.run
        puts "Seeding database..."
    
        # Create users
        users = create_users
        puts "Created #{users.size} users"
    
        # Create categories
        categories = create_categories
        puts "Created #{categories.size} categories"
    
        # Create posts
        posts = create_posts(users, categories)
        puts "Created #{posts.size} posts"
    
        # Create comments
        comments = create_comments(users, posts)
        puts "Created #{comments.size} comments"
    
        puts "Seeding complete!"
      end
    
      private def self.create_users
        [
          {username: "alice", email: "alice@example.com", first_name: "Alice", last_name: "Johnson"},
          {username: "bob", email: "bob@example.com", first_name: "Bob", last_name: "Smith"},
          {username: "charlie", email: "charlie@example.com", first_name: "Charlie", last_name: "Brown"},
        ].map do |attrs|
          User.create!(
            username: attrs[:username],
            email: attrs[:email],
            first_name: attrs[:first_name],
            last_name: attrs[:last_name]
          )
        end
      end
    
      private def self.create_categories
        ["Technology", "Lifestyle", "Travel", "Food", "Programming"].map do |name|
          Category.create!(name: name)
        end
      end
    
      private def self.create_posts(users : Array(User), categories : Array(Category))
        posts = [] of Post
    
        users.each do |user|
          rand(2..5).times do |i|
            category = categories.sample
            posts << Post.create!(
              title: "#{user.display_name}'s Post ##{i + 1}",
              content: "This is a sample blog post by #{user.full_name}. " * 10,
              user_id: user.id.not_nil!,
              category_id: category.id,
              published: rand < 0.8,  # 80% published
              views_count: rand(0_i64..500_i64)
            )
          end
        end
    
        posts
      end
    
      private def self.create_comments(users : Array(User), posts : Array(Post))
        comments = [] of Comment
    
        posts.select(&.published?).each do |post|
          rand(0..5).times do
            commenter = rand < 0.9 ? users.sample : nil  # 10% anonymous
            comments << Comment.create!(
              content: "Great post! " + ["I learned a lot.", "Thanks for sharing.", "Very helpful!"].sample,
              post_id: post.id.not_nil!,
              user_id: commenter.try(&.id)
            )
          end
        end
    
        comments
      end
    end
    
    # Run if executed directly
    if PROGRAM_NAME.includes?("seeders")
      BlogEngine.setup
      Seeders.run
    end
    crystal src/seeders.cr
    # src/monitoring.cr
    require "./blog_engine"
    
    module Monitoring
      def self.print_stats
        puts ""
        puts "Blog Statistics"
        puts "==============="
        puts ""
    
        # Basic counts
        puts "Content:"
        puts "  Users: #{User.count} (#{User.where(active: true).count} active)"
        puts "  Categories: #{Category.count}"
        puts "  Posts: #{Post.count} (#{Post.where(published: true).count} published)"
        puts "  Comments: #{Comment.count}"
        puts ""
    
        # Engagement
        puts "Engagement:"
        total_views = Post.where(published: true).sum(:views_count)
        avg_views = Post.where(published: true).count > 0 ?
                    total_views / Post.where(published: true).count : 0
        puts "  Total views: #{total_views}"
        puts "  Average views per post: #{avg_views}"
        puts ""
    
        # Top content
        puts "Top Posts by Views:"
        Post.where(published: true)
            .order(views_count: :desc)
            .limit(5)
            .all
            .each do |post|
          author = post.user.try(&.display_name) || "Unknown"
          puts "  #{post.views_count} views - \"#{post.title}\" by #{author}"
        end
        puts ""
    
        # Most active users
        puts "Most Active Authors:"
        User.all
            .map { |u| {user: u, count: u.posts.where(published: true).count} }
            .sort_by { |h| -h[:count] }
            .first(5)
            .each do |h|
          puts "  #{h[:count]} posts - #{h[:user].full_name}"
        end
      end
    end
    
    # Run if executed directly
    if PROGRAM_NAME.includes?("monitoring")
      BlogEngine.setup
      Monitoring.print_stats
    end
    # src/demo.cr
    require "./blog_engine"
    require "./seeders"
    require "./monitoring"
    
    puts "="*60
    puts "CQL Blog Engine Demo"
    puts "="*60
    puts ""
    
    # Setup
    BlogEngine.setup
    BlogEngine.migrate
    
    # Seed data if database is empty
    if User.count == 0
      Seeders.run
    end
    
    # Show statistics
    Monitoring.print_stats
    
    # Demonstrate queries
    puts ""
    puts "Recent Published Posts:"
    puts "-"*40
    
    Post.where(published: true)
        .order(created_at: :desc)
        .limit(5)
        .all
        .each do |post|
      author = post.user.try(&.full_name) || "Unknown"
      comment_count = post.comments.count
      puts ""
      puts "#{post.title}"
      puts "  by #{author}"
      puts "  #{post.word_count} words | #{post.views_count} views | #{comment_count} comments"
      puts "  #{post.excerpt(100)}"
    end
    
    puts ""
    puts "="*60
    puts "Demo complete!"
    puts "="*60
    crystal src/demo.cr
    # src/database.cr
    adapter = if ENV["CRYSTAL_ENV"]? == "production"
      CQL::Adapter::Postgres
    else
      CQL::Adapter::SQLite
    end
    
    uri = ENV["DATABASE_URL"]? || "sqlite3://./db/development.db"
    
    BlogDB = CQL::Schema.define(:blog_db, adapter: adapter, uri: uri) do
    end
    # Production
    export CRYSTAL_ENV=production
    export DATABASE_URL=postgres://user:pass@host:5432/blog_production
    crystal init app myapp
    cd myapp
    dependencies:
      cql:
        github: azutoolkit/cql
        version: ~> 0.0.435
    
      # Choose your database driver:
      pg: # For PostgreSQL
        github: will/crystal-pg
        version: "~> 0.26.0"
    
      # OR
      mysql: # For MySQL
        github: crystal-lang/crystal-mysql
        version: "~> 0.14.0"
    
      # OR
      sqlite3: # For SQLite
        github: crystal-lang/crystal-sqlite3
        version: "~> 0.18.0"
    shards install
    # src/database.cr
    require "cql"
    require "pg"
    
    DATABASE_URL = ENV["DATABASE_URL"]? || "postgres://username:password@localhost:5432/myapp_development"
    
    AcmeDB = CQL::Schema.define(
      :acme_db,
      adapter: CQL::Adapter::Postgres,
      uri: DATABASE_URL
    ) do
      # Tables will be defined through migrations
    end
    # src/database.cr
    require "cql"
    require "sqlite3"
    
    AcmeDB = CQL::Schema.define(
      :acme_db,
      adapter: CQL::Adapter::SQLite,
      uri: "sqlite3://./db/development.db"
    ) do
      # Tables will be defined through migrations
    end
    # src/database.cr
    require "cql"
    require "mysql"
    
    AcmeDB = CQL::Schema.define(
      :acme_db,
      adapter: CQL::Adapter::MySql,
      uri: "mysql://username:password@localhost:3306/myapp_development"
    ) do
      # Tables will be defined through migrations
    end
    mkdir -p migrations
    # migrations/001_create_users.cr
    class CreateUsers < CQL::Migration(1)
      def up
        schema.table :users do
          primary :id, Int64, auto_increment: true
          column :name, String, null: false
          column :email, String, null: false
          column :active, Bool, default: false
          timestamps  # Creates created_at and updated_at columns
    
          index [:email], unique: true
        end
    
        schema.users.create!
      end
    
      def down
        schema.users.drop!
      end
    end
    # src/models/user.cr
    struct User
      include CQL::ActiveRecord::Model(Int64)
    
      # Connect to the database and table
      db_context AcmeDB, :users
    
      # Define your model attributes
      property id : Int64?
      property name : String
      property email : String
      property active : Bool = false
      property created_at : Time?
      property updated_at : Time?
    
      # Constructor for creating new instances
      def initialize(@name : String, @email : String, @active : Bool = false)
      end
    end
    # src/setup.cr
    require "./database"
    require "./models/*"
    require "../migrations/*"
    
    # Configure automatic schema file synchronization
    config = CQL::MigratorConfig.new(
      schema_file_path: "src/schemas/acme_schema.cr",
      schema_name: :AcmeSchema,
      auto_sync: true
    )
    
    # Initialize the database
    AcmeDB.init
    
    # Create migrator and apply migrations
    migrator = AcmeDB.migrator(config)
    migrator.up
    
    puts "Database initialized with #{migrator.applied_migrations.size} migrations"
    crystal src/setup.cr
    # src/myapp.cr
    require "./database"
    require "./models/*"
    
    AcmeDB.init
    
    # CREATE: Add new users
    user = User.new(name: "Alice Johnson", email: "alice@example.com", active: true)
    
    if user.save
      puts "User created with ID: #{user.id}"
    else
      puts "Failed to create user"
    end
    
    # Or create and save in one step
    bob = User.create!(
      name: "Bob Smith",
      email: "bob@example.com",
      active: true
    )
    puts "Created user: #{bob.name}"
    
    # READ: Find users
    found_user = User.find(1)
    if found_user
      puts "Found user: #{found_user.name}"
    end
    
    # Find by attributes
    alice = User.find_by(email: "alice@example.com")
    puts "User: #{alice.try(&.name)}"
    
    # Query with conditions
    active_users = User.where(active: true).all
    puts "Active users: #{active_users.size}"
    
    # UPDATE: Modify users
    if found_user
      found_user.active = false
      found_user.save
      puts "User updated"
    end
    
    # DELETE: Remove users
    inactive = User.where(active: false).first
    inactive.try(&.delete!)
    puts "Inactive user deleted"
    crystal src/myapp.cr
    # migrations/002_create_posts.cr
    class CreatePosts < CQL::Migration(2)
      def up
        schema.table :posts do
          primary :id, Int64, auto_increment: true
          column :title, String, null: false
          column :body, String, null: false
          column :user_id, Int64, null: false
          timestamps
    
          foreign_key [:user_id], references: :users, references_columns: [:id], on_delete: :cascade
          index [:user_id]
        end
    
        schema.posts.create!
      end
    
      def down
        schema.posts.drop!
      end
    end
    # src/models/post.cr
    struct Post
      include CQL::ActiveRecord::Model(Int64)
    
      db_context AcmeDB, :posts
    
      property id : Int64?
      property title : String
      property body : String
      property user_id : Int64
      property created_at : Time?
      property updated_at : Time?
    
      # Associations
      belongs_to :user, User, :user_id
    
      def initialize(@title : String, @body : String, @user_id : Int64)
      end
    end
    # src/models/user.cr (updated)
    struct User
      include CQL::ActiveRecord::Model(Int64)
    
      db_context AcmeDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property active : Bool = false
      property created_at : Time?
      property updated_at : Time?
    
      # Associations
      has_many :posts, Post, :user_id
    
      def initialize(@name : String, @email : String, @active : Bool = false)
      end
    end
    # Create user and posts
    user = User.create!(name: "Alice", email: "alice@example.com")
    
    post1 = Post.create!(
      title: "My First Post",
      body: "This is my first blog post!",
      user_id: user.id.not_nil!
    )
    
    post2 = Post.create!(
      title: "Another Post",
      body: "More content here",
      user_id: user.id.not_nil!
    )
    
    # Access associated records
    user_posts = user.posts.all
    puts "#{user.name} has #{user_posts.size} posts"
    
    # Access parent record
    post = Post.find(1)
    if post
      author = post.user
      puts "Post '#{post.title}' by #{author.try(&.name)}"
    end
    struct User
      include CQL::ActiveRecord::Model(Int64)
    
      db_context AcmeDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property active : Bool = false
      property created_at : Time?
      property updated_at : Time?
    
      has_many :posts, Post, :user_id
    
      # Add validations
      validate :name, presence: true, size: (1..100)
      validate :email, presence: true, match: /@/
    
      def initialize(@name : String, @email : String, @active : Bool = false)
      end
    end
    User.transaction do |tx|
      user = User.create!(name: "Charlie", email: "charlie@example.com")
    
      post = Post.create!(
        title: "Welcome Post",
        body: "Thanks for joining!",
        user_id: user.id.not_nil!
      )
    
      # If any operation fails, everything is rolled back
      puts "User and welcome post created successfully"
    end
    myapp/
    ├── shard.yml
    ├── src/
    │   ├── myapp.cr           # Main application
    │   ├── database.cr        # Database configuration
    │   ├── setup.cr           # Migration runner
    │   ├── models/
    │   │   ├── user.cr        # User model
    │   │   └── post.cr        # Post model
    │   └── schemas/
    │       └── acme_schema.cr # Auto-generated schema
    ├── migrations/
    │   ├── 001_create_users.cr
    │   └── 002_create_posts.cr
    └── db/
        └── development.db     # SQLite database file (if using SQLite)
    begin
      AcmeDB.init
      puts "Database connection successful"
    rescue ex
      puts "Database connection failed: #{ex.message}"
    end
    migrator = AcmeDB.migrator(config)
    puts "Applied migrations: #{migrator.applied_migrations.size}"
    puts "Pending migrations: #{migrator.pending_migrations.size}"
    
    # Reset database (development only)
    migrator.down_to(0)  # Rollback all
    migrator.up          # Reapply all
    # src/models/user.cr
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context BlogDB, :users
    
      property id : Int64?
      property username : String
      property email : String
      property first_name : String?
      property last_name : String?
      property active : Bool = true
      property created_at : Time?
      property updated_at : Time?
    
      # Relationships
      has_many :posts, Post, :user_id
      has_many :comments, Comment, :user_id
    
      def initialize(
        @username : String,
        @email : String,
        @first_name : String? = nil,
        @last_name : String? = nil,
        @active : Bool = true
      )
      end
    
      # Custom methods
      def full_name : String
        if first_name && last_name
          "#{first_name} #{last_name}"
        elsif first_name
          first_name.not_nil!
        else
          username
        end
      end
    
      def display_name : String
        first_name || username
      end
    end
    # src/models/category.cr
    struct Category
      include CQL::ActiveRecord::Model(Int64)
      db_context BlogDB, :categories
    
      property id : Int64?
      property name : String
      property slug : String
      property created_at : Time?
      property updated_at : Time?
    
      # Relationships
      has_many :posts, Post, :category_id
    
      def initialize(@name : String, @slug : String? = nil)
        @slug ||= generate_slug(@name)
      end
    
      # Generate URL-friendly slug from name
      private def generate_slug(text : String) : String
        text.downcase
            .gsub(/[^a-z0-9\s-]/, "")
            .gsub(/\s+/, "-")
            .gsub(/-+/, "-")
            .strip("-")
      end
    end
    # src/models/post.cr
    struct Post
      include CQL::ActiveRecord::Model(Int64)
      db_context BlogDB, :posts
    
      property id : Int64?
      property title : String
      property content : String
      property published : Bool = false
      property views_count : Int64 = 0_i64
      property user_id : Int64
      property category_id : Int64?
      property created_at : Time?
      property updated_at : Time?
    
      # Relationships
      belongs_to :user, User, :user_id
      belongs_to :category, Category, :category_id
      has_many :comments, Comment, :post_id
    
      def initialize(
        @title : String,
        @content : String,
        @user_id : Int64,
        @category_id : Int64? = nil,
        @published : Bool = false,
        @views_count : Int64 = 0_i64
      )
      end
    
      # Custom methods
      def published? : Bool
        published
      end
    
      def draft? : Bool
        !published
      end
    
      def word_count : Int32
        content.split.size
      end
    
      def reading_time_minutes : Int32
        (word_count / 200.0).ceil.to_i  # Assuming 200 words per minute
      end
    
      def excerpt(length : Int32 = 150) : String
        if content.size <= length
          content
        else
          content[0, length].rstrip + "..."
        end
      end
    
      def increment_views!
        @views_count += 1
        save
      end
    end
    # src/models/comment.cr
    struct Comment
      include CQL::ActiveRecord::Model(Int64)
      db_context BlogDB, :comments
    
      property id : Int64?
      property content : String
      property post_id : Int64
      property user_id : Int64?  # Nullable for anonymous comments
      property created_at : Time?
      property updated_at : Time?
    
      # Relationships
      belongs_to :post, Post, :post_id
      belongs_to :user, User, :user_id
    
      def initialize(
        @content : String,
        @post_id : Int64,
        @user_id : Int64? = nil
      )
      end
    
      # Custom methods
      def anonymous? : Bool
        user_id.nil?
      end
    
      def author_name : String
        if u = user
          u.display_name
        else
          "Anonymous"
        end
      end
    end
    # src/blog_engine.cr
    require "./database"
    require "./models/user"
    require "./models/category"
    require "./models/post"
    require "./models/comment"
    
    module BlogEngine
      VERSION = "0.1.0"
    
      def self.setup
        BlogDB.init
        puts "Database connected"
      end
    
      def self.migrate
        require "../migrations/*"
    
        migrator = BlogDB.migrator(MIGRATOR_CONFIG)
        pending = migrator.pending_migrations.size
    
        if pending > 0
          puts "Running #{pending} pending migration(s)..."
          migrator.up
          puts "Migrations complete"
        else
          puts "Database is up to date"
        end
      end
    end
    # src/test_models.cr
    require "./blog_engine"
    
    BlogEngine.setup
    
    puts "Testing Models"
    puts "=============="
    
    # Create a user
    alice = User.new(
      username: "alice",
      email: "alice@example.com",
      first_name: "Alice",
      last_name: "Johnson"
    )
    
    if alice.save
      puts "Created user: #{alice.full_name} (ID: #{alice.id})"
    else
      puts "Failed to create user"
      exit 1
    end
    
    # Create a category
    tech = Category.new(name: "Technology")
    if tech.save
      puts "Created category: #{tech.name} (slug: #{tech.slug})"
    end
    
    # Create a post
    post = Post.new(
      title: "Getting Started with Crystal",
      content: "Crystal is a wonderful programming language...",
      user_id: alice.id.not_nil!,
      category_id: tech.id,
      published: true
    )
    
    if post.save
      puts "Created post: #{post.title}"
      puts "  Word count: #{post.word_count}"
      puts "  Reading time: #{post.reading_time_minutes} min"
    end
    
    # Create a comment
    comment = Comment.new(
      content: "Great article!",
      post_id: post.id.not_nil!,
      user_id: alice.id
    )
    
    if comment.save
      puts "Created comment by: #{comment.author_name}"
    end
    
    # Test relationships
    puts ""
    puts "Testing Relationships"
    puts "====================="
    
    # User -> Posts
    puts "#{alice.full_name}'s posts:"
    alice.posts.all.each do |p|
      puts "  - #{p.title}"
    end
    
    # Post -> User (author)
    puts ""
    puts "Post author: #{post.user.try(&.full_name)}"
    
    # Post -> Category
    puts "Post category: #{post.category.try(&.name)}"
    
    # Post -> Comments
    puts "Post comments: #{post.comments.all.size}"
    
    # Comment -> User
    puts "Comment author: #{comment.author_name}"
    
    puts ""
    puts "All tests passed!"
    crystal src/test_models.cr
    belongs_to :user, User, :user_id
    post = Post.find(1)
    author = post.user  # Returns User?
    has_many :posts, Post, :user_id
    user = User.find(1)
    all_posts = user.posts.all           # Get all posts
    published = user.posts.where(published: true).all  # Filter
    count = user.posts.count             # Count posts
    CRUD Operations
    Adding Features

    Enabled for development

    Your First CQL App
    Part 4: CRUD Operations
    Part 5: Adding Features

    INT

    text

    TEXT

    TEXT

    TEXT

    boolean

    INTEGER

    BOOLEAN

    TINYINT

    timestamps

    TEXT (x2)

    TIMESTAMP (x2)

    DATETIME (x2)

    Part 4: CRUD Operations
    Part 5: Adding Features
    Part 4: CRUD Operations
    Add Validations
    Part 4: CRUD Operations
    Part 5: Adding Features
    Migrations
    GitHub Issuesarrow-up-right
    Tutorials
    How-to Guides
    Reference
    Explanation

    Define a Model

    This guide shows you how to define a CQL Active Record model that maps to a database table.

    hashtag
    Basic Model Definition

    To define a model, create a struct or class that includes the Active Record module:

    hashtag
    Key Components

    hashtag
    1. Include the Active Record Module

    The type parameter specifies your primary key type: Int32, Int64, UUID, or String (for ULIDs).

    hashtag
    2. Set the Database Context

    This connects the model to a specific schema and table.

    hashtag
    3. Define Properties

    hashtag
    4. Create a Constructor

    hashtag
    Primary Key Types

    hashtag
    Integer (default)

    hashtag
    UUID

    hashtag
    ULID (for sortable IDs)

    hashtag
    Excluding Fields from Persistence

    Use the DB::Field annotation to exclude virtual fields:

    hashtag
    Adding Custom Methods

    Add business logic directly in your model:

    hashtag
    Verify Your Model

    Test that your model works:

    hashtag
    Common Issues

    "No database context defined": Ensure you called db_context with correct schema and table.

    "Could not find column": Property names must match database column names.

    "Type mismatch": Crystal property types must match database column types.

    hashtag
    Related

    How-to Overview

    Task-oriented guides that provide step-by-step instructions for accomplishing specific goals. Each guide focuses on a single task and assumes you have basic CQL knowledge.

    hashtag
    Models

    • Define a Model

    hashtag
    Relationships

    hashtag
    Querying

    hashtag
    Data Operations

    hashtag
    Migrations

    hashtag
    Configuration

    hashtag
    Caching

    hashtag
    Performance

    hashtag
    Testing

    hashtag
    Troubleshooting


    Need more context? Check out the section to understand the concepts behind these tasks.

    Part 4: CRUD Operations

    In this part, you'll learn how to create, read, update, and delete records in your blog engine. You'll also explore querying patterns, bulk operations, and best practices for working with data.

    hashtag
    What You'll Learn

    • Creating records (single and bulk)

    Real-World Examples

    This tutorial demonstrates real-world migration workflows for managing database schema changes in production environments.

    hashtag
    What You'll Learn

    • Planning schema changes

    Use Timestamps

    This guide shows you how to use automatic timestamps for tracking when records are created and updated.

    hashtag
    Add Timestamps to Schema

    Use the timestamps helper in your table definition:

    This creates two columns:

    Add Optimistic Locking

    This guide shows you how to add optimistic locking to prevent concurrent updates from overwriting each other.

    hashtag
    When to Use

    Use optimistic locking when:

    • Multiple users might edit the same record simultaneously

    Performance Monitoring

    This tutorial shows you how to track and optimize query performance in your CQL application.

    hashtag
    What You'll Learn

    • Setting up query logging

    Set Up Has Many

    This guide shows you how to set up a has_many relationship where one model has multiple related records.

    hashtag
    When to Use

    Use has_many when:

    Set Up Belongs To

    This guide shows you how to set up a belongs_to relationship where one model references another via a foreign key.

    hashtag
    When to Use

    Use belongs_to when:

    Use Cursor Pagination

    This guide shows you how to implement cursor-based pagination for efficient navigation through large datasets.

    hashtag
    Why Cursor Pagination?

    Cursor pagination is better than offset pagination for:

    Use Callbacks

    This guide shows you how to use lifecycle callbacks to run code at specific points during a model's lifecycle.

    hashtag
    Available Callbacks

    hashtag
    Validation Callbacks

    Add Validations

    This guide shows you how to add validations to your CQL models to ensure data integrity.

    hashtag
    Basic Validation Syntax

    Use the validate macro to define validations:

    Set Up Has One

    This guide shows you how to set up a has_one relationship where one model has exactly one related model.

    hashtag
    When to Use

    Use has_one when:

    Create Query Scopes

    This guide shows you how to create reusable query scopes for common filtering patterns.

    hashtag
    Define a Scope

    Add class methods that return query builders:

    hashtag

    Paginate Results

    This guide shows you how to paginate query results for display in pages.

    hashtag
    Basic Pagination

    Use limit and offset:

    Delete Records

    This guide shows you how to delete records from the database.

    hashtag
    Delete a Single Record

    hashtag
    Delete with delete!

    Use Transactions

    This guide shows you how to use database transactions to ensure multiple operations succeed or fail together.

    hashtag
    Basic Transaction

    Wrap operations in a transaction block:

    Set Up Connection Pooling

    Configure database connection pooling for better performance and resource management.

    hashtag
    Prerequisites

    • CQL installed and configured

    Create a Migration

    This guide shows you how to create database migrations to manage schema changes.

    hashtag
    Migration File Structure

    Create migration files in a migrations/ directory:

    Use Per-Request Caching

    Cache query results within a single HTTP request to avoid duplicate queries.

    hashtag
    Prerequisites

    • CQL installed and configured

    Configure Database Connection

    This guide shows you how to configure your database connection in CQL.

    hashtag
    Basic PostgreSQL Connection

    hashtag
    Basic SQLite Connection

    Monitor Performance

    Track query performance and identify bottlenecks in your CQL application.

    hashtag
    Prerequisites

    • Working CQL application

    Optimize Queries

    Improve query performance with indexing, eager loading, and query optimization techniques.

    hashtag
    Prerequisites

    • Working CQL application

    Test Models

    Write effective tests for your CQL Active Record models.

    hashtag
    Prerequisites

    • CQL application with models

    Set Up Test Databases

    Configure a separate database for running tests without affecting development data.

    hashtag
    Prerequisites

    • CQL installed

    Fix Migration Errors

    This guide helps you diagnose and fix common migration errors.

    hashtag
    Common Error: Table Already Exists

    Error:

    Cause: Running migration twice or table was created manually.

    Solutions:

    Avoid N+1 Queries

    Prevent the N+1 query problem that causes excessive database queries.

    hashtag
    Understanding N+1

    The N+1 problem occurs when fetching a collection (1 query) and then accessing a relationship for each item (N queries).

    Set Up Many-to-Many

    This guide shows you how to set up a many-to-many relationship using a join table.

    hashtag
    When to Use

    Use many-to-many when:

    • A Post has many Tags, and a Tag has many Posts

    Find Records

    This guide shows you how to find records by their primary key or attributes.

    hashtag
    Find by ID

    hashtag
    Find by ID (raise if not found)

    Error Codes

    Common CQL errors and their meanings.

    hashtag
    Validation Errors

    hashtag
    ValidationError

    Build Complex Queries

    This guide shows you how to build complex queries with multiple conditions, joins, and aggregations.

    hashtag
    Chain Multiple Methods

    hashtag
    Compound Conditions

    Explanation Overview

    Understanding-oriented discussions that clarify concepts, explain design decisions, and provide background knowledge. Read these to deepen your understanding of CQL and ORMs in general.

    hashtag
    Concepts

    Fundamental concepts and ideas:

    Concepts

    An Object-Relational Mapping (ORM) is a programming technique that lets you interact with a relational database using your programming language's native objects, rather than writing raw SQL queries.

    hashtag
    The Problem ORMs Solve

    Without an ORM, you would write code like this:

    With an ORM like CQL:

    The ORM handles:

    Community

    Get help and connect with other CQL users.

    hashtag
    Getting Help

    hashtag
    GitHub Issues

    Glossary

    Common terms and definitions used in CQL documentation.

    hashtag
    A

    hashtag
    Active Record

    Resources Overview

    Additional resources to support your CQL journey.

    hashtag
    Support

    • - Frequently asked questions

    Implement Soft Deletes

    This guide shows you how to implement soft deletes so records are marked as deleted rather than permanently removed.

    hashtag
    Prerequisites

    Your table needs a deleted_at timestamp column:

    Create Records

    This guide shows you how to create new records in the database.

    hashtag
    Create with new + save

    Create an instance and save separately:

    hashtag

    Filter with Where Clauses

    This guide shows you how to filter records using where clauses.

    hashtag
    Basic Where

    hashtag
    Multiple Conditions

    Run Migrations

    This guide shows you how to run database migrations.

    hashtag
    Setup Migrator

    hashtag
    Run All Pending Migrations

    Add Columns

    This guide shows you how to add columns to existing tables using migrations.

    hashtag
    Add a Single Column

    hashtag
    Add Multiple Columns

    Enable Query Caching

    Improve performance by caching frequently executed queries.

    hashtag
    Prerequisites

    • CQL installed and configured

    Configure Multiple Environments

    This guide shows you how to configure different database connections for development, test, and production environments.

    hashtag
    Environment Detection

    hashtag

    Rollback Migrations

    This guide shows you how to rollback database migrations.

    hashtag
    Rollback Last Migration

    hashtag
    Rollback Multiple Migrations

    Update Records

    This guide shows you how to update existing records in the database.

    hashtag
    Update with save

    Change attributes and save:

    hashtag

    Enable SSL Connections

    Configure SSL/TLS for secure database connections.

    hashtag
    Prerequisites

    • CQL installed and configured

    Configure Redis Cache

    Set up Redis as your query cache store for shared caching across processes.

    hashtag
    Prerequisites

    • CQL installed

    Fix Validation Errors

    This guide helps you diagnose and fix validation errors.

    hashtag
    Check What's Invalid

    hashtag
    Common Error: Presence Validation Failed

    Quick Reference

    Essential CQL patterns for quick reference.

    hashtag
    Schema Definition

    hashtag
    Model Definition

    Migration DSL

    Complete reference for the CQL Migration DSL (Domain Specific Language).

    hashtag
    Migration Class

    Migrations inherit from CQL::Migration(VERSION) where VERSION is an integer (typically a timestamp):

    Reference Overview

    Information-oriented technical descriptions of CQL's APIs, configuration options, and database-specific features. Use this section when you need to look up exact syntax, method signatures, or configuration values.

    hashtag
    API Reference

    Comprehensive documentation of CQL's classes and methods:

    Design Patterns

    The Active Record pattern is the primary pattern used in CQL. Understanding it helps you write better models and make informed architectural decisions.

    hashtag
    What is Active Record?

    Active Record is a design pattern where an object wraps a row in a database table, encapsulating both the data and the database access logic.

    In Active Record:

    Validation Options

    Complete reference for all validation options available in CQL models.

    hashtag
    Validation Macro

    hashtag
    Common Options

    Callback Hooks

    Complete reference for lifecycle callbacks in CQL Active Record models.

    hashtag
    Overview

    Callbacks allow you to trigger logic at specific points in a record's lifecycle:

    hashtag

    FAQ

    hashtag
    General

    hashtag
    What is CQL?

    CQL (Crystal Query Language) is an ORM (Object-Relational Mapping) library for Crystal that provides type-safe database interactions, migrations, and Active Record patterns.

    Contributing

    Thank you for your interest in contributing to CQL!

    hashtag
    Ways to Contribute

    hashtag
    Report Bugs

    Repository Pattern

    The Repository pattern provides an alternative to Active Record by separating data persistence from domain objects. While CQL primarily uses Active Record, you can implement Repository patterns for specific use cases.

    hashtag
    What is the Repository Pattern?

    In the Repository pattern:

    Create Indexes

    This guide shows you how to create database indexes to improve query performance.

    hashtag
    Create a Simple Index

    hashtag
    Create a Unique Index

    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property created_at : Time?
      property updated_at : Time?
    
      def initialize(@name : String, @email : String)
      end
    end
    What is an ORM? - Understanding Object-Relational Mapping
  • Type Safety in CQLarrow-up-right - How CQL leverages Crystal's type system

  • Schemas vs Migrationsarrow-up-right - Understanding the two approaches to database structure

  • hashtag
    Design Patterns

    Architectural patterns supported by CQL:

    • Active Record Pattern - The primary pattern in CQL

    • Repository Pattern - Alternative data access approach

    • Data Mapper Patternarrow-up-right - Separating domain from persistence

    hashtag
    Architecture

    How CQL works internally:

    • CQL Architecturearrow-up-right - High-level system design

    • Query Executionarrow-up-right - How queries are built and executed

    • Migration Systemarrow-up-right - How migrations work

    hashtag
    Best Practices

    Guidelines for effective CQL usage:

    • Model Designarrow-up-right - Structuring your models

    • Security Considerationsarrow-up-right - Protecting your application

    • Deployment Guidearrow-up-right - Running CQL in production


    Ready to apply this knowledge? Head to the How-to Guides for practical instructions.

    A design pattern where objects carry both data and behavior. In CQL, models that include CQL::ActiveRecord::Model follow this pattern.

    hashtag
    Adapter

    The database-specific driver that CQL uses to communicate with different database systems. CQL supports PostgreSQL, MySQL, and SQLite adapters.

    hashtag
    Association

    A relationship between two models. See: belongs_to, has_one, has_many.

    hashtag
    B

    hashtag
    Belongs To

    A relationship where a model references another model via a foreign key. The model with belongs_to holds the foreign key.

    hashtag
    C

    hashtag
    Callback

    A method that is automatically called at specific points during a model's lifecycle (e.g., before_save, after_create).

    hashtag
    Column

    A single field in a database table, represented as a property in a CQL model.

    hashtag
    CRUD

    Create, Read, Update, Delete - the four basic operations for persistent storage.

    hashtag
    D

    hashtag
    Database Context

    The connection between a model and its database table, defined using db_context.

    hashtag
    Down Migration

    The reverse operation of a migration, typically used to rollback changes.

    hashtag
    F

    hashtag
    Foreign Key

    A column that references the primary key of another table, establishing a relationship between tables.

    hashtag
    H

    hashtag
    Has Many

    A relationship where a model has multiple related records in another table.

    hashtag
    Has One

    A relationship where a model has exactly one related record in another table.

    hashtag
    I

    hashtag
    Index

    A database structure that improves query performance on specific columns.

    hashtag
    M

    hashtag
    Migration

    A versioned change to the database schema, allowing incremental updates to the database structure.

    hashtag
    Model

    A Crystal struct or class that represents a database table and provides methods for interacting with its data.

    hashtag
    O

    hashtag
    Optimistic Locking

    A concurrency control strategy that detects conflicts when saving records by checking a version column.

    hashtag
    ORM

    Object-Relational Mapping - a technique for converting data between incompatible type systems (objects and relational databases).

    hashtag
    P

    hashtag
    Persistence

    The process of saving data to a database so it survives beyond the application's runtime.

    hashtag
    Primary Key

    A unique identifier for each record in a table, typically named id.

    hashtag
    Property

    A Crystal attribute that maps to a database column.

    hashtag
    Q

    hashtag
    Query Builder

    A fluent interface for constructing SQL queries programmatically.

    hashtag
    R

    hashtag
    Relationship

    A connection between two database tables, represented in CQL using belongs_to, has_one, or has_many.

    hashtag
    Rollback

    Reverting a database migration to its previous state.

    hashtag
    S

    hashtag
    Schema

    The structure of a database, including its tables, columns, indexes, and relationships.

    hashtag
    Scope

    A reusable query method defined on a model.

    hashtag
    Soft Delete

    A technique where records are marked as deleted (using a timestamp) rather than being removed from the database.

    hashtag
    T

    hashtag
    Table

    A collection of related data organized in rows and columns within a database.

    hashtag
    Timestamps

    Columns that automatically track when records are created (created_at) and updated (updated_at).

    hashtag
    Transaction

    A sequence of database operations that are executed as a single unit, ensuring all operations succeed or all are rolled back.

    hashtag
    U

    hashtag
    Up Migration

    The forward operation of a migration that applies changes to the database.

    hashtag
    UUID

    Universally Unique Identifier - a 128-bit identifier that can be used as a primary key.

    hashtag
    V

    hashtag
    Validation

    Rules that ensure data meets certain criteria before being saved to the database.

    hashtag
    Version Column

    A column used for optimistic locking that increments with each update.

    Model Methodsarrow-up-right - Instance and class methods available on Active Record models
  • Query Methodsarrow-up-right - Methods for building and executing queries

  • Schema DSLarrow-up-right - Schema definition language reference

  • Migration DSL - Migration definition language reference

  • Validation Options - All available validation types and options

  • Callback Hooks - Lifecycle callbacks reference

  • Configuration Optionsarrow-up-right - All configuration settings

  • hashtag
    Database Support

    Database-specific features and considerations:

    • PostgreSQLarrow-up-right - PostgreSQL-specific features and types

    • MySQLarrow-up-right - MySQL-specific features and types

    • SQLitearrow-up-right - SQLite-specific features and limitations

    hashtag
    Quick Reference

    • Quick Reference Card - Common operations at a glance

    • Glossary - CQL terminology and definitions

    • Error Codes - Common errors and their meanings


    Want to understand why things work this way? Visit the Explanation section for conceptual discussions.

    Set Up Many-to-Many
    Create Query Scopes
  • Paginate Results

  • Use Cursor Pagination

  • Use Transactions
    Add Columns
  • Create Indexes

  • Enable SSL Connections
    Add Validations
    Use Callbacks
    Implement Soft Deletes
    Add Optimistic Locking
    Use Timestamps
    Set Up Belongs To
    Set Up Has One
    Set Up Has Many
    Find Records
    Filter with Where Clauses
    Build Complex Queries
    Create Records
    Update Records
    Delete Records
    Create a Migration
    Run Migrations
    Rollback Migrations
    Configure Database Connection
    Set Up Connection Pooling
    Configure Multiple Environments
    Enable Query Caching
    Configure Redis Cache
    Use Per-Request Caching
    Optimize Queries
    Avoid N+1 Queries
    Monitor Performance
    Test Models
    Set Up Test Databases
    Fix Connection Errors
    Fix Migration Errors
    Fix Validation Errors
    Explanation

    Community - Get help and connect with other CQL users

  • Contributing - How to contribute to CQL

  • hashtag
    External Links

    • GitHub Repositoryarrow-up-right - Source code and issue tracking

    • Crystal Languagearrow-up-right - The language CQL is built with

    FAQ
    Add Validations
    Use Callbacks
    Set Up Relationships

    Creating safe migrations

  • Testing migrations before deployment

  • Rolling back when needed

  • Handling data migrations

  • hashtag
    Prerequisites

    • Completed a getting started tutorial

    • Understanding of database migrations

    hashtag
    Workflow 1: Adding a New Feature

    Let's add a tagging system to a blog application.

    hashtag
    Step 1: Plan the Schema Changes

    Before writing code, plan what tables and columns you need:

    hashtag
    Step 2: Create the Migrations

    Create migrations in the correct order:

    hashtag
    Step 3: Test Locally

    hashtag
    Step 4: Create the Models

    hashtag
    Workflow 2: Modifying Existing Tables

    Adding a new column to an existing table requires careful planning.

    hashtag
    Step 1: Plan for Zero Downtime

    When adding columns:

    • Make new columns nullable OR provide defaults

    • Add columns first, then update code

    • Never remove columns that existing code depends on

    hashtag
    Step 2: Create the Migration

    hashtag
    Step 3: Deploy Strategy

    1. Deploy migration (adds column with default)

    2. Deploy new code that uses the column

    3. Backfill data if needed

    hashtag
    Workflow 3: Data Migrations

    Sometimes you need to transform existing data.

    hashtag
    Step 1: Create Migration with Data Transformation

    hashtag
    Step 2: For Large Tables, Use Batching

    hashtag
    Workflow 4: Rolling Back

    hashtag
    Safe Rollback

    hashtag
    When Rollback Isn't Possible

    Some migrations can't be rolled back:

    • Data loss migrations (dropping columns with data)

    • Data transformations

    • Renaming with data loss

    Mark these clearly:

    hashtag
    Workflow 5: Production Deployment

    hashtag
    Pre-Deployment Checklist

    1. Test migration locally

    2. Test migration on staging

    3. Backup production database

    4. Plan rollback strategy

    5. Schedule maintenance window if needed

    hashtag
    Deployment Script

    hashtag
    Best Practices Summary

    1. Always write reversible migrations when possible

    2. Test migrations on a copy of production data

    3. Make additive changes first (add columns before code uses them)

    4. Use transactions for related changes

    5. Batch large data migrations

    6. Document irreversible migrations

    7. Have a rollback plan before deploying

    hashtag
    Next Steps

    • Create a Migration

    • Rollback Migrations

    • Migration DSL Reference

    created_at - Set when record is first saved

  • updated_at - Updated each time record is saved

  • hashtag
    Add to Model

    Define the timestamp properties in your model:

    hashtag
    Automatic Timestamps

    CQL automatically sets timestamps when you save:

    hashtag
    Manual Timestamps with Callbacks

    If you need more control, use callbacks:

    hashtag
    Query by Timestamp

    Filter records by their timestamps:

    hashtag
    Add Timestamps to Existing Table

    Create a migration:

    hashtag
    Useful Timestamp Methods

    Add helper methods to your model:

    hashtag
    Verify It Works

    hashtag
    Related

    • Define a Model

    • Use Callbacks

    • Filter with Where Clauses

  • Conflicts are rare but data integrity is important

  • You want to avoid database-level row locks

  • hashtag
    Prerequisites

    Your table needs a version column:

    Or add to an existing table:

    hashtag
    Enable Optimistic Locking

    Include the module and configure the version column:

    hashtag
    Handle Updates

    Updates automatically check the version:

    hashtag
    Handle Conflicts

    When a conflict occurs, reload and retry:

    hashtag
    Retry with Limit

    Implement retry logic with a maximum attempts:

    hashtag
    How It Works

    1. Record is loaded with current version (e.g., version 5)

    2. When updating, CQL generates: UPDATE users SET name = ?, version = 6 WHERE id = ? AND version = 5

    3. If another process updated first (version is now 6), the WHERE clause matches 0 rows

    4. CQL raises OptimisticLockError

    5. Your code handles by reloading and retrying

    hashtag
    Combine with Soft Deletes

    You can use both modules together:

    hashtag
    Verify It Works

    hashtag
    Best Practices

    1. Always reload after conflict - Get latest data before retrying

    2. Limit retries - Avoid infinite loops

    3. Inform users - In UIs, tell users the data changed

    4. Use for important data - Not every table needs locking

    5. Test conflict scenarios - Write tests that simulate concurrent updates

    hashtag
    Related

    • Define a Model

    • Implement Soft Deletes

    • Use Transactions

    Identifying slow queries

  • Detecting N+1 query problems

  • Building a performance dashboard

  • Optimization strategies

  • hashtag
    Prerequisites

    • A working CQL application

    • Basic understanding of database performance concepts

    hashtag
    Step 1: Enable Query Logging

    Configure logging to capture all database queries:

    hashtag
    Step 2: Create a Query Monitor

    Build a simple query monitor to track execution times:

    hashtag
    Step 3: Wrap Database Operations

    Create a timing wrapper:

    hashtag
    Step 4: Detect N+1 Queries

    N+1 queries happen when you load a list, then query for each item:

    hashtag
    Solution: Eager Loading

    hashtag
    Step 5: Create a Dashboard

    Build a simple performance dashboard:

    hashtag
    Step 6: Monitor in Tests

    Add performance assertions to your tests:

    hashtag
    Step 7: Production Monitoring

    For production, integrate with your monitoring stack:

    hashtag
    Optimization Checklist

    When you find performance issues:

    hashtag
    1. Add Missing Indexes

    hashtag
    2. Use Select to Limit Columns

    hashtag
    3. Batch Large Operations

    hashtag
    4. Cache Expensive Queries

    hashtag
    Summary

    You've learned:

    1. How to enable query logging

    2. Building a query monitor

    3. Detecting N+1 queries

    4. Creating a performance dashboard

    5. Monitoring in tests and production

    hashtag
    Next Steps

    • Optimize Queries

    • Avoid N+1 Queries

    • Enable Query Caching

    A User has many Posts
  • A Post has many Comments

  • A Category has many Products

  • hashtag
    Schema Setup

    hashtag
    Define the Relationship

    Add has_many to the parent model:

    hashtag
    Query Related Records

    hashtag
    Get All Related Records

    hashtag
    Count Related Records

    hashtag
    Filter Related Records

    hashtag
    Create Related Records

    hashtag
    Delete Related Records

    hashtag
    Delete Specific Posts

    hashtag
    Delete All Posts

    hashtag
    Cascade Delete with Foreign Keys

    Configure foreign key to cascade deletes:

    Now when a user is deleted, their posts are automatically deleted.

    hashtag
    Check for Related Records

    hashtag
    Verify It Works

    hashtag
    Related

    • Set Up Belongs To

    • Set Up Has One

    • Set Up Many-to-Many

    A Comment belongs to a Post
  • An Order belongs to a User

  • A Photo belongs to an Album

  • The model with belongs_to holds the foreign key.

    hashtag
    Schema Setup

    Create tables with a foreign key column:

    hashtag
    Define the Relationship

    Add belongs_to to the child model:

    hashtag
    Access the Parent

    hashtag
    Create with Parent

    hashtag
    Set Foreign Key Directly

    hashtag
    Set via Association

    hashtag
    Build Associated Parent

    Build a parent without saving:

    hashtag
    Create Associated Parent

    Create and save parent in one step:

    hashtag
    Optional Belongs To

    For optional relationships, make the foreign key nullable:

    hashtag
    Verify It Works

    hashtag
    Related

    • Set Up Has Many

    • Set Up Has One

    • Define a Model

    Large datasets (offset becomes slow)
  • Real-time data (new items don't shift pages)

  • Infinite scroll interfaces

  • API pagination

  • hashtag
    Basic Cursor Pagination

    Use the ID as a cursor:

    hashtag
    Usage

    hashtag
    Bidirectional Cursors

    Support forward and backward navigation:

    hashtag
    Timestamp-Based Cursor

    For chronologically ordered data:

    hashtag
    Compound Cursor

    When ordering by non-unique column:

    hashtag
    API Response Format

    hashtag
    Encode/Decode Cursors

    For opaque cursors:

    hashtag
    Verify Cursor Pagination Works

    hashtag
    Related

    • Paginate Results

    • Build Complex Queries

    • Create Query Scopes

  • before_validation - Before validations run

  • after_validation - After validations complete

  • hashtag
    Save Callbacks (create and update)

    • before_save - Before any save operation

    • after_save - After any save operation

    hashtag
    Create Callbacks (new records only)

    • before_create - Before inserting a new record

    • after_create - After inserting a new record

    hashtag
    Update Callbacks (existing records only)

    • before_update - Before updating an existing record

    • after_update - After updating an existing record

    hashtag
    Destroy Callbacks

    • before_destroy - Before deleting a record

    • after_destroy - After deleting a record

    hashtag
    Basic Usage

    Register a callback by calling its macro with a method name:

    hashtag
    Callback Order

    When saving a new record:

    1. before_validation

    2. Validations run

    3. after_validation

    4. before_save

    5. before_create

    6. INSERT into database

    7. after_create

    8. after_save

    When updating an existing record:

    1. before_validation

    2. Validations run

    3. after_validation

    4. before_save

    5. before_update

    6. UPDATE in database

    7. after_update

    8. after_save

    When destroying:

    1. before_destroy

    2. DELETE from database

    3. after_destroy

    hashtag
    Halting the Chain

    Return false from a before_* callback to halt execution:

    hashtag
    Common Use Cases

    hashtag
    Normalizing Data

    hashtag
    Setting Timestamps

    hashtag
    Generating Slugs

    hashtag
    Sending Notifications

    hashtag
    Cleaning Up Related Data

    hashtag
    Multiple Callbacks

    You can register multiple callbacks for the same event:

    hashtag
    Best Practices

    1. Keep callbacks simple - Complex logic belongs in service objects

    2. Always return true unless you intend to halt

    3. Make callback methods private - They're internal to the model

    4. Avoid external API calls in callbacks without error handling

    5. Test callbacks - Ensure they work as expected

    hashtag
    Verify It Works

    hashtag
    Related

    • Define a Model

    • Add Validations

    • Callback Hooks Reference

    hashtag
    Available Validators

    hashtag
    Presence and Required

    hashtag
    Size

    hashtag
    Numeric Comparisons

    hashtag
    Pattern Matching

    hashtag
    Inclusion and Exclusion

    hashtag
    Custom Error Messages

    Add the message parameter:

    hashtag
    Checking Validity

    hashtag
    Check if Valid

    hashtag
    Access Errors

    hashtag
    Validate and Raise

    hashtag
    Custom Validators

    For complex validation logic, create a custom validator:

    hashtag
    Uniqueness Validation

    Check for uniqueness manually in a custom validation:

    hashtag
    Validations and Save

    Validations run automatically when saving:

    hashtag
    Verify It Works

    hashtag
    Related

    • Define a Model

    • Use Callbacks

    • Validation Options Reference

    A User has one Profile
  • An Order has one ShippingAddress

  • A Company has one Address

  • The other model holds the foreign key.

    hashtag
    Schema Setup

    Note: The unique index ensures only one profile per user.

    hashtag
    Define the Relationship

    Add has_one to the parent model:

    hashtag
    Access the Related Record

    hashtag
    Create Related Record

    hashtag
    Create Profile for User

    hashtag
    Build and Save

    hashtag
    Update Related Record

    hashtag
    Delete Related Record

    hashtag
    Verify It Works

    hashtag
    Related

    • Set Up Belongs To

    • Set Up Has Many

    • Define a Model

    Use Scopes

    hashtag
    Scope with Parameters

    hashtag
    Conditional Scopes

    hashtag
    Default Scope Pattern

    Create a method for commonly used conditions:

    hashtag
    Scope Chaining

    Scopes can be chained in any order:

    hashtag
    Scope with Ordering

    hashtag
    Complex Scopes

    hashtag
    Scope in Related Queries

    Scopes work with relationships:

    hashtag
    Verify Scopes Work

    hashtag
    Related

    • Filter with Where Clauses

    • Build Complex Queries

    • Paginate Results

    hashtag
    Pagination Helper

    Create a reusable pagination method:

    hashtag
    Pagination Info

    Get total count for pagination UI:

    hashtag
    Cursor-Based Pagination

    For better performance with large datasets:

    hashtag
    Timestamp-Based Pagination

    For chronological data:

    hashtag
    Keyset Pagination

    More efficient for large datasets:

    hashtag
    Web API Example

    hashtag
    Verify Pagination Works

    hashtag
    Related

    • Filter with Where Clauses

    • Build Complex Queries

    • Create Query Scopes

    Raises if record doesn't exist:

    hashtag
    Delete with destroy!

    destroy! triggers callbacks:

    hashtag
    Delete by ID

    hashtag
    Delete with Conditions

    hashtag
    Delete All Records

    hashtag
    Soft Delete (if enabled)

    If using soft deletes:

    hashtag
    Cascade Delete

    With foreign key cascade:

    hashtag
    Delete Related Records First

    Without cascade, delete children first:

    hashtag
    Delete with Transaction

    Ensure atomic deletion:

    hashtag
    Check Before Delete

    hashtag
    Verify Deletion

    hashtag
    Related

    • Create Records

    • Update Records

    • Implement Soft Deletes

    hashtag
    Automatic Rollback on Error

    If any operation fails, all changes are rolled back:

    hashtag
    Handle Transaction Errors

    hashtag
    Transfer Between Records

    Classic use case - moving money between accounts:

    hashtag
    Create Related Records

    hashtag
    Nested Operations

    All nested operations are part of the same transaction:

    hashtag
    Manual Rollback

    Raise an exception to trigger rollback:

    hashtag
    Transaction with Return Value

    Return a value from the transaction:

    hashtag
    Cleanup Operations

    Ensure cleanup happens even on failure:

    hashtag
    Verify Transaction Behavior

    hashtag
    Related

    • Create Records

    • Update Records

    • Add Optimistic Locking

    Database connection working

    hashtag
    Basic Pool Configuration

    Configure pool settings when defining your schema:

    hashtag
    Pool Settings

    hashtag
    pool_size

    Maximum number of connections in the pool:

    Guidelines:

    • Web apps: 2-3x your web server threads

    • Background jobs: Match worker count

    • Maximum: Check database limits

    hashtag
    checkout_timeout

    How long to wait for an available connection:

    If timeout is reached, raises CQL::ConnectionTimeout.

    hashtag
    retry_attempts

    Number of times to retry failed connections:

    hashtag
    Environment-Specific Configuration

    hashtag
    Monitoring Pool Health

    hashtag
    Verify Configuration

    Test your pool under load:

    hashtag
    Common Issues

    Pool exhaustion:

    • Increase pool_size

    • Reduce connection hold time

    • Use transactions efficiently

    Connection timeouts:

    • Increase checkout_timeout

    • Check network latency

    • Verify database is responsive

    hashtag
    See Also

    • Configure Database Connection

    • Configure Multiple Environments

    • Enable SSL Connections

    Web application with request lifecycle

    hashtag
    The Problem

    Within a single request, the same query may run multiple times:

    hashtag
    Enable Request Cache

    Wrap your request handling with a request cache context:

    hashtag
    How It Works

    With request caching enabled, identical queries are cached:

    hashtag
    Automatic Caching

    All queries within the request cache context are automatically cached:

    hashtag
    Manual Cache Control

    hashtag
    Skip Cache for a Query

    hashtag
    Clear Request Cache

    hashtag
    With Framework Integration

    hashtag
    Kemal

    hashtag
    Lucky

    hashtag
    Amber

    hashtag
    What Gets Cached

    Cached:

    • find queries

    • where queries

    • count queries

    • exists? checks

    Not cached:

    • Write operations (save, update, delete)

    • Raw SQL queries

    • Queries after write operations on the same table

    hashtag
    Debugging

    Log cache hits:

    hashtag
    Verify It Works

    hashtag
    Best Practices

    1. Enable for all requests - Use middleware for consistent behavior

    2. Clear after writes - Ensure fresh data after modifications

    3. Don't rely on it for long-term caching - Use Redis for that

    4. Monitor in development - Log hits/misses to find N+1 queries

    hashtag
    See Also

    • Enable Query Caching

    • Configure Redis Cache

    • Avoid N+1 Queries

    hashtag
    Basic MySQL Connection

    hashtag
    Use Environment Variables

    hashtag
    Connection String Format

    hashtag
    PostgreSQL

    hashtag
    MySQL

    hashtag
    SQLite

    hashtag
    Test Connection

    hashtag
    Connection with Options (PostgreSQL)

    hashtag
    Create Database Directory (SQLite)

    hashtag
    Multiple Databases

    hashtag
    Verify Connection

    hashtag
    Related

    • Configure Multiple Environments

    • Set Up Connection Pooling

    • Enable SSL Connections

    Log or metrics infrastructure

    hashtag
    Enable Query Logging

    Log all queries with execution time:

    hashtag
    Log Slow Queries

    Only log queries exceeding a threshold:

    hashtag
    Track Query Metrics

    Collect metrics for monitoring systems:

    hashtag
    Per-Request Tracking

    Track queries per HTTP request:

    hashtag
    Query Analysis

    Analyze query patterns:

    hashtag
    Database Statistics

    Query database statistics directly:

    hashtag
    Connection Pool Monitoring

    Monitor pool usage:

    hashtag
    Export to Prometheus

    hashtag
    Verify Setup

    Test your monitoring:

    hashtag
    See Also

    • Optimize Queries

    • Avoid N+1 Queries

    • Performance Monitoring Tutorial

    Understanding of your data access patterns

    hashtag
    Add Database Indexes

    Indexes dramatically speed up queries on frequently searched columns.

    hashtag
    Single Column Index

    hashtag
    Composite Index

    For queries filtering on multiple columns:

    hashtag
    Unique Index

    Enforce uniqueness and speed up lookups:

    hashtag
    Use Select to Limit Columns

    Only fetch columns you need:

    hashtag
    Use Pluck for Single Values

    When you only need specific columns without model objects:

    hashtag
    Limit Result Sets

    Always paginate or limit large queries:

    hashtag
    Use Count Instead of Size

    For counting records without loading them:

    hashtag
    Batch Processing

    Process large datasets in batches:

    hashtag
    Use Exists Instead of Count

    When checking for presence:

    hashtag
    Optimize Ordering

    Ensure ordered columns are indexed:

    hashtag
    Analyze Slow Queries

    Log queries to find bottlenecks:

    hashtag
    Verify Optimization

    Check query execution plans:

    Look for:

    • Index scans (good) vs sequential scans (bad)

    • Low row estimates

    • Efficient join methods

    hashtag
    See Also

    • Avoid N+1 Queries

    • Create Indexes

    • Monitor Performance

    Crystal spec testing framework
  • Test database configured

  • hashtag
    Basic Model Tests

    hashtag
    Testing Persistence

    hashtag
    Testing Callbacks

    hashtag
    Testing Relationships

    hashtag
    Testing Scopes

    hashtag
    Test Helpers

    Create helpers for common operations:

    hashtag
    Factory Pattern

    hashtag
    Database Cleanup

    Clean up between tests:

    hashtag
    Running Tests

    hashtag
    See Also

    • Set Up Test Databases

    • Add Validations

    • Use Callbacks

    Database server running

  • Test framework configured

  • hashtag
    Create Test Database

    hashtag
    PostgreSQL

    hashtag
    MySQL

    hashtag
    SQLite

    SQLite creates the file automatically; use a separate path.

    hashtag
    Configure Test Environment

    Create a test configuration:

    hashtag
    Environment Variables

    Set test database URL:

    Or in your test helper:

    hashtag
    Run Migrations for Tests

    Before running tests, migrate the test database:

    Or with a script:

    hashtag
    Database Cleaning Strategies

    hashtag
    Truncation (Recommended)

    Fast cleanup by truncating tables:

    hashtag
    Transaction Rollback

    Wrap each test in a transaction that rolls back:

    Note: This won't work if your tests use multiple connections or spawn fibers.

    hashtag
    Delete Strategy

    Slower but compatible with all scenarios:

    hashtag
    In-Memory SQLite

    For fastest tests, use in-memory SQLite:

    hashtag
    Separate Test Schema

    Use a dedicated schema module for tests:

    hashtag
    Parallel Tests

    For parallel test execution, use separate databases:

    hashtag
    Verify Setup

    hashtag
    See Also

    • Test Models

    • Configure Multiple Environments

    • Run Migrations

  • Check migration status:

    1. Mark migration as applied (if table exists and is correct):

    1. Drop and recreate (development only):

    hashtag
    Common Error: Table Does Not Exist

    Error:

    Cause: Trying to alter or reference a table that wasn't created.

    Solutions:

    1. Run migrations in order:

    1. Check migration order - ensure create comes before alter

    hashtag
    Common Error: Column Already Exists

    Error:

    Solutions:

    1. Check if column exists before adding:

    1. Remove the column first if replacing:

    hashtag
    Common Error: Cannot Drop Column

    Error:

    Cause: Foreign key or index depends on the column.

    Solutions:

    1. Drop dependencies first:

    hashtag
    Common Error: Null Constraint Violation

    Error:

    Cause: Adding NOT NULL column to table with existing data.

    Solutions:

    1. Add with default value:

    1. Add as nullable, update, then add constraint:

    hashtag
    Common Error: Foreign Key Violation

    Error:

    Cause: Referenced record doesn't exist.

    Solutions:

    1. Ensure parent records exist first

    2. Set foreign key to nullable:

    1. Use ON DELETE CASCADE or SET NULL:

    hashtag
    Debugging Migrations

    hashtag
    Reset Database (Development Only)

    hashtag
    Stuck Migration

    If a migration failed halfway:

    1. Check what was created:

    1. Manually fix state:

    1. Drop partially created objects:

    1. Re-run migration:

    hashtag
    Related

    • Create a Migration

    • Run Migrations

    • Rollback Migrations

    hashtag
    The Problem

    With 100 posts, this runs 101 queries.

    hashtag
    Use Eager Loading

    Load relationships upfront with batch queries using preload.

    hashtag
    Basic Preload

    hashtag
    Multiple Relationships

    hashtag
    Use Joins for Filtering

    When filtering by associated records:

    hashtag
    Select Only Needed Columns

    Reduce memory by selecting specific columns:

    hashtag
    Batch Loading

    For custom relationships or complex scenarios:

    hashtag
    Detecting N+1 Queries

    Enable query logging during development:

    Look for repeated similar queries:

    hashtag
    Common N+1 Scenarios

    hashtag
    In Views/Templates

    hashtag
    Manual Counter Columns

    For frequently accessed counts, add a counter column and maintain it manually:

    hashtag
    Aggregations

    hashtag
    Verify Fix

    Before:

    After:

    hashtag
    See Also

    • Optimize Queries

    • Monitor Performance

    • Set Up Has Many

    A User belongs to many Groups, and a Group has many Users

  • A Product is in many Categories, and a Category has many Products

  • hashtag
    Schema Setup

    Create three tables: two main tables and a join table.

    hashtag
    Define the Models

    hashtag
    Post Model

    hashtag
    Tag Model

    hashtag
    Join Table Model

    hashtag
    Create Relationships

    hashtag
    Add Tags to a Post

    hashtag
    Create Join Records Directly

    hashtag
    Query Through Join Table

    hashtag
    Get Tags for a Post

    hashtag
    Get Posts for a Tag

    hashtag
    Find Posts with Specific Tag

    hashtag
    Remove Relationships

    hashtag
    Remove a Tag from Post

    hashtag
    Remove All Tags from Post

    hashtag
    Verify It Works

    hashtag
    Related

    • Set Up Has Many

    • Set Up Belongs To

    • Use Transactions

    hashtag
    Find by Attribute

    hashtag
    Find by Multiple Attributes

    hashtag
    Find First Record

    hashtag
    Find Last Record

    hashtag
    Find All Records

    hashtag
    Find with Order

    hashtag
    Check if Record Exists

    hashtag
    Find or Initialize

    hashtag
    Safe Navigation

    Use try for safe access:

    hashtag
    Verify Find Works

    hashtag
    Related

    • Filter with Where Clauses

    • Build Complex Queries

    • Paginate Results

    Raised when model validations fail.

    Common causes:

    • Required field is empty

    • Value doesn't match format

    • Value outside allowed range

    Solution: Check model.errors for specific field errors.

    hashtag
    Locking Errors

    hashtag
    OptimisticLockError

    Raised when a concurrent update is detected.

    Common causes:

    • Another process updated the record

    • Version column mismatch

    Solution: Reload the record and retry the operation.

    hashtag
    Record Errors

    hashtag
    RecordNotFound

    Raised when find! cannot locate a record.

    Solution: Use find (returns nil) instead, or handle the exception.

    hashtag
    RecordInvalid

    Raised when save! or create! fails validation.

    Solution: Check validations before saving, or use non-bang methods.

    hashtag
    Database Errors

    hashtag
    ConnectionError

    Raised when database connection fails.

    Common causes:

    • Database server not running

    • Wrong connection string

    • Network issues

    • Authentication failure

    Solution: Verify connection settings and database availability.

    hashtag
    QueryError

    Raised when a SQL query fails.

    Common causes:

    • Syntax error in raw SQL

    • Reference to non-existent column

    • Type mismatch

    Solution: Check the SQL being generated and table structure.

    hashtag
    Migration Errors

    hashtag
    MigrationError

    Raised when a migration fails.

    Common causes:

    • Table already exists

    • Column already exists

    • Foreign key constraint violation

    • Invalid SQL syntax

    Solution: Check migration status and database state.

    hashtag
    IrreversibleMigration

    Raised when attempting to rollback an irreversible migration.

    Solution: Manually handle the rollback or skip it.

    hashtag
    Schema Errors

    hashtag
    TableNotFound

    Raised when referencing a table that doesn't exist.

    Solution: Ensure migrations have run and table exists.

    hashtag
    ColumnNotFound

    Raised when referencing a column that doesn't exist.

    Solution: Check column name spelling and run migrations.

    hashtag
    Configuration Errors

    hashtag
    ConfigurationError

    Raised for invalid configuration settings.

    Common causes:

    • Missing required configuration

    • Invalid adapter name

    • Malformed connection string

    Solution: Review configuration settings.

    hashtag
    Debugging Tips

    hashtag
    Enable Logging

    hashtag
    Print Error Details

    hashtag
    Check Validation Errors

    hashtag
    AND

    hashtag
    OR

    hashtag
    Mixed

    hashtag
    Subqueries

    hashtag
    Aggregations

    hashtag
    Count

    hashtag
    Sum

    hashtag
    Average

    hashtag
    Min/Max

    hashtag
    Grouping Results

    hashtag
    Select Specific Columns

    hashtag
    Order by Multiple Columns

    hashtag
    Offset for Pagination

    hashtag
    Distinct Results

    hashtag
    Query Related Records

    hashtag
    Complex Example

    hashtag
    Raw SQL (when needed)

    For very complex queries:

    hashtag
    Verify Query Works

    hashtag
    Related

    • Filter with Where Clauses

    • Create Query Scopes

    • Paginate Results

  • Translating method calls into SQL queries

  • Mapping database columns to object properties

  • Type conversions between database and Crystal types

  • Connection management

  • hashtag
    Key Concepts

    hashtag
    Models

    A model is a Crystal struct or class that represents a database table. Each instance of the model represents a row in that table.

    hashtag
    Queries

    The ORM provides methods that translate to SQL:

    ORM Method
    SQL Equivalent

    User.find(1)

    SELECT * FROM users WHERE id = 1

    User.where(active: true)

    SELECT * FROM users WHERE active = true

    User.count

    SELECT COUNT(*) FROM users

    user.save

    INSERT INTO users ... or UPDATE users ...

    hashtag
    Relationships

    ORMs express table relationships as object associations:

    hashtag
    Benefits of Using an ORM

    hashtag
    1. Type Safety

    CQL leverages Crystal's type system to catch errors at compile time:

    hashtag
    2. Security

    ORMs automatically parameterize queries, preventing SQL injection:

    hashtag
    3. Productivity

    Write less code and focus on business logic:

    hashtag
    4. Database Portability

    The same model code works with different databases:

    hashtag
    5. Maintainability

    Changes to your data model are centralized:

    hashtag
    When NOT to Use an ORM

    ORMs aren't always the best choice:

    • Complex analytical queries: Raw SQL may be clearer

    • Performance-critical operations: Direct SQL might be faster

    • Bulk operations: ORM overhead can add up

    • Database-specific features: Some features don't translate

    CQL lets you drop to raw SQL when needed:

    hashtag
    ORM Patterns

    hashtag
    Active Record

    CQL primarily uses the Active Record pattern, where:

    • Objects carry both data and behavior

    • Models know how to persist themselves

    • Business logic lives in the model

    hashtag
    Other Patterns

    CQL also supports:

    • Repository Pattern: Separate persistence from domain objects

    • Data Mapper Pattern: Decouple domain and database layers

    See Design Patterns for more details.

    hashtag
    Summary

    An ORM like CQL:

    • Maps database tables to Crystal objects

    • Translates method calls to SQL

    • Provides type safety and security

    • Increases productivity

    • Makes code more maintainable

    While ORMs have overhead, they're excellent for most application development tasks, especially CRUD operations and working with relationships.

    Report bugs or request features on the CQL GitHub repositoryarrow-up-right.

    When reporting issues, please include:

    • CQL version

    • Crystal version

    • Database type and version

    • Minimal code to reproduce the issue

    • Error messages and stack traces

    hashtag
    GitHub Discussions

    For questions and discussions, use GitHub Discussionsarrow-up-right.

    hashtag
    Contributing

    hashtag
    How to Contribute

    1. Fork the repository

    2. Create a feature branch

    3. Make your changes

    4. Write or update tests

    5. Submit a pull request

    hashtag
    Code Style

    Follow Crystal's standard style guide. Run crystal tool format before committing.

    hashtag
    Running Tests

    hashtag
    Documentation

    Documentation improvements are always welcome. The docs are in the /docs directory.

    hashtag
    Resources

    hashtag
    Crystal Language

    • Crystal Languagearrow-up-right - Official Crystal website

    • Crystal API Docsarrow-up-right - Standard library documentation

    • Crystal Forumarrow-up-right - Community forum

    hashtag
    Database Documentation

    • PostgreSQL Documentationarrow-up-right

    • MySQL Documentationarrow-up-right

    • SQLite Documentationarrow-up-right

    hashtag
    Support the Project

    hashtag
    Star on GitHub

    If you find CQL useful, consider starring the repositoryarrow-up-right.

    hashtag
    Spread the Word

    Share your experience with CQL on social media or write blog posts about it.

    hashtag
    Contribute Code

    Bug fixes, new features, and documentation improvements are always welcome.

    hashtag
    Enable Soft Deletes

    Include the SoftDeletable module in your model:

    hashtag
    Soft Delete Records

    hashtag
    Delete a Single Record

    hashtag
    Delete by ID

    hashtag
    Delete by Attributes

    hashtag
    Delete All Records

    hashtag
    Restore Records

    hashtag
    Restore a Single Record

    hashtag
    Restore by ID

    hashtag
    Restore All Deleted Records

    hashtag
    Query Records

    By default, queries exclude soft-deleted records:

    hashtag
    Include Deleted Records

    hashtag
    Query Only Deleted Records

    hashtag
    Permanently Delete

    To actually remove records from the database:

    hashtag
    Add Index for Performance

    Add an index on the deleted_at column:

    hashtag
    Cleanup Old Records

    Create a job to permanently delete old soft-deleted records:

    hashtag
    Cascade Soft Deletes

    Soft delete related records when a parent is deleted:

    hashtag
    Verify It Works

    hashtag
    Related

    • Define a Model

    • Use Callbacks

    • Add Optimistic Locking

    Create with save!

    Use save! to raise an exception on failure:

    hashtag
    Create in One Step

    Use create to instantiate and save:

    hashtag
    Create! in One Step

    Use create! to raise on failure:

    hashtag
    Create Multiple Records

    hashtag
    Create with Relationships

    hashtag
    Create with Default Values

    Set defaults in your model:

    hashtag
    Create with Timestamp

    Timestamps are set automatically if configured:

    hashtag
    Handle Validation Errors

    hashtag
    Verify Creation

    hashtag
    Related

    • Update Records

    • Delete Records

    • Use Transactions

    hashtag
    Chain Where Clauses

    hashtag
    Comparison Operators

    Use blocks for comparisons:

    hashtag
    Range Queries

    hashtag
    Date Comparisons

    hashtag
    NULL Checks

    hashtag
    LIKE Queries

    hashtag
    OR Conditions

    hashtag
    AND Conditions

    hashtag
    Combine Hash and Block

    hashtag
    Order Results

    hashtag
    Limit Results

    hashtag
    Count Results

    hashtag
    Verify Filtering Works

    hashtag
    Related

    • Find Records

    • Build Complex Queries

    • Create Query Scopes

    hashtag
    Check Migration Status

    hashtag
    Run to Specific Version

    hashtag
    Create Migration Script

    hashtag
    Run From Command Line

    hashtag
    Auto-Run on Startup

    Run migrations when your app starts:

    hashtag
    Verify Migrations Ran

    hashtag
    Related

    • Create a Migration

    • Rollback Migrations

    • Migration DSL Reference

    hashtag
    Add Column with Default

    hashtag
    Add Column with Index

    hashtag
    Add Foreign Key Column

    hashtag
    Add Timestamp Columns

    hashtag
    Add Column for Soft Deletes

    hashtag
    Add Version Column for Locking

    hashtag
    Update Model After Migration

    After running the migration, update your model:

    hashtag
    Verify Column Added

    hashtag
    Related

    • Create a Migration

    • Create Indexes

    • Migration DSL Reference

    Understanding of your query patterns

    hashtag
    Basic Query Caching

    Enable caching for specific queries:

    hashtag
    Configure Default Cache

    Set up a cache store in your schema:

    hashtag
    Cache Stores

    hashtag
    Memory Cache

    Simple in-memory cache (per-process):

    hashtag
    Redis Cache

    Shared cache across processes:

    See Configure Redis Cache for details.

    hashtag
    Cache Keys

    CQL generates cache keys from the query:

    hashtag
    Custom Cache Keys

    Override the default key:

    hashtag
    Cache Invalidation

    hashtag
    Manual Invalidation

    hashtag
    Automatic Invalidation

    Invalidate on model changes:

    hashtag
    Conditional Caching

    Cache based on conditions:

    hashtag
    Cache Statistics

    Monitor cache performance:

    hashtag
    Verify Caching

    hashtag
    Best Practices

    1. Cache read-heavy queries - Queries run many times with same parameters

    2. Short TTLs for volatile data - Data that changes frequently

    3. Long TTLs for static data - Reference tables, configurations

    4. Invalidate on writes - Clear cache when data changes

    hashtag
    See Also

    • Configure Redis Cache

    • Use Per-Request Caching

    • Optimize Queries

    Environment-Based Configuration

    hashtag
    Separate Database Per Environment

    hashtag
    Using Configuration Files

    hashtag
    Set Environment

    hashtag
    Environment-Specific Features

    hashtag
    Verify Environment

    hashtag
    Related

    • Configure Database Connection

    • Set Up Connection Pooling

    • Enable SSL Connections

    hashtag
    Rollback to Specific Version

    hashtag
    Rollback All Migrations

    hashtag
    Check Before Rollback

    hashtag
    Safe Rollback Script

    hashtag
    Handle Irreversible Migrations

    Some migrations can't be rolled back:

    When you try to rollback:

    hashtag
    Refresh Database

    Rollback all and re-migrate (development only):

    hashtag
    Verify Rollback

    hashtag
    Related

    • Create a Migration

    • Run Migrations

    • Migration DSL Reference

    Update with save!

    Raise on failure:

    hashtag
    Update with update!

    Update multiple attributes at once:

    hashtag
    Bulk Update

    Update multiple records matching a condition:

    hashtag
    Increment a Value

    hashtag
    Conditional Update

    hashtag
    Update Only Changed Fields

    CQL tracks changes automatically:

    hashtag
    Update with Validation

    Validations run on update:

    hashtag
    Update Timestamps

    If your model has timestamps, updated_at is set automatically:

    hashtag
    Update Without Callbacks

    To skip callbacks (use carefully):

    hashtag
    Verify Update

    hashtag
    Related

    • Create Records

    • Delete Records

    • Add Validations

    Database server with SSL enabled
  • SSL certificates (if using certificate verification)

  • hashtag
    PostgreSQL SSL

    hashtag
    Basic SSL

    Enable SSL in your connection URL:

    hashtag
    SSL Modes

    Mode
    Description

    disable

    No SSL

    allow

    Try non-SSL first, then SSL

    prefer

    Try SSL first, then non-SSL

    require

    SSL required, no certificate verification

    verify-ca

    SSL required, verify CA

    verify-full

    SSL required, verify CA and hostname

    hashtag
    With Certificate Verification

    hashtag
    MySQL SSL

    hashtag
    Environment Variables

    Store certificates securely:

    hashtag
    Verify SSL Connection

    Test that SSL is working:

    hashtag
    Cloud Database SSL

    hashtag
    AWS RDS

    hashtag
    Heroku

    Heroku manages SSL automatically:

    hashtag
    Troubleshooting

    Certificate verification failed:

    • Check certificate paths are correct

    • Verify CA certificate matches server

    • Check certificate expiration

    Connection refused:

    • Verify server has SSL enabled

    • Check firewall allows SSL port

    • Try sslmode=require first

    hashtag
    See Also

    • Configure Database Connection

    • Configure Multiple Environments

    • Fix Connection Errors

    Redis server running

  • crystal-redis shard installed

  • hashtag
    Install Redis Shard

    Add to your shard.yml:

    Run shards install.

    hashtag
    Basic Configuration

    hashtag
    Connection Options

    hashtag
    Using Redis URL

    hashtag
    Key Prefixing

    Namespace your cache keys:

    All keys will be prefixed: myapp:cache:users:1

    hashtag
    TTL Configuration

    hashtag
    Connection Pooling

    For high-traffic apps:

    hashtag
    Cluster Support

    For Redis Cluster:

    hashtag
    Cache Operations

    hashtag
    Monitoring

    Check Redis cache stats:

    Direct Redis monitoring:

    hashtag
    Verify Configuration

    hashtag
    Troubleshooting

    Connection refused:

    • Verify Redis is running: redis-cli ping

    • Check host/port configuration

    • Check firewall rules

    Authentication failed:

    • Verify password is correct

    • Check Redis requirepass configuration

    Memory issues:

    • Set maxmemory in Redis config

    • Use maxmemory-policy volatile-lru

    hashtag
    See Also

    • Enable Query Caching

    • Use Per-Request Caching

    • Configure Database Connection

    Error:

    Cause: Field is empty or nil.

    Solutions:

    1. Provide a value:

    1. If field should be optional, adjust validation:

    hashtag
    Common Error: Format Validation Failed

    Error:

    Cause: Value doesn't match the required format.

    Solutions:

    1. Fix the value:

    1. Check the regex pattern:

    hashtag
    Common Error: Size Validation Failed

    Error:

    Solutions:

    1. Provide longer value:

    1. Adjust size requirement if too strict:

    hashtag
    Common Error: Uniqueness Validation Failed

    Error:

    Cause: Another record has the same value.

    Solutions:

    1. Use a different value:

    1. Find and handle the existing record:

    hashtag
    Common Error: Numeric Validation Failed

    Error:

    Solutions:

    1. Provide valid number:

    1. Check validation constraints:

    hashtag
    Debugging Validation

    Print all errors with details:

    hashtag
    Skip Validation (Use Carefully)

    Sometimes you need to bypass validation:

    hashtag
    Test Validations

    hashtag
    Handle Validation in Controllers

    hashtag
    Custom Error Messages

    Make errors user-friendly:

    hashtag
    Related

    • Add Validations

    • Validation Options Reference

    • Create Records

    hashtag
    CRUD Operations

    hashtag
    Querying

    hashtag
    Relationships

    hashtag
    Validations

    hashtag
    Callbacks

    hashtag
    Migrations

    hashtag
    Run Migrations

    hashtag
    Transactions

    hashtag
    Soft Deletes

    hashtag
    Optimistic Locking

    hashtag
    Connection Strings

    hashtag
    presence

    Validates that a field is not nil and not empty.

    Error: "field_name is required"

    hashtag
    format / match

    Validates that a field matches a regular expression.

    Error: "field_name format is invalid"

    hashtag
    size

    Validates the length of a string or size of a collection.

    Error: "field_name must be between X and Y characters"

    hashtag
    gt / gte / lt / lte

    Validates numeric values are within bounds.

    Error: "field_name must be greater than X" / "field_name must be less than or equal to X"

    hashtag
    in

    Validates that a value is in a list of allowed values.

    Error: "field_name must be one of: X, Y, Z"

    hashtag
    exclude

    Validates that a value is not in a list of excluded values.

    Error: "field_name must not be included in [X, Y, Z]"

    hashtag
    Combining Validations

    Multiple validations can be applied to a single field:

    hashtag
    Model Example

    hashtag
    Checking Validation

    hashtag
    Custom Validations

    For complex validations, use callbacks:

    hashtag
    See Also

    • Add Validations

    • Fix Validation Errors

    • Callback Hooks Reference

    Available Callbacks

    hashtag
    before_save

    Runs before any save operation (create or update).

    Use cases:

    • Data normalization

    • Setting computed fields

    • Validation that requires database access

    hashtag
    after_save

    Runs after any successful save operation.

    Use cases:

    • Triggering side effects

    • Updating related records

    • Sending notifications

    • Cache invalidation

    hashtag
    before_create

    Runs only before INSERT operations (new records).

    Use cases:

    • Setting default values

    • Generating tokens or UUIDs

    • Recording creation metadata

    hashtag
    after_create

    Runs only after successful INSERT operations.

    Use cases:

    • Sending welcome emails

    • Creating related records

    • Notifying other systems

    hashtag
    before_update

    Runs only before UPDATE operations (existing records).

    Use cases:

    • Tracking changes

    • Validating state transitions

    • Incrementing version numbers

    hashtag
    after_update

    Runs only after successful UPDATE operations.

    Use cases:

    • Change notifications

    • Audit logging

    • Syncing with external systems

    hashtag
    before_destroy

    Runs before DELETE operations.

    Use cases:

    • Validation before deletion

    • Checking dependencies

    • Preventing deletion of protected records

    hashtag
    after_destroy

    Runs after successful DELETE operations.

    Use cases:

    • Cleaning up files

    • Removing from search indexes

    • Cascade deletes not handled by database

    hashtag
    Return Values

    Important: Callbacks must return true to continue the operation.

    hashtag
    Callback Order

    When saving a new record:

    1. before_save

    2. before_create

    3. (database INSERT)

    4. after_create

    5. after_save

    When updating an existing record:

    1. before_save

    2. before_update

    3. (database UPDATE)

    4. after_update

    5. after_save

    When deleting:

    1. before_destroy

    2. (database DELETE)

    3. after_destroy

    hashtag
    Complete Example

    hashtag
    See Also

    • Use Callbacks

    • Define a Model

    • Add Validations

    hashtag
    What databases does CQL support?

    CQL supports:

    • PostgreSQL

    • MySQL

    • SQLite

    hashtag
    Is CQL production-ready?

    CQL is actively maintained and used in production applications. However, as with any library, thoroughly test your specific use case.

    hashtag
    Models

    hashtag
    Should I use class or struct for models?

    Both work with CQL. Use struct for value semantics (recommended for most cases) or class if you need reference semantics.

    hashtag
    Why is my primary key nullable?

    Primary keys should be Type? (nullable) because new records don't have an ID until saved:

    hashtag
    How do I add a field that isn't in the database?

    Use the DB::Field(ignore: true) annotation:

    hashtag
    Queries

    hashtag
    How do I write raw SQL?

    hashtag
    Why is my query returning an empty array?

    Check:

    1. Data exists in the database

    2. Your where conditions are correct

    3. If using soft deletes, records might be deleted

    hashtag
    How do I debug what SQL is being generated?

    Enable query logging:

    hashtag
    Relationships

    hashtag
    When do I use belongs_to vs has_many?

    • belongs_to: The model has a foreign key column (e.g., Comment has post_id)

    • has_many: The other model has the foreign key (e.g., Post has many Comments)

    hashtag
    How do I eager load associations?

    Load related IDs first, then batch load:

    hashtag
    Migrations

    hashtag
    How do I add a column to an existing table?

    hashtag
    How do I rollback a migration?

    hashtag
    What if a migration fails halfway through?

    Manually check the database state, fix any issues, and potentially mark the migration as not applied:

    hashtag
    Performance

    hashtag
    How do I avoid N+1 queries?

    Load related data in batches:

    hashtag
    Should I add indexes?

    Add indexes on:

    • Foreign key columns

    • Columns used in WHERE clauses

    • Columns used in ORDER BY

    hashtag
    Troubleshooting

    hashtag
    "Connection refused" error

    The database server isn't running or the connection string is wrong. Check:

    1. Database is running: pg_isready or mysqladmin ping

    2. Connection string is correct

    hashtag
    "Table does not exist" error

    Migrations haven't run. Execute:

    hashtag
    "Validation failed" but I don't know why

    Check the errors array:

    hashtag
    Best Practices

    hashtag
    Should I use save or save!?

    • Use save when you want to handle failure gracefully

    • Use save! when failure should raise an exception

    hashtag
    When should I use transactions?

    When multiple operations must succeed or fail together:

    hashtag
    How do I test my models?

    Use a test database and reset it between tests:

    Domain objects (entities) hold data and business logic
  • Repositories handle all database operations

  • The domain layer doesn't know about persistence

  • hashtag
    Why Use Repository Pattern?

    hashtag
    Separation of Concerns

    Domain objects focus on business logic, not database operations:

    hashtag
    Testability

    Easily mock repositories for testing:

    hashtag
    Flexibility

    Switch data sources without changing domain logic:

    hashtag
    Implementing Repository with CQL

    hashtag
    Step 1: Define Your Entity

    hashtag
    Step 2: Create a CQL Model (Internal)

    hashtag
    Step 3: Implement the Repository

    hashtag
    Step 4: Use the Repository

    hashtag
    When to Use Repository Pattern

    hashtag
    Good Use Cases

    1. Complex domain logic - Keep business rules separate from persistence

    2. Multiple data sources - Same domain, different storage

    3. Heavy testing - Easy to mock repositories

    4. Large teams - Clear boundaries between concerns

    hashtag
    Not Ideal For

    1. Simple CRUD apps - Overkill for basic operations

    2. Rapid prototyping - Slows initial development

    3. Small projects - Adds unnecessary complexity

    hashtag
    Comparison: Active Record vs Repository

    The difference is subtle in simple cases but significant for complex domains.

    hashtag
    Hybrid Approach

    You can mix both patterns:

    This gives you the best of both worlds: simplicity for CRUD and separation for complex logic.

    hashtag
    Create a Composite Index

    Index multiple columns together:

    hashtag
    Index on Foreign Keys

    Always index foreign key columns:

    hashtag
    Index for Frequently Filtered Columns

    hashtag
    Index for Soft Deletes

    hashtag
    Unique Composite Index

    Prevent duplicate combinations:

    hashtag
    Index Naming Convention

    Use consistent naming:

    hashtag
    When to Create Indexes

    Create indexes for:

    • Primary keys (automatic)

    • Foreign keys

    • Columns used in WHERE clauses

    • Columns used in ORDER BY

    • Columns used in JOIN conditions

    • Columns with unique constraints

    hashtag
    When NOT to Create Indexes

    Avoid unnecessary indexes on:

    • Small tables (< 1000 rows)

    • Columns with low cardinality (few unique values)

    • Tables with frequent writes (indexes slow down inserts)

    • Columns rarely used in queries

    hashtag
    Verify Index Works

    hashtag
    Related

    • Create a Migration

    • Add Columns

    • Optimize Queries

    include CQL::ActiveRecord::Model(Int64)
    db_context MyDB, :users
    property id : Int64?          # Primary key (nullable for new records)
    property name : String        # Required field
    property active : Bool = false  # With default value
    property bio : String?        # Nullable field
    property created_at : Time?   # Timestamp
    property updated_at : Time?   # Timestamp
    def initialize(@name : String, @email : String)
    end
    struct Product
      include CQL::ActiveRecord::Model(Int64)
      db_context StoreDB, :products
    
      property id : Int64?
      # ...
    end
    struct Session
      include CQL::ActiveRecord::Model(UUID)
      db_context MyDB, :sessions
    
      property id : UUID?
      # ...
    end
    struct Event
      include CQL::ActiveRecord::Model(String)
      db_context EventDB, :events
    
      property id : String?
      # ...
    end
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property password : String?
    
      @[DB::Field(ignore: true)]
      property password_confirmation : String?
    
      def initialize(@name : String, @password : String? = nil, @password_confirmation : String? = nil)
      end
    end
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property first_name : String
      property last_name : String
      property email : String
    
      def initialize(@first_name : String, @last_name : String, @email : String)
      end
    
      def full_name : String
        "#{first_name} #{last_name}"
      end
    
      def email_domain : String
        email.split("@").last
      end
    end
    # Create an instance
    user = User.new("John", "john@example.com")
    
    # Check attributes
    user.name      # => "John"
    user.id        # => nil (not saved yet)
    
    # Save to database
    user.save
    user.id        # => 1 (now assigned)
    
    # Find it again
    found = User.find(user.id)
    New tables needed:
    - tags (id, name, slug)
    - post_tags (post_id, tag_id) - join table
    
    New indexes:
    - Unique index on tags.slug
    - Composite index on post_tags
    # migrations/005_create_tags.cr
    class CreateTags < CQL::Migration(5)
      def up
        schema.table :tags do
          primary :id, Int64, auto_increment: true
          column :name, String, null: false
          column :slug, String, null: false
          timestamps
    
          index [:slug], unique: true
        end
    
        schema.tags.create!
      end
    
      def down
        schema.tags.drop!
      end
    end
    # migrations/006_create_post_tags.cr
    class CreatePostTags < CQL::Migration(6)
      def up
        schema.table :post_tags do
          column :post_id, Int64, null: false
          column :tag_id, Int64, null: false
          column :created_at, Time
    
          foreign_key [:post_id], references: :posts, references_columns: [:id], on_delete: :cascade
          foreign_key [:tag_id], references: :tags, references_columns: [:id], on_delete: :cascade
          index [:post_id]
          index [:tag_id]
          index [:post_id, :tag_id], unique: true
        end
    
        schema.post_tags.create!
      end
    
      def down
        schema.post_tags.drop!
      end
    end
    # Run migrations
    crystal src/migrate.cr up
    
    # Verify
    crystal src/migrate.cr status
    struct Tag
      include CQL::ActiveRecord::Model(Int64)
      db_context AppDB, :tags
    
      property id : Int64?
      property name : String
      property slug : String
      property created_at : Time?
      property updated_at : Time?
    
      has_many :post_tags, PostTag, :tag_id
    
      def initialize(@name : String)
        @slug = name.downcase.gsub(/[^a-z0-9]+/, "-").strip("-")
      end
    end
    
    struct PostTag
      include CQL::ActiveRecord::Model(Int64)
      db_context AppDB, :post_tags
    
      property post_id : Int64
      property tag_id : Int64
      property created_at : Time?
    
      belongs_to :post, Post, :post_id
      belongs_to :tag, Tag, :tag_id
    
      def initialize(@post_id : Int64, @tag_id : Int64)
      end
    end
    # migrations/007_add_featured_to_posts.cr
    class AddFeaturedToPosts < CQL::Migration(7)
      def up
        schema.alter :posts do
          add_column :featured, Bool, default: false
          add_column :featured_at, Time, null: true
        end
    
        schema.alter :posts do
          create_index :idx_posts_featured, [:featured]
        end
      end
    
      def down
        schema.alter :posts do
          drop_index :idx_posts_featured
          drop_column :featured_at
          drop_column :featured
        end
      end
    end
    # migrations/008_normalize_user_emails.cr
    class NormalizeUserEmails < CQL::Migration(8)
      def up
        # Add temporary column
        schema.alter :users do
          add_column :email_normalized, String, null: true
        end
    
        # Migrate data (done in batches for large tables)
        schema.exec("UPDATE users SET email_normalized = LOWER(TRIM(email))")
    
        # Verify
        count = schema.exec("SELECT COUNT(*) FROM users WHERE email_normalized IS NULL").first
        raise "Data migration incomplete" if count > 0
    
        # Swap columns
        schema.alter :users do
          drop_column :email
        end
    
        schema.exec("ALTER TABLE users RENAME COLUMN email_normalized TO email")
    
        # Recreate index
        schema.alter :users do
          create_index :idx_users_email, [:email], unique: true
        end
      end
    
      def down
        # This migration cannot be easily reversed
        raise "Cannot rollback email normalization"
      end
    end
    def up
      batch_size = 1000
      offset = 0
    
      loop do
        result = schema.exec(<<-SQL
          UPDATE users
          SET email_normalized = LOWER(TRIM(email))
          WHERE id IN (
            SELECT id FROM users
            WHERE email_normalized IS NULL
            LIMIT #{batch_size}
          )
        SQL
        )
    
        break if result.rows_affected == 0
        offset += batch_size
    
        # Optional: Add a small delay to reduce database load
        sleep 0.1
      end
    end
    # Rollback last migration
    crystal src/migrate.cr down
    
    # Rollback to specific version
    crystal src/migrate.cr down_to 5
    def down
      raise "Cannot rollback: #{self.class.name} - data would be lost"
    end
    # scripts/deploy_migrations.cr
    require "../src/cql_config"
    require "../migrations/*"
    
    puts "Starting migration deployment..."
    puts "================================"
    
    AppDB.init
    
    migrator = AppDB.migrator(CQL_MIGRATOR_CONFIG)
    
    pending = migrator.pending_migrations.size
    if pending == 0
      puts "No pending migrations"
      exit 0
    end
    
    puts "Pending migrations: #{pending}"
    puts ""
    
    # Run with transaction per migration
    migrator.pending_migrations.each do |version|
      puts "Running migration #{version}..."
    
      begin
        migrator.up_to(version)
        puts "  Success!"
      rescue ex
        puts "  FAILED: #{ex.message}"
        puts ""
        puts "Migration failed. Database may be in inconsistent state."
        puts "Please investigate and fix manually."
        exit 1
      end
    end
    
    puts ""
    puts "All migrations completed successfully!"
    schema.table :posts do
      primary :id, Int64, auto_increment: true
      column :title, String
      timestamps  # Adds created_at and updated_at columns
    end
    schema.posts.create!
    struct Post
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :posts
    
      property id : Int64?
      property title : String
      property created_at : Time?
      property updated_at : Time?
    
      def initialize(@title : String)
      end
    end
    post = Post.new("My Post")
    post.save!
    
    post.created_at  # => 2024-01-15 10:30:00 UTC
    post.updated_at  # => 2024-01-15 10:30:00 UTC
    
    # Later update
    post.title = "Updated Title"
    post.save!
    
    post.created_at  # => 2024-01-15 10:30:00 UTC (unchanged)
    post.updated_at  # => 2024-01-15 11:45:00 UTC (updated)
    struct Post
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :posts
    
      property id : Int64?
      property title : String
      property created_at : Time?
      property updated_at : Time?
    
      before_create :set_created_at
      before_save :set_updated_at
    
      def initialize(@title : String)
      end
    
      private def set_created_at
        @created_at = Time.utc
        true
      end
    
      private def set_updated_at
        @updated_at = Time.utc
        true
      end
    end
    # Records created today
    Post.where { created_at > Time.utc.at_beginning_of_day }.all
    
    # Records updated in last hour
    Post.where { updated_at > 1.hour.ago }.all
    
    # Records created between dates
    Post.where { created_at.between(start_date, end_date) }.all
    
    # Order by most recent
    Post.order(created_at: :desc).all
    class AddTimestampsToPosts < CQL::Migration(20240115)
      def up
        schema.alter :posts do
          add_column :created_at, Time, null: true
          add_column :updated_at, Time, null: true
        end
    
        # Optionally set existing records to current time
        schema.exec("UPDATE posts SET created_at = NOW(), updated_at = NOW() WHERE created_at IS NULL")
      end
    
      def down
        schema.alter :posts do
          drop_column :created_at
          drop_column :updated_at
        end
      end
    end
    struct Post
      # ... properties ...
    
      def created_today? : Bool
        created_at.try(&.to_date) == Time.utc.to_date
      end
    
      def recently_updated? : Bool
        updated_at.try { |t| t > 1.hour.ago } || false
      end
    
      def age : Time::Span?
        created_at.try { |t| Time.utc - t }
      end
    
      def formatted_created_at : String
        created_at.try(&.to_s("%B %d, %Y")) || "Unknown"
      end
    end
    post = Post.create!(title: "Test")
    
    post.created_at.nil?  # => false
    post.updated_at.nil?  # => false
    
    sleep 1
    post.title = "Updated"
    post.save!
    
    post.created_at < post.updated_at  # => true
    schema.table :users do
      primary :id, Int64, auto_increment: true
      column :name, String
      lock_version :version  # Adds integer column with default 1
      timestamps
    end
    schema.users.create!
    class AddVersionToUsers < CQL::Migration(20240115)
      def up
        schema.alter :users do
          add_column :version, Int32, default: 1, null: false
        end
      end
    
      def down
        schema.alter :users do
          drop_column :version
        end
      end
    end
    struct User
      include CQL::ActiveRecord::Model(Int64)
      include CQL::ActiveRecord::OptimisticLocking
    
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property version : Int32?
    
      optimistic_locking version_column: :version
    
      def initialize(@name : String)
      end
    end
    user = User.find!(1)
    user.name = "Updated Name"
    
    begin
      user.update!
      puts "Updated! New version: #{user.version}"
    rescue CQL::OptimisticLockError
      puts "Someone else updated this record!"
    end
    user = User.find!(1)
    user.name = "My Changes"
    
    begin
      user.update!
    rescue CQL::OptimisticLockError
      user.reload!  # Get latest data from database
      user.name = "My Changes"  # Re-apply changes
      user.update!  # Try again
    end
    def update_with_retry(user, max_retries = 3)
      attempts = 0
    
      loop do
        attempts += 1
        user.name = "Updated Name"
    
        begin
          user.update!
          return true
        rescue CQL::OptimisticLockError
          if attempts >= max_retries
            puts "Max retries reached"
            return false
          end
    
          puts "Conflict, retrying (#{attempts}/#{max_retries})..."
          user.reload!
        end
      end
    end
    struct User
      include CQL::ActiveRecord::Model(Int64)
      include CQL::ActiveRecord::SoftDeletable
      include CQL::ActiveRecord::OptimisticLocking
    
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property version : Int32?
    
      optimistic_locking version_column: :version
    
      def initialize(@name : String)
      end
    end
    # Load same record twice (simulating two users)
    user1 = User.find!(1)
    user2 = User.find!(1)
    
    # First update succeeds
    user1.name = "User 1's change"
    user1.update!  # Works, version now 2
    
    # Second update fails
    user2.name = "User 2's change"
    begin
      user2.update!  # Raises OptimisticLockError
    rescue CQL::OptimisticLockError
      puts "Conflict detected!"
    end
    # src/database.cr
    require "log"
    
    # Configure logging
    Log.setup do |c|
      backend = Log::IOBackend.new
      c.bind("cql.*", :debug, backend)
    end
    
    AppDB = CQL::Schema.define(
      :app_db,
      adapter: CQL::Adapter::Postgres,
      uri: DATABASE_URL
    ) do
    end
    # src/monitoring/query_monitor.cr
    module QueryMonitor
      @@queries = [] of QueryRecord
      @@enabled = true
    
      struct QueryRecord
        property sql : String
        property duration_ms : Float64
        property timestamp : Time
        property source : String
    
        def initialize(@sql, @duration_ms, @source = "")
          @timestamp = Time.utc
        end
    
        def slow?(threshold_ms = 100.0)
          duration_ms > threshold_ms
        end
      end
    
      def self.enable
        @@enabled = true
      end
    
      def self.disable
        @@enabled = false
      end
    
      def self.record(sql : String, duration_ms : Float64, source = "")
        return unless @@enabled
        @@queries << QueryRecord.new(sql, duration_ms, source)
      end
    
      def self.queries
        @@queries
      end
    
      def self.clear
        @@queries.clear
      end
    
      def self.slow_queries(threshold_ms = 100.0)
        @@queries.select(&.slow?(threshold_ms))
      end
    
      def self.total_time_ms
        @@queries.sum(&.duration_ms)
      end
    
      def self.report
        puts "Query Performance Report"
        puts "========================"
        puts "Total queries: #{@@queries.size}"
        puts "Total time: #{total_time_ms.round(2)}ms"
        puts ""
    
        if (slow = slow_queries).any?
          puts "Slow queries (>100ms):"
          slow.each do |q|
            puts "  #{q.duration_ms.round(2)}ms - #{q.sql[0, 80]}..."
          end
          puts ""
        end
    
        # Group by similar queries (potential N+1)
        grouped = @@queries.group_by { |q| normalize_query(q.sql) }
        repeated = grouped.select { |_, v| v.size > 1 }
    
        if repeated.any?
          puts "Repeated queries (potential N+1):"
          repeated.each do |pattern, instances|
            puts "  #{instances.size}x - #{pattern[0, 60]}..."
          end
        end
      end
    
      private def self.normalize_query(sql : String)
        # Replace specific values with placeholders
        sql.gsub(/= \d+/, "= ?")
           .gsub(/= '[^']*'/, "= ?")
           .gsub(/IN \([^)]+\)/, "IN (?)")
      end
    end
    # src/monitoring/timed_queries.cr
    module TimedQueries
      def self.measure(source = "")
        start = Time.monotonic
        result = yield
        duration = (Time.monotonic - start).total_milliseconds
        QueryMonitor.record("query", duration, source)
        result
      end
    end
    
    # Usage in your code:
    posts = TimedQueries.measure("load_posts") do
      Post.where(published: true).all
    end
    # BAD: N+1 problem
    posts = Post.where(published: true).all
    posts.each do |post|
      author = post.user  # This triggers a query for EACH post!
      puts "#{post.title} by #{author.try(&.name)}"
    end
    
    # The monitor would show something like:
    # 1x SELECT * FROM posts WHERE published = true
    # 10x SELECT * FROM users WHERE id = ?
    # GOOD: Single query with join
    posts = Post.where(published: true).all
    user_ids = posts.map(&.user_id).uniq
    users_by_id = User.where { id.in(user_ids) }.all.index_by(&.id)
    
    posts.each do |post|
      author = users_by_id[post.user_id]?
      puts "#{post.title} by #{author.try(&.name)}"
    end
    
    # The monitor shows:
    # 1x SELECT * FROM posts WHERE published = true
    # 1x SELECT * FROM users WHERE id IN (?, ?, ...)
    # src/monitoring/dashboard.cr
    require "./query_monitor"
    
    module PerformanceDashboard
      def self.display
        puts ""
        puts "="*60
        puts "CQL Performance Dashboard"
        puts "="*60
        puts ""
    
        display_query_stats
        display_slow_queries
        display_n_plus_one_warnings
        display_recommendations
      end
    
      private def self.display_query_stats
        queries = QueryMonitor.queries
        return puts "No queries recorded" if queries.empty?
    
        puts "Query Statistics"
        puts "-"*40
    
        total = queries.size
        total_time = QueryMonitor.total_time_ms
    
        puts "Total queries: #{total}"
        puts "Total time: #{total_time.round(2)}ms"
        puts "Average time: #{(total_time / total).round(2)}ms"
        puts ""
      end
    
      private def self.display_slow_queries
        slow = QueryMonitor.slow_queries(50.0)
        return if slow.empty?
    
        puts "Slow Queries (>50ms)"
        puts "-"*40
    
        slow.sort_by(&.duration_ms).reverse.first(5).each do |q|
          puts "#{q.duration_ms.round(2)}ms"
          puts "  #{q.sql[0, 70]}..."
          puts ""
        end
      end
    
      private def self.display_n_plus_one_warnings
        grouped = QueryMonitor.queries.group_by { |q|
          q.sql.gsub(/= \d+/, "= ?").gsub(/= '[^']*'/, "= ?")
        }
    
        warnings = grouped.select { |_, v| v.size > 3 }
        return if warnings.empty?
    
        puts "Potential N+1 Queries"
        puts "-"*40
    
        warnings.each do |pattern, instances|
          puts "Executed #{instances.size} times:"
          puts "  #{pattern[0, 60]}..."
          puts ""
        end
      end
    
      private def self.display_recommendations
        queries = QueryMonitor.queries
        return if queries.empty?
    
        puts "Recommendations"
        puts "-"*40
    
        # Check for too many queries
        if queries.size > 50
          puts "- High query count (#{queries.size}). Consider:"
          puts "  * Caching frequently accessed data"
          puts "  * Combining queries with joins"
          puts ""
        end
    
        # Check for slow average
        avg = QueryMonitor.total_time_ms / queries.size
        if avg > 50
          puts "- Slow average query time (#{avg.round(2)}ms). Consider:"
          puts "  * Adding indexes to filtered columns"
          puts "  * Optimizing complex queries"
          puts ""
        end
    
        # Check for N+1
        grouped = queries.group_by { |q| q.sql.gsub(/= \d+/, "= ?") }
        repeated = grouped.count { |_, v| v.size > 3 }
        if repeated > 0
          puts "- Detected #{repeated} potential N+1 patterns. Consider:"
          puts "  * Using eager loading"
          puts "  * Batching related queries"
          puts ""
        end
    
        if queries.size <= 50 && avg <= 50 && repeated == 0
          puts "Performance looks good!"
        end
      end
    end
    # spec/performance_spec.cr
    describe "Performance" do
      before_each do
        QueryMonitor.clear
      end
    
      it "loads post list efficiently" do
        # Create test data
        10.times { create_post }
    
        # Load posts
        QueryMonitor.enable
        posts = Post.where(published: true).all
        posts.each { |p| p.user }
    
        # Assert query count
        QueryMonitor.queries.size.should be <= 2
    
        # Assert no slow queries
        QueryMonitor.slow_queries(100.0).should be_empty
      end
    end
    # src/monitoring/metrics.cr
    module Metrics
      def self.record_query(duration_ms : Float64, query_type : String)
        # Send to your metrics system (StatsD, Prometheus, etc.)
        # Example with a hypothetical metrics client:
        # MetricsClient.timing("cql.query.duration", duration_ms, tags: ["type:#{query_type}"])
      end
    
      def self.increment_query_count(query_type : String)
        # MetricsClient.increment("cql.query.count", tags: ["type:#{query_type}"])
      end
    
      def self.record_slow_query(sql : String, duration_ms : Float64)
        # Log slow queries for investigation
        Log.warn { "Slow query (#{duration_ms.round(2)}ms): #{sql}" }
      end
    end
    # Check if filtering on a column without an index
    Post.where(status: "published").all  # Is status indexed?
    # Instead of loading entire records
    titles = Post.where(published: true).select(:id, :title).all
    # Instead of loading all records
    Post.find_each(batch_size: 100) do |post|
      process(post)
    end
    # Cache results of expensive aggregations
    def total_views
      @total_views ||= Post.where(published: true).sum(:views_count)
    end
    schema.table :users do
      primary :id, Int64, auto_increment: true
      column :name, String
      timestamps
    end
    schema.users.create!
    
    schema.table :posts do
      primary :id, Int64, auto_increment: true
      column :user_id, Int64, null: false
      column :title, String
      column :body, String
      timestamps
    
      foreign_key [:user_id], references: :users, references_columns: [:id]
      index [:user_id]
    end
    schema.posts.create!
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
    
      has_many :posts, Post, :user_id
    
      def initialize(@name : String)
      end
    end
    
    struct Post
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :posts
    
      property id : Int64?
      property user_id : Int64
      property title : String
      property body : String
    
      belongs_to :user, User, :user_id
    
      def initialize(@title : String, @body : String, @user_id : Int64)
      end
    end
    user = User.find(1)
    posts = user.posts.all
    
    posts.each do |post|
      puts post.title
    end
    user = User.find(1)
    count = user.posts.count
    puts "User has #{count} posts"
    user = User.find(1)
    
    # Published posts only
    published = user.posts.where(published: true).all
    
    # Most recent posts
    recent = user.posts.order(created_at: :desc).limit(5).all
    user = User.create!(name: "John")
    
    # Create posts for user
    post1 = Post.create!(
      title: "First Post",
      body: "Hello world",
      user_id: user.id.not_nil!
    )
    
    post2 = Post.create!(
      title: "Second Post",
      body: "Another post",
      user_id: user.id.not_nil!
    )
    
    user.posts.count  # => 2
    user = User.find(1)
    user.posts.where(published: false).each(&.delete!)
    user = User.find(1)
    user.posts.all.each(&.delete!)
    schema.table :posts do
      # ...
      foreign_key [:user_id], references: :users, references_columns: [:id], on_delete: :cascade
    end
    user = User.find(1)
    
    if user.posts.count > 0
      puts "User has posts"
    else
      puts "User has no posts"
    end
    user = User.create!(name: "Test User")
    
    Post.create!(title: "Post 1", body: "Body 1", user_id: user.id.not_nil!)
    Post.create!(title: "Post 2", body: "Body 2", user_id: user.id.not_nil!)
    
    user.posts.count  # => 2
    user.posts.all.map(&.title)  # => ["Post 1", "Post 2"]
    schema.table :posts do
      primary :id, Int64, auto_increment: true
      column :title, String
      timestamps
    end
    schema.posts.create!
    
    schema.table :comments do
      primary :id, Int64, auto_increment: true
      column :body, String
      column :post_id, Int64, null: false
      timestamps
    
      foreign_key [:post_id], references: :posts, references_columns: [:id]
    end
    schema.comments.create!
    struct Comment
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :comments
    
      property id : Int64?
      property body : String
      property post_id : Int64
    
      belongs_to :post, Post, :post_id
    
      def initialize(@body : String, @post_id : Int64)
      end
    end
    comment = Comment.find(1)
    post = comment.post  # Returns Post?
    
    if post
      puts "Comment on: #{post.title}"
    end
    post = Post.create!(title: "My Post")
    
    comment = Comment.new("Great post!", post.id.not_nil!)
    comment.save!
    post = Post.create!(title: "My Post")
    
    comment = Comment.new("Great post!", 0_i64)
    comment.post = post  # Sets post_id automatically
    comment.save!
    comment = Comment.new("My comment", 0_i64)
    post = comment.build_post(title: "New Post")
    
    # Save parent, then child
    post.save!
    comment.post = post
    comment.save!
    comment = Comment.new("My comment", 0_i64)
    post = comment.create_post(title: "New Post")
    
    comment.save!  # post_id is now set
    schema.table :comments do
      # ...
      column :user_id, Int64, null: true  # Optional
    end
    
    struct Comment
      # ...
      property user_id : Int64?
    
      belongs_to :user, User, :user_id
    
      def initialize(@body : String, @post_id : Int64, @user_id : Int64? = nil)
      end
    end
    
    comment = Comment.find(1)
    if user = comment.user
      puts "By: #{user.name}"
    else
      puts "Anonymous"
    end
    post = Post.create!(title: "Test Post")
    comment = Comment.create!(body: "Test Comment", post_id: post.id.not_nil!)
    
    comment.post.try(&.title)  # => "Test Post"
    comment.post_id == post.id  # => true
    def fetch_posts(after : Int64?, limit : Int32 = 20)
      query = Post.published.order(id: :desc).limit(limit + 1)
    
      if after
        query = query.where { id < after }
      end
    
      items = query.all
    
      # Check if there are more items
      has_more = items.size > limit
      items = items.first(limit) if has_more
    
      {
        items: items,
        next_cursor: has_more ? items.last?.try(&.id) : nil,
        has_more: has_more
      }
    end
    # First page
    result = fetch_posts(nil)
    result[:items].each { |post| puts post.title }
    
    # Next page
    if cursor = result[:next_cursor]
      result = fetch_posts(cursor)
    end
    def fetch_posts(after : Int64? = nil, before : Int64? = nil, limit : Int32 = 20)
      query = Post.published.limit(limit + 1)
    
      if after
        items = query.where { id < after }.order(id: :desc).all
        {
          items: items.first(limit),
          next_cursor: items.size > limit ? items[limit - 1].id : nil,
          prev_cursor: after,
          has_more: items.size > limit
        }
      elsif before
        items = query.where { id > before }.order(id: :asc).all
        items = items.reverse  # Restore descending order
        {
          items: items.first(limit),
          next_cursor: items.last?.try(&.id),
          prev_cursor: items.size > limit ? items.first.id : nil,
          has_more: items.size > limit
        }
      else
        items = query.order(id: :desc).all
        {
          items: items.first(limit),
          next_cursor: items.size > limit ? items[limit - 1].id : nil,
          prev_cursor: nil,
          has_more: items.size > limit
        }
      end
    end
    def fetch_activity(before : Time? = nil, limit : Int32 = 20)
      query = Activity.order(created_at: :desc).limit(limit + 1)
    
      if before
        query = query.where { created_at < before }
      end
    
      items = query.all
      has_more = items.size > limit
      items = items.first(limit) if has_more
    
      {
        items: items,
        next_cursor: has_more ? items.last?.try(&.created_at) : nil,
        has_more: has_more
      }
    end
    # Cursor contains both the sort value and ID for uniqueness
    struct Cursor
      property views : Int64
      property id : Int64
    
      def self.parse(s : String) : self?
        parts = s.split(":")
        return nil unless parts.size == 2
        new(parts[0].to_i64, parts[1].to_i64)
      rescue
        nil
      end
    
      def to_s : String
        "#{@views}:#{@id}"
      end
    
      def initialize(@views, @id)
      end
    end
    
    def fetch_popular_posts(after : String? = nil, limit : Int32 = 20)
      query = Post.published.order(views_count: :desc, id: :desc).limit(limit + 1)
    
      if after && (cursor = Cursor.parse(after))
        query = query.where {
          (views_count < cursor.views) |
          ((views_count == cursor.views) & (id < cursor.id))
        }
      end
    
      items = query.all
      has_more = items.size > limit
      items = items.first(limit) if has_more
    
      next_cursor = if has_more && (last = items.last?)
        Cursor.new(last.views_count, last.id.not_nil!).to_s
      end
    
      {items: items, next_cursor: next_cursor, has_more: has_more}
    end
    def posts_api(params)
      result = fetch_posts(params["after"]?.try(&.to_i64))
    
      {
        data: result[:items].map(&.to_json),
        pagination: {
          next_cursor: result[:next_cursor],
          has_more: result[:has_more]
        }
      }
    end
    require "base64"
    
    def encode_cursor(id : Int64) : String
      Base64.strict_encode("cursor:#{id}")
    end
    
    def decode_cursor(cursor : String) : Int64?
      decoded = Base64.decode_string(cursor)
      if decoded.starts_with?("cursor:")
        decoded[7..].to_i64
      end
    rescue
      nil
    end
    # Create test data
    10.times do |i|
      Post.create!(title: "Post #{i + 1}", body: "...", published: true)
    end
    
    # Fetch pages
    page1 = fetch_posts(nil, limit: 3)
    page1[:items].size       # => 3
    page1[:has_more]         # => true
    
    page2 = fetch_posts(page1[:next_cursor], limit: 3)
    page2[:items].size       # => 3
    page2[:has_more]         # => true
    
    # Items don't overlap
    page1_ids = page1[:items].map(&.id)
    page2_ids = page2[:items].map(&.id)
    (page1_ids & page2_ids).empty?  # => true
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property email : String
    
      before_save :normalize_email
      after_create :send_welcome_email
    
      def initialize(@name : String, @email : String)
      end
    
      private def normalize_email
        @email = @email.downcase.strip
        true  # Return true to continue
      end
    
      private def send_welcome_email
        puts "Sending welcome email to #{@email}"
        true
      end
    end
    struct Article
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :articles
    
      property id : Int64?
      property title : String
      property published : Bool = false
    
      before_save :check_can_publish
    
      def initialize(@title : String, @published : Bool = false)
      end
    
      private def check_can_publish
        if @published && @title.empty?
          errors.add(:title, "cannot be blank when publishing")
          return false  # Halt - save won't proceed
        end
        true
      end
    end
    
    # This save will fail
    article = Article.new(title: "", published: true)
    article.save  # => false
    article.errors.first.message  # => "cannot be blank when publishing"
    before_validation :normalize_data
    
    private def normalize_data
      @email = @email.downcase.strip
      @name = @name.strip
      true
    end
    before_create :set_created_at
    before_save :set_updated_at
    
    private def set_created_at
      @created_at = Time.utc
      true
    end
    
    private def set_updated_at
      @updated_at = Time.utc
      true
    end
    before_save :generate_slug
    
    private def generate_slug
      @slug = @title.downcase.gsub(/[^a-z0-9]+/, "-").strip("-")
      true
    end
    after_create :notify_admin
    
    private def notify_admin
      # Send notification (email, webhook, etc.)
      puts "New user registered: #{@email}"
      true
    end
    before_destroy :cleanup_files
    
    private def cleanup_files
      # Delete associated files
      File.delete(@avatar_path) if @avatar_path && File.exists?(@avatar_path)
      true
    end
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property status : String = "pending"
    
      before_save :normalize_data
      before_save :check_permissions
      before_save :set_timestamps
    
      def initialize(@name : String, @email : String)
      end
    
      private def normalize_data
        @email = @email.downcase
        true
      end
    
      private def check_permissions
        if @status == "banned"
          errors.add(:status, "banned users cannot be saved")
          return false  # Halts - set_timestamps won't run
        end
        true
      end
    
      private def set_timestamps
        @updated_at = Time.utc
        true
      end
    end
    user = User.new("John", " JOHN@EXAMPLE.COM ")
    user.save
    
    user.email  # => "john@example.com" (normalized by callback)
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property age : Int32 = 0
    
      validate :name, presence: true
      validate :email, required: true, match: /@/
      validate :age, gt: 0, lt: 120
    
      def initialize(@name : String, @email : String, @age : Int32 = 0)
      end
    end
    validate :name, presence: true      # Not nil AND not empty
    validate :user_id, required: true   # Not nil (can be empty)
    validate :username, size: 3..20     # Between 3 and 20 characters
    validate :zip_code, size: 5         # Exactly 5 characters
    validate :age, gt: 0                # Greater than 0
    validate :age, gte: 18              # Greater than or equal to 18
    validate :score, lt: 100            # Less than 100
    validate :score, lte: 100           # Less than or equal to 100
    validate :priority, eq: 1           # Exactly 1
    validate :email, match: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
    validate :phone, match: /\A\d{3}-\d{3}-\d{4}\z/
    validate :status, in: ["active", "inactive", "pending"]
    validate :rating, in: 1..5
    validate :username, exclude: ["admin", "root", "system"]
    validate :name, presence: true, message: "Name cannot be blank"
    validate :email, match: /@/, message: "Please enter a valid email address"
    validate :age, gt: 0, message: "Age must be a positive number"
    user = User.new("", "invalid-email", -5)
    
    if user.valid?
      puts "User is valid"
    else
      puts "User has errors"
    end
    user = User.new("", "invalid-email", -5)
    
    unless user.valid?
      user.errors.each do |error|
        puts "#{error.field}: #{error.message}"
      end
    end
    user = User.new("", "invalid", -5)
    
    begin
      user.validate!
    rescue ex
      puts "Validation failed: #{ex.message}"
    end
    class PasswordValidator < CQL::ActiveRecord::Validations::CustomValidator
      def valid? : Array(CQL::ActiveRecord::Validations::Error)
        errors = [] of CQL::ActiveRecord::Validations::Error
        password = @record.password
    
        unless password && password.size >= 8
          errors << CQL::ActiveRecord::Validations::Error.new(:password, "must be at least 8 characters")
        end
    
        unless password && password.matches?(/[A-Z]/)
          errors << CQL::ActiveRecord::Validations::Error.new(:password, "must contain an uppercase letter")
        end
    
        unless password && password.matches?(/\d/)
          errors << CQL::ActiveRecord::Validations::Error.new(:password, "must contain a number")
        end
    
        errors
      end
    end
    
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property password : String?
    
      use PasswordValidator
    
      def initialize(@name : String, @password : String? = nil)
      end
    end
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property email : String
    
      validate :email, presence: true
    
      def validate
        super
        validate_email_uniqueness
      end
    
      private def validate_email_uniqueness
        existing = User.find_by(email: @email)
        if existing && existing.id != @id
          errors.add(:email, "has already been taken")
        end
      end
    
      def initialize(@email : String)
      end
    end
    user = User.new("", "invalid")
    
    # save returns false if invalid
    if user.save
      puts "Saved!"
    else
      puts "Failed: #{user.errors.map(&.message).join(", ")}"
    end
    
    # save! raises if invalid
    begin
      user.save!
    rescue ex
      puts "Error: #{ex.message}"
    end
    # Test valid data
    user = User.new("John Doe", "john@example.com", 30)
    user.valid?  # => true
    
    # Test invalid data
    bad_user = User.new("", "invalid", -5)
    bad_user.valid?  # => false
    bad_user.errors.size  # => 3
    schema.table :users do
      primary :id, Int64, auto_increment: true
      column :name, String
      timestamps
    end
    schema.users.create!
    
    schema.table :profiles do
      primary :id, Int64, auto_increment: true
      column :user_id, Int64, null: false
      column :bio, String
      column :avatar_url, String
    
      foreign_key [:user_id], references: :users, references_columns: [:id]
      index [:user_id], unique: true
      timestamps
    end
    schema.profiles.create!
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
    
      has_one :profile, Profile, :user_id
    
      def initialize(@name : String)
      end
    end
    
    struct Profile
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :profiles
    
      property id : Int64?
      property user_id : Int64
      property bio : String?
      property avatar_url : String?
    
      belongs_to :user, User, :user_id
    
      def initialize(@user_id : Int64, @bio : String? = nil, @avatar_url : String? = nil)
      end
    end
    user = User.find(1)
    profile = user.profile  # Returns Profile?
    
    if profile
      puts "Bio: #{profile.bio}"
    else
      puts "No profile"
    end
    user = User.create!(name: "John")
    
    profile = Profile.create!(
      user_id: user.id.not_nil!,
      bio: "Hello world",
      avatar_url: "/avatars/john.jpg"
    )
    
    user.profile.try(&.bio)  # => "Hello world"
    user = User.find(1)
    
    profile = Profile.new(user.id.not_nil!, "My bio")
    profile.save!
    user = User.find(1)
    
    if profile = user.profile
      profile.bio = "Updated bio"
      profile.save!
    end
    user = User.find(1)
    user.profile.try(&.delete!)
    user = User.create!(name: "Test User")
    
    profile = Profile.create!(
      user_id: user.id.not_nil!,
      bio: "Test bio"
    )
    
    user.profile.try(&.bio)  # => "Test bio"
    profile.user.try(&.name)  # => "Test User"
    struct Post
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :posts
    
      property id : Int64?
      property title : String
      property published : Bool = false
      property views_count : Int64 = 0
      property created_at : Time?
    
      # Scopes
      def self.published
        where(published: true)
      end
    
      def self.draft
        where(published: false)
      end
    
      def self.popular(min_views = 100)
        where { views_count >= min_views }
      end
    
      def self.recent(days = 7)
        where { created_at > days.days.ago }
      end
    end
    # Single scope
    Post.published.all
    
    # Chain scopes
    Post.published.popular.recent.all
    
    # With other methods
    Post.published.order(created_at: :desc).limit(10).all
    struct User
      def self.by_role(role : String)
        where(role: role)
      end
    
      def self.created_after(date : Time)
        where { created_at > date }
      end
    
      def self.age_between(min : Int32, max : Int32)
        where { age.between(min, max) }
      end
    end
    
    # Usage
    User.by_role("admin").all
    User.created_after(1.month.ago).all
    User.age_between(18, 65).all
    struct Post
      def self.published_if(condition : Bool)
        condition ? published : self
      end
    end
    
    # Usage - only filter if condition is true
    show_published = params["published"]? == "true"
    Post.published_if(show_published).all
    struct Post
      def self.active
        where(deleted_at: nil).where(published: true)
      end
    end
    
    # Always start queries from active scope
    Post.active.order(created_at: :desc).all
    # All these work
    Post.published.popular.recent.all
    Post.recent.published.popular.all
    Post.popular.published.all
    struct Post
      def self.newest_first
        order(created_at: :desc)
      end
    
      def self.most_popular
        order(views_count: :desc)
      end
    end
    
    Post.published.newest_first.limit(10).all
    Post.published.most_popular.limit(10).all
    struct Post
      def self.featured
        published
          .where { views_count > 1000 }
          .order(views_count: :desc)
      end
    
      def self.by_author(user : User)
        where(user_id: user.id)
      end
    
      def self.in_category(category : Category)
        where(category_id: category.id)
      end
    end
    
    # Usage
    Post.featured.limit(5).all
    Post.by_author(current_user).published.all
    Post.in_category(tech_category).recent.all
    user = User.find!(1)
    
    # Use scopes on associated records
    user.posts.published.all
    user.posts.recent(30).popular(50).all
    Post.create!(title: "Draft", body: "...", published: false, views_count: 10)
    Post.create!(title: "Published", body: "...", published: true, views_count: 200)
    Post.create!(title: "Popular", body: "...", published: true, views_count: 500)
    
    Post.published.count       # => 2
    Post.draft.count           # => 1
    Post.popular(100).count    # => 2
    Post.popular(300).count    # => 1
    page = 1
    per_page = 10
    
    posts = Post.order(created_at: :desc)
                .limit(per_page)
                .offset((page - 1) * per_page)
                .all
    def paginate(query, page : Int32 = 1, per_page : Int32 = 10)
      offset = (page - 1) * per_page
      query.limit(per_page).offset(offset).all
    end
    
    # Usage
    posts = paginate(Post.where(published: true), page: 2, per_page: 20)
    def paginated_results(query, page : Int32 = 1, per_page : Int32 = 10)
      total = query.count
      total_pages = (total / per_page.to_f).ceil.to_i
      offset = (page - 1) * per_page
    
      {
        items: query.limit(per_page).offset(offset).all,
        page: page,
        per_page: per_page,
        total: total,
        total_pages: total_pages,
        has_next: page < total_pages,
        has_prev: page > 1
      }
    end
    
    # Usage
    result = paginated_results(Post.published, page: 2)
    puts "Page #{result[:page]} of #{result[:total_pages]}"
    puts "Showing #{result[:items].size} of #{result[:total]} items"
    def cursor_paginate(last_id : Int64?, limit : Int32 = 10)
      query = Post.order(id: :desc).limit(limit)
    
      if last_id
        query = query.where { id < last_id }
      end
    
      items = query.all
      next_cursor = items.last?.try(&.id)
    
      {items: items, next_cursor: next_cursor}
    end
    
    # First page
    result = cursor_paginate(nil)
    
    # Next page
    result = cursor_paginate(result[:next_cursor])
    def paginate_by_time(before : Time?, limit : Int32 = 10)
      query = Post.order(created_at: :desc).limit(limit)
    
      if before
        query = query.where { created_at < before }
      end
    
      items = query.all
      next_cursor = items.last?.try(&.created_at)
    
      {items: items, next_cursor: next_cursor}
    end
    def keyset_paginate(after_id : Int64?, limit : Int32 = 10)
      query = Post.where(published: true)
                  .order(views_count: :desc, id: :desc)
                  .limit(limit)
    
      if after_id
        # Get the reference record
        ref = Post.find(after_id)
        if ref
          query = query.where {
            (views_count < ref.views_count) |
            ((views_count == ref.views_count) & (id < after_id))
          }
        end
      end
    
      query.all
    end
    def list_posts(params)
      page = (params["page"]? || "1").to_i
      per_page = [(params["per_page"]? || "10").to_i, 100].min  # Max 100
    
      query = Post.published.order(created_at: :desc)
      result = paginated_results(query, page, per_page)
    
      {
        posts: result[:items].map(&.to_json),
        meta: {
          page: result[:page],
          per_page: result[:per_page],
          total: result[:total],
          total_pages: result[:total_pages]
        }
      }
    end
    # Create 25 posts
    25.times do |i|
      Post.create!(title: "Post #{i + 1}", body: "...", published: true)
    end
    
    # Test pagination
    page1 = Post.order(id: :asc).limit(10).offset(0).all
    page2 = Post.order(id: :asc).limit(10).offset(10).all
    page3 = Post.order(id: :asc).limit(10).offset(20).all
    
    page1.size  # => 10
    page2.size  # => 10
    page3.size  # => 5
    
    page1.first.title  # => "Post 1"
    page2.first.title  # => "Post 11"
    page3.first.title  # => "Post 21"
    user = User.find(1)
    user.try(&.delete!)
    puts "User deleted"
    user = User.find!(1)
    user.delete!
    user = User.find!(1)
    user.destroy!  # Runs before_destroy and after_destroy callbacks
    User.delete!(1)
    # Delete all inactive users
    User.where(active: false).delete!
    
    # Delete posts older than a year
    Post.where { created_at < 1.year.ago }.delete!
    # Delete all comments
    Comment.delete_all
    
    # Delete all with condition
    User.where(role: "guest").delete_all
    user = User.find!(1)
    user.delete!         # Sets deleted_at timestamp
    user.deleted?        # => true
    
    user.restore!        # Restores the record
    user.deleted?        # => false
    
    user.force_delete!   # Permanently removes from database
    # If posts have: on_delete: :cascade
    user = User.find!(1)
    user.delete!  # Also deletes all user's posts
    user = User.find!(1)
    user.posts.all.each(&.delete!)
    user.delete!
    User.transaction do
      user = User.find!(1)
      user.posts.all.each(&.delete!)
      user.comments.all.each(&.delete!)
      user.delete!
    end
    user = User.find!(1)
    
    if user.posts.count > 0
      puts "Cannot delete user with posts"
    else
      user.delete!
    end
    user = User.find!(1)
    user.delete!
    
    User.find(1)  # => nil
    User.transaction do
      user = User.create!(name: "John", email: "john@example.com")
      Profile.create!(user_id: user.id.not_nil!, bio: "Hello")
    end
    # Both records created, or neither
    User.transaction do
      user = User.create!(name: "John", email: "john@example.com")
      raise "Something went wrong!"  # Triggers rollback
      Profile.create!(user_id: user.id.not_nil!, bio: "Hello")
    end
    # Neither record is created
    begin
      User.transaction do
        user = User.create!(name: "John", email: "john@example.com")
        # ... more operations
      end
      puts "Transaction successful"
    rescue ex
      puts "Transaction failed: #{ex.message}"
    end
    def transfer(from_id : Int64, to_id : Int64, amount : BigDecimal)
      Account.transaction do
        from = Account.find!(from_id)
        to = Account.find!(to_id)
    
        raise "Insufficient funds" if from.balance < amount
    
        from.balance -= amount
        to.balance += amount
    
        from.save!
        to.save!
      end
    end
    def create_post_with_comments(author : User, title : String, comments : Array(String))
      Post.transaction do
        post = Post.create!(
          title: title,
          body: "Post body",
          user_id: author.id.not_nil!
        )
    
        comments.each do |comment_text|
          Comment.create!(
            body: comment_text,
            post_id: post.id.not_nil!,
            user_id: author.id
          )
        end
    
        post
      end
    end
    User.transaction do
      user = User.create!(name: "John", email: "john@example.com")
    
      # These are all part of the same transaction
      3.times do |i|
        Post.create!(
          title: "Post #{i + 1}",
          body: "Content",
          user_id: user.id.not_nil!
        )
      end
    end
    User.transaction do
      user = User.create!(name: "John", email: "john@example.com")
    
      if some_condition_fails
        raise "Validation failed"  # Rolls back the transaction
      end
    
      # Continue with more operations
    end
    result = User.transaction do
      user = User.create!(name: "John", email: "john@example.com")
      profile = Profile.create!(user_id: user.id.not_nil!)
      {user: user, profile: profile}
    end
    
    puts "Created user: #{result[:user].name}"
    begin
      User.transaction do
        # Operations that might fail
        user = User.create!(name: "John", email: "john@example.com")
        external_api.create_account(user)  # Might fail
      end
    rescue ex
      # Handle or log the error
      Log.error { "Transaction failed: #{ex.message}" }
      raise ex
    end
    initial_count = User.count
    
    begin
      User.transaction do
        User.create!(name: "Test", email: "test@example.com")
        raise "Rollback!"
      end
    rescue
    end
    
    User.count == initial_count  # => true (rolled back)
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::Postgres,
      uri: ENV["DATABASE_URL"]
    ) do
      # Pool settings
      pool_size 25
      checkout_timeout 5.seconds
      retry_attempts 3
    end
    pool_size 25  # Default: 5
    checkout_timeout 5.seconds  # Default: 5 seconds
    retry_attempts 3  # Default: 1
    pool = case ENV["APP_ENV"]?
           when "production"  then 25
           when "staging"     then 10
           else                    5
           end
    
    MyDB = CQL::Schema.define(:my_db, adapter: CQL::Adapter::Postgres, uri: db_url) do
      pool_size pool
    end
    # Check current pool status
    stats = MyDB.pool_stats
    puts "Available: #{stats[:available]}"
    puts "In use: #{stats[:busy]}"
    puts "Size: #{stats[:size]}"
    # Simulate concurrent connections
    10.times do
      spawn do
        User.count
        puts "Connection successful"
      end
    end
    sleep 1.second
    # Header partial
    current_user = User.find(session[:user_id])  # Query 1
    
    # Sidebar partial
    current_user = User.find(session[:user_id])  # Query 2 (duplicate!)
    
    # Main content
    current_user = User.find(session[:user_id])  # Query 3 (duplicate!)
    class RequestCacheMiddleware
      include HTTP::Handler
    
      def call(context)
        CQL::RequestCache.with_cache do
          call_next(context)
        end
      end
    end
    
    # Add to your middleware stack
    server = HTTP::Server.new([
      RequestCacheMiddleware.new,
      # ... other middleware
      MyApp.new
    ])
    CQL::RequestCache.with_cache do
      user1 = User.find(1)  # Executes query
      user2 = User.find(1)  # Returns cached result
      user3 = User.find(1)  # Returns cached result
    
      # Only 1 query executed!
    end
    CQL::RequestCache.with_cache do
      # All these are cached
      User.find(1)
      Post.where(user_id: 1).all
      Comment.where(post_id: 5).count
    end
    CQL::RequestCache.with_cache do
      # Force fresh data
      user = User.uncached.find(1)
    end
    CQL::RequestCache.with_cache do
      user = User.find(1)
    
      # After an update, clear cache
      user.update!(name: "New Name")
      CQL::RequestCache.clear
    
      user = User.find(1)  # Fresh query
    end
    before_all do |env|
      CQL::RequestCache.start
    end
    
    after_all do |env|
      CQL::RequestCache.stop
    end
    abstract class BrowserAction < Lucky::Action
      around :with_request_cache
    
      def with_request_cache
        CQL::RequestCache.with_cache { yield }
      end
    end
    class ApplicationController < Amber::Controller::Base
      around_action :with_request_cache
    
      private def with_request_cache
        CQL::RequestCache.with_cache { yield }
      end
    end
    CQL::RequestCache.on_hit do |query|
      Log.debug { "Cache hit: #{query}" }
    end
    
    CQL::RequestCache.on_miss do |query|
      Log.debug { "Cache miss: #{query}" }
    end
    CQL::RequestCache.with_cache do
      start = Time.monotonic
    
      100.times { User.find(1) }
    
      elapsed = Time.monotonic - start
      puts "100 finds in #{elapsed.total_milliseconds}ms"
      # Should be very fast (< 5ms) with caching
      # Would be slow (> 100ms) without caching
    end
    require "cql"
    require "pg"
    
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::Postgres,
      uri: "postgres://username:password@localhost:5432/myapp_development"
    ) do
    end
    
    MyDB.init
    require "cql"
    require "sqlite3"
    
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::SQLite,
      uri: "sqlite3://./db/development.db"
    ) do
    end
    
    MyDB.init
    require "cql"
    require "mysql"
    
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::MySql,
      uri: "mysql://username:password@localhost:3306/myapp_development"
    ) do
    end
    
    MyDB.init
    DATABASE_URL = ENV["DATABASE_URL"]? || "postgres://localhost/myapp_development"
    
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::Postgres,
      uri: DATABASE_URL
    ) do
    end
    postgres://username:password@host:port/database
    postgres://user:pass@localhost:5432/myapp
    postgres://localhost/myapp  # Uses default user and port
    mysql://username:password@host:port/database
    mysql://user:pass@localhost:3306/myapp
    sqlite3://path/to/database.db
    sqlite3://./db/development.db
    sqlite3://:memory:  # In-memory database
    begin
      MyDB.init
      puts "Connected successfully"
    rescue ex
      puts "Connection failed: #{ex.message}"
      exit 1
    end
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::Postgres,
      uri: "postgres://user:pass@localhost:5432/myapp?sslmode=require"
    ) do
    end
    db_path = "./db/development.db"
    Dir.mkdir_p(File.dirname(db_path)) unless Dir.exists?(File.dirname(db_path))
    
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::SQLite,
      uri: "sqlite3://#{db_path}"
    ) do
    end
    PrimaryDB = CQL::Schema.define(
      :primary,
      adapter: CQL::Adapter::Postgres,
      uri: ENV["PRIMARY_DATABASE_URL"]
    ) do
    end
    
    AnalyticsDB = CQL::Schema.define(
      :analytics,
      adapter: CQL::Adapter::Postgres,
      uri: ENV["ANALYTICS_DATABASE_URL"]
    ) do
    end
    
    # Use different schemas for different models
    struct User
      db_context PrimaryDB, :users
    end
    
    struct PageView
      db_context AnalyticsDB, :page_views
    end
    MyDB.init
    
    # Run a simple query
    MyDB.exec("SELECT 1")
    puts "Database connection verified"
    MyDB.on_query do |sql, duration|
      Log.info { "[CQL] (#{duration.total_milliseconds.round(2)}ms) #{sql}" }
    end
    SLOW_QUERY_THRESHOLD = 100.milliseconds
    
    MyDB.on_query do |sql, duration|
      if duration > SLOW_QUERY_THRESHOLD
        Log.warn { "[SLOW QUERY] (#{duration.total_milliseconds.round(2)}ms) #{sql}" }
      end
    end
    module QueryMetrics
      @@query_count = Atomic(Int64).new(0)
      @@total_time = Atomic(Int64).new(0)
    
      def self.record(duration : Time::Span)
        @@query_count.add(1)
        @@total_time.add(duration.total_microseconds.to_i64)
      end
    
      def self.stats
        {
          count: @@query_count.get,
          total_ms: @@total_time.get / 1000.0,
          avg_ms: @@total_time.get / [@@query_count.get, 1].max / 1000.0
        }
      end
    
      def self.reset
        @@query_count.set(0)
        @@total_time.set(0)
      end
    end
    
    MyDB.on_query do |sql, duration|
      QueryMetrics.record(duration)
    end
    class QueryTracker
      property queries = [] of {String, Time::Span}
    
      def record(sql : String, duration : Time::Span)
        @queries << {sql, duration}
      end
    
      def total_time
        @queries.sum(&.[1])
      end
    
      def count
        @queries.size
      end
    end
    
    # In your request middleware
    tracker = QueryTracker.new
    MyDB.with_query_callback(tracker.method(:record)) do
      # Handle request
      response = handle_request(request)
    
      Log.info { "Request completed: #{tracker.count} queries in #{tracker.total_time.total_milliseconds}ms" }
      response
    end
    module QueryAnalyzer
      @@patterns = Hash(String, {count: Int32, total_time: Time::Span}).new
    
      def self.record(sql : String, duration : Time::Span)
        # Normalize query by removing specific values
        pattern = sql.gsub(/= \d+/, "= ?")
                     .gsub(/= '[^']*'/, "= ?")
                     .gsub(/IN \([^)]+\)/, "IN (?)")
    
        existing = @@patterns[pattern]? || {count: 0, total_time: Time::Span.zero}
        @@patterns[pattern] = {
          count: existing[:count] + 1,
          total_time: existing[:total_time] + duration
        }
      end
    
      def self.report
        @@patterns.to_a
          .sort_by { |_, stats| -stats[:total_time].total_milliseconds }
          .first(10)
          .each do |pattern, stats|
            puts "#{stats[:count]}x (#{stats[:total_time].total_milliseconds.round(2)}ms total): #{pattern[0..100]}"
          end
      end
    end
    # PostgreSQL
    stats = MyDB.exec(<<-SQL
      SELECT
        relname as table,
        seq_scan,
        idx_scan,
        n_live_tup as rows
      FROM pg_stat_user_tables
      ORDER BY seq_scan DESC
      LIMIT 10
    SQL
    )
    
    stats.each do |row|
      puts "#{row["table"]}: #{row["seq_scan"]} seq scans, #{row["idx_scan"]} idx scans"
    end
    spawn do
      loop do
        stats = MyDB.pool_stats
        Log.info { "Pool: #{stats[:busy]}/#{stats[:size]} connections in use" }
        sleep 30.seconds
      end
    end
    require "prometheus"
    
    query_counter = Prometheus.counter("cql_queries_total", "Total CQL queries")
    query_duration = Prometheus.histogram("cql_query_duration_seconds", "Query duration")
    
    MyDB.on_query do |sql, duration|
      query_counter.inc
      query_duration.observe(duration.total_seconds)
    end
    # Run some queries
    User.all
    Post.where(published: true).all
    User.find(1)
    
    # Check your logs/metrics
    QueryMetrics.stats  # => {count: 3, total_ms: 15.5, avg_ms: 5.17}
    class AddIndexes < CQL::Migration(1)
      def up
        add_index :users, :email
        add_index :posts, :user_id
        add_index :posts, :published_at
      end
    
      def down
        remove_index :users, :email
        remove_index :posts, :user_id
        remove_index :posts, :published_at
      end
    end
    # For queries like: WHERE user_id = ? AND status = ?
    add_index :orders, [:user_id, :status]
    add_index :users, :email, unique: true
    # Instead of
    users = User.all
    
    # Use
    users = User.select(:id, :name, :email).all
    # Returns array of emails
    emails = User.where(active: true).pluck(:email)
    
    # Returns array of [id, name] tuples
    data = User.select(:id, :name).pluck(:id, :name)
    # Pagination
    users = User.limit(20).offset(40).all
    
    # Or use cursor pagination for large datasets
    users = User.where { id > last_id }.limit(20).all
    # Efficient - runs COUNT query
    count = User.where(active: true).count
    
    # Inefficient - loads all records
    count = User.where(active: true).all.size
    User.where(needs_update: true).each_batch(1000) do |batch|
      batch.each do |user|
        user.update!(processed: true)
      end
    end
    # Efficient - stops at first match
    if User.where(email: email).exists?
      # ...
    end
    
    # Inefficient - counts all matches
    if User.where(email: email).count > 0
      # ...
    end
    # Add index for common ordering
    add_index :posts, :created_at
    
    # Query uses index
    Post.order(created_at: :desc).limit(10).all
    MyDB.on_query do |sql, duration|
      if duration > 100.milliseconds
        Log.warn { "Slow query (#{duration}ms): #{sql}" }
      end
    end
    result = MyDB.exec("EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com'")
    puts result.first
    require "spec"
    require "../src/models/*"
    
    describe User do
      describe "#valid?" do
        it "is valid with valid attributes" do
          user = User.new("John", "john@example.com")
          user.valid?.should be_true
        end
    
        it "is invalid without email" do
          user = User.new("John", "")
          user.valid?.should be_false
        end
    
        it "is invalid with malformed email" do
          user = User.new("John", "invalid-email")
          user.valid?.should be_false
        end
      end
    end
    describe User do
      describe "#save" do
        it "persists to database" do
          user = User.new("John", "john@example.com")
          user.save.should be_true
          user.id.should_not be_nil
        end
    
        it "sets timestamps" do
          user = User.new("John", "john@example.com")
          user.save
          user.created_at.should_not be_nil
        end
      end
    
      describe ".find" do
        it "retrieves a saved user" do
          user = User.create!(name: "John", email: "john@example.com")
          found = User.find(user.id.not_nil!)
          found.should_not be_nil
          found.not_nil!.name.should eq("John")
        end
      end
    end
    describe Post do
      describe "callbacks" do
        it "generates slug before save" do
          post = Post.new(title: "Hello World", body: "Content")
          post.save
          post.slug.should eq("hello-world")
        end
    
        it "normalizes title" do
          post = Post.new(title: "  HELLO  ", body: "Content")
          post.save
          post.title.should eq("Hello")
        end
      end
    end
    describe Post do
      describe "relationships" do
        it "belongs to user" do
          user = User.create!(name: "John", email: "john@example.com")
          post = Post.create!(user_id: user.id.not_nil!, title: "Test", body: "Body")
    
          post.user.id.should eq(user.id)
        end
    
        it "has many comments" do
          post = Post.create!(user_id: 1, title: "Test", body: "Body")
          Comment.create!(post_id: post.id.not_nil!, body: "Nice!")
          Comment.create!(post_id: post.id.not_nil!, body: "Great!")
    
          post.comments.count.should eq(2)
        end
      end
    end
    describe Post do
      describe "scopes" do
        before_each do
          Post.create!(title: "Published", body: "...", published: true)
          Post.create!(title: "Draft", body: "...", published: false)
        end
    
        it ".published returns only published posts" do
          Post.published.count.should eq(1)
          Post.published.first.not_nil!.title.should eq("Published")
        end
    
        it ".drafts returns only draft posts" do
          Post.drafts.count.should eq(1)
        end
      end
    end
    module TestHelpers
      def create_user(name = "Test User", email = "test@example.com")
        User.create!(name: name, email: email)
      end
    
      def create_post(user : User, title = "Test Post")
        Post.create!(user_id: user.id.not_nil!, title: title, body: "Content")
      end
    end
    
    Spec.before_each do
      extend TestHelpers
    end
    module Factory
      def self.user(attrs = {} of Symbol => String)
        User.create!(
          name: attrs[:name]? || "Test User #{rand(1000)}",
          email: attrs[:email]? || "test#{rand(1000)}@example.com"
        )
      end
    
      def self.post(user : User? = nil, attrs = {} of Symbol => String)
        user ||= self.user
        Post.create!(
          user_id: user.id.not_nil!,
          title: attrs[:title]? || "Test Post",
          body: attrs[:body]? || "Test content"
        )
      end
    end
    
    # Usage
    user = Factory.user(name: "John")
    post = Factory.post(user, title: "My Post")
    Spec.before_each do
      # Truncate tables
      MyDB.exec("TRUNCATE users, posts, comments RESTART IDENTITY CASCADE")
    end
    
    # Or use transactions
    Spec.around_each do |example|
      MyDB.transaction do
        example.run
        raise Rollback.new  # Always rollback
      end
    end
    # Run all tests
    crystal spec
    
    # Run specific file
    crystal spec spec/models/user_spec.cr
    
    # Run with verbose output
    crystal spec --verbose
    createdb myapp_test
    mysql -e "CREATE DATABASE myapp_test"
    # src/config/database.cr
    module Config
      def self.database_url
        case ENV["APP_ENV"]?
        when "test"
          ENV["TEST_DATABASE_URL"]? || "postgres://localhost/myapp_test"
        when "production"
          ENV["DATABASE_URL"]
        else
          ENV["DATABASE_URL"]? || "postgres://localhost/myapp_development"
        end
      end
    end
    
    # src/db.cr
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::Postgres,
      uri: Config.database_url
    ) do
    end
    # .env.test
    TEST_DATABASE_URL=postgres://localhost/myapp_test
    
    # Run tests
    APP_ENV=test crystal spec
    # spec/spec_helper.cr
    ENV["APP_ENV"] = "test"
    ENV["TEST_DATABASE_URL"] = "postgres://localhost/myapp_test"
    
    require "../src/db"
    require "../src/models/*"
    # spec/spec_helper.cr
    ENV["APP_ENV"] = "test"
    require "../src/db"
    
    # Run migrations
    MyDB.migrator.up
    #!/bin/bash
    # bin/test
    APP_ENV=test crystal run src/migrate.cr
    APP_ENV=test crystal spec "$@"
    # spec/spec_helper.cr
    Spec.before_each do
      # PostgreSQL
      MyDB.exec(<<-SQL
        TRUNCATE users, posts, comments
        RESTART IDENTITY CASCADE
      SQL
      )
    
      # MySQL
      # MyDB.exec("SET FOREIGN_KEY_CHECKS = 0")
      # MyDB.exec("TRUNCATE users")
      # MyDB.exec("SET FOREIGN_KEY_CHECKS = 1")
    
      # SQLite
      # MyDB.exec("DELETE FROM users")
      # MyDB.exec("DELETE FROM sqlite_sequence WHERE name='users'")
    end
    Spec.around_each do |example|
      MyDB.transaction do
        example.run
        raise DB::Rollback.new
      end
    end
    Spec.after_each do
      Comment.delete_all
      Post.delete_all
      User.delete_all
    end
    # spec/spec_helper.cr
    TestDB = CQL::Schema.define(
      :test_db,
      adapter: CQL::Adapter::SQLite,
      uri: "sqlite3://:memory:"
    ) do
    end
    
    Spec.before_suite do
      # Run migrations on in-memory database
      TestDB.migrator.up
    end
    # spec/support/test_schema.cr
    TestDB = CQL::Schema.define(:test, adapter: CQL::Adapter::SQLite, uri: "sqlite3://:memory:") do
      table :users do
        primary_key :id, Int64
        column :name, String
        column :email, String
        timestamps
      end
    end
    
    # Redefine models to use test database
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context TestDB, :users
      # ... properties
    end
    worker_id = ENV["TEST_WORKER_ID"]? || "0"
    test_db = "myapp_test_#{worker_id}"
    
    TestDB = CQL::Schema.define(:test, adapter: CQL::Adapter::Postgres, uri: "postgres://localhost/#{test_db}") do
    end
    # spec/database_spec.cr
    describe "Database" do
      it "connects to test database" do
        MyDB.exec("SELECT 1").should_not be_nil
      end
    
      it "has migrated schema" do
        User.count.should eq(0)  # Table exists
      end
    end
    relation "users" already exists
    puts migrator.applied_migrations
    migrator.mark_as_applied(migration_number)
    schema.users.drop! if schema.table?(:users)
    relation "users" does not exist
    migrator.up  # Run all pending migrations
    column "email" of relation "users" already exists
    def up
      unless schema.column_exists?(:users, :email)
        schema.alter :users do
          add_column :email, String
        end
      end
    end
    def up
      schema.alter :users do
        drop_column :email if column_exists?(:email)
        add_column :email, String, null: false
      end
    end
    cannot drop column "user_id" because other objects depend on it
    def up
      schema.alter :posts do
        drop_foreign_key [:user_id]
        drop_index :idx_posts_user_id
        drop_column :user_id
      end
    end
    column "name" contains null values
    schema.alter :users do
      add_column :name, String, null: false, default: "Unknown"
    end
    def up
      schema.alter :users do
        add_column :name, String, null: true
      end
    
      schema.exec("UPDATE users SET name = 'Unknown' WHERE name IS NULL")
    
      schema.alter :users do
        change_column :name, String, null: false
      end
    end
    insert or update on table "posts" violates foreign key constraint
    bigint :user_id, null: true
    foreign_key [:user_id], references: :users, on_delete: :set_null
    migrator = MyDB.migrator(config)
    
    puts "Applied migrations: #{migrator.applied_migrations}"
    puts "Pending migrations: #{migrator.pending_migrations}"
    puts "Current version: #{migrator.current_version}"
    def reset_database
      migrator = MyDB.migrator(config)
    
      puts "Rolling back all migrations..."
      migrator.down_to(0)
    
      puts "Running all migrations..."
      migrator.up
    
      puts "Database reset complete"
    end
    # List tables
    schema.tables
    -- Remove from migration tracking
    DELETE FROM cql_schema_migrations WHERE version = 5;
    schema.partial_table.drop! if schema.table?(:partial_table)
    migrator.up
    # 1 query to fetch posts
    posts = Post.all
    
    # N queries - one for each post's author!
    posts.each do |post|
      puts post.user.name  # Triggers a query each time
    end
    # 2 queries total: posts + users
    posts = Post.preload(:user).all
    
    posts.each do |post|
      puts post.user.name  # No additional query
    end
    # Load multiple associations
    posts = Post
      .preload(:user, :comments)
      .all
    # Fetch posts with comments from active users
    posts = Post
      .joins(:user)
      .where("users.active = ?", true)
      .all
    posts = Post
      .select(:id, :title, :user_id)
      .all
    posts = Post.all
    
    # Load all users at once
    user_ids = posts.map(&.user_id).uniq
    users_by_id = User.where(id: user_ids).index_by(&.id)
    
    posts.each do |post|
      user = users_by_id[post.user_id]
      puts user.name
    end
    MyDB.on_query do |sql, duration|
      Log.debug { sql }
    end
    SELECT * FROM users WHERE id = 1
    SELECT * FROM users WHERE id = 2
    SELECT * FROM users WHERE id = 3
    ...
    # Controller
    @posts = Post.preload(:user, :comments).all
    
    # View - no additional queries
    <% @posts.each do |post| %>
      <p>By: <%= post.user.name %></p>
      <p>Comments: <%= post.comments.size %></p>
    <% end %>
    # Add column in migration
    schema.alter :posts do
      add_column :comments_count, Int32, default: 0
    end
    
    # Update counter when adding comments
    def create_comment(post : Post, content : String)
      Comment.create!(content: content, post_id: post.id.not_nil!)
      post.comments_count = post.comments.count.to_i32
      post.save!
    end
    
    # Now use post.comments_count instead of post.comments.count
    # Instead of
    posts.each { |p| p.comments.count }  # N+1
    
    # Use
    Post.joins(:comments)
        .group(:id)
        .select("posts.*, COUNT(comments.id) as comments_count")
        .all
    Post Load (2ms) SELECT * FROM posts
    User Load (1ms) SELECT * FROM users WHERE id = 1
    User Load (1ms) SELECT * FROM users WHERE id = 2
    User Load (1ms) SELECT * FROM users WHERE id = 3
    ... (100 more queries)
    Post Load (2ms) SELECT * FROM posts
    User Load (3ms) SELECT * FROM users WHERE id IN (1, 2, 3, ...)
    schema.table :posts do
      primary :id, Int64, auto_increment: true
      column :title, String
      timestamps
    end
    schema.posts.create!
    
    schema.table :tags do
      primary :id, Int64, auto_increment: true
      column :name, String
      timestamps
    end
    schema.tags.create!
    
    # Join table
    schema.table :post_tags do
      column :post_id, Int64, null: false
      column :tag_id, Int64, null: false
      column :created_at, Time
    
      foreign_key [:post_id], references: :posts, references_columns: [:id], on_delete: :cascade
      foreign_key [:tag_id], references: :tags, references_columns: [:id], on_delete: :cascade
      index [:post_id]
      index [:tag_id]
      index [:post_id, :tag_id], unique: true
    end
    schema.post_tags.create!
    struct Post
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :posts
    
      property id : Int64?
      property title : String
    
      has_many :post_tags, PostTag, :post_id
    
      def initialize(@title : String)
      end
    
      # Helper to get tags
      def tags : Array(Tag)
        tag_ids = post_tags.all.map(&.tag_id)
        return [] of Tag if tag_ids.empty?
        Tag.where { id.in(tag_ids) }.all
      end
    
      # Helper to add a tag
      def add_tag(tag : Tag)
        PostTag.create!(post_id: id.not_nil!, tag_id: tag.id.not_nil!)
      end
    
      # Helper to remove a tag
      def remove_tag(tag : Tag)
        PostTag.where(post_id: id, tag_id: tag.id).each(&.delete!)
      end
    end
    struct Tag
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :tags
    
      property id : Int64?
      property name : String
    
      has_many :post_tags, PostTag, :tag_id
    
      def initialize(@name : String)
      end
    
      # Helper to get posts
      def posts : Array(Post)
        post_ids = post_tags.all.map(&.post_id)
        return [] of Post if post_ids.empty?
        Post.where { id.in(post_ids) }.all
      end
    end
    struct PostTag
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :post_tags
    
      property post_id : Int64
      property tag_id : Int64
      property created_at : Time?
    
      belongs_to :post, Post, :post_id
      belongs_to :tag, Tag, :tag_id
    
      def initialize(@post_id : Int64, @tag_id : Int64)
      end
    end
    post = Post.create!(title: "My Post")
    tag1 = Tag.create!(name: "Crystal")
    tag2 = Tag.create!(name: "ORM")
    
    # Add tags
    post.add_tag(tag1)
    post.add_tag(tag2)
    
    post.tags.map(&.name)  # => ["Crystal", "ORM"]
    PostTag.create!(post_id: post.id.not_nil!, tag_id: tag.id.not_nil!)
    post = Post.find(1)
    tags = post.tags
    tags.each { |tag| puts tag.name }
    tag = Tag.find_by(name: "Crystal")
    posts = tag.posts if tag
    posts.each { |post| puts post.title }
    crystal_tag = Tag.find_by(name: "Crystal")
    if crystal_tag
      post_ids = PostTag.where(tag_id: crystal_tag.id).all.map(&.post_id)
      crystal_posts = Post.where { id.in(post_ids) }.all
    end
    post.remove_tag(tag)
    post.post_tags.all.each(&.delete!)
    post = Post.create!(title: "Test")
    tag = Tag.create!(name: "Test Tag")
    
    post.add_tag(tag)
    
    post.tags.map(&.name)  # => ["Test Tag"]
    tag.posts.map(&.title)  # => ["Test"]
    
    post.remove_tag(tag)
    post.tags  # => []
    user = User.find(1)
    if user
      puts "Found: #{user.name}"
    else
      puts "Not found"
    end
    user = User.find!(1)  # Raises if not found
    puts user.name
    user = User.find_by(email: "john@example.com")
    puts user.try(&.name)
    post = Post.find_by(user_id: 1, published: true)
    user = User.first
    puts user.try(&.name)
    user = User.last
    puts user.try(&.name)
    users = User.all
    users.each { |u| puts u.name }
    # First by creation date
    oldest = User.order(created_at: :asc).first
    
    # Last by creation date
    newest = User.order(created_at: :desc).first
    exists = User.exists?(email: "john@example.com")
    puts "User exists: #{exists}"
    user = User.find_by(email: "john@example.com")
    user ||= User.new("John", "john@example.com")
    user = User.find(1)
    name = user.try(&.name) || "Unknown"
    User.create!(name: "John", email: "john@example.com")
    
    User.find_by(name: "John")  # => User instance
    User.find_by(name: "Jane")  # => nil
    begin
      user.validate!
    rescue CQL::ActiveRecord::Validations::ValidationError => ex
      puts ex.message  # Contains validation error messages
    end
    begin
      user.update!
    rescue CQL::OptimisticLockError
      user.reload!  # Get latest version
      # Re-apply changes and retry
    end
    begin
      user = User.find!(999)
    rescue CQL::RecordNotFound
      puts "User not found"
    end
    begin
      User.create!(name: "", email: "")
    rescue CQL::RecordInvalid => ex
      puts "Failed: #{ex.message}"
    end
    def down
      raise "Cannot rollback: data would be lost"
    end
    Log.setup do |c|
      c.bind("cql.*", :debug, Log::IOBackend.new)
    end
    begin
      # operation
    rescue ex
      puts "Error type: #{ex.class}"
      puts "Message: #{ex.message}"
      puts "Backtrace: #{ex.backtrace.first(5).join("\n")}"
    end
    unless model.valid?
      model.errors.each do |error|
        puts "#{error.field}: #{error.message}"
      end
    end
    posts = Post
      .where(published: true)
      .where { views_count > 100 }
      .order(created_at: :desc)
      .limit(10)
      .all
    User.where { (active == true) & (verified == true) & (age > 18) }.all
    User.where { (role == "admin") | (role == "moderator") | (role == "owner") }.all
    User.where { (active == true) & ((role == "admin") | (role == "moderator")) }.all
    # Get users who have posts
    active_author_ids = Post.where(published: true).pluck(:user_id)
    authors = User.where { id.in(active_author_ids) }.all
    total = User.count
    active = User.where(active: true).count
    total_views = Post.where(published: true).sum(:views_count)
    avg_age = User.where(active: true).avg(:age)
    oldest_post = Post.min(:created_at)
    most_views = Post.max(:views_count)
    # Posts per category
    Category.all.map do |cat|
      {
        name: cat.name,
        posts: cat.posts.count
      }
    end
    # Only fetch needed columns
    names = User.where(active: true).pluck(:name)
    Post.order(published: :desc, created_at: :desc).all
    page = 2
    per_page = 10
    
    Post.order(created_at: :desc)
        .limit(per_page)
        .offset((page - 1) * per_page)
        .all
    unique_categories = Post.select(:category_id).distinct.all
    user = User.find!(1)
    
    # Posts with conditions
    published_posts = user.posts.where(published: true).all
    
    # Ordered posts
    recent_posts = user.posts.order(created_at: :desc).limit(5).all
    # Get top 10 active users with most published posts
    users_with_counts = User.where(active: true).all.map do |user|
      post_count = user.posts.where(published: true).count
      {user: user, post_count: post_count}
    end
    
    top_users = users_with_counts
      .sort_by { |h| -h[:post_count] }
      .first(10)
    
    top_users.each do |h|
      puts "#{h[:user].name}: #{h[:post_count]} posts"
    end
    result = MyDB.exec(<<-SQL
      SELECT u.name, COUNT(p.id) as post_count
      FROM users u
      LEFT JOIN posts p ON p.user_id = u.id AND p.published = true
      WHERE u.active = true
      GROUP BY u.id
      ORDER BY post_count DESC
      LIMIT 10
    SQL
    )
    # Setup test data
    user = User.create!(name: "John", email: "john@example.com", active: true)
    Post.create!(title: "Post 1", body: "...", user_id: user.id.not_nil!, published: true, views_count: 150)
    Post.create!(title: "Post 2", body: "...", user_id: user.id.not_nil!, published: true, views_count: 50)
    
    # Test query
    popular = Post.where(published: true).where { views_count > 100 }.all
    popular.size  # => 1
    popular.first.title  # => "Post 1"
    # Raw SQL approach
    result = db.exec("SELECT id, name, email FROM users WHERE id = ?", [1])
    row = result.first
    user_id = row[0].as(Int64)
    user_name = row[1].as(String)
    user_email = row[2].as(String)
    # ORM approach
    user = User.find(1)
    puts user.name
    puts user.email
    struct User                           # Represents the 'users' table
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?                # Column: id
      property name : String              # Column: name
      property email : String             # Column: email
    end
    # Database: posts table has a user_id column
    # ORM: Post belongs_to User
    
    post = Post.find(1)
    author = post.user  # No SQL needed in your code!
    user.name = 123      # Compile error: expected String, got Int32
    user.age = "thirty"  # Compile error: expected Int32, got String
    # Safe - CQL parameterizes the value
    User.where(email: user_input)
    
    # Dangerous raw SQL
    db.exec("SELECT * FROM users WHERE email = '#{user_input}'")
    # Create user with validation
    user = User.create!(
      name: "John",
      email: "john@example.com"
    )
    
    # Instead of:
    # 1. Write INSERT SQL
    # 2. Handle errors
    # 3. Get the inserted ID
    # 4. Validate before insert
    # etc.
    # Works with PostgreSQL, MySQL, or SQLite
    User.where(active: true).order(created_at: :desc).all
    # Add a new field - one place to change
    struct User
      property phone : String?  # Add property
    end
    
    # All code using User automatically has access to phone
    result = MyDB.exec(<<-SQL
      SELECT u.name, COUNT(p.id) as post_count
      FROM users u
      LEFT JOIN posts p ON p.user_id = u.id
      GROUP BY u.id
      HAVING COUNT(p.id) > 10
    SQL
    )
    user = User.new("John", "john@example.com")
    user.save  # The user object saves itself
    crystal spec
    schema.table :users do
      primary :id, Int64, auto_increment: true
      column :name, String
      column :deleted_at, Time, null: true
      timestamps
    end
    schema.users.create!
    struct User
      include CQL::ActiveRecord::Model(Int64)
      include CQL::ActiveRecord::SoftDeletable
    
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property created_at : Time?
      property updated_at : Time?
      # deleted_at is handled automatically
    
      def initialize(@name : String)
      end
    end
    user = User.find(1)
    user.delete!       # Sets deleted_at to current time
    
    user.deleted?      # => true
    user.deleted_at    # => 2024-01-15 10:30:00 UTC
    User.delete!(user_id)
    User.delete_by!(email: "spam@example.com")
    User.delete_all   # Soft deletes all users
    user.restore!
    
    user.deleted?      # => false
    user.deleted_at    # => nil
    User.restore!(user_id)
    User.restore_all
    User.all           # Only active users
    User.count         # Only counts active users
    User.find(1)       # Returns nil if user is deleted
    User.with_deleted.all        # All users including deleted
    User.with_deleted.find(1)    # Finds even if deleted
    User.with_deleted.count      # Counts all users
    User.only_deleted.all        # Only deleted users
    User.only_deleted.count      # Count of deleted users
    user.force_delete!           # Permanently removes from database
    User.force_delete!(user_id)  # By ID
    User.force_delete_all        # Permanently delete all records
    schema.alter :users do
      create_index :idx_users_deleted_at, [:deleted_at]
    end
    def cleanup_old_deleted_records
      cutoff = 1.year.ago
      old_records = User.only_deleted
        .where { deleted_at < cutoff }
        .all
    
      old_records.each(&.force_delete!)
      puts "Deleted #{old_records.size} old records"
    end
    struct Post
      include CQL::ActiveRecord::Model(Int64)
      include CQL::ActiveRecord::SoftDeletable
    
      db_context MyDB, :posts
    
      has_many :comments, Comment, :post_id
    
      after_destroy :soft_delete_comments
    
      private def soft_delete_comments
        comments.all.each(&.delete!)
        true
      end
    end
    # Create a user
    user = User.create!(name: "John")
    
    # Soft delete
    user.delete!
    User.count           # => 0
    User.with_deleted.count  # => 1
    
    # Restore
    user.restore!
    User.count           # => 1
    
    # Permanent delete
    user.force_delete!
    User.with_deleted.count  # => 0
    user = User.new("John", "john@example.com")
    
    if user.save
      puts "Created user #{user.id}"
    else
      puts "Failed: #{user.errors.map(&.message).join(", ")}"
    end
    user = User.new("John", "john@example.com")
    user.save!  # Raises if validation fails
    puts "Created user #{user.id}"
    user = User.create(name: "John", email: "john@example.com")
    
    if user.persisted?
      puts "Created user #{user.id}"
    else
      puts "Failed to create"
    end
    user = User.create!(
      name: "John",
      email: "john@example.com",
      age: 30
    )
    puts "Created user #{user.id}"
    users = ["Alice", "Bob", "Charlie"].map do |name|
      User.create!(
        name: name,
        email: "#{name.downcase}@example.com"
      )
    end
    
    puts "Created #{users.size} users"
    user = User.create!(name: "John", email: "john@example.com")
    
    post = Post.create!(
      title: "My First Post",
      body: "Hello world!",
      user_id: user.id.not_nil!
    )
    
    puts "Created post for user #{user.name}"
    struct User
      property status : String = "pending"
      property role : String = "user"
    end
    
    user = User.create!(name: "John", email: "john@example.com")
    user.status  # => "pending"
    user.role    # => "user"
    user = User.create!(name: "John", email: "john@example.com")
    user.created_at  # => 2024-01-15 10:30:00 UTC
    user.updated_at  # => 2024-01-15 10:30:00 UTC
    user = User.new("", "invalid-email")
    
    unless user.save
      user.errors.each do |error|
        puts "#{error.field}: #{error.message}"
      end
    end
    user = User.create!(name: "John", email: "john@example.com")
    
    user.id.nil?        # => false (has ID now)
    user.persisted?     # => true
    user.new_record?    # => false
    active_users = User.where(active: true).all
    users = User.where(active: true, role: "admin").all
    users = User
      .where(active: true)
      .where(role: "admin")
      .all
    # Greater than
    adults = User.where { age > 18 }.all
    
    # Less than
    young = User.where { age < 30 }.all
    
    # Greater than or equal
    User.where { age >= 18 }.all
    
    # Less than or equal
    User.where { age <= 65 }.all
    
    # Not equal
    User.where { status != "banned" }.all
    # Between
    User.where { age.between(18, 65) }.all
    
    # In a set
    User.where { role.in(["admin", "moderator"]) }.all
    # Records from last week
    User.where { created_at > 1.week.ago }.all
    
    # Records from specific date
    User.where { created_at > Time.utc(2024, 1, 1) }.all
    
    # Records between dates
    Post.where { published_at.between(start_date, end_date) }.all
    # Where null
    User.where(deleted_at: nil).all
    
    # Where not null (use block)
    User.where { deleted_at != nil }.all
    # Contains
    User.where { name.like("%john%") }.all
    
    # Starts with
    User.where { email.like("admin%") }.all
    
    # Ends with
    User.where { email.like("%@example.com") }.all
    User.where { (role == "admin") | (role == "moderator") }.all
    User.where { (age > 18) & (active == true) }.all
    User.where(active: true).where { age > 18 }.all
    User.where(active: true)
        .order(created_at: :desc)
        .all
    User.where(active: true)
        .limit(10)
        .all
    count = User.where(active: true).count
    puts "Active users: #{count}"
    User.create!(name: "John", email: "john@example.com", age: 25, active: true)
    User.create!(name: "Jane", email: "jane@example.com", age: 30, active: false)
    
    User.where(active: true).count  # => 1
    User.where { age > 20 }.count   # => 2
    require "cql"
    require "../migrations/*"
    
    MyDB.init
    
    config = CQL::MigratorConfig.new(
      schema_file_path: "src/schemas/app_schema.cr",
      schema_name: :AppSchema,
      auto_sync: true
    )
    
    migrator = MyDB.migrator(config)
    migrator.up
    puts "Migrations complete"
    puts "Applied: #{migrator.applied_migrations.size}"
    puts "Pending: #{migrator.pending_migrations.size}"
    # Run up to migration 5
    migrator.up_to(5)
    # src/migrate.cr
    require "./database"
    require "../migrations/*"
    
    MyDB.init
    
    config = CQL::MigratorConfig.new(
      schema_file_path: "src/schemas/app_schema.cr",
      schema_name: :AppSchema,
      auto_sync: true
    )
    
    migrator = MyDB.migrator(config)
    
    case ARGV[0]?
    when "up"
      migrator.up
      puts "Migrated up"
    when "down"
      migrator.down
      puts "Rolled back one migration"
    when "status"
      puts "Applied: #{migrator.applied_migrations.size}"
      puts "Pending: #{migrator.pending_migrations.size}"
    when "version"
      puts "Current version: #{migrator.current_version}"
    else
      puts "Usage: crystal src/migrate.cr [up|down|status|version]"
    end
    # Run all pending migrations
    crystal src/migrate.cr up
    
    # Check status
    crystal src/migrate.cr status
    
    # Rollback one migration
    crystal src/migrate.cr down
    # src/app.cr
    require "./database"
    require "../migrations/*"
    
    def setup_database
      MyDB.init
    
      config = CQL::MigratorConfig.new(
        schema_file_path: "src/schemas/app_schema.cr",
        schema_name: :AppSchema,
        auto_sync: true
      )
    
      migrator = MyDB.migrator(config)
    
      pending = migrator.pending_migrations.size
      if pending > 0
        puts "Running #{pending} pending migration(s)..."
        migrator.up
      end
    end
    
    setup_database
    # ... rest of your app
    migrator = MyDB.migrator(config)
    migrator.up
    
    # Verify
    puts "Applied migrations:"
    migrator.applied_migrations.each do |version|
      puts "  - #{version}"
    end
    class AddAvatarToUsers < CQL::Migration(5)
      def up
        schema.alter :users do
          add_column :avatar_url, String, null: true
        end
      end
    
      def down
        schema.alter :users do
          drop_column :avatar_url
        end
      end
    end
    class AddProfileFieldsToUsers < CQL::Migration(6)
      def up
        schema.alter :users do
          add_column :bio, String, null: true
          add_column :website, String, null: true
          add_column :location, String, null: true
        end
      end
    
      def down
        schema.alter :users do
          drop_column :bio
          drop_column :website
          drop_column :location
        end
      end
    end
    class AddActiveToUsers < CQL::Migration(7)
      def up
        schema.alter :users do
          add_column :active, Bool, default: true, null: false
        end
      end
    
      def down
        schema.alter :users do
          drop_column :active
        end
      end
    end
    class AddSlugToPosts < CQL::Migration(8)
      def up
        schema.alter :posts do
          add_column :slug, String, null: true
        end
    
        schema.alter :posts do
          create_index :idx_posts_slug, [:slug], unique: true
        end
      end
    
      def down
        schema.alter :posts do
          drop_index :idx_posts_slug
          drop_column :slug
        end
      end
    end
    class AddCategoryToPosts < CQL::Migration(9)
      def up
        schema.alter :posts do
          add_column :category_id, Int64, null: true
          create_index :idx_posts_category_id, [:category_id]
          foreign_key [:category_id], references: :categories, references_columns: [:id], on_delete: :set_null
        end
      end
    
      def down
        schema.alter :posts do
          drop_foreign_key :fk_posts_category_id
          drop_index :idx_posts_category_id
          drop_column :category_id
        end
      end
    end
    class AddTimestampsToPosts < CQL::Migration(10)
      def up
        schema.alter :posts do
          add_column :created_at, Time, null: true
          add_column :updated_at, Time, null: true
        end
    
        # Optionally set current time for existing records
        schema.exec("UPDATE posts SET created_at = NOW(), updated_at = NOW() WHERE created_at IS NULL")
      end
    
      def down
        schema.alter :posts do
          drop_column :created_at
          drop_column :updated_at
        end
      end
    end
    class AddDeletedAtToUsers < CQL::Migration(11)
      def up
        schema.alter :users do
          add_column :deleted_at, Time, null: true
        end
    
        schema.alter :users do
          create_index :idx_users_deleted_at, [:deleted_at]
        end
      end
    
      def down
        schema.alter :users do
          drop_index :idx_users_deleted_at
          drop_column :deleted_at
        end
      end
    end
    class AddVersionToUsers < CQL::Migration(12)
      def up
        schema.alter :users do
          add_column :version, Int32, default: 1, null: false
        end
      end
    
      def down
        schema.alter :users do
          drop_column :version
        end
      end
    end
    struct User
      # Existing properties...
    
      # New property
      property avatar_url : String?
    
      # Don't forget to update constructor if needed
      def initialize(@name : String, @email : String, @avatar_url : String? = nil)
      end
    end
    migrator.up
    
    # Test the new column works
    user = User.create!(name: "John", email: "john@example.com", avatar_url: "/avatars/john.png")
    user.avatar_url  # => "/avatars/john.png"
    # Cache user by ID for 5 minutes
    user = User.cache(5.minutes).find(user_id)
    
    # Cache query results
    posts = Post.cache(1.minute)
                .where(published: true)
                .order(created_at: :desc)
                .limit(10)
                .all
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::Postgres,
      uri: ENV["DATABASE_URL"]
    ) do
      cache_store CQL::Cache::Memory.new
      default_cache_ttl 5.minutes
    end
    cache_store CQL::Cache::Memory.new(max_size: 1000)
    cache_store CQL::Cache::Redis.new(
      host: ENV["REDIS_HOST"],
      port: 6379,
      db: 1
    )
    # These generate the same cache key:
    User.cache(1.minute).find(1)
    User.cache(1.minute).find(1)  # Cache hit!
    
    # Different query = different key:
    User.cache(1.minute).where(id: 1).first  # Different key
    posts = Post.cache(1.hour, key: "homepage_posts")
                .where(featured: true)
                .limit(5)
                .all
    # Clear specific cache
    MyDB.cache.delete("homepage_posts")
    
    # Clear all caches
    MyDB.cache.clear
    struct Post
      after_save :invalidate_cache
      after_destroy :invalidate_cache
    
      private def invalidate_cache
        MyDB.cache.delete("homepage_posts")
        MyDB.cache.delete("post:#{@id}")
        true
      end
    end
    def find_user(id : Int64, use_cache : Bool = true)
      query = User.where(id: id)
      query = query.cache(5.minutes) if use_cache
      query.first
    end
    stats = MyDB.cache.stats
    puts "Hits: #{stats[:hits]}"
    puts "Misses: #{stats[:misses]}"
    puts "Hit rate: #{stats[:hit_rate]}%"
    # First call - cache miss
    user = User.cache(1.minute).find(1)
    puts "First: #{user.name}"
    
    # Second call - cache hit (no query logged)
    user = User.cache(1.minute).find(1)
    puts "Second: #{user.name}"
    CRYSTAL_ENV = ENV["CRYSTAL_ENV"]? || "development"
    require "cql"
    
    CRYSTAL_ENV = ENV["CRYSTAL_ENV"]? || "development"
    
    DATABASE_URL = case CRYSTAL_ENV
    when "production"
      ENV["DATABASE_URL"]? || raise "DATABASE_URL required in production"
    when "test"
      ENV["TEST_DATABASE_URL"]? || "postgres://localhost/myapp_test"
    else
      ENV["DATABASE_URL"]? || "postgres://localhost/myapp_development"
    end
    
    ADAPTER = case CRYSTAL_ENV
    when "test"
      # Use SQLite for faster tests
      CQL::Adapter::SQLite
    else
      CQL::Adapter::Postgres
    end
    
    MyDB = CQL::Schema.define(:my_db, adapter: ADAPTER, uri: DATABASE_URL) do
    end
    # config/database.cr
    
    module DatabaseConfig
      CRYSTAL_ENV = ENV["CRYSTAL_ENV"]? || "development"
    
      def self.connection_url : String
        case CRYSTAL_ENV
        when "production"
          ENV["DATABASE_URL"]
        when "test"
          "sqlite3://./db/test.db"
        when "development"
          "postgres://localhost/myapp_development"
        else
          raise "Unknown environment: #{CRYSTAL_ENV}"
        end
      end
    
      def self.adapter
        case CRYSTAL_ENV
        when "test"
          CQL::Adapter::SQLite
        else
          CQL::Adapter::Postgres
        end
      end
    end
    
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: DatabaseConfig.adapter,
      uri: DatabaseConfig.connection_url
    ) do
    end
    # config/database/development.cr
    module Database::Development
      URL = "postgres://localhost/myapp_development"
      ADAPTER = CQL::Adapter::Postgres
    end
    
    # config/database/test.cr
    module Database::Test
      URL = "sqlite3://./db/test.db"
      ADAPTER = CQL::Adapter::SQLite
    end
    
    # config/database/production.cr
    module Database::Production
      URL = ENV["DATABASE_URL"]
      ADAPTER = CQL::Adapter::Postgres
    end
    
    # config/database.cr
    {% if env("CRYSTAL_ENV") == "production" %}
      require "./database/production"
      DB_CONFIG = Database::Production
    {% elsif env("CRYSTAL_ENV") == "test" %}
      require "./database/test"
      DB_CONFIG = Database::Test
    {% else %}
      require "./database/development"
      DB_CONFIG = Database::Development
    {% end %}
    
    MyDB = CQL::Schema.define(:my_db, adapter: DB_CONFIG::ADAPTER, uri: DB_CONFIG::URL) do
    end
    # Development (default)
    crystal run src/app.cr
    
    # Test
    CRYSTAL_ENV=test crystal spec
    
    # Production
    CRYSTAL_ENV=production DATABASE_URL=postgres://... crystal run src/app.cr
    CRYSTAL_ENV = ENV["CRYSTAL_ENV"]? || "development"
    
    if CRYSTAL_ENV == "development"
      # Enable query logging
      Log.setup do |c|
        c.bind("cql.*", :debug, Log::IOBackend.new)
      end
    end
    
    if CRYSTAL_ENV == "production"
      # Use connection pooling
      # Enable SSL
    end
    puts "Running in #{CRYSTAL_ENV} environment"
    puts "Database: #{DATABASE_URL.gsub(/:[^:@]+@/, ":****@")}"  # Hide password
    migrator = MyDB.migrator(config)
    migrator.down
    puts "Rolled back one migration"
    # Rollback last 3 migrations
    3.times { migrator.down }
    # Rollback to version 5
    migrator.down_to(5)
    # Rollback everything (use with caution!)
    migrator.down_to(0)
    puts "Current version: #{migrator.current_version}"
    puts "Applied migrations: #{migrator.applied_migrations.size}"
    
    # Only rollback if there are migrations to rollback
    if migrator.applied_migrations.any?
      migrator.down
      puts "Rolled back to version: #{migrator.current_version}"
    else
      puts "No migrations to rollback"
    end
    # src/rollback.cr
    require "./database"
    require "../migrations/*"
    
    MyDB.init
    
    config = CQL::MigratorConfig.new(
      schema_file_path: "src/schemas/app_schema.cr",
      schema_name: :AppSchema,
      auto_sync: true
    )
    
    migrator = MyDB.migrator(config)
    
    current = migrator.current_version
    puts "Current version: #{current}"
    
    if current == 0
      puts "No migrations to rollback"
      exit 0
    end
    
    print "Rollback migration #{current}? (y/n): "
    if gets.try(&.strip) == "y"
      migrator.down
      puts "Rolled back to version: #{migrator.current_version}"
    else
      puts "Cancelled"
    end
    class DropLegacyTable < CQL::Migration(10)
      def up
        schema.legacy_users.drop!
      end
    
      def down
        # Can't restore dropped table with data
        raise "Cannot rollback: table was dropped with data"
      end
    end
    begin
      migrator.down
    rescue ex
      puts "Rollback failed: #{ex.message}"
    end
    def refresh_database
      migrator = MyDB.migrator(config)
    
      puts "Rolling back all migrations..."
      migrator.down_to(0)
    
      puts "Re-running all migrations..."
      migrator.up
    
      puts "Database refreshed"
    end
    migrator = MyDB.migrator(config)
    
    # Record current state
    before = migrator.applied_migrations.size
    
    # Rollback
    migrator.down
    
    # Verify
    after = migrator.applied_migrations.size
    puts "Rolled back: #{before} -> #{after} migrations"
    user = User.find(1)
    if user
      user.name = "Updated Name"
      user.save
      puts "Updated!"
    end
    user = User.find!(1)
    user.name = "Updated Name"
    user.save!  # Raises if validation fails
    user = User.find!(1)
    user.update!(
      name: "New Name",
      email: "new@example.com",
      age: 31
    )
    # Update all inactive users
    User.where(active: false).update!(active: true)
    
    # Update with specific value
    Post.where(user_id: 1).update!(published: true)
    post = Post.find!(1)
    post.views_count += 1
    post.save!
    user = User.find!(1)
    
    if user.status == "pending"
      user.status = "active"
      user.activated_at = Time.utc
      user.save!
    end
    user = User.find!(1)
    user.name = "New Name"  # Only name changed
    user.save!              # Only updates name column
    user = User.find!(1)
    user.email = "invalid"
    
    unless user.save
      puts user.errors.map(&.message).join(", ")
    end
    user = User.find!(1)
    user.name = "Updated"
    user.save!
    
    user.updated_at  # => Current time
    # Direct update bypasses callbacks
    User.where(id: 1).update!(name: "Direct Update")
    user = User.find!(1)
    original_name = user.name
    
    user.name = "New Name"
    user.save!
    
    User.find!(1).name  # => "New Name"
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::Postgres,
      uri: "postgres://user:pass@host/db?sslmode=require"
    ) do
    end
    uri = "postgres://user:pass@host/db?" + {
      "sslmode"     => "verify-full",
      "sslcert"     => "/path/to/client.crt",
      "sslkey"      => "/path/to/client.key",
      "sslrootcert" => "/path/to/ca.crt"
    }.map { |k, v| "#{k}=#{v}" }.join("&")
    
    MyDB = CQL::Schema.define(:my_db, adapter: CQL::Adapter::Postgres, uri: uri) do
    end
    uri = "mysql://user:pass@host/db?" + {
      "ssl-mode" => "REQUIRED",
      "ssl-ca"   => "/path/to/ca.pem"
    }.map { |k, v| "#{k}=#{v}" }.join("&")
    
    MyDB = CQL::Schema.define(:my_db, adapter: CQL::Adapter::MySQL, uri: uri) do
    end
    uri = "postgres://#{ENV["DB_USER"]}:#{ENV["DB_PASS"]}@#{ENV["DB_HOST"]}/#{ENV["DB_NAME"]}?" +
          "sslmode=verify-full&" +
          "sslcert=#{ENV["SSL_CERT_PATH"]}&" +
          "sslkey=#{ENV["SSL_KEY_PATH"]}&" +
          "sslrootcert=#{ENV["SSL_CA_PATH"]}"
    # PostgreSQL
    result = MyDB.exec("SHOW ssl")
    puts "SSL enabled: #{result.first["ssl"]}"
    
    # Or check connection info
    MyDB.exec("SELECT pg_backend_pid()") do |rs|
      puts "Connected with SSL"
    end
    uri = "postgres://user:pass@rds-host.amazonaws.com/db?sslmode=verify-full&sslrootcert=/path/to/rds-ca-2019-root.pem"
    MyDB = CQL::Schema.define(:my_db, adapter: CQL::Adapter::Postgres, uri: ENV["DATABASE_URL"]) do
    end
    dependencies:
      redis:
        github: stefanwille/crystal-redis
        version: ~> 2.8.0
    require "cql"
    require "redis"
    
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::Postgres,
      uri: ENV["DATABASE_URL"]
    ) do
      cache_store CQL::Cache::Redis.new(
        host: "localhost",
        port: 6379
      )
    end
    cache_store CQL::Cache::Redis.new(
      host: ENV["REDIS_HOST"]? || "localhost",
      port: (ENV["REDIS_PORT"]? || "6379").to_i,
      password: ENV["REDIS_PASSWORD"]?,
      db: 1,                    # Use database 1 for cache
      pool_size: 5,
      timeout: 2.seconds
    )
    cache_store CQL::Cache::Redis.new(
      url: ENV["REDIS_URL"]  # redis://user:pass@host:port/db
    )
    cache_store CQL::Cache::Redis.new(
      host: "localhost",
      prefix: "myapp:cache:"
    )
    MyDB = CQL::Schema.define(:my_db, adapter: CQL::Adapter::Postgres, uri: db_url) do
      cache_store CQL::Cache::Redis.new(host: "localhost")
      default_cache_ttl 10.minutes
    end
    
    # Override per query
    User.cache(1.hour).find(1)
    cache_store CQL::Cache::Redis.new(
      host: "localhost",
      pool_size: 25,
      checkout_timeout: 3.seconds
    )
    cache_store CQL::Cache::RedisCluster.new(
      nodes: [
        "redis://node1:6379",
        "redis://node2:6379",
        "redis://node3:6379"
      ]
    )
    # Manual cache operations
    cache = MyDB.cache
    
    # Set value
    cache.set("custom_key", data.to_json, ttl: 1.hour)
    
    # Get value
    value = cache.get("custom_key")
    
    # Delete
    cache.delete("custom_key")
    
    # Clear all (with prefix)
    cache.clear
    stats = MyDB.cache.stats
    puts "Hits: #{stats[:hits]}"
    puts "Misses: #{stats[:misses]}"
    puts "Memory: #{stats[:memory_used]}"
    redis-cli INFO stats
    redis-cli MONITOR
    # Test connection
    if MyDB.cache.connected?
      puts "Redis connected"
    else
      puts "Redis connection failed"
    end
    
    # Test caching
    User.cache(1.minute).find(1)
    User.cache(1.minute).find(1)  # Should be cached
    
    puts MyDB.cache.stats
    user = User.new("", "invalid-email")
    
    unless user.valid?
      user.errors.each do |error|
        puts "#{error.field}: #{error.message}"
      end
    end
    name: can't be blank
    user = User.new("John", "john@example.com")  # Not empty
    # Remove presence validation or make it conditional
    validate :bio, presence: true, on: :update
    email: is invalid
    user.email = "john@example.com"  # Valid email format
    # Make sure pattern matches expected format
    validate :email, match: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
    username: is too short (minimum is 3 characters)
    user.username = "john"  # At least 3 characters
    validate :username, size: 2..50  # Allow shorter names
    email: has already been taken
    user.email = "different@example.com"
    existing = User.find_by(email: email)
    if existing
      # Handle duplicate - update existing or reject
    end
    age: must be greater than 0
    user.age = 25  # Positive number
    validate :age, gt: 0, lt: 150
    user = User.new("", "bad", -5)
    
    if user.valid?
      puts "Valid!"
    else
      puts "Validation failed:"
      puts "Errors count: #{user.errors.size}"
    
      user.errors.each do |error|
        puts "  Field: #{error.field}"
        puts "  Message: #{error.message}"
        puts ""
      end
    end
    # Direct database update bypasses model validation
    User.where(id: 1).update!(email: "override@example.com")
    describe User do
      it "requires a name" do
        user = User.new("", "test@example.com")
        user.valid?.should be_false
        user.errors.map(&.field).should contain(:name)
      end
    
      it "requires valid email format" do
        user = User.new("John", "invalid")
        user.valid?.should be_false
        user.errors.map(&.field).should contain(:email)
      end
    
      it "accepts valid data" do
        user = User.new("John", "john@example.com")
        user.valid?.should be_true
      end
    end
    def create(params)
      user = User.new(params[:name], params[:email])
    
      if user.save
        {status: "success", user: user.to_json}
      else
        {
          status: "error",
          errors: user.errors.map { |e| {field: e.field, message: e.message} }
        }
      end
    end
    validate :email, required: true, message: "Please provide your email address"
    validate :password, size: 8..100, message: "Password must be at least 8 characters"
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::Postgres,  # or SQLite, MySql
      uri: "postgres://localhost/myapp"
    ) do
    end
    
    MyDB.init
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property created_at : Time?
      property updated_at : Time?
    
      def initialize(@name : String, @email : String)
      end
    end
    # Create
    user = User.create!(name: "John", email: "john@example.com")
    
    # Read
    user = User.find(1)
    user = User.find_by(email: "john@example.com")
    users = User.where(active: true).all
    
    # Update
    user.name = "Jane"
    user.save!
    
    # Delete
    user.delete!
    # Basic queries
    User.all
    User.first
    User.last
    User.count
    
    # Where clauses
    User.where(active: true).all
    User.where { age > 18 }.all
    User.where { name.like("%john%") }.all
    
    # Chaining
    User.where(active: true)
        .order(created_at: :desc)
        .limit(10)
        .all
    # Define
    belongs_to :user, User, :user_id
    has_one :profile, Profile, :user_id
    has_many :posts, Post, :user_id
    
    # Use
    user.posts.all
    post.user
    validate :name, presence: true
    validate :email, required: true, match: /@/
    validate :age, gt: 0, lt: 150
    validate :status, in: ["active", "pending"]
    before_save :normalize_data
    after_create :send_notification
    before_destroy :cleanup
    class CreateUsers < CQL::Migration(1)
      def up
        schema.table :users do
          primary :id, Int64, auto_increment: true
          column :name, String, null: false
          timestamps
        end
        schema.users.create!
      end
    
      def down
        schema.users.drop!
      end
    end
    config = CQL::MigratorConfig.new(
      schema_file_path: "src/schemas/app_schema.cr",
      schema_name: :AppSchema,
      auto_sync: true
    )
    
    migrator = MyDB.migrator(config)
    migrator.up
    User.transaction do
      user = User.create!(name: "John", email: "john@example.com")
      Profile.create!(user_id: user.id.not_nil!)
    end
    include CQL::ActiveRecord::SoftDeletable
    
    user.delete!      # Soft delete
    user.restore!     # Restore
    user.force_delete!  # Permanent delete
    
    User.with_deleted.all
    User.only_deleted.all
    include CQL::ActiveRecord::OptimisticLocking
    optimistic_locking version_column: :version
    
    begin
      user.update!
    rescue CQL::OptimisticLockError
      user.reload!
      # Retry
    end
    # PostgreSQL
    "postgres://user:pass@host:5432/database"
    
    # MySQL
    "mysql://user:pass@host:3306/database"
    
    # SQLite
    "sqlite3://./db/app.db"
    "sqlite3://:memory:"
    validate :field_name, option: value, option2: value2
    validate :name, presence: true
    validate :email, presence: true
    validate :email, match: /@/
    validate :phone, match: /^\d{10}$/
    validate :slug, match: /^[a-z0-9-]+$/
    validate :password, size: 8..128          # Range
    validate :username, size: 3..20
    validate :tags, size: 1..5                # Array size
    validate :age, gte: 0                # Greater than or equal to 0
    validate :age, lte: 150              # Less than or equal to 150
    validate :quantity, gte: 1, lte: 100 # Between 1 and 100
    validate :price, gt: 0               # Greater than 0
    validate :status, in: ["pending", "active", "cancelled"]
    validate :role, in: ["admin", "moderator", "user"]
    validate :priority, in: [1, 2, 3, 4, 5]
    validate :username, exclude: ["admin", "root", "system"]
    validate :status, exclude: ["deleted"]
    validate :email, presence: true, match: /@/
    validate :username, presence: true, size: 3..20, match: /^[a-z0-9_]+$/
    validate :age, presence: true, gte: 0, lte: 150
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property email : String
      property username : String
      property password : String
      property age : Int32?
      property role : String = "user"
    
      # Required fields
      validate :email, presence: true
      validate :username, presence: true
      validate :password, presence: true
    
      # Format validations
      validate :email, match: /^[^@\s]+@[^@\s]+\.[^@\s]+$/
      validate :username, match: /^[a-z0-9_]+$/
    
      # Length validations
      validate :username, size: 3..20
      validate :password, size: 8..128
    
      # Numeric constraints
      validate :age, gte: 0, lte: 150
    
      # Allowed values
      validate :role, in: ["admin", "moderator", "user"]
    end
    user = User.new("test", "test@example.com", "password123")
    
    if user.valid?
      user.save
    else
      user.errors.each do |error|
        puts error
      end
    end
    
    # Or use save! which raises on validation failure
    begin
      user.save!
    rescue CQL::ValidationError => e
      puts "Validation failed: #{e.message}"
    end
    struct Order
      include CQL::ActiveRecord::Model(Int64)
    
      property id : Int64?
      property total : Float64
      property discount : Float64 = 0.0
    
      before_save :validate_discount
    
      private def validate_discount
        if @discount > @total
          errors.add(:discount, "cannot exceed total")
          return false
        end
        true
      end
    end
    ┌──────────────┐
    │  before_save │ ─── Runs before INSERT or UPDATE
    ├──────────────┤
    │    save      │ ─── Database operation
    ├──────────────┤
    │  after_save  │ ─── Runs after INSERT or UPDATE
    └──────────────┘
    struct User
      include CQL::ActiveRecord::Model(Int64)
    
      before_save :normalize_email
    
      private def normalize_email
        @email = @email.downcase.strip
        true  # Must return true to continue
      end
    end
    struct Order
      after_save :update_inventory
    
      private def update_inventory
        order_items.each do |item|
          item.product.decrement_stock!(item.quantity)
        end
        true
      end
    end
    struct User
      before_create :set_defaults
    
      private def set_defaults
        @role ||= "member"
        @created_at = Time.utc
        true
      end
    end
    struct User
      after_create :send_welcome_email
    
      private def send_welcome_email
        Mailer.welcome(@email).deliver
        true
      end
    end
    struct Post
      before_update :track_changes
    
      private def track_changes
        @previous_status = @status_was if status_changed?
        true
      end
    end
    struct Post
      after_update :notify_subscribers
    
      private def notify_subscribers
        if @published && !@published_was
          subscribers.each do |sub|
            Mailer.new_post(sub, self).deliver
          end
        end
        true
      end
    end
    struct User
      before_destroy :check_can_delete
    
      private def check_can_delete
        if posts.any?
          errors.add(:base, "Cannot delete user with posts")
          return false
        end
        true
      end
    end
    struct User
      after_destroy :cleanup_files
    
      private def cleanup_files
        FileUtils.rm_rf("/uploads/users/#{@id}")
        true
      end
    end
    # Stops the save operation
    private def validate_something
      if some_condition_fails
        errors.add(:field, "error message")
        return false  # Halts operation
      end
      true  # Continues operation
    end
    struct Article
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :articles
    
      property id : Int64?
      property title : String
      property slug : String?
      property body : String
      property published : Bool = false
      property published_at : Time?
      property view_count : Int32 = 0
    
      # Before any save
      before_save :generate_slug
    
      # Only on create
      before_create :set_defaults
      after_create :notify_admin
    
      # Only on update
      before_update :check_publish_state
      after_update :invalidate_cache
    
      # On destroy
      before_destroy :archive_content
      after_destroy :cleanup
    
      private def generate_slug
        @slug = @title.downcase.gsub(/[^a-z0-9]+/, "-")
        true
      end
    
      private def set_defaults
        @view_count = 0
        true
      end
    
      private def notify_admin
        AdminMailer.new_article(self).deliver
        true
      end
    
      private def check_publish_state
        if @published && !published_was
          @published_at = Time.utc
        end
        true
      end
    
      private def invalidate_cache
        Cache.delete("article:#{@id}")
        Cache.delete("articles:list")
        true
      end
    
      private def archive_content
        Archive.create!(content: to_json, type: "Article")
        true
      end
    
      private def cleanup
        Cache.delete("article:#{@id}")
        true
      end
    end
    struct User  # Recommended
      include CQL::ActiveRecord::Model(Int64)
    end
    property id : Int64?  # Correct - nil until saved
    @[DB::Field(ignore: true)]
    property temp_value : String?
    results = MyDB.exec("SELECT * FROM users WHERE id = ?", [1])
    Log.setup do |c|
      c.bind("cql.*", :debug, Log::IOBackend.new)
    end
    posts = Post.where(published: true).all
    user_ids = posts.map(&.user_id).uniq
    users = User.where { id.in(user_ids) }.all.index_by(&.id)
    class AddAvatarToUsers < CQL::Migration(5)
      def up
        schema.alter :users do
          add_column :avatar_url, String, null: true
        end
      end
    
      def down
        schema.alter :users do
          drop_column :avatar_url
        end
      end
    end
    migrator.down       # Rollback one
    migrator.down_to(3) # Rollback to version 3
    DELETE FROM cql_schema_migrations WHERE version = 5;
    # Bad: N+1
    posts.each { |p| puts p.user.name }
    
    # Good: Batch load
    user_ids = posts.map(&.user_id).uniq
    users = User.where { id.in(user_ids) }.all.index_by(&.id)
    posts.each { |p| puts users[p.user_id]?.try(&.name) }
    migrator.up
    unless model.valid?
      model.errors.each do |e|
        puts "#{e.field}: #{e.message}"
      end
    end
    User.transaction do
      user = User.create!(...)
      Profile.create!(user_id: user.id.not_nil!)
    end
    before_each do
      # Clean database
      User.delete_all
    end
    
    it "creates a user" do
      user = User.create!(name: "Test", email: "test@example.com")
      user.id.should_not be_nil
    end
    ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
    │   Domain     │────>│  Repository  │────>│   Database   │
    │   Objects    │<────│              │<────│              │
    └──────────────┘     └──────────────┘     └──────────────┘
    # Entity - no database knowledge
    struct User
      property id : Int64?
      property name : String
      property email : String
    
      def full_name
        name.split.map(&.capitalize).join(" ")
      end
    end
    
    # Repository - handles persistence
    class UserRepository
      def find(id : Int64) : User?
        # Database query
      end
    
      def save(user : User) : Bool
        # Insert or update
      end
    end
    class MockUserRepository
      def find(id : Int64) : User?
        User.new("Test User", "test@example.com")
      end
    end
    
    # In tests
    service = UserService.new(MockUserRepository.new)
    abstract class UserRepository
      abstract def find(id : Int64) : User?
      abstract def save(user : User) : Bool
    end
    
    class PostgresUserRepository < UserRepository
      # PostgreSQL implementation
    end
    
    class ApiUserRepository < UserRepository
      # REST API implementation
    end
    struct User
      property id : Int64?
      property name : String
      property email : String
      property created_at : Time?
    
      def initialize(@name : String, @email : String)
      end
    
      def valid? : Bool
        !name.empty? && email.includes?("@")
      end
    end
    # This is internal to the repository
    struct UserRecord
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property created_at : Time?
    
      def initialize(@name : String, @email : String)
      end
    
      def to_entity : User
        user = User.new(@name, @email)
        user.id = @id
        user.created_at = @created_at
        user
      end
    
      def self.from_entity(user : User) : UserRecord
        record = new(user.name, user.email)
        record.id = user.id
        record
      end
    end
    class UserRepository
      def find(id : Int64) : User?
        record = UserRecord.find(id)
        record.try(&.to_entity)
      end
    
      def find_by_email(email : String) : User?
        record = UserRecord.find_by(email: email)
        record.try(&.to_entity)
      end
    
      def all : Array(User)
        UserRecord.all.map(&.to_entity)
      end
    
      def save(user : User) : Bool
        record = UserRecord.from_entity(user)
        if record.save
          user.id = record.id
          true
        else
          false
        end
      end
    
      def delete(user : User) : Bool
        return false unless user.id
        record = UserRecord.find(user.id.not_nil!)
        record.try(&.delete!) || false
      end
    
      def active_users : Array(User)
        UserRecord.where(active: true).all.map(&.to_entity)
      end
    end
    repo = UserRepository.new
    
    # Create
    user = User.new("John", "john@example.com")
    repo.save(user)
    
    # Find
    found = repo.find(user.id.not_nil!)
    
    # Query
    active = repo.active_users
    
    # Delete
    repo.delete(user)
    # Active Record
    user = User.find(1)
    user.name = "Updated"
    user.save
    
    # Repository
    user = user_repo.find(1)
    user.name = "Updated"
    user_repo.save(user)
    # Simple entities use Active Record directly
    post = Post.find(1)
    post.title = "Updated"
    post.save
    
    # Complex domain uses Repository
    order = order_repo.find(1)
    order.add_item(product, quantity)
    order_repo.save(order)
    class AddEmailIndex < CQL::Migration(5)
      def up
        schema.alter :users do
          create_index :idx_users_email, [:email]
        end
      end
    
      def down
        schema.alter :users do
          drop_index :idx_users_email
        end
      end
    end
    class AddUniqueEmailIndex < CQL::Migration(6)
      def up
        schema.alter :users do
          create_index :idx_users_email, [:email], unique: true
        end
      end
    
      def down
        schema.alter :users do
          drop_index :idx_users_email
        end
      end
    end
    class AddUserStatusIndex < CQL::Migration(7)
      def up
        schema.alter :users do
          create_index :idx_users_status_created, [:status, :created_at]
        end
      end
    
      def down
        schema.alter :users do
          drop_index :idx_users_status_created
        end
      end
    end
    class AddPostIndexes < CQL::Migration(8)
      def up
        schema.alter :posts do
          create_index :idx_posts_user_id, [:user_id]
          create_index :idx_posts_category_id, [:category_id]
        end
      end
    
      def down
        schema.alter :posts do
          drop_index :idx_posts_user_id
          drop_index :idx_posts_category_id
        end
      end
    end
    class AddFilterIndexes < CQL::Migration(9)
      def up
        schema.alter :posts do
          create_index :idx_posts_published, [:published]
          create_index :idx_posts_created_at, [:created_at]
        end
      end
    
      def down
        schema.alter :posts do
          drop_index :idx_posts_published
          drop_index :idx_posts_created_at
        end
      end
    end
    class AddDeletedAtIndex < CQL::Migration(10)
      def up
        schema.alter :users do
          create_index :idx_users_deleted_at, [:deleted_at]
        end
      end
    
      def down
        schema.alter :users do
          drop_index :idx_users_deleted_at
        end
      end
    end
    class AddUniquePostTag < CQL::Migration(11)
      def up
        schema.alter :post_tags do
          create_index :idx_post_tags_unique, [:post_id, :tag_id], unique: true
        end
      end
    
      def down
        schema.alter :post_tags do
          drop_index :idx_post_tags_unique
        end
      end
    end
    idx_{table}_{column}          # Simple index
    idx_{table}_{col1}_{col2}     # Composite index
    idx_{table}_{column}_unique   # Unique index (optional suffix)
    migrator.up
    
    # Index should speed up this query
    User.where(email: "john@example.com").first

    Reading and querying records

  • Updating records

  • Deleting records

  • Using query scopes

  • Aggregation and counting

  • hashtag
    Prerequisites

    • Completed Part 3: Models and Relationships

    hashtag
    Create Operations

    hashtag
    Creating a Single Record

    There are several ways to create records:

    hashtag
    Creating with Relationships

    hashtag
    Creating Multiple Records

    hashtag
    Read Operations

    hashtag
    Finding by ID

    hashtag
    Finding by Attributes

    hashtag
    Querying Multiple Records

    hashtag
    Complex Queries

    hashtag
    Querying Through Relationships

    hashtag
    Pagination

    hashtag
    Update Operations

    hashtag
    Updating a Single Record

    hashtag
    Incrementing Values

    hashtag
    Bulk Updates

    hashtag
    Delete Operations

    hashtag
    Deleting a Single Record

    hashtag
    Deleting with Conditions

    hashtag
    Cascading Deletes

    Remember our foreign key setup? Deleting a user cascades to their posts, which cascade to comments:

    hashtag
    Aggregations

    hashtag
    Counting

    hashtag
    Sum, Average, Min, Max

    hashtag
    Grouping and Statistics

    hashtag
    Batch Processing

    For large datasets, process records in batches:

    hashtag
    Practical Examples

    hashtag
    Blog Dashboard Data

    hashtag
    Recent Activity Feed

    hashtag
    Search Posts

    hashtag
    Summary

    In this part, you learned:

    1. Create: new + save, create, and create!

    2. Read: find, find_by, where, order, limit

    3. Update: attribute assignment + save, update!, bulk updates

    4. Delete: delete!, destroy!, bulk deletes

    5. Aggregate: count, sum, avg, min, max

    6. Batch: find_each for large datasets

    hashtag
    Next Steps

    In Part 5: Adding Features, you'll add validations, callbacks, and performance monitoring to complete your blog engine.


    Tutorial Navigation:

    • Part 1: Project Setup

    • Part 2: Database Schema

    • Part 3: Models and Relationships

    • Part 4: CRUD Operations (current)

    hashtag
    Basic Migration

    hashtag
    Migration Number

    The number in CQL::Migration(N) must be unique. Use sequential numbers or timestamps:

    hashtag
    Create Table

    hashtag
    Add Columns

    hashtag
    Add Index

    hashtag
    Add Foreign Key

    hashtag
    Rename Column

    hashtag
    Change Column Type

    hashtag
    Irreversible Migration

    Some migrations can't be reversed:

    hashtag
    Verify Migration

    Create a simple test:

    hashtag
    Related

    • Run Migrations

    • Rollback Migrations

    • Add Columns

    hashtag
    Creating Tables

    hashtag
    schema.table

    Defines a new table structure:

    Key methods:

    • schema.table :name do ... end - Define table structure

    • schema.table_name.create! - Execute CREATE TABLE

    • schema.table_name.drop! - Execute DROP TABLE

    hashtag
    Column Methods

    hashtag
    primary

    Defines the primary key column:

    Parameters:

    • name - Column name (Symbol)

    • type - Crystal type (Int32, Int64, String)

    • auto_increment - Enable auto-increment (default: true)

    hashtag
    column

    Adds a column to the table:

    Options:

    Option
    Type
    Description

    null

    Bool

    Allow NULL values (default: false)

    default

    T

    Default value

    unique

    Bool

    Add unique constraint

    index

    Bool

    Create index on column

    hashtag
    Type-Specific Column Methods

    CQL provides helper methods for common column types:

    hashtag
    Supported Types

    Crystal Type
    SQL Type

    Int32

    INTEGER

    Int64

    BIGINT

    Float32

    FLOAT

    Float64

    DOUBLE PRECISION

    String

    VARCHAR / TEXT

    Bool

    BOOLEAN

    hashtag
    timestamps

    Adds created_at and updated_at columns:

    Both columns are timestamps with current timestamp as default.

    hashtag
    Index Methods

    hashtag
    index

    Creates an index within a table definition:

    hashtag
    create_index / drop_index

    Manage indexes in alter operations:

    hashtag
    Foreign Key Methods

    hashtag
    foreign_key

    Creates a foreign key constraint:

    Parameters:

    Parameter
    Type
    Description

    columns

    Array(Symbol)

    Local column(s)

    references

    Symbol

    Target table

    references_columns

    Array(Symbol)

    Target column(s)

    on_delete

    Symbol

    :cascade, :restrict, :set_null, :no_action

    hashtag
    Composite Foreign Keys

    hashtag
    Altering Tables

    hashtag
    schema.alter

    Modifies an existing table:

    hashtag
    Alter Operations

    Method
    Description

    add_column :name, Type, options

    Add a new column

    drop_column :name

    Remove a column

    rename_column :old, :new

    Rename a column

    change_column :name, NewType

    Change column type

    create_index :name, [:cols]

    Create an index

    drop_index :name

    Drop an index

    hashtag
    Alter Examples

    hashtag
    Complete Example

    hashtag
    Migration Execution

    hashtag
    See Also

    • Create a Migration

    • Run Migrations

    • Add Columns

    A model class represents a database table

  • A model instance represents a row in that table

  • The model knows how to save itself

  • Business logic lives inside the model

  • hashtag
    Active Record in CQL

    hashtag
    Key Characteristics

    hashtag
    1. Self-Aware Persistence

    Models know how to persist themselves:

    hashtag
    2. Class Methods for Queries

    The model class provides query methods:

    hashtag
    3. Built-in Validations

    Validation logic lives in the model:

    hashtag
    4. Lifecycle Callbacks

    Models respond to persistence events:

    hashtag
    Benefits

    hashtag
    Simplicity

    Active Record is intuitive. The code reads naturally:

    hashtag
    Rapid Development

    Everything you need is in one place:

    hashtag
    Discoverability

    Looking at a model tells you everything about that entity: its data, validations, relationships, and behavior.

    hashtag
    Trade-offs

    hashtag
    Coupling

    Active Record couples your domain logic to the database structure. Changes to the database schema may require changes to business logic.

    hashtag
    Testing

    Models are harder to test in isolation because they depend on the database.

    hashtag
    Complexity

    For complex domains, models can become bloated with too much logic.

    hashtag
    When Active Record Works Best

    • CRUD-heavy applications

    • Rapid prototyping

    • Small to medium applications

    • When database schema closely matches domain model

    hashtag
    When to Consider Alternatives

    • Complex business logic

    • Multiple data sources

    • Need for database-agnostic domain

    • Heavy testing requirements

    hashtag
    Comparison with Other Patterns

    Pattern
    Data
    Persistence Logic
    Business Logic

    Active Record

    In model

    In model

    In model

    Repository

    In model

    In repository

    In model

    Data Mapper

    In entity

    In mapper

    hashtag
    Example: Complex Active Record Model

    This model:

    • Defines data (properties)

    • Handles its own persistence (save, validations)

    • Contains business logic (pay!, ship!)

    • Manages relationships (belongs_to, has_many)

    • Responds to lifecycle events (callbacks)

    All in one cohesive unit - that's the Active Record pattern.

    If you find a bug, please open an issuearrow-up-right with:
    • A clear title and description

    • Steps to reproduce the bug

    • Expected vs actual behavior

    • Your environment (CQL version, Crystal version, OS, database)

    hashtag
    Suggest Features

    Feature requests are welcome! Open an issue describing:

    • The problem you're trying to solve

    • Your proposed solution

    • Any alternatives you've considered

    hashtag
    Submit Pull Requests

    1. Fork the repository

    2. Create a branch

    3. Make your changes

      • Follow Crystal style guidelines

      • Add tests for new functionality

      • Update documentation if needed

    4. Run tests

    5. Format your code

    6. Commit your changes

    7. Push and open a PR

    hashtag
    Code Guidelines

    hashtag
    Style

    • Follow Crystal's standard style guide

    • Use crystal tool format before committing

    • Write clear, descriptive names for methods and variables

    • Add comments for complex logic

    hashtag
    Testing

    • Write tests for all new features

    • Ensure existing tests pass

    • Test edge cases and error conditions

    hashtag
    Documentation

    • Update documentation for public API changes

    • Add code examples for new features

    • Use clear, concise language

    hashtag
    Pull Request Process

    1. Update the README.md with details of changes if applicable

    2. Update the documentation with any new features

    3. The PR will be merged once it has been reviewed and approved

    hashtag
    Development Setup

    1. Clone the repository:

    2. Install dependencies:

    3. Run tests:

    hashtag
    Questions?

    If you have questions about contributing, open a discussion on GitHub or ask in the issue tracker.

    Thank you for contributing!

    # Method 1: new + save
    user = User.new(username: "john", email: "john@example.com")
    if user.save
      puts "User created with ID: #{user.id}"
    else
      puts "Failed to create user"
    end
    
    # Method 2: create! (raises on failure)
    user = User.create!(
      username: "jane",
      email: "jane@example.com",
      first_name: "Jane"
    )
    
    # Method 3: create (returns the record, check persisted?)
    user = User.create(username: "bob", email: "bob@example.com")
    if user.persisted?
      puts "User created"
    end
    # Create a user first
    author = User.create!(username: "alice", email: "alice@example.com")
    
    # Create a category
    tech = Category.create!(name: "Technology")
    
    # Create a post with relationships
    post = Post.create!(
      title: "My First Post",
      content: "Hello, world! This is my first blog post.",
      user_id: author.id.not_nil!,
      category_id: tech.id,
      published: true
    )
    
    # Create a comment on the post
    comment = Comment.create!(
      content: "Welcome to blogging!",
      post_id: post.id.not_nil!,
      user_id: author.id
    )
    # Create sample categories
    categories = ["Technology", "Lifestyle", "Travel", "Food"].map do |name|
      Category.create!(name: name)
    end
    
    puts "Created #{categories.size} categories"
    # Find returns nil if not found
    user = User.find(1)
    if user
      puts "Found: #{user.username}"
    else
      puts "User not found"
    end
    
    # Find! raises if not found
    begin
      user = User.find!(999)
    rescue ex
      puts "User not found: #{ex.message}"
    end
    # Find first matching record
    user = User.find_by(email: "alice@example.com")
    puts "Found user: #{user.try(&.username)}"
    
    # Find with multiple conditions
    post = Post.find_by(published: true, user_id: 1)
    # Get all records
    all_users = User.all
    puts "Total users: #{all_users.size}"
    
    # Filter with where
    active_users = User.where(active: true).all
    published_posts = Post.where(published: true).all
    
    # Chain conditions
    recent_published = Post
      .where(published: true)
      .order(created_at: :desc)
      .limit(10)
      .all
    
    # Get first/last
    first_user = User.first
    last_post = Post.last
    # Multiple conditions
    posts = Post
      .where(published: true)
      .where(user_id: author.id)
      .order(views_count: :desc)
      .all
    
    # Using blocks for complex conditions
    popular_posts = Post.where { views_count > 100 }.all
    recent_posts = Post.where { created_at > 1.week.ago }.all
    
    # Combining conditions
    featured = Post
      .where(published: true)
      .where { views_count > 50 }
      .order(created_at: :desc)
      .limit(5)
      .all
    # Get user's posts
    user = User.find(1)
    user_posts = user.posts.all
    
    # Filter user's posts
    published_user_posts = user.posts.where(published: true).all
    
    # Get post's comments
    post = Post.find(1)
    comments = post.comments.all
    
    # Get posts in a category
    category = Category.find_by(slug: "technology")
    tech_posts = category.posts.where(published: true).all if category
    # Simple pagination
    page = 1
    per_page = 10
    
    posts = Post
      .where(published: true)
      .order(created_at: :desc)
      .limit(per_page)
      .offset((page - 1) * per_page)
      .all
    
    # Get total for pagination info
    total = Post.where(published: true).count
    total_pages = (total / per_page.to_f).ceil.to_i
    # Method 1: Change attributes and save
    user = User.find(1)
    if user
      user.first_name = "Alice"
      user.last_name = "Smith"
      user.save
      puts "User updated"
    end
    
    # Method 2: update! with attributes
    post = Post.find(1)
    post.try(&.update!(published: true, views_count: 100_i64))
    
    # Method 3: Using update on query
    Post.where(id: 1).update!(published: true)
    # Increment view count
    post = Post.find(1)
    if post
      post.views_count += 1
      post.save
    end
    
    # Or use a custom method (defined in model)
    post.try(&.increment_views!)
    # Publish all draft posts by a user
    Post.where(user_id: 1, published: false).update!(published: true)
    
    # Deactivate users who haven't posted
    # (This would require a more complex query in practice)
    User.where(active: true).each do |user|
      if user.posts.count == 0
        user.active = false
        user.save
      end
    end
    # Method 1: Find and delete
    user = User.find(1)
    user.try(&.delete!)
    puts "User deleted"
    
    # Method 2: Using destroy (triggers callbacks)
    post = Post.find(1)
    post.try(&.destroy!)
    # Delete all comments on a post
    Comment.where(post_id: 1).delete!
    
    # Delete all unpublished posts
    Post.where(published: false).delete!
    
    # Delete old anonymous comments
    Comment.where(user_id: nil).where { created_at < 1.year.ago }.delete!
    # This deletes the user AND all their posts AND all comments on those posts
    user = User.find(1)
    user.try(&.delete!)
    # Count all
    total_users = User.count
    total_posts = Post.count
    
    # Count with conditions
    published_count = Post.where(published: true).count
    active_users = User.where(active: true).count
    
    # Count through relationships
    user = User.find(1)
    post_count = user.posts.count if user
    # Total views across all published posts
    total_views = Post.where(published: true).sum(:views_count)
    
    # Average views
    avg_views = Post.where(published: true).avg(:views_count)
    
    # Most views
    max_views = Post.where(published: true).max(:views_count)
    
    # Least views
    min_views = Post.where(published: true).min(:views_count)
    # Posts per category
    Category.all.each do |category|
      count = category.posts.where(published: true).count
      puts "#{category.name}: #{count} posts"
    end
    
    # Most active authors
    User.all.sort_by { |u| -u.posts.count }.first(5).each do |user|
      puts "#{user.display_name}: #{user.posts.count} posts"
    end
    # Process all posts in batches
    Post.find_each(batch_size: 100) do |post|
      # Process each post
      puts "Processing: #{post.title}"
    end
    def dashboard_stats
      {
        total_users: User.count,
        active_users: User.where(active: true).count,
        total_posts: Post.count,
        published_posts: Post.where(published: true).count,
        draft_posts: Post.where(published: false).count,
        total_comments: Comment.count,
        total_views: Post.sum(:views_count)
      }
    end
    
    stats = dashboard_stats
    puts "Users: #{stats[:total_users]} (#{stats[:active_users]} active)"
    puts "Posts: #{stats[:published_posts]} published, #{stats[:draft_posts]} drafts"
    puts "Total views: #{stats[:total_views]}"
    def recent_activity(limit = 10)
      posts = Post.where(published: true)
                  .order(created_at: :desc)
                  .limit(limit)
                  .all
    
      posts.map do |post|
        {
          type: "post",
          title: post.title,
          author: post.user.try(&.display_name) || "Unknown",
          date: post.created_at
        }
      end
    end
    def search_posts(query : String)
      Post.where(published: true)
          .where { title.like("%#{query}%") | content.like("%#{query}%") }
          .order(views_count: :desc)
          .all
    end
    
    results = search_posts("crystal")
    puts "Found #{results.size} posts matching 'crystal'"
    migrations/
    ├── 001_create_users.cr
    ├── 002_create_posts.cr
    └── 003_add_email_index.cr
    # migrations/001_create_users.cr
    class CreateUsers < CQL::Migration(1)
      def up
        schema.table :users do
          primary :id, Int64, auto_increment: true
          column :name, String, null: false
          column :email, String, null: false
          timestamps
        end
    
        schema.users.create!
      end
    
      def down
        schema.users.drop!
      end
    end
    class CreateUsers < CQL::Migration(1)              # Sequential
    class CreatePosts < CQL::Migration(2)
    class AddIndexes < CQL::Migration(20250125120000)  # Timestamp format
    def up
      schema.table :posts do
        primary :id, Int64, auto_increment: true
        column :title, String, null: false
        column :body, String, null: false
        column :published, Bool, default: false
        column :user_id, Int64, null: false
        timestamps
    
        foreign_key [:user_id], references: :users, references_columns: [:id]
        index [:user_id]
        index [:published]
      end
    
      schema.posts.create!
    end
    
    def down
      schema.posts.drop!
    end
    class AddAvatarToUsers < CQL::Migration(4)
      def up
        schema.alter :users do
          add_column :avatar_url, String, null: true
          add_column :bio, String, null: true
        end
      end
    
      def down
        schema.alter :users do
          drop_column :avatar_url
          drop_column :bio
        end
      end
    end
    class AddEmailIndex < CQL::Migration(5)
      def up
        schema.alter :users do
          create_index :idx_users_email, [:email], unique: true
        end
      end
    
      def down
        schema.alter :users do
          drop_index :idx_users_email
        end
      end
    end
    class AddPostsForeignKey < CQL::Migration(6)
      def up
        schema.alter :posts do
          foreign_key [:user_id], references: :users, references_columns: [:id], on_delete: :cascade
        end
      end
    
      def down
        schema.alter :posts do
          drop_foreign_key :fk_posts_user_id
        end
      end
    end
    class RenameUserName < CQL::Migration(7)
      def up
        schema.alter :users do
          rename_column :name, :full_name
        end
      end
    
      def down
        schema.alter :users do
          rename_column :full_name, :name
        end
      end
    end
    class ChangeViewsCount < CQL::Migration(8)
      def up
        schema.alter :posts do
          change_column :views_count, Int64
        end
      end
    
      def down
        schema.alter :posts do
          change_column :views_count, Int32
        end
      end
    end
    class RemoveOldColumn < CQL::Migration(9)
      def up
        schema.alter :users do
          drop_column :legacy_field
        end
      end
    
      def down
        raise "Cannot reverse: data would be lost"
      end
    end
    # Test migration
    MyDB.init
    migrator = MyDB.migrator
    migrator.up
    
    # Check table exists
    User.count  # Should not raise
    class CreateUsers < CQL::Migration(20250125001401)
      def up
        # Apply changes
      end
    
      def down
        # Rollback changes
      end
    end
    class CreateUsers < CQL::Migration(1)
      def up
        schema.table :users do
          primary :id, Int64, auto_increment: true
          column :name, String, null: false
          column :email, String, null: false
          column :active, Bool, default: true
          timestamps
    
          index [:email], unique: true
        end
    
        schema.users.create!
      end
    
      def down
        schema.users.drop!
      end
    end
    primary :id, Int64                        # Default auto_increment: true
    primary :id, Int32, auto_increment: true
    primary :uuid, String, auto_increment: false
    column :name, String                      # Required string
    column :bio, String, null: true           # Nullable
    column :active, Bool, default: true       # With default
    column :email, String, null: false, unique: true
    column :score, Int32, default: 0, index: true
    # String types
    varchar :name, size: 255
    text :description
    
    # Numeric types
    integer :age, null: false, default: 18
    bigint :balance, default: 0
    float :price, default: 0.0
    double :rating
    real :measurement
    
    # Other types
    boolean :active, default: true
    timestamp :created_at
    date :birthday
    json :metadata
    schema.table :posts do
      primary :id, Int64
      column :title, String
      timestamps
    end
    schema.table :users do
      column :email, String
      column :first_name, String
      column :last_name, String
    
      index [:email], unique: true            # Unique index
      index [:email]                          # Regular index
      index [:first_name, :last_name]         # Composite index
    end
    schema.alter :users do
      create_index :email_idx, [:email], unique: true
      drop_index :email_idx
    end
    schema.table :posts do
      primary :id, Int64
      column :user_id, Int64, null: false
      column :category_id, Int64, null: true
    
      foreign_key [:user_id], references: :users, references_columns: [:id]
      foreign_key [:category_id], references: :categories, references_columns: [:id]
    end
    foreign_key [:order_id, :product_id],
      references: :order_items,
      references_columns: [:o_id, :p_id]
    class AddPhoneToUsers < CQL::Migration(2)
      def up
        schema.alter :users do
          add_column :phone, String, null: true
          add_column :age, Int32, null: true, default: 18
        end
      end
    
      def down
        schema.alter :users do
          drop_column :phone
          drop_column :age
        end
      end
    end
    schema.alter :users do
      # Columns
      add_column :phone, String, null: true
      drop_column :legacy_field
      rename_column :email, :user_email
      change_column :age, String
    
      # Indexes
      create_index :phone_idx, [:phone]
      drop_index :old_idx
    
      # Foreign keys
      foreign_key [:department_id], references: :departments, on_delete: :cascade
      drop_foreign_key :fk_old_reference
    end
    class CreateBlogSchema < CQL::Migration(1)
      def up
        # Create users table
        schema.table :users do
          primary :id, Int64, auto_increment: true
          column :email, String, null: false
          column :username, String, null: false
          column :role, String, default: "member"
          timestamps
    
          index [:email], unique: true
          index [:username], unique: true
        end
        schema.users.create!
    
        # Create posts table
        schema.table :posts do
          primary :id, Int64, auto_increment: true
          column :user_id, Int64, null: false
          column :title, String, null: false
          column :body, String, null: false
          column :published, Bool, default: false
          column :views_count, Int64, default: 0
          timestamps
    
          foreign_key [:user_id], references: :users, references_columns: [:id]
          index [:user_id]
          index [:published]
        end
        schema.posts.create!
      end
    
      def down
        schema.posts.drop!
        schema.users.drop!
      end
    end
    migrator = schema.migrator
    
    # Apply migrations
    migrator.up                    # Apply all pending
    migrator.up(1)                 # Apply 1 migration
    
    # Rollback migrations
    migrator.down                  # Rollback all
    migrator.down(1)               # Rollback 1 migration
    migrator.rollback              # Alias for down(1)
    
    # Other operations
    migrator.redo                  # Rollback and reapply last
    migrator.up_to(version)        # Apply up to specific version
    migrator.down_to(version)      # Rollback to specific version
    
    # Status
    migrator.applied_migrations    # List applied migrations
    migrator.pending_migrations    # List pending migrations
    migrator.last                  # Get last applied migration
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property active : Bool = true
    
      # Business logic in the model
      def activate!
        @active = true
        save
      end
    
      def deactivate!
        @active = false
        save
      end
    end
    
    # Usage - the model handles its own persistence
    user = User.new("John", "john@example.com")
    user.save      # Model saves itself
    user.activate! # Business logic + persistence together
    user = User.new("John", "john@example.com")
    user.save   # INSERT
    user.name = "Jane"
    user.save   # UPDATE
    user.delete! # DELETE
    User.find(1)
    User.where(active: true)
    User.count
    User.all
    struct User
      validate :email, presence: true, match: /@/
      validate :name, presence: true
    end
    
    user = User.new("", "invalid")
    user.valid?  # false
    user.save    # won't save, returns false
    struct User
      before_save :normalize_email
      after_create :send_welcome_email
    
      private def normalize_email
        @email = @email.downcase.strip
        true
      end
    end
    # Create a user, save it, find it, update it
    user = User.create!(name: "John", email: "john@example.com")
    found = User.find(user.id)
    found.name = "Jane"
    found.save
    struct Post
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :posts
    
      property id : Int64?
      property title : String
      property body : String
      property user_id : Int64
    
      belongs_to :user, User, :user_id
      has_many :comments, Comment, :post_id
    
      validate :title, presence: true
    
      before_save :update_slug
    end
    struct Order
      include CQL::ActiveRecord::Model(Int64)
      db_context MyDB, :orders
    
      property id : Int64?
      property user_id : Int64
      property status : String = "pending"
      property total : BigDecimal = BigDecimal.new(0)
    
      belongs_to :user, User, :user_id
      has_many :order_items, OrderItem, :order_id
    
      validate :status, in: ["pending", "paid", "shipped", "completed", "cancelled"]
    
      before_save :calculate_total
      after_save :notify_status_change
    
      def pay!
        @status = "paid"
        save
      end
    
      def ship!
        raise "Cannot ship unpaid order" unless status == "paid"
        @status = "shipped"
        save
      end
    
      private def calculate_total
        @total = order_items.all.sum(&.subtotal)
        true
      end
    
      private def notify_status_change
        # Send notification
        true
      end
    end
    git checkout -b feature/my-new-feature
    git clone https://github.com/azutoolkit/cql.git
    cd cql
    shards install
    crystal spec

    size

    Int32

    Column size (for VARCHAR)

    as

    String

    SQL column alias

    Time

    TIMESTAMP

    Date

    DATE

    JSON::Any

    JSON / JSONB

    on_update

    Symbol

    :cascade, :restrict, :set_null, :no_action

    name

    String

    Optional constraint name

    foreign_key [:cols], references: :table

    Add foreign key

    drop_foreign_key :name

    Drop foreign key

    rename_table :new_name

    Rename the table

    unique_constraint [:cols]

    Add unique constraint

    check_constraint "expression"

    Add check constraint

    In entity

    Part 5: Adding Features
    Migration DSL Reference
    crystal spec
    crystal tool format
    git commit -m "Add feature: description"
    git push origin feature/my-new-feature