You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The documentation for sql-ddl.md (available at https://docs.laminas.dev/laminas-db/sql-ddl/ for reference) shows some detail but does not show the full scope of these classes and methods.
Looking to vastly increase the visibility of this class to assist in its uptake.
I have created a draft patch for review, open to comment please.
PhpDb\Sql\Ddl is a sub-component of PhpDb\Sql that provides an object-oriented abstraction for DDL (Data Definition Language) SQL statements. It allows you to programmatically define database schemas in a platform-agnostic way.
What is DDL Abstraction?
DDL abstraction enables you to:
Create, alter, and drop tables using PHP objects instead of raw SQL
Generate platform-specific SQL automatically for different databases
When to Use DDL Abstraction
Use PhpDb\Sql\Ddl when you need to:
Create database migrations programmatically
Build schema management tools
Generate database schemas dynamically
Ensure cross-platform compatibility for your database schema
Maintain database structure alongside your application code
Platform Compatibility
When combined with a platform-specific PhpDb\Sql\Sql object, DDL objects produce optimized SQL statements for your target database platform. The system automatically handles:
Platform-specific data type mappings
Quoting and identifier formatting
Constraint syntax differences
Index creation syntax
Basic Workflow
The typical workflow for using DDL abstraction:
Create a DDL object (CreateTable, AlterTable, or DropTable)
Configure the object (add columns, constraints, etc.)
Generate SQL using Sql::buildSqlString()
Execute using Adapter::query() with QUERY_MODE_EXECUTE
Note: The old Float class is deprecated; use Floating instead.
String Types
Varchar
Variable-length character string.
usePhpDb\Sql\Ddl\Column\Varchar;
$column = newVarchar('name', 255);
$column = newVarchar('email', 320); // Max email length// Can be nullable$column = newVarchar('middle_name', 100);
$column->setNullable(true);
Constructor:__construct($name, $length)
Methods:
setLength(int $length): self
getLength(): int
Char
Fixed-length character string.
usePhpDb\Sql\Ddl\Column\Char;
$column = newChar('country_code', 2); // ISO country codes$column = newChar('status', 1); // Single character status
Constructor:__construct($name, $length)
Text
Variable-length text for large strings.
usePhpDb\Sql\Ddl\Column\Text;
$column = newText('description');
$column = newText('content', 65535); // With length limit// Can be nullable and have defaults$column = newText('notes', null, true, 'No notes');
usePhpDb\Sql\Ddl\Column\Timestamp;
// Basic timestamp$column = newTimestamp('created_at');
$column->setDefault('CURRENT_TIMESTAMP');
// With automatic update on row modification$column = newTimestamp('updated_at');
$column->setDefault('CURRENT_TIMESTAMP');
$column->setOption('on_update', true);
// Generates: TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
Constructor:__construct($name)
Special Options:
on_update - When true, adds ON UPDATE CURRENT_TIMESTAMP
Boolean Type
Boolean
Boolean/bit column. Note: Boolean columns are always NOT NULL and cannot be made nullable.
usePhpDb\Sql\Ddl\Column\Boolean;
$column = newBoolean('is_active');
$column = newBoolean('is_verified');
// Attempting to make nullable has no effect$column->setNullable(true); // Does nothing - stays NOT NULL
Constructor:__construct($name)
Important: The setNullable() method is overridden to always enforce NOT NULL.
Generic Column Type
Column
Generic column type (defaults to INTEGER). Use specific types when possible.
The AlterTable class represents an ALTER TABLE statement. It provides methods to modify existing table structures.
Basic AlterTable Creation
usePhpDb\Sql\Ddl\AlterTable;
usePhpDb\Sql\TableIdentifier;
// Simple$alter = newAlterTable('users');
// With schema$alter = newAlterTable(newTableIdentifier('users', 'public'));
// Set after construction$alter = newAlterTable();
$alter->setTable('users');
Adding Columns
Add new columns to an existing table:
usePhpDb\Sql\Ddl\AlterTable;
usePhpDb\Sql\Ddl\Column;
$alter = newAlterTable('users');
// Add a single column$alter->addColumn(newColumn\Varchar('phone', 20));
// Add multiple columns$alter->addColumn(newColumn\Varchar('city', 100));
$alter->addColumn(newColumn\Varchar('country', 2));
Generated SQL:
ALTERTABLE"users"
ADD COLUMN "phone"VARCHAR(20) NOT NULL,
ADD COLUMN "city"VARCHAR(100) NOT NULL,
ADD COLUMN "country"VARCHAR(2) NOT NULL
Changing Columns
Modify existing column definitions:
$alter = newAlterTable('users');
// Change column type or properties$alter->changeColumn('name', newColumn\Varchar('name', 500));
$alter->changeColumn('age', newColumn\Integer('age'));
// Rename and change at the same time$alter->changeColumn('name', newColumn\Varchar('full_name', 200));
Generated SQL:
ALTERTABLE"users"
CHANGE COLUMN "name""full_name"VARCHAR(200) NOT NULL
Control what happens when referenced rows are deleted or updated:
$fk = newForeignKey(
'fk_order_customer',
'customer_id',
'customers',
'id',
'CASCADE', // ON DELETE CASCADE - delete orders when customer is deleted'RESTRICT'// ON UPDATE RESTRICT - prevent customer ID changes if orders exist
);
Available Actions:
CASCADE - Propagate the change to dependent rows
SET NULL - Set foreign key column to NULL
RESTRICT - Prevent the change if dependent rows exist
NO ACTION - Similar to RESTRICT (default)
Common Patterns:
// Delete child records when parent is deleted$fk = newForeignKey('fk_name', 'parent_id', 'parents', 'id', 'CASCADE');
// Set to NULL when parent is deleted (requires nullable column)$fk = newForeignKey('fk_name', 'parent_id', 'parents', 'id', 'SET NULL');
// Prevent deletion if child records exist$fk = newForeignKey('fk_name', 'parent_id', 'parents', 'id', 'RESTRICT');
Composite Foreign Key
Multiple columns reference multiple columns in another table:
usePhpDb\Sql\Ddl\Constraint\Check;
// Age must be 18 or older$check = newCheck('age >= 18', 'check_adult_age');
$table->addConstraint($check);
// Price must be positive$check = newCheck('price > 0', 'check_positive_price');
$table->addConstraint($check);
// Email must contain @$check = newCheck('email LIKE "%@%"', 'check_email_format');
$table->addConstraint($check);
Complex Check Constraints
// Discount percentage must be between 0 and 100$check = newCheck(
'discount_percent >= 0 AND discount_percent <= 100',
'check_valid_discount'
);
// End date must be after start date$check = newCheck(
'end_date > start_date',
'check_date_range'
);
// Status must be one of specific values$check = newCheck(
"status IN ('pending', 'active', 'completed', 'cancelled')",
'check_valid_status'
);
Constraint Naming Conventions
While some constraints allow optional names, it's a best practice to always provide explicit names:
// Good - explicit names for all constraints$table->addConstraint(newConstraint\PrimaryKey('id', 'pk_users'));
$table->addConstraint(newConstraint\UniqueKey('email', 'unique_user_email'));
$table->addConstraint(newConstraint\ForeignKey(
'fk_user_role',
'role_id',
'roles',
'id'
));
// This makes it easier to drop or modify constraints later$alter->dropConstraint('unique_user_email');
$alter->dropConstraint('fk_user_role');
Recommended Naming Patterns:
Primary keys: pk_<table_name>
Foreign keys: fk_<table>_<referenced_table> or fk_<table>_<column>
Unique constraints: unique_<table>_<column> or unique_<descriptive_name>
Check constraints: check_<descriptive_name>
Indexes
Indexes improve query performance by creating fast lookup structures. The Index class is in the PhpDb\Sql\Ddl\Index namespace.
Basic Index Creation
usePhpDb\Sql\Ddl\Index\Index;
// Single column index$index = newIndex('username', 'idx_username');
$table->addConstraint($index);
// With explicit name$index = newIndex('email', 'idx_user_email');
$table->addConstraint($index);
Generated SQL:
INDEX "idx_username" ("username")
Composite Indexes
Index multiple columns together:
// Index on category and price (useful for filtered sorts)$index = newIndex(['category', 'price'], 'idx_category_price');
$table->addConstraint($index);
// Index on last_name, first_name (useful for name searches)$index = newIndex(['last_name', 'first_name'], 'idx_name_search');
$table->addConstraint($index);
Generated SQL:
INDEX "idx_category_price" ("category", "price")
Index with Column Length Specifications
For large text columns, you can index only a prefix:
// Index first 50 characters of title$index = newIndex('title', 'idx_title', [50]);
$table->addConstraint($index);
// Composite index with different lengths per column$index = newIndex(
['title', 'description'],
'idx_search',
[50, 100] // Index 50 chars of title, 100 of description
);
$table->addConstraint($index);
Generated SQL (platform-specific):
INDEX "idx_search" ("title"(50), "description"(100))
Why use length specifications?
Reduces index size for large text columns
Improves index creation and maintenance performance
Particularly useful for VARCHAR/TEXT columns that store long content
Use TableIdentifier for schema-qualified table references:
usePhpDb\Sql\TableIdentifier;
// Table in default schema$identifier = newTableIdentifier('users');
// Table in specific schema$identifier = newTableIdentifier('users', 'public');
$identifier = newTableIdentifier('audit_log', 'audit');
// Use in DDL objects$table = newCreateTable(newTableIdentifier('users', 'auth'));
$alter = newAlterTable(newTableIdentifier('products', 'inventory'));
$drop = newDropTable(newTableIdentifier('temp', 'scratch'));
// In foreign keys (schema.table syntax)$fk = newForeignKey(
'fk_user_role',
'role_id',
newTableIdentifier('roles', 'auth'), // Referenced table with schema'id'
);
Platform-Specific Features
Current Status
Important: Platform-specific DDL decorators have been removed during refactoring. The decorator infrastructure exists in the codebase but specific platform implementations (MySQL, SQL Server, Oracle, SQLite) have been deprecated and removed.
What This Means
Platform specialization is handled at the Adapter Platform level, not the SQL DDL level
DDL objects are platform-agnostic - they define the structure, and the platform renders it appropriately
The decorator system can be used manually if needed via setTypeDecorator(), but this is advanced usage
Manual Decorator Usage (Advanced)
If you need custom platform-specific behavior, you can register decorators manually:
usePhpDb\Sql\Platform\Platform;
usePhpDb\Sql\Platform\PlatformDecoratorInterface;
$sqlPlatform = newPlatform($adapter);
// Create a custom decorator$customDecorator = newclassimplements PlatformDecoratorInterface {
private$subject;
publicfunctionsetSubject($subject): void {
$this->subject = $subject;
}
// Implement platform-specific logic here
};
// Register it for a specific type$sqlPlatform->setTypeDecorator(CreateTable::class, $customDecorator);
Recommendation: Most users will not need to use decorators. Rely on the built-in platform rendering instead.
Platform-Agnostic Approach
The DDL abstraction is designed to work across platforms without modification:
// This code works on MySQL, PostgreSQL, SQL Server, SQLite, etc.$table = newCreateTable('users');
$table->addColumn(newColumn\Integer('id'));
$table->addColumn(newColumn\Varchar('name', 255));
// The platform adapter handles rendering differences:// - MySQL: CREATE TABLE `users` (`id` INT NOT NULL, `name` VARCHAR(255) NOT NULL)// - PostgreSQL: CREATE TABLE "users" ("id" INTEGER NOT NULL, "name" VARCHAR(255) NOT NULL)// - SQL Server: CREATE TABLE [users] ([id] INT NOT NULL, [name] VARCHAR(255) NOT NULL)
Platform-Specific Options
Use column options for platform-specific features:
// MySQL AUTO_INCREMENT$id = newColumn\Integer('id');
$id->setOption('AUTO_INCREMENT', true);
// PostgreSQL/SQL Server IDENTITY$id = newColumn\Integer('id');
$id->setOption('identity', true);
// MySQL UNSIGNED$count = newColumn\Integer('count');
$count->setOption('unsigned', true);
Note: Not all options work on all platforms. Test your DDL against your target database.
Best Practice: Platform Detection
// Check platform before using platform-specific options$platformName = $adapter->getPlatform()->getName();
if ($platformName === 'MySQL') {
$id->setOption('AUTO_INCREMENT', true);
} elseif (in_array($platformName, ['PostgreSQL', 'SqlServer'])) {
$id->setOption('identity', true);
}
Expression Objects
Using Expressions in Check Constraints
Check constraints can accept either string expressions or Expression objects.
For complex or parameterized constraints, use Expression objects:
usePhpDb\Sql\Expression;
usePhpDb\Sql\Ddl\Constraint\Check;
// Expression with parameters$expr = newExpression(
'age >= ? AND age <= ?',
[18, 120]
);
$check = newCheck($expr, 'check_valid_age_range');
// Complex expression$expr = newExpression(
'discount_percent BETWEEN ? AND ?',
[0, 100]
);
$check = newCheck($expr, 'check_discount_range');
Expression Constructor
The Expression class accepts:
// Simple expression without parameters$expr = newExpression('column_name IS NOT NULL');
// Expression with single parameter$expr = newExpression('age >= ?', 18);
// Expression with multiple parameters (array)$expr = newExpression('value BETWEEN ? AND ?', [0, 100]);
// Expression with typed parameters (advanced)usePhpDb\Sql\Argument;
usePhpDb\Sql\ArgumentType;
$expr = newExpression(
'user_id = ?',
newArgument(123, ArgumentType::Integer)
);
When to Use Expression Objects
Use strings when:
The constraint is simple and static
No parameters are needed
The expression is platform-agnostic
Use Expression objects when:
You need parameterized values
Building constraints programmatically
Reusing expressions across multiple constraints
Working with complex conditional logic
Expression Methods
$expr = newExpression();
// Set the expression string$expr->setExpression('age >= ? AND status = ?');
// Set parameters$expr->setParameters([18, 'active']);
// Get the expression$expression = $expr->getExpression(); // Returns: 'age >= ? AND status = ?'
Column options provide a flexible way to specify platform-specific features and metadata.
How Options Work
// Set single option$column->setOption('option_name', 'option_value');
// Set multiple options$column->setOptions([
'option1' => 'value1',
'option2' => 'value2',
]);
// Get all options$options = $column->getOptions();
Documented Options
These options are tested and documented:
Option
Type
Platforms
Description
Example
AUTO_INCREMENT
bool
MySQL, MariaDB
Auto-incrementing integer
$col->setOption('AUTO_INCREMENT', true)
identity
bool
PostgreSQL, SQL Server
Identity/Serial column
$col->setOption('identity', true)
comment
string
MySQL, PostgreSQL
Column comment/description
$col->setOption('comment', 'User ID')
on_update
bool
MySQL (Timestamp)
ON UPDATE CURRENT_TIMESTAMP
$col->setOption('on_update', true)
length
int
MySQL (Integer)
Display width
$col->setOption('length', 11)
Platform-Specific Options
These options may work on specific platforms but are not guaranteed across all databases:
MySQL/MariaDB Specific
// UNSIGNED modifier$column = newColumn\Integer('count');
$column->setOption('unsigned', true);
// Generates: `count` INT UNSIGNED NOT NULL// ZEROFILL modifier$column = newColumn\Integer('code');
$column->setOption('zerofill', true);
// Generates: `code` INT ZEROFILL NOT NULL// Character set$column = newColumn\Varchar('name', 255);
$column->setOption('charset', 'utf8mb4');
// Generates: `name` VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL// Collation$column = newColumn\Varchar('name', 255);
$column->setOption('collation', 'utf8mb4_unicode_ci');
// Generates: `name` VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL
PostgreSQL Specific
// SERIAL type (via identity option)$id = newColumn\Integer('id');
$id->setOption('identity', true);
// Generates: "id" SERIAL NOT NULL// Note: PostgreSQL-specific types like ARRAY, JSONB, etc. may require// custom column classes or platform-specific handling
SQL Server Specific
// IDENTITY column$id = newColumn\Integer('id');
$id->setOption('identity', true);
// Generates: [id] INT IDENTITY NOT NULL
Not all options work on all platforms - Test your DDL against your target database
Some options are silently ignored on unsupported platforms
Platform rendering varies - the same option may produce different SQL on different platforms
Options are not validated by DDL objects - invalid options may cause SQL errors during execution
Error Handling
DDL Error Behavior
Important: DDL objects themselves do not throw exceptions during construction or configuration. They are designed to build up state without validation.
Errors typically occur during:
SQL Generation - When buildSqlString() is called
Execution - When the adapter executes the DDL statement
Different platforms may reject different DDL constructs:
// SQLite doesn't support DROP CONSTRAINT$alter = newAlterTable('users');
$alter->dropConstraint('unique_email');
try {
$adapter->query($sql->buildSqlString($alter), $adapter::QUERY_MODE_EXECUTE);
} catch (\Exception$e) {
// SQLite will throw an error: ALTER TABLE syntax does not support DROP CONSTRAINTecho"Platform error: " . $e->getMessage();
}
functionalterTable($adapter, $alterTable) {
$platformName = $adapter->getPlatform()->getName();
// Check if platform supports ALTER TABLE ... DROP CONSTRAINTif ($platformName === 'SQLite' && hasDropConstraint($alterTable)) {
thrownew \RuntimeException(
'SQLite does not support DROP CONSTRAINT in ALTER TABLE'
);
}
// Proceed with execution$sql = newSql($adapter);
$adapter->query($sql->buildSqlString($alterTable), $adapter::QUERY_MODE_EXECUTE);
}
$sql = newSql($adapter);
// Generate the SQL string to see what will be executed$sqlString = $sql->buildSqlString($table);
echo$sqlString . "\n";
// Review before executingif (confirmExecution($sqlString)) {
$adapter->query($sqlString, $adapter::QUERY_MODE_EXECUTE);
}
// Use Integer for most numeric IDs and counters$id = newColumn\Integer('id'); // -2,147,483,648 to 2,147,483,647$count = newColumn\Integer('view_count');
// Use BigInteger for very large numbers$bigId = newColumn\BigInteger('user_id'); // -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807// Use Decimal for money and precise calculations$price = newColumn\Decimal('price', 10, 2); // DECIMAL(10,2) - $99,999,999.99$tax = newColumn\Decimal('tax_rate', 5, 4); // DECIMAL(5,4) - 0.9999 (99.99%)// Use Floating for scientific/approximate calculations (avoid for money!)$latitude = newColumn\Floating('lat', 10, 6); // GPS coordinates$measurement = newColumn\Floating('temp', 5, 2); // Temperature readings
String Types
// Use Varchar for bounded strings with known max length$email = newColumn\Varchar('email', 320); // Max email length (RFC 5321)$username = newColumn\Varchar('username', 50);
$countryCode = newColumn\Varchar('country', 2); // ISO 3166-1 alpha-2// Use Char for fixed-length strings$statusCode = newColumn\Char('status', 1); // Single character: 'A', 'P', 'C'$currencyCode = newColumn\Char('currency', 3); // ISO 4217: 'USD', 'EUR', 'GBP'// Use Text for unbounded or very large strings$description = newColumn\Text('description'); // Product descriptions$content = newColumn\Text('article_content'); // Article content$notes = newColumn\Text('notes'); // User notes
Rule of Thumb:
String <= 255 chars with known max → Varchar
Fixed length → Char
No length limit or very large → Text
Date/Time Types
// Use Date for dates without time$birthDate = newColumn\Date('birth_date');
$eventDate = newColumn\Date('event_date');
// Use Time for times without date$openTime = newColumn\Time('opening_time');
$duration = newColumn\Time('duration');
// Use Datetime for specific moments in time (platform-agnostic)$appointmentTime = newColumn\Datetime('appointment_at');
$publishedAt = newColumn\Datetime('published_at');
// Use Timestamp for automatic tracking (created/updated)$created = newColumn\Timestamp('created_at');
$created->setDefault('CURRENT_TIMESTAMP');
$updated = newColumn\Timestamp('updated_at');
$updated->setDefault('CURRENT_TIMESTAMP');
$updated->setOption('on_update', true);
Columns with low cardinality (few unique values) like boolean
Columns rarely used in queries
Columns that change frequently in write-heavy tables
Index Best Practices
// 1. Index foreign keys$table->addColumn(newColumn\Integer('user_id'));
$table->addConstraint(newConstraint\ForeignKey(
'fk_order_user',
'user_id',
'users',
'id'
));
$table->addConstraint(newIndex('user_id', 'idx_user'));
// 2. Composite indexes for common query patterns// If you often query: WHERE category_id = ? ORDER BY created_at DESC$table->addConstraint(newIndex(['category_id', 'created_at'], 'idx_category_date'));
// 3. Covering indexes (columns used together in WHERE/ORDER)// Query: WHERE status = 'active' AND priority = 'high' ORDER BY created_at$table->addConstraint(newIndex(['status', 'priority', 'created_at'], 'idx_active_priority'));
// 4. Prefix indexes for large text columns$table->addConstraint(newIndex('title', 'idx_title', [100])); // Index first 100 chars
Index Order Matters
// For query: WHERE category_id = ? ORDER BY created_at DESCnewIndex(['category_id', 'created_at'], 'idx_category_date'); // Good// Less effective for the same query:newIndex(['created_at', 'category_id'], 'idx_date_category'); // Not optimal
Rule: Most selective (filters most rows) columns should come first.
// Check if table exists before creatingfunctionsafeCreateTable($adapter, $tableName, $ddlObject) {
$sql = newSql($adapter);
// Check existence (platform-specific)$platformName = $adapter->getPlatform()->getName();
$exists = false;
if ($platformName === 'MySQL') {
$result = $adapter->query(
"SHOW TABLES LIKE '$tableName'",
$adapter::QUERY_MODE_EXECUTE
);
$exists = $result->count() > 0;
}
if (!$exists) {
$adapter->query(
$sql->buildSqlString($ddlObject),
$adapter::QUERY_MODE_EXECUTE
);
}
}
Pattern 3: Idempotent Migrations
// Use IF NOT EXISTS (platform-specific)// Note: PhpDb DDL doesn't support IF NOT EXISTS directly// You'll need to handle this at the SQL level or check existence firstfunctioncreateTableIfNotExists($adapter, $tableName, CreateTable$table) {
$sql = newSql($adapter);
$platformName = $adapter->getPlatform()->getName();
if ($platformName === 'MySQL') {
// Manually construct IF NOT EXISTS$sqlString = $sql->buildSqlString($table);
$sqlString = str_replace('CREATE TABLE', 'CREATE TABLE IF NOT EXISTS', $sqlString);
$adapter->query($sqlString, $adapter::QUERY_MODE_EXECUTE);
} else {
// Fallback: check and createsafeCreateTable($adapter, $tableName, $table);
}
}
Performance Considerations
1. Batch Multiple DDL Operations
// Bad: Multiple ALTER TABLE statements$alter1 = newAlterTable('users');
$alter1->addColumn(newColumn\Varchar('phone', 20));
$adapter->query($sql->buildSqlString($alter1), $adapter::QUERY_MODE_EXECUTE);
$alter2 = newAlterTable('users');
$alter2->addColumn(newColumn\Varchar('city', 100));
$adapter->query($sql->buildSqlString($alter2), $adapter::QUERY_MODE_EXECUTE);
// Good: Single ALTER TABLE with multiple operations$alter = newAlterTable('users');
$alter->addColumn(newColumn\Varchar('phone', 20));
$alter->addColumn(newColumn\Varchar('city', 100));
$adapter->query($sql->buildSqlString($alter), $adapter::QUERY_MODE_EXECUTE);
2. Add Indexes After Bulk Insert
// For large initial data loads:// 1. Create table without indexes$table = newCreateTable('products');
$table->addColumn(newColumn\Integer('id'));
$table->addColumn(newColumn\Varchar('name', 255));
// ... more columns ...$adapter->query($sql->buildSqlString($table), $adapter::QUERY_MODE_EXECUTE);
// 2. Load data// ... insert thousands of rows ...// 3. Add indexes after data is loaded$alter = newAlterTable('products');
$alter->addConstraint(newIndex('name', 'idx_name'));
$alter->addConstraint(newIndex(['category_id', 'price'], 'idx_category_price'));
$adapter->query($sql->buildSqlString($alter), $adapter::QUERY_MODE_EXECUTE);
3. Foreign Key Impact
Foreign keys add overhead to INSERT/UPDATE/DELETE operations:
// If you need to bulk load data, consider:// 1. Disable foreign key checks (platform-specific)// 2. Load data// 3. Re-enable foreign key checks// MySQL example (outside DDL abstraction):$adapter->query('SET FOREIGN_KEY_CHECKS = 0', $adapter::QUERY_MODE_EXECUTE);
// ... bulk operations ...$adapter->query('SET FOREIGN_KEY_CHECKS = 1', $adapter::QUERY_MODE_EXECUTE);
Testing DDL Changes
1. Test on Development Copy
// Always test DDL on a copy of production data$devAdapter = newAdapter($devConfig);
$prodAdapter = newAdapter($prodConfig);
// Test migration on dev firsttry {
executeMigration($devAdapter, $ddlObjects);
echo"Dev migration successful\n";
// If successful, run on productionexecuteMigration($prodAdapter, $ddlObjects);
} catch (\Exception$e) {
echo"Migration failed on dev: " . $e->getMessage() . "\n";
// Don't touch production
}
2. Generate and Review SQL
// Generate DDL SQL and review before executing$sql = newSql($adapter);
foreach ($ddlObjectsas$ddl) {
$sqlString = $sql->buildSqlString($ddl);
echo$sqlString . ";\n\n";
}
// Review output, then execute if satisfied
Looks good. We need to decide how we are going to organize and aggregate the documentation around the adapter packages and their components such as the Sql\Ddl decorators...
Activity
simon-mundy commentedon Nov 17, 2025
DDL Abstraction
Table of Contents
Introduction & Overview
PhpDb\Sql\Ddlis a sub-component ofPhpDb\Sqlthat provides an object-oriented abstraction for DDL (Data Definition Language) SQL statements. It allows you to programmatically define database schemas in a platform-agnostic way.What is DDL Abstraction?
DDL abstraction enables you to:
When to Use DDL Abstraction
Use
PhpDb\Sql\Ddlwhen you need to:Platform Compatibility
When combined with a platform-specific
PhpDb\Sql\Sqlobject, DDL objects produce optimized SQL statements for your target database platform. The system automatically handles:Basic Workflow
The typical workflow for using DDL abstraction:
Sql::buildSqlString()Adapter::query()withQUERY_MODE_EXECUTECreating Tables
The
CreateTableclass represents aCREATE TABLEstatement. You can build complex table definitions using a fluent, object-oriented interface.Basic Table Creation
Generated SQL:
Setting the Table Name
You can set the table name during construction or after instantiation:
Schema-Qualified Tables
Use
TableIdentifierto create tables in a specific schema:Generated SQL:
Temporary Tables
Create temporary tables by passing
trueas the second parameter:Generated SQL:
CREATE TEMPORARY TABLE "temp_data" (...)Adding Columns
Columns are added using the
addColumn()method with column type objects:Adding Constraints
Table-level constraints are added using
addConstraint():Column-Level Constraints
Columns can have constraints attached directly:
Generated SQL:
Fluent Interface Pattern
All DDL objects support method chaining for cleaner code:
Complete Example: User Table
Column Types Reference
All column types are in the
PhpDb\Sql\Ddl\Columnnamespace and implementColumnInterface.Numeric Types
Integer
Standard integer column.
Constructor:
__construct($name, $nullable = false, $default = null, array $options = [])Methods:
setNullable(bool $nullable): selfisNullable(): boolsetDefault(string|int|null $default): selfgetDefault(): string|int|nullsetOption(string $name, mixed $value): selfsetOptions(array $options): selfBigInteger
For larger integer values (typically 64-bit).
Constructor:
__construct($name, $nullable = false, $default = null, array $options = [])Decimal
Fixed-point decimal numbers with precision and scale.
Constructor:
__construct($name, $precision, $scale = null)Methods:
setDigits(int $digits): self- Set precisiongetDigits(): int- Get precisionsetDecimal(int $decimal): self- Set scalegetDecimal(): int- Get scaleFloating
Floating-point numbers.
Constructor:
__construct($name, $digits, $decimal)Note: The old
Floatclass is deprecated; useFloatinginstead.String Types
Varchar
Variable-length character string.
Constructor:
__construct($name, $length)Methods:
setLength(int $length): selfgetLength(): intChar
Fixed-length character string.
Constructor:
__construct($name, $length)Text
Variable-length text for large strings.
Constructor:
__construct($name, $length = null, $nullable = false, $default = null, array $options = [])Binary Types
Binary
Fixed-length binary data.
Constructor:
__construct($name, $length, $nullable = false, $default = null, array $options = [])Varbinary
Variable-length binary data.
Constructor:
__construct($name, $length)Blob
Binary large object for very large binary data.
Constructor:
__construct($name, $length = null, $nullable = false, $default = null, array $options = [])Date and Time Types
Date
Date without time.
Constructor:
__construct($name)Time
Time without date.
Constructor:
__construct($name)Datetime
Date and time combined.
Constructor:
__construct($name)Timestamp
Timestamp with special capabilities.
Constructor:
__construct($name)Special Options:
on_update- Whentrue, addsON UPDATE CURRENT_TIMESTAMPBoolean Type
Boolean
Boolean/bit column. Note: Boolean columns are always NOT NULL and cannot be made nullable.
Constructor:
__construct($name)Important: The
setNullable()method is overridden to always enforce NOT NULL.Generic Column Type
Column
Generic column type (defaults to INTEGER). Use specific types when possible.
Constructor:
__construct($name = null)Common Column Methods
All column types share these methods:
Modifying Tables
The
AlterTableclass represents anALTER TABLEstatement. It provides methods to modify existing table structures.Basic AlterTable Creation
Adding Columns
Add new columns to an existing table:
Generated SQL:
Changing Columns
Modify existing column definitions:
Generated SQL:
Dropping Columns
Remove columns from a table:
Generated SQL:
Adding Constraints
Add table constraints:
Dropping Constraints
Remove constraints from a table:
Generated SQL:
Adding Indexes
Add indexes to improve query performance:
Dropping Indexes
Remove indexes from a table:
Generated SQL:
Complex AlterTable Example
Combine multiple operations in a single statement:
Dropping Tables
The
DropTableclass represents aDROP TABLEstatement.Basic Drop Table
Generated SQL:
Schema-Qualified Drop
Generated SQL:
Dropping Multiple Tables
Execute multiple drop statements:
Constraints In-Depth
Constraints enforce data integrity rules at the database level. All constraints are in the
PhpDb\Sql\Ddl\Constraintnamespace.Primary Key Constraints
A primary key uniquely identifies each row in a table.
Single-Column Primary Key
Composite Primary Key
Multiple columns together form the primary key:
Column-Level Primary Key
Attach primary key directly to a column:
Generated SQL:
Foreign Key Constraints
Foreign keys enforce referential integrity between tables.
Basic Foreign Key
Generated SQL:
Foreign Key with Referential Actions
Control what happens when referenced rows are deleted or updated:
Available Actions:
CASCADE- Propagate the change to dependent rowsSET NULL- Set foreign key column to NULLRESTRICT- Prevent the change if dependent rows existNO ACTION- Similar to RESTRICT (default)Common Patterns:
Composite Foreign Key
Multiple columns reference multiple columns in another table:
Generated SQL:
Unique Constraints
Unique constraints ensure column values are unique across all rows.
Single-Column Unique Constraint
Generated SQL:
Composite Unique Constraint
Multiple columns together must be unique:
Generated SQL:
Check Constraints
Check constraints enforce custom validation rules.
Simple Check Constraints
Complex Check Constraints
Constraint Naming Conventions
While some constraints allow optional names, it's a best practice to always provide explicit names:
Recommended Naming Patterns:
pk_<table_name>fk_<table>_<referenced_table>orfk_<table>_<column>unique_<table>_<column>orunique_<descriptive_name>check_<descriptive_name>Indexes
Indexes improve query performance by creating fast lookup structures. The
Indexclass is in thePhpDb\Sql\Ddl\Indexnamespace.Basic Index Creation
Generated SQL:
Composite Indexes
Index multiple columns together:
Generated SQL:
Index with Column Length Specifications
For large text columns, you can index only a prefix:
Generated SQL (platform-specific):
Why use length specifications?
Adding Indexes to Existing Tables
Use
AlterTableto add indexes:Dropping Indexes
Index Naming Conventions
Use descriptive names that indicate the purpose:
Complete Index Example
Advanced Features
Column Options System
All columns support a flexible options system for platform-specific features and additional metadata.
Setting Options
Common Options
AUTO_INCREMENT (MySQL, MariaDB):
comment (Most platforms):
on_update (Timestamp columns):
length (Integer/BigInteger):
identity (SQL Server, PostgreSQL):
Nullable and Default Values
Setting Nullable
Note: Boolean columns cannot be made nullable:
Setting Default Values
Fluent Interface Patterns
All DDL objects support method chaining for cleaner, more readable code.
Chaining Column Configuration
Chaining Table Construction
Chaining Constraint Configuration
Inspecting DDL Objects
Use
getRawState()to inspect the internal configuration of DDL objects:This is useful for:
Working with Table Identifiers
Use
TableIdentifierfor schema-qualified table references:Platform-Specific Features
Current Status
Important: Platform-specific DDL decorators have been removed during refactoring. The decorator infrastructure exists in the codebase but specific platform implementations (MySQL, SQL Server, Oracle, SQLite) have been deprecated and removed.
What This Means
setTypeDecorator(), but this is advanced usageManual Decorator Usage (Advanced)
If you need custom platform-specific behavior, you can register decorators manually:
Recommendation: Most users will not need to use decorators. Rely on the built-in platform rendering instead.
Platform-Agnostic Approach
The DDL abstraction is designed to work across platforms without modification:
Platform-Specific Options
Use column options for platform-specific features:
Note: Not all options work on all platforms. Test your DDL against your target database.
Best Practice: Platform Detection
Expression Objects
Using Expressions in Check Constraints
Check constraints can accept either string expressions or
Expressionobjects.String Expressions (Simple)
For simple constraints, use strings:
Expression Objects (Advanced)
For complex or parameterized constraints, use
Expressionobjects:Expression Constructor
The
Expressionclass accepts:When to Use Expression Objects
Use strings when:
Use Expression objects when:
Expression Methods
Complete Example: Dynamic Check Constraints
Column Options Reference
Column options provide a flexible way to specify platform-specific features and metadata.
How Options Work
Documented Options
These options are tested and documented:
AUTO_INCREMENT$col->setOption('AUTO_INCREMENT', true)identity$col->setOption('identity', true)comment$col->setOption('comment', 'User ID')on_update$col->setOption('on_update', true)length$col->setOption('length', 11)Platform-Specific Options
These options may work on specific platforms but are not guaranteed across all databases:
MySQL/MariaDB Specific
PostgreSQL Specific
SQL Server Specific
Common Option Patterns
Auto-Incrementing Primary Key
Timestamp with Auto-Update
Documented Column with Comment
Option Compatibility Notes
Important Considerations:
Error Handling
DDL Error Behavior
Important: DDL objects themselves do not throw exceptions during construction or configuration. They are designed to build up state without validation.
Errors typically occur during:
buildSqlString()is calledException Types
DDL-related operations can throw:
Common Error Scenarios
1. Empty Expression
2. SQL Execution Errors
3. Platform-Specific Errors
Different platforms may reject different DDL constructs:
Error Handling Best Practices
1. Wrap DDL Execution in Try-Catch
2. Validate Platform Capabilities
3. Transaction Wrapping
Debugging DDL Issues
Use getRawState() for Inspection
Generate SQL Without Execution
Log DDL Statements
Best Practices
Column Type Selection
Numeric Types
String Types
Rule of Thumb:
Date/Time Types
Naming Conventions
Table Names
Column Names
Constraint Names
Index Names
Index Strategy
When to Add Indexes
DO index:
DON'T index:
Index Best Practices
Index Order Matters
Rule: Most selective (filters most rows) columns should come first.
Schema Migration Patterns
Pattern 1: Versioned Migrations
Pattern 2: Safe Migrations
Pattern 3: Idempotent Migrations
Performance Considerations
1. Batch Multiple DDL Operations
2. Add Indexes After Bulk Insert
3. Foreign Key Impact
Foreign keys add overhead to INSERT/UPDATE/DELETE operations:
Testing DDL Changes
1. Test on Development Copy
2. Generate and Review SQL
3. Backup Before DDL
Complete Examples & Patterns
Example 1: E-Commerce Product Table
Example 2: User Authentication System
Example 3: Multi-Tenant Schema
Example 4: Database Migration Pattern
Example 5: Audit Log Table
simon-mundy commentedon Nov 27, 2025
@tyrsson @settermjd This is subject to the BC changes for Argument classes changed in PR#87
tyrsson commentedon Nov 30, 2025
Looks good. We need to decide how we are going to organize and aggregate the documentation around the adapter packages and their components such as the Sql\Ddl decorators...
simon-mundy commentedon Jan 5, 2026
Closed with #101
tyrsson commentedon Jan 7, 2026
Just leaving a note here that it was merged into 0.4.x and originally slated for 0.4.2 but that minor was skipped in favor of just releasing 0.5.0.