-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-16281][SQL] Implement parse_url SQL function #14008
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 2 commits
def5982
a2ab582
08c20e0
1651b5d
a02ad9b
65c2a6a
441cea2
1319b37
859d143
34a10d4
f75e3ae
c2c4513
c072104
4299ac5
fd70c6a
40e1cd4
7b0bca4
013ff46
95114ef
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 |
|---|---|---|
|
|
@@ -17,7 +17,9 @@ | |
|
|
||
| package org.apache.spark.sql.catalyst.expressions | ||
|
|
||
| import java.net.URL | ||
| import java.text.{DecimalFormat, DecimalFormatSymbols} | ||
| import java.util.regex.Pattern | ||
| import java.util.{HashMap, Locale, Map => JMap} | ||
|
Member
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. Also, imports need to be sorted. |
||
|
|
||
| import org.apache.spark.sql.catalyst.InternalRow | ||
|
|
@@ -27,6 +29,8 @@ import org.apache.spark.sql.catalyst.util.ArrayData | |
| import org.apache.spark.sql.types._ | ||
| import org.apache.spark.unsafe.types.{ByteArray, UTF8String} | ||
|
|
||
| import scala.util.control.NonFatal | ||
|
Member
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. Oh, we have import ordering. You should move
Member
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. By the way, I'm wondering why this does not cause build errors. Usually, it's caught by build errors. Anyway, could you run
Contributor
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.
I just use IDEA to do the compile job, it seems this script does not be called. I'll manually call it next time. Thanks. |
||
|
|
||
| //////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| // This file defines expressions for string operations. | ||
| //////////////////////////////////////////////////////////////////////////////////////////////////// | ||
|
|
@@ -652,6 +656,125 @@ case class StringRPad(str: Expression, len: Expression, pad: Expression) | |
| override def prettyName: String = "rpad" | ||
| } | ||
|
|
||
| /** | ||
| * Extracts a part from a URL | ||
| */ | ||
| @ExpressionDescription( | ||
| usage = "_FUNC_(url, partToExtract[, key]) - extracts a part from a URL", | ||
| extended = """Parts: HOST, PATH, QUERY, REF, PROTOCOL, AUTHORITY, FILE, USERINFO. | ||
| Key specifies which query to extract. | ||
| Examples: | ||
| > SELECT _FUNC_('http://spark.apache.org/path?query=1', 'HOST')\n 'spark.apache.org' | ||
| > SELECT _FUNC_('http://spark.apache.org/path?query=1', 'QUERY')\n 'query=1' | ||
| > SELECT _FUNC_('http://spark.apache.org/path?query=1', 'QUERY', 'query')\n '1'""") | ||
|
Member
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. Maybe,
Contributor
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. OK |
||
| case class ParseUrl(children: Expression*) | ||
|
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. there is no vararg in parse url, is there?
Contributor
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. The parameter I'll keep on trying to do it better. If there is any advice, that will be very helpful. Thanks again.
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. I think you can just declare a separate ctor that accepts two args
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. should we use
Contributor
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. You are right. I'll fix this. |
||
| extends Expression with ImplicitCastInputTypes with CodegenFallback { | ||
|
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. It's ok to use
Contributor
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. OK. I'll open a JIRA for the codegen part and keep working on it.
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. here -- i don't think it makes a lot of sense to use ImplicitCastInputTypes here, since we are talking about urls. Why don't we just use ExpectsInputTypes
Contributor
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. I am trying to make spark's behavior mostly like hive.
Should we keep the same in spark?
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. I think it's OK in this case to not follow. This function is so esoteric that I doubt people will complain. If they do, we can always add the implicit casting later.
Contributor
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. OK, I'll use ExpectsInputTypes.
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. Actually let's just keep it. Might as well since the code is already written.
Contributor
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. Well, I have missed this comment and finished the change...
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. oh well this works |
||
|
|
||
| override def nullable = true | ||
|
Member
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.
Member
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. Public functions need explicit type declaration.
Contributor
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. Yes, |
||
| override def inputTypes: Seq[DataType] = Seq.fill(children.size)(StringType) | ||
| override def dataType: DataType = StringType | ||
|
|
||
| @transient private var lastUrlStr: UTF8String = _ | ||
| @transient private var lastUrl: URL = _ | ||
| // last key in string, we will update the pattern if key value changed. | ||
| @transient private var lastKey: UTF8String = _ | ||
| // last regex pattern, we cache it for performance concern | ||
| @transient private var pattern: Pattern = _ | ||
|
|
||
| private lazy val stringExprs = children.toArray | ||
|
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. I don't think it's necessary... we only have 2 or 3 parameters...
Contributor
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. OK.. |
||
| private val HOST = UTF8String.fromString("HOST") | ||
| private val PATH = UTF8String.fromString("PATH") | ||
| private val QUERY = UTF8String.fromString("QUERY") | ||
| private val REF = UTF8String.fromString("REF") | ||
| private val PROTOCOL = UTF8String.fromString("PROTOCOL") | ||
| private val FILE = UTF8String.fromString("FILE") | ||
| private val AUTHORITY = UTF8String.fromString("AUTHORITY") | ||
| private val USERINFO = UTF8String.fromString("USERINFO") | ||
| private val REGEXPREFIX = "(&|^)" | ||
| private val REGEXSUBFIX = "=([^&]*)" | ||
|
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. should we put these constants into an object?
Contributor
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. Make sense. |
||
|
|
||
| override def checkInputDataTypes(): TypeCheckResult = { | ||
| if (children.size > 3 || children.size < 2) { | ||
| TypeCheckResult.TypeCheckFailure("parse_url function requires two or three arguments") | ||
|
Member
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. And, here, let's use TypeCheckResult.TypeCheckFailure(s"$prettyName function requires two or three arguments")
Contributor
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. OK, thank you. |
||
| } else { | ||
| super[ImplicitCastInputTypes].checkInputDataTypes() | ||
| } | ||
| } | ||
|
|
||
| def parseUrlWithoutKey(url: Any, partToExtract: Any): Any = { | ||
|
Member
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. Could you make this
Contributor
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. OK |
||
| if (url == null || partToExtract == null) { | ||
| null | ||
| } else { | ||
| if (lastUrlStr == null || !url.equals(lastUrlStr)) { | ||
|
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. is this optimization mainly for when the
Contributor
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. Yes. When the
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. you can follow
Contributor
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. Thought we judge on the |
||
| try { | ||
| lastUrlStr = url.asInstanceOf[UTF8String].clone() | ||
|
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. so if all urls are different, we need to clone them everytime?
Contributor
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. Em, good point. I'll try to find a better way. |
||
| lastUrl = new URL(url.toString) | ||
| } catch { | ||
| case NonFatal(_) => return null | ||
|
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. We should check the possible exceptions for
Contributor
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. As hive returns null for invalid urls, it seems reasonable that spark does the same thing.
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. oh this is just code style suggestion: only catch the exception that may be thrown in the
Contributor
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. Yes, it only throws
|
||
| } | ||
| } | ||
|
|
||
| if (partToExtract.equals(HOST)) { | ||
| UTF8String.fromString(lastUrl.getHost) | ||
| } else if (partToExtract.equals(PATH)) { | ||
| UTF8String.fromString(lastUrl.getPath) | ||
| } else if (partToExtract.equals(QUERY)) { | ||
| UTF8String.fromString(lastUrl.getQuery) | ||
| } else if (partToExtract.equals(REF)) { | ||
| UTF8String.fromString(lastUrl.getRef) | ||
| } else if (partToExtract.equals(PROTOCOL)) { | ||
| UTF8String.fromString(lastUrl.getProtocol) | ||
| } else if (partToExtract.equals(FILE)) { | ||
| UTF8String.fromString(lastUrl.getFile) | ||
| } else if (partToExtract.equals(AUTHORITY)) { | ||
| UTF8String.fromString(lastUrl.getAuthority) | ||
| } else if (partToExtract.equals(USERINFO)) { | ||
| UTF8String.fromString(lastUrl.getUserInfo) | ||
| } else { | ||
| null | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override def eval(input: InternalRow): Any = { | ||
|
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. this is somewhat convoluted with 4 levels of nesting, I think you can rewrite it this way to make it easier to follow
Contributor
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. Make sense, I'll fix this. |
||
| val url = stringExprs(0).eval(input) | ||
|
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. usually we will cast the result of
Contributor
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. Make sense. I am trying to declare vars more explicitly. Thanks. |
||
| val partToExtract = stringExprs(1).eval(input) | ||
| if (stringExprs.size == 2) { | ||
|
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. I think it's more clear to put the null handling at the very beginning, e.g.
Contributor
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. Oh, this implementation is very good! |
||
| parseUrlWithoutKey(url, partToExtract) | ||
| } else { // QUERY with key | ||
| if (partToExtract == null || !partToExtract.equals(QUERY)) { | ||
| null | ||
| } else { | ||
| val query = parseUrlWithoutKey(url, partToExtract) | ||
| if (query == null) { | ||
| null | ||
| } else { | ||
| val key = stringExprs(2).eval(input) | ||
| if (key == null) { | ||
| null | ||
| } else { | ||
| if (!key.equals(lastKey)) { | ||
| lastKey = key.asInstanceOf[UTF8String].clone() | ||
| val sb = new StringBuilder() | ||
| sb.append(REGEXPREFIX).append(key.toString).append(REGEXSUBFIX) | ||
| pattern = Pattern.compile(sb.toString()) | ||
| } | ||
|
|
||
| val m = pattern.matcher(query.toString) | ||
| if (m.find()) { | ||
| UTF8String.fromString(m.group(2)) | ||
| } else { | ||
| null | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override def prettyName: String = "parse_url" | ||
| } | ||
|
|
||
| /** | ||
| * Returns the input formatted according do printf-style format strings | ||
| */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -725,4 +725,45 @@ class StringExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { | |
| checkEvaluation(FindInSet(Literal("abf"), Literal("abc,b,ab,c,def")), 0) | ||
| checkEvaluation(FindInSet(Literal("ab,"), Literal("abc,b,ab,c,def")), 0) | ||
| } | ||
|
|
||
| test("ParseUrl") { | ||
| def checkParseUrl(expected: String, urlStr: String, partToExtract: String): Unit = { | ||
| checkEvaluation( | ||
| ParseUrl(Literal.create(urlStr, StringType), Literal.create(partToExtract, StringType)), | ||
| expected) | ||
| } | ||
| def checkParseUrlWithKey( | ||
| expected: String, urlStr: String, | ||
|
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. one parameter one line please
Contributor
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. OK |
||
| partToExtract: String, key: String): Unit = { | ||
| checkEvaluation( | ||
| ParseUrl(Literal.create(urlStr, StringType), Literal.create(partToExtract, StringType), | ||
| Literal.create(key, StringType)), expected) | ||
| } | ||
|
|
||
| checkParseUrl("spark.apache.org", "http://spark.apache.org/path?query=1", "HOST") | ||
| checkParseUrl("/path", "http://spark.apache.org/path?query=1", "PATH") | ||
| checkParseUrl("query=1", "http://spark.apache.org/path?query=1", "QUERY") | ||
| checkParseUrl("Ref", "http://spark.apache.org/path?query=1#Ref", "REF") | ||
| checkParseUrl("http", "http://spark.apache.org/path?query=1", "PROTOCOL") | ||
| checkParseUrl("/path?query=1", "http://spark.apache.org/path?query=1", "FILE") | ||
| checkParseUrl("spark.apache.org:8080", "http://spark.apache.org:8080/path?query=1", "AUTHORITY") | ||
| checkParseUrl("userinfo", "http://userinfo@spark.apache.org/path?query=1", "USERINFO") | ||
|
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. what will happen if there is no userinfo in the url?
Contributor
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. Then the result is |
||
| checkParseUrlWithKey("1", "http://spark.apache.org/path?query=1", "QUERY", "query") | ||
|
|
||
| // Null checking | ||
| checkParseUrl(null, null, "HOST") | ||
| checkParseUrl(null, "http://spark.apache.org/path?query=1", null) | ||
| checkParseUrl(null, null, null) | ||
| checkParseUrl(null, "test", "HOST") | ||
| checkParseUrl(null, "http://spark.apache.org/path?query=1", "NO") | ||
| checkParseUrlWithKey(null, "http://spark.apache.org/path?query=1", "HOST", "query") | ||
| checkParseUrlWithKey(null, "http://spark.apache.org/path?query=1", "QUERY", "quer") | ||
| checkParseUrlWithKey(null, "http://spark.apache.org/path?query=1", "QUERY", null) | ||
|
Member
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. Could you add exceptional cases by using the following statement?
Contributor
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. I am not sure. Is there any exceptional case?
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. e.g. invalid url, invalid
Contributor
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. Oh sorry, I miss the point.
Contributor
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. As invalid url and invalid part just get
Member
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. Invalid
Member
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. I'm not sure how to handle this kind of malformed queries. Hive makes reasonable message.
Contributor
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. Yes, you are right. Invalid |
||
| checkParseUrlWithKey(null, "http://spark.apache.org/path?query=1", "QUERY", "") | ||
|
|
||
| // arguments checking | ||
| assert(ParseUrl(Literal("1")).checkInputDataTypes().isFailure) | ||
| assert(ParseUrl(Literal("1"), Literal("2"), Literal("3"), Literal("4")) | ||
| .checkInputDataTypes().isFailure) | ||
| } | ||
| } | ||
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.
this should go before printf
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.
OK, Thank you for review. I'll fix this.