-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-41271][SQL] Support parameterized SQL queries by sql()
#38864
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6b42040
fe3ca7a
ab3c615
3cfef61
c264476
17b9ea3
0f32b75
2806334
d16e85b
e48e2e1
16b9a27
693cd92
4e55269
4d1198f
fcdf5f2
10c7680
cddff4a
a0f568a
2e93bec
cf03c3b
0605f22
210d23c
16966c9
b8a6fd9
89c5371
e883aad
d05526c
ce19640
5a1f33d
43fd1b9
5ad46f8
637778c
d2ce096
d3ac69c
08e6dfa
27927fb
c390260
81cb619
4b7cb23
0a3e500
15e2b27
165a21d
2857350
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -795,6 +795,11 @@ | |
| } | ||
| } | ||
| }, | ||
| "INVALID_SQL_ARG" : { | ||
| "message" : [ | ||
| "The argument <name> of `sql()` is invalid. Consider to replace it by a SQL literal statement." | ||
| ] | ||
| }, | ||
| "INVALID_SQL_SYNTAX" : { | ||
| "message" : [ | ||
| "Invalid SQL syntax: <inputString>" | ||
|
|
@@ -1130,6 +1135,11 @@ | |
| "Unable to convert SQL type <toType> to Protobuf type <protobufType>." | ||
| ] | ||
| }, | ||
| "UNBOUND_SQL_PARAMETER" : { | ||
| "message" : [ | ||
| "Found the unbound parameter: <name>. Please, fix `args` and provide a mapping of the parameter to a SQL literal statement." | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Won't this same error be used fro all other APIs (JDBC, SQL (when we support). So we may not want to refer to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is a premature generalisation. Let's make it more generic when we will need that. |
||
| ] | ||
| }, | ||
| "UNCLOSED_BRACKETED_COMMENT" : { | ||
| "message" : [ | ||
| "Found an unclosed bracketed comment. Please, append */ at the end of the comment." | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| /* | ||
| * 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.spark.sql.catalyst.expressions | ||
|
|
||
| import org.apache.spark.SparkException | ||
| import org.apache.spark.sql.catalyst.analysis.AnalysisErrorAt | ||
| import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan | ||
| import org.apache.spark.sql.catalyst.trees.TreePattern.{PARAMETER, TreePattern} | ||
| import org.apache.spark.sql.errors.QueryErrorsBase | ||
| import org.apache.spark.sql.types.DataType | ||
|
|
||
| /** | ||
| * The expression represents a named parameter that should be replaced by a literal. | ||
| * | ||
| * @param name The identifier of the parameter without the marker. | ||
| */ | ||
| case class Parameter(name: String) extends LeafExpression with Unevaluable { | ||
| override lazy val resolved: Boolean = false | ||
|
|
||
| private def unboundError(methodName: String): Nothing = { | ||
| throw SparkException.internalError( | ||
| s"Cannot call `$methodName()` of the unbound parameter `$name`.") | ||
| } | ||
| override def dataType: DataType = unboundError("dataType") | ||
| override def nullable: Boolean = unboundError("nullable") | ||
|
|
||
| final override val nodePatterns: Seq[TreePattern] = Seq(PARAMETER) | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Finds all named parameters in the given plan and substitutes them by literals of `args` values. | ||
| */ | ||
| object Parameter extends QueryErrorsBase { | ||
| def bind(plan: LogicalPlan, args: Map[String, Expression]): LogicalPlan = { | ||
| if (!args.isEmpty) { | ||
| args.filter(!_._2.isInstanceOf[Literal]).headOption.foreach { case (name, expr) => | ||
| expr.failAnalysis( | ||
| errorClass = "INVALID_SQL_ARG", | ||
| messageParameters = Map("name" -> toSQLId(name))) | ||
| } | ||
| plan.transformAllExpressionsWithPruning(_.containsPattern(PARAMETER)) { | ||
| case Parameter(name) if args.contains(name) => args(name) | ||
| } | ||
| } else { | ||
| plan | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| /* | ||
| * 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.spark.sql | ||
|
|
||
| import org.apache.spark.sql.test.SharedSparkSession | ||
|
|
||
| class ParametersSuite extends QueryTest with SharedSparkSession { | ||
|
|
||
| test("bind parameters") { | ||
| val sqlText = | ||
| """ | ||
| |SELECT id, id % :div as c0 | ||
| |FROM VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9) AS t(id) | ||
| |WHERE id < :constA | ||
| |""".stripMargin | ||
| val args = Map("div" -> "3", "constA" -> "4L") | ||
| checkAnswer( | ||
| spark.sql(sqlText, args), | ||
| Row(0, 0) :: Row(1, 1) :: Row(2, 2) :: Row(3, 0) :: Nil) | ||
|
|
||
| checkAnswer( | ||
| spark.sql("""SELECT contains('Spark \'SQL\'', :subStr)""", Map("subStr" -> "'SQL'")), | ||
| Row(true)) | ||
| } | ||
|
|
||
| test("non-substituted parameters") { | ||
| checkError( | ||
| exception = intercept[AnalysisException] { | ||
| spark.sql("select :abc, :def", Map("abc" -> "1")) | ||
| }, | ||
| errorClass = "UNBOUND_SQL_PARAMETER", | ||
| parameters = Map("name" -> "`def`"), | ||
| context = ExpectedContext( | ||
| fragment = ":def", | ||
| start = 13, | ||
| stop = 16)) | ||
| checkError( | ||
| exception = intercept[AnalysisException] { | ||
| sql("select :abc").collect() | ||
| }, | ||
| errorClass = "UNBOUND_SQL_PARAMETER", | ||
| parameters = Map("name" -> "`abc`"), | ||
| context = ExpectedContext( | ||
| fragment = ":abc", | ||
| start = 7, | ||
| stop = 10)) | ||
| } | ||
|
|
||
| test("non-literal argument of `sql()`") { | ||
| Seq("col1 + 1", "CAST('100' AS INT)", "map('a', 1, 'b', 2)", "array(1)").foreach { arg => | ||
| checkError( | ||
| exception = intercept[AnalysisException] { | ||
| spark.sql("SELECT :param1 FROM VALUES (1) AS t(col1)", Map("param1" -> arg)) | ||
| }, | ||
| errorClass = "INVALID_SQL_ARG", | ||
| parameters = Map("name" -> "`param1`"), | ||
| context = ExpectedContext( | ||
| fragment = arg, | ||
| start = 0, | ||
| stop = arg.length - 1)) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we more explicit about why it is invalid?
"The argument of
sql()is not a literal." What is a "SQL Literal statement"?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any SQL statement that can produce a literal, see https://spark.apache.org/docs/latest/sql-ref-literals.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's an expression then. Statements are top level. (Like SELECT, UPDATE, CRAETE, SET).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about just say
SQL literal?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will change this in #39183