Schema Management (Sculpt CLI)

Master ObjectQuel's Sculpt tool for comprehensive database schema management, from interactive entity generation to automated migrations and reverse engineering from existing databases.

Introduction to Sculpt

Sculpt is ObjectQuel's powerful command-line tool that bridges the gap between your entity definitions and database schema. It provides interactive entity generation, automated migration creation, and seamless schema synchronization:

# Core Sculpt commands for schema management
php bin/sculpt make:entity              # Interactive entity creation
php bin/sculpt make:entity-from-table   # Generate entity from existing table
php bin/sculpt make:migrations          # Create migrations from entity changes
php bin/sculpt quel:migrate             # Apply migrations to database
php bin/sculpt quel:migrate --rollback  # Rollback recent migrations

Key Benefits:

  • Interactive entity generation with guided prompts
  • Automatic migration generation from entity annotations
  • Reverse engineering from existing database schemas
  • Version-controlled schema changes
  • Database-agnostic migration system

Interactive Entity Generation

The make:entity command provides an interactive wizard for creating properly structured entities:

Basic Entity Creation

$ php bin/sculpt make:entity

 Entity name (e.g., Product, User, Order):
 > Product

 Table name [products]:
 > products

 Add properties to your entity:

 Property name (press <return> to stop adding properties):
 > name

 Property type [string]:
 > string

 Property length [255]:
 > 255

 Can this property be null? [no]:
 > no

 Property name (press <return> to stop adding properties):
 > price

 Property type [string]:
 > decimal

 Property precision [10,2]:
 > 10,2

 Can this property be null? [no]:
 > no

 Property name (press <return> to stop adding properties):
 > description

 Property type [string]:
 > text

 Can this property be null? [no]:
 > yes

 Property name (press <return> to stop adding properties):
 >

 Add relationships to your entity:

 Relationship type (OneToMany, ManyToOne, OneToOne, or press <return> to skip):
 > ManyToOne

 Related entity (e.g., Category, User):
 > Category

 Property name for this relationship [category]:
 > category

 Relationship type (OneToMany, ManyToOne, OneToOne, or press <return> to skip):
 >

 Success! Created: src/Entity/ProductEntity.php

Generated Entity Structure

<?php
namespace App\Entity;

use Quellabs\ObjectQuel\Annotations\Orm;

/**
 * @Orm\Table(name="products")
 */
class ProductEntity {

    /**
     * @Orm\Column(name="product_id", type="integer", primary_key=true)
     * @Orm\PrimaryKeyStrategy(strategy="identity")
     */
    private ?int $productId = null;

    /**
     * @Orm\Column(name="name", type="string", limit=255)
     */
    private string $name;

    /**
     * @Orm\Column(name="price", type="decimal", limit="10,2")
     */
    private float $price;

    /**
     * @Orm\Column(name="description", type="text", nullable=true)
     */
    private ?string $description = null;

    /**
     * @Orm\Column(name="category_id", type="integer")
     */
    private int $categoryId;

    /**
     * @Orm\ManyToOne(targetEntity="CategoryEntity", inversedBy="products")
     */
    private CategoryEntity $category;

    // Generated getters and setters
    public function getProductId(): ?int { return $this->productId; }
    public function getName(): string { return $this->name; }
    public function setName(string $name): void { $this->name = $name; }
    public function getPrice(): float { return $this->price; }
    public function setPrice(float $price): void { $this->price = $price; }
    public function getDescription(): ?string { return $this->description; }
    public function setDescription(?string $description): void { $this->description = $description; }
    public function getCategory(): CategoryEntity { return $this->category; }
    public function setCategory(CategoryEntity $category): void {
        $this->category = $category;
        $this->categoryId = $category->getCategoryId();
    }
}

Reverse Engineering with make:entity-from-table

Generate entities from existing database tables with full annotation support:

Table-to-Entity Generation

$ php bin/sculpt make:entity-from-table

 Available tables:
 [0] categories
 [1] products
 [2] users
 [3] orders
 [4] order_items

 Select table to generate entity from:
 > 1

 Entity name [ProductEntity]:
 > ProductEntity

 Namespace [App\Entity]:
 > App\Entity

 Generate relationships? [yes]:
 > yes

 Success! Created: src/Entity/ProductEntity.php

 Detected relationships:
 - ManyToOne: category_id -> CategoryEntity
 - OneToMany: Referenced by OrderItemEntity.product_id

Generated Entity with Detected Relationships

<?php
namespace App\Entity;

use Quellabs\ObjectQuel\Annotations\Orm;
use Quellabs\ObjectQuel\Collections\EntityCollection;

/**
 * @Orm\Table(name="products")
 * @Orm\Index(name="idx_category_id", columns={"category_id"})
 */
class ProductEntity {

    /**
     * @Orm\Column(name="product_id", type="integer", primary_key=true)
     * @Orm\PrimaryKeyStrategy(strategy="identity")
     */
    private ?int $productId = null;

    /**
     * @Orm\Column(name="name", type="string", limit=255)
     */
    private string $name;

    /**
     * @Orm\Column(name="category_id", type="integer")
     */
    private int $categoryId;

    /**
     * @Orm\Column(name="created_at", type="datetime")
     */
    private \DateTime $createdAt;

    /**
     * Auto-detected relationship based on foreign key
     * @Orm\ManyToOne(targetEntity="CategoryEntity", inversedBy="products")
     */
    private CategoryEntity $category;

    /**
     * Auto-detected inverse relationship
     * @Orm\OneToMany(targetEntity="OrderItemEntity", mappedBy="productId")
     */
    public EntityCollection $orderItems;

    public function __construct() {
        $this->orderItems = new EntityCollection();
    }

    // Generated getters and setters...
}

Automated Migration Generation

The make:migrations command analyzes your entity definitions and generates database migrations automatically:

Creating Migrations from Entity Changes

$ php bin/sculpt make:migrations

 Analyzing entity definitions...
 Comparing with current database schema...

 Changes detected:
 ✓ New table: products
 ✓ New table: categories
 ✓ New index: products.idx_category_id
 ✓ Modified column: users.email (added unique constraint)
 ✓ Dropped column: orders.legacy_status

 Generate migration? [yes]:
 > yes

 Migration name [EntitySchemaMigration]:
 > CreateProductsAndCategories

 Success! Created: database/migrations/20250603_145623_CreateProductsAndCategories.php

Generated Migration File

<?php
use Phinx\Migration\AbstractMigration;

class CreateProductsAndCategories20250603145623 extends AbstractMigration {

    /**
     * This migration was automatically generated by ObjectQuel Sculpt
     *
     * Based on entity definitions:
     * - App\Entity\ProductEntity
     * - App\Entity\CategoryEntity
     */

    public function up(): void {
        // Create categories table
        $this->table('categories', ['id' => false, 'primary_key' => ['category_id']])
            ->addColumn('category_id', 'integer', [
                'limit' => 11,
                'null' => false,
                'signed' => false,
                'identity' => true
            ])
            ->addColumn('name', 'string', [
                'limit' => 255,
                'null' => false
            ])
            ->addColumn('description', 'text', [
                'null' => true
            ])
            ->addColumn('created_at', 'datetime', [
                'null' => false
            ])
            ->create();

        // Create products table
        $this->table('products', ['id' => false, 'primary_key' => ['product_id']])
            ->addColumn('product_id', 'integer', [
                'limit' => 11,
                'null' => false,
                'signed' => false,
                'identity' => true
            ])
            ->addColumn('name', 'string', [
                'limit' => 255,
                'null' => false
            ])
            ->addColumn('price', 'decimal', [
                'precision' => 10,
                'scale' => 2,
                'null' => false
            ])
            ->addColumn('category_id', 'integer', [
                'limit' => 11,
                'null' => false
            ])
            ->addColumn('created_at', 'datetime', [
                'null' => false
            ])
            ->addIndex(['category_id'], [
                'name' => 'idx_category_id'
            ])
            ->addForeignKey('category_id', 'categories', 'category_id', [
                'delete' => 'RESTRICT',
                'update' => 'CASCADE'
            ])
            ->create();

        // Modify existing users table
        $this->table('users')
            ->addIndex(['email'], [
                'name' => 'idx_unique_email',
                'unique' => true
            ])
            ->removeColumn('legacy_status')
            ->save();
    }

    public function down(): void {
        $this->table('products')->drop()->save();
        $this->table('categories')->drop()->save();

        $this->table('users')
            ->removeIndex(['email'])
            ->addColumn('legacy_status', 'string', ['limit' => 20, 'null' => true])
            ->save();
    }
}

Migration Execution and Management

Applying Migrations

# Apply all pending migrations
$ php bin/sculpt quel:migrate

 Pending migrations:
 ✓ 20250603_145623_CreateProductsAndCategories.php
 ✓ 20250603_150112_AddProductIndexes.php

 Apply migrations? [yes]:
 > yes

 == 20250603145623 CreateProductsAndCategories: migrating
 == 20250603145623 CreateProductsAndCategories: migrated (0.0234s)

 == 20250603150112 AddProductIndexes: migrating
 == 20250603150112 AddProductIndexes: migrated (0.0156s)

 All Done. Took 0.0390s

Migration Rollback

# Rollback the last migration
$ php bin/sculpt quel:migrate --rollback

 Last migration: 20250603_150112_AddProductIndexes.php

 Rollback this migration? [yes]:
 > yes

 == 20250603150112 AddProductIndexes: reverting
 == 20250603150112 AddProductIndexes: reverted (0.0123s)

# Rollback multiple migrations
$ php bin/sculpt quel:migrate --rollback --steps=3

 Rollback 3 migrations? [yes]:
 > yes

 == 20250603150112 AddProductIndexes: reverting
 == 20250603150112 AddProductIndexes: reverted (0.0123s)
 == 20250603145623 CreateProductsAndCategories: reverting
 == 20250603145623 CreateProductsAndCategories: reverted (0.0234s)
 == 20250603140055 CreateUsersTable: reverting
 == 20250603140055 CreateUsersTable: reverted (0.0198s)

Migration Status and Information

# Check migration status
$ php bin/sculpt quel:migrate --status

 Status   Migration ID    Migration Name
 ----------------------------------------------
    up    20250603140055  CreateUsersTable
    up    20250603145623  CreateProductsAndCategories
  down    20250603150112  AddProductIndexes
  down    20250603151034  AddProductReviews

# Get detailed help
$ php bin/sculpt help quel:migrate

Description:
  Execute database migrations

Usage:
  quel:migrate [options]

Options:
      --rollback        Rollback the last migration
      --steps=STEPS     Number of migrations to rollback [default: 1]
      --dry-run         Show what would be executed without running
      --status          Show migration status

Advanced Schema Management

Complex Entity Relationships

$ php bin/sculpt make:entity

 Entity name:
 > OrderItem

 Add relationships:

 Relationship type:
 > ManyToOne

 Related entity:
 > Order

 Relationship type:
 > ManyToOne

 Related entity:
 > Product

 Add indexes for performance? [yes]:
 > yes

 Success! Created OrderItemEntity with optimized indexes

Generated Entity with Performance Optimizations

/**
 * @Orm\Table(name="order_items")
 * @Orm\Index(name="idx_order_performance", columns={"order_id", "product_id"})
 * @Orm\Index(name="idx_product_sales", columns={"product_id", "quantity", "unit_price"})
 * @Orm\UniqueIndex(name="unq_order_product", columns={"order_id", "product_id"})
 */
class OrderItemEntity {

    /**
     * @Orm\Column(name="order_item_id", type="integer", primary_key=true)
     * @Orm\PrimaryKeyStrategy(strategy="identity")
     */
    private ?int $orderItemId = null;

    /**
     * @Orm\Column(name="order_id", type="integer")
     */
    private int $orderId;

    /**
     * @Orm\Column(name="product_id", type="integer")
     */
    private int $productId;

    /**
     * @Orm\Column(name="quantity", type="integer")
     */
    private int $quantity;

    /**
     * @Orm\Column(name="unit_price", type="decimal", limit="10,2")
     */
    private float $unitPrice;

    /**
     * @Orm\ManyToOne(targetEntity="OrderEntity", inversedBy="items")
     * @Orm\RequiredRelation
     */
    private OrderEntity $order;

    /**
     * @Orm\ManyToOne(targetEntity="ProductEntity", inversedBy="orderItems")
     * @Orm\RequiredRelation
     */
    private ProductEntity $product;
}

Development Workflow Integration

Typical Development Cycle

# 1. Create new entity
$ php bin/sculpt make:entity
# Interactive creation of CustomerEntity

# 2. Generate migration for new entity
$ php bin/sculpt make:migrations
# Analyzes CustomerEntity, creates migration

# 3. Apply migration to development database
$ php bin/sculpt quel:migrate
# Creates customers table with proper schema

# 4. Modify entity (add properties, relationships)
# Edit src/Entity/CustomerEntity.php manually

# 5. Generate migration for changes
$ php bin/sculpt make:migrations
# Detects changes, creates alter table migration

# 6. Apply changes
$ php bin/sculpt quel:migrate
# Updates database schema to match entity

# 7. Test in development
# Run application tests to verify changes

# 8. Commit to version control
$ git add .
$ git commit -m "Add Customer entity with contact information"

# 9. Deploy to staging/production
$ php bin/sculpt quel:migrate
# Applies same migrations to production database

Team Collaboration

# Developer A creates new feature
$ php bin/sculpt make:entity
$ php bin/sculpt make:migrations
$ git push

# Developer B pulls changes
$ git pull
$ php bin/sculpt quel:migrate  # Applies A's migrations locally

# Both developers stay synchronized with same schema

Production Deployment Strategies

Safe Production Migrations

# Review pending migrations before applying
$ php bin/sculpt quel:migrate --status

 Status   Migration ID    Migration Name
 ----------------------------------------------
    up    20250603140055  CreateUsersTable
    up    20250603145623  CreateProductsAndCategories
  down    20250603150112  AddCustomerIndexes
  down    20250603151034  OptimizeProductQueries

# Use dry-run to preview changes
$ php bin/sculpt quel:migrate --dry-run

 Would execute:
 == 20250603150112 AddCustomerIndexes: migrating
 CREATE INDEX idx_customer_email ON customers (email);
 CREATE INDEX idx_customer_region ON customers (region, status);

 == 20250603151034 OptimizeProductQueries: migrating
 ALTER TABLE products ADD COLUMN search_vector TEXT;
 CREATE INDEX idx_product_search ON products (search_vector);

# Apply with confidence
$ php bin/sculpt quel:migrate

Rollback Strategy

# Backup before major migrations
$ mysqldump database > backup_before_migration.sql

# Apply migration
$ php bin/sculpt quel:migrate

# If issues occur, rollback immediately
$ php bin/sculpt quel:migrate --rollback

# Or restore from backup if rollback insufficient
$ mysql database < backup_before_migration.sql

Best Practices and Tips

Entity Design Guidelines:

  • Use descriptive entity and property names
  • Define appropriate relationships during entity creation
  • Add indexes for frequently queried columns
  • Use nullable types correctly for optional properties
  • Consider data validation constraints

Migration Management:

  • Generate migrations frequently during development
  • Review generated migrations before applying
  • Use descriptive migration names
  • Test migrations on staging before production
  • Keep migration files in version control

Performance Considerations:

  • Add indexes during entity creation, not as afterthought
  • Use composite indexes for multi-column queries
  • Consider unique constraints for data integrity
  • Plan for schema evolution from the beginning
  • Monitor query performance and adjust indexes accordingly

Sculpt provides a complete schema management solution that keeps your database schema synchronized with your entity definitions while maintaining full version control and deployment flexibility.