Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,9 @@ class SparkConnectPlanner(
s"_pos_$idx" -> expr
}.toMap
val resolvedParams = session.resolveAndValidateParameters(paramMap)
Some(PositionalParameterContext(resolvedParams.values.toSeq))
// Look up by the positional key instead of relying on `resolvedParams.values`:
// the map does not preserve insertion order for 5+ entries.
Some(PositionalParameterContext(paramList.indices.map(idx => resolvedParams(s"_pos_$idx"))))
} else if (!args.isEmpty) {
// Use named arguments (literals) - already resolved
val paramMap = args.asScala.toMap.transform((_, v) => transformLiteral(v))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,18 @@ class SparkConnectPlannerSuite extends SparkFunSuite with SparkConnectPlanTest {
assert(array(2).toString == InternalRow(3, "kafka", 3, "kafka").toString)
}

test("SPARK-58341: transform SQL with 5 or more positional arguments binds in order") {
val sql = proto.SQL
.newBuilder()
.setQuery("SELECT ?, ?, ?, ?, ?, ?")
(1 to 6).foreach { v =>
sql.addPosArguments(proto.Expression.newBuilder().setLiteral(toLiteralProto(v)))
}

val df = Dataset.ofRows(spark, transform(proto.Relation.newBuilder.setSql(sql).build()))
assert(df.collect() === Array(Row(1, 2, 3, 4, 5, 6)))
}

test("transform UnresolvedStar with target field") {
val rows = (0 until 10).map { i =>
InternalRow(InternalRow(InternalRow(i, i + 1)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,16 +531,19 @@ class SparkSession private(
val parsedPlan = {
// Always parse with parameter context to detect unbound parameter markers.
// Even if args is empty, we need to detect and reject parameter markers in the SQL.
val (paramMap, resolvedParams) = if (args.nonEmpty) {
val resolvedParams = if (args.nonEmpty) {
val pMap = args.zipWithIndex.map { case (arg, idx) =>
s"_pos_$idx" -> lit(arg).expr
}.toMap
(pMap, resolveAndValidateParameters(pMap))
resolveAndValidateParameters(pMap)
} else {
(Map.empty[String, Expression], Map.empty[String, Expression])
Map.empty[String, Expression]
}

val paramContext = PositionalParameterContext(resolvedParams.values.toSeq)
// Look up by the positional key instead of relying on `resolvedParams.values`:
// the map does not preserve insertion order for 5+ entries.
val paramContext =
PositionalParameterContext(args.indices.map(idx => resolvedParams(s"_pos_$idx")))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding 1. The fix is correct as written. Just flagging a possible follow-up: the underlying trap is that resolveAndValidateParameters (SparkSession.scala:469) returns a plain immutable Map, which loses insertion order at 5+ entries -- so every caller that wants positional order has to rebuild it from the _pos_<idx> keys (here and in SparkConnectPlanner.buildParameterContext). Fixing it at the source -- return a scala.collection.immutable.ListMap (insertion-order-preserving) or expose a positional Seq -- would make .values.toSeq order-safe for all callers and remove the per-callsite rebuild, protecting future ones from the same bug. Not for this PR: the surgical fix is the safer, easy-to-backport choice; worth a separate follow-up ticket if you agree.

val parsed = sessionState.sqlParser.parsePlanWithParameters(sqlText, paramContext)

// Check for SQL scripting with positional parameters
Expand Down
10 changes: 10 additions & 0 deletions sql/core/src/test/scala/org/apache/spark/sql/ParametersSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ class ParametersSuite extends SharedSparkSession {
Row(true))
}

test("SPARK-58341: bind 5 or more positional parameters in order") {
checkAnswer(
spark.sql("SELECT ?, ?, ?, ?, ?, ?", Array(1, 2, 3, 4, 5, 6)),
Row(1, 2, 3, 4, 5, 6))

checkAnswer(
spark.sql("SELECT ? + ?, ? * ?, ? - ?", Array(1, 2, 30, 4, 500, 6)),
Row(3, 120, 494))
}

test("parameter binding is case sensitive") {
checkAnswer(
spark.sql("SELECT :p, :P", Map("p" -> 1, "P" -> 2)),
Expand Down