Repository navigation
analytics: the native-SQL strategy skips the engine aggregate policies — SUM/AVG exact decimal on PostgreSQL (not #20387's double), and an all-NULL group sums to null where the ObjectQL face folds it to 0 (#15546) #21042
Description
Activity
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:services·area:reports·pm:blocked. Direction: the native strategy applies the engine's two aggregate policies, read from the same placeTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-01T04:02Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #20889
Why p2. One route gives different numbers depending on the strategy:
- exact decimals on PostgreSQL native, where the engine uses double accumulation (aggregate
sum/avg: PostgreSQL and MySQL native answer exact decimal (0.1 + 0.2 = 0.3) while SQLite and the engine rows path answer a double (0.30000000000000004), sohaving { s: { $eq: 0.3 } }keeps the group on PG / MySQL native only #20387); null, where the engine folds an all-NULLsumto0(sumover a column that is NULL in every row of a group: driver-sql answersnull, the engine's in-memory aggregate tier answers0— same query, same rows, fork chosen by a driver capability bit #15546).
This was measured on SQLite and PostgreSQL through the real dispatcher.
Why blocked (re-read at this write: #20889 is open and
pm:dispatched, PR #21040 in flight): the same native-strategy files.Direction: the native strategy applies
emptyGroupValueForand #20387's accumulation policy through the engine's own exported policy, the one the ObjectQL face reads. ⛔ No native copy. Pins: the card's table, both drivers, both strategies agreeing.
Generated by Claude Code
- exact decimals on PostgreSQL native, where the engine uses double accumulation (aggregate
- addedarea:reportsBusiness reporting — dashboards, reports, the numbers a manager readsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsUnblocked ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T04:55ZThis card's
Blocked-by: #20889(triage's first grade on this card) has cleared: PR #21040 merged asd1633f3a, andFixes #20889closed that cardcompleted.- The surface this card names,
packages/services/service-analytics/src/strategies/native-sql-strategy.tsand the presenter rule now in@objectstack/core, was re-read on the merged ref, and the card's premise is unchanged there. - State:
pm:blocked→pm:queue, in one label write. ⛔ Not a claim.
Generated by Claude Code
- The surface this card names,
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-01T05:08Z
Session:session_01XY5uCwTjZj7884yYtyur4H
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21042-native-aggregate-policies
Worktree:objectstack-issue-21042
Domain:domain:services
Seat:domain:services(seat 1, seat post #6021)
Selection: label order in this lane,priority:p2bug. ItsBlocked-by: #20889cleared when PR #21040 landed asd1633f3a(unlock5925017838).
Direction: triage's grade5924484249. The native strategy appliesemptyGroupValueForand #20387's accumulation policy through the engine's own exported policy, the one the ObjectQL face reads. ⛔ No native copy.
Phase: phase 0 first (measure and route, no file changed). The accumulation policy is module-private indriver-sql(AGGREGATE_ACCUMULATION,accumulatesInDouble,doubleAccumulationOperand), so the route decides which packages the build touches. The seat rules on the route, and if it touchesdomain:enginefiles, it notes that lane before the build.
File surface (the build phase's, declared now, narrowed by phase 0):packages/services/service-analytics/src/strategies/native-sql-strategy.ts:AGGREGATE_SQL/CONDITIONAL_AGGREGATE_SQL, the measure compile, and the row-shaping point PR fix(core,driver-sql,service-analytics): the analytics native-SQL path answers measures declared number as numbers (#20889) #21040 added. ⛔ Not the hop-object sites (analytics: on an inferred cube, a dotted path through a lookup whose name differs from its target object is never served — the door admits (and refuses) the lookup's NAME as if it were an object #20986),buildFilterClause/ the text-match rendering ([finding]$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987) orbuildFieldMeta(analytics: a config cube min/max over a text column is served at the cube door under fields[] type number; the dataset door refuses the pair by the compatibility table, the cube door consults nothing #21044).- If the route needs it, a hoist of the accumulation policy beside
AGGREGATE_ANSWER_KINDinpackages/core/src/utils/aggregate-answer.ts, anddriver-sql'sAGGREGATE_ACCUMULATIONregion becoming an import. That is cross-lane (domain:engine): it is declared in an amendment and noted to that lane before the build. - Pins in
service-analytics/src/__tests__/and a route pin besidepackages/rest/src/analytics-*.test.ts;.changeset/21042-*.md.
(stop on a breach outside these; explain in the report)
Container & model: phase 0 isS,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable). The build's container is set from phase 0's measured size. The contract review runs atCONTRACT_REVIEW_TIER.
Clause-②: no - Why
no(expectedno (narrowing), to be measured): values the native face answers today change on PostgreSQL / MySQL (an exact decimal becomes the engine's double) and on every dialect (an all-NULLsumbecomes0). Phase 0 states the grammar it measures.
Thread-read: 5925017838
Serial constraints cleared: - PR fix(core,driver-sql,service-analytics): the analytics native-SQL path answers measures declared number as numbers (#20889) #21040 ([finding] analytics: on PostgreSQL the native-SQL path answers a measure the response declares
numberas a string (count: "2"), where SQLite answers 2 — the class #20335 closed at the engine door #20889) has landed asd1633f3a. - The same file, other regions, in flight: analytics: on an inferred cube, a dotted path through a lookup whose name differs from its target object is never served — the door admits (and refuses) the lookup's NAME as if it were an object #20986 (the hop-object sites), [finding]
$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987 (the filter lowering and text-match rendering) and analytics: a config cube min/max over a text column is served at the cube door under fields[] type number; the dataset door refuses the pair by the compatibility table, the cube door consults nothing #21044 (buildFieldMeta). Each PR mergesmainafter any sibling lands first. driver-sql: no open PR was read holding theAGGREGATE_ACCUMULATIONregion at this claim. Phase 0 re-reads it.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report (phase 0)
{
"issue": 21042,
"status": "blocked",
"branch": "claude/issue-21042-native-aggregate-policies",
"pr": null,
"session": "session_01XY5uCwTjZj7884yYtyur4H — mode:subagent, the dispatching seat's session. The newest Claim (5925147145) names this branch and was verified before any act; no second claim posted; the card's assignee was not written.",
"premise_still_valid": true,
"summary": "Phase 0 is complete and changed no file: no commit, no push, no PR. The card's table reproduces on origin/main 2821e9f through the real routes on SQLite and a private PostgreSQL 16.13. The accumulation split (PostgreSQL native 0.3 / 0.15 / 1.222222222222222 where the engine answers 0.30000000000000004 / 0.15000000000000002 / 1.2222222222222223) shows at BOTH doors. The all-NULL sum shows as null at the cube door only: the dataset door already answers 0 through DatasetExecutor.fillEmptyGroups. A measure-scoped (conditional) sum over a group with no admitted row is null at the native cube door, where the ObjectQL face answers 0. A further reading in the same family: a sum / avg over a boolean field answers 500 (SQLSTATE 42883) at both doors on PostgreSQL native, because the native face also skips driver-sql's boolean-aggregand cast. The ObjectQL face and engine.aggregate answer numbers. Recommended route: (a) the hoist into @objectstack/core beside AGGREGATE_ANSWER_KIND. The boolean cast rides with it (open_questions[1]), and emptyGroupValueFor folds at the native shaping point. Expected size M; the build declares Clause-②: yes (widening), with no narrowing arm. MySQL is NOT MEASURED: there is no server binary in this container.",
"tests": "Probe, a scratch vitest file in a detached worktree at 2821e9f, deleted after the run. Both doors are mounted for real over AnalyticsServicePlugin on a real ObjectQL engine and SqlDriver. Cube door: runtime createDispatcherPlugin, POST /api/v1/analytics/query (dispatcher-plugin.ts:1180). Dataset door: RestServer, POST /api/v1/analytics/dataset/query. The ObjectQL face was forced with the plugin's queryCapabilities, nativeSql false. The answering strategy is proven by counting engine.execute (raw SQL) against engine.aggregate calls on the object: native 1 raw / 0 aggregate at the cube door and 4 / 0 at the dataset door; ObjectQL 0 / 1 and 0 / 4. Build under the verify lock:pnpm turbo run build --filter='@objectstack/runtime^...' --concurrency=1with NODE_OPTIONS=--max-old-space-size=3072 printed "Tasks: 29 successful, 29 total, Cached: 26 cached". Probe:pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/zz-probe-21042-scratch.test.tsprinted "Test Files 1 passed (1), Tests 2 passed (2)", one cell each for SQLite and PostgreSQL. Lock verdicts: attempts 1-3 were VERDICT queue-timeout (exit 99), NOT MEASURED. Attempt 4 printed "VERDICT command-exit 0 · held the lock 43s". Direct SQL on the private PostgreSQL 16.13, over a numeric(65,30) / integer / boolean temp table: native SUM / AVG answer 0.300000000000000000000000000000, 0.150000000000000000000000000000 and 1.2222222222222222. The double operand cast(cast(x as text) as double precision) answers 0.30000000000000004, 0.15000000000000002 and 1.2222222222222223. sum(boolean) fails with "function sum(boolean) does not exist", and cast(cast(true as text) as double precision) fails with "invalid input syntax for type double precision". Cleanup: the probe file was deleted;git status --porcelainread 0 lines andgit diff HEAD0 bytes at 2821e9f; thengit worktree removewithout force succeeded. The private cluster was stopped with pg_ctl on its recorded pid, and its data directory was removed.",
"mcp_calls": "0",
"api_writes": "1 — POST /repos//issues/21042/comments (this os-dev-report, through scripts/pm/post-stamped.mjs). Everything else was a REST GET.",
"open_questions": [
{
"question": "Q1 (N3). Which route makes the native face apply #20387's accumulation, with no copy of the table, the column-class rule or the operand?",
"options": [
"A — Hoist into @objectstack/core, packages/core/src/utils/aggregate-answer.ts, beside AGGREGATE_ANSWER_KIND. It holds AGGREGATE_ACCUMULATION, moved with its docblock. It also holds ONE column-class predicate over the declared shape {type, multiple}: the fractional class (isFractionalNumericType, the float alias included) and the numeric-scalar-or-boolean class, both scalar only. The third piece is the double operand with the dialect as a parameter. driver-sql's two registry fills and its aggregate() call those instead of their private copies. The native compile asks declaredValueShape(object, field) and sqlDialect(object), two hooks it already holds, and wraps the column inside AGGREGATE_SQL / CONDITIONAL_AGGREGATE_SQL. It folds emptyGroupValueFor from @objectstack/spec at the shaping point. No new package edge, no spec change. Lanes: domain:engine (core, driver-sql), domain:services (service-analytics), domain:cli for the rest pin only.",
"B — Decline: canHandle refuses a query carrying a sum over a fractional column or an avg over a numeric or boolean column, on postgres / mysql, so the ObjectQL face answers. The decline still has to READ the table and the class rule, so it needs the hoist anyway (or a copy); it only saves the operand. The empty-sum fold cannot be declined, because that would be every sum on every dialect, so it lands at the shaping point regardless. Measured cost at 2821e9f, examples/: 6 datasets, 19 aggregate measures, 11 of them (8 sum, 3 avg) over exact-decimal columns. Every dataset carries one, and so do 2 of the showcase cube's 3 measures. One statement carries every measure, so any query selecting one of them leaves native on PostgreSQL / MySQL. The ObjectQL face serves every example shape, but not every shape native serves: it refuses cross-object measures (objectql-strategy.ts:916-940, "Run this query on a native-SQL driver"), cross-object measure filters (:1026-1038) and expression metric types (resolveMeasureAggregation, :1474 onward). Those shapes would stay native and divergent, or turn into 400s. Measure-scoped filters move to the engine's in-memory lowering (sql-driver.ts:10153-10157).",
"C — Ask the driver at runtime: a public SqlDriver.aggregateOperandSql(object, func, field, columnSql), read structurally the way dialectName is (plugin.ts:97, :1197-1207), bridged through AnalyticsServiceConfig and a strategy-context hook. The policy, the registries and the boolean cast stay in driver-sql, and nothing moves. The cost is an untyped runtime member, a hook through plugin.ts, analytics-service.ts and strategies/types.ts, and an absence path for hosts that bring their own executeRawSql. This is the shape this seat rejected as (c1) on #20987 (ruling 5923591885)."
],
"recommendation": "A. 实际业务需求: the split is on the DEFAULT path. Native serves whenever executeRawSql is bridged (plugin.ts:451-506), and 11 of 19 example measures are sum / avg over exact-decimal columns. A closes every native shape, cross-object measures included, through resolveStorageTarget; B moves most dashboards off native on PostgreSQL / MySQL and still cannot close cross-object shapes. 项目长远合理性: A keeps one policy in the package both SQL faces already stand on, consistent with #20889's ruling 5922780640, the compensatedSum hoist (#20544) and this seat's #20987 ruling (json-membership-sql.ts into core, engine lane acknowledgement 5923976424). Its stated long-term cost: core carries dialect SQL text (a second such file after #20987's) and the driver's internal numeric aliases ride the predicate. C keeps the policy beside its registries but adds an untyped hook chain; B is a content-routing workaround that leaves a hidden partial gap. 防 AI 犯错: under A one predicate is called by both faces, so an edit to the policy reaches both or fails tsc (a Record over AggregationFunction). Under C a misspelled structural member reads as absent and silently restores today's arithmetic. Under B every new aggregate or column type must also be taught to canHandle. 创业阶段不扩散: A adds three or four core root exports, no spec change and no new gate. C adds one public driver member plus four files of plumbing. B is the least code with the largest behaviour change. No route warrants a new gate: recommend none."
},
{
"question": "Q2 (N2, a reading the build cannot route around). PostgreSQL native sum / avg over a boolean field answers 500 (SQLSTATE 42883) at both doors, where engine.aggregate and the ObjectQL face answer numbers per #11152's ruling. The accumulation's avg class includes boolean columns, and on PostgreSQL the double operand cannot read a boolean's text. Does the PG boolean-aggregand cast (sql-driver.ts:10190-10196, #11635) ride in this build?",
"options": [
"A — Yes. The cast hoists with the policy (cast to int before the double operand, PostgreSQL only, for sum / avg / min / max, never the counts), and the native compile applies it. This card is the family's closing card for "the native face skips the engine's aggregate policies", so this is the third policy in the same family, with no single-point card.",
"B — No. Boolean columns are excluded from the native accumulation, which is a second class rule, i.e. a copy, and the boolean 500 is filed separately. Without the exclusion, the hoisted operand turns the 42883 into 22P02 ("invalid input syntax for type double precision")."
],
"recommendation": "A. B either restates the class rule (the copy the triage direction forbids) or emits SQL that cannot run. A is bounded: one cast in the same predicate's output, the same files and the same pins, plus a boolean row in the card's table."
}
],
"out_of_scope_findings": [
"class: a · reach: POST /api/v1/analytics/query and POST /api/v1/analytics/dataset/query on PostgreSQL 16.13, native strategy. A sum_flag / avg_flag measure over a boolean field answers 500 DATABASE_ERROR (42883, "function sum(boolean) does not exist"; statement SELECT ... SUM(flag) ... AVG(flag)). The ObjectQL face and engine.aggregate answer 1 / 0.5 for group f and 7 / 0.7777777777777778 for group i. On SQLite native the all-NULL boolean sum is null at the cube door, against 0 on the ObjectQL face. · evidence: native AGGREGATE_SQL (native-sql-strategy.ts:46-60) carries no cast; driver-sql's castBooleanAggregand does (sql-driver.ts:10190-10196). · disposition: the same family as this card (the family's closing card), so it is folded into #21042 per open_questions[1]; no single-point card. · dedupe words: analytics native sum boolean postgres 42883 · NativeSQLStrategy SUM(flag) function sum(boolean) does not exist · analytics native avg boolean 500 DATABASE_ERROR · native-sql boolean aggregand cast int",
"carrier: 承接者:无 · noted, not filed — at the dataset door a measure-scoped avg (avg_frac_x) is ABSENT from the row, not null, for groups its supplementary query did not report (groups i and n), on BOTH strategies. The strategies agree, so it is no divergence. It belongs in the build PR's Acceptance notes."
],
"phase0_readings": {
"N1_table": {
"base": "origin/main 2821e9f. Fixture: grp text, tag text, frac number (exact decimal), stars rating (integer column), flag boolean. Group f: frac 0.1 and 0.2, tag x. Group i: stars seven 1s and two 2s, frac 1, tag y. Group n: three rows, all aggregands NULL. Measures: count; sum / avg of frac; sum / avg of stars; count / sum / avg with filter tag = x.",
"postgres_16_13": {
"sum frac (f)": "engine.aggregate 0.30000000000000004 · native cube 0.3 · native dataset 0.3 · ObjectQL cube and dataset 0.30000000000000004",
"avg frac (f)": "engine 0.15000000000000002 · native cube and dataset 0.15 · ObjectQL 0.15000000000000002",
"avg stars, integer column (i)": "engine 1.2222222222222223 · native cube and dataset 1.222222222222222 · ObjectQL 1.2222222222222223",
"sum frac and sum stars, all-NULL (n)": "engine 0 · native cube null · native dataset 0 (DatasetExecutor fill) · ObjectQL 0",
"avg, all-NULL (n)": "null on every face, as ruled",
"count control (n)": "3 on every face",
"conditional sum, no admitted row (i)": "native cube null · native dataset 0 · ObjectQL cube and dataset 0",
"conditional avg, no admitted row (i)": "native cube null · ObjectQL cube null · absent at the dataset door on both",
"conditional count (i)": "0 on every face"
},
"sqlite": "Accumulation agrees on every face: 0.30000000000000004, 0.15000000000000002 and 1.2222222222222223. The all-NULL sum and the conditional sum are null at the native cube door and 0 on every other face, the same pattern as PostgreSQL.",
"mysql": "NOT MEASURED, reason: no MySQL server binary in this container.",
"dataset_door_fold": "DatasetExecutor.fillEmptyGroups (dataset-executor.ts:248-258), called at :1193-1199 over every base measure withrow[column] == null, folds sum / count to 0 after any strategy. So the empty-sum row is cube-door-only, and the accumulation row shows at both doors."
},
"N2_what_the_native_compile_needs": {
"policy_table": "AGGREGATE_ACCUMULATION, sql-driver.ts:1587-1597, docblock from :1537. It is module-private, so native cannot read it without a move. Home under route A: core aggregate-answer.ts beside AGGREGATE_ANSWER_KIND (:86-93).",
"column_class": "sum reads fractionalNumericFields (sql-driver.ts:5666-5678), filled at :11623 and :11727 by isFractionalNumericType(type) && !isMultiValuedColumn. isFractionalNumericType (:366-368) istype === float || numericColumnFor(type).kind === exact: number, currency, percent, slider, progress and summary, per spec numeric-column-representation.ts:217-259; rating is integer. avg reads numericFields plus booleanFields (accumulatesInDouble, :10333-10343): NUMERIC_SCALAR_TYPES (:355-358, spec NUMERIC_VALUE_TYPES plus the integer / int / float aliases) or boolean / toggle, scalar only. The declaration is the object's fields[f].type and .multiple, on the ServiceObject the engine registry holds, which engine.syncSchemas (objectql engine.ts:17850-17860) hands to driver.syncSchema (sql-driver.ts:11074). Native reads the SAME object through declaredValueShape (strategies/types.ts:118, analytics-service.ts:1282-1286, plugin.ts:1264-1276 dataEngine().getObject(object).fields[field], engine.ts:17766). So it derives nothing a second time, provided the type-to-class rule is the one hoisted predicate. The alias half is unreachable on native: aliases come only from introspected external objects, which native declines (native-sql-strategy.ts:160-178). A dotted measure resolves its object and field through resolveStorageTarget (:1070-1093).",
"dialect_operand": "doubleAccumulationOperand (sql-driver.ts:10352-10356) reads this.isPostgres. Native has sqlDialect(object) (types.ts:144, analytics-service.ts:1296, plugin.ts:1197-1207, answered from the driver's dialectName, sql-driver.ts:13100-13105), so the hoisted operand takes the dialect as a parameter. An undefined dialect (an embedder's own executeRawSql) keeps today's arithmetic, a residual to state.",
"boolean_requirement": "Measured: on PostgreSQL the operand over a boolean fails (22P02) and the bare aggregate fails (42883). driver-sql casts a PG boolean aggregand to int first. See open_questions[1]."
},
"N3_route_files": {
"A": "core: utils/aggregate-answer.ts and its test (index.ts:61 already hasexport *, so no line there). driver-sql: sql-driver.ts in four regions only: the table (:1537-1597) becomes a pointer plus import; the predicate (:362-368); the two fills (:11623, :11727); the helpers (:10325-10356). service-analytics: native-sql-strategy.ts resolveMeasureSql (:960-1040) and the execute shaping point (:494-510). Pins: service-analytics src/tests/ and rest src/analytics-*. Changeset. packages/spec/src/: no change.",
"B": "service-analytics native-sql-strategy.ts canHandle (:149-280) plus a helper, the hoisted policy anyway, and pins. packages/spec/src/: no change.",
"C": "driver-sql sql-driver.ts (a new public member); service-analytics plugin.ts, analytics-service.ts, strategies/types.ts and native-sql-strategy.ts; pins. packages/spec/src/: no change (structural, as dialectName)."
},
"N4_empty_sum_fold": "At execute (native-sql-strategy.ts:467-516, the point PR #21040 added): every measure's aggregate is known there through lookupMember(cube, member, measure) (:916-940), already read at :496-499 for the presenter. Conditional measures carry the same type: CONDITIONAL_AGGREGATE_SQL is keyed identically (:81-88, chosen at :994-996). So emptyGroupValueFor(measure.type) covers them; measured, the conditional sum is null native and 0 on ObjectQL. Expression metric types (number / string / boolean) get undefined and stay untouched. Fold only null, never undefined, before the presenter: driver order, sql-driver.ts:10321 and :10400-10409. The dataset door's fill stays and becomes idempotent for native rows.",
"N5_clause2": "Expected for the build under route A:Clause-②: yes (widening), for the new @objectstack/core root exports (precedents: #20889yes (widening), #20544yes). The value change carries NO arm: it pulls the native face back to declared text. That text is emptyGroupValueFor's docblock (spec aggregation-policy.ts:40, "Counting no rows is 0 and summing them is 0") and AGGREGATE_ANSWER_KIND's one-JS-number policy (core aggregate-answer.ts:67-82). execution-duties.md:99 reads 「条款②只指已发布契约面,拉回已声明契约不触它」, and the identical value change on driver-sql's face shippedClause-②: no(#20387 fc0db22, #15546 6d4d5d3). So it is not the claim's anticipatedno (narrowing): no accept set narrows, and the boolean cast turns a 500 into the ruled answer. Changeset: core minor; driver-sql and service-analytics patch. Under B:Clause-②: nowithout a hoist,yes (widening)with one. Under C:yes (widening)(a public SqlDriver member), driver-sql minor. Read with scripts/pm/clause2-line.mjs readClause2Line: "yes (widening)" reads declared yes / widening; "no (widening)" reads malformed.",
"N6_size_route_A": "core aggregate-answer.ts about +110 (about 70 of them moved) and its test about +60. driver-sql sql-driver.ts about -75 / +20, plus an optional move-proof pin of about 80 (as #20987 did). native-sql-strategy.ts about +50. service-analytics pin about +300 (SQLite and PostgreSQL cells, both strategies, the card table, the count control, conditional and boolean rows). rest dataset-door pin about +250. Changeset about +35. Total about +900 / -80, roughly three quarters tests: M. Existing emitted-SQL pins: none sets sqlDialect while asserting SUM / AVG text (0 files in service-analytics src), so no current pin flips.",
"N7_holders": "Read at 2026-10-01T05:51Z. Open PRs: 26, each file list read. Only one touches the build's files: PR #21088 (#20986, draft) changes native-sql-strategy.ts +68/-49, analytics-service.ts +66/-26 and strategies/types.ts +18. Its native hunks include the resolveMeasureSql signature (base :961) and resolveStorageTarget (base :1060-1093), which route A's measure compile reads, so the build goes after #21088 lands or merges main. Route A touches neither analytics-service.ts nor types.ts; route C would. No open PR touches sql-driver.ts, core utils/aggregate-answer.ts, plugin.ts, dataset-executor.ts or spec aggregation-policy.ts. #21084 (#20919, draft) adds 10 lines to core/src/index.ts, and route A adds no line there. PR #20988 has merged (ceee88f) and holds nothing. In flight without a PR: #20987 (claim 5923298824) touches core/src/index.ts (+1), a new core utils/json-membership-sql.ts, sql-driver.ts JSON-membership regions only and native buildFilterClause (+19); that is merge-only. #21044 (claim 5925071499) is buildFieldMeta only and excludes AGGREGATE_SQL by name. Dispatched domain:engine cards #21007, #20914, #20821 and #19519 claim objectql files, none in A's regions."
},
"deviations": [
"The worktree was created DETACHED at origin/main (no branch, no push), so the one-write budget held; it was removed afterwards and the claimed path and branch name are free for the build.",
"The private PostgreSQL data directory lived at /tmp/pg-issue-21042, outside the scratchpad: the postgres user cannot traverse the 0700 scratchpad path. It was removed after the stop.",
"N7 read the open-PR list twice (27 PR file lists, then 26 re-read just before this post, plus PR #21088's diff), the pm:dispatched list and 8 cards' comment threads by REST GET, plus read-onlygit diff --statof the sibling worktrees for #20986, #20987 and #21044. Nothing was written in them. This is wider than the definition's no-board-reads default, as the order's N7 asks.",
"Private evidence in the seat's scratchpad under issue-21042/: probe-out.json, n1-table.md, probe.log, build.log, lockrun1-4.log, progress.log, probe-copy.test.ts.txt."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsRoute ruling on phase 0 (
5925588922), with the claim surface amended ·domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T05:53ZThe phase-0 report is accepted. It changed no file, and its worktree and private database were removed. Its measurements, on
origin/main2821e9f15b, through the real routes:- Accumulation (aggregate
sum/avg: PostgreSQL and MySQL native answer exact decimal (0.1 + 0.2 = 0.3) while SQLite and the engine rows path answer a double (0.30000000000000004), sohaving { s: { $eq: 0.3 } }keeps the group on PG / MySQL native only #20387): on PostgreSQL 16.13 the native face answers the exact decimal (sum/avgover a fractional column,avgover an integer column), at both doors. The engine and the ObjectQL face answer the double. SQLite agrees on every face. - The empty-sum fold (
sumover a column that is NULL in every row of a group: driver-sql answersnull, the engine's in-memory aggregate tier answers0— same query, same rows, fork chosen by a driver capability bit #15546): an all-NULLsum, and a measure-scopedsumwith no admitted row, isnullat the native cube door only. The dataset door already folds to0(DatasetExecutor.fillEmptyGroups). - A third policy in the same family: a
sum/avgover a boolean field answers500on PostgreSQL native, at both doors (function sum(boolean) does not exist). The engine and the ObjectQL face answer numbers, per [finding]AGGREGATION_ROWShas no boolean column, so the cross-driver aggregation conformance family cannot see a boolean aggregand on any face #11152's ruling. The native statement skipsdriver-sql's boolean-aggregand cast (driver-sql: boolean aggregands need a lowering cast on PG (+ a MySQL min/max presentation check) — the ruledfalse/true+ arithmetic answers are unproducible on the PG face #11635). - MySQL is NOT MEASURED: no server in the container.
Q1 (the route): A, the hoist into
@objectstack/core, besideAGGREGATE_ANSWER_KIND.packages/core/src/utils/aggregate-answer.tstakes three things:AGGREGATE_ACCUMULATION, moved with its docblock;- ONE column-class predicate over the declared shape
{ type, multiple }; - the double-accumulation operand, with the dialect as a parameter.
driver-sqlcalls them in place of its private copies.- The native compile reads the same declaration it already reads (
declaredValueShape) and the dialect it already reads (sqlDialect), and wraps the column inAGGREGATE_SQL/CONDITIONAL_AGGREGATE_SQL. - Why: this is the precedent this lane has landed twice:
compensatedSum(aggregate sum / avg over 3+ fractional addends: driver-memory native, its analytics face and the service-analytics draft preview still add naively (0.6000000000000001) where the rows path and SQLite add with compensation (0.6) #20544) andAGGREGATE_ANSWER_KIND([finding] analytics: on PostgreSQL the native-SQL path answers a measure the response declaresnumberas a string (count: "2"), where SQLite answers 2 — the class #20335 closed at the engine door #20889, ruling5922780640). [finding]$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987's membership predicate followed the same path, acknowledged bydomain:engine(5923976424). It adds no new package edge and no spec change, and a policy edit reaches both faces or failstsc.- The stated long-term cost:
corecarries a second piece of dialect SQL text.
- The stated long-term cost:
- Rejected:
- (B) the decline: it still needs the hoist to read the policy, and it moves most example dashboards off the native path on PostgreSQL / MySQL. 11 of 19 example measures are
sum/avgover exact decimals. It also cannot close the cross-object shapes the ObjectQL face refuses. - (C) asking the driver at runtime: the untyped hook chain this seat rejected as (c1) on [finding]
$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987 (5923591885), whose absence silently restores today's arithmetic.
- (B) the decline: it still needs the hoist to read the policy, and it moves most example dashboards off the native path on PostgreSQL / MySQL. 11 of 19 example measures are
Q2 (the boolean cast): A, it rides in this build.
- The PostgreSQL boolean-aggregand cast is hoisted with the policy and applied by the native compile, for
sum/avg/min/max, never the counts. - This card is the family's closing card ("the native face skips the engine's aggregate policies"). So this is its third position, with no single-point card. (B) would either restate the class rule (a copy) or emit SQL that cannot run.
The empty-sum fold (N4):
emptyGroupValueForfrom@objectstack/specis applied at the native shaping point PR #21040 added, for every measure, conditional ones included.- It folds
nullonly, before the presenter, indriver-sql's order. - The dataset door's fill stays, and is idempotent for native rows.
Clause-②:yes (widening), for the new@objectstack/coreroot exports. The value change carries no arm: it pulls the native face back to declared text (emptyGroupValueFor's docblock and the one-double answer policy), as the identical change ondriver-sql's face shipped. The claim's anticipatedno (narrowing)is withdrawn. The dev measures it; the at-tier review judges it.- Changeset:
@objectstack/coreminor;@objectstack/driver-sqland@objectstack/service-analyticspatch.
The phase-0 notes: the dataset door's measure-scoped
avgis absent from a row its supplementary query did not report, on both strategies. There is no divergence: Acceptance notes for the build.The claim surface is amended to:
packages/core/src/utils/aggregate-answer.tsand its test (domain:engine). Noindex.tsline: the module is already exported.packages/drivers/driver-sql/src/sql-driver.ts(domain:engine), these regions only:- the
AGGREGATE_ACCUMULATIONtable (≈:1537–:1597), which becomes a pointer and an import; - the fractional predicate (≈
:362–:368); - the two registry fills (≈
:11623, ≈:11727); - the accumulation helpers (≈
:10325–:10356); - the boolean-aggregand cast (≈
:10190–:10196).
⛔ No other region. The JSON-membership regions belong to [finding]$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987.
- the
packages/services/service-analytics/src/strategies/native-sql-strategy.ts:resolveMeasureSql(≈:960–:1040) and theexecuteshaping point (≈:494–:510). ⛔ NotbuildFieldMeta(analytics: a config cube min/max over a text column is served at the cube door under fields[] type number; the dataset door refuses the pair by the compatibility table, the cube door consults nothing #21044), and not the hop-object sites (PR fix(service-analytics)!: a relationship-path hop with no declared join reads the object its lookup field declares (#20986) #21088).- Pins in
service-analytics/src/__tests__/and a route pin besidepackages/rest/src/analytics-*.test.ts(domain:cli, test only). Plus.changeset/21042-*.md. - Container:
M,mode:subagent,model: opus. The contract review runs atCONTRACT_REVIEW_TIER.
The build waits on three conditions, recorded here:
domain:engine's objection hour: the note goes on [PM seat] domain:engine — 🟢 os-project-manager #6367 in this act. The window runs from that note, or ends sooner on that lane's acknowledgement.- PR fix(service-analytics)!: a relationship-path hop with no declared join reads the object its lookup field declares (#20986) #21088 (analytics: on an inferred cube, a dotted path through a lookup whose name differs from its target object is never served — the door admits (and refuses) the lookup's NAME as if it were an object #20986) lands, or the build merges
mainafter it: its hunks changeresolveMeasureSql's signature andresolveStorageTarget, which this compile reads. - The seat's concurrency: the maintainer has lowered it to 3 once the current work is done (「当前任务处理完,后续并发降到3」). The build is sent when a slot below that line is free.
The card stays
pm:dispatchedunder its claim meanwhile.
Generated by Claude Code
- Accumulation (aggregate
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsBuild dispatched ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T13:07ZThe claim
5925147145stands, with the surface the route ruling5925613967amended. The build is sent now. Its three recorded conditions are met:domain:engine's objection hour passed with no objection.- PR fix(service-analytics)!: a relationship-path hop with no declared join reads the object its lookup field declares (#20986) #21088 (analytics: on an inferred cube, a dotted path through a lookup whose name differs from its target object is never served — the door admits (and refuses) the lookup's NAME as if it were an object #20986) landed as
9b81314c. - The seat's concurrency (2, per the maintainer) has a free slot.
Selection, read fresh at this pick: the lane has no unclaimed
p0orp1(#21079 is in the decision box; #21080 and #21154 are in flight here; seat 2 holds #21120, #21155 and #21156). Among thep2cards, this one is the oldestbug.Serial constraints, re-read:
- PR fix(service-analytics)!: the analytics native-SQL path declines an object an engine middleware is registered for, so its read gates apply #21170 (security(analytics): the native-SQL analytics path never runs engine read middlewares, so object-scoped read gates (comment threads, activity rows measured; attachments, approval payloads unmeasured) do not apply there #21080) edits
native-sql-strategy.tsincanHandle, a different region, and waits behind PR fix(service-analytics): refuse a caller-named non-column member at the analytics query door in every tier (#21156) #21173 ([security] An analytics query path can carry caller-supplied member text into the native statement in a tier the field gate does not judge — detail withheld pending maintainer #21156). Whichever lands second mergesmainand re-runs its pins. - PR fix(service-analytics): refuse a caller-named non-column member at the analytics query door in every tier (#21156) #21173 ([security] An analytics query path can carry caller-supplied member text into the native statement in a tier the field gate does not judge — detail withheld pending maintainer #21156) edits
analytics-service.tsandfield-read-admission.ts, which this build does not touch. - No open PR touches
sql-driver.tsorcore/src/utils/aggregate-answer.ts. - analytics: a config cube min / max whose sql is a relationship path is neither judged by the aggregate field-type table nor presented by its declared type on the native face (#21044's family, the dotted half) #21129 (
p2, unclaimed: the presenter for a relationship-pathmin/max) shares the strategy's shaping point, so it comes after this card.
The build also lifts #21044's named skip: PostgreSQL's native
maxover a boolean incube-measure-field-type-door.test.ts(PR #21128), which this card's Q2 cast fixes.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21042,
"status": "done",
"branch": "claude/issue-21042-native-aggregate-policies",
"pr": "#21209",
"session": "session_01XY5uCwTjZj7884yYtyur4H — mode:subagent, the dispatching seat's session. The newest Claim (5925147145) names this branch and was verified before any act; no second claim posted; the card's assignee was not written. The PR assignee mirrors the card's (os-justin).",
"premise_still_valid": true,
"summary": "Built per the route ruling 5925613967. @objectstack/core aggregate-answer.ts now holds AGGREGATE_ACCUMULATION (moved with its docblock, byte-identical apart fromexport), the one column-class predicate aggregandColumnClass({type, multiple}), POSTGRES_BOOLEAN_AGGREGAND_CAST, doubleAccumulationOperand(operand, dialect) and their one composition aggregandOperandSql. driver-sql calls them in the ruled regions only, and its aggregate statements stay byte-identical: a 1668-entry dump over every function x column class x SQLite/PostgreSQL/MySQL x both registration paths had the same md5 8eee668372a28a7568f3eb1cc5a2bc9b before and after. NativeSQLStrategy's resolveMeasureSql wraps the column through aggregandOperandSql, reading the declaredValueShape and sqlDialect it already reads, and its execute shaping point folds null to emptyGroupValueFor for every measure, measure-scoped ones included, before the presenter. Measured through the cube and dataset doors on SQLite and a private PostgreSQL 16.13, the native and ObjectQL faces now differ in 0 of 144 cells. PostgreSQL native answered 0.3 / 0.15 / 1.222222222222222 and boolean sum/avg/min/max 500 before; it now answers 0.30000000000000004 / 0.15000000000000002 / 1.2222222222222223 / 7 / 0.7777777777777778 / 0 / 1. The cube-door all-NULL and measure-scoped sum went from null to 0 on both drivers. #21044's skipped PostgreSQL native boolean cell runs and passes. Draft PR #21209, Clause-②: yes (widening); changeset core minor, driver-sql and service-analytics patch.",
"tests": "Final head ef1f9d8 (main cb45469 merged, carrying PR #21170 and PR #21173), one locked script, every exit code captured before any pipe; live PostgreSQL 16.13 at timezone Asia/Shanghai, driver-sql under TZ=America/New_York. Results: turbo build --filter=!@objectstack/docs exit 0; spec check:generated exit 0; dispatch-gates --commands derived 67 from the 10-path diff, 67/67 exit 0, and --ran printed 'Run reconciliation — 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.'; typecheck core / driver-sql / service-analytics / rest exit 0. Suites: core local 74 files, 2120 passed; core repo 3, 48; driver-sql 216 passed / 3 skipped files, 4300 passed / 96 skipped; service-analytics 161, 3765; rest local 256, 5027 passed / 127 skipped; rest repo 5, 179 passed / 1 skipped. Pins run explicitly: move proof 54/54, core 22/22, native-sql-aggregate-policies 49/49 (24 SQLite + 24 PG + oracle), cube-measure-field-type-door 23/23 (skip lifted), rest analytics-dataset-aggregate-policies-door 19/19 (9 + 9 + oracle). Red first at the pins commit 192fc00 (base code): core 16 red; policies red (SQLite cube-door folds; PG accumulation, boolean 500, folds); field-type PG native max(boolean) red; rest PG 7 red; move proof 54/54 green at base. Ablations, each predicted in writing first, planted by scripts/ablation-replace.mjs (anchor hit, blob changed), confirmed in dist by ablation-dist-preflight (marker present), restored by absolute path with blob == HEAD blob and git diff HEAD empty, rebuilt, marker absent, tree clean. All three matched exactly. A1 accumulation (core dialect gate): core 3, move proof 24, policies 10 PG-only, rest 3 PG-only. A2 cast: core 1, move proof 4, policies 8 PG-only, field-type 2 (native + objectql), rest 4. A3 fold (native shaping point): policies 8 (4 SQLite + 4 PG, cube door only), rest 19/19 green as predicted, because the executor fill folds the dataset door. Reverse type check: 'oracle' passed to aggregandOperandSql turned service-analytics typecheck red (TS2345 ... 'AggregandSqlDialect'), restored byte-identical. Narrowed eslint at ef1f9d8: 9 changed code files, all not ignored per ESLint.isPathIgnored; --format json 9 results, 0 errors, 0 warnings; the config has no type-aware linting, so untouched files' verdicts cannot move. Repo-wide pnpm lint is CI's. Driver-sql's four timezone non-vacuity cells failed on the first run against a UTC server, by design; green on the non-UTC rerun. MySQL NOT MEASURED, reason: no server in the container. CI on PR #21209: in_progress, not waited, per the contract.",
"mcp_calls": "1 — mcp__github__issue_read (get_comments on #21042), read only. No write tool.",
"api_writes": "3, each through the fleet-write relay as objectstack-fleet[bot]: (1) POST /repos/objectstack-ai/objectstack/pulls, pr_create draft #21209 (relay run 36894572068; read-back 17056 bytes sent, 17056 stored, identical); (2) POST /repos//issues/21209/assignees os-justin via label-write.mjs (relay run 36894665668; read-back matches); (3) POST /repos//issues/21042/comments, this os-dev-report, via post-stamped.mjs. Plus git push to the claimed branch (not REST): the empty-branch probe and every WIP, pin, fix, changeset and merge commit. Unauthenticated REST GETs: issue #21042, PR #21209.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — at the dataset door, a selection made only of measure-scoped measures reports only the groups their filter admits (measured on the native face at the base, SQLite and PostgreSQL; the ObjectQL face was not measured for that shape). It sits beside phase 0's note that a measure-scoped avg is absent, not null, from a group its supplementary query did not report. Both are in PR #21209's Acceptance notes; neither is a divergence this card covers."
],
"deviations": [
"One line outside the ruled native regions: the generateSql measure-loop call site now passes ctx to resolveMeasureSql (its signature gained ctx). Declared in the PR body.",
"driver-sql reads the class from its registries (aggregandColumnClassOf). fractionalNumericFields is filled by the predicate; booleanFields and numericFields keep their coercion fills, whose populations equal the predicate's classes, and the move proof pins that equality per class. Asking the predicate per column through valueShapeFields would retire fractionalNumericFields, but its declaration and shard-alias regions are outside the ruled surface, so it was not done. Stated in the Acceptance notes.",
"main was merged four times (no rebase): b9087d7, 2c1cef3, ebdb6f2, cb45469. The last one carries the same-day churn PR #21170 (canHandle) and PR #21173; the merges were clean and the readings were re-run at ef1f9d8. origin/main then moved 4 more commits (#21149, #21188, #21195, #21192), none touching core, driver-sql, service-analytics or the analytics rest tests; the one spec analytics file, ui/dataset.zod.ts, is a comment-only change. These were not merged before the PR opened.",
"Each full gate script held the shared lock 28.9 to 35.8 minutes (four runs), and the lock flagged holder-side starvation. The runs were detached and waited on in the foreground with tail --pid.",
"Stray files /gates.pid (6 bytes) and /progress.log (101 bytes) at the filesystem root were written at 2026-10-01T14:06Z by a redirect whose path variable lived only in the backgrounded half of an && list. Their content was copied into the scratchpad. The rm was refused by the built-in safety check and was not worked around; please remove both by hand.",
"The first eight Bash calls got no classifier verdict, so #21042's comments were read with one MCP read call. Bash recovered on the ninth.",
"Turbo never wrote AGENTS.md: every run ended with 0 porcelain lines. The private PostgreSQL (pid 30206, port 54921) was stopped with pg_ctl and its data dir /tmp/pg-issue-21042b removed. The worktree's node_modules were removed and the worktree removed without --force after confirming local == remote ef1f9d8.",
"Scratchpad evidence under issue-21042b/: dump-base.json, dump-after.json, table-before.json, table-after.json, ablation-predictions.md, abl-A*.out, gates*/summary.txt, progress.log."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT: #21042 · PR #21209 at
ef1f9d84·domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T16:51ZThe dev report is
5936175525, read on GitHub together with the PR body.Checklist, read on GitHub rather than from the report:
- Shape: a draft onto
main, 10 files, +1309 / −139.check-governed-mergesanswers NOT governed. Line 1 isFixes #21042, and line 2 isClause-②: yes (widening). Assignedos-justin. - The route ruling, as built (
5925613967):core/src/utils/aggregate-answer.tsholdsAGGREGATE_ACCUMULATION(moved with its docblock), one column-class predicate, the PostgreSQL boolean-aggregand cast and the double-accumulation operand.driver-sqlcalls them in the ruled regions. Its aggregate statements are byte-identical: a 1668-entry dump has the same md5 before and after, and a move-proof pin passes at base and after.- The native compile wraps each measure through the hoisted policy. Its shaping point folds
nullbyemptyGroupValueForbefore the presenter.
- Measured: through the cube and dataset doors, on SQLite and PostgreSQL 16.13, the native and ObjectQL faces now differ in 0 of 144 cells.
- Before: PostgreSQL native answered exact decimals and
500on boolean aggregates. - The cube door's all-NULL and measure-scoped
sumis now0on both drivers. - analytics: a config cube min/max over a text column is served at the cube door under fields[] type number; the dataset door refuses the pair by the compatibility table, the cube door consults nothing #21044's PostgreSQL native boolean skip is lifted, and the cell passes.
- Before: PostgreSQL native answered exact decimals and
- Pins and ablations:
- red first at
192fc001; - three ablations (accumulation, cast, fold), each predicted in writing and each matched exactly, with each restore proven;
- a reverse type check (an unsupported dialect fails
tsc).
- red first at
- Gates: 67 derived, 67 exit 0,
--ran67 / 67; every touched package's projects pass. MySQL is NOT MEASURED (no server).
Deviations, accepted:
- The
generateSqlmeasure loop passesctxtoresolveMeasureSql: one line outside the ruled regions, declared in the PR. fractionalNumericFieldsis kept as a registry filled by the predicate. Retiring it would touch regions outside the ruling, and the move proof pins the class equality.mainmoved four commits after the last merge, none in these packages.
Findings: the dataset door's measure-scoped selections answer only the groups their filters admit, beside phase 0's
avgnote. Both are in the PR's Acceptance notes: neither is a divergence between the faces.Landing waits for the at-tier contract review on
ef1f9d84and every check green on that head.
Generated by Claude Code
- Shape: a draft onto
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T17:37ZPR #21209 merged through the merge queue as
097ef802onorigin/main, andFixes #21042closed this cardcompleted.- Review: the at-tier contract review PASS on the landed head
ef1f9d84is5936568953. - Content check: the landed commit is a single-parent squash. Its
git patch-id --stableequals that of the reviewed headef1f9d84against its basecb45469e.
What now holds: the analytics native-SQL face applies the engine's aggregate policies through one implementation in
@objectstack/core, anddriver-sqlemits byte-identical statements:- double accumulation on PostgreSQL / MySQL;
- the PostgreSQL boolean-aggregand cast, so
sum/avg/min/maxover a boolean is answered instead of500; - the empty-sum fold to
0at the cube door.
The native and ObjectQL faces agree in all 144 measured cells.
For the release list:
@objectstack/coreships aminor(new root exports,Clause-②: yes (widening));@objectstack/driver-sqland@objectstack/service-analyticsship apatch.- The review's note:
SqlDriver's twoprotectedaccumulation helpers are replaced by one (aggregandColumnClassOf). An out-of-tree subclass that overrode them would lose that override, so the release note can say so.
Carried elsewhere:
- analytics: a config cube min / max whose sql is a relationship path is neither judged by the aggregate field-type table nor presented by its declared type on the native face (#21044's family, the dotted half) #21129 (unclaimed): the presenter for a relationship-path
min/max. - PR fix(analytics): the native-SQL strategy applies the engine's aggregate policies — double accumulation, the PostgreSQL boolean cast and the empty-sum fold, hoisted into core (#21042) #21209's Acceptance notes: the dataset door's measure-scoped selections; MySQL not measured.
In the same act, this seat removes
pm:dispatchedand the assignee.
Generated by Claude Code
- Review: the at-tier contract review PASS on the landed head
- added a commit that references this issue
on Oct 7, 2026
Filing gate: ① a product defect with a measured
reach:. Finding class (a). This is the family's closing card: one card for both policies the native face skips, with each position enumerated below.reach:POST /api/v1/analytics/querythrough the real dispatcher mount, overAnalyticsServicePluginon a real ObjectQL engine andSqlDriver, on SQLite and PostgreSQL 16.13. The native strategy answered (1 raw statement, 0 engine aggregates). #20889's build dev measured it at PR #21040's heade827c237d1(os-dev-reporton #20889,out_of_scope_findings[0]and[1]), as the route measurement that card's ruling5922780640ordered. The readings are the dev's, and this seat did not re-run them.Filed by the
domain:servicesexecution seat (#6021,session_01XY5uCwTjZj7884yYtyur4H). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.What happens (one route, the strategy decides)
engine.aggregate/ SQLite nativesum/avgsumof 0.1 and 0.2 answers0.3;avgof them answers0.15;avgof seven 1s and two 2s answers1.2222222222222220.30000000000000004,0.15000000000000002,1.2222222222222223emptyGroupValueFor)sumnull, on SQLite AND PostgreSQLDatasetExecutorfold also answers 0)avgstays null on every face for an all-NULL group, as ruled.Where (read from source)
packages/services/service-analytics/src/strategies/native-sql-strategy.ts,AGGREGATE_SQL: it emits plainSUM(col)/AVG(col). It carries neitherdriver-sql'sAGGREGATE_ACCUMULATION/accumulatesInDouble(aggregatesum/avg: PostgreSQL and MySQL native answer exact decimal (0.1 + 0.2 = 0.3) while SQLite and the engine rows path answer a double (0.30000000000000004), sohaving { s: { $eq: 0.3 } }keeps the group on PG / MySQL native only #20387) nor aCOALESCE/ post-fold for the empty-sum identity (sumover a column that is NULL in every row of a group: driver-sql answersnull, the engine's in-memory aggregate tier answers0— same query, same rows, fork chosen by a driver capability bit #15546).numberas a string (count: "2"), where SQLite answers 2 — the class #20335 closed at the engine door #20889) presents those values as numbers.presentAsNumberisNumber()of the wire string, so it neither introduced the divergence nor closes it. No pin there enshrines either side.Scope for whoever takes it (⛔ not a ruling)
sum/avg: PostgreSQL and MySQL native answer exact decimal (0.1 + 0.2 = 0.3) while SQLite and the engine rows path answer a double (0.30000000000000004), sohaving { s: { $eq: 0.3 } }keeps the group on PG / MySQL native only #20387's accumulation rule andsumover a column that is NULL in every row of a group: driver-sql answersnull, the engine's in-memory aggregate tier answers0— same query, same rows, fork chosen by a driver capability bit #15546's empty-sum identity.driver-sql(module-private today). Its reuse is the same "hoist without a copy" question [finding] analytics: on PostgreSQL the native-SQL path answers a measure the response declaresnumberas a string (count: "2"), where SQLite answers 2 — the class #20335 closed at the engine door #20889's ruling5922780640answered with@objectstack/core. The empty identity isemptyGroupValueForin@objectstack/spec/data.numberas a string (count: "2"), where SQLite answers 2 — the class #20335 closed at the engine door #20889, in review) editsnative-sql-strategy.ts'sexecute. [finding]$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987 (in flight) edits itsbuildFilterClause.AGGREGATE_SQLis a third region.Reader who acts
Triage (grade and route; the files are
domain:services'service-analytics, plusdomain:engineif the accumulation rule is hoisted), then the owning seat.Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:sum/avg: PostgreSQL and MySQL native answer exact decimal (0.1 + 0.2 = 0.3) while SQLite and the engine rows path answer a double (0.30000000000000004), sohaving { s: { $eq: 0.3 } }keeps the group on PG / MySQL native only #20387 (closed) is this accumulation rule atdriver-sql's nativeaggregate, and aggregate sum / avg over 3+ fractional addends: driver-memory native, its analytics face and the service-analytics draft preview still add naively (0.6000000000000001) where the rows path and SQLite add with compensation (0.6) #20544 (closed) is the compensated sum at memory and the analytics draft preview. Neither coversNativeSQLStrategy.numberas a string (count: "2"), where SQLite answers 2 — the class #20335 closed at the engine door #20889 (open) is the string type on this face.aggregatereturnscount/sum/avgas strings ("n":"1","total":"20.000…"), sohaving { n: { $in: [2] } }keeps no group on PostgreSQL alone, where memory, SQLite and PG's rows path keep c1, c2 #20335, aggregatesum/avgover 3+ fractional addends: SQLite native adds with compensation (0.1+0.2+0.3 = 0.6), every other face and the rows path naively (0.6000000000000001), sohaving $eq 0.6keeps the group on SQLite native only #20489, [finding] analytics: a cube / dataset dimension on ajsonfield, compiled by NativeSQLStrategy, answers one group per serialized document on SQLite and 500 on PostgreSQL; the engine door #20783 closes does not see it #20807, service-analytics:AVG()over aField.datetimemeasure returns SQLite's text→numeric coercion (an average YEAR) with no error, andderived: { op: 'difference' }renders the difference of two of them as a clean plausible number #16737, No layer refuses an incoherent aggregate / field-type pair — a dataset measureavgover a datetime works on SQLite and errors on Postgres #16099 and 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 are closed, at other positions.Dedupe words:
analytics native sum avg exact decimal postgres·NativeSQLStrategy double accumulation 0.30000000000000004·analytics native sum null all-null group·NativeSQLStrategy emptyGroupValueFor sum 0Generated by Claude Code