-
Notifications
You must be signed in to change notification settings - Fork 29.4k
[SPARK-59633][SQL] Make unary trim collation-aware for case-insensitive ICU collations #58908
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
base: master
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -30,6 +30,12 @@ | |
| */ | ||
| public final class CollationSupport { | ||
|
|
||
| private static final UTF8String DEFAULT_TRIM_STRING = UTF8String.fromString(" "); | ||
|
|
||
| private static boolean useCollationAwareDefaultTrim(final int collationId) { | ||
|
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. Document why useCollationAwareDefaultTrim is only isCaseInsensitive. That helper is the whole policy, and it is not obvious. A short comment should say: CI/CI_AI ICU collations use primary/secondary strength, so UCA treats many Zs characters as equal to U+0020. |
||
| return CollationFactory.isCaseInsensitive(collationId); | ||
| } | ||
|
|
||
| /** | ||
|
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. Nit: unary exec(src, collationId) could delegate to exec(src, SPACE_UTF8, collationId) for CI instead of calling execICU directly. Same result today; one dispatcher if trim collation policy changes. |
||
| * Collation-aware string expressions. | ||
| */ | ||
|
|
@@ -506,6 +512,11 @@ public static class StringTrim { | |
| public static UTF8String exec(final UTF8String srcString) { | ||
| return execBinary(srcString); | ||
| } | ||
| public static UTF8String exec(final UTF8String srcString, final int collationId) { | ||
| return useCollationAwareDefaultTrim(collationId) ? | ||
| execICU(srcString, collationId) : | ||
| execBinary(srcString); | ||
| } | ||
| public static UTF8String exec( | ||
| final UTF8String srcString, | ||
| final UTF8String trimString, | ||
|
|
@@ -527,6 +538,13 @@ public static UTF8String exec( | |
| public static String genCode(final String srcString) { | ||
| return String.format("CollationSupport.StringTrim.execBinary(%s)", srcString); | ||
| } | ||
| public static String genCode(final String srcString, final int collationId) { | ||
| if (useCollationAwareDefaultTrim(collationId)) { | ||
| return String.format( | ||
| "CollationSupport.StringTrim.execICU(%s, %d)", srcString, collationId); | ||
| } | ||
| return genCode(srcString); | ||
| } | ||
| public static String genCode( | ||
| final String srcString, | ||
| final String trimString, | ||
|
|
@@ -553,6 +571,11 @@ public static UTF8String execLowercase( | |
| final int collationId) { | ||
| return CollationAwareUTF8String.lowercaseTrim(srcString, trimString, collationId); | ||
| } | ||
| public static UTF8String execICU( | ||
| final UTF8String srcString, | ||
| final int collationId) { | ||
| return execICU(srcString, DEFAULT_TRIM_STRING, collationId); | ||
| } | ||
| public static UTF8String execICU( | ||
| final UTF8String srcString, | ||
| final UTF8String trimString, | ||
|
|
@@ -571,6 +594,11 @@ public static class StringTrimLeft { | |
| public static UTF8String exec(final UTF8String srcString) { | ||
| return execBinary(srcString); | ||
| } | ||
| public static UTF8String exec(final UTF8String srcString, final int collationId) { | ||
| return useCollationAwareDefaultTrim(collationId) ? | ||
| execICU(srcString, collationId) : | ||
| execBinary(srcString); | ||
| } | ||
| public static UTF8String exec( | ||
| final UTF8String srcString, | ||
| UTF8String trimString, | ||
|
|
@@ -589,6 +617,13 @@ public static UTF8String exec( | |
| public static String genCode(final String srcString) { | ||
| return String.format("CollationSupport.StringTrimLeft.execBinary(%s)", srcString); | ||
| } | ||
| public static String genCode(final String srcString, final int collationId) { | ||
| if (useCollationAwareDefaultTrim(collationId)) { | ||
| return String.format( | ||
| "CollationSupport.StringTrimLeft.execICU(%s, %d)", srcString, collationId); | ||
| } | ||
| return genCode(srcString); | ||
| } | ||
| public static String genCode( | ||
| final String srcString, | ||
| final String trimString, | ||
|
|
@@ -613,6 +648,11 @@ public static UTF8String execLowercase( | |
| final UTF8String trimString) { | ||
| return CollationAwareUTF8String.lowercaseTrimLeft(srcString, trimString); | ||
| } | ||
| public static UTF8String execICU( | ||
| final UTF8String srcString, | ||
| final int collationId) { | ||
| return execICU(srcString, DEFAULT_TRIM_STRING, collationId); | ||
| } | ||
| public static UTF8String execICU( | ||
| final UTF8String srcString, | ||
| final UTF8String trimString, | ||
|
|
@@ -625,6 +665,11 @@ public static class StringTrimRight { | |
| public static UTF8String exec(final UTF8String srcString) { | ||
| return execBinary(srcString); | ||
| } | ||
| public static UTF8String exec(final UTF8String srcString, final int collationId) { | ||
| return useCollationAwareDefaultTrim(collationId) ? | ||
| execICU(srcString, collationId) : | ||
| execBinary(srcString); | ||
| } | ||
| public static UTF8String exec( | ||
| final UTF8String srcString, | ||
| final UTF8String trimString, | ||
|
|
@@ -646,6 +691,13 @@ public static UTF8String exec( | |
| public static String genCode(final String srcString) { | ||
| return String.format("CollationSupport.StringTrimRight.execBinary(%s)", srcString); | ||
| } | ||
| public static String genCode(final String srcString, final int collationId) { | ||
| if (useCollationAwareDefaultTrim(collationId)) { | ||
| return String.format( | ||
| "CollationSupport.StringTrimRight.execICU(%s, %d)", srcString, collationId); | ||
| } | ||
| return genCode(srcString); | ||
| } | ||
| public static String genCode( | ||
| final String srcString, | ||
| final String trimString, | ||
|
|
@@ -671,6 +723,11 @@ public static UTF8String execLowercase( | |
| final int collationId) { | ||
| return CollationAwareUTF8String.lowercaseTrimRight(srcString, trimString, collationId); | ||
| } | ||
| public static UTF8String execICU( | ||
| final UTF8String srcString, | ||
| final int collationId) { | ||
| return execICU(srcString, DEFAULT_TRIM_STRING, collationId); | ||
| } | ||
| public static UTF8String execICU( | ||
| final UTF8String srcString, | ||
| final UTF8String trimString, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2757,11 +2757,11 @@ private void assertStringTrim(String collationName, String sourceString, String | |
|
|
||
|
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. assertStringTrimLeft / assertStringTrimRight still call the old 1-arg exec(src). The PR updated assertStringTrim to pass collationId when the trim string is the default space, but Left/Right helpers still hit the binary-only overloads. Existing testStringTrimLeft / testStringTrimRight default-space cases therefore do not exercise the new product path. After this lands, those 1-arg methods are a footgun: they look like the public API but no longer match StringTrimLeft.doEval. |
||
| if (trimString == null) { | ||
| // Trim string is ASCII space. | ||
| result = CollationSupport.StringTrim.exec(src); | ||
| UTF8String trimLeft = CollationSupport.StringTrimLeft.exec(src); | ||
| resultTrimLeftRight = CollationSupport.StringTrimRight.exec(trimLeft); | ||
| UTF8String trimRight = CollationSupport.StringTrimRight.exec(src); | ||
| resultTrimRightLeft = CollationSupport.StringTrimLeft.exec(trimRight); | ||
| result = CollationSupport.StringTrim.exec(src, collationId); | ||
| UTF8String trimLeft = CollationSupport.StringTrimLeft.exec(src, collationId); | ||
| resultTrimLeftRight = CollationSupport.StringTrimRight.exec(trimLeft, collationId); | ||
| UTF8String trimRight = CollationSupport.StringTrimRight.exec(src, collationId); | ||
| resultTrimRightLeft = CollationSupport.StringTrimLeft.exec(trimRight, collationId); | ||
| } else { | ||
| // Trim string is specified. | ||
| result = CollationSupport.StringTrim.exec(src, trim, collationId); | ||
|
|
@@ -2778,6 +2778,96 @@ private void assertStringTrim(String collationName, String sourceString, String | |
| assertEquals(resultTrimRightLeft, result); | ||
| } | ||
|
|
||
| private void assertDefaultStringTrims( | ||
| String collationName, | ||
| String sourceString, | ||
| String expectedLeft, | ||
| String expectedRight, | ||
| String expectedBoth) throws SparkException { | ||
| int collationId = CollationFactory.collationNameToId(collationName); | ||
| UTF8String source = UTF8String.fromString(sourceString); | ||
| UTF8String defaultTrimString = UTF8String.fromString(" "); | ||
|
|
||
| UTF8String trimLeft = CollationSupport.StringTrimLeft.exec(source, collationId); | ||
| UTF8String trimRight = CollationSupport.StringTrimRight.exec(source, collationId); | ||
| UTF8String trimBoth = CollationSupport.StringTrim.exec(source, collationId); | ||
|
|
||
| assertEquals(UTF8String.fromString(expectedLeft), trimLeft); | ||
| assertEquals(UTF8String.fromString(expectedRight), trimRight); | ||
| assertEquals(UTF8String.fromString(expectedBoth), trimBoth); | ||
| assertEquals( | ||
| CollationSupport.StringTrimLeft.exec(source, defaultTrimString, collationId), trimLeft); | ||
| assertEquals( | ||
| CollationSupport.StringTrimRight.exec(source, defaultTrimString, collationId), trimRight); | ||
| assertEquals( | ||
| CollationSupport.StringTrim.exec(source, defaultTrimString, collationId), trimBoth); | ||
| } | ||
|
|
||
| @Test | ||
| public void testDefaultStringTrimsUseCollation() throws SparkException { | ||
| String[] spaceSeparators = { | ||
| "\u00A0", "\u1680", "\u2000", "\u2001", "\u2002", "\u2003", "\u2004", "\u2005", | ||
| "\u2006", "\u2007", "\u2008", "\u2009", "\u200A", "\u202F", "\u205F", "\u3000" | ||
| }; | ||
| String[] unaffectedCollations = { | ||
| UTF8_BINARY, | ||
| "UTF8_BINARY_RTRIM", | ||
| UTF8_LCASE, | ||
| "UTF8_LCASE_RTRIM", | ||
| UNICODE, | ||
| "UNICODE_RTRIM" | ||
| }; | ||
| String[] affectedCollations = { | ||
| UNICODE_CI, | ||
| "UNICODE_CI_RTRIM", | ||
| "UNICODE_CI_AI", | ||
| "UNICODE_CI_AI_RTRIM" | ||
| }; | ||
|
|
||
| for (String collation : unaffectedCollations) { | ||
| int collationId = CollationFactory.collationNameToId(collation); | ||
| assertEquals( | ||
| "CollationSupport.StringTrim.execBinary(source)", | ||
| CollationSupport.StringTrim.genCode("source", collationId)); | ||
| assertEquals( | ||
| "CollationSupport.StringTrimLeft.execBinary(source)", | ||
| CollationSupport.StringTrimLeft.genCode("source", collationId)); | ||
| assertEquals( | ||
| "CollationSupport.StringTrimRight.execBinary(source)", | ||
| CollationSupport.StringTrimRight.genCode("source", collationId)); | ||
| } | ||
| for (String collation : affectedCollations) { | ||
| int collationId = CollationFactory.collationNameToId(collation); | ||
| assertEquals( | ||
| String.format("CollationSupport.StringTrim.execICU(source, %d)", collationId), | ||
| CollationSupport.StringTrim.genCode("source", collationId)); | ||
| assertEquals( | ||
| String.format("CollationSupport.StringTrimLeft.execICU(source, %d)", collationId), | ||
| CollationSupport.StringTrimLeft.genCode("source", collationId)); | ||
| assertEquals( | ||
| String.format("CollationSupport.StringTrimRight.execICU(source, %d)", collationId), | ||
| CollationSupport.StringTrimRight.genCode("source", collationId)); | ||
| } | ||
|
|
||
| for (String separator : spaceSeparators) { | ||
| String source = separator + "abc" + separator; | ||
| for (String collation : unaffectedCollations) { | ||
| assertDefaultStringTrims(collation, source, source, source, source); | ||
| } | ||
| for (String collation : affectedCollations) { | ||
| assertDefaultStringTrims( | ||
| collation, source, "abc" + separator, separator + "abc", "abc"); | ||
| } | ||
| } | ||
|
|
||
| for (String collation : affectedCollations) { | ||
| assertDefaultStringTrims(collation, " abc ", "abc ", " abc", "abc"); | ||
| assertDefaultStringTrims(collation, "\tabc\t", "\tabc\t", "\tabc\t", "\tabc\t"); | ||
| assertDefaultStringTrims( | ||
| collation, "\u200Babc\u200B", "\u200Babc\u200B", "\u200Babc\u200B", "\u200Babc\u200B"); | ||
| } | ||
| } | ||
|
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. Add the JIRA reproduction as a SQL test. collations-padding-trim.sql already covers two-arg TRIM/LTRIM/RTRIM and has no unary TRIM(col COLLATE UNICODE_CI) case. A golden SQL test for trim(concat(chr(160), 'abc', chr(160)) COLLATE UNICODE_CI) |
||
|
|
||
|
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. Test gaps - the Catalyst test only checks NBSP on UNICODE_CI vs UNICODE. Worth adding, still in checkEvaluation:
|
||
| @Test | ||
| public void testStringTrim() throws SparkException { | ||
| // Basic tests. | ||
|
|
||
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.
Reuse UTF8String.SPACE_UTF8 instead of a new DEFAULT_TRIM_STRING.
Same value, already a public constant on UTF8String.