You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
finding(objectql): backfillSummaryNulls cannot fill a JUST-CREATED min/max/avg roll-up — summaryNullIsBackfillable decides on the function alone, so "never computed" is indistinguishable from "no child rows" #15064
Filed by the repo:cloud PM seat (objectstack#6026, session session_01EK4Q5Nrx779cxjdeTxsK7P) as the producer-side half of cloud#1908. ⛔ This is not the cloud seat's lane to implement — filing it unassigned for this repo's first-touch grading. Nothing has been changed in this repo.
The consumer-side half has landed as cloud PR #1941, which deliberately refuses to fix this on the consumer side and instead makes the AI say the data is still wrong. See "Why not fix it in cloud" below.
Measured (at framework 3f64fe6c, which is cloud's current pin)
A roll-up value has exactly three producers, all here:
producer
file
when it runs
ObjectQL.initializeSummaryFields
packages/objectql/src/engine.ts
parent INSERT — seeds the empty-set value
ObjectQL.recomputeSummaries
packages/objectql/src/engine.ts
a CHILD row is written
backfillSummaryNulls
packages/objectql/src/summary-backfill.ts
the one-off behind os migrate summary-nulls
Creating a roll-up field reaches none of them. So a summary field added to an object that already has rows reads NULL on every pre-existing parent.
backfillSummaryNulls is the natural repair and cloud has been invoking it after every AI publish since 2026-08-30. It cannot help for min/max/avg:
A hole on every row — "this column was created seconds ago and nothing has ever computed it". Here the null means never computed, children or not.
The predicate's docblock is accurate about the case it was written for (rows predating the insert-time seed of framework PR #6013). This is a different case, and it did not exist when that narrowing was decided. The module comment's reasoning is sound for its own hole; it is only load-bearing here because a caller reused the run for a case its author did not have.
Why this matters downstream (the measured user-visible consequence)
From cloud#1908: a user's 客户 object already had 跟进记录 when the AI added max(follow_up_record.follow_up_time). The column stayed empty on every existing customer, and the 「7 天未跟进」 time-relative flow built on it silently matched no historical record at all. Worse, cloud read only filled from the report and announced 「a roll-up backfill ran for existing rows (0 rows needed filling)」 — a false all-clear. cloud PR #1941 fixes the false all-clear; it cannot fix the empty column.
⛔ Why not fix it in cloud
Computing the value on the cloud side would be a second answer to "what does this roll-up equal" — precisely the drift summary-aggregate.ts was extracted to prevent. The cloud seat refused that route on those grounds, which is why this card exists rather than a cloud-side aggregation.
Options — ⛔ this seat is NOT ruling, the choice belongs to this repo's lane
A. Give backfillSummaryNulls a caller-supplied scope. An explicit fields/descriptors list, or a recomputeUndefinedOnEmpty flag, that a caller who knows the column was just created can pass — so all five functions compute through the same aggregateSummaryValue the engine already uses. The empty-set narrowing stays the default for os migrate summary-nulls.
B. Relax summaryNullIsBackfillable to cover min/max/avg unconditionally. Cheapest diff. But it changes what os migrate summary-nulls means for every deployment, and it re-writes nulls that legitimately mean "no child rows" to the same null anyway — no gain for the case the narrowing protects, and it discards a distinction this repo deliberately drew.
C. Leave the framework alone; let callers aggregate themselves. Already refused on the cloud side, for the drift reason above. Listed for completeness.
The cloud seat's recommendation is A, offered as input and nothing more:
packages/objectql/src/summary-aggregate.ts — summaryEmptySetValue / summaryNullIsBackfillable
packages/objectql/src/summary-backfill.ts — partitionDescriptors(), and the `continue` above scannedObjects.push
packages/objectql/src/engine.ts — initializeSummaryFields / recomputeSummaries
Reproduce: create a parent with child rows, then declare a max/min/avg summary field on the parent, then run backfillSummaryNulls({ apply: true, objects: [parent] }). Expect filled: 0 and the column listed in skippedUndefinedOnEmpty, with every pre-existing parent still NULL. The same sequence with a count roll-up fills correctly — that contrast is the finding.
Filed by the
repo:cloudPM seat (objectstack#6026, sessionsession_01EK4Q5Nrx779cxjdeTxsK7P) as the producer-side half of cloud#1908. ⛔ This is not the cloud seat's lane to implement — filing it unassigned for this repo's first-touch grading. Nothing has been changed in this repo.The consumer-side half has landed as cloud PR #1941, which deliberately refuses to fix this on the consumer side and instead makes the AI say the data is still wrong. See "Why not fix it in cloud" below.
Measured (at framework
3f64fe6c, which iscloud's current pin)A roll-up value has exactly three producers, all here:
ObjectQL.initializeSummaryFieldspackages/objectql/src/engine.tsObjectQL.recomputeSummariespackages/objectql/src/engine.tsbackfillSummaryNullspackages/objectql/src/summary-backfill.tsos migrate summary-nullsCreating a roll-up field reaches none of them. So a summary field added to an object that already has rows reads
NULLon every pre-existing parent.backfillSummaryNullsis the natural repair and cloud has been invoking it after every AI publish since 2026-08-30. It cannot help formin/max/avg:…and in the walk:
So for an object whose only roll-up is a
max, the walk never runs and the report returnsfilled: 0with the column named inskippedUndefinedOnEmpty.The gap, stated precisely
summaryNullIsBackfillabledecides on the FUNCTION alone. It therefore cannot distinguish two cases that look identical in storage:null— "this parent has no child rows". Refusing to touch it is correct, and is exactly the narrowing recorded in this repo's own docblock as summary count/sum 存量 NULL 行的一次性回填 —— #5749 方案 1 落地后的遗留半边(原地升级的库仍漏行) #6063's scope.nullmeans never computed, children or not.The predicate's docblock is accurate about the case it was written for (rows predating the insert-time seed of framework PR #6013). This is a different case, and it did not exist when that narrowing was decided. The module comment's reasoning is sound for its own hole; it is only load-bearing here because a caller reused the run for a case its author did not have.
Why this matters downstream (the measured user-visible consequence)
From cloud#1908: a user's 客户 object already had 跟进记录 when the AI added
max(follow_up_record.follow_up_time). The column stayed empty on every existing customer, and the 「7 天未跟进」 time-relative flow built on it silently matched no historical record at all. Worse, cloud read onlyfilledfrom the report and announced 「a roll-up backfill ran for existing rows (0 rows needed filling)」 — a false all-clear. cloud PR #1941 fixes the false all-clear; it cannot fix the empty column.⛔ Why not fix it in cloud
Computing the value on the cloud side would be a second answer to "what does this roll-up equal" — precisely the drift
summary-aggregate.tswas extracted to prevent. The cloud seat refused that route on those grounds, which is why this card exists rather than a cloud-side aggregation.Options — ⛔ this seat is NOT ruling, the choice belongs to this repo's lane
A. Give
backfillSummaryNullsa caller-supplied scope. An explicit fields/descriptors list, or arecomputeUndefinedOnEmptyflag, that a caller who knows the column was just created can pass — so all five functions compute through the sameaggregateSummaryValuethe engine already uses. The empty-set narrowing stays the default foros migrate summary-nulls.B. Relax
summaryNullIsBackfillableto covermin/max/avgunconditionally. Cheapest diff. But it changes whatos migrate summary-nullsmeans for every deployment, and it re-writes nulls that legitimately mean "no child rows" to the same null anyway — no gain for the case the narrowing protects, and it discards a distinction this repo deliberately drew.C. Leave the framework alone; let callers aggregate themselves. Already refused on the cloud side, for the drift reason above. Listed for completeness.
The cloud seat's recommendation is A, offered as input and nothing more:
v9h3_customer.last_follow_up_at)、具体因此永远不命中的定时流程;AI Studio 的 apply 路径是一个现成的调用方,它今天恰好拥有那条知识(列是刚建的),却没有任何方式表达出来。Re-check
Reproduce: create a parent with child rows, then declare a
max/min/avgsummary field on the parent, then runbackfillSummaryNulls({ apply: true, objects: [parent] }). Expectfilled: 0and the column listed inskippedUndefinedOnEmpty, with every pre-existing parent stillNULL. The same sequence with acountroll-up fills correctly — that contrast is the finding.