From 2e3e2f87b913917b0fe354695d1bd7efe4418346 Mon Sep 17 00:00:00 2001 From: Niels Pardon Date: Thu, 4 Jun 2026 17:31:37 +0200 Subject: [PATCH 1/4] feat(extensions): support deprecation info in extensions Substrait v0.86.0 added the ability to mark simple-extension entries as deprecated (substrait-io/substrait#1014). An optional `deprecated` object (`since`, optional `reason`, optional `metadata`) can appear on types, scalar/aggregate/window functions, and individual function implementations. Parse and expose this information so tooling can surface deprecation warnings, mirroring the existing `metadata` support. Function-level deprecation propagates to each resolved variant; an impl-level deprecation takes precedence over the parent function's when both are present. Type variations are not modeled in substrait-java's extension schema, so type-variation deprecation is out of scope. Closes #811 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../substrait/extension/SimpleExtension.java | 66 +++++++++- .../extension/DeprecationExtensionTest.java | 117 ++++++++++++++++++ .../extensions/deprecation_extensions.yaml | 57 +++++++++ 3 files changed, 234 insertions(+), 6 deletions(-) create mode 100644 core/src/test/java/io/substrait/extension/DeprecationExtensionTest.java create mode 100644 core/src/test/resources/extensions/deprecation_extensions.yaml diff --git a/core/src/main/java/io/substrait/extension/SimpleExtension.java b/core/src/main/java/io/substrait/extension/SimpleExtension.java index 1b3f05820..a1434aa57 100644 --- a/core/src/main/java/io/substrait/extension/SimpleExtension.java +++ b/core/src/main/java/io/substrait/extension/SimpleExtension.java @@ -122,6 +122,32 @@ public interface Option { List getValues(); } + /** + * Deprecation information for an extension entry (type, function, or function implementation). + * See substrait#1014. + * + *

Consumers of extension files are not required to understand or validate deprecation fields; + * the information is provided so tooling can surface deprecation warnings. + */ + @JsonDeserialize(as = ImmutableSimpleExtension.DeprecationStatus.class) + @JsonSerialize(as = ImmutableSimpleExtension.DeprecationStatus.class) + @JsonIgnoreProperties(ignoreUnknown = true) + @Value.Immutable + public interface DeprecationStatus { + /** + * The version at which the entry was deprecated, as a core semantic version string (e.g. {@code + * "1.2.0"}). + */ + @JsonProperty(required = true) + String since(); + + /** Optional human-readable description of why the entry was deprecated. */ + Optional reason(); + + /** Optional arbitrary data provided by the extension author. */ + Optional> metadata(); + } + @JsonSerialize(as = ImmutableSimpleExtension.ValueArgument.class) @JsonDeserialize(as = ImmutableSimpleExtension.ValueArgument.class) @Value.Immutable @@ -280,6 +306,8 @@ public String description() { public abstract Optional> metadata(); + public abstract Optional deprecated(); + public List requiredArguments() { return requiredArgsSupplier.get(); } @@ -387,10 +415,13 @@ public abstract static class ScalarFunction { public abstract Optional> metadata(); + public abstract Optional deprecated(); + public abstract List impls(); public Stream resolve(String urn) { - return impls().stream().map(f -> f.resolve(urn, name(), description(), metadata())); + return impls().stream() + .map(f -> f.resolve(urn, name(), description(), metadata(), deprecated())); } } @@ -399,7 +430,11 @@ public Stream resolve(String urn) { @Value.Immutable public abstract static class ScalarFunctionVariant extends Function { public ScalarFunctionVariant resolve( - String urn, String name, String description, Optional> metadata) { + String urn, + String name, + String description, + Optional> metadata, + Optional deprecated) { return ImmutableSimpleExtension.ScalarFunctionVariant.builder() .urn(urn) .name(name) @@ -408,6 +443,7 @@ public ScalarFunctionVariant resolve( .args(args()) .options(options()) .metadata(metadata) + .deprecated(deprecated().isPresent() ? deprecated() : deprecated) .ordered(ordered()) .variadic(variadic()) .returnType(returnType()) @@ -427,10 +463,13 @@ public abstract static class AggregateFunction { public abstract Optional> metadata(); + public abstract Optional deprecated(); + public abstract List impls(); public Stream resolve(String urn) { - return impls().stream().map(f -> f.resolve(urn, name(), description(), metadata())); + return impls().stream() + .map(f -> f.resolve(urn, name(), description(), metadata(), deprecated())); } } @@ -446,10 +485,13 @@ public abstract static class WindowFunction { public abstract Optional> metadata(); + public abstract Optional deprecated(); + public abstract List impls(); public Stream resolve(String urn) { - return impls().stream().map(f -> f.resolve(urn, name(), description(), metadata())); + return impls().stream() + .map(f -> f.resolve(urn, name(), description(), metadata(), deprecated())); } public static ImmutableSimpleExtension.WindowFunction.Builder builder() { @@ -476,7 +518,11 @@ public String toString() { public abstract TypeExpression intermediate(); AggregateFunctionVariant resolve( - String urn, String name, String description, Optional> metadata) { + String urn, + String name, + String description, + Optional> metadata, + Optional deprecated) { return ImmutableSimpleExtension.AggregateFunctionVariant.builder() .urn(urn) .name(name) @@ -485,6 +531,7 @@ AggregateFunctionVariant resolve( .args(args()) .options(options()) .metadata(metadata) + .deprecated(deprecated().isPresent() ? deprecated() : deprecated) .ordered(ordered()) .variadic(variadic()) .decomposability(decomposability()) @@ -520,7 +567,11 @@ public String toString() { } WindowFunctionVariant resolve( - String urn, String name, String description, Optional> metadata) { + String urn, + String name, + String description, + Optional> metadata, + Optional deprecated) { return ImmutableSimpleExtension.WindowFunctionVariant.builder() .urn(urn) .name(name) @@ -529,6 +580,7 @@ WindowFunctionVariant resolve( .args(args()) .options(options()) .metadata(metadata) + .deprecated(deprecated().isPresent() ? deprecated() : deprecated) .ordered(ordered()) .variadic(variadic()) .decomposability(decomposability()) @@ -567,6 +619,8 @@ public abstract static class Type { public abstract Optional> metadata(); + public abstract Optional deprecated(); + public TypeAnchor getAnchor() { return anchorSupplier.get(); } diff --git a/core/src/test/java/io/substrait/extension/DeprecationExtensionTest.java b/core/src/test/java/io/substrait/extension/DeprecationExtensionTest.java new file mode 100644 index 000000000..9b662f739 --- /dev/null +++ b/core/src/test/java/io/substrait/extension/DeprecationExtensionTest.java @@ -0,0 +1,117 @@ +package io.substrait.extension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.substrait.TestBase; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Verifies that deprecation information can be read from extension YAML files at multiple levels: + * + *

    + *
  • Type-level deprecation + *
  • Function-level deprecation (scalar, aggregate, window) + *
  • Function-implementation-level deprecation (individual overloads) + *
+ * + * See substrait#1014. + */ +class DeprecationExtensionTest extends TestBase { + + static final String URN = "extension:test:deprecation_extensions"; + static final SimpleExtension.ExtensionCollection DEPRECATION_EXTENSION; + + static { + try { + String extensionStr = asString("extensions/deprecation_extensions.yaml"); + DEPRECATION_EXTENSION = SimpleExtension.load(extensionStr); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + DeprecationExtensionTest() { + super(DEPRECATION_EXTENSION); + } + + @Test + void testTypeDeprecation() { + SimpleExtension.TypeAnchor anchor = SimpleExtension.TypeAnchor.of(URN, "deprecatedType"); + SimpleExtension.DeprecationStatus deprecation = + extensions.getType(anchor).deprecated().orElseThrow(); + assertEquals("0.50.0", deprecation.since()); + assertEquals("Replaced by newType", deprecation.reason().orElseThrow()); + } + + @Test + void testScalarFunctionDeprecation() { + SimpleExtension.FunctionAnchor anchor = + SimpleExtension.FunctionAnchor.of(URN, "deprecatedScalar:i64"); + SimpleExtension.DeprecationStatus deprecation = + extensions.getScalarFunction(anchor).deprecated().orElseThrow(); + assertEquals("0.1.1", deprecation.since()); + assertEquals("Use newScalar instead", deprecation.reason().orElseThrow()); + } + + @Test + void testScalarFunctionDeprecationWithMetadata() { + SimpleExtension.FunctionAnchor anchor = + SimpleExtension.FunctionAnchor.of(URN, "deprecatedScalarWithMetadata:i64"); + SimpleExtension.DeprecationStatus deprecation = + extensions.getScalarFunction(anchor).deprecated().orElseThrow(); + assertEquals("2.0.0", deprecation.since()); + Map metadata = deprecation.metadata().orElseThrow(); + assertEquals("newScalar", metadata.get("alternative")); + } + + @Test + void testAggregateFunctionDeprecation() { + SimpleExtension.FunctionAnchor anchor = + SimpleExtension.FunctionAnchor.of(URN, "deprecatedAggregate:i64"); + SimpleExtension.DeprecationStatus deprecation = + extensions.getAggregateFunction(anchor).deprecated().orElseThrow(); + assertEquals("1.2.0", deprecation.since()); + assertTrue(deprecation.reason().isEmpty()); + } + + @Test + void testWindowFunctionDeprecation() { + SimpleExtension.FunctionAnchor anchor = + SimpleExtension.FunctionAnchor.of(URN, "deprecatedWindow:i64"); + SimpleExtension.DeprecationStatus deprecation = + extensions.getWindowFunction(anchor).deprecated().orElseThrow(); + assertEquals("1.3.0", deprecation.since()); + } + + @Test + void testImplLevelDeprecation() { + // Only the i64 overload of evolvingScalar is deprecated; the fp32 overload is not. + SimpleExtension.DeprecationStatus deprecation = + extensions + .getScalarFunction(SimpleExtension.FunctionAnchor.of(URN, "evolvingScalar:i64")) + .deprecated() + .orElseThrow(); + assertEquals("3.1.0", deprecation.since()); + assertEquals("Use the fp32 overload instead", deprecation.reason().orElseThrow()); + + assertTrue( + extensions + .getScalarFunction(SimpleExtension.FunctionAnchor.of(URN, "evolvingScalar:fp32")) + .deprecated() + .isEmpty()); + } + + @Test + void testNonDeprecatedAggregateAsWindowFunction() { + // Aggregate functions are also registered as window functions; deprecation must carry over. + SimpleExtension.FunctionAnchor anchor = + SimpleExtension.FunctionAnchor.of(URN, "deprecatedAggregate:i64"); + assertFalse(extensions.getWindowFunction(anchor).deprecated().isEmpty()); + assertEquals("1.2.0", extensions.getWindowFunction(anchor).deprecated().orElseThrow().since()); + } +} diff --git a/core/src/test/resources/extensions/deprecation_extensions.yaml b/core/src/test/resources/extensions/deprecation_extensions.yaml new file mode 100644 index 000000000..b06195b4c --- /dev/null +++ b/core/src/test/resources/extensions/deprecation_extensions.yaml @@ -0,0 +1,57 @@ +%YAML 1.2 +--- +urn: extension:test:deprecation_extensions +types: + - name: "deprecatedType" + deprecated: + since: "0.50.0" + reason: "Replaced by newType" +scalar_functions: + # Function-level deprecation + - name: "deprecatedScalar" + deprecated: + since: "0.1.1" + reason: "Use newScalar instead" + impls: + - args: + - value: i64 + return: i64 + # Function-level deprecation with reason and metadata + - name: "deprecatedScalarWithMetadata" + deprecated: + since: "2.0.0" + reason: "This function is deprecated for removal in a future version" + metadata: + alternative: "newScalar" + impls: + - args: + - value: i64 + return: i64 + # Only one overload (impl) is deprecated + - name: "evolvingScalar" + impls: + - args: + - value: fp32 + return: fp32 + - args: + - value: i64 + deprecated: + since: "3.1.0" + reason: "Use the fp32 overload instead" + return: i64 +aggregate_functions: + - name: "deprecatedAggregate" + deprecated: + since: "1.2.0" + impls: + - args: + - value: i64 + return: i64 +window_functions: + - name: "deprecatedWindow" + deprecated: + since: "1.3.0" + impls: + - args: + - value: i64 + return: i64 From b2a74f1cf1b5a4d07e50580a26d54879573daaa6 Mon Sep 17 00:00:00 2001 From: Niels Pardon Date: Tue, 9 Jun 2026 18:51:07 +0200 Subject: [PATCH 2/4] docs: remove PR link Signed-off-by: Niels Pardon --- .../java/io/substrait/extension/DeprecationExtensionTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/test/java/io/substrait/extension/DeprecationExtensionTest.java b/core/src/test/java/io/substrait/extension/DeprecationExtensionTest.java index 9b662f739..4d30231de 100644 --- a/core/src/test/java/io/substrait/extension/DeprecationExtensionTest.java +++ b/core/src/test/java/io/substrait/extension/DeprecationExtensionTest.java @@ -18,8 +18,6 @@ *
  • Function-level deprecation (scalar, aggregate, window) *
  • Function-implementation-level deprecation (individual overloads) * - * - * See substrait#1014. */ class DeprecationExtensionTest extends TestBase { From 4eaea5edb742bbe277cd132100bf4f118796ab77 Mon Sep 17 00:00:00 2001 From: Niels Pardon Date: Tue, 9 Jun 2026 18:57:09 +0200 Subject: [PATCH 3/4] docs: remove PR link Signed-off-by: Niels Pardon --- core/src/main/java/io/substrait/extension/SimpleExtension.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/io/substrait/extension/SimpleExtension.java b/core/src/main/java/io/substrait/extension/SimpleExtension.java index a1434aa57..393dfb9a0 100644 --- a/core/src/main/java/io/substrait/extension/SimpleExtension.java +++ b/core/src/main/java/io/substrait/extension/SimpleExtension.java @@ -124,7 +124,6 @@ public interface Option { /** * Deprecation information for an extension entry (type, function, or function implementation). - * See substrait#1014. * *

    Consumers of extension files are not required to understand or validate deprecation fields; * the information is provided so tooling can surface deprecation warnings. From e914bcb3c934a105bbb291949803f4684df186c3 Mon Sep 17 00:00:00 2001 From: Niels Pardon Date: Thu, 11 Jun 2026 08:58:57 +0200 Subject: [PATCH 4/4] fix: change visibility of ScalarFunctionVariant.resolve() Signed-off-by: Niels Pardon --- core/src/main/java/io/substrait/extension/SimpleExtension.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/io/substrait/extension/SimpleExtension.java b/core/src/main/java/io/substrait/extension/SimpleExtension.java index 393dfb9a0..658481cfe 100644 --- a/core/src/main/java/io/substrait/extension/SimpleExtension.java +++ b/core/src/main/java/io/substrait/extension/SimpleExtension.java @@ -428,7 +428,7 @@ public Stream resolve(String urn) { @JsonSerialize(as = ImmutableSimpleExtension.ScalarFunctionVariant.class) @Value.Immutable public abstract static class ScalarFunctionVariant extends Function { - public ScalarFunctionVariant resolve( + ScalarFunctionVariant resolve( String urn, String name, String description,