This is an automated email from the ASF dual-hosted git repository.
cloud-fan pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new be794c001580 [SPARK-53154][SQL][PYTHON] Add `hmac` SQL function
be794c001580 is described below
commit be794c0015803b59d213cea4a0750051d13d2d06
Author: Vinod KC <[email protected]>
AuthorDate: Wed Jul 22 19:58:57 2026 +0800
[SPARK-53154][SQL][PYTHON] Add `hmac` SQL function
### What changes were proposed in this pull request?
Add a built-in `hmac(key, message[, algorithm])` scalar function that
computes a keyed-hash message authentication code (HMAC) using the JVM's
`javax.crypto.Mac`.
**API surface added:**
- SQL: `hmac(key, message)` / `hmac(key, message, algorithm)`
- Scala DataFrame: `functions.hmac(key, message)` / `functions.hmac(key,
message, algorithm)`
- PySpark (classic + Connect): `pyspark.sql.functions.hmac(key, message,
algorithm=None)`
**Key design choices:**
- Returns `BinaryType` (raw MAC bytes), not a hex string — so the output of
one
`hmac` call feeds directly into the `key` of the next with no `unhex` in
between.
This is the critical property for chained key-derivation (e.g. AWS SigV4).
- Argument order is `(key, message)`, matching Python's `hmac.new(key,
msg)` and RFC 2104's `HMAC(K, m)`.
- Default algorithm is SHA-256. Supported: SHA-224, SHA-256, SHA-384,
SHA-512, SHA-1, MD5 (case-insensitive; both `SHA-256` and `SHA256` spellings
accepted).
### Why are the changes needed?
Spark had no single expression node for HMAC. Users had to hand-build the
RFC 2104
formula from `sha2`, `concat`, bitwise `xor`, and `unhex` primitives. The
motivating
real-world use case derives an AWS SigV4 signing key by chaining **four**
HMACs, each step's output becoming
the next step's key:
```sql
-- Four-step SigV4 key derivation (AWS docs, "Signature Version 4").
-- Each hmac() output is BinaryType, feeding directly as the next key -- no
unhex anywhere.
SELECT hex(
hmac(
hmac(
hmac(
hmac(concat('AWS4', secret), date),
region),
service),
'aws4_request')
) AS signing_key;
```
Without this function, each HMAC step is a deep sha2/unhex/concat/xor
subtree.
Nesting four of them multiplies the tree size to the point where the
Catalyst analyzer/optimizer runs out of memory on realistic inputs. hmac
collapses each level to a single StaticInvoke node.
### Does this PR introduce _any_ user-facing change?
Yes. Adds a new built-in SQL function `hmac` and corresponding Scala/PySpark
DataFrame API entries. No existing behavior is changed.
Example:
```
-- Default algorithm (SHA-256)
SELECT hex(hmac('key', 'message'));
-- 6E9EF29B75FFFC5B7ABAE527D58FDADB2FE42E7219011976917343065F58ED4A
```
```
-- Explicit algorithm
SELECT hex(hmac('key', 'message', 'SHA-512'));
--
E477384D7CA229DD1426E64B63EBF2D36EBD6D7E669A6735424E72EA6C01D3F8B56EB39C36D8232F5427999B8D1A3F9CD1128FC69F4D75B434216810FA367E98
```
```
-- Chaining: binary output feeds directly as the next key
SELECT hex(hmac(hmac('key', 'message'), 'message2'));
-- 28042756E0362D954B61661BB4DEC9FF9F6C970D346CAEB6DB36ED7B735AB38C
```
### How was this patch tested?
Added tests in `ExpressionImplUtilsSuite`, `MiscExpressionsSuite` ,
`misc-functions.sql`
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude (sonnet-5)
Closes #57344 from vinodkc/br_SPARK-53154.
Authored-by: Vinod KC <[email protected]>
Signed-off-by: Wenchen Fan <[email protected]>
(cherry picked from commit 2bb0b352b10584be06c3f6c5aeffe5714716a539)
Signed-off-by: Wenchen Fan <[email protected]>
---
.../src/main/resources/error/error-conditions.json | 10 +++
.../source/reference/pyspark.sql/functions.rst | 1 +
python/pyspark/sql/connect/functions/builtin.py | 14 ++++
python/pyspark/sql/functions/__init__.py | 1 +
python/pyspark/sql/functions/builtin.py | 59 ++++++++++++++++
.../scala/org/apache/spark/sql/functions.scala | 34 +++++++++
.../catalyst/expressions/ExpressionImplUtils.java | 37 ++++++++++
.../sql/catalyst/analysis/FunctionRegistry.scala | 1 +
.../spark/sql/catalyst/expressions/misc.scala | 53 ++++++++++++++
.../spark/sql/errors/QueryExecutionErrors.scala | 18 +++++
.../expressions/ExpressionImplUtilsSuite.scala | 59 ++++++++++++++++
.../expressions/MiscExpressionsSuite.scala | 36 +++++++++-
.../sql-functions/sql-expression-schema.md | 1 +
.../analyzer-results/misc-functions.sql.out | 63 +++++++++++++++++
.../resources/sql-tests/inputs/misc-functions.sql | 14 ++++
.../sql-tests/results/misc-functions.sql.out | 81 ++++++++++++++++++++++
16 files changed, 481 insertions(+), 1 deletion(-)
diff --git a/common/utils/src/main/resources/error/error-conditions.json
b/common/utils/src/main/resources/error/error-conditions.json
index 6eeff1445d07..9f6fb5ee99b2 100644
--- a/common/utils/src/main/resources/error/error-conditions.json
+++ b/common/utils/src/main/resources/error/error-conditions.json
@@ -4594,6 +4594,16 @@
"Invalid extension: <invalidValue>. Extension is limited to exactly
3 letters (e.g. csv, tsv, etc...)"
]
},
+ "HMAC_ALGORITHM" : {
+ "message" : [
+ "expects one of the algorithms 'SHA-224', 'SHA-256', 'SHA-384',
'SHA-512', 'SHA-1', 'MD5', but got <algorithm>."
+ ]
+ },
+ "HMAC_CRYPTO_ERROR" : {
+ "message" : [
+ "detail message: <detailMessage>"
+ ]
+ },
"INTEGER" : {
"message" : [
"expects an integer literal, but got <invalidValue>."
diff --git a/python/docs/source/reference/pyspark.sql/functions.rst
b/python/docs/source/reference/pyspark.sql/functions.rst
index 2e6049f37870..7ce413fde0bb 100644
--- a/python/docs/source/reference/pyspark.sql/functions.rst
+++ b/python/docs/source/reference/pyspark.sql/functions.rst
@@ -660,6 +660,7 @@ Misc Functions
current_path
current_schema
current_user
+ hmac
input_file_block_length
input_file_block_start
input_file_name
diff --git a/python/pyspark/sql/connect/functions/builtin.py
b/python/pyspark/sql/connect/functions/builtin.py
index be5e5a4f0c6d..acf1ee6b069f 100644
--- a/python/pyspark/sql/connect/functions/builtin.py
+++ b/python/pyspark/sql/connect/functions/builtin.py
@@ -5383,6 +5383,20 @@ def zeroifnull(col: "ColumnOrName") -> Column:
zeroifnull.__doc__ = pysparkfuncs.zeroifnull.__doc__
+def hmac(
+ key: "ColumnOrName",
+ message: "ColumnOrName",
+ algorithm: Optional["ColumnOrName"] = None,
+) -> Column:
+ if algorithm is None:
+ return _invoke_function_over_columns("hmac", key, message)
+ else:
+ return _invoke_function_over_columns("hmac", key, message, algorithm)
+
+
+hmac.__doc__ = pysparkfuncs.hmac.__doc__
+
+
def aes_encrypt(
input: "ColumnOrName",
key: "ColumnOrName",
diff --git a/python/pyspark/sql/functions/__init__.py
b/python/pyspark/sql/functions/__init__.py
index f1a0e1de5b75..7780bae38ee9 100644
--- a/python/pyspark/sql/functions/__init__.py
+++ b/python/pyspark/sql/functions/__init__.py
@@ -520,6 +520,7 @@ __all__ = [ # noqa: F405
"current_path",
"current_schema",
"current_user",
+ "hmac",
"input_file_block_length",
"input_file_block_start",
"input_file_name",
diff --git a/python/pyspark/sql/functions/builtin.py
b/python/pyspark/sql/functions/builtin.py
index 9e092ef1c4fb..77ec5353e91c 100644
--- a/python/pyspark/sql/functions/builtin.py
+++ b/python/pyspark/sql/functions/builtin.py
@@ -31491,6 +31491,65 @@ def zeroifnull(col: "ColumnOrName") -> Column:
return _invoke_function_over_columns("zeroifnull", col)
+@_try_remote_functions
+def hmac(
+ key: "ColumnOrName",
+ message: "ColumnOrName",
+ algorithm: Optional["ColumnOrName"] = None,
+) -> Column:
+ """
+ Returns the keyed-hash message authentication code (HMAC) of `message`
using `key` and the
+ given hash `algorithm`. The result is returned as raw MAC bytes; wrap it
with :func:`hex` or
+ :func:`base64` for a textual value. The default algorithm is 'SHA-256'.
+
+ .. versionadded:: 4.3.0
+
+ Parameters
+ ----------
+ key : :class:`~pyspark.sql.Column` or column name
+ The secret key, as a binary value.
+ message : :class:`~pyspark.sql.Column` or column name
+ The message to authenticate, as a binary value.
+ algorithm : :class:`~pyspark.sql.Column` or column name, optional
+ The hash algorithm. Valid values: SHA-224, SHA-256, SHA-384, SHA-512,
SHA-1, MD5.
+ The default is SHA-256.
+
+ Returns
+ -------
+ :class:`~pyspark.sql.Column`
+ A new column that contains the raw HMAC bytes.
+
+ Examples
+ --------
+
+ Example 1: Compute the HMAC with the default SHA-256 algorithm.
+
+ >>> import pyspark.sql.functions as sf
+ >>> df = spark.createDataFrame([("key", "message")], ["key", "message"])
+ >>> df.select(sf.hex(sf.hmac(df.key, df.message))).show(truncate=False)
+ +----------------------------------------------------------------+
+ |hex(hmac(key, message, SHA-256)) |
+ +----------------------------------------------------------------+
+ |6E9EF29B75FFFC5B7ABAE527D58FDADB2FE42E7219011976917343065F58ED4A|
+ +----------------------------------------------------------------+
+
+ Example 2: Compute the HMAC with an explicit algorithm.
+
+ >>> import pyspark.sql.functions as sf
+ >>> df = spark.createDataFrame([("key", "message")], ["key", "message"])
+ >>> df.select(sf.hex(sf.hmac(df.key, df.message,
sf.lit("SHA-1")))).show(truncate=False)
+ +----------------------------------------+
+ |hex(hmac(key, message, SHA-1)) |
+ +----------------------------------------+
+ |2088DF74D5F2146B48146CAF4965377E9D0BE3A4|
+ +----------------------------------------+
+ """
+ if algorithm is None:
+ return _invoke_function_over_columns("hmac", key, message)
+ else:
+ return _invoke_function_over_columns("hmac", key, message, algorithm)
+
+
@_try_remote_functions
def aes_encrypt(
input: "ColumnOrName",
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 6d824a675223..938ac3d38153 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
@@ -6633,6 +6633,40 @@ object functions {
*/
def uuid(seed: Column): Column = Column.fn("uuid", seed)
+ /**
+ * Returns the keyed-hash message authentication code (HMAC) of `message`
using `key` and the
+ * given hash `algorithm`. The result is returned as raw MAC bytes; wrap it
with `hex` or
+ * `base64` for a textual value.
+ *
+ * @param key
+ * The secret key, as a binary value.
+ * @param message
+ * The message to authenticate, as a binary value.
+ * @param algorithm
+ * The hash algorithm. Valid values: SHA-224, SHA-256, SHA-384, SHA-512,
SHA-1, MD5.
+ *
+ * @group misc_funcs
+ * @since 4.3.0
+ */
+ def hmac(key: Column, message: Column, algorithm: Column): Column =
+ Column.fn("hmac", key, message, algorithm)
+
+ /**
+ * Returns the keyed-hash message authentication code (HMAC) of `message`
using `key` and
+ * SHA-256. The result is returned as raw MAC bytes; wrap it with `hex` or
`base64` for a
+ * textual value. To use a different algorithm, call the three-argument
overload.
+ *
+ * @param key
+ * The secret key, as a binary value.
+ * @param message
+ * The message to authenticate, as a binary value.
+ *
+ * @group misc_funcs
+ * @since 4.3.0
+ */
+ def hmac(key: Column, message: Column): Column =
+ Column.fn("hmac", key, message)
+
/**
* Returns an encrypted value of `input` using AES in given `mode` with the
specified `padding`.
* Key lengths of 16, 24 and 32 bits are supported. Supported combinations
of (`mode`,
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 e744ae3e2fc2..3f2c7e1fc5d4 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
@@ -30,6 +30,7 @@ import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.zip.CRC32;
import javax.crypto.Cipher;
+import javax.crypto.Mac;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
@@ -181,6 +182,42 @@ public class ExpressionImplUtils {
);
}
+ /**
+ * Computes a keyed-hash message authentication code (HMAC) of the given
message using the
+ * given key and hash algorithm.
+ * @param key The secret key.
+ * @param message The message to authenticate.
+ * @param algorithm The hash algorithm. Supported values (case-insensitive):
+ * SHA-224, SHA-256, SHA-384, SHA-512, SHA-1, MD5.
+ * @return The raw HMAC bytes.
+ */
+ public static byte[] hmac(byte[] key, byte[] message, UTF8String algorithm) {
+ String macName = hmacName(algorithm.toString());
+ try {
+ Mac mac = Mac.getInstance(macName);
+ mac.init(new SecretKeySpec(key, macName));
+ return mac.doFinal(message);
+ } catch (GeneralSecurityException | IllegalArgumentException e) {
+ throw QueryExecutionErrors.hmacCryptoError(e.getMessage());
+ }
+ }
+
+ /**
+ * Maps a user-facing hash algorithm name to its JCA {@link Mac} algorithm
name. Supported
+ * algorithms are SHA-224, SHA-256, SHA-384, SHA-512, SHA-1 and MD5
(case-insensitive).
+ */
+ private static String hmacName(String algorithm) {
+ return switch (algorithm.toUpperCase(Locale.ROOT)) {
+ case "SHA-224", "SHA224" -> "HmacSHA224";
+ case "SHA-256", "SHA256" -> "HmacSHA256";
+ case "SHA-384", "SHA384" -> "HmacSHA384";
+ case "SHA-512", "SHA512" -> "HmacSHA512";
+ case "SHA-1", "SHA1" -> "HmacSHA1";
+ case "MD5" -> "HmacMD5";
+ default -> throw
QueryExecutionErrors.hmacUnsupportedAlgorithmError(algorithm);
+ };
+ }
+
/**
* Function to return the Spark version.
* @return
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 b34a5b7ad1b0..5b39aa06f1e4 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
@@ -851,6 +851,7 @@ object FunctionRegistry {
expression[Sha2]("sha2"),
expression[AesEncrypt]("aes_encrypt"),
expression[AesDecrypt]("aes_decrypt"),
+ expression[Hmac]("hmac"),
expression[SparkPartitionID]("spark_partition_id"),
expression[InputFileName]("input_file_name"),
expression[InputFileBlockStart]("input_file_block_start"),
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/misc.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/misc.scala
index 651b1a231279..e80277666109 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/misc.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/misc.scala
@@ -636,3 +636,56 @@ case class TryAesDecrypt(
this.copy(replacement = newChild)
}
// scalastyle:on line.size.limit
+
+/**
+ * A function that computes a keyed-hash message authentication code (HMAC) of
the `message`
+ * using the given `key` and hash `algorithm`. The result is returned as raw
MAC bytes, which
+ * can be fed back in as the `key` of a subsequent HMAC to chain derivations,
or wrapped with
+ * `hex`/`base64` for a textual representation.
+ */
+// scalastyle:off line.size.limit
+@ExpressionDescription(
+ usage = """
+ _FUNC_(key, message[, algorithm]) - Returns the keyed-hash message
authentication code (HMAC) of `message` using `key` and the hash `algorithm`.
The result is returned as raw MAC bytes; wrap it with `hex` or `base64` for a
textual value. The default algorithm is 'SHA-256'.
+ """,
+ arguments = """
+ Arguments:
+ * key - The secret key, as a binary value.
+ * message - The message to authenticate, as a binary value.
+ * algorithm - Optional hash algorithm. Valid values: SHA-224, SHA-256,
SHA-384, SHA-512, SHA-1, MD5. The default is SHA-256.
+ """,
+ examples = """
+ Examples:
+ > SELECT hex(_FUNC_('key', 'message'));
+ 6E9EF29B75FFFC5B7ABAE527D58FDADB2FE42E7219011976917343065F58ED4A
+ > SELECT hex(_FUNC_('key', 'message', 'SHA-512'));
+
E477384D7CA229DD1426E64B63EBF2D36EBD6D7E669A6735424E72EA6C01D3F8B56EB39C36D8232F5427999B8D1A3F9CD1128FC69F4D75B434216810FA367E98
+ """,
+ since = "4.3.0",
+ group = "misc_funcs")
+// scalastyle:on line.size.limit
+case class Hmac(key: Expression, message: Expression, algorithm: Expression)
+ extends RuntimeReplaceable with ImplicitCastInputTypes {
+
+ override lazy val replacement: Expression = StaticInvoke(
+ classOf[ExpressionImplUtils],
+ BinaryType,
+ "hmac",
+ Seq(key, message, algorithm),
+ inputTypes)
+
+ def this(key: Expression, message: Expression) =
+ this(key, message, Literal("SHA-256"))
+
+ override def prettyName: String = "hmac"
+
+ override def inputTypes: Seq[AbstractDataType] =
+ Seq(BinaryType, BinaryType, StringTypeWithCollation(supportsTrimCollation
= true))
+
+ override def children: Seq[Expression] = Seq(key, message, algorithm)
+
+ override protected def withNewChildrenInternal(
+ newChildren: IndexedSeq[Expression]): Expression = {
+ copy(newChildren(0), newChildren(1), newChildren(2))
+ }
+}
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 b54a939eb696..80feddecff30 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
@@ -2514,6 +2514,24 @@ private[sql] object QueryExecutionErrors extends
QueryErrorsBase with ExecutionE
"functionName" -> toSQLId("aes_encrypt")))
}
+ def hmacUnsupportedAlgorithmError(algorithm: String): RuntimeException = {
+ new SparkRuntimeException(
+ errorClass = "INVALID_PARAMETER_VALUE.HMAC_ALGORITHM",
+ messageParameters = Map(
+ "parameter" -> toSQLId("algorithm"),
+ "functionName" -> toSQLId("hmac"),
+ "algorithm" -> toSQLValue(algorithm, StringType)))
+ }
+
+ def hmacCryptoError(detailMessage: String): RuntimeException = {
+ new SparkRuntimeException(
+ errorClass = "INVALID_PARAMETER_VALUE.HMAC_CRYPTO_ERROR",
+ messageParameters = Map(
+ "parameter" -> toSQLId("key"),
+ "functionName" -> toSQLId("hmac"),
+ "detailMessage" -> detailMessage))
+ }
+
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 e197c71026eb..e3a9177bfdbc 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
@@ -403,6 +403,65 @@ class ExpressionImplUtilsSuite extends SparkFunSuite {
// scalastyle:on nonascii
}
+ // RFC 4231 test case 1 (HMAC-SHA-224/256/384/512) and RFC 2202 test case 1
+ // (HMAC-SHA-1 / HMAC-MD5): key is 0x0b repeated, data is "Hi There".
+ private val hmacKey20 = Array.fill[Byte](20)(0x0b)
+ private val hmacKey16 = Array.fill[Byte](16)(0x0b)
+ private val hmacData = "Hi There".getBytes("UTF-8")
+
+ private def hmacHex(key: Array[Byte], message: Array[Byte], algorithm:
String): String = {
+ ExpressionImplUtils.hmac(key, message, UTF8String.fromString(algorithm))
+ .map(b => f"$b%02x").mkString
+ }
+
+ test("Hmac with supported algorithms") {
+ assert(hmacHex(hmacKey20, hmacData, "SHA-224") ==
+ "896fb1128abbdf196832107cd49df33f47b4b1169912ba4f53684b22")
+ assert(hmacHex(hmacKey20, hmacData, "SHA-256") ==
+ "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7")
+ assert(hmacHex(hmacKey20, hmacData, "SHA-384") ==
+ "afd03944d84895626b0825f4ab46907f15f9dadbe4101ec682aa034c7cebc59c" +
+ "faea9ea9076ede7f4af152e8b2fa9cb6")
+ assert(hmacHex(hmacKey20, hmacData, "SHA-512") ==
+ "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cde" +
+ "daa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854")
+ assert(hmacHex(hmacKey20, hmacData, "SHA-1") ==
+ "b617318655057264e28bc0b6fb378c8ef146be00")
+ assert(hmacHex(hmacKey16, hmacData, "MD5") ==
+ "9294727a3638bb1c13f48ef8158bfc9d")
+ }
+
+ test("Hmac algorithm name is case-insensitive and accepts the compact form")
{
+ val expected =
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
+ assert(hmacHex(hmacKey20, hmacData, "sha-256") == expected)
+ assert(hmacHex(hmacKey20, hmacData, "SHA256") == expected)
+ }
+
+ test("Hmac unsupported algorithm error") {
+ checkError(
+ exception = intercept[SparkRuntimeException] {
+ ExpressionImplUtils.hmac(hmacKey20, hmacData,
UTF8String.fromString("SHA-3"))
+ },
+ condition = "INVALID_PARAMETER_VALUE.HMAC_ALGORITHM",
+ parameters = Map(
+ "parameter" -> "`algorithm`",
+ "functionName" -> "`hmac`",
+ "algorithm" -> "'SHA-3'"))
+ }
+
+ test("Hmac empty key error") {
+ checkError(
+ exception = intercept[SparkRuntimeException] {
+ ExpressionImplUtils.hmac(Array.empty[Byte], hmacData,
UTF8String.fromString("SHA-256"))
+ },
+ condition = "INVALID_PARAMETER_VALUE.HMAC_CRYPTO_ERROR",
+ parameters = Map(
+ "parameter" -> "`key`",
+ "functionName" -> "`hmac`",
+ "detailMessage" -> ".*"),
+ matchPVals = true)
+ }
+
test("TryValidate UTF8 string") {
def tryValidateUTF8(str: UTF8String, expected: UTF8String): Unit = {
assert(ExpressionImplUtils.tryValidateUTF8String(str) == expected)
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MiscExpressionsSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MiscExpressionsSuite.scala
index ff0183eb265b..032762470900 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MiscExpressionsSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MiscExpressionsSuite.scala
@@ -21,7 +21,7 @@ import java.io.PrintStream
import scala.util.Random
-import org.apache.spark.SparkFunSuite
+import org.apache.spark.{SparkFunSuite, SparkRuntimeException}
import org.apache.spark.sql.catalyst.analysis.TypeCheckResult.DataTypeMismatch
import org.apache.spark.sql.catalyst.util.TypeUtils.ordinalNumber
import org.apache.spark.sql.types._
@@ -112,4 +112,38 @@ class MiscExpressionsSuite extends SparkFunSuite with
ExpressionEvalHelper {
assert(outputCodegen.contains(s"Result of $inputExpr is 1"))
assert(outputEval.contains(s"Result of $inputExpr is 1"))
}
+
+ test("Hmac") {
+ def bytes(hex: String): Array[Byte] =
+ hex.grouped(2).map(Integer.parseInt(_, 16).toByte).toArray
+
+ val key = Literal("key".getBytes("UTF-8"))
+ val message = Literal("message".getBytes("UTF-8"))
+
+ // Default algorithm is SHA-256.
+ checkEvaluation(
+ new Hmac(key, message),
+
bytes("6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a"))
+
+ // Explicit algorithm.
+ checkEvaluation(
+ Hmac(key, message, Literal("SHA-1")),
+ bytes("2088df74d5f2146b48146caf4965377e9d0be3a4"))
+
+ // Null propagation.
+ checkEvaluation(Hmac(Literal.create(null, BinaryType), message,
Literal("SHA-256")), null)
+ checkEvaluation(Hmac(key, Literal.create(null, BinaryType),
Literal("SHA-256")), null)
+ checkEvaluation(Hmac(key, message, Literal.create(null, StringType)), null)
+ }
+
+ test("Hmac unsupported algorithm") {
+ checkErrorInExpression[SparkRuntimeException](
+ Hmac(Literal("key".getBytes("UTF-8")),
Literal("message".getBytes("UTF-8")),
+ Literal("SHA-3")),
+ "INVALID_PARAMETER_VALUE.HMAC_ALGORITHM",
+ Map(
+ "parameter" -> "`algorithm`",
+ "functionName" -> "`hmac`",
+ "algorithm" -> "'SHA-3'"))
+ }
}
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 4ed20ba4aa93..99285d126acd 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
@@ -166,6 +166,7 @@
| org.apache.spark.sql.catalyst.expressions.Hex | hex | SELECT hex(17) |
struct<hex(17):string> |
| org.apache.spark.sql.catalyst.expressions.HllSketchEstimate |
hll_sketch_estimate | SELECT hll_sketch_estimate(hll_sketch_agg(col)) FROM
VALUES (1), (1), (2), (2), (3) tab(col) |
struct<hll_sketch_estimate(hll_sketch_agg(col, 12)):bigint> |
| org.apache.spark.sql.catalyst.expressions.HllUnion | hll_union | SELECT
hll_sketch_estimate(hll_union(hll_sketch_agg(col1), hll_sketch_agg(col2))) FROM
VALUES (1, 4), (1, 4), (2, 5), (2, 5), (3, 6) tab(col1, col2) |
struct<hll_sketch_estimate(hll_union(hll_sketch_agg(col1, 12),
hll_sketch_agg(col2, 12), false)):bigint> |
+| org.apache.spark.sql.catalyst.expressions.Hmac | hmac | SELECT
hex(hmac('key', 'message')) | struct<hex(hmac(key, message, SHA-256)):string> |
| org.apache.spark.sql.catalyst.expressions.HourExpressionBuilder | hour |
SELECT hour('2018-02-14 12:58:59') | struct<hour(2018-02-14 12:58:59):int> |
| org.apache.spark.sql.catalyst.expressions.Hypot | hypot | SELECT hypot(3, 4)
| struct<HYPOT(3, 4):double> |
| org.apache.spark.sql.catalyst.expressions.ILike | ilike | SELECT
ilike('Spark', '_Park') | struct<ilike(Spark, _Park):boolean> |
diff --git
a/sql/core/src/test/resources/sql-tests/analyzer-results/misc-functions.sql.out
b/sql/core/src/test/resources/sql-tests/analyzer-results/misc-functions.sql.out
index 708ba0ad7b58..a470d33d9893 100644
---
a/sql/core/src/test/resources/sql-tests/analyzer-results/misc-functions.sql.out
+++
b/sql/core/src/test/resources/sql-tests/analyzer-results/misc-functions.sql.out
@@ -233,3 +233,66 @@ SELECT assert_true(col1 <= col2) FROM VALUES
('2025-03-01', '2025-03-10')
-- !query analysis
Project [assert_true((col1#x <= col2#x), '(col1 <= col2)' is not true!) AS
assert_true((col1 <= col2), '(col1 <= col2)' is not true!)#x]
+- LocalRelation [col1#x, col2#x]
+
+
+-- !query
+SELECT hex(hmac('key', 'message'))
+-- !query analysis
+Project [hex(hmac(cast(key as binary), cast(message as binary), SHA-256)) AS
hex(hmac(key, message, SHA-256))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hex(hmac('key', 'message', 'SHA-256'))
+-- !query analysis
+Project [hex(hmac(cast(key as binary), cast(message as binary), SHA-256)) AS
hex(hmac(key, message, SHA-256))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hex(hmac('key', 'message', 'SHA-512'))
+-- !query analysis
+Project [hex(hmac(cast(key as binary), cast(message as binary), SHA-512)) AS
hex(hmac(key, message, SHA-512))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT lower(hex(hmac('key', 'message', 'sha256')))
+-- !query analysis
+Project [lower(hex(hmac(cast(key as binary), cast(message as binary),
sha256))) AS lower(hex(hmac(key, message, sha256)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hex(hmac(hmac('key', 'message'), 'message2'))
+-- !query analysis
+Project [hex(hmac(hmac(cast(key as binary), cast(message as binary), SHA-256),
cast(message2 as binary), SHA-256)) AS hex(hmac(hmac(key, message, SHA-256),
message2, SHA-256))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hmac(CAST(NULL AS BINARY), 'message')
+-- !query analysis
+Project [hmac(cast(null as binary), cast(message as binary), SHA-256) AS
hmac(CAST(NULL AS BINARY), message, SHA-256)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hmac('key', CAST(NULL AS BINARY))
+-- !query analysis
+Project [hmac(cast(key as binary), cast(null as binary), SHA-256) AS hmac(key,
CAST(NULL AS BINARY), SHA-256)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hmac('key', 'message', CAST(NULL AS STRING))
+-- !query analysis
+Project [hmac(cast(key as binary), cast(message as binary), cast(null as
string)) AS hmac(key, message, CAST(NULL AS STRING))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hmac('key', 'message', 'SHA-3')
+-- !query analysis
+Project [hmac(cast(key as binary), cast(message as binary), SHA-3) AS
hmac(key, message, SHA-3)#x]
++- OneRowRelation
diff --git a/sql/core/src/test/resources/sql-tests/inputs/misc-functions.sql
b/sql/core/src/test/resources/sql-tests/inputs/misc-functions.sql
index 417af4220641..e0b7b6ac3f88 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/misc-functions.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/misc-functions.sql
@@ -44,3 +44,17 @@ SET spark.sql.legacy.raiseErrorWithoutErrorClass=false;
-- Implicit alias of assert expression
SELECT assert_true(col1 <= col2) FROM VALUES ('2025-03-01', '2025-03-10');
+
+-- hmac
+SELECT hex(hmac('key', 'message'));
+SELECT hex(hmac('key', 'message', 'SHA-256'));
+SELECT hex(hmac('key', 'message', 'SHA-512'));
+SELECT lower(hex(hmac('key', 'message', 'sha256')));
+-- Chaining: the binary output of one hmac feeds in as the key of the next.
+SELECT hex(hmac(hmac('key', 'message'), 'message2'));
+-- Null propagation.
+SELECT hmac(CAST(NULL AS BINARY), 'message');
+SELECT hmac('key', CAST(NULL AS BINARY));
+SELECT hmac('key', 'message', CAST(NULL AS STRING));
+-- Unsupported algorithm.
+SELECT hmac('key', 'message', 'SHA-3');
diff --git
a/sql/core/src/test/resources/sql-tests/results/misc-functions.sql.out
b/sql/core/src/test/resources/sql-tests/results/misc-functions.sql.out
index efcb0409bf93..9373dc63289a 100644
--- a/sql/core/src/test/resources/sql-tests/results/misc-functions.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/misc-functions.sql.out
@@ -321,3 +321,84 @@ SELECT assert_true(col1 <= col2) FROM VALUES
('2025-03-01', '2025-03-10')
struct<assert_true((col1 <= col2), '(col1 <= col2)' is not true!):void>
-- !query output
NULL
+
+
+-- !query
+SELECT hex(hmac('key', 'message'))
+-- !query schema
+struct<hex(hmac(key, message, SHA-256)):string>
+-- !query output
+6E9EF29B75FFFC5B7ABAE527D58FDADB2FE42E7219011976917343065F58ED4A
+
+
+-- !query
+SELECT hex(hmac('key', 'message', 'SHA-256'))
+-- !query schema
+struct<hex(hmac(key, message, SHA-256)):string>
+-- !query output
+6E9EF29B75FFFC5B7ABAE527D58FDADB2FE42E7219011976917343065F58ED4A
+
+
+-- !query
+SELECT hex(hmac('key', 'message', 'SHA-512'))
+-- !query schema
+struct<hex(hmac(key, message, SHA-512)):string>
+-- !query output
+E477384D7CA229DD1426E64B63EBF2D36EBD6D7E669A6735424E72EA6C01D3F8B56EB39C36D8232F5427999B8D1A3F9CD1128FC69F4D75B434216810FA367E98
+
+
+-- !query
+SELECT lower(hex(hmac('key', 'message', 'sha256')))
+-- !query schema
+struct<lower(hex(hmac(key, message, sha256))):string>
+-- !query output
+6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a
+
+
+-- !query
+SELECT hex(hmac(hmac('key', 'message'), 'message2'))
+-- !query schema
+struct<hex(hmac(hmac(key, message, SHA-256), message2, SHA-256)):string>
+-- !query output
+28042756E0362D954B61661BB4DEC9FF9F6C970D346CAEB6DB36ED7B735AB38C
+
+
+-- !query
+SELECT hmac(CAST(NULL AS BINARY), 'message')
+-- !query schema
+struct<hmac(CAST(NULL AS BINARY), message, SHA-256):binary>
+-- !query output
+NULL
+
+
+-- !query
+SELECT hmac('key', CAST(NULL AS BINARY))
+-- !query schema
+struct<hmac(key, CAST(NULL AS BINARY), SHA-256):binary>
+-- !query output
+NULL
+
+
+-- !query
+SELECT hmac('key', 'message', CAST(NULL AS STRING))
+-- !query schema
+struct<hmac(key, message, CAST(NULL AS STRING)):binary>
+-- !query output
+NULL
+
+
+-- !query
+SELECT hmac('key', 'message', 'SHA-3')
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.SparkRuntimeException
+{
+ "errorClass" : "INVALID_PARAMETER_VALUE.HMAC_ALGORITHM",
+ "sqlState" : "22023",
+ "messageParameters" : {
+ "algorithm" : "'SHA-3'",
+ "functionName" : "`hmac`",
+ "parameter" : "`algorithm`"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]