staudenmeir/laravel-cte
Adds Common Table Expression (CTE) support to Laravel’s query builder and Eloquent. Build WITH and recursive CTE queries (plus materialized, custom columns, cycle detection) across MySQL, Postgres, SQLite, SQL Server, MariaDB, Oracle, and SingleStore.
Install the package:
composer require staudenmeir/laravel-cte
For Laravel 13.x, use ^1.13.0; for 12.x, use ^1.12.0, etc. (see version compatibility).
First use case: Replace a nested subquery with a CTE for a simple join:
$posts = DB::table('posts')
->withExpression('users', DB::table('users')->select('id', 'name'))
->join('users', 'users.id', '=', 'posts.user_id')
->get();
withExpression() for basic CTEs and withRecursiveExpression() for hierarchical data.WITH RECURSIVE).QueriesExpressions trait (Laravel 5.5–5.7 only).$categories = DB::table('categories')
->whereNull('parent_id')
->unionAll(
DB::table('categories')
->select('categories.*')
->join('tree', 'tree.id', '=', 'categories.parent_id')
)
->withRecursiveExpression('tree', $query)
->get();
withRecursiveExpressionAndCycleDetection() for PostgreSQL/MySQL 10.5.2+ to avoid infinite loops.$query = DB::table('orders')
->select('customer_id', DB::raw('sum(amount) as total'))
->groupBy('customer_id');
$results = DB::table('customers')
->withMaterializedExpression('order_totals', $query)
->join('order_totals', 'order_totals.customer_id', '=', 'customers.id')
->get();
DB::table('profiles')
->withExpression('inactive_users', DB::table('users')->where('active', false))
->update(['last_updated' => now()])
->whereIn('user_id', DB::table('inactive_users')->select('id'));
QueriesExpressions trait:
class User extends Model {
use \Staudenmeir\LaravelCte\Eloquent\QueriesExpressions;
}
$tree = User::from('tree')
->withRecursiveExpression('tree', User::whereNull('parent_id')->unionAll(...))
->get();
AppServiceProvider for global CTE support:
public function boot() {
DB::extend('cte', function ($app, $config) {
return new \Staudenmeir\LaravelCte\Query\Builder($app['db']->connection());
});
}
$cte = DB::table('logs')
->where('created_at', '>', request('date'))
->select('user_id', DB::raw('count(*) as count'));
$results = DB::table('users')
->withExpression('recent_logs', $cte)
->join('recent_logs', 'recent_logs.user_id', '=', 'users.id')
->get();
$cte = DB::table('products')
->where('price', '>', 100)
->limit(1000); // Cap intermediate results
Database-Specific Syntax:
WITH RECURSIVE syntax (8.0+/10.5.2+). Older versions throw errors.WITH RECURSIVE (use WITH for non-recursive).OracleBuilder).Cycle Detection:
$query->where('depth', '<', 100); // Arbitrary depth limit
Column Mismatches:
// Bad: Implicit columns may not match.
$cte = DB::table('orders')->select('*');
// Good: Explicit columns.
$cte = DB::table('orders')->select('id', 'user_id', 'amount');
Eloquent Limitations:
QueriesExpressions trait for Eloquent support.Performance Quirks:
max_recursion_depth).toSql() to verify CTE syntax:
$query = DB::table('posts')->withExpression('users', ...);
dd($query->toSql());
$query->toSql(); // SQL
$query->getBindings(); // Bindings
Custom Grammar:
Override the grammar for database-specific tweaks (e.g., Oracle’s WITH syntax):
use Staudenmeir\LaravelCte\Query\Grammars\OracleGrammar;
DB::connection()->setQueryGrammar(new OracleGrammar());
Query Macros: Add reusable CTE patterns:
DB::macro('hierarchy', function ($table, $parentColumn) {
return DB::table($table)
->whereNull($parentColumn)
->unionAll(
DB::table($table)
->select('*')
->join('tree', 'tree.id', '=', $parentColumn)
);
});
// Usage:
$tree = DB::table('categories')
->withRecursiveExpression('tree', DB::hierarchy('categories', 'parent_id'))
->get();
Event Listeners: Hook into query events to modify CTEs dynamically:
DB::listen(function ($query) {
if ($query->hasCte('users')) {
$query->where('users.active', true);
}
});
$builder = new \Staudenmeir\LaravelCte\Query\Builder(app('db')->connection());
$results = $builder->withExpression('users', ...)->get();
OracleBuilder explicitly:
$builder = new \Sta
How can I help you explore Laravel packages today?