Preserve $sort document order in $group $push accumulator (fixes #499)#10
Preserve $sort document order in $group $push accumulator (fixes #499)#10richardsimmonds wants to merge 1 commit into
Conversation
The $push accumulator within $group did not preserve the document ordering established by a preceding $sort stage. PostgreSQL provides no ordering guarantee for rows feeding GroupAggregate (a re-sort on the group key, or a HashAggregate, scrambles the input order), so the accumulated arrays came out in arbitrary order, diverging from MongoDB semantics. Fix: when a compatible sort spec is in effect (either the context sortSpec from an immediately preceding $sort stage, or the suffix sort spec pushed down by the $sortGroup prefix optimization), attach it to the bson_array_agg aggregate as an ORDER BY via the existing SetAccumulatorSortOrder helper, mirroring what is already done for $first/$last. Gated to PG16+ consistent with the existing ordered-aggregate handling (PG15 does not push ordered aggregates to the index and crashes in ProcessOrderByStatements). Adds regression tests covering ascending, descending, and non-_id-field sorts, both unsharded and sharded. Fixes documentdb#499 Signed-off-by: richardsimmonds <richardsimmonds314@gmail.com>
richardsimmonds
left a comment
There was a problem hiding this comment.
Review Summary
Verdict: NEEDS CHANGES — the core fix is correct and well-scoped, but the test story has three holes that will surface as CI failures.
What's good
- Root-cause analysis is right: PostgreSQL gives no input-order guarantee into GroupAggregate, so attaching the sort spec as an aggregate-level ORDER BY is the correct mechanism. It mirrors exactly what
SetAccumulatorSortOrderalready does for$first/$last(lines 7091–7096), and reusesIsSortSpecCompatibleForPushToAccumulatorOperatorfor the$natural/$metaguards. No new mechanisms to maintain. - The
createdAccumulatorEntryout-param onAddArrayAggGroupAccumulatorfollows the establishedAddSimpleGroupAccumulatorpattern, andAddGroupExpressionalready supported the out-param — the only call-site change isNULL→createdAccumulatorEntry. NULL-safety checks out: the out-param is only requested whenpushSortSpec != NULL, andAddGroupExpressionunconditionally sets it when non-NULL, soSetAccumulatorSortOrdernever sees a NULL TLE. - DCO: Signed-off-by present on b78c2b9. Single commit, no unrelated changes sneaking in.
- Behaviour is correctly fenced: only fires when
context->sortSpecis live (reset by any non-order-preserving stage in the mutate loop) or when the$sortGroupsuffix spec is passed down.
Blocking issues
-
PG15 will fail the new regression tests. The ORDER-BY attach is gated to
PG_VERSION_NUM >= 160000, but the new tests live ininternal/pg_documentdb_distributed, which runs on the PG15 leg ofregress_tests.yml(make check, matrix includes 15). On PG15 the fix is inert, so array order remains planner-dependent — the new expected output asserting MongoDB order will fail or flake there. See inline comment for options. -
Existing EXPLAIN expected outputs not regenerated. The
pg_documentdbregress suites EXPLAIN exactly this pipeline shape: firstlast test 36a ("$push only → sort NOT pushed") and sumavg Test 31 ("$push is order-sensitive — sort must be preserved"), across base/_pg16/_pg18 expected variants. Their committed plans showbson_array_agg(...)with no aggorder; on PG16+ this patch adds one, so those files change. The PR body says "if CI surfaces it I'll update those expected files" — that's a known-incomplete state, not a maybe. Run the suites and commit the regenerated outputs in this PR. Also re-runbson_aggregation_stage_inversematch_tests(sql lines 109–111): those pipelines sort and group on the same key, so the new within-group ORDER BY compares all-equal keys and may reshuffle the committed array contents. -
The new tests never run on PG18. The PG18 CI leg runs
make check-no-distributed, which skipsinternal/pg_documentdb_distributedentirely. Consider duplicating the issue-499 regression cases into thepg_documentdbsuite so the fix is exercised on every PG version CI builds.
Non-blocking notes
- No CI checks have run on this PR yet (status is empty), so none of the expected-output files are validated. If the new
.outwas hand-written rather than generated bymake check, expect whitespace/header-padding diffs on top of the issues above. - Perf: the aggregate-level ORDER BY re-sorts rows within each group (extra
bson_orderbyeval per row) even though the upstream Sort already ordered them — unavoidable given the planner's re-sort on the group key, and correctness wins, but worth remembering if large-array$pushworkloads regress. if (pushSortSpec != NULL && PG_VERSION_NUM >= 160000)as a runtime condition rather than#ifmatches the existing usage at the fullscan-clause site (line 7541), so no style complaint.
Final merge call is Patty's; my recommendation is fix items 1–2 (CI-breaking) before this leaves draft, and strongly consider item 3.
Reviewed by Hermes (reviewer profile)
| pushSortSpec != NULL ? | ||
| &accumulatorTle : NULL); | ||
|
|
||
| if (pushSortSpec != NULL && PG_VERSION_NUM >= 160000) |
There was a problem hiding this comment.
Blocking (interacts with the new tests): this guard means PG15 keeps the old arbitrary-order behaviour — a defensible call given the ProcessOrderByStatements crash you cite — but the new regression tests in internal/pg_documentdb_distributed assert sorted output and DO run on the PG15 CI leg (make check, matrix.pg_version: 15). On PG15 the array order is whatever the planner's group-key re-sort produces (quicksort, not stable), so those tests will fail or flake there. Options: (a) PG15 alternate expected file, (b) gate the new tests on server version, or (c) un-gate the fix if the PG15 crash is limited to the index-pushdown path rather than plain aggorder. As committed, the matrix cannot pass.
| * attach the sort spec to the array_agg aggregate's ORDER BY | ||
| * so the accumulated array preserves the requested order. | ||
| * See GitHub issue #499. */ | ||
| const bson_value_t *pushSortSpec = NULL; |
There was a problem hiding this comment.
Logic is right and matches the $first/$last precedent. One consequence to be explicit about: the context->sortSpec branch fires for EVERY $sort → $group+$push pipeline on PG16+, which changes committed EXPLAIN outputs — at minimum firstlast 36a ("$push only → sort NOT pushed") and sumavg Test 31 in pg_documentdb/src/test/regress/expected/ (base, _pg16, _pg18 variants), where bson_array_agg currently has no aggorder. Regenerate and commit those in this PR rather than waiting for CI. Also re-run bson_aggregation_stage_inversematch_tests — its pipelines sort and group on the same key, so the new within-group ORDER BY compares all-equal values and may reshuffle the committed arrays.
| SELECT documentdb_api.insert_one('db','testGroupWithPushSort','{ "_id": 9, "name": "whiskey", "category": "A" }', NULL); | ||
| SELECT documentdb_api.insert_one('db','testGroupWithPushSort','{ "_id": 10, "name": "zulu", "category": "A" }', NULL); | ||
|
|
||
| /* $sort ascending followed by $group with $push: arrays should be in ascending order of _id */ |
There was a problem hiding this comment.
Good test shape — asc/desc/non-_id field plus the sharded re-run mirrors how neighbouring suites cover both topologies, and the trailing $sort {_id:1} keeps row order deterministic. Two coverage gaps: (1) this suite is skipped on the PG18 CI leg (make check-no-distributed excludes internal/), so the fix is never exercised on PG18 — consider duplicating these cases into the pg_documentdb regress suite; (2) on PG15 the fix is compiled out, so these assertions won't hold there (see my comment on the C change).
Summary
Fixes documentdb#499 —
$groupwith$pushdid not preserve the document order established by a preceding$sortstage, diverging from MongoDB semantics.Root cause
PostgreSQL gives no ordering guarantee for rows feeding a
GroupAggregate: the planner re-sorts on the group key (or usesHashAggregate), which scrambles the carefully $sort-ed input beforebson_array_aggsees it. So the accumulated arrays came out in arbitrary order.Fix
In
HandleGroupCore, when handling$push:context->sortSpecfrom an immediately preceding$sortstage, or the suffix sort spec pushed down by the$sortGroupprefix optimization (accumulatorSortSpec) — attach it to thebson_array_aggaggregate as an aggregate-levelORDER BYvia the existingSetAccumulatorSortOrderhelper.$first/$lastordered accumulators.IsSortSpecCompatibleForPushToAccumulatorOperator(rejects$naturaland non-numeric directions like$meta).ProcessOrderByStatements).AddArrayAggGroupAccumulatorgains an optionalcreatedAccumulatorEntryout-param (same pattern asAddSimpleGroupAccumulator) so the caller can reach theAggrefto attach the order-by.Tests
Extended
bson_aggregation_pipeline_tests_push_groupwith regression tests for issue documentdb#499:$sort {_id: 1}→$group+$push: ascending order preserved$sort {_id: -1}: descending order preserved$sorton a non-_idfieldshard_collectionto verify sharded behaviourCI will validate the expected outputs across PG versions.
Notes for reviewers
context->sortSpecis live (i.e.$sortimmediately precedes$group, since any non-order-preserving stage resetssortSpecto EOD) or when the$sortGroupoptimization passes a suffix sort — so existing pipelines without a preceding $sort are unaffected.$sortGroupoptimization path where the Sort node must remain; that decision logic (AnalyzeSortGroupAccumulators) is untouched. The new ORDER-BY attach for $push happens in addition to the retained Sort node, which CI may surface in those EXPLAIN outputs — if so I'll update those expected files.