diff --git a/core/src/main/java/io/substrait/extension/SimpleExtension.java b/core/src/main/java/io/substrait/extension/SimpleExtension.java index 1b3f05820..658481cfe 100644 --- a/core/src/main/java/io/substrait/extension/SimpleExtension.java +++ b/core/src/main/java/io/substrait/extension/SimpleExtension.java @@ -122,6 +122,31 @@ public interface Option { List getValues(); } + /** + * Deprecation information for an extension entry (type, function, or function implementation). + * + *

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 +305,8 @@ public String description() { public abstract Optional> metadata(); + public abstract Optional deprecated(); + public List requiredArguments() { return requiredArgsSupplier.get(); } @@ -387,10 +414,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())); } } @@ -398,8 +428,12 @@ public Stream resolve(String urn) { @JsonSerialize(as = ImmutableSimpleExtension.ScalarFunctionVariant.class) @Value.Immutable public abstract static class ScalarFunctionVariant extends Function { - public ScalarFunctionVariant resolve( - String urn, String name, String description, Optional> metadata) { + ScalarFunctionVariant resolve( + String urn, + String name, + String description, + Optional> metadata, + Optional deprecated) { return ImmutableSimpleExtension.ScalarFunctionVariant.builder() .urn(urn) .name(name) @@ -408,6 +442,7 @@ public ScalarFunctionVariant resolve( .args(args()) .options(options()) .metadata(metadata) + .deprecated(deprecated().isPresent() ? deprecated() : deprecated) .ordered(ordered()) .variadic(variadic()) .returnType(returnType()) @@ -427,10 +462,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 +484,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 +517,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 +530,7 @@ AggregateFunctionVariant resolve( .args(args()) .options(options()) .metadata(metadata) + .deprecated(deprecated().isPresent() ? deprecated() : deprecated) .ordered(ordered()) .variadic(variadic()) .decomposability(decomposability()) @@ -520,7 +566,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 +579,7 @@ WindowFunctionVariant resolve( .args(args()) .options(options()) .metadata(metadata) + .deprecated(deprecated().isPresent() ? deprecated() : deprecated) .ordered(ordered()) .variadic(variadic()) .decomposability(decomposability()) @@ -567,6 +618,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..4d30231de --- /dev/null +++ b/core/src/test/java/io/substrait/extension/DeprecationExtensionTest.java @@ -0,0 +1,115 @@ +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: + * + *

+ */ +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