tpetry/laravel-postgresql-enhanced
Adds PostgreSQL-specific power to Laravel beyond the “lowest common denominator”: enhanced migrations (zero-downtime, extensions, functions, triggers, views/materialized views), advanced indexes (concurrent, partial, include, full-text, temporal), domains and table options.
composer require tpetry/laravel-postgresql-enhanced
config/database.php uses PostgreSQL as the default connection.use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->index('email', 'idx_users_email_active')->where('active', true);
});
}
};
Schema facade with PostgreSQL-specific methods (e.g., createExtension, createFunction).Tpetry\PostgresqlEnhanced\Schema\Blueprint in migrations for advanced column/index options.RETURNING clauses via DB::statement() or custom query methods.Schema Modifications:
Schema::table() with the enhanced Blueprint for zero-downtime changes:
Schema::table('orders', function (Blueprint $table) {
$table->string('status')->change()->nullable()->after('amount');
});
ZeroDowntimeMigration trait to auto-revert if the migration times out:
use Tpetry\PostgresqlEnhanced\Schema\Concerns\ZeroDowntimeMigration;
class UpdateOrderStatus extends Migration {
use ZeroDowntimeMigration;
protected float $timeout = 3.0; // 3-second timeout
}
Extensions and Functions:
pg_trgm for fuzzy matching) in a migration:
Schema::createExtension('pg_trgm');
Schema::createFunction(
'calculate_discount',
['price' => 'numeric', 'discount' => 'numeric'],
'numeric',
'sql:expression',
'price * (1 - discount)'
);
Advanced Indexes:
$table->index(['name', 'surname'], 'idx_user_name_surname')->using('gin');
$table->index('email', 'idx_user_email_trgm')->using('gin')->with('trgm_ops');
CTEs (Common Table Expressions):
DB::statement('
WITH active_users AS (
SELECT * FROM users WHERE active = true
)
SELECT * FROM active_users WHERE created_at > NOW() - INTERVAL \'1 year\'
');
Full-Text Search:
$results = DB::table('articles')
->where('content', '@@', 'laravel postgresql')
->get();
Lateral Joins:
$query = DB::table('orders')
->select('orders.*')
->joinSub(
DB::table('order_items')
->select('order_id', 'product_id')
->whereColumn('order_id', 'orders.id')
->limit(1),
'lateral_subquery',
function ($join) {
$join->on('lateral_subquery.order_id', '=', 'orders.id');
}
);
Custom Casts:
use Tpetry\PostgresqlEnhanced\Casts\JsonCast;
class User extends Model {
protected $casts = [
'metadata' => JsonCast::class,
'tags' => \Tpetry\PostgresqlEnhanced\Casts\ArrayCast::class,
];
}
Refresh Data on Save:
class Order extends Model {
protected $refreshOnSave = ['total', 'tax'];
}
IDE Autocomplete:
php artisan ide-helper:generate
Or manually add the PHPStan extension path to phpstan.neon:
includes:
- vendor/tpetry/laravel-postgresql-enhanced/phpstan-extension.neon
Zero-Downtime Migrations:
->ifNotExists() for indexes/triggers to avoid errors.Functional Indexes:
LOWER(name)) may not work as expected if the function is not deterministic.IMMUTABLE or STABLE:
Schema::createFunction('lower_name', ['name' => 'text'], 'text', 'sql:expression', 'LOWER(name)', [
'volatility' => 'immutable',
]);
PostgreSQL-Specific Syntax:
RETURNING *) must be executed via DB::statement() or DB::select().$updatedRows = DB::table('users')
->where('id', 1)
->update(['name' => 'New Name']);
$returning = DB::select('UPDATE users SET name = ? RETURNING *', ['New Name']);
Explain Queries:
Use the explain() method to analyze query plans:
$explanation = DB::table('users')->explain()->toSql();
Transaction Rollbacks: Wrap zero-downtime migrations in transactions to ensure atomicity:
DB::transaction(function () {
Schema::table('users', function (Blueprint $table) {
$table->string('email')->change();
});
});
Extension Conflicts:
extension "xyz" already exists.createExtensionIfNotExists() to avoid failures.Custom Query Methods:
Extend the query builder by adding methods to app/Providers/AppServiceProvider.php:
use Illuminate\Support\Facades\DB;
use Tpetry\PostgresqlEnhanced\Query\Builder;
public function boot(): void {
Builder::macro('pgFullTextSearch', function ($column, $query) {
return $this->where($column, '@@', $query);
});
}
Usage:
DB::table('articles')->pgFullTextSearch('content', 'laravel')->get();
Blueprint Extensions:
Add custom methods to the Blueprint class by publishing and modifying the package’s config:
php artisan vendor:publish --provider="Tpetry\PostgresqlEnhanced\PostgresqlEnhancedServiceProvider"
Then extend app/Providers/PostgresqlEnhancedServiceProvider.php.
TimescaleDB Support: For time-series data, enable the TimescaleDB extension:
Schema::createExtension('timescaledb');
Use timescaledb column types (e.g., timestamp with time zone) and leverage hypertable partitioning:
$table->timestampTz('created_at')->withTimePrecision(6);
Concurrent Indexes: Add indexes concurrently to avoid table locks:
$table->index('email')->concurrently();
Unlogged Tables:
Use unlogged tables for temporary data to skip WAL (Write-Ahead Logging):
$table->unlogged();
Storage Parameters: Optimize storage for large text columns:
$table->text('description')->storage('extended');
How can I help you explore Laravel packages today?