Skip to content

perf(sql): reduce repeated expression execution when a column is referenced by other columns#6093

Merged
bluestreak01 merged 38 commits intomasterfrom
victor_expr_ref_count
Jan 13, 2026
Merged

perf(sql): reduce repeated expression execution when a column is referenced by other columns#6093
bluestreak01 merged 38 commits intomasterfrom
victor_expr_ref_count

Conversation

@kafka1991
Copy link
Copy Markdown
Collaborator

@kafka1991 kafka1991 commented Sep 1, 2025

This PR will materialize function in the same-level queryModel that are depended on by other projection columns. Note that it does not collect references from its parent QueryModel and dependencies from the current queryModels orderBy and whereClause.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 1, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR refactors the function memoization architecture in QuestDB's SQL engine. It removes per-record memoization hooks from core function interfaces, introduces a new MemoizerFunction interface with 17 type-specific memoizer implementations (each using single-value caching), integrates memoization into code generation via a feature flag, and removes the PriorityMetadata parameter from group-by utilities while updating memoization type handling in virtual cursor factories.

Changes

Cohort / File(s) Summary
Core Function Interface Removals
io/questdb/cairo/sql/Function.java, io/questdb/griffin/engine/functions/BinaryFunction.java, io/questdb/griffin/engine/functions/TernaryFunction.java, io/questdb/griffin/engine/functions/QuaternaryFunction.java, io/questdb/griffin/engine/functions/UnaryFunction.java
Removed default memoize(Record record) method from five core function interfaces and related imports, eliminating per-record memoization delegation pattern.
Memoizer Interface & Base
io/questdb/griffin/engine/functions/memoization/MemoizerFunction.java
Introduced new MemoizerFunction interface extending UnaryFunction with a memoize(Record) contract for type-specific memoizer implementations.
Type-Specific Memoizers (Scalar Types)
io/questdb/griffin/engine/functions/memoization/BooleanFunctionMemoizer.java, io/questdb/griffin/engine/functions/memoization/ByteFunctionMemoizer.java, io/questdb/griffin/engine/functions/memoization/CharFunctionMemoizer.java, io/questdb/griffin/engine/functions/memoization/DateFunctionMemoizer.java, io/questdb/griffin/engine/functions/memoization/DoubleFunctionMemoizer.java, io/questdb/griffin/engine/functions/memoization/FloatFunctionMemoizer.java, io/questdb/griffin/engine/functions/memoization/IntFunctionMemoizer.java, io/questdb/griffin/engine/functions/memoization/ShortFunctionMemoizer.java, io/questdb/griffin/engine/functions/memoization/TimestampFunctionMemoizer.java
Updated all scalar memoizers to implement MemoizerFunction, replace two-record memoization state with single-value caching (validValue flag + cached value), simplify init/memoize logic, and add public getArg() accessor. Removed shouldMemoize() methods.
Type-Specific Memoizers (Complex Types)
io/questdb/griffin/engine/functions/memoization/IPv4FunctionMemoizer.java, io/questdb/griffin/engine/functions/memoization/Long256FunctionMemoizer.java, io/questdb/griffin/engine/functions/memoization/UuidFunctionMemoizer.java
Similar updates as scalar memoizers: implement MemoizerFunction, replace two-record state with single/dual cached values, simplify memoization flow, remove shouldMemoize().
Type-Specific Memoizers (String/Symbol)
io/questdb/griffin/engine/functions/memoization/StrFunctionMemoizer.java, io/questdb/griffin/engine/functions/memoization/SymbolFunctionMemoizer.java, io/questdb/griffin/engine/functions/memoization/VarcharFunctionMemoizer.java
Introduced new memoizers extending string/symbol function types and implementing MemoizerFunction with dual-sink caching (A/B values) for string results; delegate initialization and expose getArg()/getName().
Array Memoizer
io/questdb/griffin/engine/functions/memoization/ArrayFunctionMemoizer.java
Introduced new array memoizer extending ArrayFunction, implementing MemoizerFunction, with DerivedArrayView caching and invalidation via validValue flag.
RND Functions (shouldMemoize Override)
io/questdb/griffin/engine/functions/rnd/RndDoubleArrayFunctionFactory.java, io/questdb/griffin/engine/functions/rnd/RndStrFunction.java, io/questdb/griffin/engine/functions/rnd/RndSymbolFunctionFactory.java, io/questdb/griffin/engine/functions/rnd/RndVarcharFunction.java
Added shouldMemoize() returning true to four RND function classes/factories to enable memoization for random value generation.
Function Parsing & Code Generation
io/questdb/griffin/FunctionParser.java
Removed memoizer wrapping logic and imports; functions no longer conditionally wrapped with memoization proxies during parsing.
SQL Code Generation
io/questdb/griffin/SqlCodeGenerator.java
Added comprehensive memoization support under ALLOW_FUNCTION_MEMOIZATION flag: imports for 17 memoizer types, type-based function wrapping in virtual projection and select-with-subquery paths, and removed PriorityMetadata parameter from GroupByUtils.assembleGroupByFunctions() call sites.
SQL Optimizer
io/questdb/griffin/SqlOptimiser.java
Added static import of ALLOW_FUNCTION_MEMOIZATION, new tempExprs field, and new methods collectColumnRefCount() and collectColumnRefCountRecursive() invoked at end of optimization when feature flag enabled.
Group-By Utilities
io/questdb/griffin/engine/groupby/GroupByUtils.java
Removed PriorityMetadata outPriorityMetadata parameter from assembleGroupByFunctions() public signature; replaced instanceof casting with pattern matching in multiple locations.
Virtual Cursor Factories
io/questdb/griffin/engine/table/VirtualRecordCursorFactory.java, io/questdb/griffin/engine/table/VirtualFunctionRecordCursor.java
Updated VirtualRecordCursorFactory constructor to remove allowMemoization parameter, changed memoization detection from flag-based to instanceof MemoizerFunction check; updated VirtualFunctionRecordCursor field/parameter types from ObjList<Function> to ObjList<MemoizerFunction>.
Query Model
io/questdb/griffin/model/QueryModel.java
Added columnAliasRefCounts field (CharSequenceIntHashMap), new methods getRefCount() and incrementColumnRefCount() for tracking column reference counts.
Array Test Updates
core/src/test/java/io/questdb/test/cairo/ArrayOrderBookTest.java
Enabled memoization globally in setup, replaced inline SQL strings with multi-line text blocks, added assertPlanNoLeakCheck assertions to validate memoization plan structure.
Explain Plan Tests
core/src/test/java/io/questdb/test/griffin/ExplainPlanTest.java
Added allowFunctionMemoization() calls in three tests; updated expected filter output to reflect changed memoization behavior (e.g., removed memoize wrapper).
JSON Extract Memoization Tests
core/src/test/java/io/questdb/test/griffin/JsonExtractMemoizationTest.java
Replaced simple assertions with assertQueryAndPlan, added plan validation for memoized json\_extract usage, expanded expected result sets with additional computed columns.
Projection Reference Tests
core/src/test/java/io/questdb/test/griffin/ProjectionReferenceTest.java
Converted inline SQL strings to multi-line text blocks; added four new test methods: testProjectionInOrderByWithString(), testProjectionInOrderByWithSymbol(), testProjectionInOrderByWithVarchar(), testProjectionWithArray().
RND Memoization Tests
core/src/test/java/io/questdb/test/griffin/RndMemoizationTest.java
Added four new test methods: testRndArray(), testRndStr(), testRndSymbol(), testRndVarchar() validating memoization for random value functions.
SQL Optimizer Tests
core/src/test/java/io/questdb/test/griffin/SqlOptimiserTest.java
Added @Before setUp() to enable ALLOW_FUNCTION_MEMOIZATION; updated testDuplicateColumnsInWindowModel expectation to include memoize wrapper; added two new test methods: testFunctionMemoizationBasicColumnRefCount(), testFunctionMemoizationUnionModel().

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Areas requiring extra attention:

  • Public API changes: Removal of PriorityMetadata parameter from GroupByUtils.assembleGroupByFunctions() — verify all call sites updated and no downstream impacts on group-by processing.
  • Memoization strategy shift: Transition from two-record to single-value caching across 17 memoizer classes — ensure correctness of invalidation logic (validValue flags) and that reuse patterns match expected semantics.
  • Feature flag integration: ALLOW_FUNCTION_MEMOIZATION is used in SqlCodeGenerator and SqlOptimiser — verify conditional paths are correct and feature can be safely toggled.
  • VirtualRecordCursorFactory refactoring: Change from allowMemoization boolean parameter to instanceof MemoizerFunction detection — ensure type-based routing doesn't miss functions that should be memoized.
  • Test expectations: Multiple test files updated with plan assertions and behavioral changes — validate that test expectations align with actual execution behavior and memoization boundaries.
  • Column reference counting: New collectColumnRefCount() methods in SqlOptimiser — verify traversal logic correctly identifies and counts multi-use columns for memoization.

Possibly related PRs

  • PR #6272 — Modifies VirtualFunctionRecordCursor and VirtualRecordCursorFactory alongside PriorityMetadata / Long Top-K support; overlapping constructor and field signature changes in virtual cursor infrastructure.

Suggested labels

Performance, SQL, Enhancement

Suggested reviewers

  • bluestreak01
  • puzpuzpuz
  • nwoolmer

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.42% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective: reducing repeated expression execution by materializing referenced columns in the same-level QueryModel.
Description check ✅ Passed The description is clearly related to the changeset, explaining the PR's purpose of materializing functions that are depended on by other projection columns.

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

❤️ Share

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

@kafka1991 kafka1991 marked this pull request as draft September 1, 2025 15:06
@kafka1991 kafka1991 changed the title perf(sql): Reducing repeat expression execute. perf(sql): Reduce repeat expression execute. Sep 2, 2025
@kafka1991 kafka1991 marked this pull request as ready for review September 2, 2025 02:05
@puzpuzpuz puzpuzpuz changed the title perf(sql): Reduce repeat expression execute. perf(sql): reduce repeat expression execute Sep 2, 2025
@kafka1991 kafka1991 changed the title perf(sql): reduce repeat expression execute perf(sql): reduce repeat expression execute in projection reference Sep 2, 2025
@kafka1991 kafka1991 changed the title perf(sql): reduce repeat expression execute in projection reference perf(sql): reduce repeat expression execute when a column is referenced by other columns. Sep 2, 2025
@puzpuzpuz puzpuzpuz changed the title perf(sql): reduce repeat expression execute when a column is referenced by other columns. perf(sql): reduce repeated expression execution when a column is referenced by other columns Dec 9, 2025
@kafka1991
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 11, 2025

✅ Actions performed

Review triggered.

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (8)
core/src/main/java/io/questdb/griffin/engine/functions/rnd/RndVarcharFunction.java (1)

87-90: Confirm memoization scope for rnd_varchar and add regression test coverage

Enabling shouldMemoize() on a non-deterministic, random function makes sense to avoid re-evaluating rnd_varchar when other projection columns depend on it (e.g., SELECT rnd_varchar(5,10,2) v, length(v) FROM ...). This is only safe if the memoizer’s cache is strictly per record and reset on each row; otherwise rnd_varchar could effectively become constant over the cursor.

Please:

  • Double-check that the MemoizerFunction / memoizer wrappers invalidate cached values on every Record advance for this function.
  • Add a focused test that:
    • Verifies SELECT rnd_varchar(...) v, length(v) yields consistent v/length(v) within a row.
    • Verifies consecutive rows still produce the same distribution/sequence characteristics as before (not accidentally frozen across rows).

Given the coverage report shows 0% on this new branch for RndVarcharFunction, this test would also close that gap. Based on learnings, deterministic Rnd seeds in QuestDB tests will keep such tests stable across runs.

If you’d like, I can sketch an example JUnit test case for this scenario.

core/src/main/java/io/questdb/griffin/model/QueryModel.java (1)

1095-1105: Clarify method semantics: dual use of refCount parameter.

The parameter refCount serves dual purposes: it's added to the existing count when the key exists (line 1100), but used as an initial value when the key doesn't exist (line 1102). This dual behavior isn't obvious from the method signature and could confuse maintainers.

Consider renaming to make the semantics clearer:

-public void incrementColumnRefCount(CharSequence alias, int refCount) {
+public void addColumnRefCount(CharSequence alias, int delta) {
     if (columnAliasIndexes.get(alias) > -1) {
         int keyIndex = columnAliasRefCounts.keyIndex(alias);
         if (keyIndex < 0) {
             int old = columnAliasRefCounts.valueAt(keyIndex);
-            columnAliasRefCounts.putAt(keyIndex, alias, old + refCount);
+            columnAliasRefCounts.putAt(keyIndex, alias, old + delta);
         } else {
-            columnAliasRefCounts.putAt(keyIndex, alias, refCount);
+            columnAliasRefCounts.putAt(keyIndex, alias, delta);
         }
     }
 }

Or add a comment explaining that refCount represents the delta to add, which initializes to that value if the alias hasn't been tracked yet.

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

174-175: Consider clearing tempExprs in clear() for consistency

tempExprs is only used inside collectColumnRefCount and is explicitly cleared there, so leaving it out of clear() is safe. Still, for symmetry with other scratch collections (tempColumns, tempColumns2, tempIntList, etc.) and to reduce the chance of future misuse, it may be worth adding tempExprs.clear() to clear().

     public void clear() {
         clearForUnionModelInJoin();
         contextPool.clear();
@@
         tempColumns.clear();
         tempColumns2.clear();
+        tempExprs.clear();
     }

Also applies to: 238-263


7275-7427: Column ref-count accounting: tighten alias matching and dotted-name handling

The overall structure of collectColumnRefCount(...) makes sense (local traversal, then parent-driven propagation using getRefCount, and skipping non-CHOOSE/VIRTUAL models), but there are a couple of edge cases worth tightening:

  1. Incrementing counts for literals that aren’t produced by this model

    In the parent block, every literal encountered is passed to queryModel.incrementColumnRefCount(...) regardless of whether queryModel actually defines a column with that alias:

    if (dot == -1) {
        queryModel.incrementColumnRefCount(columnRef, refCount);
    } else {
        ...
        if (queryModel.getAlias() != null && Chars.equalsIgnoreCase(tableName, queryModel.getAlias().token)) {
            queryModel.incrementColumnRefCount(columnName, refCount);
        }
    }

    If a parent expression references a base-table column or some other token that is not a column of this queryModel, this will still increment a counter. Unless incrementColumnRefCount internally validates aliases, that can skew the heuristic.

    Suggest gating the increments on the child’s alias map, mirroring the checks you do on the local pass:

    if (dot == -1) {
  • queryModel.incrementColumnRefCount(columnRef, refCount);
  • if (queryModel.getAliasToColumnMap().contains(columnRef)) {
  •    queryModel.incrementColumnRefCount(columnRef, refCount);
    
  • }
    } else {
    final CharSequence tableName = columnRef.subSequence(0, dot);
    final CharSequence columnName = columnRef.subSequence(dot + 1, columnRef.length());
    if (queryModel.getAlias() != null && Chars.equalsIgnoreCase(tableName, queryModel.getAlias().token)) {
  •      queryModel.incrementColumnRefCount(columnName, refCount);
    
  •      if (queryModel.getAliasToColumnMap().contains(columnName)) {
    
  •          queryModel.incrementColumnRefCount(columnName, refCount);
    
  •      }
     }
    
    }
    
    
  1. Key mismatch between local and parent dotted references

    • In the local traversal (same model), for alias.col you increment on the full columnRef:

      queryModel.incrementColumnRefCount(columnRef, 1); // "alias.col"
    • In the parent-driven traversal, you increment on columnName (suffix) only:

      queryModel.incrementColumnRefCount(columnName, refCount); // "col"

    Depending on how QueryModel.incrementColumnRefCount and getRefCount key their internal map, this asymmetry could mean that some references to alias.col are counted under "alias.col" and some under "col", fragmenting counts.

    It might be safer to normalize to the same key everywhere (probably the column alias as exposed by this QueryModel, i.e. suffix without table prefix), e.g.:

    // local dotted case
  • queryModel.incrementColumnRefCount(columnRef, 1);
  • queryModel.incrementColumnRefCount(columnName, 1);
    
    …or, if you need to keep both forms, ensure `incrementColumnRefCount` always canonicalizes to the per-model alias before updating the counter.
    
    
  1. Small clarity/duplication point (optional)

    The expression-tree walks for the local block and parent block are structurally identical. Extracting a tiny helper like traverseExprAndAccumulateRefs(QueryModel target, ExpressionNode ast, int weight, @Nullable QueryModel nestedChild, boolean skipNestedColumns) would reduce duplication and make the logic easier to reason about, but this is more of a maintainability improvement than a bug.

Overall behaviour looks correct in the common cases; the above changes would make the heuristic more robust and self-documenting.


7430-7448: Double-check parent propagation for join models in collectColumnRefCountRecursive

The recursion helper currently does:

ObjList<QueryModel> joinModels = queryModel.getJoinModels();
for (int i = 1, n = joinModels.size(); i < n; i++) {
    collectColumnRefCount(parent, joinModels.getQuick(i));
}
collectColumnRefCount(parent, queryModel.getUnionModel());
...
collectColumnRefCount(parentModel, queryModel.getNestedModel());

For joins, this passes the same parent that was passed into the current call, rather than using queryModel itself as the parent of its join children. That means:

  • If queryModel is a CHOOSE/VIRTUAL layer that defines computed columns used in join criteria or upper projections, those references will not be propagated into join models via this parent link.
  • Top-level calls (collectColumnRefCount(null, root)) will traverse join models with parent == null, so no parent expressions will ever contribute to ref-counts in join branches.

If the intent is that join models should see usage coming from the “join root” model, you likely want:

for (int i = 1, n = joinModels.size(); i < n; i++) {
-    collectColumnRefCount(parent, joinModels.getQuick(i));
+    collectColumnRefCount(queryModel, joinModels.getQuick(i));
}

while preserving the existing parentModel logic for nested models of SELECT_MODEL_NONE wrappers.

If, on the other hand, you intentionally want to ignore join-root expressions when computing ref counts for join branches (e.g., because memoization is only used on projection layers, not join-side functions), it would be helpful to add a brief comment here to capture that design choice so future readers don’t “fix” it back to queryModel later.

Please double-check this against how QueryModel.getRefCount is consumed in SqlCodeGenerator for memoization decisions.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/UuidFunctionMemoizer.java (2)

49-67: Consider extracting duplicate computation logic.

Both getLong128Hi() and getLong128Lo() contain identical cache population logic. Consider extracting to a private helper method for maintainability:

+    private void ensureCached(Record rec) {
+        if (!validValue) {
+            lo = fn.getLong128Lo(rec);
+            hi = fn.getLong128Hi(rec);
+            validValue = true;
+        }
+    }
+
     @Override
     public long getLong128Hi(Record rec) {
-        if (!validValue) {
-            lo = fn.getLong128Lo(rec);
-            hi = fn.getLong128Hi(rec);
-            validValue = true;
-        }
+        ensureCached(rec);
         return hi;
     }

     @Override
     public long getLong128Lo(Record rec) {
-        if (!validValue) {
-            lo = fn.getLong128Lo(rec);
-            hi = fn.getLong128Hi(rec);
-            validValue = true;
-        }
+        ensureCached(rec);
         return lo;
     }

84-88: Minor: Extra blank line.

There's a trailing blank line inside the memoize() method body.

     @Override
     public void memoize(Record record) {
         validValue = false;
-
     }
core/src/main/java/io/questdb/griffin/engine/functions/memoization/DoubleFunctionMemoizer.java (1)

63-65: Optionally reset cache explicitly on init for defensive reuse

If DoubleFunctionMemoizer instances are ever re-initialized and then reused, it may be safer to explicitly clear the memoized state here rather than relying solely on MemoizerFunction.super.init(...) to do so. For example:

@Override
public void init(SymbolTableSource symbolTableSource, SqlExecutionContext executionContext) throws SqlException {
-    MemoizerFunction.super.init(symbolTableSource, executionContext);
+    MemoizerFunction.super.init(symbolTableSource, executionContext);
+    validValue = false;
}

Not strictly necessary if the engine always constructs fresh memoizers or if MemoizerFunction.init already guarantees a reset, but this small change would make the class self-contained and robust to future lifecycle changes.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f63af4 and ddf9824.

📒 Files selected for processing (40)
  • core/src/main/java/io/questdb/cairo/sql/Function.java (0 hunks)
  • core/src/main/java/io/questdb/griffin/FunctionParser.java (0 hunks)
  • core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java (2 hunks)
  • core/src/main/java/io/questdb/griffin/SqlOptimiser.java (4 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/BinaryFunction.java (0 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/QuaternaryFunction.java (0 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/TernaryFunction.java (0 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/UnaryFunction.java (0 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/ArrayFunctionMemoizer.java (1 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/BooleanFunctionMemoizer.java (4 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/ByteFunctionMemoizer.java (4 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/CharFunctionMemoizer.java (4 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/DateFunctionMemoizer.java (4 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/DoubleFunctionMemoizer.java (4 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/FloatFunctionMemoizer.java (4 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/IPv4FunctionMemoizer.java (4 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/IntFunctionMemoizer.java (4 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/Long256FunctionMemoizer.java (4 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/LongFunctionMemoizer.java (4 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/MemoizerFunction.java (1 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/ShortFunctionMemoizer.java (3 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/StrFunctionMemoizer.java (1 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/SymbolFunctionMemoizer.java (1 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/TimestampFunctionMemoizer.java (3 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/UuidFunctionMemoizer.java (4 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/memoization/VarcharFunctionMemoizer.java (1 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/rnd/RndDoubleArrayFunctionFactory.java (2 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/rnd/RndStrFunction.java (1 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/rnd/RndSymbolFunctionFactory.java (1 hunks)
  • core/src/main/java/io/questdb/griffin/engine/functions/rnd/RndVarcharFunction.java (1 hunks)
  • core/src/main/java/io/questdb/griffin/engine/groupby/GroupByUtils.java (2 hunks)
  • core/src/main/java/io/questdb/griffin/engine/table/VirtualFunctionRecordCursor.java (3 hunks)
  • core/src/main/java/io/questdb/griffin/engine/table/VirtualRecordCursorFactory.java (3 hunks)
  • core/src/main/java/io/questdb/griffin/model/QueryModel.java (5 hunks)
  • core/src/test/java/io/questdb/test/cairo/ArrayOrderBookTest.java (8 hunks)
  • core/src/test/java/io/questdb/test/griffin/ExplainPlanTest.java (4 hunks)
  • core/src/test/java/io/questdb/test/griffin/JsonExtractMemoizationTest.java (2 hunks)
  • core/src/test/java/io/questdb/test/griffin/ProjectionReferenceTest.java (27 hunks)
  • core/src/test/java/io/questdb/test/griffin/RndMemoizationTest.java (3 hunks)
  • core/src/test/java/io/questdb/test/griffin/SqlOptimiserTest.java (5 hunks)
💤 Files with no reviewable changes (6)
  • core/src/main/java/io/questdb/griffin/engine/functions/TernaryFunction.java
  • core/src/main/java/io/questdb/griffin/engine/functions/QuaternaryFunction.java
  • core/src/main/java/io/questdb/griffin/FunctionParser.java
  • core/src/main/java/io/questdb/griffin/engine/functions/BinaryFunction.java
  • core/src/main/java/io/questdb/cairo/sql/Function.java
  • core/src/main/java/io/questdb/griffin/engine/functions/UnaryFunction.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-19T12:21:00.062Z
Learnt from: jerrinot
Repo: questdb/questdb PR: 6413
File: core/src/test/java/io/questdb/test/cutlass/pgwire/PGJobContextTest.java:11982-12002
Timestamp: 2025-11-19T12:21:00.062Z
Learning: QuestDB Java tests use a deterministic random seed. The test utilities (e.g., io.questdb.test.tools.TestUtils and io.questdb.std.Rnd) produce reproducible sequences, so rnd_* functions (including rnd_uuid4) yield deterministic outputs across runs. Do not flag tests in core/src/test/** that assert against values produced by rnd_* as flaky due to randomness.

Applied to files:

  • core/src/main/java/io/questdb/griffin/engine/functions/rnd/RndVarcharFunction.java
  • core/src/test/java/io/questdb/test/griffin/RndMemoizationTest.java
  • core/src/test/java/io/questdb/test/griffin/ExplainPlanTest.java
  • core/src/test/java/io/questdb/test/griffin/SqlOptimiserTest.java
  • core/src/main/java/io/questdb/griffin/engine/functions/rnd/RndDoubleArrayFunctionFactory.java
📚 Learning: 2025-11-10T14:28:48.329Z
Learnt from: mtopolnik
Repo: questdb/questdb PR: 0
File: :0-0
Timestamp: 2025-11-10T14:28:48.329Z
Learning: In AsOfJoinDenseRecordCursorFactoryBase.java, the `backwardScanExhausted` flag is intentionally NOT reset in `toTop()` because backward scan results are reusable across cursor rewinds. The backward scan caches historical matches that remain valid when the cursor is rewound.

Applied to files:

  • core/src/main/java/io/questdb/griffin/model/QueryModel.java
🧬 Code graph analysis (6)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/SymbolFunctionMemoizer.java (2)
core/src/main/java/io/questdb/griffin/engine/functions/SymbolFunction.java (1)
  • SymbolFunction (52-276)
core/src/main/java/io/questdb/std/str/StringSink.java (1)
  • StringSink (31-194)
core/src/main/java/io/questdb/griffin/model/QueryModel.java (1)
core/src/main/java/io/questdb/std/CharSequenceIntHashMap.java (1)
  • CharSequenceIntHashMap (32-170)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/ArrayFunctionMemoizer.java (3)
core/src/main/java/io/questdb/cairo/arr/ArrayView.java (1)
  • ArrayView (135-600)
core/src/main/java/io/questdb/cairo/arr/DerivedArrayView.java (1)
  • DerivedArrayView (36-309)
core/src/main/java/io/questdb/std/Misc.java (1)
  • Misc (35-154)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/VarcharFunctionMemoizer.java (1)
core/src/main/java/io/questdb/std/str/Utf8StringSink.java (1)
  • Utf8StringSink (36-207)
core/src/test/java/io/questdb/test/cairo/ArrayOrderBookTest.java (1)
core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java (1)
  • SqlCodeGenerator (350-7515)
core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java (18)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/ArrayFunctionMemoizer.java (1)
  • ArrayFunctionMemoizer (37-91)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/BooleanFunctionMemoizer.java (1)
  • BooleanFunctionMemoizer (34-81)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/ByteFunctionMemoizer.java (1)
  • ByteFunctionMemoizer (34-81)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/CharFunctionMemoizer.java (1)
  • CharFunctionMemoizer (34-81)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/DateFunctionMemoizer.java (1)
  • DateFunctionMemoizer (34-81)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/DoubleFunctionMemoizer.java (1)
  • DoubleFunctionMemoizer (34-81)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/FloatFunctionMemoizer.java (1)
  • FloatFunctionMemoizer (34-81)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/IPv4FunctionMemoizer.java (1)
  • IPv4FunctionMemoizer (34-81)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/IntFunctionMemoizer.java (1)
  • IntFunctionMemoizer (34-81)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/Long256FunctionMemoizer.java (1)
  • Long256FunctionMemoizer (37-114)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/LongFunctionMemoizer.java (1)
  • LongFunctionMemoizer (34-81)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/ShortFunctionMemoizer.java (1)
  • ShortFunctionMemoizer (34-81)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/StrFunctionMemoizer.java (1)
  • StrFunctionMemoizer (35-121)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/SymbolFunctionMemoizer.java (1)
  • SymbolFunctionMemoizer (37-183)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/TimestampFunctionMemoizer.java (1)
  • TimestampFunctionMemoizer (34-82)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/UuidFunctionMemoizer.java (1)
  • UuidFunctionMemoizer (34-94)
core/src/main/java/io/questdb/griffin/engine/functions/memoization/VarcharFunctionMemoizer.java (1)
  • VarcharFunctionMemoizer (36-122)
core/src/main/java/io/questdb/cairo/ColumnType.java (1)
  • ColumnType (58-1164)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (35)
  • GitHub Check: New pull request (Hosted Running tests on windows-other-2)
  • GitHub Check: New pull request (Hosted Running tests on windows-other-1)
  • GitHub Check: New pull request (Hosted Running tests on windows-pgwire)
  • GitHub Check: New pull request (Hosted Running tests on windows-cairo-2)
  • GitHub Check: New pull request (Hosted Running tests on windows-cairo-1)
  • GitHub Check: New pull request (Hosted Running tests on windows-fuzz2)
  • GitHub Check: New pull request (SelfHosted Running tests with cover on linux-other)
  • GitHub Check: New pull request (Hosted Running tests on windows-fuzz1)
  • GitHub Check: New pull request (SelfHosted Running tests with cover on linux-pgwire)
  • GitHub Check: New pull request (Hosted Running tests on windows-griffin-sub)
  • GitHub Check: New pull request (SelfHosted Running tests with cover on linux-cairo-sub)
  • GitHub Check: New pull request (Hosted Running tests on windows-griffin-base)
  • GitHub Check: New pull request (SelfHosted Running tests with cover on linux-cairo-root)
  • GitHub Check: New pull request (Hosted Running tests on mac-other)
  • GitHub Check: New pull request (SelfHosted Running tests with cover on linux-fuzz2)
  • GitHub Check: New pull request (Hosted Running tests on mac-pgwire)
  • GitHub Check: New pull request (Hosted Running tests on mac-cairo-fuzz)
  • GitHub Check: New pull request (SelfHosted Running tests with cover on linux-fuzz1)
  • GitHub Check: New pull request (Hosted Running tests on mac-cairo)
  • GitHub Check: New pull request (SelfHosted Running tests with cover on linux-griffin-sub)
  • GitHub Check: New pull request (Hosted Running tests on mac-griffin)
  • GitHub Check: New pull request (Rust Test and Lint on linux-jdk17)
  • GitHub Check: New pull request (Trigger Enterprise CI Trigger Enterprise Pipeline)
  • GitHub Check: New pull request (SelfHosted Running tests with cover on linux-griffin-root)
  • GitHub Check: New pull request (SelfHosted Other tests on linux-x86-graal)
  • GitHub Check: New pull request (SelfHosted Griffin tests on linux-x64-zfs)
  • GitHub Check: New pull request (SelfHosted Other tests on linux-arm64)
  • GitHub Check: New pull request (SelfHosted Cairo tests on linux-x86-graal)
  • GitHub Check: New pull request (SelfHosted Cairo tests on linux-x64-zfs)
  • GitHub Check: New pull request (SelfHosted Griffin tests on linux-x86-graal)
  • GitHub Check: New pull request (SelfHosted Other tests on linux-x64-zfs)
  • GitHub Check: New pull request (SelfHosted Griffin tests on linux-arm64)
  • GitHub Check: New pull request (SelfHosted Cairo tests on linux-arm64)
  • GitHub Check: New pull request (Check Changes Check changes)
  • GitHub Check: build
🔇 Additional comments (50)
core/src/main/java/io/questdb/griffin/engine/functions/rnd/RndDoubleArrayFunctionFactory.java (2)

254-257: LGTM! Consistent memoization for random dimension lengths.

The shouldMemoize() override is correctly implemented and maintains consistency with the FixDimLenFunc variant. Both fixed and random dimension length paths now support memoization, ensuring uniform behavior across the rnd_double_array function family.


184-187: LGTM!

The shouldMemoize() override correctly enables caching for this random function, ensuring consistent values when referenced multiple times in the same row context (e.g., SELECT rnd_double_array(...) as x, x[0], x[1]). Comprehensive functional tests already exist in RndMemoizationTest.testRndArray() that verify the memoization behavior with deterministic expected values.

core/src/main/java/io/questdb/griffin/engine/functions/rnd/RndSymbolFunctionFactory.java (1)

129-132: No changes needed.

The shouldMemoize() returning true for random functions is intentional and correct design. Memoization in this context caches function results per record evaluation, not across records. Each row receives a fresh random value because the memoization cache is reset via memoize(Record record) on every record.

When a random function is referenced multiple times within the same row (e.g., SELECT rnd_int() x1, x1+1 x12), memoization ensures the same random value is used for all references, preventing inconsistency. This is the intended behavior introduced in PR #5778 ("enable SQL projection columns to reference preceding columns on the same projection").

The comprehensive RndMemoizationTest suite confirms that each row produces different random values while memoization correctly handles intra-row reuse, and the deterministic random seed used in QuestDB tests ensures reproducibility.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/MemoizerFunction.java (1)

30-32: Clean interface design for memoization.

The interface appropriately extends UnaryFunction to leverage getArg() for accessing the wrapped function, and the memoize(Record) method provides a clear contract for cache invalidation. This establishes a consistent pattern for all memoizer implementations.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/SymbolFunctionMemoizer.java (3)

80-128: Well-structured cross-caching optimization.

The logic correctly reuses cached values between getSymbol and getSymbolB variants, and properly copies into separate sinks to maintain independent storage. The null handling and validity flag management is correct.


37-51: LGTM on the class structure and field initialization.

The use of separate StringSink instances for A and B variants prevents aliasing issues. The validity flags provide a clear cache invalidation mechanism.


148-153: Cache invalidation is correct.

Resetting validity flags without clearing sinks is efficient—the sinks will be overwritten on next access anyway.

core/src/main/java/io/questdb/griffin/engine/functions/rnd/RndStrFunction.java (1)

75-78: Correct: memoization ensures consistent random values per row.

For non-deterministic random functions, enabling memoization is critical to ensure that multiple references to the same column within a row return the same value. This aligns with the PR's goal of materializing functions that are referenced by other projection columns.

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

796-806: Memoization flag usage and test isolation

Enabling memoization explicitly in these three tests and asserting on memoize(...) in the plan looks consistent with the new behavior and keeps the expectations clear.

One thing to double‑check: if allowFunctionMemoization() flips a global/static flag (e.g., on the engine or SQL code generator), make sure it’s reset between tests (via AbstractCairoTest lifecycle or an internal try/finally) so that tests which expect non‑memoized plans don’t become order‑dependent on execution order.

Also applies to: 1844-1862, 10729-10741


11535-11545: Non‑jitted async filter explicitly kept non‑memoized

The added comment and updated expectation (filter: l=rnd_long()) correctly document that the Async Filter path does not participate in function memoization, which aligns with the stated limitations of the current memoization implementation.

core/src/test/java/io/questdb/test/griffin/SqlOptimiserTest.java (4)

81-85: Memoization toggle in test setup is appropriate

Calling super.setUp() and then forcing SqlCodeGenerator.ALLOW_FUNCTION_MEMOIZATION = true in @Before setUp() keeps all optimiser tests in this class consistently on the memoized path, matching the PR’s objectives and avoiding per-test boilerplate.


417-421: Window inner VirtualRecord now correctly memoizes reused expression

The updated plan expectation for testDuplicateColumnsInWindowModel (functions [hostname,ts,memoize(usage_system),usage_system1+10]) ensures the base usage_system expression is computed once and reused via the usage_system1 alias in usage_system1 + 10. This matches the new “materialize shared expressions” behaviour and keeps the test aligned with the optimised plan.


481-568: New basic ref-count memoization test covers key projection/order/group/filter cases

testFunctionMemoizationBasicColumnRefCount exercises:

  • Projection + ORDER BY on a + b with memoization.
  • Nested subquery where an aliased a + 1 is reused multiple times.
  • Aggregation over repeated alias expressions.
  • WHERE filter over an alias while memoizing the underlying a + 1.

The expected plans and result sets are consistent and give good coverage that repeated expressions are materialized once at the appropriate model level.


570-595: Union-model memoization test validates symmetric handling across branches

testFunctionMemoizationUnionModel verifies that each side of a UNION independently memoizes a + 1 / c + 1 in an inner VirtualRecord, while the outer projections reuse the a1 alias for a1 + 1 and a1 + 2. The plan shape matches the intended optimisation, and asserting only the plan here is sufficient given the simple schema.

core/src/test/java/io/questdb/test/cairo/ArrayOrderBookTest.java (6)

27-39: Enabling memoization in ArrayOrderBook tests is consistent with optimiser changes

Importing SqlCodeGenerator and setting SqlCodeGenerator.ALLOW_FUNCTION_MEMOIZATION = true in setUpThisTest() ensures all order-book tests run with the memoized-function code path enabled, which is required for the new memoize(...) expectations in plan assertions.


48-52: Bid/ask spread assertion change is purely cosmetic

Switching to a text block for the expected result of testBidAskSpread keeps the test clearer without changing behaviour. The SQL and expected values remain correct.


76-97: Cumulative imbalance test correctly validates memoized array_sum usage

testImbalanceCumulative now:

  • Builds a readable sql string using array_sum(asks[2, 1:4]) and array_sum(bids[2, 1:4]) with aliases.
  • Asserts a plan where both sums are wrapped in memoize(...) and reused in bid_vol / ask_vol.
  • Verifies the resulting ask_vol, bid_vol, and ratio against deterministic data.

The slice bounds and expected numbers are consistent; this gives good coverage that array aggregation expressions are materialized once per row.


109-112: Price-level test refactor to text blocks improves readability

Converting the expected output in testPriceLevelForTargetVolume to a text block while keeping the SQL unchanged makes the test easier to read and does not affect semantics; the cumulative volumes and insertion_point expectations still check out.


161-165: Sudden volume drop test change is a formatting improvement

For testSuddenVolumeDrop, moving the expected result into a text block simplifies maintenance; the query and the lag-based volume drop conditions remain unchanged and correct.


219-236: Volume dropoff test now also validates memoization of array_avg slices

testVolumeDropoff now:

  • Factors the SQL into a sql string.
  • Asserts a plan where both array_avg(asks[2, 1:3]) and array_avg(asks[2, 3:6]) are memoized once in the VirtualRecord.
  • Checks that the filter WHERE top > 3 * deep is represented equivalently as 3*deep<top.

The expected top / deep values match the inserted data, and this test nicely covers memoization of windowed array averages plus a dependent filter.

core/src/main/java/io/questdb/griffin/engine/groupby/GroupByUtils.java (2)

208-248: LGTM! Pattern matching improves readability.

The use of pattern matching for instanceof (func instanceof GroupByFunction groupByFunc) is a good modernization that eliminates the need for explicit casting on line 215 and subsequent usages.


440-448: LGTM! Consistent pattern matching usage.

Same pattern matching improvement applied consistently here.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/IntFunctionMemoizer.java (1)

34-80: LGTM! Clean single-value memoization implementation.

The memoization pattern is straightforward and correct:

  • memoize(record) invalidates the cache by setting validValue = false
  • getInt(rec) lazily computes and caches the value on first access
  • isThreadSafe() correctly returns false since the cache state is not synchronized

This aligns well with the PR's goal of reducing repeated expression execution.

core/src/main/java/io/questdb/griffin/engine/table/VirtualFunctionRecordCursor.java (2)

38-74: LGTM! Type strengthening improves safety.

Changing the memoizers collection from ObjList<Function> to ObjList<MemoizerFunction> provides better type safety and makes the intent clearer. The constructor and field are consistently typed.


189-194: LGTM! Memoization trigger logic is correct.

The memoizeFunctions method correctly:

  1. Extracts the internal join record from the virtual record
  2. Iterates over all memoizers and calls memoize(joinRecord) to invalidate cached values

This is called from both hasNext() and recordAt(), ensuring memoization is properly triggered on record transitions.

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

47-64: LGTM! Comprehensive memoization test with plan verification.

Good use of assertQueryAndPlan to verify both the query results and execution plan. The plan correctly shows memoize(json_extract()::byte) confirming the function is wrapped for memoization when referenced by other columns (res + 1).


227-273: LGTM! Additional byte-type memoization test coverage.

Tests multiple JSON paths ($.a, $.b) with the same memoization pattern, ensuring the feature works consistently across different JSON field extractions.

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

85-104: LGTM! Array memoization test with derived values.

Good test coverage for rnd_double_array memoization. The test verifies that:

  1. Array is generated once and memoized
  2. arr[1] accesses the first element from the memoized array
  3. first_elem * 2 correctly operates on the memoized value

The null handling in expected results is also well-covered. Based on learnings, QuestDB tests use deterministic random seeds, so these assertions are reliable.


396-436: LGTM! String and symbol memoization tests.

Both testRndStr and testRndSymbol verify that string/symbol values are properly memoized when referenced via column aliases. The pattern of applying upper() and then concatenating with a suffix ensures the memoized value is accessed multiple times correctly.


450-469: LGTM! Varchar memoization test with Unicode support.

Good test coverage for varchar memoization including Unicode characters. The expected values demonstrate that upper() and concatenation work correctly with the memoized varchar values.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/CharFunctionMemoizer.java (1)

34-74: LGTM: Clean refactoring to single-value memoization.

The transition from dual-slot memoization to a single-value cache with a validity flag is implemented correctly. The getChar() method properly uses lazy evaluation, and memoize() correctly invalidates the cache. The implementation aligns well with the new MemoizerFunction contract.

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

88-88: Memoization toggle wiring into optimise() looks sensible

The static import of ALLOW_FUNCTION_MEMOIZATION and the guarded call to collectColumnRefCount(null, rewrittenModel) integrate the feature cleanly without affecting existing optimisation flows when the flag is disabled. No functional issues spotted here.

Also applies to: 7491-7493

core/src/main/java/io/questdb/griffin/engine/functions/memoization/ByteFunctionMemoizer.java (1)

34-81: Clean memoization implementation.

The single-value caching pattern with lazy computation and invalidation on memoize() is correct. The explicit isThreadSafe() = false appropriately reflects the mutable state.

core/src/main/java/io/questdb/griffin/engine/table/VirtualRecordCursorFactory.java (1)

64-78: Good migration to type-based memoization detection.

Using instanceof MemoizerFunction is cleaner than the previous flag-based approach. This ensures only explicitly wrapped memoizer functions are tracked, and the typed ObjList<MemoizerFunction> improves type safety.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/ShortFunctionMemoizer.java (1)

34-81: Consistent implementation with other memoizers.

Follows the same single-value caching pattern as other type-specific memoizers.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/DateFunctionMemoizer.java (1)

34-81: Consistent implementation with other memoizers.

Follows the established single-value caching pattern with appropriate long storage for date values.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/Long256FunctionMemoizer.java (1)

37-108: LGTM!

The dual A/B caching pattern is correctly implemented for Long256. The cross-referencing logic between getLong256A and getLong256B properly avoids redundant computation when one value is already cached.

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

414-482: LGTM!

Good addition of test coverage for String, Symbol, and Varchar types with projection references and ORDER BY. These tests properly exercise the memoization paths for string-like column types.


543-560: LGTM!

Comprehensive test for array projection handling with nested access and arithmetic operations. The test validates that array indexing works correctly through projection references.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/StrFunctionMemoizer.java (1)

35-121: LGTM!

Well-structured implementation following the established memoization pattern. The dual A/B caching with cross-referencing and proper null handling is correctly implemented.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/LongFunctionMemoizer.java (1)

34-81: LGTM!

Clean and correct implementation of single-value caching for the long primitive type.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/FloatFunctionMemoizer.java (1)

34-81: LGTM!

Consistent implementation following the same single-value caching pattern as other primitive type memoizers.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/ArrayFunctionMemoizer.java (1)

46-50: LGTM on resource cleanup.

The close() method correctly chains to MemoizerFunction.super.close() and frees the derivedArray using Misc.free(), ensuring proper resource management.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/VarcharFunctionMemoizer.java (1)

36-122: LGTM!

Implementation is consistent with StrFunctionMemoizer, properly adapted for Utf8Sequence types. The dual A/B caching with cross-referencing and null handling is correctly implemented.

core/src/main/java/io/questdb/griffin/engine/functions/memoization/IPv4FunctionMemoizer.java (1)

34-81: LGTM!

Consistent implementation following the single-value caching pattern for the IPv4 type (represented as int).

core/src/main/java/io/questdb/griffin/engine/functions/memoization/BooleanFunctionMemoizer.java (1)

34-81: LGTM! Clean single-value memoization implementation.

The caching logic is correct:

  • validValue defaults to false, ensuring first access computes the value
  • memoize() properly invalidates the cache for each new record
  • isThreadSafe() correctly returns false for the mutable state
  • Delegation patterns for init() and supportsRandomAccess() are appropriate
core/src/main/java/io/questdb/griffin/engine/functions/memoization/TimestampFunctionMemoizer.java (1)

34-82: LGTM! Consistent with the memoization pattern.

Implementation correctly mirrors BooleanFunctionMemoizer for timestamp values:

  • Type metadata preserved via super(fn.getType()) in constructor
  • validValue guard ensures the default 0L value is never returned without computation
  • Same correct invalidation and caching flow
core/src/main/java/io/questdb/griffin/engine/functions/memoization/DoubleFunctionMemoizer.java (1)

34-37: Single-value memoization logic looks sound

The transition to a single cached double guarded by validValue (getDouble computing once per memoization cycle and memoize invalidating the cache) is consistent with the MemoizerFunction contract and should correctly avoid repeated evaluation when multiple projection columns depend on the same underlying function. The lazy evaluation in getDouble(Record) and the simple invalidation in memoize(Record) keep this fast and easy to reason about, and isThreadSafe() == false accurately reflects the stateful nature.

Also applies to: 49-55, 73-75

core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java (2)

150-166: Memoizer imports align with usage

The added imports cover all memoizer types used later in generateSelectVirtualWithSubQuery (numeric, temporal, string, symbol, varchar, array, UUID, IPv4, Long256). No issues here.


5580-5585: Passing virtualColumnReservedSlots into VirtualRecordCursorFactory looks consistent

The new virtualColumnReservedSlots = columnCount + 1 is used both to size PriorityMetadata and as the final argument to VirtualRecordCursorFactory, ensuring the factory knows about the maximum number of virtual slots (projected columns plus optional hidden timestamp). This is coherent with the way the metadata is constructed.

@glasstiger
Copy link
Copy Markdown
Contributor

[PR Coverage check]

😍 pass : 383 / 435 (88.05%)

file detail

path covered line new line coverage
🔵 io/questdb/griffin/engine/functions/rnd/RndSymbolFunctionFactory.java 0 1 00.00%
🔵 io/questdb/griffin/engine/functions/rnd/RndStrFunction.java 0 1 00.00%
🔵 io/questdb/griffin/engine/functions/rnd/RndDoubleArrayFunctionFactory.java 0 2 00.00%
🔵 io/questdb/griffin/engine/functions/rnd/RndVarcharFunction.java 0 1 00.00%
🔵 io/questdb/griffin/engine/functions/math/RoundDoubleFunctionFactory.java 1 3 33.33%
🔵 io/questdb/griffin/engine/functions/memoization/VarcharFunctionMemoizer.java 21 36 58.33%
🔵 io/questdb/griffin/engine/functions/memoization/SymbolFunctionMemoizer.java 47 62 75.81%
🔵 io/questdb/griffin/engine/functions/memoization/UuidFunctionMemoizer.java 9 12 75.00%
🔵 io/questdb/griffin/engine/functions/memoization/StrFunctionMemoizer.java 31 36 86.11%
🔵 io/questdb/griffin/engine/functions/memoization/ArrayFunctionMemoizer.java 19 22 86.36%
🔵 io/questdb/griffin/engine/functions/memoization/Long256FunctionMemoizer.java 20 22 90.91%
🔵 io/questdb/griffin/SqlOptimiser.java 111 113 98.23%
🔵 io/questdb/griffin/engine/functions/memoization/BooleanFunctionMemoizer.java 6 6 100.00%
🔵 io/questdb/griffin/engine/functions/memoization/IPv4FunctionMemoizer.java 6 6 100.00%
🔵 io/questdb/griffin/engine/functions/memoization/ByteFunctionMemoizer.java 6 6 100.00%
🔵 io/questdb/griffin/engine/functions/memoization/FloatFunctionMemoizer.java 6 6 100.00%
🔵 io/questdb/griffin/engine/functions/memoization/TimestampFunctionMemoizer.java 6 6 100.00%
🔵 io/questdb/griffin/engine/functions/memoization/DoubleFunctionMemoizer.java 6 6 100.00%
🔵 io/questdb/griffin/SqlCodeGenerator.java 39 39 100.00%
🔵 io/questdb/griffin/engine/functions/memoization/CharFunctionMemoizer.java 6 6 100.00%
🔵 io/questdb/griffin/engine/functions/memoization/IntFunctionMemoizer.java 6 6 100.00%
🔵 io/questdb/griffin/engine/functions/memoization/DateFunctionMemoizer.java 6 6 100.00%
🔵 io/questdb/griffin/engine/table/VirtualRecordCursorFactory.java 3 3 100.00%
🔵 io/questdb/griffin/engine/functions/memoization/LongFunctionMemoizer.java 6 6 100.00%
🔵 io/questdb/griffin/model/QueryModel.java 16 16 100.00%
🔵 io/questdb/griffin/engine/functions/memoization/ShortFunctionMemoizer.java 6 6 100.00%

@bluestreak01 bluestreak01 merged commit eb6a27a into master Jan 13, 2026
43 checks passed
@bluestreak01 bluestreak01 deleted the victor_expr_ref_count branch January 13, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants