Skip to content

[#8771] feat(spark-connector): Add Spark 4.0 support, drop 3.3 and 3.4 - #12414

Open
LuciferYang wants to merge 5 commits into
apache:mainfrom
LuciferYang:spark4-support
Open

[#8771] feat(spark-connector): Add Spark 4.0 support, drop 3.3 and 3.4#12414
LuciferYang wants to merge 5 commits into
apache:mainfrom
LuciferYang:spark4-support

Conversation

@LuciferYang

@LuciferYang LuciferYang commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

This revision replaces what this branch previously proposed. The earlier one added Spark 4.0 on top of the 3.3/3.4/3.5 matrix; review here asked for the 3.x matrix to shrink first and, per @diqiu50, for the initial submission to cover two versions so the approach and its maintenance cost can be judged from something concrete. The final matrix is Spark 3.5 and 4.0. Spark 4.1 exists locally as a follow-up and is not part of this PR.

Three commits, each self-contained:

1. Support Spark 3.5 only for the 3.x line. Spark 3.3 and 3.4 are past their maintenance windows; 3.5.x is on extended LTS through November 2027, so it is the only 3.x line that will see another release. With them gone, adaptations that existed only to bridge 3.3/3.4 collapse back into spark-common: the TimestampNTZType mapping and UpdateColumnDefaultValue conversion move into SparkTypeConverter and SparkTableChangeConverter, loadTable(Identifier, Set<TableWritePrivilege>) moves into BaseCatalog, and four converter subclasses plus the two version modules go away. SparkHiveTable and HiveGravitinoOperationOperator switch to the plural PartitionsAlreadyExistException, because SupportsPartitionManagement.createPartition changed its throws clause in Spark 3.4.

2. Compile spark-common as a shared source set. spark-common stops being a Gradle module. Each version module composes its own source set from spark-common/src/main/java, spark-common/src/main/spark<NN> and its own src/main/java. The test tree works the same way, replacing the testArtifacts jar the version modules used to consume.

Compiling the shared code once against one Spark version and running it on another is how a version-specific bug hides: the compiler only ever sees the API the pinned version exposes. Per-version compilation means each line's compiler checks the shared code against its own API, and the shared tests run on each line rather than only on the pinned one. Commit 3 shows this paying off immediately.

3. Add Spark 4.0 support. New v4.0/spark and v4.0/spark-runtime, publishing gravitino-spark-4.0_2.13 and gravitino-spark-connector-runtime-4.0_2.13. Spark 4 is Scala 2.13 only and needs JDK 17, so both modules pin 2.13 rather than reading -PscalaVersion, and they opt out of the repository's JDK 8 target.

Only one class needs a Spark 4 flavor: the authorization parser, because Spark 4 added an abstract parseRoutineParam to ParserInterface. spark35 additionally holds the Paimon package, since Paimon publishes no paimon-spark-4.x artifact at the version this repository pins; the Spark 4 build excludes it and the driver plugin skips the Paimon session extension when the catalog is unavailable, rather than failing SparkSession construction.

Iceberg's loadProcedure moves out of the shared GravitinoIcebergCatalog into the per-version catalog subclasses. On 3.x ProcedureCatalog is a class Iceberg ships and loadProcedure returns Iceberg's Procedure; on 4.x Spark ships its own and the return type is UnboundProcedure. implements is declaration-level, so this one cannot be handled by same-name flavor files.

Two fixes to the shared code, both surfaced by compiling the shared tests against Spark 4 for the first time:

  • SparkTypeConverter and SparkPartitionUtils checked instanceof StringType before VarcharType and CharType. Spark 4 made both extend StringType, so that branch swallowed them and silently dropped the length. Three shared tests caught it. Subtypes are now checked first, which is correct on every supported version.
  • the shared Iceberg IT built columns with new Column(Literal.create(...)). Spark 4 rebuilt Column on ColumnNode and dropped that constructor; it now uses functions.lit(), available on both.

The 4.0 ITs pin Jersey, HK2 and jakarta.validation back to the javax flavor on the test runtime classpath: the embedded Gravitino server serves REST on Jetty 9 with Jersey 2, and Spark 4 brings the jakarta flavor transitively via spark-hive. Spark's own web UI cannot coexist with that, so the ITs disable it. Both are test-only; the runtime jar bundles neither Jersey nor the server.

Why are the changes needed?

Spark 4.0 is GA and the connector topped out at 3.5, leaving Spark 4 users with no supported path. The VarcharType bug above is the concrete argument for per-version compilation: it was reachable from any Spark 4 session and invisible to a build that only ever compiled the shared code against 3.x.

Fix: #8771

Does this PR introduce any user-facing change?

Yes. Two removals, one addition, and one behavior change on the JDBC PostgreSQL catalog.

  • gravitino-spark-connector-runtime-3.3 and -3.4 are no longer published. Spark 3.3 and 3.4 users stay on an earlier Gravitino release.
  • gravitino-spark-common is no longer published. It was an implementation detail of this repository's own version modules, which now compile its sources directly; nothing outside the repository consumed it.
  • gravitino-spark-4.0_2.13 and gravitino-spark-connector-runtime-4.0_2.13 are new. On Spark 4 the connector requires Scala 2.13 and JDK 17, and the Paimon catalog is unavailable. Glue resolves a catalog class on 4.0 but is verified on 3.5 only, because the patched Hive JARs it needs are published for Spark 3 alone.
  • GravitinoPostgreSqlCatalogSpark35 extends the shared JDBC catalog directly rather than a *Spark34 subclass, so it never picked up the three version-specific overrides the other five catalogs had. Folding those into the shared classes gives it all three at once: timestamp_ntz columns are accepted instead of rejected; ALTER TABLE … ALTER COLUMN … SET DEFAULT works instead of failing in the connector (the PostgreSQL backend already supported it); and loadTable(Identifier, Set<TableWritePrivilege>) now routes through the write path, so write operations on a PostgreSQL catalog are checked against write privileges where previously they were not. A user whose grants cover reads but not writes can start seeing denials. This aligns jdbc-postgresql with jdbc-mysql, which has had all three since [#10181] feat(spark-connector): Support TableWritePrivilege for Spark 3.5+ authorization #10194.

How was this patch tested?

Unit tests: 3.5 at 118 cases under Scala 2.12 and 115 under 2.13, the difference being the Paimon converter test the build already excluded on 2.13 before this PR; 4.0 at 116 cases. No failures.

Docker ITs on 4.0, four suites: Hive 51, Iceberg on the Hive backend 47, MySQL 34, PostgreSQL 33. 165 cases reported, 126 run and 39 skipped by their own preconditions, 0 failures. The full 3.5 suite as a regression check, ten suites across Hive, Iceberg, MySQL, PostgreSQL, Paimon, authorization and JWKS: 296 cases reported, 167 run, 0 failures. Of the 129 skips, 85 are two suites skipping wholesale for lack of an environment, Glue at 42 and the Iceberg REST backend at 43.

Also both runtime shadow jars, repo-wide spotlessCheck, and javadoc for both modules. The 4.0 jar was checked to contain the five *Spark40 catalog classes and zero Paimon classes; the 3.5 jar still carries its eight.

One test worth naming: TestGravitinoDriverPlugin.testPaimonExtensionFollowsCatalogAvailability asserts that the Paimon session extension is registered exactly when the catalog adaptor resolves a Paimon catalog. Because the shared test tree compiles into both modules, the same assertion expects registration on 3.5 and skipping on 4.0, which is what makes it a regression guard rather than a tautology.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Code Coverage Report

Overall Project 68.32% -0.49% 🟢
Files changed 45.51% 🔴

Module Coverage
aliyun 1.72% 🔴
api 49.31% 🟢
authorization-common 85.96% 🟢
aws 42.04% 🟢
azure 2.47% 🔴
catalog-common 9.92% 🔴
catalog-fileset 79.77% 🟢
catalog-glue 68.95% 🟢
catalog-hive 79.4% 🟢
catalog-jdbc-common 45.7% 🟢
catalog-jdbc-doris 81.8% 🟢
catalog-jdbc-mysql 79.33% 🟢
catalog-jdbc-postgresql 83.39% 🟢
catalog-jdbc-starrocks 79.16% 🟢
catalog-kafka 77.01% 🟢
catalog-lakehouse-generic 59.18% 🟢
catalog-lakehouse-hudi 79.1% 🟢
catalog-lakehouse-iceberg 85.86% 🟢
catalog-lakehouse-paimon 84.23% 🟢
catalog-model 77.72% 🟢
cli 44.48% 🟢
client-java 78.56% 🟢
common 52.75% 🟢
core 83.63% 🟢
filesystem-hadoop3 77.28% 🟢
flink 0.0% 🔴
flink-common 48.68% 🟢
flink-runtime 0.0% 🔴
gcp 14.12% 🔴
hadoop-auth 68.0% 🟢
hadoop-common 12.7% 🔴
hive-metastore-common 53.4% 🟢
iceberg-aliyun-bundle 0.0% 🔴
iceberg-common 64.75% 🟢
iceberg-rest-server 75.03% 🟢
idp-basic 86.02% 🟢
integration-test-common 0.0% 🔴
jobs 62.92% 🟢
lance-common 31.75% 🔴
lance-rest-server 63.47% 🟢
lineage 53.02% 🟢
optimizer 83.17% 🟢
optimizer-api 21.95% 🔴
server 87.25% 🟢
server-common 79.41% 🟢
spark 48.09% +0.19% 🟢
tencent 69.84% 🟢
trino-connector 40.29% 🟢
Files
Module File Coverage
spark SparkCatalogs.java 92.0% 🟢
SparkCatalogs.java 92.0% 🟢
GravitinoGlueCatalogSpark35.java 0.0% 🔴
GravitinoHiveCatalogSpark35.java 0.0% 🔴
GravitinoIcebergCatalogSpark35.java 0.0% 🔴
GravitinoJdbcCatalogSpark35.java 0.0% 🔴
GravitinoPaimonCatalogSpark35.java 0.0% 🔴
GravitinoGlueCatalogSpark40.java 0.0% 🔴
GravitinoHiveCatalogSpark40.java 0.0% 🔴
GravitinoIcebergCatalogSpark40.java 0.0% 🔴
GravitinoJdbcCatalogSpark40.java 0.0% 🔴
GravitinoPostgreSqlCatalogSpark40.java 0.0% 🔴
spark-common SparkCatalogKind.java 100.0% 🟢
SparkHiveTypeConverter.java 100.0% 🟢
SparkTypeConverter.java 95.35% 🟢
AuthorizationTable.java 95.24% 🟢
SparkTableChangeConverter.java 92.75% 🟢
SparkPartitionUtils.java 88.17% 🟢
SparkJdbcTypeConverter.java 83.33% 🟢
GravitinoDriverPlugin.java 31.43% 🔴
BaseCatalog.java 14.76% 🔴
SparkHiveTable.java 0.0% 🔴
GravitinoIcebergCatalog.java 0.0% 🔴
HiveGravitinoOperationOperator.java 0.0% 🔴

@roryqi
roryqi requested a review from diqiu50 August 11, 2026 05:47
@LuciferYang

Copy link
Copy Markdown
Contributor Author

Thank you @roryqi

@diqiu50

diqiu50 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Since we are going to refactor this part, I think the architecture should follow two principles:

  1. spark-common should not depend on any specific Spark version. All the suported spark vertion can build it.
  2. Each supported Spark version, including minor versions, should depend only on spark-common and should not depend on other version-specific modules. using sourceset dependency is better than binary jars

The tests should follow the same structure as well. This will keep each Spark version independent and make it much easier to deprecate or remove old versions in the future.

@LuciferYang

Copy link
Copy Markdown
Contributor Author

@diqiu50 Thanks for your suggestions. I'll take a look and get back to you later.

@LuciferYang

Copy link
Copy Markdown
Contributor Author

Agreed on both principles, and I am happy to take the refactor as a separate PR ahead of this one, then rebase this one on top. It changes how all of 3.x builds, so it deserves review on its own. Below is what I found while sizing it, and a layout to check against before I start.

Where the version boundaries actually fall. I tried compiling spark-common against 3.5 and it fails on SparkHiveTable: SupportsPartitionManagement.createPartition declares the singular PartitionAlreadyExistsException in 3.3 and the plural PartitionsAlreadyExistException in 3.4, 3.5 and 4.0. So that split is 3.3 versus 3.4+. ParserInterface splits differently: 3.3, 3.4 and 3.5 all require the same 8 methods, and 4.0 adds parseRoutineParam. And Paimon is a third axis: paimon-spark-4.0 first appears in Paimon 1.3.0, and we pin 1.2.0 (libs.versions.toml:75), so no Spark 4 Paimon artifact exists at the version we build against.

Three different boundaries means the layout has to be additive srcDir lists rather than one directory per version:

dir contents used by
java the 35 version-neutral files all
spark3x 4 Paimon classes + authz parser (3.x form) 3.3, 3.4, 3.5
spark33 SparkHiveTable, HiveGravitinoOperationOperator (singular exception) 3.3
spark34plus the same 2 with the plural exception, + the 4 *34 converters 3.4, 3.5, 4.0
spark40 authz parser (4.0 form) 4.0

I checked the pieces that make spark34plus viable, and compiled that combination to be sure: shared sources plus the spark4 SparkHiveTable and HiveGravitinoOperationOperator plus the four *34 converters build cleanly against both 3.4.3 and 3.5.3. The PartitionsAlreadyExistException(String, InternalRow, StructType) constructor they use exists in both, and the four converters are byte-identical to their v3.4 originals. Moving them there removes both the copy this PR makes and the v3.5 to v3.4 edge, which is the concrete payoff of your principle 2. Today v3.5/spark depends on spark-connector:spark-3.4: four catalogs extend *Spark34, and GravitinoJdbcCatalogSpark35 uses the *34 converters.

The shared tests are the larger half. All four version modules consume spark-common's test classes as a testArtifacts jar, ~50 files compiled once against 3.3. Under "tests follow the same structure" that tree needs the same flavor split and per-version compilation, and testArtifacts stops being shareable as a binary. That is also what would make the 4.0 ITs exercise 4.0-compiled shared code instead of 3.3 bytecode, which they do not today. The Paimon tests are part of this: three files today, excluded by a Scala-version-keyed rule rather than a Spark-version one, so on the test side that rule does need to change even though the directory split handles the main sources.

For scale, the shared sources end up compiled once per supported (Spark, Scala) pair after the refactor: 3.3/3.4/3.5 under 2.12 and 2.13, plus 4.0 under 2.13, with the 2.13 ones excluding the Paimon subset.

One limit worth stating: the Iceberg catalog subclasses cannot fully follow principle 2. implements ProcedureCatalog resolves to a class Iceberg ships on 3.x and one Spark ships on 4.0, and implements is declaration-level, so those stay per-version. The refactor can reach "no cross-version module dependency" but not "no per-version code".

Questions:

  1. Does the table above look right, and is additive srcDir composition acceptable? I would rather build the layout you want than guess at naming.
  2. Should spark-common stay a Gradle module, or should the version modules compile its sources directly? Two things keep it from being purely sources-only: it owns extra["glueHiveJarsDir"] and the downloadGlueHiveJars task that all three v3.x modules reach through evaluationDependsOn, and it is the only producer of the testArtifacts jar the spark-connector modules share. Both are relocatable, but that is a decision rather than a file move. Note also that its compileJava is pinned to --release 8 by a path-prefix rule in the root build while the 4.0 compilation needs 17, which is why spark4-common had to be listed in jdk17OnlyProjectPaths.
  3. If it stays a module, does it keep publishing as gravitino-spark-common (currently no Scala or Spark suffix, despite being built against 3.3)? Once the version modules consume sources instead, that jar has no internal consumer.

@LuciferYang

Copy link
Copy Markdown
Contributor Author

friendly ping @diqiu50

@diqiu50

diqiu50 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

We could consider supporting Spark 4.1 instead of 4.0. Spark 4.1 is more stable.

@LuciferYang

LuciferYang commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

No problem, I can adjust it to add support for Spark 4.1. Do you have any feedback on the three questions I raised earlier? Thanks

@LuciferYang

LuciferYang commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

However, my company’s business are still running on Spark 4.0.x. Is it feasible to support both Spark 4.0 and 4.1 simultaneously? I can submit multiple patches to achieve compatibility with both versions. @diqiu50

also cc @jerryshao

@diqiu50

diqiu50 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

I think we can simplify the layout further.

First, we don't need to keep Spark 3.3 support, so there is no need to introduce a spark33 source set.

Second, I don't think spark-common needs to remain a Gradle module. I would prefer to make it a shared source set that is compiled directly by each supported Spark version. This avoids giving spark-common an implicit Spark/JDK/Scala version and keeps each version fully independent.

Conceptually:

3.4 -> common + spark34
3.5 -> common + spark35
4.0 -> common + spark40

The tests should follow the same model: shared test sources should be compiled separately for each Spark version rather than distributed as a precompiled testArtifacts JAR.

The Glue download task and similar build logic can be moved out separately. I don't think build-time utilities are a strong enough reason to keep spark-common as a standalone module.

I'm also fine with a reasonable amount of code duplication between version-specific implementations. Keeping clear version boundaries, avoiding cross-version dependencies, and making each supported Spark version independently removable are more important than eliminating every duplicated line of code.

@LuciferYang

Copy link
Copy Markdown
Contributor Author

Thanks for your feedback. @diqiu50

@diqiu50

diqiu50 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Thanks @LuciferYang
We can tolerate some code duplication between spark34, spark35, and spark40, but we should first evaluate how much duplication there actually is. If it is significant, we should consider extracting more truly version-neutral code into common while still avoiding cross-version dependencies.

@LuciferYang

Copy link
Copy Markdown
Contributor Author

Thanks @LuciferYang We can tolerate some code duplication between spark34, spark35, and spark40, but we should first evaluate how much duplication there actually is. If it is significant, we should consider extracting more truly version-neutral code into common while still avoiding cross-version dependencies.

Alright, let me evaluate it against this rule first.

@jerryshao

jerryshao commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Hi @LuciferYang , we also discussed offline. I think we can support Spark 4.0 along with 4.1. But I don't want to maintain too many Spark versions. So I want to drop support for Spark 3.4, WDYT? Also cc @diqiu50

@LuciferYang

Copy link
Copy Markdown
Contributor Author

Agreed. Spark's versioning policy keeps 3.5.x on extended LTS through November 2027, while 3.3 and 3.4 are past their maintenance windows: the last releases were 3.3.4 and 3.4.4 (October 2024).

This also answers the duplication question @diqiu50 raised. Under the strict one-directory-per-version layout, dropping 3.4 takes duplicated lines from 873 to 285, and the v3.5 to v3.4 module dependency disappears by construction, which is the cross-version edge principle 2 targets. The four *34 converters merge back into their base classes, since TimestampNTZType and UpdateColumnDefaultValue exist on every remaining version.

Final matrix: 3.5, 4.0, 4.1. One note: this stops publishing gravitino-spark-connector-runtime-3.4, same as 3.3.

@LuciferYang LuciferYang changed the title [#8771] feat(spark-connector): Support Apache Spark 4.0 [#8771] refactor(spark-connector): Support Spark 3.5 only and compile spark-common per version Aug 13, 2026
@LuciferYang
LuciferYang marked this pull request as draft August 13, 2026 07:12
@diqiu50

diqiu50 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

I suggest we start by supporting two versions. That will give us a clearer picture of the feasibility of this approach and the maintenance cost involved.

@LuciferYang

Copy link
Copy Markdown
Contributor Author

I suggest we start by supporting two versions. That will give us a clearer picture of the feasibility of this approach and the maintenance cost involved.

Got it, I'll combine my local PR1 and PR2 into a single submission here, and ping you once the code is ready.

…he 3.x line

Spark 3.3 and 3.4 are past their maintenance windows, so 3.5 is the only 3.x line
that will see another release. Dropping them lets the version-specific converter
and catalog subclasses collapse back into spark-common.

Stops publishing gravitino-spark-connector-runtime-3.3 and -3.4.
…ared source set

spark-common stops being a Gradle module. Each version module composes its own
source set from spark-common/src/main/java, spark-common/src/main/spark<NN> and
its own src/main/java, so the shared code is compiled against every supported
Spark version rather than once. The test tree works the same way, replacing the
testArtifacts jar.

Stops publishing gravitino-spark-common.
Adds v4.0/spark and v4.0/spark-runtime, publishing gravitino-spark-4.0_2.13
and gravitino-spark-connector-runtime-4.0_2.13. Spark 4 is Scala 2.13 only
and needs JDK 17.

Moves SparkIcebergTable and SparkJdbcTable back to the shared tree: both
compile unchanged on 4.0, so the spark35 copies were duplication. Only the
authorization parser needs a per-version flavor.
@LuciferYang LuciferYang changed the title [#8771] refactor(spark-connector): Support Spark 3.5 only and compile spark-common per version [#8771] feat(spark-connector): Add Spark 4.0 support, drop 3.3 and 3.4 Aug 14, 2026
…xclusion lists

The spark-connector ITs are meant to run only in the dedicated Spark IT
workflow; every other workflow excludes them. v4.0 was added without being
added to those lists, so its ITs also ran inside the backend IT job, where
-PjdbcBackend=mysql makes two suites drop and recreate the same
MYSQL_JDBC_BACKEND database out from under each other. Excluded alongside 3.5
in backend IT and in the two Trino workflows.

Also raises the build job cap from 90 to 120 minutes. It has been finishing in
75-84 minutes and :core:test alone has varied from 27 to 45 minutes across runs
on identical code, so the margin was already thinner than the runner's own
variance.
@LuciferYang
LuciferYang marked this pull request as ready for review August 14, 2026 06:26
@LuciferYang

Copy link
Copy Markdown
Contributor Author

CI passed, Please review it when you have time, and I will revise it if there are any issues. Thanks @diqiu50

@diqiu50

diqiu50 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

The Spark version is already fixed by the jar on the classpath, so CatalogNameAdaptor reading SPARK_VERSION and looking the catalog classes up again is a second dispatch on the same information — and it costs us 11 hardcoded class names that nothing checks, plus a misleading provider is not supported yet warning when the jar does not match the cluster. Could each version module declare its own catalog classes and pass them to the driver plugin, instead of the shared code looking them up by version? Then the names are checked at compile time, CatalogNameAdaptor and the version package go away, and adding a Spark version stops touching shared code.

…are its own catalogs

CatalogNameAdaptor read SPARK_VERSION at runtime and looked catalog classes up
by string, a second dispatch on information the jar on the classpath already
fixes. It cost 11 hardcoded class names that nothing checked and a misleading
"provider is not supported yet" warning when the jar did not match the cluster.

Each version module now declares a SparkCatalogs class at the same FQN, holding
compile-time class references, so the driver plugin reads the table from
whichever jar is present. Provider to catalog-kind mapping is the same on every
version, so that stays shared as SparkCatalogKind.

Adding a Spark version no longer touches shared code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Improvement] Spark v4 support

3 participants