Skip to content

[Bug] Table-model UDFs fail on TAG/ATTRIBUTE predicates, SHOW/COUNT DEVICES and UPDATE since 2.0.11 #19178

Open

Description

@PDGGK

Search before asking

  • I searched in the issues and found nothing similar.

Version

The official apache/iotdb:2.0.11-standalone Docker image (2.0.11, build 7c0de06, linux/arm64), and master at d8800f1 built from source. The same steps work on apache/iotdb:2.0.10-standalone (build 3c26382).

Describe the bug and provide the minimal reproduce step

A user-defined table scalar function fails when it is used on a TAG or ATTRIBUTE column in WHERE, in SHOW DEVICES / COUNT DEVICES ... WHERE, or in UPDATE ... SET. The same function works in the SELECT list, and the built-in upper() works in all of these places.

The function only returns its argument:

package lazy;

import org.apache.iotdb.udf.api.customizer.analysis.ScalarFunctionAnalysis;
import org.apache.iotdb.udf.api.customizer.parameter.FunctionArguments;
import org.apache.iotdb.udf.api.relational.ScalarFunction;
import org.apache.iotdb.udf.api.relational.access.Record;
import org.apache.iotdb.udf.api.type.Type;

public class StrF implements ScalarFunction {
  @Override
  public ScalarFunctionAnalysis analyze(FunctionArguments arguments) {
    return new ScalarFunctionAnalysis.Builder().outputDataType(Type.STRING).build();
  }

  @Override
  public Object evaluate(Record input) {
    return input.isNull(0) ? null : input.getBinary(0);
  }
}
CREATE DATABASE db;
CREATE TABLE db.t (device_id STRING TAG, attr1 STRING ATTRIBUTE, v INT64 FIELD);
INSERT INTO db.t(time, device_id, attr1, v) VALUES (1,'d1','a',1),(2,'d1','a',2),(1,'d2','b',100);
CREATE FUNCTION str_f AS 'lazy.StrF' USING URI 'file:/tmp/lazyudf.jar';

SELECT time, device_id, str_f(device_id) FROM db.t ORDER BY device_id, time;
SELECT count(*) FROM db.t WHERE str_f(device_id) = 'd1';
SELECT count(*) FROM db.t WHERE str_f(attr1) = 'a';
SHOW DEVICES FROM db.t WHERE str_f(device_id) = 'd1';
COUNT DEVICES FROM db.t WHERE str_f(device_id) = 'd1';
UPDATE db.t SET attr1 = str_f(device_id) WHERE device_id = 'd2';

What did you expect to see?

What 2.0.10 returns: the first SELECT returns the three rows, both count(*) queries return 2, SHOW DEVICES returns d1 | a, COUNT DEVICES returns 1, and the UPDATE succeeds and sets attr1 of d2 to d2.

What did you see instead?

On 2.0.11 and master the first SELECT still works, but the rest fail:

SELECT count(*) ... WHERE str_f(device_id) = 'd1'   301: outerQueryDeadlineMs must be positive for UDF execution
SELECT count(*) ... WHERE str_f(attr1) = 'a'        301: outerQueryDeadlineMs must be positive for UDF execution
SHOW DEVICES ... WHERE str_f(device_id) = 'd1'      301: outerQueryDeadlineMs must be positive for UDF execution
COUNT DEVICES ... WHERE str_f(device_id) = 'd1'     301: IoTDBLocalFactory must not be null for UDF execution
UPDATE db.t SET attr1 = str_f(device_id) ...        305: null

For the UPDATE, the DataNode log shows IoTDBLocalFactory must not be null for UDF execution, thrown from SchemaRegionMemoryImpl#constructDevicePredicateUpdater under SchemaRegionStateMachine#write. On 2.0.11, the same statements with upper() in place of str_f all succeed.

Anything else?

The messages come from TableOperatorGenerator.IoTDBLocalFactory#createIoTDBLocal, added with IoTDBLocal in #18035. UserDefineScalarFunctionTransformer calls it in its constructor, so every user-defined scalar function needs a factory, a fragment instance id, a query id and a positive deadline, even when it never uses IoTDBLocal.

  • For schema fragments, FragmentInstanceManager#execSchemaQueryFragmentInstance creates the FragmentInstanceContext without a deadline, so it stays at -1. The data path passes DriverTask.computeDeadlineTimeInMs(instance.getTimeOut()).
  • COUNT DEVICES builds its transformer context in DataNodeTableOperatorGenerator#visitTableDeviceQueryCount without a factory.
  • UPDATE builds its transformers inside SchemaRegionStateMachine#write, where there is no query.

The UPDATE case also came up in a review comment on #18889 (#18889 (comment)), and I ran into the rest while testing #19107.

I can see two directions: give schema fragments the same deadline and factory as data fragments, and create IoTDBLocal only when a function actually uses it, with a clear error where there is no query, as in UPDATE. I'm happy to send a PR once it's clear which one is preferred.

Are you willing to submit a PR?

  • I'm willing to submit a PR!

Activity

  1. shuwenwei commented on Nov 9, 2026

    @shuwenwei
    Member

    Thanks for the detailed analysis and reproduction. We agree that allowing UDFs in UPDATE exposes a broader correctness problem than the missing QueryScope alone.

    The current schema WAL stores the raw TableDeviceAttributeUpdateNode and replays it by re-evaluating the UDF expression. This means:

    • after DROP FUNCTION, or if the UDF jar is missing, replay can fail;
    • if the same function name is re-created with a different implementation, replay may silently execute different code;
    • a non-deterministic UDF can produce different values after restart;
    • different replicas may evaluate the same plan differently, so cluster availability during replay does not by itself guarantee consistent results.

    A snapshot triggered by DROP FUNCTION is not a complete fix. It would require a cluster-wide barrier, all affected replicas to reach a consistent snapshot/log boundary before removing the UDF or jar, and it would still not solve non-deterministic re-evaluation while the function remains registered.

    For the short term, we plan to reject external UDFs in UPDATE assignments and predicates at analysis time, while keeping built-in functions and non-UDF UPDATE statements working. This is intended as a correctness guard rather than the final design: supporting UDFs in UPDATE later would require deterministic, materialized WAL entries containing the actual device/attribute changes rather than an expression that must be evaluated again during recovery.

    This does not address the remaining non-UPDATE paths reported in #19178, such as TAG/ATTRIBUTE predicates and SHOW DEVICES / COUNT DEVICES; those need separate IoTDBLocal/context handling. #19107 will remain focused on the classloader lifetime problem.

    Thanks for offering the jar and script. A minimal reproducer would also be useful as a regression case for the rejection behavior.

  2. shuwenwei commented on Nov 9, 2026

    @shuwenwei
    Member

    Thanks again for the detailed report and for validating #19107.

    For the non-UPDATE cases in this issue, classloader lifetime is not the only issue. #19107 already adds a QueryScope around schema fragment planning, but these paths still have separate query-context requirements that need to be handled independently.

    Do you still plan to submit a separate PR for these non-UPDATE cases?

  3. PDGGK commented on Nov 9, 2026

    @PDGGK
    ContributorAuthor

    Yes, it's up as #19195. Schema fragments now get the same deadline as data fragments, and COUNT DEVICES passes the IoTDBLocal context to its filter, so the TAG/ATTRIBUTE predicates and SHOW/COUNT DEVICES from this issue work, with or without IoTDBLocal. UPDATE isn't touched.

    For the regression case on the rejection, you only need a scalar UDF and the table from the description: the StrF class there, or DeviceNameFunction from the IT module, which also takes and returns a string. These are the forms I'd cover. All three fail with 305: null today, on master and with the PR:

    UPDATE db.t SET attr1 = str_f(device_id) WHERE device_id = 'd2';
    UPDATE db.t SET attr1 = 'x' WHERE str_f(device_id) = 'd2';
    UPDATE db.t SET attr1 = upper(str_f(device_id)) WHERE device_id = 'd2';

    UPDATE db.t SET attr1 = upper(device_id) WHERE device_id = 'd2' works and should keep working. DELETE DEVICES already rejects a UDF at analysis time in all eight forms I tried, so that one seems fine.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions