Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build with Gradle
run: gradle build --rerun-tasks
run: |
# fetch submodule tags since actions/checkout@v4 does not
git submodule foreach 'git fetch --unshallow || true'

gradle build --rerun-tasks
examples:
name: Build Examples
runs-on: ubuntu-latest
Expand Down Expand Up @@ -115,6 +119,9 @@ jobs:
run: gu install native-image
- name: Build with Gradle
run: |
# fetch submodule tags since actions/checkout@v4 does not
git submodule foreach 'git fetch --unshallow || true'

gradle nativeImage
- name: Smoke Test
run: |
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ jobs:
- name: Install GraalVM native image
run: gu install native-image
- name: Build with Gradle
run: gradle nativeImage
run: |
# fetch submodule tags since actions/checkout@v4 does not
git submodule foreach 'git fetch --unshallow || true'

gradle nativeImage
- name: Smoke Test
run: |
./isthmus-cli/src/test/script/smoke.sh
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ out/**

*/bin
.metals
.bloop
3 changes: 3 additions & 0 deletions ci/release/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@

set -euo pipefail

# ensure the submodule tags exist
git submodule foreach 'git fetch --unshallow || true'

gradle wrapper
./gradlew clean :core:publishToSonatype :isthmus:publishToSonatype :spark:publishToSonatype closeAndReleaseSonatypeStagingRepository
73 changes: 73 additions & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import java.io.ByteArrayOutputStream
import java.nio.charset.StandardCharsets
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.gradle.plugins.ide.idea.model.IdeaModel
import org.slf4j.LoggerFactory

plugins {
`maven-publish`
Expand Down Expand Up @@ -105,6 +110,71 @@ configurations[JavaPlugin.API_CONFIGURATION_NAME].let { apiConfiguration ->
apiConfiguration.setExtendsFrom(apiConfiguration.extendsFrom.filter { it.name != "antlr" })
}

abstract class SubstraitSpecVersionValueSource :
ValueSource<String, SubstraitSpecVersionValueSource.Parameters> {
companion object {
val logger = LoggerFactory.getLogger("SubstraitSpecVersionValueSource")
}

interface Parameters : ValueSourceParameters {
val substraitDirectory: Property<File>
}

@get:Inject abstract val execOperations: ExecOperations

override fun obtain(): String {
val stdOutput = ByteArrayOutputStream()
val errOutput = ByteArrayOutputStream()
execOperations.exec {
commandLine("git", "describe", "--tags")
standardOutput = stdOutput
errorOutput = errOutput
setIgnoreExitValue(true)
workingDir = parameters.substraitDirectory.get()
}

// capturing the error output and logging it to avoid issues with VS Code Spotless plugin
val error = String(errOutput.toByteArray())
if (error != "") {
logger.warn(error)
}

val cmdOut = String(stdOutput.toByteArray()).trim()

if (cmdOut.startsWith("v")) {
return cmdOut.substring(1)
}

return cmdOut
}
}

tasks.register("writeManifest") {
doLast {
val substraitSpecVersionProvider =
providers.of(SubstraitSpecVersionValueSource::class) {
parameters.substraitDirectory.set(project(":").file("substrait"))
}

val manifestFile =
layout.buildDirectory
.file("generated/sources/manifest/META-INF/MANIFEST.MF")
.get()
.getAsFile()
manifestFile.getParentFile().mkdirs()

manifestFile.printWriter(StandardCharsets.UTF_8).use {
it.println("Manifest-Version: 1.0")
it.println("Implementation-Title: substrait-java")
it.println("Implementation-Version: " + project.version)
it.println("Specification-Title: substrait")
it.println("Specification-Version: " + substraitSpecVersionProvider.get())
}
}
}

tasks.named("compileJava") { dependsOn("writeManifest") }

tasks {
shadowJar {
archiveClassifier.set("") // to override ".jar" instead of producing "-all.jar"
Expand All @@ -114,6 +184,8 @@ tasks {
// rename the shadowed deps so that they don't conflict with consumer's own deps
relocate("org.antlr.v4.runtime", "io.substrait.org.antlr.v4.runtime")
}

jar { manifest { from("build/generated/sources/manifest/META-INF/MANIFEST.MF") } }
}

java {
Expand All @@ -132,6 +204,7 @@ sourceSets {
main {
proto.srcDir("../substrait/proto")
resources.srcDir("../substrait/extensions")
resources.srcDir("build/generated/sources/manifest/")
java.srcDir(file("build/generated/sources/antlr/main/java/"))
}
}
Expand Down
54 changes: 54 additions & 0 deletions core/src/main/java/io/substrait/plan/Plan.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@

import io.substrait.proto.AdvancedExtension;
import io.substrait.relation.Rel;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;
import org.immutables.value.Value;

@Value.Immutable
public abstract class Plan {

@Value.Default
public Version getVersion() {
return Version.DEFAULT_VERSION;
}

public abstract List<Root> getRoots();

public abstract List<String> getExpectedTypeUrls();
Expand All @@ -19,6 +27,52 @@ public static ImmutablePlan.Builder builder() {
return ImmutablePlan.builder();
}

@Value.Immutable
public abstract static class Version {
Copy link
Member

Choose a reason for hiding this comment

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

One small thing I would suggest here is splitting the general concept of the Version POJO from the substrait-java specific Version that we want to create. That is, other java based systems might use the POJOs to manipulate Substrait plans and might want to set their own versions, which gets a bit muddied if we include defaults in the definition IMO.

What do you think about something like:

  @Value.Immutable
  public abstract static class Version {
    abstract int getMajor();

    abstract int getMinor();

    abstract int getPatch();

    abstract Optional<String> getGitHash();

    abstract Optional<String> getProducer();

    public static ImmutableVersion.Builder builder() {
      return ImmutableVersion.builder();
    }
  }

  public static final Version SUBSTRAIT_JAVA;

  static {
    final String[] VERSION_COMPONENTS = loadVersion();
      SUBSTRAIT_JAVA =
        Version.builder()
            .minor(Integer.parseInt(VERSION_COMPONENTS[1]))
            .patch(Integer.parseInt(VERSION_COMPONENTS[2]))
            .producer("substrait-java")
            .build();
  }

which keeps the POJO object generic in that it models any Version, but we provide a pre-populated Version for folks that want to use it?

Copy link
Member Author

@nielspardon nielspardon Jun 10, 2025

Choose a reason for hiding this comment

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

we can change that sure. I did set a default mainly because the substrait-validator complains if it's not set at all so I thought it makes sense to set it by default in any case. the way it's implemented with @Value.Default would still allow users to override the values. instead of doing it for the individual version components we can also do it at the full version object level.

Copy link
Member Author

Choose a reason for hiding this comment

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

I pushed an update to this code:

@Value.Immutable
public abstract class Plan {

  @Value.Default
  public Version getVersion() {
    return Version.DEFAULT_VERSION;
  }
..
  @Value.Immutable
  public abstract static class Version {
    public static final Version DEFAULT_VERSION;

    static {
      final String[] versionComponents = loadVersion();
      DEFAULT_VERSION =
          ImmutableVersion.builder()
              .major(Integer.parseInt(versionComponents[0]))
              .minor(Integer.parseInt(versionComponents[1]))
              .patch(Integer.parseInt(versionComponents[2]))
              .producer(Optional.of("substrait-java"))
              .build();
    }

public static final Version DEFAULT_VERSION;

static {
final String[] versionComponents = loadVersion();
DEFAULT_VERSION =
ImmutableVersion.builder()
.major(Integer.parseInt(versionComponents[0]))
.minor(Integer.parseInt(versionComponents[1]))
.patch(Integer.parseInt(versionComponents[2]))
.producer(Optional.of("substrait-java"))
.build();
}

public abstract int getMajor();

public abstract int getMinor();

public abstract int getPatch();

public abstract Optional<String> getGitHash();

public abstract Optional<String> getProducer();

private static String[] loadVersion() {
// load the specification version from the JAR manifest
String specificationVersion = Version.class.getPackage().getSpecificationVersion();

// load the manifest directly from the classpath if the specification version is null which is
// the case if the Version class is not in a JAR, e.g. during the Gradle build
if (specificationVersion == null) {
try {
Manifest manifest =
new Manifest(
Version.class.getClassLoader().getResourceAsStream("META-INF/MANIFEST.MF"));
specificationVersion = manifest.getMainAttributes().getValue(Name.SPECIFICATION_VERSION);
} catch (IOException e) {
throw new IllegalStateException("Could not load version from manifest", e);
}
}

return specificationVersion.split("\\.");
}
}

@Value.Immutable
public abstract static class Root {
public abstract Rel getInput();
Expand Down
13 changes: 13 additions & 0 deletions core/src/main/java/io/substrait/plan/PlanProtoConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.substrait.proto.Plan;
import io.substrait.proto.PlanRel;
import io.substrait.proto.Rel;
import io.substrait.proto.Version;
import io.substrait.relation.RelProtoConverter;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -34,6 +35,18 @@ public Plan toProto(io.substrait.plan.Plan plan) {
if (plan.getAdvancedExtension().isPresent()) {
builder.setAdvancedExtensions(plan.getAdvancedExtension().get());
}

Version.Builder versionBuilder =
Version.newBuilder()
.setMajorNumber(plan.getVersion().getMajor())
.setMinorNumber(plan.getVersion().getMinor())
.setPatchNumber(plan.getVersion().getPatch());

plan.getVersion().getGitHash().ifPresent(gh -> versionBuilder.setGitHash(gh));
plan.getVersion().getProducer().ifPresent(p -> versionBuilder.setProducer(p));

builder.setVersion(versionBuilder);

return builder.build();
}
}
18 changes: 18 additions & 0 deletions core/src/main/java/io/substrait/plan/ProtoPlanConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,29 @@ public Plan from(io.substrait.proto.Plan plan) {
Rel rel = relConverter.from(root.getInput());
roots.add(Plan.Root.builder().input(rel).names(root.getNamesList()).build());
}

ImmutableVersion.Builder versionBuilder =
ImmutableVersion.builder()
.major(plan.getVersion().getMajorNumber())
.minor(plan.getVersion().getMinorNumber())
.patch(plan.getVersion().getPatchNumber());

// protobuf field 'git_hash' is an empty string by default
if (!plan.getVersion().getGitHash().isEmpty()) {
versionBuilder.gitHash(Optional.of(plan.getVersion().getGitHash()));
}

// protobuf field 'producer' is an empty string by default
if (!plan.getVersion().getProducer().isEmpty()) {
versionBuilder.producer(Optional.of(plan.getVersion().getProducer()));
}

return Plan.builder()
.roots(roots)
.expectedTypeUrls(plan.getExpectedTypeUrlsList())
.advancedExtension(
Optional.ofNullable(plan.hasAdvancedExtensions() ? plan.getAdvancedExtensions() : null))
.version(versionBuilder.build())
.build();
}
}
21 changes: 21 additions & 0 deletions core/src/test/java/io/substrait/relation/SpecVersionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.substrait.relation;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import io.substrait.plan.Plan.Version;
import java.util.Optional;
import org.junit.jupiter.api.Test;

public class SpecVersionTest {
@Test
public void testSubstraitVersionDefaultValues() {
Version version = Version.DEFAULT_VERSION;

assertNotNull(version.getMajor());
assertNotNull(version.getMinor());
assertNotNull(version.getPatch());

assertEquals(Optional.of("substrait-java"), version.getProducer());
}
}
33 changes: 15 additions & 18 deletions isthmus/src/main/java/io/substrait/isthmus/SqlToSubstrait.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package io.substrait.isthmus;

import com.google.common.annotations.VisibleForTesting;
import io.substrait.extension.ExtensionCollector;
import io.substrait.plan.ImmutablePlan.Builder;
import io.substrait.plan.ImmutableVersion;
import io.substrait.plan.Plan.Version;
import io.substrait.plan.PlanProtoConverter;
import io.substrait.proto.Plan;
import io.substrait.proto.PlanRel;
import io.substrait.relation.RelProtoConverter;
import java.util.List;
import org.apache.calcite.plan.hep.HepPlanner;
import org.apache.calcite.plan.hep.HepProgram;
Expand Down Expand Up @@ -56,22 +57,18 @@ List<RelRoot> sqlToRelNode(String sql, List<String> tables) throws SqlParseExcep

private Plan executeInner(String sql, SqlValidator validator, Prepare.CatalogReader catalogReader)
throws SqlParseException {
var plan = Plan.newBuilder();
ExtensionCollector functionCollector = new ExtensionCollector();
var relProtoConverter = new RelProtoConverter(functionCollector);
Builder builder = io.substrait.plan.Plan.builder();
builder.version(
ImmutableVersion.builder().from(Version.DEFAULT_VERSION).producer("isthmus").build());

// TODO: consider case in which one sql passes conversion while others don't
sqlToRelNode(sql, validator, catalogReader)
.forEach(
root -> {
plan.addRelations(
PlanRel.newBuilder()
.setRoot(
relProtoConverter.toProto(
SubstraitRelVisitor.convert(
root, EXTENSION_COLLECTION, featureBoard))));
});
functionCollector.addExtensionsToPlan(plan);
return plan.build();
sqlToRelNode(sql, validator, catalogReader).stream()
.map(root -> SubstraitRelVisitor.convert(root, EXTENSION_COLLECTION, featureBoard))
.forEach(root -> builder.addRoots(root));

PlanProtoConverter planToProto = new PlanProtoConverter();

return planToProto.toProto(builder.build());
Copy link
Member

Choose a reason for hiding this comment

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

Good call on wiring this through the POJOs

}

private List<RelRoot> sqlToRelNode(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import io.substrait.debug.TreePrinter
import io.substrait.expression.{Expression => SExpression, ExpressionCreator}
import io.substrait.extension.ExtensionCollector
import io.substrait.hint.Hint
import io.substrait.plan.{ImmutableRoot, Plan}
import io.substrait.plan.{ImmutableRoot, ImmutableVersion, Plan}
import io.substrait.relation.RelProtoConverter
import io.substrait.relation.Set.SetOp
import io.substrait.relation.files.{FileFormat, ImmutableFileOrFiles}
Expand Down Expand Up @@ -543,6 +543,12 @@ class ToSubstraitRel extends AbstractLogicalPlanVisitor with Logging {
def convert(p: LogicalPlan): Plan = {
val rel = visit(p)
Plan.builder
.version(
ImmutableVersion
.builder()
.from(Plan.Version.DEFAULT_VERSION)
.producer("substrait-spark")
.build())
.roots(
Collections.singletonList(
ImmutableRoot
Expand Down
Loading