fix(QueryFormatter): forward GROUP BY to SelectFirst and SelectCount - #573
Merged
Conversation
…559) SqlQueryFormatter::SelectFirst and SelectCount took no groupBy parameter, so a populated ComposedQuery::groupBy could never reach them: .GroupBy(...).First() and .GroupBy(...).Count() silently produced ungrouped SQL. SelectAll and SelectRange already accepted it, which is what made the omission an oversight rather than a deliberate restriction. Add std::string_view groupBy to both virtuals, emitting it between the WHERE and ORDER BY clauses exactly as SelectAll/SelectRange do, and forward it from ComposedQuery::ToSql. The DataMapper query builder calls the formatter directly rather than going through ToSql, so it carried the same bug at seven further call sites (CountImpl, ExistImpl, the three FirstImpl overloads and both FirstImpl(n) overloads); all of them now pass the builder's groupBy. Counting with a GROUP BY yields one row per group rather than a single total, so both Count() surfaces document that, including the DataMapper one returning the first group's count since it reads a single value. This changes the signature of two pure virtuals, so it is a source and ABI break for any out-of-tree SqlQueryFormatter implementation. HAVING remains unsupported; that is tracked separately in the same issue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Applying /code-review findings on this branch. Count()'s doc promised "the count of the first group", but SelectCount takes no orderBy and CountImpl passes none, so no ORDER BY is emitted at all: the group read back is unspecified and may differ per DBMS and per run, and a preceding OrderBy is silently dropped. Say that instead. The three new tests only exercise ComposedQuery::ToSql. The bug being fixed lived in the seven DataMapper call sites, none of which are covered -- removing `this->_query.groupBy` from any of them keeps the suite green. Add a "Count() honors GroupBy" section to the existing Query test. The fixture's four persons split 2/2 on is_active, so `== 2` holds whichever group the engine returns first (it is 4 without the fix), and the emitted SELECT COUNT(*) FROM "Person" GROUP BY "is_active" is valid on all three backends. Document SelectFirst's new groupBy parameter: the parameter order puts orderBy before groupBy, the reverse of the required emission order, so an implementer needs telling that it goes between WHERE and ORDER BY and that `count` applies to the grouped result set. Left alone: ExistImpl and the record-returning FirstImpl now combine the full non-aggregated column list with GROUP BY, so .GroupBy(x).First() and .Exist() newly error on PostgreSQL (42803) and SQL Server (8120) where they previously returned a row. That is the SQL the caller asked for and matches what SelectAll already did, but it is an undeclared per-DBMS behavior change worth calling out in the PR description. Not compiled: this worktree has no configured build directory. The added test uses only APIs already exercised in the same file, and GroupBy is inherited from SqlBasicSelectQueryBuilder<Derived> returning Derived&, so .Count() resolves -- but it has not been built or run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
… singleton The macOS leg failed in PluginIngestionTests with "Duplicate migration timestamp 20260101000000 detected: 'fixture' conflicts with 'fixture'" - a migration reported as a duplicate of itself. `MigrationBase`'s constructor registers `this` with `MigrationManager::GetInstance()`, but nothing deregisters it. `StubMigration` instances have automatic storage duration, so each test left a dangling pointer in the singleton's list. Since `AddMigration` dereferences those pointers to compare timestamps, a later migration allocated at a recycled address matches itself and is rejected. That makes the failure depend on allocator address reuse, which is why it surfaced on macOS and on this branch, which touches nothing migration-related. Give `StubMigration` the same singleton reset `FakeMigration` already carries for exactly this reason, and document why it is there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Yaraslaut
added a commit
that referenced
this pull request
Aug 20, 2026
The branch had drifted into conflict with master, which meant GitHub could not build a merge ref and every pull_request-triggered workflow silently stopped running: the last full CI this branch saw was on 79bd7bb, and PR Labeler was the only check reporting since. Two conflicts, both mechanical: - QueryBuilders.hpp: master's doc comment wins. #573 made SelectCount forward GROUP BY, so the branch's note that a preceding GroupBy is "not part of the generated statement" describes behaviour that no longer exists. - DataMapper.hpp: the eager-loading declarations this branch adds sit directly above LoadHasOneThrough's template header, which #580 re-spelled from ThroughRecord to ThroughSpec. Kept both: the new declarations and master's signature. Verified after the merge: clang-debug builds clean, doc snippets match, and the full suite passes on sqlite3 (1426), mssql2022 (1424) and postgres (1425). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Fixes #559.
Problem
SqlQueryFormatter::SelectFirstandSelectCounttook nogroupByparameter, so a populatedComposedQuery::groupBycould never reach them —.GroupBy(...).First()and.GroupBy(...).Count()silently returned ungrouped results.SelectAllandSelectRangealready took it, which is what marks the omission as accidental.Change
std::string_view groupByto both virtuals, positioned afterorderByto matchSelectRange.SQLiteQueryFormatter(SelectCount+SelectFirst, inherited by PostgreSQL) andSqlServerQueryFormatter(SelectFirst). The clause is emitted between WHERE and ORDER BY, exactly asSelectAll/SelectRangealready do.ComposedQuery::ToSqlin theFirstandCountbranches.ToSql, so it had the same bug at 7 further sites —CountImpl,ExistImpl, the threeFirstImploverloads and bothFirstImpl(n)overloads. All now pass the builder'sgroupBy.Count()surfaces:SELECT COUNT(*) … GROUP BY xyields one row per group, and the DataMapper'sCount()returns the first group's count since it reads a single value.HAVINGis still unsupported — noted in the issue as a separate gap, deliberately left out of scope here.Tests
Three cases in
QueryBuilderTests.cppcoveringCount()+GroupBy,First()+GroupByandFirst(3)+GroupBy+OrderByacross all three dialects. All three fail on master (GROUP BY absent from the generated SQL) and pass with this change.Risk
Source/ABI break for any out-of-tree
SqlQueryFormatterimplementation, as the issue anticipated; every in-tree implementor is updated. Behaviour is unchanged wherevergroupByis empty — the generated SQL is byte-identical, which is why the rest of the suite is unaffected.Performance: no measurable impact — one extra
string_viewargument and one stream insertion of an empty view on the unchanged paths.Verification
clang-debug(ASan+UBSan+clang-tidy) on this exact commit in a clean tree: 1405 cases, 13641 assertions, all pass (1 pre-existing skip). Build is warning-free;clang-formatapplied.gcc-release. No GCC toolchain on this machine, so the compiler CI's database legs actually use is unverified here.