JOIN clause compilation
Advanced5 min read·fw-07-004
sql
Concept
JOIN clause compilation extends the Query Builder to support SQL JOIN operations. JOINs combine rows from multiple tables based on a related column. The Query Builder must track join definitions and compile them into the correct SQL fragment.
JOIN types:
INNER JOIN(orJOIN): Only rows where the join condition matches in BOTH tables.LEFT JOIN: All rows from the left table, NULL for right-table columns when no match.RIGHT JOIN: All rows from the right table, NULL for left-table columns when no match.CROSS JOIN: Cartesian product — every row from left × every row from right. Rare.
Join condition: ON left_table.column = right_table.column. Can also include additional AND conditions.
Query Builder join() API:
join($table, $first, $operator, $second): INNER JOIN.leftJoin($table, $first, $operator, $second): LEFT JOIN.joinSub($query, $alias, ...): Join against a subquery.joinWhere($table, $first, $operator, $value): Join where the second operand is a literal value (not a column).
Building the SQL: The Grammar's compileJoins(array $joins) method iterates the join definitions and builds the SQL fragment: INNER JOIN users ON posts.user_id = users.id.
Alias support: JOIN users AS u ON posts.user_id = u.id — use aliases to disambiguate columns when joining a table multiple times.
Code Example
php
<?php
namespace Framework\Database;
class JoinClause
{
private array $ons = [];
public function __construct(
public readonly string $type, // 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN'
public readonly string $table,
) {}
public function on(string $first, string $operator, string $second): static
{
$this->ons[] = ['type' => 'on', 'first' => $first, 'operator' => $operator, 'second' => $second];
return $this;
}
public function andOn(string $first, string $operator, string $second): static
{
return $this->on($first, $operator, $second);
}
public function getOns(): array { return $this->ons; }
}
class QueryBuilder
{
private array $joins = [];
public function join(string $table, string $first, string $operator, string $second): static
{
$join = new JoinClause('INNER JOIN', $table);
$join->on($first, $operator, $second);
$this->joins[] = $join;
return $this;
}
public function leftJoin(string $table, string $first, string $operator, string $second): static
{
$join = new JoinClause('LEFT JOIN', $table);
$join->on($first, $operator, $second);
$this->joins[] = $join;
return $this;
}
}
class Grammar
{
public function compileJoins(array $joins): string
{
return implode(' ', array_map(function(JoinClause $join) {
$ons = array_map(
fn($on) => "{$on['first']} {$on['operator']} {$on['second']}",
$join->getOns()
);
return "{$join->type} {$join->table} ON " . implode(' AND ', $ons);
}, $joins));
}
}
// Usage
$query = $builder
->table('posts')
->select(['posts.id', 'posts.title', 'users.name AS author_name', 'users.email AS author_email'])
->join('users', 'posts.user_id', '=', 'users.id')
->leftJoin('categories', 'posts.category_id', '=', 'categories.id')
->where('posts.status', '=', 'published')
->orderBy('posts.published_at', 'DESC');
// Compiled:
// SELECT posts.id, posts.title, users.name AS author_name, users.email AS author_email
// FROM posts
// INNER JOIN users ON posts.user_id = users.id
// LEFT JOIN categories ON posts.category_id = categories.id
// WHERE posts.status = ?
// ORDER BY posts.published_at DESC