Skip to content

fix(sql): breaking change 💥- invalid column error returned from GROUP BY on joined tables#6275

Merged
bluestreak01 merged 23 commits intomasterfrom
puzpuzpuz_enhance_select_and_sample_by_rewrites
Oct 31, 2025
Merged

fix(sql): breaking change 💥- invalid column error returned from GROUP BY on joined tables#6275
bluestreak01 merged 23 commits intomasterfrom
puzpuzpuz_enhance_select_and_sample_by_rewrites

Conversation

@puzpuzpuz
Copy link
Copy Markdown
Contributor

@puzpuzpuz puzpuzpuz commented Oct 14, 2025

Fixes #6273

Other than that, SAMPLE BY queries with selected expressions mixing timestamp and aggregate functions now use multi-threaded execution plan. Example of such query:

SELECT ts, count()::double / datediff('h', ts, dateadd('d', 1, ts, 'Europe/Copenhagen')) AS Coverage
FROM 'x'
SAMPLE BY 1d ALIGN TO CALENDAR TIME ZONE 'Europe/Copenhagen';

Also improves the way we choose execution plan for SAMPLE BY and GROUP BY queries. Namely, now we avoid adding duplicate group keys and, instead, reuse the existing ones. This means that in a query like the following one:

SELECT symbol, price, price / sum(amount) FROM trades WHERE timestamp in today();

we now end up with symbol, price as the group keys instead of symbol, price, price price1 (notice the extra price1 key).

Breaking change

When designated timestamp is selected multiple times, under different aliases, the ordered (sorted in asc order) column picked up by the query engine will be the first one. For instance, in the below example column ts1 will be marked as the ordered column:

select ts as ts1, ts as ts2, price, amount
from trades;

If one of the selected columns is used as the first ORDER BY column, it's always preferred, no matter what its position is in the SELECT clause:

-- here, ts2 is used as the ordered column
select ts as ts1, ts as ts2, price, amount
from trades
order by ts2;

Otherwise, unaliased column, i.e. the selected column matching with the designated column name, is preferred.

@puzpuzpuz puzpuzpuz self-assigned this Oct 14, 2025
@puzpuzpuz puzpuzpuz added Bug Incorrect or unexpected behavior Enhancement Enhance existing functionality SQL Issues or changes relating to SQL execution labels Oct 14, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 14, 2025

Walkthrough

Shifts select metadata handling to a new queryMetadata in SqlCodeGenerator and revises designated timestamp selection to consider ORDER BY/aliases. In SqlOptimiser, refactors select-column/alias propagation across translating/inner/group-by/window/outer models and adds a rewrite for timestamps mixed with aggregates. Updates tests to new planner/formatter outputs and expands SAMPLE BY coverage.

Changes

Cohort / File(s) Summary
Codegen: queryMetadata and timestamp designation
core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java
Replaces selectMetadata with queryMetadata; computes timestampName/firstOrderByColumn; selects designated timestamp using ORDER BY or base match; sets timestamp index on queryMetadata; updates add-column paths and factories to use queryMetadata.
Optimiser: alias/group-by/sample-by rewrites
core/src/main/java/io/questdb/griffin/SqlOptimiser.java
Refactors alias reuse/creation across translating, inner, window, group-by, outer models; changes createSelectColumn signature (allowDuplicates → isGroupBy); propagates isGroupBy through wildcard expansion; adds nextColumn helper; adds rewriteTimestampMixedWithAggregates for sample-by planning; adjusts timestamp projection paths.
Planner/Parser expectation updates
core/src/test/java/io/questdb/test/griffin/ExplainPlanTest.java, core/src/test/java/io/questdb/test/griffin/GroupByRewriteTest.java, core/src/test/java/io/questdb/test/griffin/SampleBySqlParserTest.java, core/src/test/java/io/questdb/test/griffin/SqlCompilerImplTest.java, core/src/test/java/io/questdb/test/griffin/SqlParserTest.java
Adds/updates tests for duplicate keys, aggregate key handling, constant keys; revises expected SQL/plan strings to match new select-choose/group-by nesting and aliasing; adjusts assertSql argument structure in places.
SAMPLE BY engine tests expansion
core/src/test/java/io/questdb/test/griffin/engine/groupby/SampleByTest.java
Adds extensive SAMPLE BY tests covering fill(prev/linear/null/values), time zones/DST, invalid fill cases, joins/subqueries, overlapping timestamps, empty/gap datasets, and concurrency behaviors.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant P as SqlParser
  participant O as SqlOptimiser
  participant G as SqlCodeGenerator
  participant E as Execution

  C->>P: SQL text
  P->>O: QueryModel (with select/group-by/window/sample-by)
  note right of O: Propagate/assign aliases across translating/inner/group-by/window/outer\nRewrite timestamp mixed with aggregates (sample-by)
  O-->>G: Optimised QueryModel

  G->>G: Build queryMetadata (columns)
  G->>G: Determine designated timestamp\n- Prefer first ORDER BY column\n- Else base timestamp/alias match\n- Else previous fallback
  G-->>E: CursorFactory with queryMetadata
  E-->>C: Result cursor
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning In addition to the fix for issue #6273, the PR includes broad enhancements—such as multithreaded SAMPLE BY execution, metadata migration from selectMetadata to queryMetadata, and extensive new test coverage—that exceed the scope of the linked issue. Please consider splitting the performance and metadata refactoring into a separate pull request or linking these enhancements to dedicated issues to keep the scope aligned with the bug fix.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues Check ✅ Passed The pull request implements revised alias propagation and select-column handling in SqlOptimiser to ensure columns from joined tables are correctly recognised in GROUP BY, directly addressing issue #6273’s “Invalid column” error.
Title Check ✅ Passed The PR title "fix(sql): breaking change 💥- invalid column error returned from GROUP BY on joined tables" is directly related to the primary objective of this changeset. The title clearly references Issue #6273, which is the main bug being fixed—invalid column errors occurring when using GROUP BY with joined tables. The changes in SqlCodeGenerator.java and SqlOptimiser.java substantially address this issue by restructuring metadata handling and improving column alias reuse to resolve the invalid column reference problem. While the PR also includes secondary improvements (such as SAMPLE BY multi-threaded execution and duplicate group key optimization), the title appropriately focuses on the primary bug fix, which aligns with the instructions that the title need not cover every detail of the changeset.
Description Check ✅ Passed The PR description is clearly related to the changeset and provides meaningful information about the changes. It identifies the fixed issue (#6273) regarding invalid column errors from GROUP BY on joined tables, explains new functionality for multi-threaded SAMPLE BY execution with mixed timestamp and aggregate functions, describes the improvement to avoid duplicate group keys, and documents a breaking change in designated timestamp selection with specific SQL examples. The changes across SqlCodeGenerator (timestamp selection logic and metadata handling), SqlOptimiser (alias handling, duplicate key avoidance, and rewriteTimestampMixedWithAggregates method), and multiple test files align with what the description claims is being implemented.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch puzpuzpuz_enhance_select_and_sample_by_rewrites

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@puzpuzpuz puzpuzpuz marked this pull request as ready for review October 15, 2025 13:59
@puzpuzpuz
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 15, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java (1)

5869-5914: Use base metadata, not queryMeta, for LATEST BY key resolution

prepareLatestByColumnIndexes(latestBy, …) and all index/type/symbol checks in generateLatestBy (isColumnIndexed, getColumnType, isSymbolTableStatic) as well as RecordSinkFactory.getInstance calls currently use queryMeta, forcing LATEST BY keys into the SELECT list. Change to:

  • Call prepareLatestByColumnIndexes(latestBy, metadata) instead of queryMeta
  • Perform all index/type/symbol checks (including RecordSinkFactory.getInstance) on metadata
  • Keep queryMeta strictly as the output schema and for timestampIndex

Add a regression test:

create table t(ts timestamp, sym symbol, v int) timestamp(ts);
select ts, v from t latest by sym;

to verify behavior when the LATEST BY key isn’t projected.

🧹 Nitpick comments (13)
core/src/test/java/io/questdb/test/griffin/SqlCompilerImplTest.java (1)

5267-5269: Unquoted alias variant covered.

Same outcome without quotes; good negative/positive coverage of alias normalization.

If alias-generation rules evolve, these hard-coded names may churn. Consider a small helper to derive/verify de-duplicated names from metadata to reduce brittleness.

core/src/test/java/io/questdb/test/griffin/ExplainPlanTest.java (3)

7237-7271: Good addition: verifies SAMPLE BY dedup of duplicate keys

Covers both alias-first and alias-second permutations; expected keys use a single k/k1 alongside ts. Plan formatting matches existing tests. Consider adding a calendar/time zone align variant to ensure dedup holds across both SAMPLE BY paths.


10678-10697: ORDER BY with alias and projection preserved correctly

Plan shows partiallySorted Sort with keys [ts, l1] and filter applied above. Reads consistent with earlier reorder tests. Optionally add a case ordering by explicit aliases (e.g., “order by ts1, l1”) to complement the ordinal tests.


10721-10739: ORDER BY by alias ordinal validated

“order by 2, 3” mapping to [ts1, l1] is correct and aligns with the alias propagation refactor. Consider adding a mixed-direction variant (e.g., “order by 2 desc, 3 asc”) to guard against direction handling regressions.

core/src/test/java/io/questdb/test/griffin/SqlParserTest.java (5)

4526-4529: GROUP BY alias/key dedup test updates LGTM; add a regression for referencing deduped keys

The updated expectation removes duplicate keys (k/k1) under SAMPLE BY. Consider adding a follow-up test that orders/filters by both the original expr and the deduped alias to ensure name resolution remains stable after rewrite.


4544-4547: Consistent SAMPLE BY output after virtual + group-by rewrite

Expectation now aligns with “align to first observation.” Looks good. Please also add one variant with ALIGN TO CALENDAR to cover both branches of the planner rewrite here.


9536-9564: Aliased column in SAMPLE BY: verify dedup under different align modes

These three tests cover dedup behavior for aliased k/k1 across:

  • ALIGN TO FIRST OBSERVATION
  • default
  • ALIGN TO CALENDAR

Suggestion: include an assertion on column ordering (b, sum, k, k1) to guard against non-deterministic reordering in future refactors.


10429-10451: SAMPLE BY + UNION ALL: column shape and determinism

The UNION ALL expectations for SAMPLE BY plan output look correct. Please add one mixed case where the two branches have differing align modes to verify the normalizer makes them compatible or produces a clear error.


10606-10624: Extend collision alias tests for other join types
Existing tests already verify multi-collision renames (TS, ts1, ts11) and case-insensitive aliasing. Add a complementary test using a right-hand ASOF/LT join to confirm star expansion order and deterministic suffixing under that join direction.

core/src/test/java/io/questdb/test/griffin/engine/groupby/SampleByTest.java (2)

6223-6244: Plan assertion is brittle: avoid hard‑coding “workers: 1”.

“Async Group By workers: 1” may vary with configuration. Prefer asserting the presence of “Async Group By” (or set max workers to 1 via test config) to keep this stable across environments.


6280-6345: Freeze time to avoid flakiness from now().

The last two queries divide by max(i) using datediff(..., now()). Unless the test clock is frozen globally, these are time‑dependent. Either pin “now” in the test base or use a fixed timestamp literal in the queries.

Suggested query tweaks (adjust expected values accordingly):

-                    SELECT ts, datediff('h', ts, now()) / max(i) diff1, datediff('d', ts, now()) / MaX(i) diff2
+                    SELECT ts, datediff('h', ts, timestamp '2025-01-01T00:00:00.000000Z') / max(i) diff1, datediff('d', ts, timestamp '2025-01-01T00:00:00.000000Z') / MaX(i) diff2
-                    SELECT ts, max(i), datediff('h', ts, now()) / max(i) diff1, datediff('d', ts, now()) / MaX(i) diff2
+                    SELECT ts, max(i), datediff('h', ts, timestamp '2025-01-01T00:00:00.000000Z') / max(i) diff1, datediff('d', ts, timestamp '2025-01-01T00:00:00.000000Z') / MaX(i) diff2
core/src/main/java/io/questdb/griffin/SqlOptimiser.java (2)

6634-6688: Aggregate aliasing in SAMPLE BY rewrite — small improvement

Current aliasing uses only the function name (e.g., "sum"), which may reduce readability when multiple aggregates exist. Prefer basing the alias on the full expression node to get more descriptive, yet still unique, aliases.

Apply this minimal change:

-                        aggregateAlias = createColumnAlias(node.rhs.token, sampleByModel);
+                        aggregateAlias = createColumnAlias(node.rhs, sampleByModel);

and similarly for the lhs branch:

-                        aggregateAlias = createColumnAlias(node.lhs.token, sampleByModel);
+                        aggregateAlias = createColumnAlias(node.lhs, sampleByModel);

1585-1619: Alias source for new inner aliases (optional)

When the column isn’t yet referenced by the translating model, innerAlias is sometimes derived from the group-by model’s map. Consider always deriving new aliases from the translatingModel to avoid rare cross‑model alias collisions, then wiring into groupBy/inner accordingly.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93f2484 and 440fbea.

📒 Files selected for processing (8)
  • core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java (34 hunks)
  • core/src/main/java/io/questdb/griffin/SqlOptimiser.java (12 hunks)
  • core/src/test/java/io/questdb/test/griffin/ExplainPlanTest.java (2 hunks)
  • core/src/test/java/io/questdb/test/griffin/GroupByRewriteTest.java (1 hunks)
  • core/src/test/java/io/questdb/test/griffin/SampleBySqlParserTest.java (11 hunks)
  • core/src/test/java/io/questdb/test/griffin/SqlCompilerImplTest.java (1 hunks)
  • core/src/test/java/io/questdb/test/griffin/SqlParserTest.java (7 hunks)
  • core/src/test/java/io/questdb/test/griffin/engine/groupby/SampleByTest.java (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (27)
  • GitHub Check: New pull request (SelfHosted Other tests on linux-x64-zfs)
  • GitHub Check: New pull request (SelfHosted Griffin And Fuzz tests on linux-arm64)
  • GitHub Check: New pull request (SelfHosted Other tests on linux-arm64)
  • GitHub Check: New pull request (SelfHosted Griffin And Fuzz tests on linux-x64-zfs)
  • GitHub Check: New pull request (SelfHosted Other tests Start X64Zfs Agent)
  • GitHub Check: New pull request (Hosted Running tests on windows-other)
  • GitHub Check: New pull request (Hosted Running tests on windows-pgwire)
  • GitHub Check: New pull request (Hosted Running tests on windows-cairo)
  • GitHub Check: New pull request (Hosted Running tests on windows-fuzz2)
  • GitHub Check: New pull request (Hosted Running tests on windows-fuzz1)
  • GitHub Check: New pull request (Hosted Running tests on windows-griffin)
  • GitHub Check: New pull request (Hosted Running tests with cover on linux-other)
  • GitHub Check: New pull request (Hosted Running tests on mac-other)
  • GitHub Check: New pull request (Hosted Running tests with cover on linux-pgwire)
  • GitHub Check: New pull request (Hosted Running tests on mac-pgwire)
  • GitHub Check: New pull request (Hosted Running tests with cover on linux-cairo)
  • GitHub Check: New pull request (Hosted Running tests on mac-cairo-fuzz)
  • GitHub Check: New pull request (Hosted Running tests with cover on linux-fuzz)
  • GitHub Check: New pull request (Hosted Running tests on mac-cairo)
  • GitHub Check: New pull request (Rust Test and Lint on linux-jdk17)
  • GitHub Check: New pull request (Hosted Running tests with cover on linux-griffin)
  • GitHub Check: New pull request (Hosted Running tests on mac-griffin)
  • GitHub Check: New pull request (SelfHosted Other tests Start ARM Agent)
  • GitHub Check: New pull request (SelfHosted Griffin And Fuzz tests Start ARM Agent)
  • GitHub Check: New pull request (SelfHosted Griffin And Fuzz tests Start X64Zfs Agent)
  • GitHub Check: New pull request (Check Changes Check changes)
  • GitHub Check: build
🔇 Additional comments (14)
core/src/test/java/io/questdb/test/griffin/SqlCompilerImplTest.java (3)

5250-5251: Alias de-dup looks correct (case-insensitive).

Expecting TS, ts2, ts1 matches the new unique-name policy when mixing explicit alias + unaliased + conflicting alias.


5256-5257: Wildcard + explicit alias ordering validated.

Headers TS, ts1, ts2, x, ts3, x1 are consistent with stable, unique auto‑rename under star expansion.


5261-5263: t1. + trailing alias conflict handled as expected.*

ts1 for t1.ts and ts11 for the trailing t2.ts alias confirm deterministic conflict resolution.

core/src/test/java/io/questdb/test/griffin/ExplainPlanTest.java (1)

10700-10718: Ordinal ORDER BY resolution looks right

“order by 1, 3” maps to [ts, l1] as expected; plan structure is consistent.

core/src/test/java/io/questdb/test/griffin/SampleBySqlParserTest.java (2)

32-376: Issue #6273 already covered by existing test A test at core/src/test/java/io/questdb/test/griffin/GroupByRewriteTest.java:192 verifies the query involving t2.price / sum(t1.amount); no further tests needed.


203-203: Manual test verification needed
The test run produced no output; please ensure the Gradle wrapper (gradlew) is present/executable and that SampleBySqlParserTest compiles and passes all assertions with the updated expected SQL strings.

core/src/test/java/io/questdb/test/griffin/GroupByRewriteTest.java (2)

32-212: Excellent comprehensive test coverage for duplicate key prevention.

This test method thoroughly exercises the query rewrite logic to ensure duplicate keys are not created when the same column appears multiple times in different positions or with aliases. The test at lines 190-211 is particularly important as it directly addresses the bug fix for issue #6273, verifying that columns from different tables (t1.price and t2.price) are correctly kept as separate keys rather than being incorrectly deduplicated.


214-233: LGTM! Focused test for constant key extraction.

This test correctly verifies that constant literals in the SELECT clause are not treated as group keys, ensuring optimal execution plan generation. Only the non-constant, non-aggregate column amount is correctly identified as a key.

core/src/test/java/io/questdb/test/griffin/engine/groupby/SampleByTest.java (2)

6207-6213: Validate expected rows for the first query (3 inserts → 3 buckets).

You insert 3 rows spanning different days/years; SAMPLE BY 1d ALIGN TO CALENDAR (TZ 'Europe/Copenhagen') should produce 3 buckets. Please confirm the expected block includes all rows (the snippet shows only one line).


6246-6278: LGTM: rewrite path and plan look correct.

The plan shows Async Group By with keys [ts] and values [count(*), max(ts)], and the projection computes max::long/count as expected.

core/src/main/java/io/questdb/griffin/SqlOptimiser.java (4)

1538-1583: Group-by key de‑duplication and alias propagation look solid

Reusing existing translated/group-by aliases and exposing them through outer/inner/window models avoids duplicate GBY keys and fixes JOIN ambiguity. Good improvement.

Also applies to: 1601-1619


1621-1632: Wildcard expansion correctly respects group-by context

Threading the isGroupBy flag through wildcard expansion ensures columns are pushed to the right models. Looks good.

Also applies to: 1641-1653, 1673-1684, 1702-1713


3009-3012: Helper overload for position-aware columns

Good extraction. Using SqlUtil.nextColumn with position centralizes column creation.


4078-4116: Avoid duplicate GBY keys by reusing selected columns

Recognizing when a literal is already selected (via translating→groupBy maps) and substituting the alias prevents extra GBY keys. This should address the “invalid column” on JOINs with aggregates.

@ideoma
Copy link
Copy Markdown
Collaborator

ideoma commented Oct 16, 2025

nit: you can remove the ignore tag from SampleByTest.testSampleByWithProjection2() and modify the expected results, it's fixed now

@puzpuzpuz
Copy link
Copy Markdown
Contributor Author

puzpuzpuz commented Oct 16, 2025

nit: you can remove the ignore tag from SampleByTest.testSampleByWithProjection2() and modify the expected results, it's fixed now

Thanks for the heads-up. I didn't check the disabled tests. Enabled in 6dc3685

@puzpuzpuz puzpuzpuz force-pushed the puzpuzpuz_enhance_select_and_sample_by_rewrites branch from 36e7a15 to 6dc3685 Compare October 16, 2025 16:28
Copy link
Copy Markdown
Collaborator

@ideoma ideoma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still reviewing, just wanted to ask to boost the overall test coverage, please. I do not see exception handling that is hard to hit; this is query logic, which should be fully testable.

@puzpuzpuz
Copy link
Copy Markdown
Contributor Author

Still reviewing, just wanted to ask to boost the overall test coverage, please.

Covered one branch and removed two never-taken in 7372ad2. Everything else seems covered.

ideoma
ideoma previously approved these changes Oct 29, 2025
@puzpuzpuz puzpuzpuz changed the title fix(sql): invalid column error returned from GROUP BY on joined tables fix(sql): breaking change 💥- invalid column error returned from GROUP BY on joined tables Oct 30, 2025
@glasstiger
Copy link
Copy Markdown
Contributor

[PR Coverage check]

😍 pass : 120 / 121 (99.17%)

file detail

path covered line new line coverage
🔵 io/questdb/griffin/SqlCodeGenerator.java 48 49 97.96%
🔵 io/questdb/griffin/SqlOptimiser.java 72 72 100.00%

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

Labels

Bug Incorrect or unexpected behavior Enhancement Enhance existing functionality SQL Issues or changes relating to SQL execution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invalid column error returned from GROUP BY joined tables

5 participants