|
21 | 21 | * A formula computes both correctly — including the temporal one — so the |
22 | 22 | * obvious repair looks available. It is not, and the reason is a STORAGE fact |
23 | 23 | * rather than a taste judgment: a `formula` field is virtual, no driver |
24 | | - * materialises a column for it, and so a FILTER naming one matches nothing. |
25 | | - * That is measured here, not asserted — {@link REVERSE} registers the |
26 | | - * formula-shaped object and shows `where { is_completed: false }` answering |
27 | | - * **0 rows with no error** where the stored column answers every row. Deriving |
28 | | - * would have silently emptied the "Due Today" view, the daily reminder flow and |
29 | | - * both open-task reports: a wrong answer traded for an invisible one. |
| 24 | + * materialises a column for it, and so a FILTER naming one cannot be applied |
| 25 | + * as written. That is measured here, not asserted — {@link REVERSE} registers |
| 26 | + * the formula-shaped object and shows `where { is_completed: false }` failing |
| 27 | + * where the stored column answers every row. Deriving would have emptied the |
| 28 | + * "Due Today" view, the daily reminder flow and both open-task reports. |
| 29 | + * |
| 30 | + * Since **#8296** that failure is VISIBLE. The engine's filter seam refuses a |
| 31 | + * `where` naming a virtual `formula` field with `400 INVALID_FIELD` instead of |
| 32 | + * handing the predicate to a driver with no column behind it and answering |
| 33 | + * **0 rows with no error** — which is what this test measured when #7226 was |
| 34 | + * decided, and the invisible zero was the danger: a wrong answer traded for an |
| 35 | + * unobservable one. |
| 36 | + * |
| 37 | + * The storage fact that decided #7226 is unchanged, so the decision stands and |
| 38 | + * its reasoning is stronger, not weaker: a formula field still carries no |
| 39 | + * column, a filter naming one still could not have worked, and the eight app |
| 40 | + * filters that read these flags still had to move to stored columns. Only the |
| 41 | + * failure mode changed — a silent zero became a named 400. |
30 | 42 | * |
31 | 43 | * `status` and `due_date` are stored, indexed columns that already carry the |
32 | 44 | * information, and both are declared dimensions on the `task_metrics` dataset, |
@@ -229,10 +241,23 @@ describe('#7226 — the replacement filters really select, on BOTH sides of the |
229 | 241 | * REVERSE VERIFICATION — the measurement that chose removal over derivation. |
230 | 242 | * |
231 | 243 | * Predicted direction, recorded BEFORE running it: the formula field READS |
232 | | - * correctly (so "just derive it" looks right) but is UNFILTERABLE, and the |
233 | | - * failure is silent — 0 rows, no error — rather than an exception. That |
234 | | - * asymmetry is the whole argument: an exception would have been safe, because |
235 | | - * someone would have seen it. |
| 244 | + * correctly (so "just derive it" looks right) but is UNFILTERABLE. When #7226 |
| 245 | + * ran it the failure was silent — 0 rows, no error — rather than an exception, |
| 246 | + * and this docblock named that asymmetry as the whole argument: **an exception |
| 247 | + * would have been safe, because someone would have seen it.** |
| 248 | + * |
| 249 | + * **#8296 supplied that exception**, and the second `it` below therefore |
| 250 | + * asserts a rejection envelope (`400 INVALID_FIELD`, naming the field and the |
| 251 | + * object) where it used to assert an empty array. That is this file's own |
| 252 | + * argument being adopted platform-wide — the safe design it asked for is now |
| 253 | + * the shipped one — not a correction of it. |
| 254 | + * |
| 255 | + * The verdict on the derive route is UNCHANGED. A formula field still |
| 256 | + * materialises no column and still cannot carry a predicate, so the eight app |
| 257 | + * filters that named these flags still could not have worked; removal in |
| 258 | + * favour of the stored `status` / `due_date` columns remains the only repair. |
| 259 | + * What #8296 changed is that choosing the derive route now fails where someone |
| 260 | + * can see it, instead of quietly answering an empty set. |
236 | 261 | */ |
237 | 262 | describe('REVERSE — why the derive route was rejected, measured', () => { |
238 | 263 | /** `todo_task` as it would look on the derive route. */ |
@@ -276,26 +301,39 @@ describe('REVERSE — why the derive route was rejected, measured', () => { |
276 | 301 | expect(byId.d.is_overdue).toBe(false); // no due date at all |
277 | 302 | }); |
278 | 303 |
|
279 | | - it('...and is UNFILTERABLE: 0 rows, no error — which is why deriving was refused', async () => { |
| 304 | + it('...and is UNFILTERABLE: a `where` naming one is REFUSED, 400 INVALID_FIELD (#8296)', async () => { |
280 | 305 | const ql = await bootEngine(DERIVED); |
281 | 306 | await ql.insert('derived_task', { id: 'a', subject: 'done', status: 'completed', due_date: '2020-01-01' }); |
282 | 307 | await ql.insert('derived_task', { id: 'b', subject: 'late', status: 'in_progress', due_date: '2020-01-01' }); |
283 | 308 |
|
284 | 309 | // A formula field materialises no column on any driver, so the predicate |
285 | | - // matches nothing — and returns cleanly rather than throwing. |
286 | | - expect(await ql.find('derived_task', { where: { is_completed: true } })).toEqual([]); |
287 | | - expect(await ql.find('derived_task', { where: { is_overdue: true } })).toEqual([]); |
| 310 | + // cannot be applied as written. When #7226 measured this the engine handed |
| 311 | + // it to the driver anyway and answered 0 rows with no error; since #8296 |
| 312 | + // the engine's filter seam refuses it by name. The full envelope is pinned, |
| 313 | + // not merely "it throws": a driver that happened to throw a bare `Error` |
| 314 | + // would satisfy a bare `.rejects` while proving nothing about the verdict. |
| 315 | + await expect(ql.find('derived_task', { where: { is_completed: true } })).rejects.toMatchObject({ |
| 316 | + status: 400, code: 'INVALID_FIELD', field: 'is_completed', object: 'derived_task', |
| 317 | + }); |
| 318 | + await expect(ql.find('derived_task', { where: { is_overdue: true } })).rejects.toMatchObject({ |
| 319 | + status: 400, code: 'INVALID_FIELD', field: 'is_overdue', object: 'derived_task', |
| 320 | + }); |
288 | 321 |
|
289 | 322 | // THE decisive one. On the old stored boolean this returned EVERY row; as a |
290 | | - // formula it returns NONE. Eight filters in this app relied on exactly this |
291 | | - // predicate ("Due Today", the reminder flow, both open-task reports, three |
292 | | - // distribution charts), so the derive route would have silently emptied |
293 | | - // every one of them. |
294 | | - expect(await ql.find('derived_task', { where: { is_completed: false } })).toEqual([]); |
| 323 | + // formula it is not answerable at all. Eight filters in this app relied on |
| 324 | + // exactly this predicate ("Due Today", the reminder flow, both open-task |
| 325 | + // reports, three distribution charts), so the derive route would have |
| 326 | + // broken every one of them — before #8296 by silently emptying them, after |
| 327 | + // #8296 by failing loudly on the first query. Neither is a working app, |
| 328 | + // which is why these flags were removed rather than derived. |
| 329 | + await expect(ql.find('derived_task', { where: { is_completed: false } })).rejects.toMatchObject({ |
| 330 | + status: 400, code: 'INVALID_FIELD', field: 'is_completed', object: 'derived_task', |
| 331 | + }); |
295 | 332 |
|
296 | 333 | // CONTROL — the stored column answers correctly on the same rows and the |
297 | | - // same engine, so the emptiness above is about the field being virtual, not |
298 | | - // about the fixture or the driver. |
| 334 | + // same engine, so the refusal above is about the field being virtual, not |
| 335 | + // about the fixture or the driver. (Assertions unchanged from #7226: the |
| 336 | + // anti-vacuity arm never depended on the formula's failure mode.) |
299 | 337 | expect((await ql.find('derived_task', { where: { status: 'completed' } })).map((r: any) => r.id)).toEqual(['a']); |
300 | 338 | expect((await ql.find('derived_task', { where: { status: { $ne: 'completed' } } })).map((r: any) => r.id)).toEqual(['b']); |
301 | 339 | }); |
|
0 commit comments