Comparison

Qbix and U, side by side

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.

The short answer to "do we still need .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 buysStructured queryRaw SQL`…`
Injection safetyyesyes — slots are bound parameters
Index coverage checkableyes — the predicate is a valueno — it is text
Shard routingyesno
Joins derived from foreign keysyesno — you write them
Composable in piecesyesno — the text is fixed
Joins, aggregates, CTEs, window functionsnoyes

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.

Connection and dialect

ConcernQbixUWhen
Named connectionsDb::connect('main')Config[Database.Connection.MySQL]`db.main`compile
Which engineadapter chosen at runtime from configthe connection's TYPE; the config's engine string is a cross-checkcompile
Placeholdersadapter emits ? / $n per querySQL[main]`…` emits ? or $1 directlycompile
Credentialsconfig/… merged at runtime.local/app.json, never read by the buildrun
Engine changes visiblea config edit, silentlyu.lock.json — shows as a diffcompile

Model classes

ConcernQbixUWhen
Generated baseBase_Streams_StreamBase.Streams.Stream : Database.Rowcompile
Developer subclassStreams_Stream extends Base_…same shape, extends the generated class
Regenerationscripts/Q/models.phpu2c schema pullcompile
Engine-agnostic modelyes — Db_Row knows no engineyes — deliberately non-generic
Schema sourcelive databaselive database or checked-in DDLcompile
Field types / lengthsbeforeSet_$field at runtimedeclared types (S(64), +N) on the generated classcompile
Drift detectionverify_schema_class_synccompile

Relations and joins — where U differs most

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.

ConcernQbixUWhen
Relation sourcehasOne/hasMany declarationsthe DDL's FOREIGN KEYs, parsedcompile
Root discoverycompile(), per queryroot_store(), oncecompile
Join orderingBFS levels, per queryBFS levels, once; emitted flatcompile
Two rootsruntime exceptionbuild error naming the tablescompile
Circular relationruntime exceptionbuild errorcompile
Ambiguous join keysvalidate() diagnosticcompile

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.

Indexes and sharding

ConcernQbixUWhen
Index metadata$class::indexes()indexes: [S] +G = ["id","publisherId,name"] on the generated classcompile
Is a field indexedisIndexed() — adapter round tripread from the classcompile
Missing-index warningsignalMissingIndex()after the query runsthe leftmost-prefix check exists (index_covers); not yet wired to the generated indexescompile
Shard map"indexes": {Class: {fields, partition}}not implemented

Safety

ConcernQbixUWhen
Value interpolationbound parametersbound parameters — a slot becomes $n/?, the value never enters the textcompile
Table/column interpolationdeveloper disciplinestructural position is a build error — it cannot be parameterisedcompile
XSS in viewsQ_Html helpersHTML`…` chokepoint; on* interpolation refusedcompile
RangesDb_RangeDatabase.Range type registered; no type-vs-column check yet

What U has now

The query builder and row lifecycle are implemented end-to-end. The equivalent of Qbix's PHP:

PHP — Qbix today
Db::connect('main')->select('*')->from('users')
  ->where(array('id' => 5))->fetchDbRows();

In U:

U — compiles to parameterized SQL
users = Database.Query({ store: "users" })
    .select(["name", "email", "score"])
    .where("id", "=", 5)
    .fetchAll()
FeatureState
Database.Query.where() / .select() / .orderBy() / .limit()Works — codegen lowers to u_dbq_* C runtime
Database.Row.save() / .retrieve() / lifecycleWorks — 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 concatenationWorks — both template and builder paths
Schema generation from DDL (schema pull)Works — generates typed model classes
Transactions via << ( ) with Database.RowWorks — multi-row atomic commits
hasOne / hasMany relationsRelations from DDL foreign keys; no explicit declaration syntax yet
Connection registryWire 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.

The one-line difference

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.