This is an automated email from the ASF dual-hosted git repository.
cloud-fan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 2572d806923a [SPARK-58275][SQL][PYTHON] Add `normalize` Unicode
normalization SQL function
2572d806923a is described below
commit 2572d806923a9cb5f2094a822d4fba9df979685b
Author: SreeramaYeshwanthGowd <[email protected]>
AuthorDate: Tue Aug 11 22:40:24 2026 +0800
[SPARK-58275][SQL][PYTHON] Add `normalize` Unicode normalization SQL
function
### What changes were proposed in this pull request?
Add a built in `normalize(str[, form])` scalar function that performs
Unicode normalization, backed by Spark's bundled ICU4J library.
API surface added:
- SQL: `normalize(str)` and `normalize(str, form)`
- Scala DataFrame: `functions.normalize(col)` and `functions.normalize(col,
form)`
- PySpark, classic and Spark Connect: `pyspark.sql.functions.normalize(str,
form=None)`
Key design choices:
- The default form is NFC. Supported forms are NFC, NFD, NFKC, and NFKD,
case insensitive, matching the ANSI SQL standard and PostgreSQL, Trino, and
BigQuery. Forms follow Unicode Standard Annex #15's decomposition/composition
algorithm.
- The `form` argument is a regular string expression or column, not
required to be foldable, consistent with how other Spark functions take a mode
string (for example `date_trunc`). This keeps the change small and idiomatic
and avoids adding new grammar. An invalid form raises a clear
`INVALID_PARAMETER_VALUE.NORMALIZE_FORM` error.
- Implemented as a `RuntimeReplaceable` expression backed by a
`StaticInvoke` into `ExpressionImplUtils`, so there is no hand written codegen,
following the recent `hmac` function.
- Normalization uses ICU4J (the same library backing Spark's collation
support) rather than the JDK's `java.text.Normalizer`, so behavior is pinned to
Spark's bundled ICU4J/Unicode data and does not vary across JVM vendors or
versions. This is documented on the public SQL/Scala/PySpark doc surfaces.
### Why are the changes needed?
Unicode normalization is a common need in text processing and data
cleaning, for example to compare strings that look identical but differ in code
point composition. Spark has no built in way to do this, so users fall back to
a UDF, which cannot be optimized by Catalyst. This function is part of the SQL
standard and is already available in PostgreSQL, Trino, and Google BigQuery, so
it also improves parity with the engines that Spark users migrate from.
### Does this PR introduce _any_ user-facing change?
Yes. It adds a new built in SQL function `normalize` and the corresponding
Scala and PySpark DataFrame API entries. No existing behavior changes.
Example:
```
spark-sql> SELECT normalize('fi', 'NFKC');
fi
```
### How was this patch tested?
Added unit tests in `ExpressionImplUtilsSuite` and `StringExpressionsSuite`
covering all four forms (NFC, NFD, NFKC, NFKD), a case insensitive form name,
null propagation, empty input, a supplementary (surrogate pair) character, a
canonical singleton decomposition, a compatibility decomposition, the invalid
form error, and a conformance vector for canonical reordering of combining
marks with different combining classes (Unicode Standard Annex #15). Added a
DataFrame API test in `Strin [...]
### Was this patch authored or co-authored using generative AI tooling? No
Closes #57450 from SreeramaYeshwanthGowd/add-normalize-function.
Authored-by: SreeramaYeshwanthGowd <[email protected]>
Signed-off-by: Wenchen Fan <[email protected]>
---
.../src/main/resources/error/error-conditions.json | 5 ++
.../source/reference/pyspark.sql/functions.rst | 1 +
python/pyspark/sql/connect/functions/builtin.py | 10 ++++
python/pyspark/sql/functions/__init__.py | 1 +
python/pyspark/sql/functions/builtin.py | 40 +++++++++++++++
python/pyspark/sql/tests/test_functions.py | 8 +++
.../scala/org/apache/spark/sql/functions.scala | 28 ++++++++++
.../catalyst/expressions/ExpressionImplUtils.java | 24 +++++++++
.../sql/catalyst/analysis/FunctionRegistry.scala | 1 +
.../catalyst/expressions/stringExpressions.scala | 60 ++++++++++++++++++++++
.../spark/sql/errors/QueryExecutionErrors.scala | 9 ++++
.../expressions/ExpressionImplUtilsSuite.scala | 42 +++++++++++++++
.../expressions/StringExpressionsSuite.scala | 23 ++++++++-
.../sql-functions/sql-expression-schema.md | 1 +
.../nonansi/string-functions.sql.out | 35 +++++++++++++
.../analyzer-results/string-functions.sql.out | 35 +++++++++++++
.../sql-tests/inputs/string-functions.sql | 9 +++-
.../results/nonansi/string-functions.sql.out | 49 ++++++++++++++++++
.../sql-tests/results/string-functions.sql.out | 49 ++++++++++++++++++
.../apache/spark/sql/StringFunctionsSuite.scala | 10 ++++
20 files changed, 438 insertions(+), 2 deletions(-)
diff --git a/common/utils/src/main/resources/error/error-conditions.json
b/common/utils/src/main/resources/error/error-conditions.json
index 62eef58c0543..f89f3bf5e445 100644
--- a/common/utils/src/main/resources/error/error-conditions.json
+++ b/common/utils/src/main/resources/error/error-conditions.json
@@ -4782,6 +4782,11 @@
"expects a long literal, but got <invalidValue>."
]
},
+ "NORMALIZE_FORM" : {
+ "message" : [
+ "expects one of the normalization forms 'NFC', 'NFD', 'NFKC',
'NFKD', but got <form>."
+ ]
+ },
"NULL" : {
"message" : [
"expects a non-NULL value."
diff --git a/python/docs/source/reference/pyspark.sql/functions.rst
b/python/docs/source/reference/pyspark.sql/functions.rst
index dd9231b8dd4e..a79922c8051d 100644
--- a/python/docs/source/reference/pyspark.sql/functions.rst
+++ b/python/docs/source/reference/pyspark.sql/functions.rst
@@ -193,6 +193,7 @@ String Functions
ltrim
make_valid_utf8
mask
+ normalize
octet_length
overlay
position
diff --git a/python/pyspark/sql/connect/functions/builtin.py
b/python/pyspark/sql/connect/functions/builtin.py
index ea2e2fafc501..8451ae386770 100644
--- a/python/pyspark/sql/connect/functions/builtin.py
+++ b/python/pyspark/sql/connect/functions/builtin.py
@@ -2741,6 +2741,16 @@ def try_validate_utf8(str: "ColumnOrName") -> Column:
try_validate_utf8.__doc__ = pysparkfuncs.try_validate_utf8.__doc__
+def normalize(str: "ColumnOrName", form: Optional["ColumnOrName"] = None) ->
Column:
+ if form is None:
+ return _invoke_function_over_columns("normalize", str)
+ else:
+ return _invoke_function_over_columns("normalize", str, form)
+
+
+normalize.__doc__ = pysparkfuncs.normalize.__doc__
+
+
def format_number(col: "ColumnOrName", d: int) -> Column:
return _invoke_function("format_number", _to_col(col), lit(d))
diff --git a/python/pyspark/sql/functions/__init__.py
b/python/pyspark/sql/functions/__init__.py
index a66d7d36d103..d20790f20bcb 100644
--- a/python/pyspark/sql/functions/__init__.py
+++ b/python/pyspark/sql/functions/__init__.py
@@ -154,6 +154,7 @@ __all__ = [ # noqa: F405
"ltrim",
"make_valid_utf8",
"mask",
+ "normalize",
"octet_length",
"overlay",
"position",
diff --git a/python/pyspark/sql/functions/builtin.py
b/python/pyspark/sql/functions/builtin.py
index dff38c122ca0..d5ec94fa6d62 100644
--- a/python/pyspark/sql/functions/builtin.py
+++ b/python/pyspark/sql/functions/builtin.py
@@ -16024,6 +16024,46 @@ def try_validate_utf8(str: "ColumnOrName") -> Column:
return _invoke_function_over_columns("try_validate_utf8", str)
+@_try_remote_functions
+def normalize(str: "ColumnOrName", form: Optional["ColumnOrName"] = None) ->
Column:
+ """
+ Returns the Unicode normalization of ``str`` using the given normalization
``form``, as
+ defined by Unicode Standard Annex #15. Normalization is backed by Spark's
bundled ICU4J
+ library rather than the JVM's own Unicode data, so results are stable
across JVM vendors
+ and versions.
+
+ .. versionadded:: 4.4.0
+
+ Parameters
+ ----------
+ str : :class:`~pyspark.sql.Column` or column name
+ the input string to normalize.
+ form : :class:`~pyspark.sql.Column` or column name, optional
+ the normalization form, one of 'NFC', 'NFD', 'NFKC', 'NFKD'
(case-insensitive).
+ If omitted, 'NFC' is used.
+
+ Returns
+ -------
+ :class:`~pyspark.sql.Column`
+ the normalized string.
+
+ Examples
+ --------
+ >>> import pyspark.sql.functions as sf
+ >>> df = spark.createDataFrame([("\ufb01",)], ["s"])
+ >>> df.select(sf.normalize(df.s, sf.lit("NFKC"))).show()
+ +------------------+
+ |normalize(s, NFKC)|
+ +------------------+
+ | fi|
+ +------------------+
+ """
+ if form is None:
+ return _invoke_function_over_columns("normalize", str)
+ else:
+ return _invoke_function_over_columns("normalize", str, form)
+
+
@_try_remote_functions
def format_number(col: "ColumnOrName", d: int) -> Column:
"""
diff --git a/python/pyspark/sql/tests/test_functions.py
b/python/pyspark/sql/tests/test_functions.py
index b5ee99fc9565..61b4c4d258a0 100644
--- a/python/pyspark/sql/tests/test_functions.py
+++ b/python/pyspark/sql/tests/test_functions.py
@@ -1022,6 +1022,14 @@ class FunctionsTestsMixin:
null_result = df.select(F.jaro_winkler_similarity(df.l,
F.lit(None))).first()[0]
self.assertIsNone(null_result)
+ def test_normalize_function(self):
+ df = self.spark.createDataFrame([("\ufb01",)], ["s"])
+ result = df.select(F.normalize(df.s, F.lit("NFKC"))).first()[0]
+ self.assertEqual(result, "fi")
+ # Null handling
+ null_result = df.select(F.normalize(F.lit(None),
F.lit("NFC"))).first()[0]
+ self.assertIsNone(null_result)
+
def test_between_function(self):
df = self.spark.createDataFrame(
[Row(a=1, b=2, c=3), Row(a=2, b=1, c=3), Row(a=4, b=1, c=4)]
diff --git a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
index 514ca368e6a3..96d5ec930604 100644
--- a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
+++ b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
@@ -7533,6 +7533,34 @@ object functions {
def try_validate_utf8(str: Column): Column =
Column.fn("try_validate_utf8", str)
+ /**
+ * Returns the Unicode normalization of `str` using the given normalization
`form`. Valid forms
+ * are 'NFC', 'NFD', 'NFKC', and 'NFKD', as defined by Unicode Standard
Annex #15. The form name
+ * is case-insensitive. Normalization is backed by Spark's bundled ICU4J
library rather than the
+ * JVM's own Unicode data, so results are stable across JVM vendors and
versions.
+ *
+ * @param str
+ * the input string to normalize.
+ * @param form
+ * the normalization form: 'NFC', 'NFD', 'NFKC', or 'NFKD'.
+ * @group string_funcs
+ * @since 4.4.0
+ */
+ def normalize(str: Column, form: Column): Column =
+ Column.fn("normalize", str, form)
+
+ /**
+ * Returns the Unicode normalization of `str` using the default form 'NFC'.
To use a different
+ * form, call the two-argument overload.
+ *
+ * @param str
+ * the input string to normalize.
+ * @group string_funcs
+ * @since 4.4.0
+ */
+ def normalize(str: Column): Column =
+ Column.fn("normalize", str)
+
/**
* Formats numeric column x to a format like '#,###,###.##', rounded to d
decimal places with
* HALF_EVEN round mode, and returns the result as a string column.
diff --git
a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtils.java
b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtils.java
index 3f2c7e1fc5d4..4f7446d0cd9c 100644
---
a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtils.java
+++
b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtils.java
@@ -17,6 +17,8 @@
package org.apache.spark.sql.catalyst.expressions;
+import com.ibm.icu.text.Normalizer2;
+
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
@@ -149,6 +151,28 @@ public class ExpressionImplUtils {
else return null;
}
+ /**
+ * Normalizes the given string using the given Unicode normalization form,
per the
+ * decomposition/composition algorithm in Unicode Standard Annex #15. Uses
ICU4J, the same
+ * library backing Spark's collation support, instead of the JDK's {@code
java.text.Normalizer}
+ * so the result is pinned to Spark's bundled ICU4J/Unicode data (see {@code
icu4j.version} in
+ * pom.xml) and does not vary across JVM vendors or versions.
+ *
+ * @param input the input string to normalize.
+ * @param form the normalization form, one of NFC, NFD, NFKC, NFKD
(case-insensitive).
+ * @return the normalized string.
+ */
+ public static UTF8String normalize(UTF8String input, UTF8String form) {
+ Normalizer2 normalizer = switch (form.toString().toUpperCase(Locale.ROOT))
{
+ case "NFC" -> Normalizer2.getNFCInstance();
+ case "NFD" -> Normalizer2.getNFDInstance();
+ case "NFKC" -> Normalizer2.getNFKCInstance();
+ case "NFKD" -> Normalizer2.getNFKDInstance();
+ default -> throw
QueryExecutionErrors.invalidNormalizeFormError(form.toString());
+ };
+ return UTF8String.fromString(normalizer.normalize(input.toString()));
+ }
+
public static byte[] aesEncrypt(byte[] input,
byte[] key,
UTF8String mode,
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
index d7b0f4a440d1..5eef532ab20b 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
@@ -691,6 +691,7 @@ object FunctionRegistry {
expression[ValidateUTF8]("validate_utf8"),
expression[TryValidateUTF8]("try_validate_utf8"),
expression[Quote]("quote"),
+ expression[Normalize]("normalize"),
// url functions
expression[UrlEncode]("url_encode"),
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/stringExpressions.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/stringExpressions.scala
index 6dfdea1bdc7b..45c9e9256aed 100755
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/stringExpressions.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/stringExpressions.scala
@@ -969,6 +969,66 @@ case class TryValidateUTF8(input: Expression) extends
RuntimeReplaceable with Im
}
+/**
+ * A function that returns the Unicode normalization of a string.
+ */
+// scalastyle:off
+@ExpressionDescription(
+ usage = """
+ _FUNC_(str[, form]) - Returns the Unicode normalization of `str` using the
normalization `form`.
+ Valid forms are 'NFC' (default), 'NFD', 'NFKC', and 'NFKD', as defined
by Unicode Standard
+ Annex #15: 'NFD'/'NFKD' apply canonical/compatibility decomposition;
'NFC'/'NFKC' apply the
+ same decomposition followed by canonical composition. The form name is
case-insensitive.
+ Normalization is backed by Spark's bundled ICU4J library rather than the
JVM's own Unicode
+ data, so results are stable across JVM vendors and versions.
+ """,
+ arguments = """
+ Arguments:
+ * str - a string expression to normalize.
+ * form - a string expression giving the normalization form: 'NFC',
'NFD', 'NFKC', or 'NFKD'.
+ If omitted, 'NFC' is used.
+ """,
+ examples = """
+ Examples:
+ > SELECT _FUNC_('fi', 'NFKC');
+ fi
+ """,
+ since = "4.4.0",
+ group = "string_funcs")
+// scalastyle:on
+case class Normalize(input: Expression, form: Expression)
+ extends RuntimeReplaceable with ImplicitCastInputTypes with
BinaryLike[Expression] {
+ override def nullIntolerant: Boolean = true
+
+ override lazy val replacement: Expression =
+ StaticInvoke(
+ classOf[ExpressionImplUtils],
+ input.dataType,
+ "normalize",
+ Seq(input, form),
+ inputTypes)
+
+ def this(input: Expression) = this(input, Literal("NFC"))
+
+ override def inputTypes: Seq[AbstractDataType] =
+ Seq(StringTypeWithCollation(supportsTrimCollation = true),
+ StringTypeWithCollation(supportsTrimCollation = true))
+
+ override def nodeName: String = "normalize"
+
+ override def nullable: Boolean = true
+
+ override def left: Expression = input
+
+ override def right: Expression = form
+
+ override protected def withNewChildrenInternal(
+ newLeft: Expression, newRight: Expression): Normalize = {
+ copy(input = newLeft, form = newRight)
+ }
+
+}
+
/**
* Replace all occurrences with string.
*/
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
index 8f1b16232fd4..646d296c11c6 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
@@ -2566,6 +2566,15 @@ private[sql] object QueryExecutionErrors extends
QueryErrorsBase with ExecutionE
"detailMessage" -> detailMessage))
}
+ def invalidNormalizeFormError(form: String): RuntimeException = {
+ new SparkRuntimeException(
+ errorClass = "INVALID_PARAMETER_VALUE.NORMALIZE_FORM",
+ messageParameters = Map(
+ "parameter" -> toSQLId("form"),
+ "functionName" -> toSQLId("normalize"),
+ "form" -> toSQLValue(form, StringType)))
+ }
+
def hiveTableWithAnsiIntervalsError(
table: TableIdentifier): SparkUnsupportedOperationException = {
new SparkUnsupportedOperationException(
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtilsSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtilsSuite.scala
index e3a9177bfdbc..608a13d47c59 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtilsSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtilsSuite.scala
@@ -480,4 +480,46 @@ class ExpressionImplUtilsSuite extends SparkFunSuite {
tryValidateUTF8(UTF8String.fromBytes(Array[Byte](0xFF.toByte)), null)
}
+ // These vectors follow Unicode Standard Annex #15's
decomposition/composition algorithm and
+ // pin normalize() to Spark's bundled ICU4J/Unicode data (see icu4j.version
in pom.xml): a
+ // future ICU4J upgrade that changed these mappings would fail this test,
rather than silently
+ // changing query results.
+ test("Normalize with supported forms") {
+ def normalize(input: String, form: String): String =
+ ExpressionImplUtils.normalize(
+ UTF8String.fromString(input), UTF8String.fromString(form)).toString
+
+ // scalastyle:off nonascii
+ assert(normalize("\uFB01", "NFKC") == "fi")
+ assert(normalize("A\u030A", "NFC") == "\u00C5")
+ assert(normalize("\u00C5", "NFD") == "A\u030A")
+ assert(normalize("\uFB01", "nfkc") == "fi")
+ assert(normalize("\u00BD", "NFKD") == "1" + "\u2044" + "2")
+ assert(normalize("\u00BD", "NFD") == "\u00BD")
+ assert(normalize("\uD835\uDC00", "NFKC") == "A")
+ assert(normalize("", "NFC") == "")
+
+ // Combining marks with different combining classes are canonically
reordered before
+ // composition (UAX #15 canonical ordering algorithm), so applying
COMBINING DOT BELOW
+ // (U+0323) and COMBINING ACUTE ACCENT (U+0301) to the same base letter in
either input
+ // order must normalize (NFC) to the same result.
+ val dotBelow = "\u0323"
+ val acute = "\u0301"
+ assert(normalize("a" + dotBelow + acute, "NFC") == normalize("a" + acute +
dotBelow, "NFC"))
+ // scalastyle:on nonascii
+ }
+
+ test("Normalize invalid form error") {
+ checkError(
+ exception = intercept[SparkRuntimeException] {
+ ExpressionImplUtils.normalize(
+ UTF8String.fromString("abc"), UTF8String.fromString("NFE"))
+ },
+ condition = "INVALID_PARAMETER_VALUE.NORMALIZE_FORM",
+ parameters = Map(
+ "parameter" -> "`form`",
+ "functionName" -> "`normalize`",
+ "form" -> "'NFE'"))
+ }
+
}
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/StringExpressionsSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/StringExpressionsSuite.scala
index 4ae723f33b39..0cb944192cb8 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/StringExpressionsSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/StringExpressionsSuite.scala
@@ -19,7 +19,7 @@ package org.apache.spark.sql.catalyst.expressions
import java.math.{BigDecimal => JavaBigDecimal}
-import org.apache.spark.{SPARK_DOC_ROOT, SparkFunSuite,
SparkIllegalArgumentException}
+import org.apache.spark.{SPARK_DOC_ROOT, SparkFunSuite,
SparkIllegalArgumentException, SparkRuntimeException}
import org.apache.spark.sql.AnalysisException
import org.apache.spark.sql.catalyst.analysis.TypeCheckResult
import
org.apache.spark.sql.catalyst.analysis.TypeCheckResult.{DataTypeMismatch,
InvalidFormat}
@@ -2338,4 +2338,25 @@ class StringExpressionsSuite extends SparkFunSuite with
ExpressionEvalHelper {
assert(formatNumberCopy ne formatNumber,
"freshCopyIfContainsStatefulExpression should return a new instance for
FormatNumber")
}
+
+ test("Normalize") {
+ // scalastyle:off nonascii
+ checkEvaluation(new Normalize(Literal("A\u030A")), "\u00C5")
+ checkEvaluation(Normalize(Literal("A\u030A"), Literal("NFC")), "\u00C5")
+ checkEvaluation(Normalize(Literal("\u00C5"), Literal("NFD")), "A\u030A")
+ checkEvaluation(Normalize(Literal("\uFB01"), Literal("NFKC")), "fi")
+ // scalastyle:on nonascii
+ checkEvaluation(Normalize(Literal.create(null, StringType),
Literal("NFC")), null)
+ checkEvaluation(Normalize(Literal("abc"), Literal.create(null,
StringType)), null)
+ }
+
+ test("Normalize invalid form") {
+ checkErrorInExpression[SparkRuntimeException](
+ Normalize(Literal("abc"), Literal("NFE")),
+ "INVALID_PARAMETER_VALUE.NORMALIZE_FORM",
+ Map(
+ "parameter" -> "`form`",
+ "functionName" -> "`normalize`",
+ "form" -> "'NFE'"))
+ }
}
diff --git a/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
b/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
index 402070bc0d08..7c389ed360ab 100644
--- a/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
+++ b/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
@@ -261,6 +261,7 @@
| org.apache.spark.sql.catalyst.expressions.NaNvl | nanvl | SELECT
nanvl(cast('NaN' as double), 123) | struct<nanvl(CAST(NaN AS DOUBLE),
123):double> |
| org.apache.spark.sql.catalyst.expressions.NanosToTimestamp | timestamp_nanos
| SELECT timestamp_nanos(1230219000123456789) |
struct<timestamp_nanos(1230219000123456789):timestamp_ltz(9)> |
| org.apache.spark.sql.catalyst.expressions.NextDay | next_day | SELECT
next_day('2015-01-14', 'TU') | struct<next_day(2015-01-14, TU):date> |
+| org.apache.spark.sql.catalyst.expressions.Normalize | normalize | SELECT
normalize('fi', 'NFKC') | struct<normalize(fi, NFKC):string> |
| org.apache.spark.sql.catalyst.expressions.Not | ! | SELECT ! true |
struct<(NOT true):boolean> |
| org.apache.spark.sql.catalyst.expressions.Not | not | SELECT not true |
struct<(NOT true):boolean> |
| org.apache.spark.sql.catalyst.expressions.NowExpressionBuilder | now |
SELECT now() | struct<now():timestamp> |
diff --git
a/sql/core/src/test/resources/sql-tests/analyzer-results/nonansi/string-functions.sql.out
b/sql/core/src/test/resources/sql-tests/analyzer-results/nonansi/string-functions.sql.out
index 15e6f3ada266..64b21c7114a0 100644
---
a/sql/core/src/test/resources/sql-tests/analyzer-results/nonansi/string-functions.sql.out
+++
b/sql/core/src/test/resources/sql-tests/analyzer-results/nonansi/string-functions.sql.out
@@ -2167,3 +2167,38 @@ select instr(null, null, cast(null as int), cast(null as
int))
-- !query analysis
Project [instr(cast(null as string), cast(null as string), cast(null as int),
cast(null as int)) AS instr(NULL, NULL, CAST(NULL AS INT), CAST(NULL AS INT))#x]
+- OneRowRelation
+
+
+-- !query
+select normalize('hello')
+-- !query analysis
+Project [normalize(hello, NFC) AS normalize(hello, NFC)#x]
++- OneRowRelation
+
+
+-- !query
+select normalize('hello', 'NFD')
+-- !query analysis
+Project [normalize(hello, NFD) AS normalize(hello, NFD)#x]
++- OneRowRelation
+
+
+-- !query
+select normalize('fi', 'NFKC')
+-- !query analysis
+Project [normalize(fi, NFKC) AS normalize(fi, NFKC)#x]
++- OneRowRelation
+
+
+-- !query
+select normalize(null, 'NFC')
+-- !query analysis
+Project [normalize(cast(null as string), NFC) AS normalize(NULL, NFC)#x]
++- OneRowRelation
+
+
+-- !query
+select normalize('hello', 'not_a_form')
+-- !query analysis
+Project [normalize(hello, not_a_form) AS normalize(hello, not_a_form)#x]
++- OneRowRelation
diff --git
a/sql/core/src/test/resources/sql-tests/analyzer-results/string-functions.sql.out
b/sql/core/src/test/resources/sql-tests/analyzer-results/string-functions.sql.out
index 15e6f3ada266..64b21c7114a0 100644
---
a/sql/core/src/test/resources/sql-tests/analyzer-results/string-functions.sql.out
+++
b/sql/core/src/test/resources/sql-tests/analyzer-results/string-functions.sql.out
@@ -2167,3 +2167,38 @@ select instr(null, null, cast(null as int), cast(null as
int))
-- !query analysis
Project [instr(cast(null as string), cast(null as string), cast(null as int),
cast(null as int)) AS instr(NULL, NULL, CAST(NULL AS INT), CAST(NULL AS INT))#x]
+- OneRowRelation
+
+
+-- !query
+select normalize('hello')
+-- !query analysis
+Project [normalize(hello, NFC) AS normalize(hello, NFC)#x]
++- OneRowRelation
+
+
+-- !query
+select normalize('hello', 'NFD')
+-- !query analysis
+Project [normalize(hello, NFD) AS normalize(hello, NFD)#x]
++- OneRowRelation
+
+
+-- !query
+select normalize('fi', 'NFKC')
+-- !query analysis
+Project [normalize(fi, NFKC) AS normalize(fi, NFKC)#x]
++- OneRowRelation
+
+
+-- !query
+select normalize(null, 'NFC')
+-- !query analysis
+Project [normalize(cast(null as string), NFC) AS normalize(NULL, NFC)#x]
++- OneRowRelation
+
+
+-- !query
+select normalize('hello', 'not_a_form')
+-- !query analysis
+Project [normalize(hello, not_a_form) AS normalize(hello, not_a_form)#x]
++- OneRowRelation
diff --git a/sql/core/src/test/resources/sql-tests/inputs/string-functions.sql
b/sql/core/src/test/resources/sql-tests/inputs/string-functions.sql
index c432093e1ae8..4e4cb6a401f5 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/string-functions.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/string-functions.sql
@@ -370,4 +370,11 @@ select instr('a', 'b', 1, cast(null as int));
select instr(null, 'b', 1);
select instr('a', null, 1);
select instr('a', 'b', cast(null as int), 2);
-select instr(null, null, cast(null as int), cast(null as int));
\ No newline at end of file
+select instr(null, null, cast(null as int), cast(null as int));
+
+-- normalize
+select normalize('hello');
+select normalize('hello', 'NFD');
+select normalize('fi', 'NFKC');
+select normalize(null, 'NFC');
+select normalize('hello', 'not_a_form');
\ No newline at end of file
diff --git
a/sql/core/src/test/resources/sql-tests/results/nonansi/string-functions.sql.out
b/sql/core/src/test/resources/sql-tests/results/nonansi/string-functions.sql.out
index 1a8521a37cfc..c6655b01d0e6 100644
---
a/sql/core/src/test/resources/sql-tests/results/nonansi/string-functions.sql.out
+++
b/sql/core/src/test/resources/sql-tests/results/nonansi/string-functions.sql.out
@@ -2701,3 +2701,52 @@ select instr(null, null, cast(null as int), cast(null as
int))
struct<instr(NULL, NULL, CAST(NULL AS INT), CAST(NULL AS INT)):int>
-- !query output
NULL
+
+
+-- !query
+select normalize('hello')
+-- !query schema
+struct<normalize(hello, NFC):string>
+-- !query output
+hello
+
+
+-- !query
+select normalize('hello', 'NFD')
+-- !query schema
+struct<normalize(hello, NFD):string>
+-- !query output
+hello
+
+
+-- !query
+select normalize('fi', 'NFKC')
+-- !query schema
+struct<normalize(fi, NFKC):string>
+-- !query output
+fi
+
+
+-- !query
+select normalize(null, 'NFC')
+-- !query schema
+struct<normalize(NULL, NFC):string>
+-- !query output
+NULL
+
+
+-- !query
+select normalize('hello', 'not_a_form')
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.SparkRuntimeException
+{
+ "errorClass" : "INVALID_PARAMETER_VALUE.NORMALIZE_FORM",
+ "sqlState" : "22023",
+ "messageParameters" : {
+ "form" : "'not_a_form'",
+ "functionName" : "`normalize`",
+ "parameter" : "`form`"
+ }
+}
diff --git
a/sql/core/src/test/resources/sql-tests/results/string-functions.sql.out
b/sql/core/src/test/resources/sql-tests/results/string-functions.sql.out
index b0497277c59b..e6be16af6784 100644
--- a/sql/core/src/test/resources/sql-tests/results/string-functions.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/string-functions.sql.out
@@ -2769,3 +2769,52 @@ select instr(null, null, cast(null as int), cast(null as
int))
struct<instr(NULL, NULL, CAST(NULL AS INT), CAST(NULL AS INT)):int>
-- !query output
NULL
+
+
+-- !query
+select normalize('hello')
+-- !query schema
+struct<normalize(hello, NFC):string>
+-- !query output
+hello
+
+
+-- !query
+select normalize('hello', 'NFD')
+-- !query schema
+struct<normalize(hello, NFD):string>
+-- !query output
+hello
+
+
+-- !query
+select normalize('fi', 'NFKC')
+-- !query schema
+struct<normalize(fi, NFKC):string>
+-- !query output
+fi
+
+
+-- !query
+select normalize(null, 'NFC')
+-- !query schema
+struct<normalize(NULL, NFC):string>
+-- !query output
+NULL
+
+
+-- !query
+select normalize('hello', 'not_a_form')
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.SparkRuntimeException
+{
+ "errorClass" : "INVALID_PARAMETER_VALUE.NORMALIZE_FORM",
+ "sqlState" : "22023",
+ "messageParameters" : {
+ "form" : "'not_a_form'",
+ "functionName" : "`normalize`",
+ "parameter" : "`form`"
+ }
+}
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/StringFunctionsSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/StringFunctionsSuite.scala
index 3e0c8425c54e..00b6c926b92b 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/StringFunctionsSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/StringFunctionsSuite.scala
@@ -419,6 +419,16 @@ class StringFunctionsSuite extends SharedSparkSession {
// scalastyle:on
}
+ test("string normalize") {
+ // scalastyle:off
+ val df = Seq("\uFB01").toDF("s")
+ checkAnswer(df.select(normalize($"s")), Row("\uFB01"))
+ checkAnswer(df.select(normalize($"s", lit("NFKC"))), Row("fi"))
+ checkAnswer(df.selectExpr("normalize(s, 'NFKC')"), Row("fi"))
+ checkAnswer(df.select(normalize(lit(null), lit("NFC"))), Row(null))
+ // scalastyle:on
+ }
+
test("string translate") {
val df = Seq(("translate", "")).toDF("a", "b")
checkAnswer(df.select(translate($"a", "rnlt", "123")), Row("1a2s3ae"))
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]