Implementing DataSource V2 Read Path - #162
Conversation
There was a problem hiding this comment.
Pull request overview
Implements the Spark 4 DataSource V2 read path for HBase, including filter pushdown, partition planning, and row conversion.
Changes:
- Adds the V2 provider, table, scan, batch, partition, and reader pipeline.
- Ports row-key range and predicate handling.
- Adds unit and mini-cluster integration tests.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
ScanRange.scala |
Adds row-key range operations. |
HBaseTableProvider.scala |
Adds the V2 provider entry point. |
HBaseTable.scala |
Declares table schema and read capability. |
HBaseScanBuilder.scala |
Negotiates filters and projections. |
HBaseScan.scala |
Builds the logical HBase scan. |
HBaseBatch.scala |
Plans region-based input partitions. |
HBaseInputPartition.scala |
Defines serializable partitions. |
HBasePartitionReaderFactory.scala |
Creates partition readers. |
HBasePartitionReader.scala |
Executes scans and creates rows. |
ScanRangeSuite.scala |
Tests range operations. |
HBaseScanBuilderSuite.scala |
Tests filter and column pushdown. |
HBaseTableCatalogSuite.scala |
Tests catalog parsing. |
HBaseTableProviderSuite.scala |
Tests end-to-end reads. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
taklwu
left a comment
There was a problem hiding this comment.
may you fix the commit title with JIRA prefix HBASE-30189 ?
| .orElse(intersectedPoints.lastOption.map(Utils.incrementByteArray)) | ||
| .orElse(region.end) | ||
| .orNull | ||
| Some(HBaseInputPartition(region.index, startRow, stopRow): InputPartition) |
There was a problem hiding this comment.
seems like a behavior difference from V1 that HBaseInputPartition that uses HBasePartitionReader.scala are scanning instead of Get , do you think this is good ? if not , please try to align what Spark3 does with Get.
There was a problem hiding this comment.
We should use Get too. Changing this in the next commit.
| extends Batch | ||
| with Logging { | ||
|
|
||
| override def planInputPartitions(): Array[InputPartition] = { |
There was a problem hiding this comment.
we may need to support getPreferredLocations like V1 does , you can have it in the future PR.
There was a problem hiding this comment.
This method is defined in the RDD class, which is the model used in V1. V2 uses the Batch model, so there's no RDD subclassing.
There was a problem hiding this comment.
Implemented the "preferredLocations" method in the InputPartition.
I'm actually targeting this PR to the HBASE-30189 branch, which already has two previous commits related to this HBASE-30189. The idea was to split HBASE-30189 into several PRs for ease of review. After all PRs are in HBASE-30189, I'm going to merge it into master as a single commit for HBASE-30189. |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 8 comments.
|
(!) A patch to the testing environment has been detected. |
|
(!) A patch to the testing environment has been detected. |
d2bc359 to
31be97a
Compare
…job (apache#164) Signed-off-by: Tak Lon (Stephen) Wu <taklwu@apache.org> Signed-off-by: Dávid Paksy <paksyd@apache.org> Reviewed-by: Kevin Geiszler <kevin.j.geiszler@gmail.com>
Co-authored-by: Claude Code (claude-opus-4-6) <no-reply@anthropic.com> Signed-off-by: Peter Somogyi <psomogyi@apache.org>
…emas (apache#160) Co-authored-by: Claude Code (claude-opus-4-6) <no-reply@anthropic.com> Signed-off-by: Peter Somogyi <psomogyi@apache.org>
31be97a to
2552079
Compare
|
(!) A patch to the testing environment has been detected. |
There was a problem hiding this comment.
🟡 Changes recommended
Composite keys, unions, null handling, and configuration propagation still contain correctness or scalability defects.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/datasources/HBasePartitionReader.scala:198
- The same missing-qualifier problem affects point reads: a
Getrestricted to theIsNullqualifier returns an emptyResultwhen that qualifier is absent, and line 202 drops it before the predicate can accept it. For pushed expressions that can match a missing cell, do not restrict theGetto only those qualifiers (or otherwise fetch an existence column).
scanFields.foreach { f =>
g.addColumn(f.cfBytes, f.colBytes)
}
- Files reviewed: 17/17 changed files
- Comments generated: 8
- Review effort level: Balanced
|
(!) A patch to the testing environment has been detected. |
There was a problem hiding this comment.
🟡 Changes recommended
The read path has unresolved serialization, filter encoding, partial-result, configuration, and Catalyst conversion defects.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/datasources/HBasePartitionReader.scala:177
- A positive HBase
Scanbatch size allows one logical row to be returned as multiple partialResults. This reader treats everyResultas a complete Spark row, sohbase.spark.query.batchsizecan produce duplicate rows with nulls for cells delivered in another batch. Either coalesce partial results (usingmayHaveMoreCellsInRow) before exposing them or avoid setting a cell batch size.
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/datasources/HBasePartitionReader.scala:285
- Complex values are still returned in external Scala form here.
SchemaConverters.createConverterToSQLproducesRow,Seq, andMapfor struct/array/map fields, butGenericInternalRowrequiresInternalRow,ArrayData, andMapData; reading an Avro-backed complex column will therefore fail or expose an invalid Catalyst value. Use Spark's recursive Catalyst converter rather than handling only scalar special cases.
case dt: DecimalType =>
Decimal(value.asInstanceOf[java.math.BigDecimal], dt.precision, dt.scale)
case _ => value
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/datasources/HBaseScanBuilder.scala:60
- This ignores the documented global SparkConf fallback (
HBaseSparkConfsays settings may be global or per-table). Consequentlyspark.conf.set("hbase.spark.pushdown.columnfilter", "false")has no effect unless the option is repeated on every read, and filters are still reported as fully pushed. Preserve per-read precedence but fall back to the active SparkConf as the Spark 3 path does.
val usePushDown = properties
.get(HBaseSparkConf.PUSHDOWN_COLUMN_FILTER)
.map(_.toBoolean)
.getOrElse(HBaseSparkConf.DEFAULT_PUSHDOWN_COLUMN_FILTER)
- Files reviewed: 17/17 changed files
- Comments generated: 2
- Review effort level: Balanced
|
(!) A patch to the testing environment has been detected. |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.
| val endKeys = keys.getSecond | ||
|
|
||
| val regions = startKeys.zip(endKeys).zipWithIndex.map { case ((start, end), idx) => | ||
| HBaseRegion(idx, Some(start), Some(end), |
There was a problem hiding this comment.
This isn't a valid issue. The potential problem is addressed in Range.apply(region) code, where empty keys are converted to None (see Bound.scala lines #36-44).
| if (compareRange(lowerBound, other.lowerBound) < 0 || | ||
| compareRange(upperBound, other.upperBound) < 0) { | ||
| leftRange = this | ||
| rightRange = other | ||
| } else { | ||
| leftRange = other | ||
| rightRange = this | ||
| } |
There was a problem hiding this comment.
This would still pass the overlaps check which then merges the ScanRanges results accordingly. It copies the logic from spark3 module in DefaultSource.scala. I would suggest optimisation work to be done separately.
| private val resultIterator: Iterator[Result] = { | ||
| val scanIterators = partition.scanRanges.map { range => | ||
| val scanner = buildScanner(range) | ||
| scanners += scanner | ||
| scannerToIterator(scanner) | ||
| } | ||
| val getIterator = if (partition.points.nonEmpty) { | ||
| buildGets(partition.points) | ||
| } else { | ||
| Iterator.empty | ||
| } | ||
| scanIterators.foldLeft(Iterator.empty: Iterator[Result])(_ ++ _) ++ getIterator | ||
| } |
There was a problem hiding this comment.
This is pre-existing spark3 module logic, and in practice, scan ranges intersections are very few, so this doesn't present a real performance problem.
|
Hey @taklwu , I believe I have now addressed all copilot reviews that were indeed valid. Can you give an extra look into this? |
No description provided.