Skip to main content

Expressions

Every column, table, and group in a query is an Expression. It is a marker interface with three implementations, and which one you get decides whether the database sees a quoted name or the characters you wrote.

ImplementationCompiles toUse for
IdentifierA quoted name, one segment per dotColumns, tables, anything the database should look up by name.
RawExpressionThe string as written, plus its own bindingsFunctions, operators, anything the builder has no clause for.
Aliased<expression> AS <quoted alias>Naming either of the above.

Strings become identifiers

Anywhere a method takes string|Expression, a string is read by ExpressionFactory::from(). It produces an Identifier — or an Aliased one when it finds an alias — and never a RawExpression.

$database->table('users')->select('name'); // SELECT `name`
$database->table('users')->select('users.name'); // SELECT `users`.`name`
$database->table('users')->select('users.*'); // SELECT `users`.*
$database->table('users')->select('name as who'); // SELECT `name` AS `who`

Each dot-separated segment is quoted independently, so a qualified name works without you escaping anything, and * passes through as itself.

danger

A string is never read as SQL. That is deliberate: if select() promoted a string containing ( to raw SQL, then a column name that arrived from a request would be emitted verbatim into the query. Raw SQL is always explicit.

Because of that rule, a fragment passed as a string is caught rather than quoted into nonsense:

$database->table('users')->select('COUNT(*)');
// InvalidArgumentException: The identifier [COUNT(*)] looks like SQL rather
// than a name; use a raw expression instead.

Identifier rejects a name containing (, ) or ,, and a name with an empty segment such as users. or a..b. It does not reject anything else, because a quoted identifier may legitimately contain almost anything — first name is a valid column on all four databases and is quoted correctly.

Raw expressions

RawExpression is emitted exactly as written, and carries its own bindings:

use Dirthara\Database\Query\Expression\RawExpression;

$database->table('users')
->select(new RawExpression('COALESCE(name, ?) AS name', ['unknown']))
->where('active', '=', 1);

// SELECT COALESCE(name, ?) AS name FROM `users` WHERE `active` = ?
// bindings: ['unknown', 1]

The bindings are interleaved in the order the SQL emits each clause — select, then joins, then conditions, then groups, then havings, then ordering — so a bound fragment in the select list keeps its values ahead of the conditions'.

Shorthands exist for the common positions:

$database->table('users')
->selectRaw('COUNT(*) AS total')
->groupByRaw('DATE(created_at)')
->orderByRaw('FIELD(status, ?, ?)', ['live', 'draft']);

A whole raw condition is a different thing — a RawExpression fills one slot of a comparison, while whereRaw() and havingRaw() replace the comparison itself. Reach for those when the operator you need is not one the builder knows.

A raw expression works in a condition's column position too:

$database->table('users')->where(new RawExpression('LOWER(name)'), '=', 'ada');
caution

Nothing in a RawExpression is escaped or validated. Never build one by concatenating input — put a ? in the SQL and pass the value in the bindings array, which is what the second argument is for.

Aliases

An alias can be parsed from a string or built explicitly. Both quote the alias:

use Dirthara\Database\Query\Expression\Aliased;
use Dirthara\Database\Query\Expression\Identifier;

$database->table('users as u')->select('u.name');
$database->table(new Aliased(new Identifier('users'), 'u'))->select('u.name');

// FROM `users` AS `u`

Parsing splits on a whitespace-delimited AS, case-insensitively. A string with two of them throws rather than guessing which one is the alias.

An alias makes a self-join possible, since both sides need their own name:

$database->table('users as a')
->select('a.name')
->join('users as b', 'a.manager_id', '=', 'b.id');

Aliased wraps a raw expression too, which is the portable way to name a computed column:

$database->table('users')->select(new Aliased(new RawExpression('COUNT(*)'), 'total'));

// SELECT COUNT(*) AS `total`
note

An alias only makes sense where SQL allows one — a select column, a table, a join. Nothing stops you passing an Aliased to where(), and nothing will catch it, because every slot takes the same Expression type. The result is invalid SQL from the database rather than an exception from here.

Building expressions yourself

ExpressionFactory::from() is what the builder calls, and it is safe to call directly when you want to normalise a value once:

use Dirthara\Database\Query\Expression\ExpressionFactory;

ExpressionFactory::from('users.name'); // Identifier
ExpressionFactory::from('users as u'); // Aliased(Identifier, 'u')
ExpressionFactory::from(new RawExpression('NOW()')); // returned unchanged

An Expression passed in comes back as it is, so a value can go through the factory more than once without changing.

Table names

Tables are expressions too, which is what makes aliases work. The same rules apply: a string is a name and gets quoted, so a table alias has to be an alias rather than a hand-written fragment.

$database->table('app.users'); // FROM `app`.`users`
$database->table('users as u'); // FROM `users` AS `u`

For a table-valued function or anything else a name cannot describe, use a raw expression — and note it can carry bindings, which land before the conditions:

$database->table(new RawExpression('history(?) AS h', ['2026-01-01']))
->select('h.name')
->where('h.active', '=', 1);

// FROM history(?) AS h WHERE `h`.`active` = ?
// bindings: ['2026-01-01', 1]

A note on quoting and case

Quoting a name preserves its case, and PostgreSQL folds unquoted names to lower case. A column created as userId is stored as userid unless it was created quoted, so select('userId') compiles to "userId" and will not find it. That is standard behaviour for a builder that quotes, not something this package decides — but it is the difference that surprises people moving a query from MySQL to PostgreSQL.