Skip to content

Commit af87ce0

Browse files
committed
feat(core): model type variations from simple extension YAML files
Type variations declared in the `type_variations` section of simple extension YAML files were silently ignored because there was no model object to hold them. This adds a `TypeVariation` model (name, parent type class, description, function behavior, deprecation) along with a `TypeVariationAnchor`, parses the `type_variations` section in `ExtensionSignatures`, and exposes lookup by anchor from `ExtensionCollection` (`getTypeVariation`), mirroring how types are handled. `parent` is modeled as a raw String rather than a parsed type: the schema references `$defs/type`, but the spec's own example files use bare type-class names such as `struct`, which are not valid Substrait type expressions (see substrait-io/substrait#1115). Fixes #847
1 parent a1e48c6 commit af87ce0

3 files changed

Lines changed: 256 additions & 1 deletion

File tree

core/src/main/java/io/substrait/extension/SimpleExtension.java

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,18 @@ public enum WindowType {
105105
STREAMING
106106
}
107107

108+
/**
109+
* Enumerates how functions supporting the system-preferred variation relate to a type variation.
110+
*/
111+
public enum TypeVariationFunctionBehavior {
112+
/**
113+
* Functions that support the system-preferred variation implicitly also support this variation.
114+
*/
115+
INHERITS,
116+
/** Functions must be resolved independently for this variation. */
117+
SEPARATE
118+
}
119+
108120
private SimpleExtension() {}
109121

110122
/** Describes an argument provided by a simple extension. */
@@ -372,6 +384,21 @@ static TypeAnchor of(String urn, String name) {
372384
}
373385
}
374386

387+
/** Describes a type variation anchor provided by a simple extension. */
388+
@Value.Immutable
389+
public interface TypeVariationAnchor extends Anchor {
390+
/**
391+
* Creates the corresponding of instance.
392+
*
393+
* @param urn the urn
394+
* @param name the name
395+
* @return the of
396+
*/
397+
static TypeVariationAnchor of(String urn, String name) {
398+
return ImmutableSimpleExtension.TypeVariationAnchor.builder().urn(urn).key(name).build();
399+
}
400+
}
401+
375402
/** Describes a variadic behavior provided by a simple extension. */
376403
@JsonDeserialize(as = ImmutableSimpleExtension.VariadicBehavior.class)
377404
@JsonSerialize(as = ImmutableSimpleExtension.VariadicBehavior.class)
@@ -1061,6 +1088,82 @@ public TypeAnchor getAnchor() {
10611088
}
10621089
}
10631090

1091+
/**
1092+
* Describes a type variation declared by a simple extension.
1093+
*
1094+
* <p>A type variation represents an alternative representation of a base type class (e.g. a
1095+
* dictionary-encoded string), as defined by the {@code type_variations} section of the simple
1096+
* extension schema.
1097+
*/
1098+
@JsonDeserialize(as = ImmutableSimpleExtension.TypeVariation.class)
1099+
@JsonSerialize(as = ImmutableSimpleExtension.TypeVariation.class)
1100+
@Value.Immutable
1101+
public abstract static class TypeVariation {
1102+
private final Supplier<TypeVariationAnchor> anchorSupplier =
1103+
Util.memoize(() -> TypeVariationAnchor.of(urn(), name()));
1104+
1105+
/**
1106+
* Returns the name.
1107+
*
1108+
* @return the name
1109+
*/
1110+
public abstract String name();
1111+
1112+
/**
1113+
* Returns the base type class of this variation as written in the extension file (e.g. {@code
1114+
* string} or {@code struct}).
1115+
*
1116+
* @return the parent type class
1117+
*/
1118+
@JsonProperty("parent")
1119+
public abstract String parent();
1120+
1121+
/**
1122+
* Returns the description.
1123+
*
1124+
* @return the description
1125+
*/
1126+
public abstract Optional<String> description();
1127+
1128+
/**
1129+
* Returns the function behavior, i.e. whether functions supporting the system-preferred
1130+
* variation implicitly support this variation ({@link TypeVariationFunctionBehavior#INHERITS})
1131+
* or must be resolved independently ({@link TypeVariationFunctionBehavior#SEPARATE}). Defaults
1132+
* to {@link TypeVariationFunctionBehavior#INHERITS}.
1133+
*
1134+
* @return the function behavior
1135+
*/
1136+
@Value.Default
1137+
@JsonProperty("functions")
1138+
public TypeVariationFunctionBehavior functions() {
1139+
return TypeVariationFunctionBehavior.INHERITS;
1140+
}
1141+
1142+
/**
1143+
* Returns the deprecated.
1144+
*
1145+
* @return the deprecated
1146+
*/
1147+
public abstract Optional<DeprecationStatus> deprecated();
1148+
1149+
/**
1150+
* Returns the urn.
1151+
*
1152+
* @return the urn
1153+
*/
1154+
@JacksonInject(SimpleExtension.URN_LOCATOR_KEY)
1155+
public abstract String urn();
1156+
1157+
/**
1158+
* Returns the anchor.
1159+
*
1160+
* @return the anchor
1161+
*/
1162+
public TypeVariationAnchor getAnchor() {
1163+
return anchorSupplier.get();
1164+
}
1165+
}
1166+
10641167
/** Describes an extension signatures provided by a simple extension. */
10651168
@JsonDeserialize(as = ImmutableSimpleExtension.ExtensionSignatures.class)
10661169
@JsonSerialize(as = ImmutableSimpleExtension.ExtensionSignatures.class)
@@ -1075,6 +1178,14 @@ public abstract static class ExtensionSignatures {
10751178
@JsonProperty("types")
10761179
public abstract List<Type> types();
10771180

1181+
/**
1182+
* Returns the type variations.
1183+
*
1184+
* @return the type variations
1185+
*/
1186+
@JsonProperty("type_variations")
1187+
public abstract List<TypeVariation> typeVariations();
1188+
10781189
/**
10791190
* Returns the urn.
10801191
*
@@ -1170,6 +1281,14 @@ public abstract static class ExtensionCollection {
11701281
types().stream()
11711282
.collect(
11721283
Collectors.toMap(Type::getAnchor, java.util.function.Function.identity())));
1284+
1285+
private final Supplier<Map<TypeVariationAnchor, TypeVariation>> typeVariationLookup =
1286+
Util.memoize(
1287+
() ->
1288+
typeVariations().stream()
1289+
.collect(
1290+
Collectors.toMap(
1291+
TypeVariation::getAnchor, java.util.function.Function.identity())));
11731292
private final Supplier<Map<FunctionAnchor, ScalarFunctionVariant>> scalarFunctionsLookup =
11741293
Util.memoize(
11751294
() -> {
@@ -1214,6 +1333,13 @@ public Map<String, Map<String, Object>> extensionMetadata() {
12141333
*/
12151334
public abstract List<Type> types();
12161335

1336+
/**
1337+
* Returns the type variations.
1338+
*
1339+
* @return the type variations
1340+
*/
1341+
public abstract List<TypeVariation> typeVariations();
1342+
12171343
/**
12181344
* Returns the scalar Functions.
12191345
*
@@ -1272,6 +1398,25 @@ public Type getType(TypeAnchor anchor) {
12721398
anchor.key(), anchor.urn()));
12731399
}
12741400

1401+
/**
1402+
* Returns the type variation for the given anchor.
1403+
*
1404+
* @param anchor the anchor
1405+
* @return the type variation
1406+
*/
1407+
public TypeVariation getTypeVariation(TypeVariationAnchor anchor) {
1408+
TypeVariation typeVariation = typeVariationLookup.get().get(anchor);
1409+
if (typeVariation != null) {
1410+
return typeVariation;
1411+
}
1412+
checkUrn(anchor.urn());
1413+
throw new IllegalArgumentException(
1414+
String.format(
1415+
"Unexpected type variation with name %s. The URN %s is loaded but no type variation "
1416+
+ "with this name found.",
1417+
anchor.key(), anchor.urn()));
1418+
}
1419+
12751420
/**
12761421
* Returns the scalar Function for the given arguments.
12771422
*
@@ -1299,7 +1444,9 @@ public ScalarFunctionVariant getScalarFunction(FunctionAnchor anchor) {
12991444
* @return the contains Urn
13001445
*/
13011446
public boolean containsUrn(String urn) {
1302-
return urnSupplier.get().contains(urn) || types().stream().anyMatch(t -> t.urn().equals(urn));
1447+
return urnSupplier.get().contains(urn)
1448+
|| types().stream().anyMatch(t -> t.urn().equals(urn))
1449+
|| typeVariations().stream().anyMatch(tv -> tv.urn().equals(urn));
13031450
}
13041451

13051452
private void checkUrn(String name) {
@@ -1374,6 +1521,8 @@ public ExtensionCollection merge(ExtensionCollection extensionCollection) {
13741521
.addAllWindowFunctions(extensionCollection.windowFunctions())
13751522
.addAllTypes(types())
13761523
.addAllTypes(extensionCollection.types())
1524+
.addAllTypeVariations(typeVariations())
1525+
.addAllTypeVariations(extensionCollection.typeVariations())
13771526
.extensionMetadata(mergedExtensionMetadata)
13781527
.build();
13791528
}
@@ -1499,6 +1648,7 @@ public static ExtensionCollection buildExtensionCollection(
14991648
.aggregateFunctions(aggregateFunctionVariants)
15001649
.windowFunctions(allWindowFunctionVariants)
15011650
.addAllTypes(extensionSignatures.types())
1651+
.addAllTypeVariations(extensionSignatures.typeVariations())
15021652
.extensionMetadata(extMetadata)
15031653
.build();
15041654

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package io.substrait.extension;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import io.substrait.TestBase;
8+
import java.io.IOException;
9+
import java.io.UncheckedIOException;
10+
import org.junit.jupiter.api.Test;
11+
12+
/**
13+
* Verifies that type variations declared in the {@code type_variations} section of an extension
14+
* YAML file can be read and looked up by anchor, including their parent type class, description,
15+
* function behavior, and deprecation information.
16+
*/
17+
class TypeVariationExtensionTest extends TestBase {
18+
19+
static final String URN = "extension:test:type_variation_extensions";
20+
static final SimpleExtension.ExtensionCollection TYPE_VARIATION_EXTENSION;
21+
22+
static {
23+
try {
24+
String extensionStr = asString("extensions/type_variation_extensions.yaml");
25+
TYPE_VARIATION_EXTENSION = SimpleExtension.load(extensionStr);
26+
} catch (IOException e) {
27+
throw new UncheckedIOException(e);
28+
}
29+
}
30+
31+
TypeVariationExtensionTest() {
32+
super(TYPE_VARIATION_EXTENSION);
33+
}
34+
35+
@Test
36+
void parsesInheritsVariation() {
37+
SimpleExtension.TypeVariation variation =
38+
extensions.getTypeVariation(SimpleExtension.TypeVariationAnchor.of(URN, "dict4"));
39+
assertEquals("dict4", variation.name());
40+
assertEquals("string", variation.parent());
41+
assertEquals("a four-byte dictionary encoded string", variation.description().orElseThrow());
42+
assertEquals(SimpleExtension.TypeVariationFunctionBehavior.INHERITS, variation.functions());
43+
assertTrue(variation.deprecated().isEmpty());
44+
}
45+
46+
@Test
47+
void parsesSeparateVariation() {
48+
SimpleExtension.TypeVariation variation =
49+
extensions.getTypeVariation(SimpleExtension.TypeVariationAnchor.of(URN, "avro"));
50+
assertEquals("struct", variation.parent());
51+
assertEquals(SimpleExtension.TypeVariationFunctionBehavior.SEPARATE, variation.functions());
52+
}
53+
54+
@Test
55+
void functionBehaviorDefaultsToInherits() {
56+
SimpleExtension.TypeVariation variation =
57+
extensions.getTypeVariation(
58+
SimpleExtension.TypeVariationAnchor.of(URN, "inheritsByDefault"));
59+
assertEquals(SimpleExtension.TypeVariationFunctionBehavior.INHERITS, variation.functions());
60+
}
61+
62+
@Test
63+
void parsesDeprecation() {
64+
SimpleExtension.DeprecationStatus deprecation =
65+
extensions
66+
.getTypeVariation(SimpleExtension.TypeVariationAnchor.of(URN, "deprecatedVariation"))
67+
.deprecated()
68+
.orElseThrow();
69+
assertEquals("0.86.0", deprecation.since());
70+
assertEquals("Replaced by avro", deprecation.reason().orElseThrow());
71+
}
72+
73+
@Test
74+
void unknownVariationThrows() {
75+
assertThrows(
76+
IllegalArgumentException.class,
77+
() -> extensions.getTypeVariation(SimpleExtension.TypeVariationAnchor.of(URN, "missing")));
78+
}
79+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
%YAML 1.2
2+
---
3+
urn: extension:test:type_variation_extensions
4+
type_variations:
5+
# Explicit INHERITS behavior with a description.
6+
- parent: string
7+
name: dict4
8+
description: a four-byte dictionary encoded string
9+
functions: INHERITS
10+
# Explicit SEPARATE behavior.
11+
- parent: struct
12+
name: avro
13+
description: an avro encoded struct
14+
functions: SEPARATE
15+
# functions omitted -> defaults to INHERITS.
16+
- parent: string
17+
name: inheritsByDefault
18+
description: a variation without an explicit functions behavior
19+
# Deprecated variation.
20+
- parent: struct
21+
name: deprecatedVariation
22+
description: a deprecated struct variation
23+
functions: SEPARATE
24+
deprecated:
25+
since: "0.86.0"
26+
reason: "Replaced by avro"

0 commit comments

Comments
 (0)