PDOdb provides a fluent DDL Query Builder for database schema operations without writing raw SQL. This includes creating, altering, and dropping tables, columns, indexes, and foreign keys across all supported databases.
The DDL Query Builder provides a unified API for schema operations across MySQL, MariaDB, PostgreSQL, SQLite, and Microsoft SQL Server (MSSQL). It automatically handles dialect-specific differences and syntax requirements.
use tommyknocker\pdodb\PdoDb;
$db = new PdoDb('mysql', [
'host' => 'localhost',
'username' => 'user',
'password' => 'pass',
'dbname' => 'mydb'
]);
// Get DDL Query Builder
$schema = $db->schema();The most intuitive way to create tables is using the fluent ColumnSchema API:
$schema->createTable('users', [
'id' => $schema->primaryKey(),
'username' => $schema->string(100)->notNull(),
'email' => $schema->string(255)->notNull()->unique(),
'password_hash' => $schema->string(255)->notNull(),
'age' => $schema->integer()->defaultValue(0),
'status' => $schema->integer()->defaultValue(1),
'created_at' => $schema->timestamp()->defaultExpression('CURRENT_TIMESTAMP'),
'updated_at' => $schema->timestamp()->null(),
'bio' => $schema->text(),
'balance' => $schema->decimal(10, 2)->defaultValue(0.00),
'is_active' => $schema->boolean()->defaultValue(true),
]);You can also use array definitions for a more concise syntax:
$schema->createTable('posts', [
'id' => ['type' => 'INT', 'autoIncrement' => true, 'null' => false],
'user_id' => ['type' => 'INT', 'null' => false],
'title' => ['type' => 'VARCHAR', 'length' => 255, 'null' => false],
'content' => ['type' => 'TEXT'],
'published' => ['type' => 'BOOLEAN', 'default' => false],
'views' => ['type' => 'INT', 'default' => 0],
]);For simple cases, you can use string type definitions:
$schema->createTable('tags', [
'id' => 'INT AUTO_INCREMENT PRIMARY KEY',
'name' => 'VARCHAR(100) NOT NULL',
'slug' => 'VARCHAR(100) NOT NULL UNIQUE',
]);The ColumnSchema fluent API provides many methods for column definition:
primaryKey(?int $length = null)- Integer primary key with auto-incrementbigPrimaryKey()- Big integer primary key with auto-increment (BIGINT)string(?int $length = null)- VARCHAR column (default length varies by dialect)text()- TEXT columnchar(?int $length = null)- CHAR columninteger(?int $length = null)- INTEGER columnbigInteger()- BIGINT columnsmallInteger()- SMALLINT columnboolean()- BOOLEAN columnfloat(?int $precision = null, ?int $scale = null)- FLOAT columndecimal(int $precision = 10, int $scale = 2)- DECIMAL columndatetime()- DATETIME/TIMESTAMP columntimestamp()- TIMESTAMP columndate()- DATE columntime()- TIME columnjson()- JSON column
->notNull()- Set column as NOT NULL->null()- Allow NULL values (default)->defaultValue(mixed $value)- Set default value->defaultExpression(string $expr)- Set default expression (e.g., 'CURRENT_TIMESTAMP')->autoIncrement()- Set auto-increment (MySQL/MariaDB)->unsigned()- Set unsigned (MySQL/MariaDB)->unique()- Mark column as unique->comment(string $comment)- Add column comment (MySQL/MariaDB)->after(string $column)- Place column after another (MySQL/MariaDB)->first()- Place column first (MySQL/MariaDB)
Some dialects support table options:
// MySQL/MariaDB
$schema->createTable('users', [
'id' => $schema->primaryKey(),
'name' => $schema->string(255),
], [
'engine' => 'InnoDB',
'charset' => 'utf8mb4',
'collate' => 'utf8mb4_unicode_ci',
'comment' => 'User accounts table',
]);
// PostgreSQL
$schema->createTable('users', [
'id' => $schema->primaryKey(),
'name' => $schema->string(255),
], [
'tablespace' => 'pg_default',
'with' => ['fillfactor' => 70],
]);
// Table partitioning (MySQL, PostgreSQL)
$schema->createTable('orders', [
'id' => $schema->primaryKey(),
'order_date' => $schema->date()->notNull(),
'amount' => $schema->decimal(10, 2),
], [
'partition' => 'PARTITION BY RANGE (YEAR(order_date)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026)
)',
]);
// Table inheritance (PostgreSQL only)
$schema->createTable('child_table', [
'id' => $schema->primaryKey(),
'name' => $schema->string(255),
], [
'inherits' => ['parent_table'],
]);// Create table only if it doesn't exist
$schema->createTableIfNotExists('users', [
'id' => $schema->primaryKey(),
'username' => $schema->string(100)->notNull(),
]);// Add a single column
$schema->addColumn('users', 'phone', $schema->string(20));
// Add column with position (MySQL/MariaDB)
$schema->addColumn('users', 'phone', $schema->string(20)->after('email'));
// Add column at the beginning (MySQL/MariaDB)
$schema->addColumn('users', 'priority', $schema->integer()->first());$schema->dropColumn('users', 'phone');Note: SQLite has limited support for DROP COLUMN. It's only available in SQLite 3.35.0+.
Alter column definition (type, nullability, default, etc.):
// Change column type and attributes
$schema->alterColumn('users', 'email', $schema->string(200)->notNull());
// Modify default value
$schema->alterColumn('users', 'status', $schema->integer()->defaultValue(0));
// Change nullability
$schema->alterColumn('users', 'bio', $schema->text()->null());Note: SQLite has limited support for ALTER COLUMN. You can rename columns but changing types requires recreating the table.
$schema->renameColumn('users', 'phone', 'phone_number');Note: Requires MySQL 8.0.13+, MariaDB 10.5.2+, PostgreSQL 10+, or SQLite 3.25.0+.
$schema->renameTable('posts', 'articles');// Add primary key constraint
$schema->addPrimaryKey('pk_orders', 'orders', ['order_id', 'product_id']);
// Drop primary key constraint
$schema->dropPrimaryKey('pk_orders', 'orders');// Add unique constraint
$schema->addUnique('uq_users_email', 'users', 'email');
$schema->addUnique('uq_users_username_email', 'users', ['username', 'email']);
// Drop unique constraint
$schema->dropUnique('uq_users_email', 'users');// Add check constraint
$schema->addCheck('chk_products_price', 'products', 'price > 0');
$schema->addCheck('chk_users_age', 'users', 'age >= 18 AND age <= 120');
// Drop check constraint
$schema->dropCheck('chk_products_price', 'products');// Check if constraints exist
if ($schema->indexExists('idx_users_email', 'users')) {
echo "Index exists\n";
}
if ($schema->uniqueExists('uq_users_email', 'users')) {
echo "Unique constraint exists\n";
}
if ($schema->foreignKeyExists('fk_posts_user', 'posts')) {
echo "Foreign key exists\n";
}
if ($schema->checkExists('chk_products_price', 'products')) {
echo "Check constraint exists\n";
}// Get all indexes for a table
$indexes = $schema->getIndexes('users');
// Get all foreign keys for a table
$foreignKeys = $schema->getForeignKeys('posts');
// Get all unique constraints for a table
$uniqueConstraints = $schema->getUniqueConstraints('users');
// Get all check constraints for a table
$checkConstraints = $schema->getCheckConstraints('products');// Simple index
$schema->createIndex('idx_users_email', 'users', 'email');
// Unique index
$schema->createIndex('idx_users_username', 'users', 'username', true);
// Composite index
$schema->createIndex('idx_posts_user_date', 'posts', ['user_id', 'created_at']);
// Index with sorting (ASC/DESC)
$schema->createIndex('idx_products_name_price', 'products', [
'name' => 'ASC',
'price' => 'DESC',
]);
// Partial index with WHERE clause (PostgreSQL, MSSQL, SQLite)
$schema->createIndex('idx_products_active', 'products', 'name', false, 'status = 1');
// Index with INCLUDE columns (PostgreSQL, MSSQL)
$schema->createIndex('idx_products_search', 'products', 'name', false, null, ['price', 'stock']);
// Functional index (on expression)
use tommyknocker\pdodb\helpers\Db;
$schema->createIndex('idx_users_lower_email', 'users', [Db::raw('LOWER(email)')]);// MySQL/MariaDB fulltext index
$schema->createFulltextIndex('ft_idx_articles', 'articles', ['title', 'content']);
// PostgreSQL fulltext index (uses GIN with tsvector)
$schema->createFulltextIndex('ft_idx_articles', 'articles', ['title', 'content'], 'english');// MySQL/MariaDB spatial index
$schema->createSpatialIndex('sp_idx_locations', 'locations', ['coordinates']);
// PostgreSQL spatial index (uses GIST)
$schema->createSpatialIndex('sp_idx_locations', 'locations', ['coordinates']);
// MSSQL spatial index
$schema->createSpatialIndex('sp_idx_locations', 'locations', ['coordinates']);Note: Spatial indexes require spatial data types (GEOMETRY, GEOGRAPHY, POINT, etc.) in the columns. For PostgreSQL, you may need the PostGIS extension.
// Rename index
$schema->renameIndex('idx_old_name', 'users', 'idx_new_name');$schema->dropIndex('idx_users_email', 'users');// Simple foreign key
$schema->addForeignKey(
'fk_posts_user',
'posts',
'user_id',
'users',
'id'
);
// Foreign key with actions
$schema->addForeignKey(
'fk_posts_user',
'posts',
'user_id',
'users',
'id',
'CASCADE', // ON DELETE CASCADE
'RESTRICT' // ON UPDATE RESTRICT
);
// Composite foreign key
$schema->addForeignKey(
'fk_order_items_order',
'order_items',
['order_id', 'order_date'],
'orders',
['id', 'order_date']
);$schema->dropForeignKey('fk_posts_user', 'posts');// Rename foreign key (PostgreSQL, MSSQL)
$schema->renameForeignKey('fk_old_name', 'posts', 'fk_new_name');$schema->dropTable('users');$schema->dropTableIfExists('old_table');$schema->truncateTable('users');if ($schema->tableExists('users')) {
echo "Table 'users' exists\n";
}- Auto-increment columns automatically become PRIMARY KEY
- Supports
ENGINE,CHARSET,COLLATEtable options - Supports
COMMENTon columns and tables - Supports
FIRSTandAFTERfor column positioning - Supports
UNSIGNEDfor numeric columns RENAME COLUMNrequires MySQL 8.0.13+ / MariaDB 10.5.2+
- Uses
SERIAL/BIGSERIALfor auto-increment (instead ofAUTO_INCREMENT) - Supports
TABLESPACEandWITHtable options RENAME COLUMNrequires PostgreSQL 10+
- Limited
ALTER TABLEsupport:DROP COLUMNrequires SQLite 3.35.0+RENAME COLUMNrequires SQLite 3.25.0+ALTER COLUMN(type changes) not supported
- Type system is flexible (affinity-based)
- No support for
UNSIGNED,FIRST,AFTER,COMMENT - Foreign keys must be enabled with
PRAGMA foreign_keys = ON
- Uses
IDENTITYfor auto-increment columns (instead ofAUTO_INCREMENT) - Supports filtered indexes with WHERE clause
- Supports indexes with INCLUDE columns
- Supports spatial indexes (GEOMETRY/GEOGRAPHY types)
RENAME COLUMNusessp_renamestored procedureDROP COLUMNautomatically handles default constraints- Supports
NVARCHAR(MAX)for TEXT type
-
Use Fluent API: Prefer the ColumnSchema fluent API for better readability and type safety.
-
Check Table Existence: Always check if a table exists before creating it, or use
createTableIfNotExists(). -
Handle Dialect Differences: Be aware of dialect-specific limitations, especially for SQLite.
-
Transaction Safety: Wrap schema changes in transactions when possible (though some operations like
DROP COLUMNin MySQL can't be rolled back). -
Version Control: Use migrations (see Database Migrations) for schema version control.
use tommyknocker\pdodb\PdoDb;
$db = new PdoDb('mysql', [
'host' => 'localhost',
'username' => 'user',
'password' => 'pass',
'dbname' => 'blog'
]);
$schema = $db->schema();
// Drop table if exists (cleanup)
$schema->dropTableIfExists('posts');
// Create posts table
$schema->createTable('posts', [
'id' => $schema->primaryKey(),
'user_id' => $schema->integer()->notNull(),
'title' => $schema->string(255)->notNull(),
'slug' => $schema->string(255)->notNull()->unique(),
'content' => $schema->text(),
'excerpt' => $schema->text(),
'status' => $schema->string(20)->defaultValue('draft'),
'views' => $schema->integer()->defaultValue(0)->unsigned(),
'published_at' => $schema->timestamp()->null(),
'created_at' => $schema->timestamp()->defaultExpression('CURRENT_TIMESTAMP'),
'updated_at' => $schema->timestamp()->defaultExpression('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
], [
'engine' => 'InnoDB',
'charset' => 'utf8mb4',
'collate' => 'utf8mb4_unicode_ci',
]);
// Create indexes
$schema->createIndex('idx_posts_user_id', 'posts', 'user_id');
$schema->createIndex('idx_posts_slug', 'posts', 'slug', true);
$schema->createIndex('idx_posts_status', 'posts', 'status');
// Add foreign key
$schema->addForeignKey(
'fk_posts_user',
'posts',
'user_id',
'users',
'id',
'CASCADE',
'RESTRICT'
);
echo "Table 'posts' created successfully\n";- DDL Query Builder Basics - Basic DDL operations
- Advanced DDL - Advanced DDL features (Yii2-style constraints, partial indexes, fulltext/spatial indexes)
- Dialect-Specific Types - MySQL ENUM/SET, PostgreSQL UUID/JSONB/arrays, MSSQL UNIQUEIDENTIFIER, SQLite type affinity
- Schema Introspection - Querying existing schema
- Database Migrations - Version-controlled schema changes
- Dialect Support - Database-specific differences