Skip to content

A non-dotted orderBy naming a formula field answers 200 in arbitrary order — the sort is silently dropped (measured on driver-sql + driver-memory) #6994

Description

@os-zhuang

Found while measuring #6924's premise (the SORT-axis hint text). Recorded, not claimed. Routing suggestion: domain:engine-core — this is an engine/driver refusal question, not hint text, and #6924's dispatch explicitly scoped it out rather than absorbing it.

The defect

assertSortFieldsExist (packages/metadata-protocol/src/protocol.ts) refuses a dotted orderBy (?sort=account.company_name, #4256) and an unknown field (#4226). It does not refuse a known, non-dotted field that happens to be formula-typed — a formula field is in schema.fields, so it is in gate.known and passes.

It then reaches a driver that has no column for it, and the sort is silently dropped. Query succeeds, all rows present, order arbitrary — the exact failure mode #4226/#4256 exist to stop, on a shape no gate covers.

Measurement

Real SqlDriver (better-sqlite3, on-disk) and real InMemoryDriver, both wired to a real ObjectQL engine, with ObjectStackProtocolImplementation on top for the ingress half. Object carries a formula field sort_key (expression record.title). Five rows inserted C A E B D so "sorted" and "insertion order" are distinguishable.

driver-sql (better-sqlite3):

repro_contact physical columns: ["id","created_at","updated_at","title","seq","account_id","child_total"]
formula col `sort_key` present: false

CONTROL   orderBy title asc     -> ["A","B","C","D","E"]        a real column really sorts
CONTROL   orderBy title desc    -> ["E","D","C","B","A"]
BASELINE  no sort               -> ["C","A","E","B","D"]        insertion order

FORMULA   orderBy sort_key asc  -> ["C","A","E","B","D"]   5 rows, 200
                sort_key values -> ["C","A","E","B","D"]
FORMULA   orderBy sort_key desc -> ["C","A","E","B","D"]   direction-blind

RAW SQL   order by sort_key     -> sqlite: "no such column: sort_key"

PROTOCOL  ?sort=sort_key        -> 200, 5 records, ["C","A","E","B","D"]
PROTOCOL  ?orderBy=["sort_key"] -> 200, 5 records, ["C","A","E","B","D"]

driver-memory:

CONTROL   orderBy title asc     -> ["A","B","C","D","E"]
BASELINE  no sort               -> ["C","A","E","B","D"]
FORMULA   orderBy sort_key asc  -> ["C","A","E","B","D"]
FORMULA   orderBy sort_key desc -> ["C","A","E","B","D"]

asc and desc returning the same order is what makes this a dropped sort rather than a coincidence.

Mechanism

  • SqlDriver.createColumncase 'formula': return; // Virtual — no column. schema-drift.ts's fieldHasColumn() agrees: formula is the only type with no physical column.
  • engine.find rewrites ast.fields to drop virtual formula names and inject their dependencies (planFormulaProjection), but leaves ast.sort / ast.orderBy untouched; formulas are evaluated post-query by applyFormulaPlan, after driver.find has already returned.
  • sqlite rejects the ORDER BY; the fix(sharing): 共享规则新建页 — 自定义 widget 未国际化,且「接收方」永远无可选项 #3821 unknown-column backstop retries without the sort.

The response even carries the sort_key values in plain view — C, A, E, B, D — in a column the caller asked to be sorted ascending, so the answer visibly contradicts the request and still reports success.

Worth noting for scoping: summary/rollup is not affected. It gets a real, maintained column (case 'summary': col = table.float(name)) and sorts correctly — measured orderBy child_total desc -> E D C B A over values 5 4 3 2 1. Only formula is virtual.

Why it is not fixed in #6924

#6924's scope is the refusal hint text (domain:metadata) — it stopped the hint prescribing a formula field, since following that advice landed the author in this defect one step after being refused for the dotted form. The remaining question is a different decision on a different seat:

  1. Refuse at ingressassertSortFieldsExist rejects an orderBy naming a field whose type materializes no column (400 INVALID_SORT), consistent with how it already treats dotted paths. Cheap, and it keeps the whole REST 列表:sort / select / expand 指向不存在的字段时被静默丢弃(filter 轴已收口,这三条轴还没有) #4226 family on one door.
  2. Refuse at the driver — the driver reports that it cannot order by the column instead of letting the fix(sharing): 共享规则新建页 — 自定义 widget 未国际化,且「接收方」永远无可选项 #3821 backstop swallow it. Catches internal callers reaching engine.find() directly, which the ingress gate deliberately does not cover.
  3. Materialize — actually give formula fields a stored column. Much larger, and probably contradicts the "formula is virtual" contract the codebase states in several places.

Option 1 has a wrinkle worth flagging before anyone implements it: the gate reads gate.fields[...], so it can see the type — but the refusal would need to name the same remedy the corrected #6924 hint gives ("a stored field"), or the two doors will disagree again.

Refs: #6924 (the hint text, in flight), #6673 (same correction, search axis), #4226, #4256, #3821 (the backstop that swallows it).

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions