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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/release/v1.2/pr-7316-2a8b858e5155c5230f4dfb866838a0f985730a8f
in repository https://gitbox.apache.org/repos/asf/texera.git

commit ea5d00fd2742c250d414ae88699f1864b506b449
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Aug 13 05:39:23 2026 +0000

    fix(jsonl-scan, v1.2): count the worker slice from the offset, not the file 
(#7316)
    
    ### What changes were proposed in this PR?
    
    Automated backport of #7247 to `release/v1.2`.
    
    Source: df678aa8a9a992bfeaae6f63f4fac3237d036701 ยท [automation
    run](https://github.com/apache/texera/actions/runs/30962737205)
    
    ### Any related issues, documentation, discussions?
    
    Backport of #7247. Originally linked #7245.
    
    ### How was this PR tested?
    
    Release-branch CI runs on this branch once the conflicts are resolved
    and this PR is marked ready for review.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    No.
    
    Co-authored-by: Kary Zheng <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
    Co-authored-by: Xuan Gu <[email protected]>
    Co-authored-by: Xinyuan Lin <[email protected]>
---
 .../source/scan/json/JSONLScanSourceOpExec.scala   |   7 +-
 .../scan/json/JSONLScanSourceOpExecSpec.scala      | 123 +++++++++++++++++++++
 2 files changed, 127 insertions(+), 3 deletions(-)

diff --git 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpExec.scala
 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpExec.scala
index 3c47796892..98a6a0bdcc 100644
--- 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpExec.scala
+++ 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpExec.scala
@@ -68,10 +68,11 @@ class JSONLScanSourceOpExec private[json] (
     val (it1, it2) = lines.duplicate
     val count: Int = it1.map(_ => 1).sum
 
-    val startOffset: Int = offsetValue + count / workerCount * idx
+    // Bounds into `it2`, which already begins at `offsetValue`. Adding the 
offset
+    // back in would count from the start of the FILE and skip those rows 
twice.
+    val startOffset: Int = count / workerCount * idx
     val endOffset: Int =
-      offsetValue + (if (idx != workerCount - 1) count / workerCount * (idx + 
1)
-                     else count)
+      if (idx != workerCount - 1) count / workerCount * (idx + 1) else count
 
     rows = it2.iterator.slice(startOffset, endOffset)
   }
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpExecSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpExecSpec.scala
new file mode 100644
index 0000000000..a90dcd3640
--- /dev/null
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpExecSpec.scala
@@ -0,0 +1,123 @@
+/*
+ * 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.texera.amber.operator.source.scan.json
+
+import org.apache.texera.amber.operator.source.scan.FileDecodingMethod
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+
+import java.net.URI
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+
+class JSONLScanSourceOpExecSpec extends AnyFlatSpec {
+
+  private def writeJsonl(lines: String*): URI = {
+    val path = Files.createTempFile("jsonl-scan-", ".jsonl")
+    path.toFile.deleteOnExit()
+    Files.write(path, lines.mkString("\n").getBytes(StandardCharsets.UTF_8))
+    path.toFile.toURI
+  }
+
+  private def descString(
+      uri: URI,
+      flatten: Boolean = false,
+      limit: Option[Int] = None,
+      offset: Option[Int] = None
+  ): String = {
+    val desc = new JSONLScanSourceOpDesc
+    desc.setResolvedFileName(uri)
+    desc.fileEncoding = FileDecodingMethod.UTF_8
+    desc.flatten = flatten
+    desc.limit = limit
+    desc.offset = offset
+    objectMapper.writeValueAsString(desc)
+  }
+
+  private def drain(exec: JSONLScanSourceOpExec): List[Seq[Any]] = {
+    exec.open()
+    try exec.produceTuple().map(_.getFields.toSeq).toList
+    finally exec.close()
+  }
+
+  "JSONLScanSourceOpExec" should "read each JSON line, ordering fields by 
sorted attribute name" in {
+    // keys are written name-then-id; the output must be reordered to 
id-then-name
+    val exec = new JSONLScanSourceOpExec(
+      descString(writeJsonl("""{"name":"a","id":1}""", 
"""{"name":"b","id":2}"""))
+    )
+    val rows = drain(exec)
+    assert(rows.size == 2)
+    assert(rows.head == Seq(1, "a"))
+    assert(rows(1) == Seq(2, "b"))
+  }
+
+  it should "partition rows across workers" in {
+    val uri = writeJsonl("""{"v":0}""", """{"v":1}""", """{"v":2}""", 
"""{"v":3}""")
+    val worker0 = new JSONLScanSourceOpExec(descString(uri), idx = 0, 
workerCount = 2)
+    val worker1 = new JSONLScanSourceOpExec(descString(uri), idx = 1, 
workerCount = 2)
+    assert(drain(worker0).map(_.head) == Seq(0, 1))
+    assert(drain(worker1).map(_.head) == Seq(2, 3))
+  }
+
+  it should "apply the row limit" in {
+    val uri = writeJsonl("""{"v":0}""", """{"v":1}""", """{"v":2}""", 
"""{"v":3}""", """{"v":4}""")
+    val exec = new JSONLScanSourceOpExec(descString(uri, limit = Some(2)))
+    assert(drain(exec).map(_.head) == Seq(0, 1))
+  }
+
+  it should "start at the offset and keep every row after it" in {
+    val uri = writeJsonl("""{"v":0}""", """{"v":1}""", """{"v":2}""", 
"""{"v":3}""", """{"v":4}""")
+    val exec = new JSONLScanSourceOpExec(descString(uri, offset = Some(2)))
+    assert(drain(exec).map(_.head) == Seq(2, 3, 4))
+  }
+
+  it should "apply the limit relative to the offset" in {
+    val uri = writeJsonl("""{"v":0}""", """{"v":1}""", """{"v":2}""", 
"""{"v":3}""", """{"v":4}""")
+    // The window is shorter than the offset itself, which used to empty it 
out.
+    val exec = new JSONLScanSourceOpExec(descString(uri, limit = Some(2), 
offset = Some(2)))
+    assert(drain(exec).map(_.head) == Seq(2, 3))
+  }
+
+  it should "split the offset window across workers, losing no row to either 
end" in {
+    val uri = writeJsonl("""{"v":0}""", """{"v":1}""", """{"v":2}""", 
"""{"v":3}""", """{"v":4}""")
+    val desc = descString(uri, offset = Some(1))
+    val worker0 = new JSONLScanSourceOpExec(desc, idx = 0, workerCount = 2)
+    val worker1 = new JSONLScanSourceOpExec(desc, idx = 1, workerCount = 2)
+    assert(drain(worker0).map(_.head) == Seq(1, 2))
+    assert(drain(worker1).map(_.head) == Seq(3, 4))
+  }
+
+  it should "give the last worker the remainder of the offset-and-limit 
window" in {
+    val uri = writeJsonl(
+      """{"v":0}""",
+      """{"v":1}""",
+      """{"v":2}""",
+      """{"v":3}""",
+      """{"v":4}""",
+      """{"v":5}""",
+      """{"v":6}"""
+    )
+    // Four rows over three workers: one each, and the odd row goes to the 
last.
+    val desc = descString(uri, limit = Some(4), offset = Some(2))
+    val workers =
+      (0 until 3).map(i => new JSONLScanSourceOpExec(desc, idx = i, 
workerCount = 3))
+    assert(workers.map(drain(_).map(_.head)) == Seq(Seq(2), Seq(3), Seq(4, 5)))
+  }
+}

Reply via email to