This is an automated email from the ASF dual-hosted git repository.

wchevreuil pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hbase-connectors.git


The following commit(s) were added to refs/heads/master by this push:
     new 0c799b8  HBASE-30185 Port hbase-spark lower level RDD API to spark4 
module (#157)
0c799b8 is described below

commit 0c799b8a9fc4133243035b083ae52631e9f72027
Author: Wellington Ramos Chevreuil <[email protected]>
AuthorDate: Mon Jul 13 11:52:00 2026 +0100

    HBASE-30185 Port hbase-spark lower level RDD API to spark4 module (#157)
    
    Co-authored-by: Claude Code (claude-opus-4-6) <[email protected]>
    
    Signed-off-by: Peter Somogyi <[email protected]>
---
 dev-support/jenkins/Dockerfile                     |   6 +
 dev-support/jenkins/Jenkinsfile                    |   2 +-
 dev-support/jenkins/hbase-personality.sh           |  11 +
 .../jenkins/jenkins_precommit_github_yetus.sh      |   3 +-
 pom.xml                                            |  14 +-
 spark4/hbase-spark-pushdown/pom.xml                | 380 ++++++++++-----------
 spark4/hbase-spark4/pom.xml                        |  99 +++++-
 .../apache/hadoop/hbase/spark/HBaseContext.scala   | 170 +++++++++
 .../org/apache/hadoop/hbase/spark/Logging.scala    | 101 ++++++
 .../{Spark4Placeholder.scala => NewHBaseRDD.scala} |  25 +-
 .../hadoop/hbase/spark/HBaseContextSuite.scala     | 161 +++++++++
 spark4/pom.xml                                     |  14 +-
 12 files changed, 774 insertions(+), 212 deletions(-)

diff --git a/dev-support/jenkins/Dockerfile b/dev-support/jenkins/Dockerfile
index ad68df9..45551b9 100644
--- a/dev-support/jenkins/Dockerfile
+++ b/dev-support/jenkins/Dockerfile
@@ -28,6 +28,12 @@ RUN apt-get -q update && apt-get -q install 
--no-install-recommends -y \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/*
 
+# JDK 17 for the Spark4 profile (maven-enforcer-plugin requires JDK 17+)
+RUN wget -q 
https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.11%2B9/OpenJDK17U-jdk_x64_linux_hotspot_17.0.11_9.tar.gz
 -O /tmp/jdk17.tar.gz && \
+    mkdir -p /usr/local/openjdk-17 && \
+    tar -xzf /tmp/jdk17.tar.gz --strip-components=1 -C /usr/local/openjdk-17 
&& \
+    rm /tmp/jdk17.tar.gz
+
 ###
 # Avoid out of memory errors in builds
 ###
diff --git a/dev-support/jenkins/Jenkinsfile b/dev-support/jenkins/Jenkinsfile
index eabf47b..d788de9 100644
--- a/dev-support/jenkins/Jenkinsfile
+++ b/dev-support/jenkins/Jenkinsfile
@@ -35,7 +35,7 @@ pipeline {
         PATCH_REL = 'output'
         YETUS_REL = 'yetus'
         // Branch or tag name.  Yetus release tags are 'rel/X.Y.Z'
-        YETUS_VERSION = 'rel/0.12.0'
+        YETUS_VERSION = 'rel/0.15.0'
         DOCKERFILE_REL = "${SRC_REL}/dev-support/jenkins/Dockerfile"
         YETUS_DRIVER_REL = 
"${SRC_REL}/dev-support/jenkins/jenkins_precommit_github_yetus.sh"
         ARCHIVE_PATTERN_LIST = 
'TEST-*.xml,org.apache.h*.txt,*.dumpstream,*.dump'
diff --git a/dev-support/jenkins/hbase-personality.sh 
b/dev-support/jenkins/hbase-personality.sh
index 9ec5f3f..e3d899d 100755
--- a/dev-support/jenkins/hbase-personality.sh
+++ b/dev-support/jenkins/hbase-personality.sh
@@ -30,6 +30,17 @@ function personality_modules
   local extra=""
   local MODULES=("${CHANGED_MODULES[@]}")
 
+  # When spark4 modules are in the changed set, activate the spark4 profile
+  # so the full reactor (including hbase-spark-pushdown_2.13) is built.
+  # The maven-enforcer-plugin in the spark4 profile requires JDK 17+.
+  if [[ "${MODULES[*]}" =~ spark4 ]]; then
+    extra="${extra} -Pspark4"
+    if [[ "${JAVA_HOME}" != "/usr/local/openjdk-17" ]]; then
+      export JAVA_HOME=/usr/local/openjdk-17
+      export PATH="${JAVA_HOME}/bin:${PATH}"
+    fi
+  fi
+
   clear_personality_queue
 
   # Always to install at root.
diff --git a/dev-support/jenkins/jenkins_precommit_github_yetus.sh 
b/dev-support/jenkins/jenkins_precommit_github_yetus.sh
index 5a03ada..5e2a2aa 100755
--- a/dev-support/jenkins/jenkins_precommit_github_yetus.sh
+++ b/dev-support/jenkins/jenkins_precommit_github_yetus.sh
@@ -85,8 +85,7 @@ YETUS_ARGS+=("--brief-report-file=${PATCHDIR}/brief.txt")
 YETUS_ARGS+=("--console-report-file=${PATCHDIR}/console.txt")
 YETUS_ARGS+=("--html-report-file=${PATCHDIR}/report.html")
 # enable writing back to Github
-YETUS_ARGS+=("--github-password=${GITHUB_PASSWORD}")
-YETUS_ARGS+=("--github-user=${GITHUB_USER}")
+YETUS_ARGS+=("--github-token=${GITHUB_PASSWORD}")
 # auto-kill any surefire stragglers during unit test runs
 YETUS_ARGS+=("--reapermode=kill")
 # set relatively high limits for ASF machines
diff --git a/pom.xml b/pom.xml
index 0bf8840..16d19ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -153,7 +153,7 @@
     <log4j2.version>2.25.3</log4j2.version>
     <reload4j.version>1.2.25</reload4j.version>
     <checkstyle.version>8.45.1</checkstyle.version>
-    <spotless.version>2.27.2</spotless.version>
+    <spotless.version>2.30.0</spotless.version>
     <maven.checkstyle.version>3.2.1</maven.checkstyle.version>
     <hbase.checkstyle.version>2.5.0</hbase.checkstyle.version>
     <surefire.version>3.5.2</surefire.version>
@@ -215,6 +215,9 @@
     <spark-surefire.jdk17.flags/>
     <!-- spark4/hbase-spark-pushdown artifactId suffix; exclude is a no-op 
unless -Pspark4 -->
     <spark4.scala.binary.version>2.13</spark4.scala.binary.version>
+    <spark4.scalatest.version>3.2.17</spark4.scalatest.version>
+    
<spark4.scalatest.maven.plugin.version>2.2.0</spark4.scalatest.maven.plugin.version>
+    <nashorn.version>15.4</nashorn.version>
   </properties>
   <dependencyManagement>
     <dependencies>
@@ -809,6 +812,8 @@
               <excludes>
                 <exclude>**/target/**</exclude>
                 <exclude>**/dependency-reduced-pom.xml</exclude>
+                <exclude>**/.flattened-pom.xml</exclude>
+                <exclude>.flattened-pom.xml</exclude>
               </excludes>
               <!-- define the steps to apply to those files -->
               <trimTrailingWhitespace/>
@@ -839,6 +844,13 @@
             </format>
           </formats>
         </configuration>
+        <dependencies>
+          <dependency>
+            <groupId>org.openjdk.nashorn</groupId>
+            <artifactId>nashorn-core</artifactId>
+            <version>${nashorn.version}</version>
+          </dependency>
+        </dependencies>
       </plugin>
     </plugins>
     <extensions>
diff --git a/spark4/hbase-spark-pushdown/pom.xml 
b/spark4/hbase-spark-pushdown/pom.xml
index 5dd98b0..eff501f 100644
--- a/spark4/hbase-spark-pushdown/pom.xml
+++ b/spark4/hbase-spark-pushdown/pom.xml
@@ -19,200 +19,198 @@
 
 -->
 <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>
+  <modelVersion>4.0.0</modelVersion>
 
-    <parent>
-        <groupId>org.apache.hbase.connectors</groupId>
-        <artifactId>spark4</artifactId>
-        <version>${revision}</version>
-        <relativePath>../pom.xml</relativePath>
-    </parent>
+  <parent>
+    <groupId>org.apache.hbase.connectors</groupId>
+    <artifactId>spark4</artifactId>
+    <version>${revision}</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>
 
-    <groupId>org.apache.hbase.connectors.spark</groupId>
-    <artifactId>hbase-spark-pushdown_${scala.binary.version}</artifactId>
-    <name>Apache HBase - Spark Connector SQL Pushdown (shared for Spark 
4)</name>
-    <description>
-        Builds the same pushdown sources as spark/hbase-spark-pushdown against 
Spark 4 / Scala 2.13 (see module sources).
-    </description>
+  <groupId>org.apache.hbase.connectors.spark</groupId>
+  <artifactId>hbase-spark-pushdown_${scala.binary.version}</artifactId>
+  <name>Apache HBase - Spark Connector SQL Pushdown (shared for Spark 4)</name>
+  <description>Builds the same pushdown sources as spark/hbase-spark-pushdown 
against Spark 4 / Scala 2.13 (see module sources).</description>
 
-    <dependencies>
-        <dependency>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-api</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.hbase.thirdparty</groupId>
-            <artifactId>hbase-shaded-miscellaneous</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.scala-lang</groupId>
-            <artifactId>scala-library</artifactId>
-            <version>${scala.version}</version>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.spark</groupId>
-            <artifactId>spark-core_${scala.binary.version}</artifactId>
-            <version>${spark4.version}</version>
-            <scope>provided</scope>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.scala-lang</groupId>
-                    <artifactId>scala-library</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.scala-lang</groupId>
-                    <artifactId>scalap</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>com.google.code.findbugs</groupId>
-                    <artifactId>jsr305</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.xerial.snappy</groupId>
-                    <artifactId>snappy-java</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>xerces</groupId>
-                    <artifactId>xercesImpl</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.apache.hadoop</groupId>
-                    <artifactId>hadoop-client-api</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.apache.hadoop</groupId>
-                    <artifactId>hadoop-client-runtime</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.spark</groupId>
-            <artifactId>spark-sql_${scala.binary.version}</artifactId>
-            <version>${spark4.version}</version>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.hbase</groupId>
-            <artifactId>hbase-client</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.hbase</groupId>
-            <artifactId>hbase-common</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.hbase.connectors.spark</groupId>
-            <artifactId>hbase-spark-protocol-shaded</artifactId>
-            <version>${revision}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.yetus</groupId>
-            <artifactId>audience-annotations</artifactId>
-        </dependency>
-    </dependencies>
+  <dependencies>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase.thirdparty</groupId>
+      <artifactId>hbase-shaded-miscellaneous</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.scala-lang</groupId>
+      <artifactId>scala-library</artifactId>
+      <version>${scala.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.spark</groupId>
+      <artifactId>spark-core_${scala.binary.version}</artifactId>
+      <version>${spark4.version}</version>
+      <scope>provided</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>org.scala-lang</groupId>
+          <artifactId>scala-library</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.scala-lang</groupId>
+          <artifactId>scalap</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.google.code.findbugs</groupId>
+          <artifactId>jsr305</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.xerial.snappy</groupId>
+          <artifactId>snappy-java</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>xerces</groupId>
+          <artifactId>xercesImpl</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.apache.hadoop</groupId>
+          <artifactId>hadoop-client-api</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.apache.hadoop</groupId>
+          <artifactId>hadoop-client-runtime</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.spark</groupId>
+      <artifactId>spark-sql_${scala.binary.version}</artifactId>
+      <version>${spark4.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-client</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-common</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase.connectors.spark</groupId>
+      <artifactId>hbase-spark-protocol-shaded</artifactId>
+      <version>${revision}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.yetus</groupId>
+      <artifactId>audience-annotations</artifactId>
+    </dependency>
+  </dependencies>
 
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-            </plugin>
-            <plugin>
-                <groupId>org.codehaus.mojo</groupId>
-                <artifactId>build-helper-maven-plugin</artifactId>
-                <executions>
-                    <execution>
-                        <id>add-shared-pushdown-sources-java</id>
-                        <phase>validate</phase>
-                        <goals>
-                            <goal>add-source</goal>
-                        </goals>
-                        <configuration>
-                            <sources>
-                                
<source>${project.basedir}/../../spark/hbase-spark-pushdown/src/main/java</source>
-                                
<source>${project.basedir}/../../spark/hbase-spark-pushdown/src/main/scala</source>
-                            </sources>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-enforcer-plugin</artifactId>
-                <executions>
-                    <execution>
-                        <id>banned-scala</id>
-                        <goals>
-                            <goal>enforce</goal>
-                        </goals>
-                        <configuration>
-                            <skip>true</skip>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-checkstyle-plugin</artifactId>
-            </plugin>
-            <plugin>
-                <groupId>net.alchim31.maven</groupId>
-                <artifactId>scala-maven-plugin</artifactId>
-                <version>4.9.1</version>
-                <configuration>
-                    <charset>${project.build.sourceEncoding}</charset>
-                    <scalaVersion>${scala.version}</scalaVersion>
-                    <!-- Java depends on Scala; compile Scala before javac 
sees .java sources. -->
-                    <compileOrder>ScalaThenJava</compileOrder>
-                    <!-- Use javac from maven-compiler-plugin so 
protocol-shaded.jar is on the classpath (Zinc
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>add-shared-pushdown-sources-java</id>
+            <goals>
+              <goal>add-source</goal>
+            </goals>
+            <phase>validate</phase>
+            <configuration>
+              <sources>
+                
<source>${project.basedir}/../../spark/hbase-spark-pushdown/src/main/java</source>
+                
<source>${project.basedir}/../../spark/hbase-spark-pushdown/src/main/scala</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>banned-scala</id>
+            <goals>
+              <goal>enforce</goal>
+            </goals>
+            <configuration>
+              <skip>true</skip>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>net.alchim31.maven</groupId>
+        <artifactId>scala-maven-plugin</artifactId>
+        <version>4.9.1</version>
+        <configuration>
+          <charset>${project.build.sourceEncoding}</charset>
+          <scalaVersion>${scala.version}</scalaVersion>
+          <!-- Java depends on Scala; compile Scala before javac sees .java 
sources. -->
+          <compileOrder>ScalaThenJava</compileOrder>
+          <!-- Use javac from maven-compiler-plugin so protocol-shaded.jar is 
on the classpath (Zinc
                          integration missed it for translated source roots). 
-->
-                    <sendJavaToScalac>false</sendJavaToScalac>
-                    <args>
-                        <arg>-feature</arg>
-                        <arg>-release</arg>
-                        <arg>17</arg>
-                    </args>
-                    <source>17</source>
-                    <target>17</target>
-                </configuration>
-                <executions>
-                    <execution>
-                        <id>scala-compile-first</id>
-                        <goals>
-                            <goal>compile</goal>
-                        </goals>
-                        <phase>process-resources</phase>
-                    </execution>
-                    <execution>
-                        <id>scala-test-compile</id>
-                        <goals>
-                            <goal>testCompile</goal>
-                        </goals>
-                        <phase>process-test-resources</phase>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>net.revelc.code</groupId>
-                <artifactId>warbucks-maven-plugin</artifactId>
-                <version>1.1.0</version>
-                <configuration>
-                    <ignoreRuleFailures>true</ignoreRuleFailures>
-                    <rules>
-                        <rule>
-                            
<classPattern>(?!.*(.generated.|.tmpl.|\$|PushdownMappedField|SparkSQLPushDownFilter|org.apache.hadoop.hbase.spark.hbase.package)).*</classPattern>
-                            <includeTestClasses>false</includeTestClasses>
-                            <includePublicClasses>true</includePublicClasses>
-                            
<includePackagePrivateClasses>false</includePackagePrivateClasses>
-                            
<includeProtectedClasses>false</includeProtectedClasses>
-                            
<includePrivateClasses>false</includePrivateClasses>
-                            
<classAnnotationPattern>org[.]apache[.]yetus[.]audience[.]InterfaceAudience.*</classAnnotationPattern>
-                        </rule>
-                    </rules>
-                </configuration>
-            </plugin>
-        </plugins>
-    </build>
+          <sendJavaToScalac>false</sendJavaToScalac>
+          <args>
+            <arg>-feature</arg>
+            <arg>-release</arg>
+            <arg>17</arg>
+          </args>
+          <source>17</source>
+          <target>17</target>
+        </configuration>
+        <executions>
+          <execution>
+            <id>scala-compile-first</id>
+            <goals>
+              <goal>compile</goal>
+            </goals>
+            <phase>process-resources</phase>
+          </execution>
+          <execution>
+            <id>scala-test-compile</id>
+            <goals>
+              <goal>testCompile</goal>
+            </goals>
+            <phase>process-test-resources</phase>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>net.revelc.code</groupId>
+        <artifactId>warbucks-maven-plugin</artifactId>
+        <version>1.1.0</version>
+        <configuration>
+          <ignoreRuleFailures>true</ignoreRuleFailures>
+          <rules>
+            <rule>
+              
<classPattern>(?!.*(.generated.|.tmpl.|\$|PushdownMappedField|SparkSQLPushDownFilter|org.apache.hadoop.hbase.spark.hbase.package)).*</classPattern>
+              <includeTestClasses>false</includeTestClasses>
+              <includePublicClasses>true</includePublicClasses>
+              
<includePackagePrivateClasses>false</includePackagePrivateClasses>
+              <includeProtectedClasses>false</includeProtectedClasses>
+              <includePrivateClasses>false</includePrivateClasses>
+              
<classAnnotationPattern>org[.]apache[.]yetus[.]audience[.]InterfaceAudience.*</classAnnotationPattern>
+            </rule>
+          </rules>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
 </project>
diff --git a/spark4/hbase-spark4/pom.xml b/spark4/hbase-spark4/pom.xml
index 7ab30be..ee9dff9 100644
--- a/spark4/hbase-spark4/pom.xml
+++ b/spark4/hbase-spark4/pom.xml
@@ -94,6 +94,20 @@
       <artifactId>spark-sql_${scala.binary.version}</artifactId>
       <version>${spark4.version}</version>
       <scope>provided</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>org.apache.hadoop</groupId>
+          <artifactId>hadoop-client-api</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.apache.hadoop</groupId>
+          <artifactId>hadoop-client-runtime</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.google.protobuf</groupId>
+          <artifactId>protobuf-java</artifactId>
+        </exclusion>
+      </exclusions>
     </dependency>
     <dependency>
       <groupId>org.apache.spark</groupId>
@@ -110,6 +124,66 @@
       <groupId>org.apache.yetus</groupId>
       <artifactId>audience-annotations</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-common</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-client</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-mapreduce</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>org.apache.hadoop</groupId>
+          <artifactId>hadoop-hdfs</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <!-- Test dependencies -->
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-shaded-testing-util</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-hdfs</artifactId>
+      <version>${hadoop.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-minikdc</artifactId>
+      <version>${hadoop.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.validation</groupId>
+      <artifactId>validation-api</artifactId>
+      <version>2.0.1.Final</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.protobuf</groupId>
+      <artifactId>protobuf-java</artifactId>
+      <version>2.5.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.scalatest</groupId>
+      <artifactId>scalatest_${scala.binary.version}</artifactId>
+      <version>${spark4.scalatest.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
@@ -200,7 +274,30 @@
           </execution>
           <execution>
             <id>scala-test-compile</id>
-            <phase>none</phase>
+            <goals>
+              <goal>testCompile</goal>
+            </goals>
+            <phase>process-test-resources</phase>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.scalatest</groupId>
+        <artifactId>scalatest-maven-plugin</artifactId>
+        <version>${spark4.scalatest.maven.plugin.version}</version>
+        <configuration>
+          
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
+          <junitxml>.</junitxml>
+          <filereports>WDF TestSuite.txt</filereports>
+          <parallel>false</parallel>
+        </configuration>
+        <executions>
+          <execution>
+            <id>test</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <phase>test</phase>
           </execution>
         </executions>
       </plugin>
diff --git 
a/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala
 
b/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala
new file mode 100644
index 0000000..da66ed6
--- /dev/null
+++ 
b/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala
@@ -0,0 +1,170 @@
+/*
+ * 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.hadoop.hbase.spark
+
+import org.apache.hadoop.conf.Configuration
+import org.apache.hadoop.hbase.{CellUtil, TableName}
+import org.apache.hadoop.hbase.client.{Result, Scan}
+import org.apache.hadoop.hbase.io.ImmutableBytesWritable
+import org.apache.hadoop.hbase.mapreduce.{IdentityTableMapper, 
TableInputFormat, TableMapReduceUtil}
+import org.apache.hadoop.hbase.util.Bytes
+import org.apache.hadoop.mapred.JobConf
+import org.apache.hadoop.mapreduce.Job
+import org.apache.hadoop.security.UserGroupInformation
+import org.apache.spark.{SerializableWritable, SparkContext}
+import org.apache.spark.broadcast.Broadcast
+import org.apache.spark.rdd.RDD
+import org.apache.spark.sql.Row
+import org.apache.yetus.audience.InterfaceAudience
+import scala.reflect.ClassTag
+
+/**
+ * Narrow Spark 4 port of `org.apache.hadoop.hbase.spark.HBaseContext`: enough 
for datasource scans
+ * via `NewHBaseRDD` / TableInputFormat. Bulk load, streaming, and imperative 
helpers are intentionally
+ * omitted for now and will be implemented later (see HBASE-30178).
+ */
[email protected]
+class HBaseContext(@transient val sc: SparkContext, @transient val config: 
Configuration)
+    extends Serializable
+    with Logging {
+
+  @transient protected var tmpHdfsConfiguration: Configuration = config
+
+  {
+    val j = Job.getInstance(config)
+    TableMapReduceUtil.initCredentials(j)
+  }
+
+  val broadcastedConf: Broadcast[SerializableWritable[Configuration]] =
+    sc.broadcast(new SerializableWritable(config))
+
+  LatestHBaseContextCache.latest = this
+
+  /**
+   * Produces an RDD of type U by scanning an HBase table and applying a 
transformation function
+   * to each (key, result) pair.
+   *
+   * @param tableName the name of the HBase table to scan
+   * @param scan      the Scan object defining the read parameters
+   * @param f         transformation function applied to each 
(ImmutableBytesWritable, Result) pair
+   * @tparam U        the element type of the resulting RDD
+   * @return          RDD containing the transformed scan results
+   */
+  def hbaseRDD[U: ClassTag](
+      tableName: TableName,
+      scan: Scan,
+      f: ((ImmutableBytesWritable, Result)) => U): RDD[U] = {
+    applyMapFunction(buildNewHBaseRDD(tableName, scan), f)
+  }
+
+  private def buildNewHBaseRDD(
+      tableName: TableName,
+      scan: Scan): NewHBaseRDD[ImmutableBytesWritable, Result] = {
+
+    val mapJob = Job.getInstance(getConf(broadcastedConf))
+    TableMapReduceUtil.initCredentials(mapJob)
+    TableMapReduceUtil.initTableMapperJob(
+      tableName,
+      scan,
+      classOf[IdentityTableMapper],
+      null,
+      null,
+      mapJob)
+
+    val jconf = new JobConf(mapJob.getConfiguration)
+    val jobCreds = jconf.getCredentials()
+    UserGroupInformation.setConfiguration(sc.hadoopConfiguration)
+    jobCreds.mergeAll(UserGroupInformation.getCurrentUser().getCredentials())
+
+    new NewHBaseRDD(
+      sc,
+      classOf[TableInputFormat],
+      classOf[ImmutableBytesWritable],
+      classOf[Result],
+      mapJob.getConfiguration,
+      this)
+  }
+
+  private def applyMapFunction[U: ClassTag](
+      rdd: RDD[(ImmutableBytesWritable, Result)],
+      f: ((ImmutableBytesWritable, Result)) => U): RDD[U] = {
+    rdd.map(f)
+  }
+
+  /**
+   * Produces an RDD of raw (ImmutableBytesWritable, Result) pairs by scanning 
an HBase table.
+   *
+   * @param tableName the name of the HBase table to scan
+   * @param scans     the Scan object defining the read parameters
+   * @return          RDD of (ImmutableBytesWritable, Result) pairs
+   */
+  def hbaseRDD(tableName: TableName, scans: Scan): 
RDD[(ImmutableBytesWritable, Result)] = {
+    hbaseRDD[(ImmutableBytesWritable, Result)](
+      tableName,
+      scans,
+      (r: (ImmutableBytesWritable, Result)) => r)
+  }
+
+  /**
+   * Scans an HBase table and returns an RDD of Spark SQL Rows, extracting the 
specified columns
+   * from each result. This method handles cell-to-Row conversion internally, 
avoiding Spark 4
+   * closure serialization issues that arise when client code maps over raw 
HBase results in
+   * spark-shell.
+   *
+   * @param tableName the name of the HBase table to scan
+   * @param scan      the Scan object defining the read parameters
+   * @param columns   ordered sequence of column qualifiers to extract from 
each row
+   * @return          RDD[Row] with one field per column (null if a column is 
absent in a result)
+   */
+  def hbaseRDDAsRows(tableName: String, scan: Scan, columns: Seq[String]): 
RDD[Row] = {
+    val rdd = buildNewHBaseRDD(TableName.valueOf(tableName), scan)
+    val cols = columns.toArray
+    rdd.map {
+      case (_, result) =>
+        val cells = result.listCells()
+        val values = new java.util.HashMap[String, String](cells.size())
+        val iter = cells.iterator()
+        while (iter.hasNext) {
+          val cell = iter.next()
+          values.put(
+            Bytes.toString(CellUtil.cloneQualifier(cell)),
+            Bytes.toString(CellUtil.cloneValue(cell)))
+        }
+        Row.fromSeq(cols.map(col => values.get(col): Any))
+    }
+  }
+
+  private[hbase] def getConf(
+      configBroadcast: Broadcast[SerializableWritable[Configuration]]): 
Configuration = {
+
+    if (tmpHdfsConfiguration == null) {
+      try {
+        tmpHdfsConfiguration = configBroadcast.value.value
+      } catch {
+        case ex: Exception => logError("Unable to getConfig from broadcast", 
ex)
+      }
+    }
+    tmpHdfsConfiguration
+  }
+}
+
+/** Same semantics as Spark 3: last constructed `HBaseContext` wins. */
[email protected]
+object LatestHBaseContextCache {
+  var latest: HBaseContext = null
+}
diff --git 
a/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/Logging.scala
 
b/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/Logging.scala
new file mode 100644
index 0000000..ec199e7
--- /dev/null
+++ 
b/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/Logging.scala
@@ -0,0 +1,101 @@
+/*
+ * 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.hadoop.hbase.spark
+
+import org.apache.yetus.audience.InterfaceAudience
+import org.slf4j.{Logger, LoggerFactory}
+
+/**
+ * Mirrors `org.apache.hadoop.hbase.spark.Logging` in `hbase-spark` for 
Spark-independent SLF4J use.
+ */
[email protected]
+trait Logging {
+
+  @transient private var underlyingLogger: Logger = null
+
+  protected def logName: String =
+    this.getClass.getName.stripSuffix("$")
+
+  protected def log: Logger = {
+    if (underlyingLogger == null) {
+      initializeLogIfNecessary(false)
+      underlyingLogger = LoggerFactory.getLogger(logName)
+    }
+    underlyingLogger
+  }
+
+  protected def logInfo(msg: => String): Unit = {
+    if (log.isInfoEnabled) log.info(msg)
+  }
+
+  protected def logDebug(msg: => String): Unit = {
+    if (log.isDebugEnabled) log.debug(msg)
+  }
+
+  protected def logTrace(msg: => String): Unit = {
+    if (log.isTraceEnabled) log.trace(msg)
+  }
+
+  protected def logWarning(msg: => String): Unit = {
+    if (log.isWarnEnabled) log.warn(msg)
+  }
+
+  protected def logError(msg: => String): Unit = {
+    if (log.isErrorEnabled) log.error(msg)
+  }
+
+  protected def logInfo(msg: => String, throwable: Throwable): Unit = {
+    if (log.isInfoEnabled) log.info(msg, throwable)
+  }
+
+  protected def logDebug(msg: => String, throwable: Throwable): Unit = {
+    if (log.isDebugEnabled) log.debug(msg, throwable)
+  }
+
+  protected def logTrace(msg: => String, throwable: Throwable): Unit = {
+    if (log.isTraceEnabled) log.trace(msg, throwable)
+  }
+
+  protected def logWarning(msg: => String, throwable: Throwable): Unit = {
+    if (log.isWarnEnabled) log.warn(msg, throwable)
+  }
+
+  protected def logError(msg: => String, throwable: Throwable): Unit = {
+    if (log.isErrorEnabled) log.error(msg, throwable)
+  }
+
+  protected def initializeLogIfNecessary(isInterpreter: Boolean): Unit = {
+    if (!Logging.initialized) {
+      Logging.initLock.synchronized {
+        if (!Logging.initialized) {
+          initializeLogging(isInterpreter)
+        }
+      }
+    }
+  }
+
+  private def initializeLogging(isInterpreter: Boolean): Unit = {
+    Logging.initialized = true
+  }
+
+}
+
+private object Logging {
+  @volatile private var initialized = false
+  val initLock = new Object()
+}
diff --git 
a/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/Spark4Placeholder.scala
 
b/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/NewHBaseRDD.scala
similarity index 54%
rename from 
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/Spark4Placeholder.scala
rename to 
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/NewHBaseRDD.scala
index f6f96cf..de536fd 100644
--- 
a/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/Spark4Placeholder.scala
+++ 
b/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/NewHBaseRDD.scala
@@ -15,14 +15,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.apache.hadoop.hbase.spark
 
+import org.apache.hadoop.conf.Configuration
+import org.apache.hadoop.mapreduce.InputFormat
+import org.apache.spark.{InterruptibleIterator, Partition, SparkContext, 
TaskContext}
+import org.apache.spark.rdd.NewHadoopRDD
 import org.apache.yetus.audience.InterfaceAudience
 
-/**
- * Phase-0 build marker for the Spark 4 connector line (`hbase-spark4`).
- * Replaced incrementally during feature porting; see umbrella jira 
HBASE-30178.
- */
[email protected]
-private[hbase] object Spark4Placeholder
[email protected]
+class NewHBaseRDD[K, V](
+    @transient val sc: SparkContext,
+    @transient val inputFormatClass: Class[_ <: InputFormat[K, V]],
+    @transient val keyClass: Class[K],
+    @transient val valueClass: Class[V],
+    @transient private val __conf: Configuration,
+    val hBaseContext: HBaseContext)
+    extends NewHadoopRDD(sc, inputFormatClass, keyClass, valueClass, __conf) {
+
+  override def compute(theSplit: Partition, context: TaskContext): 
InterruptibleIterator[(K, V)] = {
+    super.compute(theSplit, context)
+  }
+}
diff --git 
a/spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/HBaseContextSuite.scala
 
b/spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/HBaseContextSuite.scala
new file mode 100644
index 0000000..d6a4b98
--- /dev/null
+++ 
b/spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/HBaseContextSuite.scala
@@ -0,0 +1,161 @@
+/*
+ * 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.hadoop.hbase.spark
+
+import org.apache.hadoop.hbase.{CellUtil, HBaseTestingUtility, TableName}
+import org.apache.hadoop.hbase.client.{ConnectionFactory, Put, Scan}
+import org.apache.hadoop.hbase.filter.FirstKeyOnlyFilter
+import org.apache.hadoop.hbase.io.ImmutableBytesWritable
+import org.apache.hadoop.hbase.util.Bytes
+import org.apache.spark.{SparkConf, SparkContext}
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.funsuite.AnyFunSuite
+
+class HBaseContextSuite extends AnyFunSuite with BeforeAndAfterAll with 
Logging {
+
+  @transient var sc: SparkContext = _
+  var hbaseContext: HBaseContext = _
+  val TEST_UTIL = new HBaseTestingUtility
+
+  val tableName = "t1"
+  val columnFamily = "c"
+
+  override def beforeAll(): Unit = {
+    TEST_UTIL.startMiniCluster()
+    logInfo(" - minicluster started")
+
+    TEST_UTIL.createTable(TableName.valueOf(tableName), 
Bytes.toBytes(columnFamily))
+    logInfo(" - created table " + tableName)
+
+    populateTestData()
+
+    val sparkConf = new SparkConf()
+      .setMaster("local[2]")
+      .setAppName("HBaseContextSuite")
+      .set("spark.hadoopRDD.ignoreEmptySplits", "false")
+    sc = new SparkContext(sparkConf)
+
+    hbaseContext = new HBaseContext(sc, TEST_UTIL.getConfiguration)
+  }
+
+  override def afterAll(): Unit = {
+    if (sc != null) {
+      sc.stop()
+    }
+    TEST_UTIL.shutdownMiniCluster()
+    TEST_UTIL.cleanupTestDir()
+  }
+
+  private def populateTestData(): Unit = {
+    val connection = 
ConnectionFactory.createConnection(TEST_UTIL.getConfiguration)
+    val table = connection.getTable(TableName.valueOf(tableName))
+    try {
+      val puts = Array(
+        makePut("row1", "a", "value1"),
+        makePut("row2", "a", "value2"),
+        makePut("row2", "b", "value2b"),
+        makePut("row3", "a", "value3"),
+        makePut("row4", "a", "value4"),
+        makePut("row5", "a", "value5"))
+      puts.foreach(table.put)
+    } finally {
+      table.close()
+      connection.close()
+    }
+  }
+
+  private def makePut(rowKey: String, qualifier: String, value: String): Put = 
{
+    val put = new Put(Bytes.toBytes(rowKey))
+    put.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier), 
Bytes.toBytes(value))
+    put
+  }
+
+  test("hbaseRDD with map function transforms scan results") {
+    val scan = new Scan()
+    val cf = Bytes.toBytes(columnFamily)
+    val qual = Bytes.toBytes("a")
+
+    val rdd = hbaseContext.hbaseRDD[String](
+      TableName.valueOf(tableName),
+      scan,
+      (r: (ImmutableBytesWritable, org.apache.hadoop.hbase.client.Result)) => {
+        val key = Bytes.toString(r._1.copyBytes())
+        val value = 
Bytes.toString(CellUtil.cloneValue(r._2.getColumnLatestCell(cf, qual)))
+        s"$key=$value"
+      })
+
+    val results = rdd.collect().sorted
+    assert(results.length == 5)
+    assert(results.contains("row1=value1"))
+    assert(results.contains("row2=value2"))
+    assert(results.contains("row3=value3"))
+    assert(results.contains("row4=value4"))
+    assert(results.contains("row5=value5"))
+  }
+
+  test("hbaseRDD raw scan returns correct results with start/stop row") {
+    val scan = new Scan()
+    scan.withStartRow(Bytes.toBytes("row2"))
+    scan.withStopRow(Bytes.toBytes("row4"))
+
+    val rdd = hbaseContext.hbaseRDD(TableName.valueOf(tableName), scan)
+
+    val results = rdd.map(r => 
Bytes.toString(r._1.copyBytes())).collect().sorted
+    assert(results.length == 2)
+    assert(results(0) == "row2")
+    assert(results(1) == "row3")
+  }
+
+  test("hbaseRDD with FirstKeyOnlyFilter limits cells per row") {
+    val scan = new Scan()
+    scan.withStartRow(Bytes.toBytes("row2"))
+    scan.withStopRow(Bytes.toBytes("row3"))
+    scan.setFilter(new FirstKeyOnlyFilter())
+
+    val rdd = hbaseContext.hbaseRDD(TableName.valueOf(tableName), scan)
+
+    val cellCount = rdd.map(r => r._2.listCells().size()).collect().sum
+    // row2 has 2 cells (a, b) but FirstKeyOnlyFilter returns only 1 per row
+    assert(cellCount == 1)
+  }
+
+  test("hbaseRDDAsRows extracts specified columns into Row objects") {
+    val scan = new Scan()
+    scan.withStartRow(Bytes.toBytes("row1"))
+    scan.withStopRow(Bytes.toBytes("row2"))
+
+    val rdd = hbaseContext.hbaseRDDAsRows(tableName, scan, Seq("a"))
+
+    val rows = rdd.collect()
+    assert(rows.length == 1)
+    assert(rows(0).getString(0) == "value1")
+  }
+
+  test("hbaseRDDAsRows returns null for missing columns") {
+    val scan = new Scan()
+    scan.withStartRow(Bytes.toBytes("row1"))
+    scan.withStopRow(Bytes.toBytes("row2"))
+
+    val rdd = hbaseContext.hbaseRDDAsRows(tableName, scan, Seq("a", 
"nonexistent"))
+
+    val rows = rdd.collect()
+    assert(rows.length == 1)
+    assert(rows(0).getString(0) == "value1")
+    assert(rows(0).get(1) == null)
+  }
+}
diff --git a/spark4/pom.xml b/spark4/pom.xml
index 80f6135..336049a 100644
--- a/spark4/pom.xml
+++ b/spark4/pom.xml
@@ -28,13 +28,11 @@
   </parent>
 
   <artifactId>spark4</artifactId>
-  <packaging>pom</packaging>
   <version>${revision}</version>
+  <packaging>pom</packaging>
   <name>Apache HBase - Spark 4</name>
-  <description>
-    Maven reactor for the Apache Spark 4.x HBase connector (parallel to 
spark/).
-    Activated with profile -Pspark4 on the repository root pom. Requires JDK 
17+ (Spark 4.x).
-  </description>
+  <description>Maven reactor for the Apache Spark 4.x HBase connector 
(parallel to spark/).
+    Activated with profile -Pspark4 on the repository root pom. Requires JDK 
17+ (Spark 4.x).</description>
 
   <modules>
     <module>hbase-spark-pushdown</module>
@@ -69,10 +67,8 @@
               <rules>
                 <requireJavaVersion>
                   <version>[17,)</version>
-                  <message>
-                    The Spark 4 connector reactor requires JDK 17 or newer 
(Apache Spark 4.x).
-                    Omit -Pspark4 to build only the Kafka and Spark 3 modules 
on JDK 8+.
-                  </message>
+                  <message>The Spark 4 connector reactor requires JDK 17 or 
newer (Apache Spark 4.x).
+                    Omit -Pspark4 to build only the Kafka and Spark 3 modules 
on JDK 8+.</message>
                 </requireJavaVersion>
               </rules>
             </configuration>


Reply via email to