Skip to content

A summary roll-up declaring min/max over a non-numeric child field writes that value into a column both the value contract and the SQL DDL declare numeric #16237

Description

@os-warren

Filed unassigned by the os-dev seat working #16098 (session https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y). Not graded, no domain:* — routing and priority are the triage seat's. Established while establishing what summary STORES, so that a min/max measure over a summary field could be given a measured verdict rather than a guessed one.

What was measured

Three shipped statements, and the third contradicts the first two.

1. The spec's runtime value contract says a summary value is a finite number (packages/spec/src/data/field-value.zod.ts):

/** Value is a finite numeric scalar. `currency` IS a bare number (see header). */
export const NUMERIC_VALUE_TYPES: ReadonlySet<string> = new Set([
  'number', 'currency', 'percent', 'rating', 'slider', 'progress', 'summary',
] as const satisfies readonly FieldType[]);

valueSchemaFor therefore answers z.number().finite() for a summary field.

2. driver-sql's DDL gives it a float column — its own case arm, not the catch-all:

      case 'summary':
        col = table.float(name);
        break;

3. The roll-up vocabulary admits min/max over ANY child field, and the computed value is returned verbatim. FieldSchema.summaryOperations declares:

    object: z.string().describe('Source child object name for roll-up'),
    field: z.string().describe('Field on child object to aggregate (ignored for count)'),
    function: z.enum(['count', 'sum', 'min', 'max', 'avg']).describe('Aggregation function to apply'),

Nothing correlates function with the child field's type, and aggregateSummaryValue (packages/objectql/src/summary-aggregate.ts) passes the driver's answer straight through, with only an empty-set fallback:

  const value = rows?.[0]?.value;
  return value == null ? summaryEmptySetValue(desc.fn) : value;

So { type: 'summary', summaryOperations: { object: 'invoice_line', field: 'shipped_at', function: 'max' } } — a perfectly ordinary "latest shipment" roll-up — computes an instant and stores it into a float column that the value contract says holds a finite number.

Why this is a defect independent of what the right answer is

Whether summary should carry the aggregated child field's type (making it a second measureResultType-shaped rule, one layer down), or whether the roll-up declaration should be REFUSED when min/max is paired with a non-numeric child field, is a contract question. This card holds either way: today the declaration and the stored value can disagree, and no layer says so.

The failure is not uniform, which is the usual shape of this family: on SQLite a float column has NUMERIC affinity and takes the ISO text, on a strict dialect the insert is a runtime error, and the ADR-0104 os migrate value-shapes scan — which parses stored values against valueSchemaFor — would report the column as violating a contract nobody declared it would break.

Out of scope here

Why #16098 did not fix it

That card answers "what should AnalyticsResult.fields[].type say for a min/max over a field of declared type X". For summary the two shipped statements agree — numeric — so the producer's number is the correct wire word and no correction applies; the case where that DECLARATION is itself wrong is this card, and it lives in packages/spec / packages/objectql, not in the analytics response.

Related: #16098 (where summary is recorded as measured-numeric, with this filed as the tension) · #16099 (needs-user-decision: no layer refuses an incoherent aggregate / field-type pair) · #11455 (the driver-level envelope for arithmetic aggregates over a boolean column, closed).

Activity

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions