Report on order items, JSON totals and computed group keys - #269
Merged
Merged
Conversation
…port queries
Order reports could not answer basic questions such as "which products sold
the most this month" or "what did orders total this month". This extends the
reporting framework so extension schemas and the report builder can express them.
Schema
- Column::expression() declares a row-level SQL expression (e.g. a value read
out of a JSON column). Bare names resolve against the table or relationship
that declares it, so an expression on payload.entities reads the entity's meta.
Expression columns can be selected, filtered, sorted, grouped and aggregated.
- Aggregate computed columns (Column::count/sum/...) are now flagged `aggregate`
and resolve to their computation instead of a non-existent physical column.
Without grouping they produce a summary row; with grouping they sit beside the
group keys.
- Table/Relationship::softDeletes() leave out rows whose deleted_at is set (on
the root table, and inside the ON clause of joins).
- public_id and internal_id are no longer hidden as foreign keys.
Queries
- Group by, filter and sort by computed columns; sort a grouped report by an
aggregate's alias; new count_distinct aggregate.
- Computed expressions accept JSON_EXTRACT/JSON_UNQUOTE/JSON_VALUE and friends,
DATE(), CAST(... AS DECIMAL(p,s)) and other cast targets, DISTINCT, IN, the
->/->> operators, and INTERVAL units, without misreading keywords as columns.
- Aggregate labels use the column label ("Sum (Quantity)").
Hardening
- Computed columns are validated in grouped reports too (a client-supplied
aggregateBy computation was previously used unvalidated), their names must be
safe identifiers, SELECT is forbidden, and schema-declared columns always take
their SQL from the registry rather than the request.
- Group keys, aggregate columns, sort columns and condition fields must be
allowed columns or computed columns; sort direction is normalised.
…or the date drift check
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #269 +/- ##
============================================
Coverage 100.00% 100.00%
- Complexity 7410 7492 +82
============================================
Files 430 430
Lines 24357 24471 +114
============================================
+ Hits 24357 24471 +114
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…p name, hide system columns
- A relationship column was labelled with only the first word of its
relationship, so Order Config's namespace read "Order Namespace" and
Customer Vendor's name read "Customer Name". Use the whole relationship
label ("Order Config Namespace"), and don't repeat it when the column label
already starts with it ("Transaction ID", not "Transaction Transaction ID").
- _key and _import_id are internal bookkeeping: never list or allow them,
whatever a schema declares, on the root table or through a relationship.
This was referenced Sep 25, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Reports on orders couldn't answer basic questions like "Which products sold the most this month?" or "What did orders total this month?". The reporting framework got in the way in several places:
_idwas hidden as a foreign key, includingpublic_idandinternal_id. As a result, Order ID never appeared in the column picker.Column::count/sum/avg) resolved to a physical column that doesn't exist, e.g.orders.total_orders, so selecting them failed.orders.meta.total). Custom expressions also rejectedJSON_EXTRACT,JSON_UNQUOTEandCAST(… AS DECIMAL(15,2)), and misreadDAY,SIGNEDandDISTINCTas column names.aggregateBy.computationwithout validating it.What changed
Schema API
Column::expression($name, $sql, $type): a row-level expression column. Bare names resolve against the table or relationship that declares it. For example,JSON_EXTRACT(meta, '$.quantity')declared onpayload.entitiesreads the joined entity'smeta. An expression column can be selected, filtered, sorted, grouped by and aggregated.aggregate(also exposed intoArray()) and resolve to their computation:Table::softDeletes()/Relationship::softDeletes()leave out rows whosedeleted_atis set. On joins the filter goes in theONclause, so LEFT joins keep the parent row.public_idandinternal_idare no longer treated as foreign keys._keyand_import_idare internal bookkeeping columns. They are never listed or selectable, on a table or through a relationship, whatever a schema declares.Queries
Computed columns can be group keys, conditions and sort columns.
A grouped report can be sorted by an aggregate's alias (
sum_payload_entities_quantity).New
count_distinctaggregate.Expressions may use:
DATE()and theCASTtarget types;DISTINCT,IN,GROUP_CONCAT(… ORDER BY … SEPARATOR …)and->/->>;INTERVAL n UNIT.Cast types and interval units are keywords only where they can't be a column, so
orders.timestill resolves.Aggregate labels use the column label, e.g. "Sum (Quantity)" rather than "Sum (payload.entities.quantity)".
Hardening
SELECTis forbidden.asc/desc, because it is interpolated into the rawORDER BY.Verification
isColumnAllowedand the converter.ReportQueryConverterOrderReportingTestmodels orders → payload → entities, with JSON totals, soft-deleted rows and a second tenant. It asserts the top-products and monthly-total reports end to end, plus 15 refused report shapes.src/Support/Reportingis at 100% line coverage. php-cs-fixer is clean. phpstan reports 261 errors forsrc/Support/Reporting, against 264 onmain.ONLY_FULL_GROUP_BY) with 76 real storefront orders, reading item quantities and order totals out ofmetawith computed columns: top products, monthly totals, weekly revenue per storefront (a computed group key), and an order list with tracking/customer and a filtered computed total.Column::computed(...)columns, such ashire_days_current_month, now resolve to their expressions. Before this change, selecting them produced a reference to a column that doesn't exist.Behaviour changes to note
ReportQueryValidator.Related
feature/ember-ui-coverage-campaignuses this in the order report schema (items, tracking, summary columns, soft deletes). It deliberately declares no columns that read keys out ofmeta, sincemetahas no fixed shape; reports read those keys with computed columns.feature/code-coverage-campaignadds computed group keys and sorts, sorting by aggregates, and Count Distinct to the report builder.