Repository navigation
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
e1220ae to
92ed248
Compare
Adds AUDITED to DECLARE, so a view can record what a read of it resolved its parameters to, and makes a declared variable able to hold the list an IN filter tests against. Auditing is per variable rather than per view: `DECLARE OVERRIDABLE AUDITED @sym := 'AAPL'` records @sym, and leaves anything else undeclared to the audit. Values are captured per execution rather than at compile time, because one cached plan serves many reads - a bind variable or `now() - 1h` resolves differently each time, and the point of the trail is what a given read actually covered. Lists are spliced into IN at parse time. `DECLARE @s := ('a','b') ... IN @s` produces the same node a written-out `IN ('a','b')` does, so every existing IN overload applies unchanged and each member keeps its own type. That is what lets a list hold bind variables of any type without a typed-array literal to carry them. A trailing comma writes a list of one, which brackets alone cannot express - `('a')` is just a parenthesised scalar. The marker node carrying a list never reaches the runtime: it is spliced into IN, or refused where a list has no meaning. Two passes have to agree on that. The splice walks exactly what variable substitution walks, window clauses included; where it did not, a list in a view body or a window partition was left stranded and surfaced as an internal token in a user error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
92ed248 to
0298796
Compare
Code reviewReviewed at Verified working (probe queries, not in the PR): bind variables in a list ( Six issues.
questdb/core/src/main/java/io/questdb/griffin/SqlCompilerImpl.java Lines 5143 to 5145 in 0298796 questdb/core/src/main/java/io/questdb/griffin/SqlCompilerImpl.java Lines 5022 to 5027 in 0298796
questdb/core/src/main/java/io/questdb/griffin/SqlParser.java Lines 5652 to 5657 in 0298796 questdb/core/src/main/java/io/questdb/griffin/SqlParser.java Lines 4905 to 4909 in 0298796
questdb/core/src/main/java/io/questdb/cairo/view/ViewDefinition.java Lines 115 to 137 in 0298796
questdb/core/src/main/java/io/questdb/griffin/SqlCompilerImpl.java Lines 5928 to 5939 in 0298796
Nits, no reply needed: 🤖 Generated with Claude Code |
Review of the audited-views half turned up two behaviour gaps, plus doc and coverage ones. DROP ALL TABLES bypassed checkTableDroppable. executeDropTable consulted the veto but executeDropAllTables only skipped system tables, so the one route that drops every table at once was the route around a veto Enterprise installs for the view audit table. The throw lands in dropAllTablesFailures, so the rest still drop. An audited view read through INSERT INTO ... SELECT, CREATE TABLE AS SELECT or UPDATE ... FROM recorded nothing. recordViewAudits ran in parseSelect, which those three never reach - they build their query model through parseDml directly. parse() now attaches the collected audits to whichever query model the statement carries, so the statements that copy rows out of an audited view leave a record too. recordViewAudits skips audits a model already holds, so EXPLAIN, which carries the statement it explains, does not double up. ViewDefinition.readFrom returned as soon as it saw the extra block, skipping the definitionBlockFound check. A file carrying the extra block and no definition block handed back an empty definition rather than reporting the missing block. ViewAuditModel's javadoc named DECLARE OVERRIDABLE as what it records; recordViewAudit iterates the AUDITED set, and the two markings are independent. isAuditedView carried getFunctionParser's javadoc. Tests cover the OSS half only - AUDITED parsing, the definition-file flag and its pre-auditing shape, which statements record, and which parameters a record holds - since the audit rows, the WITH AUDIT clause and the permission guarding it live in Enterprise. DeclareListFuzzTest asserts the declared side with assertQuery().returns() rather than a second print, which adds the second cursor pass and the calculateSize() cross-check across every generated shape. Declared lists of bind variables gained a test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wm14GQVQDZ7yRbSJqp8pSF
The extra _view block carried a flag that is false on every view but the few that opted in, yet every view definition was paying for it with a changed on-disk shape. Write it only when the flag is set, which is the shape LiveViewDefinition already uses for its optional anchor block. A view nobody asked to audit now writes exactly the file it wrote before auditing existed, so rolling back to a build without this change is a non-event for it. An audited view still carries the extra block, and an older build still loads it - readers walk past block types they do not know. What a downgrade costs, confined to views that opted in: if the older build rewrites the definition it drops the flag, and rolling forward reads the view as not audited. No field an older writer does not know about can survive that. ViewAuditTest pins the block layout for both cases and that an unknown trailing block does not disturb the read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wm14GQVQDZ7yRbSJqp8pSF
…uces A declared list of one kept the node type it was parsed with. `IN @x` parses as a SET_OPERATION and `IN (2)` as a FUNCTION, and the JIT filter compiler routes on that type, so the one-member form returned the right rows while silently dropping off the JIT path. The multi-member branch already converted the type for exactly this reason; spliceIn now does it on both branches. DeclareListFuzzTest compared rows, which cannot see this, so it stayed green. It now compares plans too - that is the property the design actually claims - and DeclareTest asserts the one-member plan directly. Also on the same pass: - ViewDefinition.readFrom walked blocks as if order did not matter, but readDefinitionBlock ends in init(), which reset the audited flag. An extra block read before the definition block was silently dropped. The flag is now collected into a local and applied after the loop, and asserted. - SHOW CREATE VIEW's WITH AUDIT branch and CREATE VIEW over an audited view had no test. Both now have one. The SHOW test writes the _view file, since that factory reads the file rather than the graph. - setMetadataProbe restores the previous value instead of clearing it, so an outer probe survives an inner one, and states that it covers the cursor open rather than the compile. - An empty bracket pair is claimed by the list lookahead, so `DECLARE @x := ()` names the mistake instead of complaining about ':=' arity. - A misused list reports the opening bracket rather than the whitespace before it, and compareArgs got the VALUE_LIST guard its two siblings already had. - ViewAuditModel documents that it is pool-owned and only valid for the compilation that produced it, unlike the ViewDefinitions alongside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wm14GQVQDZ7yRbSJqp8pSF
Code review (round 2)Reviewed at 🚨 A one-member declared list fell off the JIT filter path
Same for SYMBOL. The bare The type is now set on both branches. Why it slipped, and what changed:
|
Code review (round 3)Fresh pass at Auditing round 2's own change firstRound 2 retyped the spliced
Also checked: 💡 A literal nested list silently discards members
DECLARE @a := ('x','y'), @b := (@a, 'z') … -- error: declared list can only be used on the RHS of INThe literal form takes a different route entirely: DECLARE @x := ('a', ('b','c')) SELECT s FROM k WHERE s IN @x -- returns 'a' onlyThe list became Not a regression — nothing accepted this before. 💡 The drop-veto comment asserts an invariant OSS cannot demonstrate
So if Enterprise implements CREATE OR REPLACE on the create path, it clears the flag without passing through 🧹 Nits
Also verified this pass
Suites1764 tests green at this tree: Fixes for the two findings and the first nit to follow on this branch. 🤖 Generated with Claude Code |
`DECLARE @x := ('a', ('b','c'))` quietly meant `('a','c')`. The lookahead only looks for a separator at depth 1, so the inner brackets parsed as a parenthesised scalar, which the expression parser evaluates to its last member. Losing members without saying so is the behaviour a declared list exists to replace, so it cannot be the behaviour a declared list has - and the variable spelling of the same mistake, `@b := (@A, 'z')`, was already refused. The two spellings now agree. The check reuses the same lookahead that decided the right-hand side was a list, so a list and its members are judged by one rule. A bracketed element with no separator - `(1+2)`, `greatest(1, 2)` - is untouched and stays a parenthesised scalar. Where an element starts differs between the first member and the rest: nothing is unparsed after the opening bracket, while every later member has had its first token read and pushed back, which leaves getPosition() past it. Also corrects two comments that claimed more than the code does: - executeDropView said CREATE OR REPLACE carries the audited flag forward. There is no such statement here, and the create path takes the flag from the statement rather than from what is on disk, so a statement that replaces a view would clear the marking without reaching the drop gate. ALTER VIEW does carry it forward, through ViewGraph.updateView - that half is verified. - checkTableDroppable is consulted on the two table drop routes only, not on the view, materialized view or live view drops. A view cannot be made undroppable this way, and the javadoc now says so. SHOW CREATE VIEW notes that WITH AUDIT is emitted before putAdditional(), since OSS is choosing the clause order for a clause only Enterprise parses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wm14GQVQDZ7yRbSJqp8pSF
|
Round-3 fixes pushed as
1765 tests green: |
The previous round's comment claimed there is no CREATE OR REPLACE VIEW here and warned that a replace-style statement would clear the audited marking without reaching the drop gate. Both halves were wrong. compileCreate intercepts CREATE OR REPLACE VIEW over an existing view and executes it through alterViewExecution, so it lands in ViewGraph.updateView exactly as ALTER VIEW does, and that carries the current flag forward rather than taking one from the statement. Only a CREATE over a name that does not exist yet takes the flag from the statement, and there is nothing to lose there. So the original claim was right: DROP is the only route by which an audited view loses its marking, which is what makes the drop gate sufficient. Restating it is not enough though - the invariant the gate rests on was asserted by nobody, so testRedefiningAnAuditedViewKeepsTheFlag now drives both spellings over an audited view and checks the flag in the graph and on disk after each. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wm14GQVQDZ7yRbSJqp8pSF
Correction, and the Enterprise CI failure diagnosedFirst, a correction to my last reviewI claimed there is no What actually happens: So the original comment on this PR was right: DROP is the only route by which an audited view loses its marking, which is what makes the drop gate sufficient. The invariant was asserted by nobody though, so The three questions I raised for Enterprise — answered from the ENT branch
Also closed: the audit table is protected by Enterprise CI: reproduced, and it is not caused by this PRI built the ENT companion branch against this PR's head with OSS core in the reactor. ENT compiles cleanly — so it is not a source break from the new interface methods. The audit suites themselves pass: Pulling the actual failures out of build 268191: no
All three are expected consequences of deliberate ENT design choices (the startup creation is commented as intentional, and there is a One thing worth a decision rather than a bulk expectation update: whether Separately, the ENT branch still pins its 🤖 Generated with Claude Code |
Enterprise wraps a plan for auditing only when the statement's query model carries view audits, so an empty list is what leaves a query that reads no audited view with the plan master builds. Two tests in ViewAuditTest pin that at the model level, before later work touches the parser's view expansion. testQueryReadingNoAuditedViewRecordsNothing compiles reads of a plain table, a plain table with DECLARE, a view that declares an AUDITED parameter but is not audited, the same view with a caller override, and a materialized view, plus INSERT ... SELECT, CREATE TABLE AS and UPDATE ... FROM over those sources. Each model carries no audit. A read of an audited view is the control, alone and joined to the sources that are not audited: it carries exactly one audit. testAuditsOfOneCompileDoNotCarryOverToTheNext compiles a read of an audited view and then a read of a plain table on one compiler, so both go through the same parser and the same pooled models. The second model carries no audit. Both tests fail when they should. With the isAudited() gate in SqlParser.compileViewQuery forced open, the first test fails on its first view query. With recordedViewAudits no longer cleared per compile, both fail. The commit also moves three tests to their alphabetical places. The file is new on this branch, so that adds nothing to the diff against master. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SqlExecutionContext gains isBackgroundJob(), false by default. The materialized view refresh, live view refresh and WAL apply contexts override it to return true. No principal reads data through those contexts, so Enterprise records no view audit row for them: such a row would name no real user and repeat on every refresh, or on every node that replays the WAL. SqlOptimiser.optimiseUpdate() now hands the update model's view audits to the optimised nested model. The parser attaches a statement's audits to its top model. For an UPDATE that model only carries the SET expressions, and code generation compiles the nested one into the cursor that reads the rows, so an UPDATE that read an audited view recorded nothing in Enterprise, on any table. ViewAuditTest asserted the UPDATE's audit on the top model, which is why it passed. Its helpers now assert on the model code generation reads, and the test covers a view in a sub-query as well as in the FROM clause. A new test pins the flag on the two refresh contexts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A read of an audited view records one audit per reference site. When an audited view is built from other audited views, a read of the outer view also recorded every inner one, although the principal asked for the outer view only. An audited view read inside the body of another audited view now records no audit when the outermost audited view around it covers it: every parameter the inner view declares OVERRIDABLE AUDITED is one the outer view declares AUDITED. Those are the parameters a caller can set, and an override reaches the inner view through the outer one, so an inner read that is not covered keeps its audit and the caller's value stays in the trail. Only the outermost audited view shadows, and it looks through views that are not audited; a view that is not audited never shadows. The parser counts the audited views being expanded, and after the body of the outermost one returns, drops the covered audits recorded inside it. A view that is not audited still costs one check of its flag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
QueryCompileBenchmark compiles five queries that read no audited view - a table, the table through DECLARE, a view, a view whose declared variable the caller overrides, and a materialized view - and closes each plan without opening a cursor. It uses no syntax this branch adds, so it runs unchanged on the base commit, which is how the branch's compile-time cost for such queries is measured against master. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
executeCreateMatView opens a cursor on the view's SELECT to validate its metadata and closes it without reading a row. Unlike the column-type probe in executeCreateView, it did not raise isMetadataProbe() around that open, so in Enterprise a materialized view created over an audited view left an audit row under its creator's principal. The row looked like a real read - status ok, the view's defaults - for an access that never happened. executeCreateMatView now raises the flag around the cursor open and restores the previous value afterwards, as executeCreateView does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
alterViewExecution checks that a view's new body runs by opening a cursor on it and calling hasNext() once, and hands the row to no one. It did not raise isMetadataProbe() around that open, so in Enterprise redefining a view whose new body reads an audited view - with ALTER VIEW, or CREATE OR REPLACE VIEW over an existing view, which lands in the same method - left an audit row under the principal that ran the statement, for a read nobody made. alterViewExecution now raises the flag around the cursor open and restores it afterwards, as executeCreateView and executeCreateMatView do. ViewAuditFactory reads the flag at open, so the hasNext() after the restore still records nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…in-lists # Conflicts: # core/src/main/java/io/questdb/WorkerPoolManager.java # core/src/main/java/io/questdb/griffin/SqlExecutionContextImpl.java
The Java lint job formats the tree with the project's IntelliJ code style and fails on any change. It wraps the "declare" case of the query switch, which ran past the line limit. No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review: full depth (level 3)Reviewed revision
Moderate1. The view compiler job opens audited PIVOT sub-query plans as a non-background read
Trigger: an audited A compiler subclass that logs at
Base cannot express the trigger, because it has no audited views. Suggested fix:
If recording these reads is intended, the javadoc and the PR description should say so. 2. OSS call sites of the Enterprise audit hooks have no test
The call sites:
All five are OSS code whose only consumer is Enterprise, and no test in this repository fails if any of them regresses. Deleting the DROP VIEW gate would let a principal holding DROP VIEW but not AUDIT VIEW remove an audited view, along with its marking. Clearing the probe flag instead of restoring it, or never raising it, would bring back the spurious rows the PR describes fixing. Enterprise suites may cover the consumers; nothing here does. The PR already tests the parallel engine vetoes this way (
Minor
Adjacent findings (pre-existing on base, not blocking; worth filing as issues)A1. A stale cached INSERT plan closes the transaction's parked writer over PGWire
A2. Declared variables are not substituted in named WINDOW specs
Coverage map
SummaryVerdict: approve with comments. Please address findings 1 and 2. Neither blocks the merge.
🤖 Generated with Claude Code |
The IntelliJ formatter check in CI failed on three blank lines inside text blocks in testDeclaredListWithNullMembersMatchesWrittenOutList. The formatter indents such lines to the text block's level, so this commit adds the 20 spaces it expects. Java strips trailing whitespace in text blocks, so the expected query output stays the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ViewCompilerExecutionContext now reports isBackgroundJob() as true. The view compiler job, the startup compileAllViews() and COMPILE VIEW compile under that context type, and compiling a view opens the cursor of each PIVOT ... IN (SELECT ...) sub-query in its body. Enterprise would record an audit row for each such open of an audited view, credited to the read-only context, on every recompile and startup. ViewAuditTest gains tests for the OSS call sites of the Enterprise audit hooks: the authorizeAuditView() gate on DROP VIEW and DROP ALL TABLES, and the isMetadataProbe() raise and restore around the check cursors of ALTER VIEW, CREATE VIEW and CREATE MATERIALIZED VIEW, also when getCursor() throws. A recording factory captures the flags each audited plan's cursor open sees, including on the view compiler paths. The parse-budget tests in WithClauseTest, ViewQueryTest and DeclareTest no longer pin the spent count of "statement is too complex to parse", which follows pool allocation; they assert the position, the budget part and its max. ConcurrentViewCycleTest bounds its EXPLAIN retry by the recompile attempt limit, so a plan that stays stale fails the test instead of hanging it. DeclareListFuzzTest gets its license header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code review (level 3, round 5)Reviewed
Verdict: request changes. One Critical finding, two Moderate and two Minor. Critical1. A positional
|
| Statement | 6c63556431 |
ef8006f48b |
Correct |
|---|---|---|---|
SELECT -1L v UNION ALL SELECT -2L UNION ALL SELECT * FROM (SELECT v FROM t UNION ALL SELECT v FROM t ORDER BY 1 DESC LIMIT 1) |
[110] Invalid column: 1 |
-1, -2, 1, 2, 3, 3 |
-1, -2, 3 |
WITH top1 AS (SELECT v FROM t UNION ALL SELECT v FROM t ORDER BY 1 DESC LIMIT 1) SELECT -1L v UNION ALL SELECT -2L UNION ALL SELECT * FROM top1 |
[65] Invalid column: 1 |
-1, -2, 1, 2, 3, 3 |
-1, -2, 3 |
SELECT 0L v, 0L v1 UNION ALL SELECT * FROM t t1 JOIN (SELECT v FROM t UNION ALL SELECT v FROM t ORDER BY 1 DESC LIMIT 1) t2 ON t1.v = t2.v |
[105] Invalid column: 1 |
(0,0), (1,1), (2,2), (3,3), (3,3) |
(0,0), (3,3) |
- Controls: the same inner query returns
3on both revisions when run alone, as the first outer branch or as the second. - Other shapes: the same misplacement shows in a fourth branch, under an outer
UNION,EXCEPTorINTERSECT, with an innerUNION, withORDER BYand noLIMIT, and with the inner query in brackets:UNION ALL SELECT * FROM (<inner>). - Realistic case: a top-N CTE or sub-query read as the third branch of a
UNION ALL.
At head, EXPLAIN of the first statement puts the Top K on the inner union's last branch:
Union All
Union All
VirtualRecord / long_sequence (-1L)
VirtualRecord / long_sequence (-2L)
Union All
PageFrame ... on: t
Async Top K lo: 1 keys: [v desc]
PageFrame ... on: t
Cause:
- The changed
rewriteOrderByPositionForUnionModels()(SqlOptimiser.java:10382) now walks every branch's nested model and its join models, so the positional1resolves. - The unchanged
bubbleUpOrderByAndLimitFromUnion()(SqlOptimiser.java:2287) moves a trailingORDER BY/LIMITabove the whole set operation. It recurses only into the first branch's nested model and the second branch's nested model. From the third branch on it only walks to the last branch, and it never visits join models. - On
masterthe unresolved1made such a statement fail. Now it resolves in place and stays on the last inner branch. - The PR description lists this as a fix ("a positional
ORDER BYin such a branch resolves").
Why the tests miss it: UnionTest.testUnionNestedInLaterBranchWithPositionalOrderBy (UnionTest.java:1709) uses only the bracketed-branch form, UNION ALL (SELECT ... ORDER BY 2 DESC LIMIT 1). That form is the one place where the branch-scoped result is the correct one, so the test passes either way. Its comment claims the general behaviour.
Base behaviour:
- For the positional spelling,
masterrejects every statement above withInvalid column: 1. - The named spelling (
ORDER BY v DESC LIMIT 1) already returns the same wrong rows onmaster(see Adjacent A1). - So this PR does not create the misplacement. It extends it to a spelling that used to fail loudly.
Suggested fix:
- Make
bubbleUpOrderByAndLimitFromUnion()walk every branch's nested model and its join models, as the new loop inrewriteOrderByPositionForUnionModels()does. That also fixes A1. - If that is out of scope here, keep the positional spelling rejected in those positions until the bubble-up visits them. The
queries have different number of columnscheck the new loop brings can stay. - Either way, add the three statements above to
UnionTestas.returns(...)cases with their correct rows.
Moderate
2. ConcurrentViewCycleTest now fails intermittently
- Problem: the reader's new retry limit gives up while the writers are still redefining the view.
- Net impact: CI flakiness. The production code under test behaves correctly.
- Evidence: 21 of 500 runs failed at
ef8006f48b, all with the stale-plan exception; 0 of 500 failed with the previous loop.
printExplain() (ConcurrentViewCycleTest.java:191-203) now rethrows TableReferenceOutOfDateException after maxSqlRecompileAttempts attempts, which is 10 in the test configuration. The two ALTER VIEW threads commit about 100 redefinitions in a burst. One EXPLAIN can therefore meet 10 or more redefinitions in a row, each committed while that attempt was in flight.
| Variant, 500 runs in one JVM | Failures |
|---|---|
| The test as committed | 21, each TableReferenceOutOfDateException ... [table=view1', expectedSeqTxn=36, actualSeqTxn=37] or similar, thrown from printExplain |
| The same copy with the limit raised to 1,000 | 0 |
| The previous unbounded loop, with a 60 s hang detector | 0 failures, 0 hangs |
- Instrumentation (unbounded loop): 24 calls needed 10 to 19 consecutive retries and all of them succeeded.
- No production bug: every stale attempt had compiled the definition that was current when it compiled.
- After the writers stop: no call needed more than one retry.
- Fresh JVMs: 60 single runs, 0 failures. The longest run of consecutive stale attempts was 8, against the limit of 10.
Suggested fix:
- Retry without a limit while the writer threads are running.
- Apply the
maxSqlRecompileAttemptslimit only oncedoneis set.
That still turns a plan that stays stale into a failure rather than a hang.
3. No test pins WalApplySqlExecutionContext.isBackgroundJob()
- Problem: nothing fails if the WAL apply context stops reporting a background job.
- Net impact: Enterprise would audit every WAL replay of such an
UPDATE, on every node, with no OSS test to catch it. - Evidence: a mutant without the override passes
ViewAuditTest(50 of 50),WalApplySqlExecutionContextTestandWalUpdateScalarSubqueryTest.
ViewAuditTest.testOnlyJobContextsAreBackgroundJobs (ViewAuditTest.java:603-618) leaves this context out. Its comment says the class is package-private, so only Enterprise can cover it. The class doesn't have to be instantiated to test this, because WAL apply reaches it.
A probe used the engine and graph setup of ViewAuditTest:
- Create a WAL table
t. - Create
v AS (SELECT s, l FROM t)and mark it audited. - Run
UPDATE t SET l = 42 WHERE l < (SELECT count() FROM v), thendrainWalQueue().
The audited plan's only cursor open happens at apply time, under WalApplySqlExecutionContext, with isBackgroundJob=true. With the override removed, the same probe records false.
The view has to read the target table, because WAL UPDATE refuses other tables. There is one catch: CursorOpenRecordingFactory (ViewAuditTest.java:1770) has to forward supportsUpdateRowId(). Without that, the UPDATE fails with Unsupported SQL complexity for the UPDATE statement.
The Enterprise suites may cover the consumer; nothing in OSS does.
Minor
- 4. Error position of a list override.
- Cause: the list branch of
parseDeclare()(SqlParser.java:4258-4260) builds the:=node and the variable node atpos. That is the raw offset taken before the variable was read, so it can land on whitespace or in a comment. - Effect:
variable is not overridable(QueryModel.copyDeclsFrom) reports it.DECLARE @a := 1, @s := ('a','b') SELECT * FROM v11, over a view that declares@swithoutOVERRIDABLE, reports[16], while@sis at 19. A scalar value reports its:=token. - Fix: use
variablePositionand the:=token's position.mastercannot express this statement.
- Cause: the list branch of
- 5. Duplicated test helper. ef8006f adds the same private
assertTooComplexToParse(CharSequence, int, String, long)to three classes:WithClauseTest.java:802,DeclareTest.java:5759andViewQueryTest.java:2811. All three classes extendAbstractCairoTest, so one shared copy would do.
Adjacent findings (already on master, not blocking this PR)
These reproduce identically on 6c63556431 and ef8006f48b.
A1. A set operation's trailing ORDER BY/LIMIT applies to its last branch when the set operation is read in a JOIN or in a later branch
- Net impact: wrong rows, Critical if filed on its own.
- Location:
SqlOptimiser.bubbleUpOrderByAndLimitFromUnion()(SqlOptimiser.java:2287-2348). - Symptoms:
SELECT * FROM t t1 JOIN (SELECT v FROM t UNION ALL SELECT v FROM t ORDER BY v DESC LIMIT 1) t2 ON t1.v = t2.vreturns(1,1), (2,2), (3,3), (3,3)instead of(3,3). The positional form behaves the same.- In a third union branch,
ORDER BY v DESC LIMIT 1andLIMIT 2alone are misplaced the same way.
- Fix: the same as finding 1. If this PR adopts the first option there, A1 needs no separate issue.
Coverage
- Test gate: passes. There are no Critical coverage gaps.
- Admitted coverage gaps: one, Moderate (finding 3). Finding 1 also has a test gap, covered in that finding.
- CLAUDE.md test rules: they hold on the added lines. There is no
assertSql, noreturnsOnce, nogetPlan, and no loneassertQuerywrapped inassertMemoryLeak.
Summary
- Verdict: request changes. Finding 1 is the blocker.
- Severity: 1 Critical, 2 Moderate, 2 Minor, all in the diff.
- Finding 1 is in the changed
rewriteOrderByPositionForUnionModels()hunk, which exposes the unchanged bubble-up gap. - Findings 2, 3 and 5 are in tests.
- Finding 1 is in the changed
- Submodules: none moved.
- Areas checked with no finding:
- What
attachViewReads()now attaches toINSERT ... SELECT, CTAS,UPDATEandEXPLAINplans, and what consumes it. - The table-function sweep. Probes counted one close per factory across reads, retries and failures.
- Lexer reuse,
compileBatchrepositioning, the PGWire parked writer, the page-frame stale-view check. - The
_viewextra block across every writer of the file. - The WITH scope rework and the
(rule after a declared value. - Where head and base differed on these, the difference was a fix or a tradeoff the PR documents.
- What
- Tradeoffs the PR documents and this review did not re-argue:
- The parse budget's fixed constants.
- The
DECLAREforms now refused, including in stored views. - The legacy alias renames.
- The extra memory very large
WITHlists retain. Compile time grows with it, which the description does not mention: at 4,000 CTEs a probe measured 4.1 s vs 2.3 s, and 494 MB vs 284 MB retained.
🤖 Generated with Claude Code
bubbleUpOrderByAndLimitFromUnion() lifted a set operation's trailing ORDER BY/LIMIT above the set operation only where the set operation was read at top level, in a FROM sub-query or in the first two branches of an outer set operation. In a third or later branch and in any join model the clause stayed on the last inner branch, so a top-N CTE or sub-query read there returned extra rows. The positional spelling failed on master with "Invalid column: 1"; since rewriteOrderByPositionForUnionModels() started visiting every branch, it resolved in place and returned the same wrong rows. The method now walks every branch's nested model and its join models. A bracketed branch keeps its own clause. The lift has visible side effects. An ORDER BY without LIMIT in a join's right-hand side now sorts the whole set operation, which costs a full sort where nothing observes the order. A trailing ORDER BY that names something only the last branch defines now fails to compile in those positions, as it always did at top level, so stored views of that shape go invalid. ASOF, LT and SPLICE joins over a set operation ordered by timestamp, which failed with "no timestamp", now run. parseDeclare() built the nodes of a list-valued declaration at the raw offset the previous token left behind. A caller's list override of a non-overridable view variable therefore reported "variable is not overridable" on whitespace or inside a comment. The `:=` node now sits on the `:=` token, as it does for a scalar value, and the variable node on the variable. Tests: - UnionTest pins the lift in later and middle branches, in the first and second join, through a CTE and under outer UNION, EXCEPT and INTERSECT, with plans. Mutants that skip middle branches or later join models fail it. - ViewAuditTest pins WalApplySqlExecutionContext.isBackgroundJob() through an UPDATE of a WAL table that reads an audited view. Its recording factory forwards supportsUpdateRowId() for that. - ViewQueryTest pins the error position of a list override. - ConcurrentViewCycleTest's EXPLAIN reader retries a stale plan without a limit while the ALTER VIEW threads run and applies the recompile limit only after they stop. With the limit alone, 21 to 23 of 500 runs failed while the writers were still redefining the views. - AbstractCairoTest hosts assertTooComplexToParse(), which three test classes duplicated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code review (level 3, round 6)Reviewed
Verdict: approve with comments. No Critical findings; two Moderate and three Minor. Moderate1. A malformed
|
| Statement | 6c63556431 |
fa547b7c39 |
|---|---|---|
DECLARE @x := CASE (WHEN v > 1 THEN 1 ELSE 2 END) SELECT @x FROM t |
[14] unbalanced 'case' |
AssertionError: Should have popped CASE, but got PAREN |
DECLARE @x := CASE WHEN (v WHEN 1) THEN (1) ELSE (2) END SELECT @x FROM t |
[14] unbalanced 'case' |
NullPointerException, with and without -ea |
SELECT CASE (WHEN v > 1 THEN 1 ELSE 2 END) FROM t |
AssertionError (same) |
AssertionError (same) |
- What a client sees for the first statement:
- HTTP
/exec: base returns 400 with"error":"unbalanced 'case'","position":14; head returns 500 with"error":"Should have popped CASE, but got PAREN","position":0and logs a critical error. - HTTP
/exp: head closes the connection without a response. - PGWire: head reports the assertion text at position 1; the connection stays usable.
- HTTP
- Cause: the new rule for a
(after a declared value (ExpressionParser.java:1326-1333) ends the value only whenscopeStack.size() == 0. A bracket inside an openCASEis now pushed as aPARENscope, and the unchangedENDhandling (ExpressionParser.java:1987-1989) pops it and asserts, or dereferences a nullnode. On base the old rule ended the declared value at that bracket, so the statement failed earlier with aSqlException. - Why the rule itself is right: the same old rule rejected valid values.
DECLARE @x := CASE WHEN (v > 1) THEN 1 ELSE 2 END SELECT @x FROM tfails on base with[14] unbalanced 'case'and returns2, 1, 1on head. - Scope: only spellings with a misplaced bracket or
WHENinside theCASE. A missingEND, a missingTHENand an unclosed bracket in aWHENcondition still give aSqlExceptionon head. The compiler stays usable afterwards, and nothing leaks.
Suggested fix: in the END handling, throw a positioned SqlException (unbalanced 'case') when the popped scope is not CASE or node is null, instead of asserting. That also fixes the plain SELECT spelling, which fails the same way on master. Add the two statements above to DeclareTest, and the plain spelling to ExpressionParserTest.
2. Four behaviour changes of the set-operation lift have no test
- Problem: the new
UnionTestcases cover inner, left and cross joins withORDER BY ... LIMITonly. - Net impact: a regression in the shapes below would pass CI.
- Evidence: searches of
core/src/test/javafind no such test; probes at both revisions show each change.
Searches, each returning no test of these shapes:
rg -U -n -i "(asof|lt|splice)\s+join\s*\((?s:.{0,300}?)\b(union|except|intersect)\b"rg -n -i "join\s+lateral\s*\([^;]*\b(union|except|intersect)\b[^;]*\b(order by|limit)\b"rg -n -i "(right|full)( outer)? join\s*\([^;]*\bunion\b[^;]*\b(order by|limit)\b"
An optimiser instrumented to log every lift, run over 146 test classes (11,155 tests), recorded lifts only in the five new UnionTest methods, and its join lifts all carried both ORDER BY and LIMIT on an inner, left or cross join.
With t holding 1, 2, 3, and ts1/ts2 as two timestamped tables of four rows:
| Shape | 6c63556431 |
fa547b7c39 |
|---|---|---|
SELECT a.v, b.v FROM ts1 a ASOF JOIN (SELECT v, ts FROM ts2 UNION ALL SELECT v + 1, ts FROM ts2 ORDER BY ts) b |
ArrayIndexOutOfBoundsException, for LT and SPLICE too |
1 null, 2 11, 3 21, 4 31; LT and SPLICE return rows too |
SELECT t1.v, t2.v FROM t t1 JOIN LATERAL (SELECT v FROM t WHERE v <= t1.v UNION ALL SELECT v + 10 FROM t WHERE v <= t1.v ORDER BY v DESC LIMIT 1) t2 |
7 rows | 1 11, 2 12, 3 13 |
SELECT * FROM t t1 JOIN (SELECT v FROM t UNION ALL SELECT v w FROM t ORDER BY w DESC LIMIT 1) t2 ON t1.v = t2.v |
4 rows | [78] Invalid column: w |
SELECT * FROM t t1 CROSS JOIN (SELECT v FROM t UNION ALL SELECT v + 10 FROM t LIMIT 2) t2 |
15 rows | 6 rows |
LIMIT -2 and ORDER BY alone in a join, and a RIGHT or FULL JOIN over the ORDER BY ... LIMIT form, also return different rows on the two revisions and have no test.
The PR description lists the first three as changes, and the fourth is the same fix in a join with a different clause.
This is Moderate, not Critical, because the two halves are pinned separately: the join loop by the ORDER BY ... LIMIT cases, and the wrap condition by the LIMIT-only and ORDER BY-only cases in a later branch (UnionTest.java:1936, :1948). What no test reaches is the code downstream of the wrapper for time-series and LATERAL joins.
Suggested fix: one .returns(...) statement per row in UnionTest, plus assertException for the refused ORDER BY w.
Minor
-
3. Error position of a time-series join over a set operation with a trailing
LIMIT.- Problem: the error points at position 0 and no longer names the join.
- Net impact: invalid statements only.
- Evidence: probes at both revisions.
SELECT a.v, b.v FROM ts1 a ASOF JOIN (SELECT v, ts FROM ts2 UNION ALL SELECT v + 1, ts FROM ts2 LIMIT 3) breports[27] right side of time series join has no timestampon base and[0] TIMESTAMP column is required but not providedon head. The hand-writtenASOF JOIN (SELECT * FROM (...) LIMIT 3)reports[38]on both. - Cause: the wrapper models built at
SqlOptimiser.java:2331-2342never get a model position, unlike the ones at:3454-3455, andSqlCodeGenerator.java:9855reportsmodel.getModelPosition(). The same 0 shows on base wherever base already lifted the clause, such as a set operation on the left of anASOF JOIN, and theORDER BY v LIMIT 3spelling moved the other way, from[0]on base to[27]on head. - Fix: give both wrappers the position of the set operation's first model.
-
4. PR description, set-operation sort cost.
- Problem: the cost bullet quotes one figure, 353 ms against 138 ms.
- Net impact: readers will underestimate the cost when the last branch is the small one.
- Evidence: timing probe at both revisions: one compiled plan, minimum of 7 cursor reads, each revision run twice in ABBA order. The set operation has a 2M-row first branch and a 1k-row last branch.
Shape 6c63556431fa547b7c39hash join under sum(),ORDER BYa randomLONG76 to 78 ms 329 to 347 ms cross join under sum()26 to 30 ms 159 to 167 ms third branch, full read 22 ms 143 to 165 ms hash join, ORDER BY ts98 ms 120 ms hash join, large branch last 419 to 437 ms 346 to 347 ms - The cost grows with the rows outside the last branch, which base never sorted, and the sorted copy stays in native memory while the cursor is open. Stating that would be more useful than the single figure.
- The same bullet could say that base failed with
ArrayIndexOutOfBoundsExceptionforASOF JOIN (<set operation> ORDER BY ts)unless the outer query also selected the right-hand timestamp; the description names only theno timestamperror.
-
5. Cosmetics in the new tests.
AtomicBoolean writersDone(ConcurrentViewCycleTest.java:198) has nois/hasprefix. Eleven SQL literals of 150 to 190 characters sit on one line inUnionTest(for example:1796,:2205), where CLAUDE.md asks for a text block.
Adjacent findings (already on master, not blocking this PR)
These reproduce identically on 6c63556431 and fa547b7c39. The adjacent items of earlier rounds are not repeated.
A1. Parquet export of a view skips the guard for lazily type-converted Parquet columns (outline only; I can share the reproduction privately)
- Net impact: some view exports fail at process level or write wrong values. Critical if filed.
- Location:
StaleViewCheckFactory.getBaseFactory()(:91-93) returnsbase.getBaseFactory(), and the class does not overridehasParquetConvertedColumns().ParquetExportMode.java:83-85therefore never sees the flag for a view plan and picks the direct page-frame path, where the same query on the table takes the temp-table path. - Symptom: seen through both
COPY ... TO ... PARQUETand HTTP/exp, when the table has a Parquet partition and a later column type change between a fixed-size and a variable-size type. The same export of the table itself is correct, and fixed-to-fixed changes export correctly through the view too. - Suggested fix: forward
hasParquetConvertedColumns()tobaseinStaleViewCheckFactory, along withmayHaveParquetPartitions()andusesExternalDataSource().
A2. A CTE named like an existing view is ignored
- Net impact: the query silently reads the view instead of the CTE. Critical if filed.
- Location:
SqlParser.parseFromClause()andparseJoin()look the name up as a view before they consult the WITH clauses (SqlParser.java:5116-5120,:5904-5906). - Symptom: with
CREATE VIEW vy AS (SELECT v FROM t WHERE v > 1)overtholding 1, 2, 3,WITH vy AS (SELECT 9L v) SELECT * FROM vyreturns2, 3. A CTE named like a table shadows the table as expected:WITH t AS (SELECT 9L v) SELECT * FROM treturns9. - Suggested fix: resolve the name against the WITH clauses in scope before the view lookup.
A3. Three more LATERAL shapes for #7803
- A body that is a set operation of scalar aggregates drops outer rows with no match.
SELECT t1.v, t2.c FROM t t1 JOIN LATERAL (SELECT count() c FROM t WHERE v < t1.v UNION ALL SELECT count() + 10 FROM t WHERE v < t1.v) t2returns nothing fort1.v = 1; the single-branch body returns1 0. - A view whose body joins
LATERALover a correlated set operation is created but cannot be read:UnsupportedOperationExceptionatQueryModelWrapper.setViewNameExpr(), called fromSqlOptimiser.rewriteDistinct(). Views over a single-tableLATERALbody work. - A correlated set operation in the third branch of a
LATERALbody fails with[0] Invalid column: __qdb_outer_ref__0_v.
A4. Malformed or unusual statements that end in an internal error
SELECT sum(v) OVER (PARTITION BY , g ORDER BY ts) a FROM t:NullPointerException.SELECT * FROM (SELECT $1, 1 FROM t LIMIT 1):AssertionError: wtf? $1.EXPLAIN CREATE VIEW vx AS (SELECT * FROM t):AssertionError: wtf? *.- The plain-
SELECTspelling of finding 1. - Moderate if filed; each should be a positioned
SqlException.
A5. Smaller items
ASOF JOIN (SELECT v, dateadd('s', 1, ts) ts FROM (SELECT * FROM (<set operation>) LIMIT 3)) bfails with[64] Invalid column: tson both revisions. On head the form without the hand-written wrapper now fails the same way, where base ran it with theLIMITon the last branch.- An
ALTER VIEWthat the cycle check rejects leaves the table-function factory of its new body unclosed, like theCOMPILE VIEWpath of COMPILE VIEW leaks native memory when the view body reads a table function #7713. It belongs in that issue.
I can file any of these as issues.
Coverage
- Test gate: passes. There are no Critical coverage gaps.
- Admitted coverage gaps: one, Moderate (finding 2).
- Regression tests: each new test fails without its fix, as listed at the top.
- CLAUDE.md test rules: they hold on the added lines. There is no
assertSql, noreturnsOnce, nogetPlan, and no loneassertQuerywrapped inassertMemoryLeak.
Summary
- Verdict: approve with comments. Please address findings 1 and 2; neither blocks the merge. The red Enterprise run does need an explanation first.
- Severity: 0 Critical, 2 Moderate, 3 Minor, all in the diff. Finding 1 is in the changed
(rule, which lets declared values reach an unchanged assertion. - Submodules: none moved.
- Areas checked with no finding:
- The lifted form against the hand-written
SELECT * FROM (<set operation>) ORDER BY ... LIMIT ...in about 60 pairs: every join kind, all four set operations, predicate push-down, CTEs, views,UPDATE ... FROM,INSERT ... SELECT, CTAS and materialized views. Rows and plans matched. - Table-function ownership across 27 lifted shapes: one close per factory on both revisions.
- Parser state on a pooled compiler after 44 failing statements.
- Compile cost for statements without the new syntax, and declared
INlists up to 99,000 members, which parse in linear time.
- The lifted form against the hand-written
- Tradeoffs the description documents, confirmed here:
- The sort cost of finding 4. The hand-written form and a single-table sub-query with
ORDER BYin a join keep the same sort onmaster, under an aggregate too, so the lift adds no cost those spellings did not already pay. ORDER BY ... LIMIT nin these positions sorts the whole set operation before the limit, as the top-level form does onmaster.SELECT *over aLATERALset operation with a trailingLIMITexposes__lateral_rn, as LATERAL sub-queries return wrong rows, expose internal columns or fail with internal errors: joined correlated sub-queries, LIMIT, window functions, scalar aggregates #7803 section 9 describes for the hand-written form. On head that column also reaches a table created by CTAS and breaks the column count when the join is itself aUNION ALLbranch; base returned wrong rows for the same statements.- The extra memory large
WITHlists retain: a probe here measured 496 MB against 285 MB at 4,000 CTEs, matching the description.
- The sort cost of finding 4. The hand-written form and a single-table sub-query with
🤖 Generated with Claude Code
Code review (level 3, round 7)Reviewed
Verdict: approve with comments. No Critical findings; three Moderate and three Minor, all new in this round. Moderate1.
|
6c63556431 |
fa547b7c39 |
|
|---|---|---|
| Response | HTTP 200 with the plan | HTTP 400 /exp endpoint only accepts SELECT |
EXPLAIN plans |
3 built, 3 closed | 4 built, 3 closed; ExplainPlanFactory -> StaleViewCheckFactory -> ... still open after the server and engine closed |
- Cause:
generateExplain()now builds the plan throughgenerateSelectOneShot()(SqlCompilerImpl.java:5546,:6247), soExplainPlanFactorythrowsTableReferenceOutOfDateExceptionwhen it opens the cursor.- The unchanged retry branch in
ExportQueryProcessor.java:256-260accepts onlySELECT, where the first compile at:175acceptsSELECTorEXPLAIN.closeAllButSelect()has noEXPLAINcase, so nothing frees the recompiled factory before the throw.
- Base: the retry branch is older than this PR. Base reaches the same response and the same open plan when a table the explained query reads gains a column during the request. This PR adds the view trigger.
- Size: a trivial plan leaves its JIT filter allocated (
NATIVE_JIT+2); a join withGROUP BYalso leaves 16,512 bytes ofNATIVE_JOIN_MAP. The window runs from the start of the compile to the cursor open, about 0.4 ms for a trivial view./execand PGWire return the new plan for the same sequence, and a plainSELECTon/expretries correctly. - Description: "PGWire and HTTP recompile on it" does not hold for
/exp.
Suggested fix: accept EXPLAIN in the retry branch as :175 does. Add a hooked test next to ExpParquetExportTest.testParquetExportPageFrameRecreatedViewAfterCache.
2. An UPDATE that reads a view leaks its first plan when the view is redefined during the statement
- Problem: the stale plan's
UpdateOperationis never closed when the statement retries. - Net impact:
UPDATE ... FROM <view>on non-WAL tables over HTTP/execorCairoEngine.execute(); 16.5 KB per hit. - Evidence: hooked probe at both revisions, with native memory per occurrence and plan close counts.
Trigger: UPDATE upd SET w = vw.v FROM vw WHERE upd.v = vw.v, or the view in a WHERE sub-query, while another session commits ALTER VIEW vw between the statement's compile and its execution. The statement retries and reports success.
6c63556431 |
fa547b7c39 |
|
|---|---|---|
| After 1 occurrence | nothing held | NATIVE_JOIN_MAP 16,512 bytes, NATIVE_JIT +2 |
| After 50 occurrences | nothing held, leak check passes | NATIVE_JOIN_MAP 825,600 bytes, NATIVE_JIT +100, leak check fails |
HTTP /exec, 50 occurrences |
50 plans built, 50 freed | 100 built, 50 freed |
| PGWire, 50 occurrences | no leak | no leak |
- Cause:
optimiseUpdate()now copies the referenced views onto the nested model (SqlOptimiser.java:15365), so the plan is aStaleViewCheckFactoryandUpdateOperatorImpl.java:157throws atgetCursor().- The unchanged callers do not close the operation on that path:
OperationDispatcher.java:180closes it only when it is done,CairoEngine.java:2071compiles again, andJsonQueryProcessor.java:831frees it only on interruption or out of memory. PGWire frees it atPGPipelineEntry.java:1883.
- Base: with the same trigger base ran the old view body and leaked nothing, which is the bug this PR fixes. The missing close is older, though: base leaks the same plan when the target table gains a column during the statement, and on any failed
UPDATE ... FROM(10 failures leave 10 plans open). This PR adds the view trigger. - Scope: non-WAL targets only, since a WAL target refuses a cross-table
UPDATEat compile. Neither entry point cachesUPDATEplans, so the window is one statement, about 0.5 ms. With anALTER VIEWevery 20 ms and no hook, 68 of 3,000 statements leaked. QWP egress (QwpEgressUpgradeProcessor.java:1010) has the same shape; I did not run it.
Suggested fix: free the UpdateOperation when its execution throws, in CairoEngine.execute() and JsonQueryProcessor, as PGWire does. That also closes the older leak of a failed UPDATE ... FROM. Add a hooked test to ViewAuditTest or UpdateTest.
3. Three behaviours have no test that fails when they break
- Problem: three single-line mutants of the head sources pass every test around them.
- Net impact: a regression in any of them would pass CI.
- Evidence: mutant runs at
fa547b7c39; a probe per mutant shows what a user would see.
| Production line | Mutant | Tests run under it | What a user would see |
|---|---|---|---|
SqlParser.java:7472, the args walk of readDeclaredQueries() |
skip the walk | 1,170 classes, 28,824 tests (griffin/**, cairo/view, cairo/mv, every class that uses DECLARE); none fails because of it |
DECLARE @q := (SELECT max(v) v FROM t) SELECT v, CASE WHEN v = @q THEN 'max' WHEN v = 2 THEN 'two' ELSE 'other' END c FROM @q returns 3 max on base and head, and fails with AssertionError at FunctionParser.createCursorFunction |
SqlParser.java:7675-7688, the frame-bound visits of spliceValueLists() |
skip either bound | 75 classes, 5,475 tests; none fails | DECLARE @l := (1, 2) ... ROWS BETWEEN (1 IN @l)::INT PRECEDING AND CURRENT ROW returns the rows of the written-out list on head, and fails with unknown function name: ()() |
PGPipelineEntry.java:1730 and :1752, the row-count check and isRetryable |
always retry | 59 classes, 825 tests (every class in the pgwire package); none fails because of it | a transaction commits without its earlier row while every statement reported success |
argswalk: this is the class of break round 2 found for window clauses. The only statement in the suite that reaches a declared sub-query throughargsis a single read (DeclareTest.testDeclareVariableWithBracketedOperand). A read inFROM @qplus a multi-branchCASE,coalesce(a, b, ...)or a three-argument function would pin it.- Frame bounds:
DeclareTest.testDeclaredListInWindowClause(:4652) coversPARTITION BYandORDER BYonly. - PGWire: no existing test enters the non-retry branch. A real statement does: a cached
INSERT INTO dst SELECT ..., touch(SELECT * FROM src) FROM long_sequence(1)in a transaction afterALTER TABLE src ADD COLUMN, becausetouch()opens its cursor inside the copy loop.- WAL target: head fails the statement with the stale-plan error, as intended. The mutant returns 1,
COMMITsucceeds, and the earlier row is gone. - Non-WAL target, extended protocol: the failed row leaves the writer distressed with its row count unchanged, so head retries on it, the statement returns 1, and
COMMITfails withTable 'dst' is distressed. Base commits without the earlier row, so this is no regression. Treating a distressed writer as not retryable would fail the statement instead of theCOMMIT.
- WAL target: head fails the statement with the stale-plan error, as intended. The mutant returns 1,
Suggested fix: one .returns(...) statement for each of the first two rows in DeclareTest, and the touch() sequence in PreparedStatementInvalidationTest for both table kinds.
Minor
- 4. Error position of the two
DECLARElimits inside a view body.- Problem: the position is an offset into the view's body, not into the statement.
- Net impact: refused statements only; the position can lie past the end of the statement.
- Evidence: probe at both revisions. The 54-character
SELECT a.c0 FROM vb a JOIN vb b ON a.c1 = b.c1 LIMIT 1reports[19997] declared variables expand to too many expression nodes. - Cause:
SqlParser.java:4371and:8636reportquery.positionandnode.position.checkParseBudget()at:908reports the statement's read of the outermost view in the same situation.DeclareTest.java:3337notes the difference and checks the message only. - Fix: make the same
viewExpansionDepth > 0 ? viewReferencePosition : positionchoice.
- 5. PR description, PIVOT retry.
- Problem: "the statement's own compile retries" holds only where the caller retries.
- Net impact: readers will expect a raced PIVOT to succeed over PGWire.
- Evidence: probe at both revisions. A
PIVOT ... FOR cat IN (SELECT c FROM cats)whosecatsgains a column during the compile returnscached query plan cannot be used because table schema has changed [table=cats...]to the client, in simple and extended mode. Base returnedunbalanced (, aClassCastExceptiontext or'create', 'rename' or 'select' expectedfor the same race. - Head is the clear improvement, and
PivotTestpins the surfacing for a plainSELECT. The description could say thatCairoEngine.execute()retries the statement and PGWire reports the error.
- 6. Member order in
SqlParser. Two groups of new private methods sit out of alphabetical order:borrowLookaheadLexertoparseValueListbetweenparseDeclaredQueryandparseDml(:4421-4537), andreadDeclaredQueriestospliceInbetweenrewriteCountAndWindowExpressionsandrewriteDeclaredVariables(:7444-7721).
Adjacent findings (already on master, not blocking this PR)
These reproduce identically on 6c63556431 and fa547b7c39. The adjacent items of earlier rounds are not repeated.
A1. Compile time grows exponentially with the length of some arithmetic expressions (outline only; I can share the reproduction privately)
- Net impact: a short statement keeps a compile thread busy indefinitely. Critical if filed.
- Symptom: compile time doubles with each extra term of the expression. I stopped a 100-term statement after 10 minutes.
- Reachability: any plain
SELECT;DECLAREis not involved, and this PR does not touch the code.
A2. The older halves of findings 1 and 2
/expanswers/exp endpoint only accepts SELECTand leaves a plan open when a table read by anEXPLAINgains a column during the request.- A failed
UPDATE ... FROMleaves its plan open (16,512 bytes): on a schema change of the target during the statement, oncannot update parquet-format partition, and on a busy writer. - The fixes suggested in findings 1 and 2 close both.
A3. Internal errors on valid statements
- A set operation with a
SAMPLE BYbranch on the right of a join:SELECT count() FROM t x CROSS JOIN (SELECT ts, sum(v) sv FROM tt SAMPLE BY 2s UNION ALL SELECT ts, v FROM tt) jfails withAssertionErroratSqlCodeGenerator.checkIfSetCastIsRequired. The same set operation in aFROMsub-query works. CREATE MATERIALIZED VIEW mv2 AS (WITH c AS (SELECT s FROM tw WHERE v > 1) SELECT ts, sum(v) sv FROM tw WHERE s IN (SELECT s FROM c) SAMPLE BY 1d) PARTITION BY DAYis created and goes invalid withCannot invoke "MemoryR.getLong(long)" because "this.column" is null.DECLARE @q := (SELECT 1) SHOW COLUMNS FROM @q:NullPointerExceptionatChars.lowerCaseHashCode.- Moderate if filed; each should work or fail with a positioned
SqlException.
A4. Smaller DECLARE gaps
- Variables are not substituted in a
PIVOTclause or inTIMESTAMP(@ts); both fail withInvalid column: @.... - A nested
DECLAREblock cannot read a variable of the enclosing query. FROM @q a JOIN @q bis refused: a declared sub-query is not accepted as a join operand.DECLARE @q := (SELECT max(v) FROM k) SELECT v = @q IS NULL FROM tleaks 4,160 bytes per run. It is theDECLAREspelling of the(SELECT ...) IS NULLleak an earlier round listed.
I can file any of these as issues.
Coverage
- Test gate: passes. There are no Critical coverage gaps.
- Admitted coverage gaps: one, Moderate (finding 3, three rows). Findings 1 and 2 have no test either, which those findings cover.
- Mutation check: about 160 single-line mutants of the changed production lines. Apart from finding 3 and a few that only change the error text or position for malformed input, the suite kills them. That includes every mutant of the WITH scope, the CTE and view digit bookkeeping, the declared sub-query copies, the set-operation loops, the stale-view checks, the batch restart, the view definition blocks and the table-function sweep.
- CLAUDE.md test rules: they hold on the added lines. There is no
assertSql, noreturnsOnce, nogetPlan, and no loneassertQuerywrapped inassertMemoryLeak.
Summary
- Verdict: approve with comments. Please address findings 1 to 3 here and findings 1 and 2 of round 6; none blocks the merge. The red Enterprise run still needs an explanation first.
- Severity: 0 Critical, 3 Moderate, 3 Minor. Findings 1 and 2 are out-of-diff breakage: unchanged callers that mishandle the stale-plan exception this PR now raises for
EXPLAINandUPDATEplans over views. Both need another session to redefine the view within a sub-millisecond window, which is why they do not block. The rest are in the diff. - Submodules: none moved.
- Areas checked with no finding:
- About 330 valid statements run on both revisions, in both alias modes where names matter: CTE nesting, WITH scope in and around view bodies,
DECLAREwith CTEs in every order, overrides, DML and materialized views. Every difference was a fix or a change the description documents. - Pooled parser state: 34 failing statements, each followed by canary queries on the same engine. They include budget and limit failures inside a CTE re-parse, a declared copy and a view expansion, and two stack overflows.
- Declared lists against the written-out list in 109 pairs, on rows and plans.
- The lift against the hand-written
SELECT * FROM (<set operation>) ORDER BY ... LIMIT ...in about 75 pairs: every join kind, CTEs read several times, views,EXCEPT/INTERSECT, negative, two-argument and bind-variableLIMIT,UPDATE ... FROM,INSERT ... SELECT, CTAS and PIVOT. Rows matched, with no internal error that base lacks. EXPLAINof 20 statement kinds (plan text unchanged apart from the lift), 23 multi-statement batches (same texts and error positions), and stale plans over/execand PGWire forEXPLAIN,INSERT ... SELECTandUPDATE.- Table-function ownership for about 370 factories across
SELECT,EXPLAIN, DML, views, PIVOT with a borrowed compiler, retries, batches and a failingclose(): one close per factory on head. Native memory and file descriptors over 20-run loops showed no growth apart from finding 2. - The PGWire parked writer in 15 scenarios: WAL and non-WAL, simple and extended, batches, views and tables altered, dropped or re-created. Head keeps the parked row wherever base lost it. The route into the writer double close that round 2 described is unchanged.
ConcurrentViewCycleTestpassed 36 of 36 runs.
- About 330 valid statements run on both revisions, in both alias modes where names matter: CTE nesting, WITH scope in and around view bodies,
- Tradeoffs the description documents, confirmed here and not re-argued:
- The
DECLAREforms now refused, including in stored views. A block with no comma beforeOVERRIDABLEwas accepted onmaster. ORDER BYnames and column counts now checked for set operations in joins and later branches.- A caller's CTE no longer replacing a table inside a view's sub-queries.
- The
DECLARElimits. They refuse a 54-character self-join of a view whose 20,120-character body reads a 3,500-argument variable 20 times.
- The
🤖 Generated with Claude Code
OperationDispatcher.execute() closed the operation only when the statement completed. An UPDATE that reads another table on a non-WAL target keeps its plan in the operation, so a statement that failed at execution left the plan open: 16,512 bytes of NATIVE_JOIN_MAP per statement over CairoEngine.execute(), HTTP /exec and QWP egress. This branch made the leak easier to reach, because an UPDATE over a view now throws TableReferenceOutOfDateException when the view changes between compile and execution, and the caller compiles again. The dispatcher now closes the operation on every exit when the caller passes closeOnDone, unless a future took it. JsonQueryProcessor drops the close it ran on interruption and out of memory, which the dispatcher now covers. PGWire already closed the operation. ExportQueryProcessor's retry branch accepted only SELECT, where the first compile accepts SELECT or EXPLAIN. An EXPLAIN whose view or table changed between the request's compile and its cursor open got HTTP 400 "/exp endpoint only accepts SELECT", and nothing closed the recompiled plan. The retry branch now accepts EXPLAIN too. The two DECLARE limits reported an offset into the stored view text when they tripped inside a view body, so the position could lie past the end of the statement. parseDeclaredQuery() now reports the statement's read of the outermost view for a sub-query declared in a view body, and the declared-variable visitor does the same while it reads a view body. A sub-query the statement declared keeps its own position, even when a view body reads it. SqlParser also moves twelve private methods to their alphabetical slots, with no change to their text. New tests: - ExpParquetExportTest: an EXPLAIN on /exp recompiles after a view or a table changes before the cursor opens. - UpdateTest, IODispatcherTest, QwpEgressDdlExecTest: a failed or retried UPDATE ... FROM closes its plan over each entry point. - DeclareTest: a declared sub-query read below a node with more than two operands, a declared list in both window frame bounds, and the position of both limits inside view bodies. - PreparedStatementInvalidationTest: a cached INSERT ... SELECT whose plan goes stale after the copy started a row, for a WAL and a non-WAL target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code review (level 3, round 8)Reviewed
Verdict: approve with comments. No Critical findings. Two Moderate and two Minor are new in this round. Moderate1. A statement refused inside a CTE re-read leaves lexer stash frames in the pooled compiler
Trigger, with
Suggested fix: call 2. One rule of the copy-limit error position has no test
Suggested fix: add that statement to 3. Round 6 items, still openBoth reproduce unchanged at this head; round 6 has the detail.
Minor
Adjacent findings (already on
|
| Fix | Reverted to | Tests that fail |
|---|---|---|
The dispatcher closes a failed UPDATE |
closeOnDone && isDone |
the four new UpdateTest methods, two in IODispatcherTest and two in QwpEgressDdlExecTest, on NATIVE_JOIN_MAP 16,512 or NATIVE_JIT 2 |
The catch dropped from JsonQueryProcessor.executeUpdate |
same hunk | the existing IODispatcherTest#testExecuteAndCancelSqlCommandsOnQueryEndpoint, on NATIVE_JOIN_MAP 99,072 |
/exp retries a stale EXPLAIN |
SELECT only |
both new ExpParquetExportTest methods, with HTTP 400 /exp endpoint only accepts SELECT |
Positions of the two DECLARE limits |
query.position, node.position |
testDeclareVariableAsSubQueryReadTooManyTimesInsideViews (expected:<159> but was:<15>), testDeclaredVariablesExpandToTooManyNodesInsideViews (expected:<44> but was:<35027>) |
| Round 7's surviving mutants | skip the args walk; skip a frame bound; always retry |
testDeclareVariableAsSubQueryReadInFunctionArguments, testDeclaredListInWindowFrameBounds, testInsertSelectInTransactionAfterPlanWentStaleMidCopy_wal |
- The dropped catch, out-of-memory half: no test reaches an out-of-memory
UPDATEover/execon either revision. A probe shows head and base both free the plan there, and the close now sits in afinallythat does not look at the exception. I accept that gap. - Stability: the four
UpdateTestmethods passed 15 of 15 runs and the eight network methods 8 of 8. - Hooks: each hooked test asserts that its hook fired (
writerGetCount == 2,requestCompileCount == 2) and restores the listener in afinally. - CLAUDE.md test rules: they hold on the added lines. There is no
returnsOnce,getPlan, sleep or@Ignore, and the oneassertSqlis theTestServerMainwrapper. - "Moves twelve private methods ... with no change to their text": it holds. A member-by-member comparison of
fa547b7c39and this head finds two members with changed text, the two error positions.
Summary
- Verdict: approve with comments. Please address findings 1 and 2 here, along with 3a and 3b from round 6. None blocks the merge. The red Enterprise run still needs an explanation first.
- Severity: 0 Critical, 4 Moderate (2 new), 5 Minor (2 new). All are in the diff; this round admitted no out-of-diff breakage.
- Submodules: none moved.
- Tradeoff confirmed, "A failed
UPDATEno longer keeps its operation":- Calling
execute(null)again on the sameCompiledQueryafter a busy-writer failure throwsNullPointerExceptionatUpdateOperatorImpl.java:135on head, where base applied the update. - Base already throws the same exception on a second
execute()after a successful one. - No caller in
core,benchmarks,compat,utilsor the documentation does that. PGWire executes a cachedUPDATEmore than once, but it passescloseOnDoneasfalseand is unchanged. - On "Enterprise callers were not checked": a local checkout of the paired Enterprise branch, with its
questdbsubmodule at this head, has no main-code caller ofCompiledQuery.execute(), and its test callers execute each compiled statement once. - The description could say so and name the exception.
- Calling
- Areas checked with no finding:
- Every exit of
OperationDispatcher.execute()for anUPDATE, overCairoEngine.execute(),/execwith its async-wait continuation, and QWP egress: sync success and failure, a busy writer with and without a sequence, async success, timeout and writer-side failure, a cancelled statement and an out-of-memory one. Head closes the plan once on each, apart from A1. /expwith a staleEXPLAINin CSV and Parquet, andEXPLAIN UPDATE,INSERTandCREATE TABLE ASover a redefined view. AnEXPLAINplan never enters the select cache.- The positions of both limits through
SELECT,CREATE,ALTERandCOMPILE VIEW,INSERT ... SELECT, CTAS,EXPLAINandUPDATE: each lands on the view read or on the reference in the statement's own text. - The rest of the diff, with every statement run on both revisions:
- the set-operation lift in 81 shapes;
- a bracket after a declared value in 474 combinations;
- WITH scope in 170 statements;
- declared lists against the written-out list in 768 pairs;
- about 52,000 token-mutated
DECLAREstatements, with a canary query after each. - Every difference was a fix or a change the description documents, and head showed no internal error that base lacks, apart from finding 3a.
- Pooled parser state after 57 failing statements, table-function ownership in 52 statement shapes (one close each on head), and 22 multi-statement batches.
- Every exit of
🤖 Generated with Claude Code
This commit answers the round 8 review of the PR. SqlParser.parseWith() stashes the lexer's read-ahead before it parses a CTE body again and restored it only when that parse returned. The parse budget and the declared sub-query limit can throw there, and each refused statement then left its frames in the pooled compiler's lexer for good. parseWith() now pushes the frame ahead of the try and pops it in the finally. GenericLexer gains a @testonly getStashSize() so WithClauseTest can read the stash after a refusal. ExpressionParser refuses malformed CASE expressions with a positioned SqlException where it hit an AssertionError or a NullPointerException. The bracket rule for declared values keeps a bracket inside an open CASE in the value, which exposed the CASE handling to brackets that cut across it: - END, WHEN, THEN and ELSE throw "unbalanced 'case'" at the CASE keyword when the flush finds no case node or a bracket's scope sits on top of the scope stack; - a CASE slot that delivers no value, such as an empty bracket, throws "missing arguments" at the keyword that follows it; - the member access on a bracket, (expr).name, no longer counts an operand the listener never received, so a dangling operator after it fails the arity check instead of taking a neighbour's operand. SqlOptimiser.bubbleUpOrderByAndLimitFromUnion() gives the two models it wraps around a set operation the position of the set operation's first model. A time-series join over a set operation with a trailing LIMIT reported "TIMESTAMP column is required but not provided" at position 0; it now points at the bracketed sub-query. The OperationDispatcher comment no longer promises that of() frees an UPDATE's plan on a full writer command queue. Tests: - WithClauseTest asserts an empty lexer stash after a statement refused inside a CTE re-read, once per limit; - DeclareTest and ExpressionParserTest pin each malformed CASE shape with its position, next to a well-formed sibling that still parses; - DeclareTest pins the copy limit's position for a statement-declared sub-query whose last copy a view body asks for; - UnionTest covers the set-operation lift under ASOF, LT and SPLICE JOIN, JOIN LATERAL, RIGHT and FULL JOIN, with LIMIT alone and ORDER BY alone, the refused ORDER BY on an alias of the last branch, and the error position of a lifted set operation without a timestamp; - ten long SQL literals in UnionTest become text blocks, ConcurrentViewCycleTest renames writersDone to isWritersDone, and IODispatcherTest moves one import into order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code review (level 3, round 9)Reviewed
Verdict: approve. There are no Critical, Moderate or Minor findings. Two bugs that already exist on What
|
| Fix | Reverted to | Tests that fail |
|---|---|---|
END refuses an empty slot |
check removed | 3 ExpressionParserTest and 3 DeclareTest methods (expected exception, NPE) |
WHEN/THEN/ELSE refuse an empty slot |
check removed | 5 methods (NPE in rewriteCase, positions) |
END scope check |
the old assert |
3 methods (Should have popped CASE, but got PAREN or BRACKET) |
WHEN/THEN/ELSE scope check |
push without the check | 6 methods (opStack is empty at ']', positions) |
(expr).name arity |
argStackDepth++ |
testDotDereferenceEmptyBrace, testDotDereferenceDanglingOperator, testDeclareVariableWithMalformedCaseMemberAccess |
parseWith() pops its frame on a throw |
stash/unstash inside the try |
both new WithClauseTest methods (expected:<0> but was:<6>, <48>) |
| Wrapper positions | both lines removed | testUnionNestedInTimeSeriesJoinWithLimitHasNoTimestamp (expected:<38> but was:<0>) |
| Round 8 #2, the copy-limit position rule | viewsBeingCompiled.size() > 0 |
testDeclareVariableAsSubQueryReadTooManyTimesByViewBody (expected:<40> but was:<3034>) |
| Round 8 #3b, the lift in joins | the join-model loop skipped | all six new UnionTest methods |
- CLAUDE.md test rules: they hold on the added lines. There is no
assertSql,getPlan,returnsOnce,@Ignoreor reflection.WithClauseTestreads the lexer through aSqlCompilerImplsubclass. The field is alreadyprotectedonmaster.
Adjacent findings (already on master, not blocking this PR)
Each reproduces identically on 77c6cf2d6b and 455f6dabec.
A1. A simple CASE whose operand is a call, a cast, a bracket or another CASE runs as a searched CASE
- Net impact:
- With a BOOLEAN operand, the query silently returns wrong rows.
- With any other operand type, it fails with a misleading error.
- Location:
ExpressionParser.java:2045, theargCount == 0test at the firstWHEN(:1989onmaster). - Symptom:
- With
t(i INT, b BOOLEAN)holding(1, false),(2, true),(3, true):SELECT b, CASE (b) WHEN true THEN 'x' ELSE 'y' END FROM treturnsxfor all three rows; row 1 should bey.CASE CASE WHEN b THEN true ELSE false END WHEN true THEN 'x' ELSE 'y' ENDreturnsxfor all three rows as well.EXPLAINshows the expression folded to a constant, for example'y'forWHEN false.
- With other types, valid SQL fails with
BOOLEAN expected, found INTat the firstWHENvalue:CASE abs(i) WHEN 1 THEN ...CASE coalesce(n, 0) WHEN 0 THEN ...CASE cast(i AS LONG) WHEN 1 THEN ...CASE (i) WHEN 1 THEN ...
CASE i WHEN ...,CASE i + 0 WHEN ...andCASE i::INT WHEN ...work.
- With
- Reachability: any query or view with such an operand. Standard SQL allows any value expression as the operand, and PostgreSQL accepts these spellings.
- Cause: the first
WHENdecides between a simple and a searchedCASEbyargCount, the number of operators it pops. An operand that ends in)orENDhas already left the operator stack, so the count is 0. The parser then takes it forCASE WHEN, adds the searched-case marker, and leaves the operand behind. - Suggested fix: test
argStackDepth == 0there instead ofargCount == 0. A probe with that one-line change returns the right rows for every shape above.CaseFunctionFactoryTest,SwitchFunctionFactoryTest,CaseCommonTest,NullCaseFunctionTest,ExpressionParserTestandDeclareTestpass, 653 of 654. The exception isExpressionParserTest.testCaseWithCast, which pins the current RPN forcase (cast(1 as int)) when 1 ..., searched-case marker included, and needs its expectation updated. - Severity if filed standalone: Critical (silent wrong results for a supported query shape).
A2. <column> AT TIME ZONE '<tz>' fails with too few arguments for 'to_timezone'
- Net impact: valid SQL refused with an arity error; workarounds exist.
- Location:
ExpressionParser.java:1623(theAT TIME ZONEbranch) and:1746(the zone constant). - Symptom:
SELECT ts AT TIME ZONE 'Europe/Berlin' FROM tfails with[23] too few arguments for 'to_timezone' [found=1,expected=2]. InWHEREit fails the same way.(ts) AT TIME ZONE ...,ts::TIMESTAMP AT TIME ZONE ...andto_timezone(ts, ...)return the right values.TimestampAtTimeZoneTestcovers only a cast operand.
- Reachability: any query that applies
AT TIME ZONEto a bare column. - Cause: a bare column is still on the operator stack when the zone constant is counted and
to_timezoneis pushed above it. The flush then reachesto_timezonewith only the zone counted. A bracket or a cast has already flushed its operand by then, so those spellings work. - Suggested fix: flush a literal operand to the output when
AT TIME ZONEfollows it, before the zone constant is counted. - Severity if filed standalone: Moderate.
I can file either of these as an issue.
Summary
- Verdict: approve. The red
enterprise-ci-lightrun at this head needs an explanation before merge. - Severity: no Critical, Moderate or Minor findings, so no in-diff or out-of-diff breakage was admitted.
- Submodules: none moved.
- Areas checked with no finding:
- every hunk of
455f6dabec, as described above; - a fresh pass over the whole production diff. In each of these areas head matched base or a documented fix, or turned a base internal error into a positioned one:
EXPLAINthroughgenerateSelectOneShot();- declared lists against the written-out lists;
- the lexical WITH scope across
INSERT,UPDATE ... FROM, CTAS,CREATE/ALTER VIEW,COPYandPIVOT; - the set-operation lift under
ASOF,LT,SPLICE,WINDOWandHORIZONjoins;
- the differential fuzz runs:
- every head-vs-base difference was a change the description documents, or an internal error on base that head turns into a positioned error;
- for every declared, CTE and view statement that compiled, head matched its own inline-expanded form, 122 view pairs included;
- no internal error appeared on head that the base spelling of the same statement does not also reach.
- every hunk of
- Tradeoff confirmed: the
DECLAREnode cap does not grow with the statement.MAX_DECLARED_VARIABLE_NODESis 100,000 whatever the statement's length, while the parse budget grows with it. So a long statement that reuses a large declared value fails on head where base runs it.- Trigger, on
twith 1,000 rows:DECLARE @region := <CASE v with 1,000 branches> SELECT sum(CASE WHEN @region = 'r0' THEN amt END) c0, ..., c49 FROM t, 22,165 characters.- Head fails with
[22125] declared variables expand to too many expression nodes [max=100000]. - Base returns the 50 sums.
- With 49 references, both revisions return the same rows.
- Head fails with
- Reading the value once in a sub-query,
... FROM (SELECT @region r, amt FROM t), works on head. - The description documents the limit and its reason: the copies stay in the compiler's pool. So this is not a finding. "Limits and costs" sizes only the parse budget, though, and could give an example like this one.
🤖 Generated with Claude Code
…o audited-views-with-in-lists
|
/azp run macwin |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
Bring in HORIZON JOIN right-hand side filtering (#7688), authenticated browser WebSockets for QWP (#7683) and the QWP fixes for row counts on parse-time statements and sender close() timeouts (#7770), including the java-questdb-client bump to c41a4172. QwpEgressDdlExecTest conflicted on imports only: the branch added SqlExecutionContext and Path for the stale view plan tests, while master added Os for the Windows guard. The resolution keeps all three. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[PR Coverage check]😍 pass : 968 / 996 (97.19%) file detail
|
|
Adds
AUDITEDtoDECLARE, so a view can record what a read of it resolved its parameters to, and lets a declared variable hold the list anINfilter tests against. The audit trail itself lives in Enterprise (questdb/questdb-enterprise#1203); this PR is the parser and view-definition half. User documentation: questdb/documentation#561, a draft until both PRs merge.Reviews of the branch also found parser, optimiser, PGWire and HTTP bugs, most of them older than the branch. Other fixes lists them, and Compatibility and tradeoffs lists what they change for existing statements and stored views.
Audited views
AUDITEDmarks a variable whose resolved value a read of an audited view records. It is independent ofOVERRIDABLE, and the two may appear in either order:OVERRIDABLEsays whether a caller may set the variable,AUDITEDwhether a read records what it resolved to. A fixed default such asnow() - 1hstill resolves differently on every execution. The parser accepts the marker only in theDECLAREblock that opens a statement or a view body. In a sub-query, a CTE or a later set-operation branch it fails withAUDITED is only allowed in the top-level DECLARE block, because no read would record it there.ViewAuditModelper reference site: the view's name and id, and eachAUDITEDparameter with the unevaluated expression it resolved to, the caller's override or the view's default. It reads the names off the view's model, so a caller cannot add, drop or rename an audited parameter. The parser keeps sites separate, because two sites bound to$1and$2can agree on one execution of a cached plan and differ on the next; Enterprise merges rows per execution.AUDITEDevery parameter the inner view declaresOVERRIDABLE AUDITED. A read of the outer view is then one read. A view that is not audited never shadows.parse()attaches the records to whichever query model the statement carries:SELECT,INSERT ... SELECT,CREATE TABLE AS SELECT,UPDATE(SqlOptimiser.optimiseUpdatehands them to the model code generation reads) andEXPLAIN(now generated throughgenerateSelectOneShot(), becauseExplainPlanFactoryopens the plan's cursor and prints values its sub-queries read). APIVOT ... IN (SELECT ...)sub-query runs while the statement compiles, so its model gets the audits of the audited views its plan reads.ViewDefinitioncarries anauditedflag, written to_viewas a second block only when set, so a view nobody audits writes the file it wrote before. An older build skips the unknown block and still loads the view. If that build rewrites the definition (a recompile, anALTER), it drops the block, and rolling forward reads the view as not audited.SqlExecutionContext.isBackgroundJob()is true forMatViewRefreshSqlExecutionContext,LiveViewRefreshSqlExecutionContext,WalApplySqlExecutionContextandViewCompilerExecutionContext, and Enterprise records nothing under them.executeCreateMatViewandalterViewExecutionraiseisMetadataProbe()around their validation cursors, asexecuteCreateViewalready did; a check that fails still records anerrorrow. Two limitations follow, accepted in review: a materialized view over an audited view serves that view's data with no audit row, and anUPDATEof a WAL table that reads an audited view records nothing, because its read happens in WAL apply.SecurityContext.authorizeAuditView(refuses by default);CairoEngine.checkTableDroppable, consulted byDROP TABLE,DROP ALL TABLESand a text import withoverwrite=true;checkTableSuspendableandcheckTableResumableFromTxnforSUSPEND WALandRESUME WAL FROM TXN;SqlCompilerImpl.getFunctionParserandvalidateAuditedViewBody, whichALTER VIEWandCREATE OR REPLACE VIEWcall with the new body before its check cursor opens.StaleViewCheckFactorynow also checks ingetPageFrameCursor(), which Parquet export and QWP egress use. Onmasterthose kept serving a view's old body after it was dropped and re-created.Utf8Sink.escapeJsonStr(Utf8Sequence)escapes NUL as\u0000instead of failing an assertion or writing invalid JSON. HTTP/execwritesVARCHARvalues through the same method.Declared IN lists
A parenthesised list was not an expression before:
SELECT ('a','b')returnedb, andDECLARE @x := (1, 2, 3)failed withbracket lists are not supported. The parser now splices a declared list intoINat parse time and produces the node a written-outIN (a, b, c)produces. Every existingINoverload therefore applies unchanged, each member keeps its own type, bind variables of any type work as members, and a declared TIMESTAMP list still plans as an interval scan. A list anywhere else fails withdeclared list can only be used on the right-hand side of IN.A lookahead for a separator decides whether brackets hold a list. It reads the text with a second lexer borrowed from the view-lexer pool, so it treats comments, quotes and hints as the parse does, and a bracketed
SELECT,WITHorDECLAREstays a sub-query. Errors about a misused list report the declaration's position, as any substituted variable does. A caller's list override of a variable that is not overridable reportsvariable is not overridableat its:=token, as a scalar override does.Other fixes
Each bullet says where the bug came from: master (reproduced there or the same code there) or branch.
DECLAREDECLARE @x := (SELECT ... ORDER BY ... LIMIT 1) SELECT * FROM @xlost its sort,s IN @x OR s IN @xreturned every row, andFROM @x WHERE s IN @xthrew aNullPointerException. The first read takes the model the declaration parsed; later reads parse a copy from the declaration's text with the declarations and views the original saw. Reads in window clauses count too.DECLARE @f := (1 = 1) ... WHERE NOT @f OR @freturned 0 rows, because foldingNOTrewrote the shared node. Each read also copies the windows in the value, which fixesSELECT x, @w a, @w bfailing withDuplicate columnand a top-1 sub-query in a shared window losing its sort.(1 + 2) * (3),NOT (1 = 1),-(1),abs(1 + (2)),2 BETWEEN (1) AND (3)andcast((1) AS LONG)now parse as values, as do values ending in an array cast or a trailing decimal point before a bracketed statement.DECLAREvariables of its definition (master for plain CTEs).DECLARE @x := 1 WITH c AS (SELECT @x v) SELECT * FROM (DECLARE @x := 2 SELECT * FROM c UNION ALL SELECT * FROM c)returned1, 2and now returns1, 1.auditedoroverridableright after aDECLAREblock are tables again, not markers (master foroverridable).circular view reference detected(branch), and a declared sub-query read twice in a nestedSELECTwithoutFROMparses again (branch).',' expected),,,or a leading comma (missing declaration), an unquoted undeclared variable in a block's first declaration, a list member with a dangling operand, and an empty quoted identifier.(SELECT 1) = (2, 3), a comment glued toDECLARE, a sub-query as a cast type, and a declared sub-query read inTIME ZONE,WITH OFFSET,SAMPLE BY,FILL,LATEST BY,LATEST ON, aWINDOW JOINbound or a joinTOLERANCE(query is not allowed here).variable name expected after AUDITEDpoints at the token in the variable's place.CASEin a declared value fails with a positioned error (branch for declared values; the plainSELECTspellings fail the same way on master). The bracket rule above keeps a bracket inside an openCASEin the value, which exposed theCASEhandling ofExpressionParserto brackets that cut across it:DECLARE @x := CASE (WHEN v > 1 THEN 1 ELSE 2 END) SELECT @x FROM thit anAssertionErrorandCASE WHEN (v WHEN 1) THEN (1) ELSE (2) ENDaNullPointerException. AWHEN,THEN,ELSEorENDthat finds a bracket still open, or noCASEto close, now fails withunbalanced 'case'at theCASEkeyword. A slot that delivers no value, as inCASE WHEN () THEN () ELSE 2 END, fails withmissing argumentsat the keyword after it. A member access on a bracket,(expr).name, no longer counts an operand it never delivered, so().xand(v).x +fail withtoo few arguments;SELECT ().x FROM tthrew aNullPointerExceptiononmaster.CTEs and views
... WHERE symbol IN (SELECT symbol FROM allowed), a caller'sWITH allowed AS (...)replaced the table the view reads. An expression sub-query now sees every WITH around it, as a FROM clause at the same place does. A nested WITH may reuse an enclosing CTE's name, and a CTE re-read binds names as its definition did, so it never sees later CTEs or itself.cairo.sql.column.alias.expression.enabled=false). CTE re-reads, view expansions and declared copies continued a statement-widecolumnNcounter, soWITH w AS (SELECT 1.5, 2.5) SELECT column2 FROM w UNION ALL SELECT column2 FROM wreturned2.5, 1.5. A re-read now numbers from its definition's start and a view expansion from 1.ViewCompilerJobretries (master). A table re-created during the job's compile left the view and its dependents INVALID. The job now compiles again, up tocairo.sql.max.recompile.attempts+ 1 times.Set operations
ORDER BY/LIMITapplies to the whole set operation wherever it is read (master).bubbleUpOrderByAndLimitFromUnion()lifted the clause above the set operation only where the set operation was read at top level, in aFROMsub-query or in the first two branches of another set operation. In a third or later branch and in any JOIN it stayed on the last branch. Withtholding 1, 2, 3,SELECT * FROM t t1 JOIN (SELECT v FROM t UNION ALL SELECT v FROM t ORDER BY v DESC LIMIT 1) t2 ON t1.v = t2.vreturned four rows and now returns(3, 3). The positional spelling,ORDER BY 1, failed there withInvalid column: 1and now resolves.ASOF,LTandSPLICE JOIN (<set operation> ORDER BY ts)failed withright side of time series join has no timestamp, or with anArrayIndexOutOfBoundsExceptionunless the outer query also selected the right-hand timestamp, and now run. A bracketed branch,UNION ALL (SELECT ... ORDER BY ... LIMIT ...), keeps its own clause.ts1 a ASOF JOIN (SELECT v, ts FROM ts2 UNION ALL SELECT v + 1, ts FROM ts2 LIMIT 3) bfailed with[0] TIMESTAMP column is required but not provided. The wrappers now carry the position of the set operation's first model and the error points at the bracketed sub-query,[38], as it does for the hand-writtenSELECT * FROM (<set operation>) LIMIT 3.masterreported[27] right side of time series join has no timestampfor this statement, so the message still differs.queries have different number of columns, where they hit anAssertionErroror returned wrong rows (master).Compile-time memory
mastera 22-character read of a nine-level view chain ran a 1.5 GB heap out of memory.SqlParser.checkParseBudget()runs before every CTE re-parse, view expansion and declared copy, and fails withstatement is too complex to parse [models=<n>, max=<m>]once the parse holds more than 1,000 + 2C query models, 10,000 + 20C expression nodes, 5,000 + 10C query columns or 250,000 + 500C characters, where C is the statement's length plus each expanded view body.DECLARElimits (branch). A statement may read declared sub-queries at most 100 times and expand declared variables to at most 100,000 expression nodes. A 418-character chain of@v1 := @v0 + @v0, ...retained 1.5 GiB in a pooled compiler; it now fails having retained about 23 MB. A parse also reuses a lexer that an earlier parse of the same text finished with. Inside a view body both limits report the statement's read of the outermost view, as the parse budget does; they reported an offset into the stored view text,[19997]for a 54-character statement. A sub-query the statement itself declared keeps its own position, even when a view body reads it.parseWith()stashes what the lexer has read ahead before it parses a CTE body again, and restored it only when that parse returned. Each statement refused inside a re-read left its frames in the pooled compiler's lexer for the life of the process: 6 entries for a statement past the declared sub-query limit, 48 for a 12-level CTE chain past the parse budget.parseWith()now pops the frame in afinally.read_parquet()in an unused CTE or unselected column leaked 256 bytes per compile, on success and on failure.freeUnclaimedTableFactories()closes every factory still attached to its model after generation.StackOverflowError(master).SqlParser.clear()now emptiesExpressionParser's stacks, so the next statements on that pooled compiler no longer fail or parse wrongly.Retries and stale plans
INsub-query retry (master). When a table changed mid-compile, the retry re-parsed the borrowed pooled compiler's last statement, from any connection, and the PIVOT took another statement's columns. The PIVOT now usesSqlCompiler.generateSelectWithoutRetries(), soSqlCompiler.compile()throwsTableReferenceOutOfDateExceptionand the caller that holds the statement's text decides what happens next.CairoEngine.execute()andViewCompilerJobcompile the statement again, as doesCREATE TABLE AS SELECTfor theSELECTit compiles when it executes. PGWire, in simple and extended mode, does not retry a compile: it reportscached query plan cannot be used because table schema has changed [table=...], and the client has to send the statement again.INSERT ... SELECTwrote statement 1's rows.compileBatch()now opens the lexer at the current statement.INSERTplan failed as stale,PGPipelineEntry.msgExecuteInsert()released the plan's writer, which could be the one an earlierINSERTin the transaction parked, and rolled its rows back while every statement reported success. It now leaves a parked writer parked onTableReferenceOutOfDateException. This branch makes the trigger more likely, since redefining a view makes plans over it stale./expretries a staleEXPLAIN(master; a redefined view as the trigger is branch). When a table or view read by anEXPLAINchanged between the request's compile and its cursor open,/expanswered HTTP 400/exp endpoint only accepts SELECTand left the recompiled plan open. The retry branch ofExportQueryProcessornow acceptsEXPLAIN, as the first compile does, and the request returns the new plan.UPDATEcloses its plan (master; a redefined view as the trigger is branch).OperationDispatcher.execute()closed the operation only when the statement completed, so anUPDATEon a non-WAL table that reads another table or a view and failed at execution kept its plan, 16,512 bytes ofNATIVE_JOIN_MAPper statement forUPDATE ... FROM. The triggers are a view redefined during the statement (over HTTP/exec,CairoEngine.execute()and QWP egress),cannot update parquet-format partition, and a busy writer when the caller passes no event sequence to wait on. The dispatcher now closes the operation on every exit unless a future took it. PGWire already closed it.Compatibility and tradeoffs
Statements and stored views that change behaviour:
ORDER BY. A trailingORDER BYin a join's right-hand side or a later branch now resolves against the set operation's result columns. A name only the last branch defines (an alias,t2.col, a column that branch does not select) compiled there and sorted the last branch; it now fails withInvalid columnorInvalid table name or alias, as the same set operation always did at top level. AnASOF/LTright-hand side of the form((<set operation> ORDER BY ...) TIMESTAMP(ts))whoseORDER BYis not the ascending timestamp gets the error a single-table right-hand side gets. The view compiler marks stored views of these shapes INVALID.ORDER BYwithoutLIMITnow sorts the whole set operation, not its last branch. Where the order is observable, the old order was wrong. Where nothing observes it, such as an aggregate over the join, the rows are the same and the statement pays one materialising sort per execution. The cost grows with the rows outside the last branch, whichmasternever sorted, and the sorted copy stays in native memory while the cursor is open. Review probes over a set operation with a 2M-row first branch and a 1k-row last branch (one compiled plan, minimum of 7 cursor reads): a hash join undersum()withORDER BYa randomLONGtook 329 to 347 ms against 76 to 78 ms, a cross join undersum()159 to 167 ms against 26 to 30 ms, a full read of a third branch 143 to 165 ms against 22 ms, and a hash join withORDER BY ts120 ms against 98 ms. With the large branch last, whichmasteralready sorted, it took 346 to 347 ms against 419 to 437 ms. The sort also turnsHash JoinintoHash Join Light, which can change the output order of an inner join. With a designated-timestampORDER BY, a join's right-hand side sorts fully where the top level usesUnion All Merge, as the hand-writtenSELECT * FROM (<set operation>) ORDER BY tsalready did.LATERALbodies. A correlated set operation with a trailingLIMITreturned wrong rows and now returns the right ones.SELECT *over it exposes the internal__lateral_rncolumn, and a positionalORDER BY 1fails withInvalid column: 1. Both already happen onmasterfor the hand-writtenLATERAL (SELECT * FROM (...) ORDER BY ... LIMIT ...)and for a body without a set operation; LATERAL sub-queries return wrong rows, expose internal columns or fail with internal errors: joined correlated sub-queries, LIMIT, window functions, scalar aggregates #7803 tracks them (section 9 and the section 17 comment).EXPLAINplan over a view carriesStaleViewCheckFactory, so one compiled before the view changed throwsTableReferenceOutOfDateException; PGWire,/execand/exprecompile on it.COPY (SELECT ... FROM view) TO ... PARQUETdoes not recompile: with the view redefined between the statement and the export job, the export ends asfailedinsys.copy_export_log, wheremasterexported the old body.DECLARElimit, when they useDECLAREsyntax now refused, or when they carry a nestedAUDITEDmarker (only an earlier build of this branch could store one). A view whose expression sub-query has a nested CTE sharing a table's name, or that reads a CTE twice under aDECLAREthat does not enclose its definition, returns different rows.column3of a self-join, fail withInvalid column; stored views that read a view twice or after constants get new metadata names; a materialized view writes corrected values only into rows refreshed after the upgrade, andREFRESH MATERIALIZED VIEW ... FULLmakes it consistent.DECLARE AUDITEDis refused. Commented-out text glued toDECLAREno longer declares anything (DECLARE--@w := 3,\n@x := 1 SELECT @wdeclared@wonmaster). Some diagnostics on invalid input changed position or wording: aCASEslot left empty by a bracket reportsmissing argumentswhere a later check reported'then' expectedor'when' expectedat the same position, and other malformedCASEspellings that reached an unrelated error now reportunbalanced 'case'. Two spellings that compiled by accident are refused:CASE -(x).y WHEN ...(aNullPointerExceptionor a type error onmaster) anda BETWEEN (x).y IS NULL AND c(a wrong result onmaster).arr[ : (expr).name]compiles, asarr[ : 2]does.TIMESTAMP column is required but not providederror now carries an offset into the view's text, 215 for a 51-character statement in one probe, where it carried 0. Other code-generator errors raised from a view body already do this onmaster, the hand-written spelling of the same body included; the parse budget and theDECLARElimits are the only errors remapped to the statement's read of the view. HTTP and PGWire pass the position through unchanged.INsub-query reads an audited view carries the audit on two plans, so Enterprise writes two rows for it until it decides whether the reads share a row.UPDATEno longer keeps its operation.CompiledQuery.execute()withcloseOnDone, which the one-argument overload always passes, closes the operation when execution throws, not only when it completes. A caller that kept theCompiledQueryand executed it again after a busy-writer failure applied the update onmaster; it now gets aNullPointerExceptionfromUpdateOperatorImpland has to compile the statement again.masteralready throws the same exception on a secondexecute()after a successful one. Every caller in this repository already compiles again or gives up, and PGWire, which executes a cachedUPDATEmore than once, passescloseOnDoneasfalseand is unchanged. The paired Enterprise branch has no main-code caller ofCompiledQuery.execute(), and its test callers execute each compiled statement once.Limits and costs:
DECLARElimits are constants, not configuration. The budget refuses CTE and view doubling chains of nine or more levels, and a short statement that reads a 10-mart KPI view (383 models a read) about 17 or more times; a 1,419-character statement that reads it 20 times uses 98% of the model part. No test statement outside the budget's own tests uses more than 21% of any part. The budget bounds parse memory only: the optimiser and code generator still take several times more from never-shrinking pools, as onmaster.-Xmx1gone WITH list runs out of memory at about 5,250 CTEs rather than 7,700.ViewCompilerJob's retry loop wraps code generation's own, so one task can compile up to 121 times with the default of 10 attempts, asCREATE TABLE AS SELECTcan.Left open, the same on
master:COMPILE VIEWover a view that reads a table function leaks its factory (#7713, larger here when the body reads one declared sub-query several times); a declared sub-query as a whole window key fails with an internal exception (#7712); two expressions that differ only in the sub-query they read are de-duplicated as one (#7711).Left open, a regression for declared values: a bracket in a declared value that holds
x BETWEEN .5 AND 2followed by,or], and().count(*)in aCASEslot, end in an internal error (AssertionError: opStack is empty at ',', aNullPointerExceptionwith assertions off), as the plainSELECTspellings do onmaster. Before the bracket rule above, such a value failed earlier withunbalanced 'case'. The cause sits outside theCASEhandling: a leading-dot literal afterBETWEENglues onto thebetweennode, so theANDflushes the whole operator stack. The same cause makesSELECT v BETWEEN .5 AND 2 FROM treturn a column of2onmasterand here. Targeted fuzzing reaches these shapes; a general statement fuzz met them 7 times in 1,152,871 statements.Cost for queries that read no audited view
A statement that reads no audited view carries no audit record, so Enterprise builds the plan it built before, and nothing changes per cursor or per row. The compile path gains constant work: a few cleared collections, one flag check per view reference, a parse budget check per CTE re-parse, view expansion and declared copy, and a lookup of the definition per CTE re-read.
QueryCompileBenchmarkcompiles five queries that use no syntax this branch adds. It ran here and on the merge basef9d4f5138: 6 forks of 10 one-second iterations per side, ABBA order, 4 pinned cores.masterDECLAREDECLAREoverrideEvery interval includes zero and allocation is unchanged. The override row leans slower in every comparison (+0.7% [-0.4%, +1.9%] over 16 forks) and is left open at a microsecond at most. The numbers predate the
DECLAREand follow-up fixes and were not re-run; those add a copy of the declared value per reference and the checks listed above.Tests
OSS tests exercise what OSS owns. The audit rows, the
WITH AUDITclause and its permission are Enterprise, so the tests that need an audited view set the flag throughViewGraph.DeclareTest,DeclareListFuzzTest- declared lists in both spellings, NULL and bind-variable members, the lookahead's comment and quote handling;AUDITEDparsing and every refused nested marker; eachDECLAREfix above; the parse budget and the 100-copy limit. The fuzz test compares a spliced list with the written-out list over 19 column types.ViewAuditTest- which statements record a read and with which parameters, shadowing,UPDATEand PIVOTINrecords,isBackgroundJob()for each job context (including WAL apply, through anUPDATEof a WAL table that reads an audited view), the_viewflag round trip, and the OSS call sites of the Enterprise hooks.WithClauseTest,ViewQueryTest- WITH scoping in and around view bodies, shadowing, each budget part with its position and limit, legacy-mode names, and the error position of a list override.UnionTest- a set operation with a trailingORDER BY/LIMITin a later or middle branch, in a first or second JOIN, through a CTE, under outerUNION/EXCEPT/INTERSECT, with its plan, and the bracketed-branch control.PivotTest,SqlCompilerImplTest,PGMultiStatementMessageTest,ViewCompilerJobTest- hooks change a table at a fixed point of the compile, with no sleeps, for the PIVOTINrace, batch retries and view compiles.TableFunctionOwnershipTest,ExpressionParserTest,SqlParserTest- one close per table-function factory on every path; parser state after a stack overflow; positioned errors.ConcurrentViewCycleTest- theEXPLAINreader recompiles on a stale plan without a limit while theALTER VIEWthreads run and with the configured limit once they stop. The limit alone failed 21 to 23 of 500 runs.DropTableVetoTest,WalApplyVetoTest,Utf8StringSinkTest- the engine vetoes and the NUL escape.ExpParquetExportTest,UpdateTest,IODispatcherTest,QwpEgressDdlExecTest- hooks redefine a view or alter a table between a statement's compile and its execution, with no sleeps: the/expEXPLAINretry, and the plan of a retried or failedUPDATE ... FROMoverCairoEngine.execute(),/execand QWP egress (throughQwpQueryClient), which the native-memory leak check observes.PreparedStatementInvalidationTest- a cachedINSERT ... SELECTwhose plan goes stale after the copy started a row, inside a transaction, for a WAL and a non-WAL target. The non-WAL case pins what the server does today: the statement reports success and theCOMMITfails on a distressed writer.DeclareTestalso covers a declared sub-query read below a node with more than two operands, a declared list in both window frame bounds, and the position of bothDECLARElimits inside view bodies, including a statement-declared sub-query whose last allowed copy a view body asks for.DeclareTest,ExpressionParserTest- malformedCASEexpressions with their positions, at expression level and through a compiled statement: a bracket around or ahead of a keyword, a keyword inside a subscript, a cast or a call, an empty bracket in each slot, and a member access on an empty bracket or ahead of a dangling operator, each next to a well-formed sibling that still parses.WithClauseTestalso reads the lexer's stash, through a@TestOnlyaccessor, after a statement refused inside a CTE re-read, once per limit.UnionTestalso covers the lift underASOF,LTandSPLICE JOIN,JOIN LATERAL,RIGHTandFULL JOIN, withLIMITalone andORDER BYalone, the refusedORDER BYon an alias of the last branch, and the error position of a lifted set operation that has no timestamp.Each regression test fails without its fix, and review ran the new tests against mutants of the fixes. The tests added for coverage alone, such as the
UnionTestlift shapes, pin behaviour of code their commit does not change; review ran them against a mutant that disables the lift in joins, which fails all of them.Test runs:
fa547b7c39: one sequential run ofio.questdb.test.griffin.**,io.questdb.test.cairo.view.**,io.questdb.test.cairo.mv.**,PGMultiStatementMessageTest,PreparedStatementInvalidationTestandPGPivotTest: 28,821 tests, 0 failures, 0 errors, 371 skipped (existing@IgnoreandAssumeguards).1092d74be3: one sequential run of thecutlass.http,cutlass.pgwire,cutlass.qwpandcairo.viewtest packages and theUPDATE,ALTER,DECLARE, view, PIVOT, WITH and parser classes: 8,186 tests, 0 failures, 0 errors, 106 skipped (guards in existing tests). Thegriffin.**run above predates this commit.455f6dabec, this head: one sequential run ofio.questdb.test.griffin.**, thecairo.viewtest package,IODispatcherTestandPGJobContextTest: 28,412 tests in 1,136 classes, 0 failures, 0 errors, 382 skipped (guards in existing tests; this commit adds no@Ignore).e8d7265f84with the paired Enterprise branch. No Enterprise run covers later commits.🤖 Generated with Claude Code