Repository: questdb/questdb
Pull request #5655: fix(sql): non-deterministic function not rejected when creating materialized view
Author: login: puzpuzpuz
Labels: name: Bug
description: Incorrect or unexpected behavior
name: SQL
description: Issues or changes relating to SQL execution
name: Materialized View
Created: 2025-05-02T11:10:01Z | Merged: 2025-05-10T22:01:41Z

Fixes #5645

**Important note**: if a materialized view with non-deterministic SQL function, e.g. `now()` or `today()`, was created on 8.3.1 or older version, with this change it'll be marked as invalid on the first refresh. This is expected since non-deterministic functions were never supported by incrementally refreshed materialized views.

Also, changes `FunctionParser` to keep parallel filters enabled when compiling cursor functions, e.g. IN operator for symbol. Disabling parallel filters is no longer need since #5445 changed the functions to execute the sub-query in `Function#init()`.

Linked issue #5645: Non-deterministic SQL functions are not banned in materialized views
### To reproduce

Filters like `WHERE timestamp in today()` are accepted in mat view queries while they should lead to `Non-deterministic column` SQL error.

### QuestDB version:

8.3.1

### OS, in case of Docker specify Docker and the Host OS:

Linux

### File System, in case of Docker specify Host File System:

ext4

### Full Name:

Andrei Pechkurov

### Affiliation:

QuestDB

### Have you followed Linux, MacOs kernel configuration steps to increase Maximum open files and Maximum virtual memory areas limit?

- [x] Yes, I have

### Additional context

_No response_

Diff:
diff --git a/core/src/main/java/io/questdb/cairo/mv/MatViewRefreshExecutionContext.java b/core/src/main/java/io/questdb/cairo/mv/MatViewRefreshExecutionContext.java
index f419cd6dde58..3a1863e9dd62 100644
--- a/core/src/main/java/io/questdb/cairo/mv/MatViewRefreshExecutionContext.java
+++ b/core/src/main/java/io/questdb/cairo/mv/MatViewRefreshExecutionContext.java
@@ -53,17 +53,20 @@ public MatViewRefreshExecutionContext(CairoEngine engine, int workerCount, int s
             setParallelGroupByEnabled(false);
             setParallelReadParquetEnabled(false);
         }
-        with(
-                new ReadOnlySecurityContext() {
-                    @Override
-                    public void authorizeInsert(TableToken tableToken) {
-                        if (!tableToken.equals(viewTableToken)) {
-                            throw CairoException.authorization().put("Write permission denied").setCacheable(true);
-                        }
-                    }
-                },
-                new BindVariableServiceImpl(engine.getConfiguration())
-        );
+        this.securityContext = new ReadOnlySecurityContext() {
+            @Override
+            public void authorizeInsert(TableToken tableToken) {
+                if (!tableToken.equals(viewTableToken)) {
+                    throw CairoException.authorization().put("Write permission denied").setCacheable(true);
+                }
+            }
+        };
+        this.bindVariableService = new BindVariableServiceImpl(engine.getConfiguration());
+    }
+
+    @Override
+    public boolean allowNonDeterministicFunctions() {
+        return false;
     }
 
     public void clearReader() {
@@ -125,6 +128,11 @@ public void overrideWhereIntrinsics(TableToken tableToken, IntrinsicModel intrin
         intrinsicModel.setBetweenBoundary(new IndexedParameterLinkFunction(2, ColumnType.TIMESTAMP, 0));
     }
 
+    @Override
+    public void setAllowNonDeterministicFunction(boolean value) {
+        // no-op
+    }
+
     // tsLo is inclusive, tsHi is exclusive
     public void setRange(long tsLo, long tsHi) throws SqlException {
         getBindVariableService().setTimestamp(1, tsLo);
diff --git a/core/src/main/java/io/questdb/griffin/FunctionParser.java b/core/src/main/java/io/questdb/griffin/FunctionParser.java
index 1f9c06faca58..33b606f43979 100644
--- a/core/src/main/java/io/questdb/griffin/FunctionParser.java
+++ b/core/src/main/java/io/questdb/griffin/FunctionParser.java
@@ -579,6 +579,9 @@ private Function checkAndCreateFunction(
                     .I$();
             Misc.freeObjList(args);
             throw SqlException.position(position).put("bad function factory (NULL), check log");
+        } else if (!sqlExecutionContext.allowNonDeterministicFunctions() && function.isNonDeterministic()) {
+            Misc.freeObjList(args);
+            throw SqlException.nonDeterministicColumn(node.position, node.token);
         }
         return function;
     }
@@ -681,16 +684,11 @@ private Function createConstant(int position, final CharSequence tok) throws Sql
 
     private Function createCursorFunction(ExpressionNode node) throws SqlException {
         assert node.queryModel != null;
-        // Disable async offload for in (select ...) sub-queries to avoid infinite loops
-        // due to nested reduce calls. See SqlCodeGenerator#testBug484() for the reproducer.
-        boolean currentFilterEnabled = sqlExecutionContext.isParallelFilterEnabled();
-        sqlExecutionContext.setParallelFilterEnabled(false);
         // Make sure to override timestamp required flag from base query.
         sqlExecutionContext.pushTimestampRequiredFlag(false);
         try {
             return new CursorFunction(sqlCodeGenerator.generate(node.queryModel, sqlExecutionContext));
         } finally {
-            sqlExecutionContext.setParallelFilterEnabled(currentFilterEnabled);
             sqlExecutionContext.popTimestampRequiredFlag();
         }
     }
@@ -705,7 +703,6 @@ private Function createFunction(
             throw invalidFunction(node, args);
         }
 
-
         final int argCount = args == null ? 0 : args.size();
         FunctionFactory candidate = null;
         FunctionFactoryDescriptor candidateDescriptor = null;
@@ -716,9 +713,11 @@ private Function createFunction(
         int bestMatch = MATCH_NO_MATCH;
         boolean isWindowContext = !sqlExecutionContext.getWindowContext().isEmpty();
 
-
-        if (SqlKeywords.isCastKeyword(node.token) && argCount == 2
-                && args.getQuick(1).isConstant()) skipAssigningType:{
+        if (
+                SqlKeywords.isCastKeyword(node.token)
+                        && argCount == 2
+                        && args.getQuick(1).isConstant()
+        ) skipAssigningType:{
             // If this the cast into same type, return the first argument
             if (args.getQuick(0).getType() == args.getQuick(1).getType()) {
                 return args.getQuick(0);
diff --git a/core/src/main/java/io/questdb/griffin/SqlCompilerImpl.java b/core/src/main/java/io/questdb/griffin/SqlCompilerImpl.java
index 00696423682e..9bced6a36b7e 100644
--- a/core/src/main/java/io/questdb/griffin/SqlCompilerImpl.java
+++ b/core/src/main/java/io/questdb/griffin/SqlCompilerImpl.java
@@ -2231,12 +2231,16 @@ private void compileMatViewQuery(
                 throw e;
             }
             createMatViewOp.validateAndUpdateMetadataFromModel(executionContext, optimiser.getFunctionFactoryCache(), queryModel);
-            queryModel.setIsMatView(true);
+
+            final boolean ogAllowNonDeterministic = executionContext.allowNonDeterministicFunctions();
+            executionContext.setAllowNonDeterministicFunction(false);
             try {
                 compiledQuery.ofSelect(generateSelectWithRetries(queryModel, executionContext, false));
             } catch (SqlException e) {
                 e.setPosition(e.getPosition() + selectTextPosition);
                 throw e;
+            } finally {
+                executionContext.setAllowNonDeterministicFunction(ogAllowNonDeterministic);
             }
         } catch (Throwable th) {
             QueryProgress.logError(th, -1, sqlText, executionContext, beginNanos);
diff --git a/core/src/main/java/io/questdb/griffin/SqlException.java b/core/src/main/java/io/questdb/griffin/SqlException.java
index 9b63905fc890..23e1ad7b28e0 100644
--- a/core/src/main/java/io/questdb/griffin/SqlException.java
+++ b/core/src/main/java/io/questdb/griffin/SqlException.java
@@ -107,7 +107,7 @@ public static SqlException matViewDoesNotExist(int position, CharSequence tableN
     }
 
     public static SqlException nonDeterministicColumn(int position, CharSequence column) {
-        return position(position).put("Non-deterministic column: ").put(column);
+        return position(position).put("non-deterministic function cannot be used in materialized view: ").put(column);
     }
 
     public static SqlException parserErr(int position, @Nullable CharSequence tok, @NotNull CharSequence msg) {
diff --git a/core/src/main/java/io/questdb/griffin/SqlExecutionContext.java b/core/src/main/java/io/questdb/griffin/SqlExecutionContext.java
index 3564a468cf4a..1cacdb46734b 100644
--- a/core/src/main/java/io/questdb/griffin/SqlExecutionContext.java
+++ b/core/src/main/java/io/questdb/griffin/SqlExecutionContext.java
@@ -52,6 +52,10 @@
 
 public interface SqlExecutionContext extends Sinkable, Closeable {
 
+    // Returns true when the context doesn't require all SQL functions to be deterministic.
+    // Deterministic-only functions are enforced e.g. when compiling a mat view.
+    boolean allowNonDeterministicFunctions();
+
     void clearWindowContext();
 
     @Override
@@ -208,6 +212,8 @@ default void overrideWhereIntrinsics(TableToken tableToken, IntrinsicModel intri
 
     void resetFlags();
 
+    void setAllowNonDeterministicFunction(boolean value);
+
     void setCacheHit(boolean value);
 
     void setCancelledFlag(AtomicBoolean cancelled);
diff --git a/core/src/main/java/io/questdb/griffin/SqlExecutionContextImpl.java b/core/src/main/java/io/questdb/griffin/SqlExecutionContextImpl.java
index ed9c5af2147a..733d5407f064 100644
--- a/core/src/main/java/io/questdb/griffin/SqlExecutionContextImpl.java
+++ b/core/src/main/java/io/questdb/griffin/SqlExecutionContextImpl.java
@@ -59,11 +59,13 @@ public class SqlExecutionContextImpl implements SqlExecutionContext {
     private final IntStack timestampRequiredStack = new IntStack();
     private final WindowContextImpl windowContext = new WindowContextImpl();
     private final int workerCount;
-    private BindVariableService bindVariableService;
-    private boolean cacheHit = false;
+    protected BindVariableService bindVariableService;
+    protected SecurityContext securityContext;
+    private boolean allowNonDeterministicFunction = true;
+    private boolean cacheHit;
     private SqlExecutionCircuitBreaker circuitBreaker = SqlExecutionCircuitBreaker.NOOP_CIRCUIT_BREAKER;
     private MicrosecondClock clock;
-    private boolean cloneSymbolTables = false;
+    private boolean cloneSymbolTables;
     private boolean columnPreTouchEnabled = true;
     private boolean columnPreTouchEnabledOverride = true;
     private boolean containsSecret;
@@ -75,7 +77,6 @@ public class SqlExecutionContextImpl implements SqlExecutionContext {
     private boolean parallelReadParquetEnabled;
     private Rnd random;
     private long requestFd = -1;
-    private SecurityContext securityContext;
     private boolean useSimpleCircuitBreaker;
 
     public SqlExecutionContextImpl(CairoEngine cairoEngine, int workerCount, int sharedWorkerCount) {
@@ -103,6 +104,11 @@ public SqlExecutionContextImpl(CairoEngine cairoEngine, int workerCount) {
         this(cairoEngine, workerCount, workerCount);
     }
 
+    @Override
+    public boolean allowNonDeterministicFunctions() {
+        return allowNonDeterministicFunction;
+    }
+
     @Override
     public void clearWindowContext() {
         windowContext.clear();
@@ -299,6 +305,12 @@ public void resetFlags() {
         this.cacheHit = false;
         this.columnPreTouchEnabled = true;
         this.columnPreTouchEnabledOverride = true;
+        this.allowNonDeterministicFunction = true;
+    }
+
+    @Override
+    public void setAllowNonDeterministicFunction(boolean value) {
+        this.allowNonDeterministicFunction = value;
     }
 
     @Override
diff --git a/core/src/main/java/io/questdb/griffin/engine/groupby/GroupByUtils.java b/core/src/main/java/io/questdb/griffin/engine/groupby/GroupByUtils.java
index 7107a960a33b..27204c66df87 100644
--- a/core/src/main/java/io/questdb/griffin/engine/groupby/GroupByUtils.java
+++ b/core/src/main/java/io/questdb/griffin/engine/groupby/GroupByUtils.java
@@ -122,10 +122,6 @@ public static void assembleGroupByFunctions(
                             executionContext
                     );
 
-                    if (model.isMatView() && function.isNonDeterministic()) {
-                        throw SqlException.nonDeterministicColumn(node.position, node.token);
-                    }
-
                     // record functions will have all model function, including consecutive duplicates
                     outRecordFunctions.add(function);
 
diff --git a/core/src/main/java/io/questdb/griffin/model/QueryModel.java b/core/src/main/java/io/questdb/griffin/model/QueryModel.java
index 1dcf1a807f99..393025ca3591 100644
--- a/core/src/main/java/io/questdb/griffin/model/QueryModel.java
+++ b/core/src/main/java/io/questdb/griffin/model/QueryModel.java
@@ -167,7 +167,6 @@ public class QueryModel implements Mutable, ExecutionModel, AliasTranslator, Sin
     //simple flag to mark when limit x,y in current model (part of query) is already taken care of by existing factories e.g. LimitedSizeSortedLightRecordCursorFactory
     //and doesn't need to be enforced by LimitRecordCursor. We need it to detect whether current factory implements limit from this or inner query .
     private boolean isLimitImplemented;
-    private boolean isMatViewModel;
     // A flag to mark intermediate SELECT translation models. Such models do not contain the full list of selected
     // columns (e.g. they lack virtual columns), so they should be skipped when rewriting positional ORDER BY.
     private boolean isSelectTranslation = false;
@@ -458,7 +457,6 @@ public void clear() {
         // TODO: replace booleans with an enum-like type: UPDATE/MAT_VIEW/INSERT_AS_SELECT/SELECT
         //  default is SELECT
         isUpdateModel = false;
-        isMatViewModel = false;
         modelType = ExecutionModel.QUERY;
         updateSetColumns.clear();
         updateTableColumnTypes.clear();
@@ -639,7 +637,6 @@ public boolean equals(Object o) {
                 && orderByAdviceMnemonic == that.orderByAdviceMnemonic
                 && tableId == that.tableId
                 && isUpdateModel == that.isUpdateModel
-                && isMatViewModel == that.isMatViewModel
                 && modelType == that.modelType
                 && artificialStar == that.artificialStar
                 && skipped == that.skipped
@@ -1069,7 +1066,7 @@ public int hashCode() {
                 isSelectTranslation, selectModelType, nestedModelIsSubQuery,
                 distinct, unionModel, setOperationType,
                 modelPosition, orderByAdviceMnemonic, tableId,
-                isUpdateModel, isMatViewModel, modelType, updateTableModel,
+                isUpdateModel, modelType, updateTableModel,
                 updateTableToken, artificialStar, fillFrom, fillStride, fillTo, fillValues, decls
         );
     }
@@ -1094,10 +1091,6 @@ public boolean isLimitImplemented() {
         return isLimitImplemented;
     }
 
-    public boolean isMatView() {
-        return isMatViewModel;
-    }
-
     public boolean isNestedModelIsSubQuery() {
         return nestedModelIsSubQuery;
     }
@@ -1329,10 +1322,6 @@ public void setForceBackwardScan(boolean forceBackwardScan) {
         this.forceBackwardScan = forceBackwardScan;
     }
 
-    public void setIsMatView(boolean isMatView) {
-        this.isMatViewModel = isMatView;
-    }
-
     public void setIsUpdate(boolean isUpdate) {
         this.isUpdateModel = isUpdate;
     }
diff --git a/core/src/test/java/io/questdb/test/cairo/mv/CreateMatViewTest.java b/core/src/test/java/io/questdb/test/cairo/mv/CreateMatViewTest.java
index 506cccc62426..b419368deab1 100644
--- a/core/src/test/java/io/questdb/test/cairo/mv/CreateMatViewTest.java
+++ b/core/src/test/java/io/questdb/test/cairo/mv/CreateMatViewTest.java
@@ -613,6 +613,71 @@ public void testCreateMatViewNoTimestampInSelect() throws Exception {
 
     @Test
     public void testCreateMatViewNonDeterministicFunction() throws Exception {
+        assertMemoryLeak(() -> {
+            createTable(TABLE1);
+            createTable(TABLE2);
+
+            // nested in SELECT
+            assertExceptionNoLeakCheck(
+                    "create materialized view test as select coalesce(ts, now()) ts, avg(v) from " + TABLE1 + " sample by 30s",
+                    53,
+                    "non-deterministic function cannot be used in materialized view: now"
+            );
+            assertNull(getMatViewDefinition("test"));
+
+            // WHERE clause
+            assertExceptionNoLeakCheck(
+                    "create materialized view test as (select ts, avg(v) from " + TABLE1 + " where ts in today() sample by 30s) partition by month",
+                    76,
+                    "non-deterministic function cannot be used in materialized view: today"
+            );
+            assertNull(getMatViewDefinition("test"));
+
+            assertExceptionNoLeakCheck(
+                    "create materialized view test as select ts, avg(v) from " + TABLE1 + " where k is not null and now() = '2020-01-01' sample by 30s",
+                    87,
+                    "non-deterministic function cannot be used in materialized view: now"
+            );
+            assertNull(getMatViewDefinition("test"));
+
+            // IN (cursor)
+            assertExceptionNoLeakCheck(
+                    "create materialized view test as select ts, avg(v) from " + TABLE1 + " where ts in (select now()) sample by 30s",
+                    83,
+                    "non-deterministic function cannot be used in materialized view: now"
+            );
+            assertNull(getMatViewDefinition("test"));
+
+            // JOIN
+            assertExceptionNoLeakCheck(
+                    "create materialized view test with base " + TABLE1 + " as select t1.ts, max(t2.dt) from " + TABLE1 + " t1 " +
+                            "left outer join (select k, sysdate() dt from " + TABLE2 + ") t2 on (k) sample by 30s",
+                    117,
+                    "non-deterministic function cannot be used in materialized view: sysdate"
+            );
+            assertNull(getMatViewDefinition("test"));
+
+            // UNION
+            assertExceptionNoLeakCheck(
+                    "create materialized view test with base " + TABLE1 + " as select t1.ts, max(t2.ts) from " + TABLE1 + " t1 " +
+                            "left outer join (select 'a' k, '2020-01-01' ts union all select 'b' k, systimestamp() ts) t2 on (k) sample by 30s",
+                    161,
+                    "non-deterministic function cannot be used in materialized view: systimestamp"
+            );
+            assertNull(getMatViewDefinition("test"));
+
+            // ORDER BY
+            assertExceptionNoLeakCheck(
+                    "create materialized view test as (select ts, avg(v) from " + TABLE1 + " sample by 30s order by rnd_int(), ts)",
+                    87,
+                    "non-deterministic function cannot be used in materialized view: rnd_int"
+            );
+            assertNull(getMatViewDefinition("test"));
+        });
+    }
+
+    @Test
+    public void testCreateMatViewNonDeterministicFunctionInSelect() throws Exception {
         final String[][] functions = new String[][]{
                 {"sysdate()", "sysdate"},
                 {"systimestamp()", "systimestamp"},
@@ -654,9 +719,11 @@ public void testCreateMatViewNonDeterministicFunction() throws Exception {
                 {"rnd_geohash(5)", "rnd_geohash"}
         };
 
-        for (String[] func : functions) {
-            testCreateMatViewNonDeterministicFunction(func[0], func[1]);
-        }
+        assertMemoryLeak(() -> {
+            for (String[] func : functions) {
+                testCreateMatViewNonDeterministicFunctionInSelect(func[0], func[1]);
+            }
+        });
     }
 
     @Test
@@ -1901,16 +1968,22 @@ private void testCreateMatViewNoPartitionBy(int samplingInterval, char samplingI
         assertMatViewMetadata(matViewName, query, TABLE1, samplingInterval, samplingIntervalUnit);
     }
 
-    private void testCreateMatViewNonDeterministicFunction(String func, String columnName) throws Exception {
-        assertMemoryLeak(() -> {
-            createTable(TABLE1);
-            assertExceptionNoLeakCheck(
-                    "create materialized view test as (select ts, " + func + ", avg(v) from " + TABLE1 + " sample by 30s) partition by month",
-                    45,
-                    "Non-deterministic column: " + columnName
-            );
-            assertNull(getMatViewDefinition("test"));
-        });
+    private void testCreateMatViewNonDeterministicFunctionInSelect(String func, String columnName) throws Exception {
+        createTable(TABLE1);
+
+        assertExceptionNoLeakCheck(
+                "create materialized view test as (select ts, " + func + ", avg(v) from " + TABLE1 + " sample by 30s) partition by month",
+                45,
+                "non-deterministic function cannot be used in materialized view: " + columnName
+        );
+        assertNull(getMatViewDefinition("test"));
+
+        assertExceptionNoLeakCheck(
+                "create materialized view test as (select ts, f, a from (select ts, " + func + " f, avg(v) a from " + TABLE1 + " sample by 30s)) partition by month",
+                67,
+                "non-deterministic function cannot be used in materialized view: " + columnName
+        );
+        assertNull(getMatViewDefinition("test"));
     }
 
     private void testCreateMatViewWithNonDedupBaseKeys(String[] queries, String errorMessage, int[] errorPositions) throws Exception {
diff --git a/core/src/test/java/io/questdb/test/cairo/mv/MatViewTest.java b/core/src/test/java/io/questdb/test/cairo/mv/MatViewTest.java
index ed2fd33aef13..25331ee3f406 100644
--- a/core/src/test/java/io/questdb/test/cairo/mv/MatViewTest.java
+++ b/core/src/test/java/io/questdb/test/cairo/mv/MatViewTest.java
@@ -36,6 +36,7 @@
 import io.questdb.cairo.wal.WalUtils;
 import io.questdb.griffin.SqlCompiler;
 import io.questdb.griffin.SqlException;
+import io.questdb.griffin.SqlExecutionContextImpl;
 import io.questdb.griffin.engine.functions.test.TestTimestampCounterFactory;
 import io.questdb.mp.SOCountDownLatch;
 import io.questdb.std.Files;
@@ -1113,6 +1114,46 @@ public void testCreateMatViewLoop() throws Exception {
         });
     }
 
+    @Test
+    public void testCreateMatViewNonDeterministicFunctionCompatibility() throws Exception {
+        // Verifies that even if someone was able to create a mat view with non-deterministic function
+        // on an older version, it'll be marked as invalid on the next refresh.
+        assertMemoryLeak(() -> {
+            execute(
+                    "create table base_price (" +
+                            "sym varchar, price double, ts timestamp" +
+                            ") timestamp(ts) partition by DAY WAL"
+            );
+
+            try (var ctx = new SqlExecutionContextImpl(engine, 1) {
+                @Override
+                public boolean allowNonDeterministicFunctions() {
+                    return true;
+                }
+            }) {
+                ctx.with(sqlExecutionContext.getSecurityContext(), bindVariableService, sqlExecutionContext.getRandom(), sqlExecutionContext.getRequestFd(), circuitBreaker);
+                execute("create materialized view price_1h as select sym, last(price) as price, ts from base_price where ts in today() sample by 42h", ctx);
+            }
+
+            execute(
+                    "insert into base_price values('gbpusd', 1.320, '2024-09-10T12:01')" +
+                            ",('gbpusd', 1.323, '2024-09-10T12:02')" +
+                            ",('jpyusd', 103.21, '2024-09-10T12:02')" +
+                            ",('gbpusd', 1.321, '2024-09-10T13:02')"
+            );
+            currentMicros = parseFloorPartialTimestamp("2023-01-01T01:01:01.123456Z");
+            drainQueues();
+
+            assertQueryNoLeakCheck(
+                    "view_name\trefresh_type\tbase_table_name\tlast_refresh_start_timestamp\tlast_refresh_finish_timestamp\tview_sql\tview_table_dir_name\tinvalidation_reason\tview_status\trefresh_base_table_txn\tbase_table_txn\trefresh_limit_value\trefresh_limit_unit\n" +
+                            "price_1h\tincremental\tbase_price\t2023-01-01T01:01:01.123456Z\t2023-01-01T01:01:01.123456Z\tselect sym, last(price) as price, ts from base_price where ts in today() sample by 42h\tprice_1h~2\t[65]: non-deterministic function cannot be used in materialized view: today\tinvalid\t-1\t1\t0\tHOUR\n",
+                    "materialized_views",
+                    null,
+                    false
+            );
+        });
+    }
+
     @Test
     public void testDisableParallelSqlExecution() throws Exception {
         setProperty(PropertyKey.CAIRO_MAT_VIEW_PARALLEL_SQL_ENABLED, "false");
@@ -1935,7 +1976,7 @@ public void testQueryWithCte() throws Exception {
             final String viewSql = "with starting_point as ( " +
                     "  select ts from aux_start_date " +
                     "  union " +
-                    "  select interval_start(yesterday()) " +
+                    "  select '2024-09-10' " +
                     "), " +
                     "latest_query as ( " +
                     "  select * " +
diff --git a/core/src/test/java/io/questdb/test/cutlass/text/SqlExecutionContextStub.java b/core/src/test/java/io/questdb/test/cutlass/text/SqlExecutionContextStub.java
index 681eb963c38d..dcaf72bf5e9c 100644
--- a/core/src/test/java/io/questdb/test/cutlass/text/SqlExecutionContextStub.java
+++ b/core/src/test/java/io/questdb/test/cutlass/text/SqlExecutionContextStub.java
@@ -47,6 +47,11 @@ public SqlExecutionContextStub(@NotNull CairoEngine engine) {
         this.engine = engine;
     }
 
+    @Override
+    public boolean allowNonDeterministicFunctions() {
+        return true;
+    }
+
     @Override
     public void clearWindowContext() {
     }
@@ -199,6 +204,10 @@ public void pushTimestampRequiredFlag(boolean flag) {
     public void resetFlags() {
     }
 
+    @Override
+    public void setAllowNonDeterministicFunction(boolean value) {
+    }
+
     @Override
     public void setCacheHit(boolean value) {
     }
diff --git a/core/src/test/java/io/questdb/test/griffin/ExplainPlanTest.java b/core/src/test/java/io/questdb/test/griffin/ExplainPlanTest.java
index bdc9514de2ea..78ce7eac4bdf 100644
--- a/core/src/test/java/io/questdb/test/griffin/ExplainPlanTest.java
+++ b/core/src/test/java/io/questdb/test/griffin/ExplainPlanTest.java
@@ -3333,7 +3333,8 @@ public void testGroupBySymbolWithSubQueryFilter() throws Exception {
                         "  keys: [s]\n" +
                         "  values: [avg(l)]\n" +
                         "  filter: s in cursor \n" +
-                        "    Filter filter: s='key'\n" +
+                        "    Async JIT Filter workers: 1\n" +
+                        "      filter: s='key' [pre-touch]\n" +
                         "        PageFrame\n" +
                         "            Row forward scan\n" +
                         "            Frame forward scan on: a [state-shared]\n" +
diff --git a/core/src/test/java/io/questdb/test/griffin/engine/table/AsyncFilteredRecordCursorFactoryTest.java b/core/src/test/java/io/questdb/test/griffin/engine/table/AsyncFilteredRecordCursorFactoryTest.java
index a1a1edf07893..da2b34c68ed9 100644
--- a/core/src/test/java/io/questdb/test/griffin/engine/table/AsyncFilteredRecordCursorFactoryTest.java
+++ b/core/src/test/java/io/questdb/test/griffin/engine/table/AsyncFilteredRecordCursorFactoryTest.java
@@ -976,53 +976,53 @@ private void withDoublePool(CustomisableRunnable runnable) throws Exception {
             TestUtils.setupWorkerPool(sharedPool, engine);
             sharedPool.start();
 
-            final WorkerPool stealingPool = new TestWorkerPool("pool1", stealingPoolWorkerCount);
-
-            SOCountDownLatch doneLatch = new SOCountDownLatch(1);
-
-            stealingPool.assign(new SynchronizedJob() {
-                boolean run = true;
-
-                @Override
-                protected boolean runSerially() {
-                    if (run) {
-                        try {
-                            try (SqlCompiler compiler = engine.getSqlCompiler()) {
-                                runnable.run(
-                                        engine, compiler, new DelegatingSqlExecutionContext() {
-                                            @Override
-                                            public Rnd getRandom() {
-                                                return rnd;
+            try (final WorkerPool stealingPool = new TestWorkerPool("pool1", stealingPoolWorkerCount)) {
+
+                SOCountDownLatch doneLatch = new SOCountDownLatch(1);
+
+                stealingPool.assign(new SynchronizedJob() {
+                    boolean run = true;
+
+                    @Override
+                    protected boolean runSerially() {
+                        if (run) {
+                            try {
+                                try (SqlCompiler compiler = engine.getSqlCompiler()) {
+                                    runnable.run(
+                                            engine, compiler, new DelegatingSqlExecutionContext() {
+                                                @Override
+                                                public Rnd getRandom() {
+                                                    return rnd;
+                                                }
+
+                                                @Override
+                                                public int getWorkerCount() {
+                                                    return sharedPoolWorkerCount;
+                                                }
                                             }
-
-                                            @Override
-                                            public int getWorkerCount() {
-                                                return sharedPoolWorkerCount;
-                                            }
-                                        }
-                                );
+                                    );
+                                }
+                            } catch (Throwable e) {
+                                e.printStackTrace(System.out);
+                                errorCounter.incrementAndGet();
+                            } finally {
+                                doneLatch.countDown();
+                                run = false;
                             }
-                        } catch (Throwable e) {
-                            e.printStackTrace(System.out);
-                            errorCounter.incrementAndGet();
-                        } finally {
-                            doneLatch.countDown();
-                            run = false;
+                            return true;
                         }
-                        return true;
+                        return false;
                     }
-                    return false;
-                }
-            });
+                });
 
-            stealingPool.start();
+                stealingPool.start();
 
-            try {
-                doneLatch.await();
-                Assert.assertEquals(0, errorCounter.get());
-            } finally {
-                sharedPool.halt();
-                stealingPool.halt();
+                try {
+                    doneLatch.await();
+                    Assert.assertEquals(0, errorCounter.get());
+                } finally {
+                    sharedPool.halt();
+                }
             }
         });
     }
@@ -1086,6 +1086,11 @@ public int getWorkerCount() {
 
     private static abstract class DelegatingSqlExecutionContext implements SqlExecutionContext {
 
+        @Override
+        public boolean allowNonDeterministicFunctions() {
+            return sqlExecutionContext.allowNonDeterministicFunctions();
+        }
+
         @Override
         public void clearWindowContext() {
             sqlExecutionContext.clearWindowContext();
@@ -1262,6 +1267,11 @@ public void resetFlags() {
             sqlExecutionContext.resetFlags();
         }
 
+        @Override
+        public void setAllowNonDeterministicFunction(boolean value) {
+            sqlExecutionContext.setAllowNonDeterministicFunction(value);
+        }
+
         @Override
         public void setCacheHit(boolean value) {
             sqlExecutionContext.setCacheHit(value);


Reviews:
user: login: ideoma
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-05-07T09:40:10Z
user: login: puzpuzpuz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T10:04:23Z
user: login: puzpuzpuz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T10:04:44Z
user: login: bluestreak01
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-10T00:34:55Z

Review comments:
diff_hunk: @@ -104,6 +104,11 @@ public TableReader getReader(TableToken tableToken) {
         return getCairoEngine().getReader(tableToken);
     }
 
+    @Override
+    public boolean isDeterministic() {
path: core/src/main/java/io/questdb/cairo/mv/MatViewRefreshExecutionContext.java
user: login: ideoma
body: nit: I'd make it the same naming as functions with non-deterministic wording, e.g. `allowNonDeterministic`. The default value of `true` is also more obvious for `allow` property.
created_at: 2025-05-07T09:25:35Z
author_association: COLLABORATOR
diff_hunk: @@ -579,6 +579,9 @@ private Function checkAndCreateFunction(
                     .I$();
             Misc.freeObjList(args);
             throw SqlException.position(position).put("bad function factory (NULL), check log");
+        } else if (sqlExecutionContext.isDeterministic() && function.isNonDeterministic()) {
path: core/src/main/java/io/questdb/griffin/FunctionParser.java
user: login: ideoma
body: nit: this doesn't look very clear with the current naming pattern.
created_at: 2025-05-07T09:27:43Z
author_association: COLLABORATOR
diff_hunk: @@ -104,6 +104,11 @@ public TableReader getReader(TableToken tableToken) {
         return getCairoEngine().getReader(tableToken);
     }
 
+    @Override
+    public boolean isDeterministic() {
path: core/src/main/java/io/questdb/cairo/mv/MatViewRefreshExecutionContext.java
user: login: puzpuzpuz
body: Renamed in 245eb3b35c7cc5589c94c1384105ea28d71b7061
created_at: 2025-05-07T10:04:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -579,6 +579,9 @@ private Function checkAndCreateFunction(
                     .I$();
             Misc.freeObjList(args);
             throw SqlException.position(position).put("bad function factory (NULL), check log");
+        } else if (sqlExecutionContext.isDeterministic() && function.isNonDeterministic()) {
path: core/src/main/java/io/questdb/griffin/FunctionParser.java
user: login: puzpuzpuz
body: Improved in 245eb3b35c7cc5589c94c1384105ea28d71b7061
created_at: 2025-05-07T10:04:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: puzpuzpuz
created_at: 2025-05-08T13:58:53Z
body: @ideoma could you take one more looks at this one?
author_association: CONTRIBUTOR
user: login: glasstiger
created_at: 2025-05-10T00:58:28Z
body: #### [PR Coverage check]

:heart_eyes: **pass** : 22 / 24 (91.67%)




#### file detail

|   |path|covered line|new line|coverage|
|----|----|----|----|----|
|:large_blue_circle:|io/questdb/griffin/model/QueryModel.java|0|1|00.00%|
|:large_blue_circle:|io/questdb/cairo/mv/MatViewRefreshExecutionContext.java|7|8|87.50%|
|:large_blue_circle:|io/questdb/griffin/FunctionParser.java|6|6|100.00%|
|:large_blue_circle:|io/questdb/griffin/SqlExecutionContextImpl.java|5|5|100.00%|
|:large_blue_circle:|io/questdb/griffin/SqlException.java|1|1|100.00%|
|:large_blue_circle:|io/questdb/griffin/SqlCompilerImpl.java|3|3|100.00%|



author_association: CONTRIBUTOR