staudenmeir/laravel-cte
Adds Common Table Expression (CTE) support to Laravel’s query builder and Eloquent. Build WITH and WITH RECURSIVE queries, materialized CTEs, custom columns and cycle detection, plus CTEs for insert/update/delete. Works across major databases (MySQL, Postgres, SQLite, SQL Server, Oracle).
Installation:
composer require staudenmeir/laravel-cte
For Laravel 5.5–5.7, add the QueriesExpressions trait to Eloquent models:
use \Staudenmeir\LaravelCte\Eloquent\QueriesExpressions;
First Query: Define a CTE and use it in a join:
$posts = DB::table('posts as p')
->select('p.*', 'u.name')
->withExpression('u', DB::table('users'))
->join('u', 'u.id', '=', 'p.user_id')
->get();
Replace a recursive PHP loop with a recursive CTE for a category tree:
$query = DB::table('categories')
->whereNull('parent_id')
->unionAll(
DB::table('categories')
->select('categories.*')
->join('tree', 'tree.id', '=', 'categories.parent_id')
);
$tree = DB::table('tree')
->withRecursiveExpression('tree', $query)
->get();
CTE Chaining: Chain CTEs for complex workflows (e.g., reporting):
$query = DB::table('orders')
->withExpression('users', DB::table('users')->select('id', 'name'))
->withExpression('recent_orders', function ($q) {
$q->from('orders')
->where('created_at', '>', now()->subMonth());
})
->join('users', 'users.id', '=', 'orders.user_id')
->join('recent_orders', 'recent_orders.user_id', '=', 'users.id');
Recursive Relationships:
Use withRecursiveExpression for nested data (e.g., comments):
$comments = DB::table('comments')
->whereNull('parent_id')
->unionAll(
DB::table('comments')
->select('comments.*')
->join('tree', 'tree.id', '=', 'comments.parent_id')
)
->withRecursiveExpression('tree', $query)
->get();
Materialized CTEs (PostgreSQL/SQLite): Optimize performance for large datasets:
$query = DB::table('posts')
->withMaterializedExpression('popular_posts', DB::table('posts')
->where('views', '>', 1000)
->orderBy('views', 'desc')
)
->join('popular_posts', 'popular_posts.id', '=', 'posts.id');
Model-Level CTEs: Extend Eloquent models with CTEs:
class Post extends Model {
public function scopeWithAuthor($query) {
return $query->withExpression('author', DB::table('users')
->whereColumn('users.id', 'posts.user_id')
);
}
}
Recursive Relationships:
Pair with staudenmeir/laravel-adjacency-list for tree structures:
class Category extends Model {
use \Staudenmeir\LaravelCte\Eloquent\QueriesExpressions;
use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
public function children() {
return $this->hasMany(Category::class, 'parent_id');
}
}
// Update
DB::table('profiles')
->withExpression('u', DB::table('users')->select('id', 'name'))
->join('u', 'u.id', '=', 'profiles.user_id')
->update(['name' => DB::raw('u.name')]);
// Delete
DB::table('inactive_users')
->withExpression('u', DB::table('users')->where('last_login', '<', now()->subYear()))
->whereIn('id', DB::table('u')->select('id'))
->delete();
Database Compatibility:
withMaterializedExpression().withRecursiveExpressionAndCycleDetection().Column Mismatches: Ensure CTE columns match the main query’s schema to avoid SQL errors:
// ❌ Error: Column 'name' not found
DB::table('posts')
->withExpression('u', DB::table('users')->select('id'))
->join('u', 'u.id', '=', 'posts.user_id')
->select('posts.*', 'u.name'); // 'name' missing in CTE
// ✅ Fix: Include all required columns
DB::table('posts')
->withExpression('u', DB::table('users')->select('id', 'name'))
->join('u', 'u.id', '=', 'posts.user_id')
->select('posts.*', 'u.name');
Oracle/Lumen Quirks:
$builder = new \Staudenmeir\LaravelCte\Query\OracleBuilder(DB::connection());
QueriesExpressions trait in Eloquent models.Performance:
WITH RECURSIVE explicitly if needed.Inspect Raw SQL:
Use toSql() and getBindings() to debug CTEs:
$sql = $query->toSql();
$bindings = $query->getBindings();
Cycle Detection: For infinite loops, enable cycle detection:
$query = DB::table('tree')
->withRecursiveExpressionAndCycleDetection('tree', $recursiveQuery, 'id')
->get();
PostgreSQL-Specific:
EXPLAIN ANALYZE to profile materialized CTEs:
EXPLAIN ANALYZE SELECT * FROM (WITH cte AS (...)) main_query;
Custom Grammar:
Override grammar for unsupported databases by extending \Staudenmeir\LaravelCte\Query\Grammars\Grammar.
Query Macros: Add reusable CTE patterns:
DB::macro('withUserData', function ($query) {
return $query->withExpression('u', DB::table('users')->select('id', 'name'));
});
Cycle Detection Customization (PostgreSQL): Rename cycle detection columns:
$query->withRecursiveExpressionAndCycleDetection(
'tree',
$recursiveQuery,
'id',
'is_cycle_detected', // Custom cycle column name
'path' // Custom path column name
);
Hybrid Queries: Combine CTEs with Laravel’s query builder features:
$query = DB::table('orders')
->withExpression('u', DB::table('users'))
->join('u', 'u.id', '=', 'orders.user_id')
->whereHasMorph('orders', [Product::class, Service::class]);
Dynamic CTEs: Build CTEs dynamically based on input:
$cte = DB::table('products')
->where('category_id', $request->category_id);
$query = DB::table('products as p')
->withExpression('filtered', $cte)
->join('filtered', 'filtered.id', '=', 'p.id');
Testing:
Use DB::shouldReceive('select')->with(...) to test CTEs in PHPUnit:
DB::shouldReceive('select')
->once()
->with('SELECT * FROM (WITH cte AS (...)) main_query');
How can I help you explore Laravel packages today?