The query builder is the lower-level fluent API for SQL-family work.
Most day-to-day feature code should still start with repositories or the entity manager. The query builder becomes useful when:
- repository find options are not expressive enough
- you need precise SQL control
- you want dialect-specific features such as PostgreSQL
DISTINCT ON, MySQL and MariaDBON DUPLICATE KEY UPDATE, or SQL ServerTOP
The key architectural idea
The SQL query builder now has typed dialect roots.
That means:
switchToMysql()returns a MySQL-flavoured builderswitchToMariaDb()returns a MariaDB-flavoured builderswitchToPostgres()returns a PostgreSQL-flavoured builderswitchToSqlite()returns a SQLite-flavoured builderswitchToMsSql()returns an MSSQL-flavoured builder
This matters because the fluent path can stay honest. Once you switch into a dialect, you can follow methods that make sense for that dialect without pretending every backend supports the same syntax.
Creating a root query
Start from a live data source:
<?php
use Assegai\Orm\Queries\Sql\SQLQuery;
$query = SQLQuery::forConnection(
db: $dataSource->getClient(),
dialect: $dataSource->getDialect(),
);
SQLQuery::forConnection(...) inspects the connection and gives you the right SQL-family root builder.
A basic SELECT example
<?php
$result = $query
->select()
->all(['id', 'title', 'is_now_showing'])
->from('movies')
->where(['is_now_showing' => true])
->orderBy(['id' => 'DESC'])
->limit(20)
->execute();
$rows = $result->getData();
Read that chain in order:
select()begins aSELECTall([...])chooses the columnsfrom('movies')chooses the tablewhere([...])adds conditionsorderBy([...])sorts the resultslimit(20)constrains the result sizeexecute()runs the SQL
PostgreSQL-specific fluency
Once you switch to PostgreSQL, you can use PostgreSQL-only fluency such as distinctOn(...).
<?php
$latestPerCity = $query
->switchToPostgres()
->select()
->distinctOn(['city'])
->all(['city', 'name', 'created_at'])
->from('cinemas')
->orderBy([
'city' => 'ASC',
'created_at' => 'DESC',
])
->execute()
->getData();
That is exactly the kind of case typed dialect builders are for. DISTINCT ON is a PostgreSQL idea, so it belongs on the PostgreSQL branch.
MySQL and MariaDB fluency
The MySQL family can expose methods such as highPriority() and onDuplicateKeyUpdate(...).
<?php
$query
->switchToMysql()
->select()
->highPriority()
->all(['id', 'title'])
->from('movies')
->where(['is_now_showing' => true])
->limit(10)
->execute();
For insert-style workflows:
<?php
$query
->switchToMysql()
->insertInto('movies')
->singleRow(['title', 'synopsis'])
->values(['Harbor Lights', 'A missing reel returns to circulation.'])
->onDuplicateKeyUpdate([
'synopsis = VALUES(synopsis)',
])
->execute();
The MariaDB root follows the same family shape:
<?php
$query
->switchToMariaDb()
->insertInto('movies')
->singleRow(['title', 'synopsis'])
->values(['Evening Signal', 'A projectionist starts hearing coded broadcasts.'])
->onDuplicateKeyUpdate([
'synopsis = VALUES(synopsis)',
])
->execute();
MSSQL-specific fluency
The MSSQL branch can expose SQL Server ideas such as top(...).
<?php
$recentMovies = $query
->switchToMsSql()
->select()
->top(5)
->all(['id', 'title', 'created_at'])
->from('movies')
->orderBy(['created_at' => 'DESC'])
->execute()
->getData();
That keeps SQL Server-specific selection on the SQL Server branch instead of treating it as generic SQL.
PostgreSQL delete with RETURNING
The PostgreSQL branch can also expose RETURNING on deletes:
<?php
$deleted = $query
->switchToPostgres()
->deleteFrom('showtimes')
->where(['id' => 42])
->returning(['id', 'movie_id'])
->execute()
->getData();
That kind of fluent branch is exactly why the dialect-root design matters.
When not to use the query builder
Do not reach for the query builder just because it feels powerful.
Prefer a repository or entity manager when:
- you are doing normal CRUD on one entity
- you want relation loading through find options
- you want the ORM to keep more of the persistence logic aligned with entity metadata
Use the query builder when the SQL itself is the important part of the task.
Practical advice
- Start with repositories.
- Move to the entity manager when the workflow spans multiple entities.
- Drop to the query builder when SQL precision or dialect-specific fluency matters.
- Treat
switchTo...as a real branch into a typed dialect path, not just a string toggle.
Next steps
To understand how these branches are configured in real apps, continue with Drivers.