vinothchandar commented on code in PR #843:
URL: https://github.com/apache/incubator-xtable/pull/843#discussion_r3607590820
##########
.github/workflows/mvn-ci-build.yml:
##########
@@ -24,10 +24,12 @@ on:
push:
branches:
- "main"
+ - "branch-0.4"
Review Comment:
I was a little confused at first to see the version number. But it seems you
already cut the release branch
##########
.github/workflows/spark-runtime-validation.yml:
##########
@@ -0,0 +1,85 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+name: Spark Runtime Bundle Validation
+
+# Validates the shaded xtable-spark-runtime bundle end-to-end:
ITXTableSparkRuntimeBundle
+# spark-submits the bundled jar against a real Spark distribution for one case
per direction
+# across Hudi, Iceberg and Delta. The IT self-skips unless SPARK_HOME is set,
so the main CI
+# never exercises it; this workflow installs a matching Spark 3.4 distro and
runs it.
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ !contains(github.ref, 'main') }}
+
+# Runs on every push/PR to the covered branches (no path filter) so the check
always
+# reports — required status checks that never trigger leave a PR blocked on a
pending check.
+on:
+ push:
+ branches:
+ - "main"
+ - "branch-0.4"
+
+ pull_request:
+ branches:
+ - "main"
+ - "branch-0.4"
+
+ workflow_dispatch:
+
+env:
+ SPARK_VERSION: "3.4.3"
Review Comment:
This cannot be 3.5 or something.?
##########
xtable-spark-runtime/src/main/java/org/apache/xtable/spark/XTableSparkSync.java:
##########
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.xtable.spark;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import lombok.extern.log4j.Log4j2;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.spark.sql.SparkSession;
+
+/**
+ * Standalone {@code spark-submit} entry point that runs a single XTable sync
using the relocated
+ * {@code xtable-spark-runtime} bundle. It is the RunSync-equivalent for this
bundle and is
+ * primarily used to validate that the shaded jar is self-contained on a real
Spark install.
+ *
+ * <pre>
+ * $SPARK_HOME/bin/spark-submit \
+ * --class org.apache.xtable.spark.XTableSparkSync \
+ * xtable-spark-runtime_2.12-<ver>.jar \
+ * --basePath /warehouse/db/orders --sourceFormat HUDI --targets
ICEBERG,DELTA
+ * </pre>
+ */
+@Log4j2
+public final class XTableSparkSync {
+
+ private static final String BASE_PATH = "basePath";
+ private static final String DATA_PATH = "dataPath";
+ private static final String SOURCE_FORMAT = "sourceFormat";
+ private static final String TARGETS = "targets";
+ private static final String TABLE_NAME = "tableName";
+ private static final String NAMESPACE = "namespace";
+ private static final String PARTITION_SPEC = "partitionSpec";
+ private static final String HELP = "help";
+
+ private static final Options OPTIONS =
+ new Options()
+ .addOption(
+ Option.builder()
+ .longOpt(BASE_PATH)
+ .hasArg()
+ .required()
+ .desc("The base path of the source table")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(SOURCE_FORMAT)
+ .hasArg()
+ .required()
+ .desc("The source table format, e.g. HUDI, DELTA or ICEBERG")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(TARGETS)
+ .hasArg()
+ .required()
+ .desc("Comma-separated target formats to sync to, e.g.
ICEBERG,DELTA")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(DATA_PATH)
+ .hasArg()
+ .desc("The path of the data files if different from the base
path")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(TABLE_NAME)
+ .hasArg()
+ .desc("The table name; defaults to the last segment of the
base path")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(NAMESPACE)
+ .hasArg()
+ .desc("The dot-separated table namespace")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(PARTITION_SPEC)
+ .hasArg()
+ .desc("The Hudi source partition field spec, e.g.
level:VALUE")
+ .build())
+ .addOption(Option.builder().longOpt(HELP).desc("Displays help
information").build());
+
+ private XTableSparkSync() {}
+
+ public static void main(String[] args) {
+ CommandLineParser parser = new DefaultParser();
+ CommandLine cmd;
+ try {
+ cmd = parser.parse(OPTIONS, args);
+ } catch (ParseException e) {
+ new HelpFormatter().printHelp("xtable-spark-sync", OPTIONS, true);
+ throw new IllegalArgumentException("Failed to parse arguments", e);
+ }
+ if (cmd.hasOption(HELP)) {
+ new HelpFormatter().printHelp("xtable-spark-sync", OPTIONS, true);
+ return;
+ }
+
+ String basePath = cmd.getOptionValue(BASE_PATH);
+ String tableName = cmd.getOptionValue(TABLE_NAME,
basePathToName(basePath));
+ List<String> targetFormats =
+ Arrays.stream(cmd.getOptionValue(TARGETS).split(","))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .map(s -> s.toUpperCase(Locale.ROOT))
Review Comment:
The resolved target list is never validated to be non-empty. An input like
`--targets ","` (or all-blank entries) passes through trim/filter and yields an
empty `targetFormats`, so the sync silently produces no target tables and does
nothing. Consider failing fast with a clear message when the resulting list is
empty.
##########
xtable-spark-runtime/src/main/java/org/apache/xtable/spark/XTableSparkSync.java:
##########
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.xtable.spark;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import lombok.extern.log4j.Log4j2;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.spark.sql.SparkSession;
+
+/**
+ * Standalone {@code spark-submit} entry point that runs a single XTable sync
using the relocated
+ * {@code xtable-spark-runtime} bundle. It is the RunSync-equivalent for this
bundle and is
+ * primarily used to validate that the shaded jar is self-contained on a real
Spark install.
+ *
+ * <pre>
+ * $SPARK_HOME/bin/spark-submit \
+ * --class org.apache.xtable.spark.XTableSparkSync \
+ * xtable-spark-runtime_2.12-<ver>.jar \
+ * --basePath /warehouse/db/orders --sourceFormat HUDI --targets
ICEBERG,DELTA
+ * </pre>
+ */
+@Log4j2
+public final class XTableSparkSync {
+
+ private static final String BASE_PATH = "basePath";
+ private static final String DATA_PATH = "dataPath";
+ private static final String SOURCE_FORMAT = "sourceFormat";
+ private static final String TARGETS = "targets";
+ private static final String TABLE_NAME = "tableName";
+ private static final String NAMESPACE = "namespace";
+ private static final String PARTITION_SPEC = "partitionSpec";
+ private static final String HELP = "help";
+
+ private static final Options OPTIONS =
+ new Options()
+ .addOption(
+ Option.builder()
+ .longOpt(BASE_PATH)
+ .hasArg()
+ .required()
+ .desc("The base path of the source table")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(SOURCE_FORMAT)
+ .hasArg()
+ .required()
+ .desc("The source table format, e.g. HUDI, DELTA or ICEBERG")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(TARGETS)
+ .hasArg()
+ .required()
+ .desc("Comma-separated target formats to sync to, e.g.
ICEBERG,DELTA")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(DATA_PATH)
+ .hasArg()
+ .desc("The path of the data files if different from the base
path")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(TABLE_NAME)
+ .hasArg()
+ .desc("The table name; defaults to the last segment of the
base path")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(NAMESPACE)
+ .hasArg()
+ .desc("The dot-separated table namespace")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(PARTITION_SPEC)
+ .hasArg()
+ .desc("The Hudi source partition field spec, e.g.
level:VALUE")
+ .build())
+ .addOption(Option.builder().longOpt(HELP).desc("Displays help
information").build());
+
+ private XTableSparkSync() {}
+
+ public static void main(String[] args) {
+ CommandLineParser parser = new DefaultParser();
+ CommandLine cmd;
+ try {
+ cmd = parser.parse(OPTIONS, args);
+ } catch (ParseException e) {
+ new HelpFormatter().printHelp("xtable-spark-sync", OPTIONS, true);
+ throw new IllegalArgumentException("Failed to parse arguments", e);
+ }
+ if (cmd.hasOption(HELP)) {
+ new HelpFormatter().printHelp("xtable-spark-sync", OPTIONS, true);
+ return;
+ }
+
+ String basePath = cmd.getOptionValue(BASE_PATH);
+ String tableName = cmd.getOptionValue(TABLE_NAME,
basePathToName(basePath));
+ List<String> targetFormats =
+ Arrays.stream(cmd.getOptionValue(TARGETS).split(","))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .map(s -> s.toUpperCase(Locale.ROOT))
+ .collect(Collectors.toList());
+ String namespace = cmd.getOptionValue(NAMESPACE);
+
+ TableSyncSpec spec =
+ TableSyncSpec.builder()
+ .key(tableName)
+ .basePath(basePath)
+ .dataPath(cmd.getOptionValue(DATA_PATH))
+ .namespace(namespace == null ? null : namespace.split("\\."))
+ .partitionSpec(cmd.getOptionValue(PARTITION_SPEC))
+
.sourceFormat(cmd.getOptionValue(SOURCE_FORMAT).toUpperCase(Locale.ROOT))
Review Comment:
An unsupported `--sourceFormat` isn't rejected here; it surfaces only later
in `XTableSyncService.sourceProviderFor(...)` as an
`UnsupportedOperationException`, after the SparkSession has already been
created via `getOrCreate()`. Consider validating the source format (and
required args) up front in the CLI so an invalid value fails fast without
spinning up Spark.
##########
xtable-core/src/main/java/org/apache/xtable/conversion/ConversionTargetFactory.java:
##########
@@ -87,7 +90,27 @@ public ConversionTarget createConversionTargetForName(
TableFormat.DELTA.equalsIgnoreCase(tableFormatName)
&&
DeltaConversionTargetConfig.fromProperties(properties).isUseKernel();
ServiceLoader<ConversionTarget> loader =
ServiceLoader.load(ConversionTarget.class);
- for (ConversionTarget target : loader) {
+ Iterator<ConversionTarget> iterator = loader.iterator();
+ while (true) {
+ ConversionTarget target;
+ try {
+ // hasNext() also resolves provider classes lazily, so it can throw
+ // ServiceConfigurationError too - it must be inside the guard
alongside next().
+ if (!iterator.hasNext()) {
+ break;
+ }
+ target = iterator.next();
+ } catch (ServiceConfigurationError | LinkageError error) {
+ // A registered target whose engine library is not on the classpath
(e.g. Delta when only
+ // Hudi/Iceberg are provided). Skip it so a subset of engines can
still be used; a missing
+ // engine for the requested format surfaces below as
NotSupportedException. The offending
+ // provider is consumed before the error is thrown, so the next
hasNext() advances past it.
+ log.warn(
Review Comment:
The `warn` log is good; consider also logging the caught error's
class/message (or the provider name) so operators can distinguish "engine
intentionally not on classpath" from "engine present but failing to link.?
##########
xtable-spark-runtime/src/main/java/org/apache/xtable/spark/XTableSparkSync.java:
##########
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.xtable.spark;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import lombok.extern.log4j.Log4j2;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.spark.sql.SparkSession;
+
+/**
+ * Standalone {@code spark-submit} entry point that runs a single XTable sync
using the relocated
+ * {@code xtable-spark-runtime} bundle. It is the RunSync-equivalent for this
bundle and is
+ * primarily used to validate that the shaded jar is self-contained on a real
Spark install.
+ *
+ * <pre>
+ * $SPARK_HOME/bin/spark-submit \
+ * --class org.apache.xtable.spark.XTableSparkSync \
+ * xtable-spark-runtime_2.12-<ver>.jar \
+ * --basePath /warehouse/db/orders --sourceFormat HUDI --targets
ICEBERG,DELTA
+ * </pre>
+ */
+@Log4j2
+public final class XTableSparkSync {
+
+ private static final String BASE_PATH = "basePath";
+ private static final String DATA_PATH = "dataPath";
+ private static final String SOURCE_FORMAT = "sourceFormat";
+ private static final String TARGETS = "targets";
+ private static final String TABLE_NAME = "tableName";
+ private static final String NAMESPACE = "namespace";
+ private static final String PARTITION_SPEC = "partitionSpec";
+ private static final String HELP = "help";
+
+ private static final Options OPTIONS =
+ new Options()
+ .addOption(
+ Option.builder()
+ .longOpt(BASE_PATH)
+ .hasArg()
+ .required()
+ .desc("The base path of the source table")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(SOURCE_FORMAT)
+ .hasArg()
+ .required()
+ .desc("The source table format, e.g. HUDI, DELTA or ICEBERG")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(TARGETS)
+ .hasArg()
+ .required()
+ .desc("Comma-separated target formats to sync to, e.g.
ICEBERG,DELTA")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(DATA_PATH)
+ .hasArg()
+ .desc("The path of the data files if different from the base
path")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(TABLE_NAME)
+ .hasArg()
+ .desc("The table name; defaults to the last segment of the
base path")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(NAMESPACE)
+ .hasArg()
+ .desc("The dot-separated table namespace")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(PARTITION_SPEC)
+ .hasArg()
+ .desc("The Hudi source partition field spec, e.g.
level:VALUE")
+ .build())
+ .addOption(Option.builder().longOpt(HELP).desc("Displays help
information").build());
+
+ private XTableSparkSync() {}
+
+ public static void main(String[] args) {
+ CommandLineParser parser = new DefaultParser();
+ CommandLine cmd;
+ try {
+ cmd = parser.parse(OPTIONS, args);
+ } catch (ParseException e) {
+ new HelpFormatter().printHelp("xtable-spark-sync", OPTIONS, true);
+ throw new IllegalArgumentException("Failed to parse arguments", e);
+ }
+ if (cmd.hasOption(HELP)) {
+ new HelpFormatter().printHelp("xtable-spark-sync", OPTIONS, true);
+ return;
+ }
+
+ String basePath = cmd.getOptionValue(BASE_PATH);
+ String tableName = cmd.getOptionValue(TABLE_NAME,
basePathToName(basePath));
+ List<String> targetFormats =
+ Arrays.stream(cmd.getOptionValue(TARGETS).split(","))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .map(s -> s.toUpperCase(Locale.ROOT))
+ .collect(Collectors.toList());
+ String namespace = cmd.getOptionValue(NAMESPACE);
+
+ TableSyncSpec spec =
+ TableSyncSpec.builder()
+ .key(tableName)
+ .basePath(basePath)
+ .dataPath(cmd.getOptionValue(DATA_PATH))
+ .namespace(namespace == null ? null : namespace.split("\\."))
+ .partitionSpec(cmd.getOptionValue(PARTITION_SPEC))
+
.sourceFormat(cmd.getOptionValue(SOURCE_FORMAT).toUpperCase(Locale.ROOT))
+ .targets(targetFormats)
+ .build();
+
+ SparkSession spark =
SparkSession.builder().appName("xtable-spark-sync").getOrCreate();
+ try {
+ Configuration hadoopConf = spark.sparkContext().hadoopConfiguration();
+ log.info("Starting standalone XTable sync for {}", spec.getBasePath());
+ new XTableSyncService().sync(spec, hadoopConf);
+ log.info("Completed XTable sync for {}", spec.getBasePath());
+ } finally {
+ spark.stop();
+ }
+ }
+
+ private static String basePathToName(String basePath) {
Review Comment:
throw errors or handle edge cases like these?: a `basePath` of `"/"` (or one
that is only a trailing slash) yields an empty table name, and a path with no
`/` returns the whole string. add UTs
rename: basePathToTableName? (is that what you intend?)
##########
xtable-core/src/main/java/org/apache/xtable/conversion/ConversionTargetFactory.java:
##########
@@ -96,7 +119,10 @@ && isDeltaKernelTarget(target) == useKernel) {
throw new NotSupportedException("Target format is not yet supported: " +
tableFormatName);
}
+ private static final String DELTA_KERNEL_TARGET_CLASS =
+ "org.apache.xtable.kernel.DeltaKernelConversionTarget";
+
private static boolean isDeltaKernelTarget(ConversionTarget target) {
- return target instanceof DeltaKernelConversionTarget;
+ return DELTA_KERNEL_TARGET_CLASS.equals(target.getClass().getName());
Review Comment:
It won't fail at compile time if the class is renamed/moved. I assume tests
would fail? to catch this during dev?
##########
xtable-spark-runtime/pom.xml:
##########
@@ -0,0 +1,382 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You under the Apache License, Version 2.0
+ ~ (the "License"); you may not use this file except in compliance with
+ ~ the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.xtable</groupId>
+ <artifactId>xtable</artifactId>
+ <version>0.2.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>xtable-spark-runtime_${scala.binary.version}</artifactId>
+ <name>XTable Project Spark Runtime</name>
+ <description>
+ Thin, relocated, drop-in Spark bundle that runs incremental XTable
metadata sync. v1 ships
+ the standalone spark-submit entry point (XTableSparkSync); the
config-only, driver-side
+ QueryExecutionListener on-ramp is a planned follow-up (see rfc/rfc-3).
Review Comment:
So right now, the sync needs to be manually invoked?
##########
xtable-spark-runtime/src/main/java/org/apache/xtable/spark/XTableSparkSync.java:
##########
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.xtable.spark;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import lombok.extern.log4j.Log4j2;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.spark.sql.SparkSession;
+
+/**
+ * Standalone {@code spark-submit} entry point that runs a single XTable sync
using the relocated
+ * {@code xtable-spark-runtime} bundle. It is the RunSync-equivalent for this
bundle and is
+ * primarily used to validate that the shaded jar is self-contained on a real
Spark install.
+ *
+ * <pre>
+ * $SPARK_HOME/bin/spark-submit \
+ * --class org.apache.xtable.spark.XTableSparkSync \
+ * xtable-spark-runtime_2.12-<ver>.jar \
+ * --basePath /warehouse/db/orders --sourceFormat HUDI --targets
ICEBERG,DELTA
+ * </pre>
+ */
+@Log4j2
+public final class XTableSparkSync {
+
+ private static final String BASE_PATH = "basePath";
+ private static final String DATA_PATH = "dataPath";
+ private static final String SOURCE_FORMAT = "sourceFormat";
+ private static final String TARGETS = "targets";
+ private static final String TABLE_NAME = "tableName";
+ private static final String NAMESPACE = "namespace";
+ private static final String PARTITION_SPEC = "partitionSpec";
+ private static final String HELP = "help";
+
+ private static final Options OPTIONS =
+ new Options()
+ .addOption(
+ Option.builder()
+ .longOpt(BASE_PATH)
+ .hasArg()
+ .required()
+ .desc("The base path of the source table")
+ .build())
+ .addOption(
+ Option.builder()
+ .longOpt(SOURCE_FORMAT)
Review Comment:
do we have a standard enum, where this can mapped from a format string back
the internal int/short? (why long)
##########
xtable-spark-runtime/src/main/java/org/apache/xtable/spark/XTableSparkSync.java:
##########
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.xtable.spark;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import lombok.extern.log4j.Log4j2;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.spark.sql.SparkSession;
+
+/**
+ * Standalone {@code spark-submit} entry point that runs a single XTable sync
using the relocated
+ * {@code xtable-spark-runtime} bundle. It is the RunSync-equivalent for this
bundle and is
+ * primarily used to validate that the shaded jar is self-contained on a real
Spark install.
+ *
+ * <pre>
+ * $SPARK_HOME/bin/spark-submit \
+ * --class org.apache.xtable.spark.XTableSparkSync \
+ * xtable-spark-runtime_2.12-<ver>.jar \
+ * --basePath /warehouse/db/orders --sourceFormat HUDI --targets
ICEBERG,DELTA
+ * </pre>
+ */
+@Log4j2
+public final class XTableSparkSync {
+
+ private static final String BASE_PATH = "basePath";
Review Comment:
if these are cli options - can we drop the camel case to be more unix like?
i.e --basepath vs --basePath?
##########
rfc/rfc-3/rfc-3.md:
##########
@@ -0,0 +1,234 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+# RFC-3: xtable-spark-runtime - in-job metadata sync via a thin Spark bundle
+
+## Proposers
+
+- @vinishjail97
+
+## Approvers
+- Anyone from the XTable community can approve/add feedback.
Review Comment:
You can pencil me in
##########
rfc/rfc-3/rfc-3.md:
##########
@@ -0,0 +1,234 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+# RFC-3: xtable-spark-runtime - in-job metadata sync via a thin Spark bundle
+
+## Proposers
+
+- @vinishjail97
+
+## Approvers
+- Anyone from the XTable community can approve/add feedback.
+
+## Status
Review Comment:
the rfc looks good, given the implementation is in the same PR as well, you
can look at my comments on those
##########
.github/workflows/spark-runtime-validation.yml:
##########
@@ -0,0 +1,85 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+name: Spark Runtime Bundle Validation
+
+# Validates the shaded xtable-spark-runtime bundle end-to-end:
ITXTableSparkRuntimeBundle
+# spark-submits the bundled jar against a real Spark distribution for one case
per direction
+# across Hudi, Iceberg and Delta. The IT self-skips unless SPARK_HOME is set,
so the main CI
+# never exercises it; this workflow installs a matching Spark 3.4 distro and
runs it.
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ !contains(github.ref, 'main') }}
+
+# Runs on every push/PR to the covered branches (no path filter) so the check
always
+# reports — required status checks that never trigger leave a PR blocked on a
pending check.
+on:
+ push:
+ branches:
+ - "main"
Review Comment:
The Spark version is hardcoded to `3.4.3`. Since this module targets the
Hudi 0.x / Spark 3.4 line, that's reasonable, but consider parameterizing it
(matrix or workflow input) or at least adding a comment explaining why 3.4.x is
pinned, so the validation stays easy to bump. (A maintainer also raised this
above.)
##########
.github/workflows/spark-runtime-validation.yml:
##########
@@ -0,0 +1,85 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+name: Spark Runtime Bundle Validation
+
+# Validates the shaded xtable-spark-runtime bundle end-to-end:
ITXTableSparkRuntimeBundle
+# spark-submits the bundled jar against a real Spark distribution for one case
per direction
+# across Hudi, Iceberg and Delta. The IT self-skips unless SPARK_HOME is set,
so the main CI
+# never exercises it; this workflow installs a matching Spark 3.4 distro and
runs it.
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ !contains(github.ref, 'main') }}
+
+# Runs on every push/PR to the covered branches (no path filter) so the check
always
+# reports — required status checks that never trigger leave a PR blocked on a
pending check.
+on:
+ push:
+ branches:
+ - "main"
+ - "branch-0.4"
+
+ pull_request:
+ branches:
+ - "main"
+ - "branch-0.4"
+
+ workflow_dispatch:
+
+env:
+ SPARK_VERSION: "3.4.3"
+ SPARK_HADOOP_PROFILE: "hadoop3"
+
+jobs:
+ validate-spark-runtime-bundle:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v5
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Cache Spark distribution
+ uses: actions/cache@v4
+ with:
+ path: ~/spark
+ key: spark-${{ env.SPARK_VERSION }}-bin-${{ env.SPARK_HADOOP_PROFILE
}}
+
+ - name: Install Spark
+ run: |
+ set -euo pipefail
+ SPARK_DIR="spark-${SPARK_VERSION}-bin-${SPARK_HADOOP_PROFILE}"
Review Comment:
curious, any way to specify any caching so that we don't install spark each
run?
Not blocking - sth to think about
##########
xtable-spark-runtime/src/main/resources/META-INF/NOTICE-bundled:
##########
@@ -0,0 +1,22 @@
+Apache XTable (incubating)
+Copyright 2024-2026 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (https://www.apache.org/).
+
+--------------------------------------------------------------------------------
+
+This binary artifact bundles the following projects with NOTICE:
+
+--------------------------------------------------------------------------------
+
+Group: commons-cli Name: commons-cli Version: 1.5.0
Review Comment:
are we shading this fully. In my adventures, commons version mismatches left
and right with hadoop/spark classes/installs all the time
##########
xtable-spark-runtime/pom.xml:
##########
@@ -0,0 +1,382 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You under the Apache License, Version 2.0
+ ~ (the "License"); you may not use this file except in compliance with
+ ~ the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.xtable</groupId>
+ <artifactId>xtable</artifactId>
+ <version>0.2.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>xtable-spark-runtime_${scala.binary.version}</artifactId>
+ <name>XTable Project Spark Runtime</name>
+ <description>
+ Thin, relocated, drop-in Spark bundle that runs incremental XTable
metadata sync. v1 ships
+ the standalone spark-submit entry point (XTableSparkSync); the
config-only, driver-side
+ QueryExecutionListener on-ramp is a planned follow-up (see rfc/rfc-3).
+ </description>
+
+ <dependencies>
+ <!-- XTable engine (bundled) -->
+ <dependency>
+ <groupId>org.apache.xtable</groupId>
+ <artifactId>xtable-core_${scala.binary.version}</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <!-- Command-line parsing for the standalone XTableSparkSync entry
point (bundled). -->
+ <dependency>
+ <groupId>commons-cli</groupId>
+ <artifactId>commons-cli</artifactId>
+ </dependency>
+
+ <!-- Logging API (lombok @Log4j2); provided by the cluster runtime -->
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- Spark: provided by the cluster. Brings QueryExecutionListener,
QueryExecution,
+ Catalyst LogicalPlan, and SparkSession. -->
+ <dependency>
+ <groupId>org.apache.spark</groupId>
+ <artifactId>spark-sql_${scala.binary.version}</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- Hadoop: provided by the cluster. -->
+ <dependency>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-common</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <!--
+ Engine libraries (Hudi / Iceberg / Delta) are provided, not bundled:
the user brings their
+ own engine version and the thin bundle stays compatible across
versions (see the shade
+ plugin below). Hudi is supplied as the hudi-spark bundle, whose
regenerated DecimalWrapper
+ links cleanly on Avro >=1.10; raw hudi-common's
Avro-1.8/1.9-generated model classes
+ reference org.apache.avro.data.TimeConversions.TimeConversion,
removed in Avro 1.10 and thus
+ absent from the Avro 1.12 that Iceberg 1.9.2 requires.
+ -->
+ <dependency>
+ <groupId>org.apache.hudi</groupId>
+
<artifactId>hudi-spark${spark.version.prefix}-bundle_${scala.binary.version}</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <!--
+ XTable's Hudi TARGET writes via the Java client
(HoodieJavaEngineContext), which the Spark
Review Comment:
+1 . we should keep it this way, and publish the support matrix of what
format versions works for source -> target? (could we add docs for that?)
##########
rfc/rfc-3/rfc-3.md:
##########
@@ -0,0 +1,234 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+# RFC-3: xtable-spark-runtime - in-job metadata sync via a thin Spark bundle
+
+## Proposers
+
+- @vinishjail97
+
+## Approvers
+- Anyone from the XTable community can approve/add feedback.
+
+## Status
+
+GH Feature Request: https://github.com/apache/incubator-xtable/issues/836
+
+> Please keep the status updated in `rfc/README.md`.
+
+## Abstract
+
+XTable's modules are published to Maven, so a user can depend on `xtable-core`
and assemble their own
+runtime today. But there is no maintained, thin, drop-in artifact — everyone
re-solves the same
+shading/classpath problem, or uses `xtable-utilities`, an unshaded ~1 GB fat
jar that is not practical
+to add to a Spark job and requires running a separate `RunSync` process with
YAML config files.
+
+XTable conversion is metadata-only and lightweight, so the common case — "I
already write this table
+with Spark, keep it in sync in other formats" — should be a one-dependency,
config-only addition to an
+existing pipeline. This RFC proposes `xtable-spark-runtime`: a thin, relocated
Spark bundle that
+registers a driver-side listener and, after each successful write to a source
table, runs an
+incremental `ConversionController.sync(...)` for the configured targets. It
complements — does not
+replace — the standalone CLI (`RunSync`).
+
+## Background
+
+Everything needed to run a sync already exists in the engine; the gap is
purely packaging and a trigger:
+
+- The entire sync entry surface is Hadoop-`Configuration`-only: `new
ConversionController(conf).sync(config, sourceProvider)`.
+ The caller supplies only the **source** `ConversionSourceProvider`; target
providers are created
+ internally by `ConversionTargetFactory` (`ConversionController.java`).
Source providers instantiate
+ directly (`new HudiConversionSourceProvider()` /
`IcebergConversionSourceProvider` /
+ `DeltaConversionSourceProvider`) and take `.init(conf)`.
+- `SyncMode.INCREMENTAL` is self-healing: `ConversionController` auto-falls
back to a full snapshot
+ per-target when there is no prior sync metadata or incremental is not safe
(`isIncrementalSyncSufficient`).
+ First sync is effectively full; subsequent syncs are incremental.
+- The sync watermark (last-synced instant, pending commits) is persisted in
the **target's**
+ `TableSyncMetadata`. There is no additional state to keep on the client side.
+- `xtable-hudi-support-extensions` is a pure-Java precedent for a
Spark-adjacent module whose
+ `XTableSyncTool` already builds `SourceTable`/`TargetTable` and calls
`ConversionController.sync(...)`;
+ it declares Spark deps as `provided` and uses a Scala-suffixed artifactId.
+- Delta conversion currently runs Delta-on-Spark in-process (`delta-core`);
`DeltaConversionUtils.buildSparkSession`
+ uses `SparkSession.builder()...getOrCreate()` with no `master`, so it reuses
the host job's active
+ session rather than starting a second `SparkContext`. (A Spark-free Delta
**Kernel** path exists and,
+ once it becomes the default, removes the per-Spark-version coupling —
tracked separately.)
+
+## Implementation
+
+A new **pure-Java** module `xtable-spark-runtime_${scala.binary.version}`
(package `org.apache.xtable.spark`).
+
+### What ships in v1
+
+This PR delivers the **thin bundle plus the standalone `spark-submit` entry
point
+`XTableSparkSync`** — enough to run a relocated, in-cluster sync from one
command. The config-only,
+driver-side auto-sync (`XTableSyncListener` registered via
`spark.sql.queryExecutionListeners`, with
+`XTableSparkConfig` and `PlanTargetResolver`) is the headline on-ramp this RFC
designs toward, but it
+is **deferred to a follow-up PR** — those classes are not in v1. The listener
design is retained
+below as the target so the entry point and config schema stay aligned.
+
+### Activation — v1 (CLI entry point)
Review Comment:
Can we also document how to call this from code i.e from within pySpark or
spark code? These could go into the quickstart as well. Can we revamp that
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]