Skip to content

[core] Validate aggregate function types at DDL time - #10983

Open
ZZZxDong wants to merge 1 commit into
apache:masterfrom
ZZZxDong:fix/aggregate-type-check-on-ddl
Open

ZZZxDong wants to merge 1 commit into
apache:masterfrom
ZZZxDong:fix/aggregate-type-check-on-ddl

Conversation

@ZZZxDong

@ZZZxDong ZZZxDong commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Problem

CREATE TABLE accepts an aggregate-function that is incompatible with the column type, and the failure only surfaces later on the data path:

CREATE TABLE t (
  k    STRING,
  tags STRING,   -- not ARRAY
  PRIMARY KEY (k) NOT ENFORCED
) WITH (
  'merge-engine' = 'aggregation',
  'fields.tags.aggregate-function' = 'collect'
);
-- succeeds

Any read, write, or compaction on that table then fails deterministically with:

Data type for collect column must be 'Array' but was 'STRING'.

The cause is that SchemaValidation#validateMergeFunctionFactory only calls FactoryUtil.discoverFactory, i.e. it verifies that the aggregation function name exists, but never calls FieldAggregatorFactory#create, which is where the type checks live. So DDL only proves the merge engine is valid and the function name is spelled correctly.

The same gap applies to the other type-checking aggregators (sum, product, listagg, merge_map, hll_sketch, theta_sketch, roaringbitmap32/64, bool_and/bool_or, nested_update, nested_partial_update). It is also present for partial-update: PartialUpdateMergeFunction wraps each aggregator in a Supplier, so even instantiating the merge function does not trigger the check.

Because the resulting IllegalArgumentException is deterministic and maps to HTTP 400 in REST catalogs, an invalid table is effectively unreadable until someone fixes its options or drops it.

Fix

Instantiate each aggregation factory once during schema validation, so type-incompatible configurations are rejected at CREATE TABLE / ALTER TABLE time, reusing the existing factory error messages:

  • aggregation: resolve the function through AggregateMergeFunction#getAggFuncName (primary key columns map to primary_key, sequence fields to last_value, otherwise field-level option then default), then instantiate.
  • partial-update / versioned-partial-update: skip sequence fields, map primary key columns to primary_key, otherwise use the field-level option or the default. Fields that never get an aggregator are skipped: sequence-group ordering fields for partial-update, and multi-version columns for versioned-partial-update.

No behavior change for valid tables; the check only constructs the object that the read/write path would have constructed anyway.

Tests

  • collect / sum on a STRING column now fails at DDL time for both aggregation and partial-update.
  • Compatible types (collect on ARRAY<STRING>, sum on a numeric column) still pass.
  • A partial-update sequence-group ordering field configured with an incompatible aggregation function is not rejected, since it never gets an aggregator.
  • SchemaManagerTest#testResetSequenceGroupForAggregateFunction used a VARCHAR column with sum; the column type is corrected so the fixture is valid.

@ZZZxDong
ZZZxDong force-pushed the fix/aggregate-type-check-on-ddl branch 2 times, most recently from 2528750 to 1951444 Compare November 4, 2026 06:49

@JunRuiLee JunRuiLee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@ZZZxDong
ZZZxDong force-pushed the fix/aggregate-type-check-on-ddl branch from 1951444 to 9528cd9 Compare November 5, 2026 02:38

@JingsongLi JingsongLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rejecting an incompatible aggregator at DDL time is valuable. The local JDK 8 reactor passed all 229 selected schema, aggregate and partial-update tests. A separate time-travel test passed on the exact base and failed on this head. The upstream Core failure is the separate CSV regression tracked in #10998.

}
// Instantiate once so that type-incompatible configurations (e.g. 'collect' on a
// non-array column) are rejected at DDL time instead of at read/write time.
FieldAggregatorFactory.create(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Keep valid historical projections readable after aggregate schema evolution

AbstractFileStoreTable.runtimeStore() also runs this check, before the read projection is applied. I created a primary-key partial-update table with label VARCHAR(16) protected by fields.seq.sequence-group=label, wrote snapshot 1, and widened label to STRING while enabling listagg through ALTER. Both DDL operations succeed. A read of only id at snapshot 1 passes on the base and now fails before planning with list agg column must be STRING ... VARCHAR(16), because time travel combines the old type with current options. The same eager check also blocks previously working projections on pre-upgrade tables with incompatible aggregators on unused columns. Please run the eager type check on CREATE/ALTER and preserve the runtime projection behavior for stored schemas. Add the historical projection as a regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — I'll restrict the eager check to the CREATE/ALTER schema-commit path so reads of stored schemas keep the previous projection-scoped behavior, and add a regression test for the time-travel projection you described.

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