Comparison
U's Database layer is a port of
Qbix's Db_Row, Db_Query and Db_Relation, with one
organising idea: anything whose inputs are static should happen at
build time. This page is honest about which parts of that are done.
compile means the compiler decides it and a mistake fails the build. run means it happens per query, as in PHP.
.get()?"Yes — but not for convenience. If everything ends up as SQL anyway, a structured API looks like ceremony. It isn't, because structure is the only thing a compiler can reason about. A hand-written SQL string is opaque: you cannot ask it which index it needs, which shard it belongs to, or whether its joins are ordered correctly. A structured query you can.
| What it buys | Structured query | Raw SQL`…` |
|---|---|---|
| Injection safety | yes | yes — slots are bound parameters |
| Index coverage checkable | yes — the predicate is a value | no — it is text |
| Shard routing | yes | no |
| Joins derived from foreign keys | yes | no — you write them |
| Composable in pieces | yes | no — the text is fixed |
| Joins, aggregates, CTEs, window functions | no | yes |
So they are not competitors. The structured path is the checkable one
and should cover ordinary CRUD; SQL`…` is the expressive escape
hatch for the rest. Every mature system lands here — ActiveRecord and
find_by_sql, Doctrine and native queries, Qbix's own Db_Query
alongside raw Db::query. What is unusual in U is that the escape hatch is
also parameterised, so taking it costs expressiveness but not safety.
| Concern | Qbix | U | When |
|---|---|---|---|
| Named connections | Db::connect('main') | Config[Database.Connection.MySQL]`db.main` | compile |
| Which engine | adapter chosen at runtime from config | the connection's TYPE; the config's engine string is a cross-check | compile |
| Placeholders | adapter emits ? / $n per query | SQL[main]`…` emits ? or $1 directly | compile |
| Credentials | config/… merged at runtime | .local/app.json, never read by the build | run |
| Engine changes visible | a config edit, silently | u.lock.json — shows as a diff | compile |
| Concern | Qbix | U | When |
|---|---|---|---|
| Generated base | Base_Streams_Stream | Base.Streams.Stream : Database.Row | compile |
| Developer subclass | Streams_Stream extends Base_… | same shape, extends the generated class | — |
| Regeneration | scripts/Q/models.php | u2c schema pull | compile |
| Engine-agnostic model | yes — Db_Row knows no engine | yes — deliberately non-generic | — |
| Schema source | live database | live database or checked-in DDL | compile |
| Field types / lengths | beforeSet_$field at runtime | declared types (S(64), +N) on the generated class | compile |
| Drift detection | — | verify_schema_class_sync | compile |
Qbix's Db_Relation::compile() does not ask for a join order; it
derives one. Find the root table (referenced but never referencing),
level breadth-first, and the levels are the join order. Every input is
static, so U does it at build time.
| Concern | Qbix | U | When |
|---|---|---|---|
| Relation source | hasOne/hasMany declarations | the DDL's FOREIGN KEYs, parsed | compile |
| Root discovery | compile(), per query | root_store(), once | compile |
| Join ordering | BFS levels, per query | BFS levels, once; emitted flat | compile |
| Two roots | runtime exception | build error naming the tables | compile |
| Circular relation | runtime exception | build error | compile |
| Ambiguous join keys | — | validate() diagnostic | compile |
Verified rather than claimed: the joins derived from
posts→users and comments→posts run on SQLite,
PostgreSQL 16 and MariaDB 10.11 and return identical rows, including the
LEFT JOIN NULL for a parent with no grandchildren. Note what that
exposed: the DDL is dialect-specific (SQLite rejects MySQL's inline
KEY name (cols)), but the relation graph is not, because
FOREIGN KEY … REFERENCES is spelled the same everywhere.
| Concern | Qbix | U | When |
|---|---|---|---|
| Index metadata | $class::indexes() | indexes: [S] +G = ["id","publisherId,name"] on the generated class | compile |
| Is a field indexed | isIndexed() — adapter round trip | read from the class | compile |
| Missing-index warning | signalMissingIndex() — after the query runs | the leftmost-prefix check exists (index_covers); not yet wired to the generated indexes | compile |
| Shard map | "indexes": {Class: {fields, partition}} | not implemented | — |
| Concern | Qbix | U | When |
|---|---|---|---|
| Value interpolation | bound parameters | bound parameters — a slot becomes $n/?, the value never enters the text | compile |
| Table/column interpolation | developer discipline | structural position is a build error — it cannot be parameterised | compile |
| XSS in views | Q_Html helpers | HTML`…` chokepoint; on* interpolation refused | compile |
| Ranges | Db_Range | Database.Range type registered; no type-vs-column check yet | — |
The query builder and row lifecycle are implemented end-to-end. The equivalent of Qbix's PHP:
Db::connect('main')->select('*')->from('users')
->where(array('id' => 5))->fetchDbRows();In U:
users = Database.Query({ store: "users" })
.select(["name", "email", "score"])
.where("id", "=", 5)
.fetchAll()| Feature | State |
|---|---|
Database.Query.where() / .select() / .orderBy() / .limit() | Works — codegen lowers to u_dbq_* C runtime |
Database.Row.save() / .retrieve() / lifecycle | Works — change tracking, deep-copy snapshot |
Terminal operations: .fetchAll(), .fetchRow(), .execute() | Works — +A fiber-aware |
Dialect-aware SQL ($1 for Postgres, ? for MySQL) | Works — verified against three engines |
| Injection safety — all values as parameters, never concatenation | Works — both template and builder paths |
Schema generation from DDL (schema pull) | Works — generates typed model classes |
Transactions via << ( ) with Database.Row | Works — multi-row atomic commits |
hasOne / hasMany relations | Relations from DDL foreign keys; no explicit declaration syntax yet |
| Connection registry | Wire clients (Postgres, MySQL, SQLite) exist; connection routing not yet configurable from U |
The honest summary: the query builder, row lifecycle, injection safety, and
dialect handling all work. The SQL`…` template is still available
as the raw-SQL escape hatch with the same safety guarantees (compile-time validation,
parameterized slots), but the everyday API is the builder chain — the same pattern
Qbix developers already know.
Qbix computes relations, index checks and dialect choices per query, at runtime, and reports mistakes as exceptions or after-the-fact signals. U computes the same things once, at build time, and reports mistakes as build errors. That is the whole of the difference — everything above is a consequence of it.