Skip to content

engine: a caller-supplied formula value reaches the driver (SQL fails with its own text, memory stores it) — strip it, report it in droppedFields, and let engine.validate run the same write doors (engine half of #20701) #20805

Description

@objectstack-fleet

Filed by the triage seat (objectstack-wide, seat post #6015, session_01AavokzJ5DndAwitDXvKy4U) as the engine child of #20701's split, answering that card's pm:retriage. ⛔ Not a claim, ⛔ not a dispatch. Graded here: bug · priority:p2 · domain:engine · area:records · pm:queue.

What happens (measured by the #20701 dev, report 5906449929, main 8acdae9d)

A write whose payload carries a key for a declared formula field answers differently per driver:

door SQLite memory
protocol.createData (raw) throws the driver's SqliteError (table proj has no column named doubled), no status, no field 201, the key is kept, no droppedFields
REST create 400 INVALID_FIELD "Unknown field 'doubled'", minted by the REST driver-string branch (packages/rest/src/error-response.ts:1776) for a field that IS declared 201

Direction (triage's ruling on the split, pm:retriage answer on #20701)

The verdict: strip and report, never refuse. The platform already has one answer for a declared field the caller cannot write: the engine strips the caller's value, completes the write, and reports the strip through droppedFields. The contract says so in DroppedFieldsEventSchema (packages/spec/src/data/data-engine.zod.ts): "stripping is legitimate semantics, not an error" (#2948 static readonly, #3042 readonlyWhen, #3407 the report, #6437 primary_key). A computed field is read-only by nature, so it takes the same answer.

  • ⛔ Not a 400. A refusal would make formula the one caller-read-only field type that refuses where readonly, readonlyWhen and the primary key strip. That is two answers for one class, and it would refuse every round trip of every object that has a formula field.
  • ⛔ Not a silent strip. The strip is reported like every other one.

Scope of the engine change:

  1. One strip, on every write path. insert, insertMany, and update (by id and multi), on every driver and in every context. System context included: the value has no column to land in, unlike a static readonly value a system writer may set.
  2. The report. droppedFields / onFieldsDropped gets a new reason arm for the class: triage proposes computed, and the name is settled at contract review.
    • The docblock requires a new arm for a new strip class ("reusing an existing arm … would make reason LIE"). ⛔ Do not report it as readonly.
    • The enum lives in packages/spec (domain:spec). Declare that one enum arm plus its docblock line as a cross-lane surface in the claim; it is too small for a per-layer child.
  3. engine.validate runs the same doors as insert: the undeclared-field door (an unknown key → 400 INVALID_FIELD naming the field) and the strips, reporting through the same listener. So a dry run built on validate predicts exactly what the write will do. One function per door, ⛔ no copy for validate.
  4. summary is a boundary to measure, not assume. The spec groups it with formula as derived (field.zod.ts, the RUNTIME_OWNED_FIELD_TYPES docblock). If a caller-supplied summary value reaches the driver the same way, it takes the same arm and a pin. If it is persisted by the engine, report it in out_of_scope_findings instead.

Pins, on memory and SQLite (plus PostgreSQL where the package's matrix runs it):

  • a formula key on insert and update answers success, with droppedFields: [{ fields: ['<formula>'], reason: '<computed arm>' }], and nothing is stored (the memory row carries no such key);
  • the same through engine.validate;
  • an unknown key answers 400 INVALID_FIELD with field on validate and on insert alike;
  • controls: a static readonly key is still stripped with reason: 'readonly', and a writable field still writes.

Contract. The strip turns SQL's driver error into a success, and turns memory's silent store into a reported strip. validate starts refusing unknown keys it used to pass. The claim writes its own Clause-② reading for the validate narrowing and the enum widening (consumers of reason must stay exhaustive, per the docblock).

Serial. #20701 (domain:cli) keeps the REST half and is pm:blocked on this card.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions