Skip to content

[SPARK-58295][SQL][PYTHON] Add to_base32 and from_base32 functions - #57466

Closed
SreeramaYeshwanthGowd wants to merge 7 commits into
apache:masterfrom
SreeramaYeshwanthGowd:add-base32-functions
Closed

[SPARK-58295][SQL][PYTHON] Add to_base32 and from_base32 functions#57466
SreeramaYeshwanthGowd wants to merge 7 commits into
apache:masterfrom
SreeramaYeshwanthGowd:add-base32-functions

Conversation

@SreeramaYeshwanthGowd

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Add a matched pair of built in scalar functions:

  • to_base32(binary) encodes bytes as an RFC 4648 Base32 string.
  • from_base32(string) decodes an RFC 4648 Base32 string back to binary.

Spark already ships hex/unhex (base16) and base64/unbase64, so this completes the standard base16/base32/base64 encoding ladder.

API surface added:

  • SQL: to_base32(binary) and from_base32(string)
  • Scala DataFrame: functions.to_base32(col) and functions.from_base32(col)
  • PySpark, classic and Spark Connect: pyspark.sql.functions.to_base32 and from_base32

Key design choices:

  • Implemented as RuntimeReplaceable expressions backed by a StaticInvoke, mirroring the existing base64/unbase64 functions, so there is no hand written codegen.
  • The Base32 codec delegates to org.apache.commons.codec.binary.Base32 (RFC 4648), the same way base64 delegates to java.util.Base64. java.util.Base64 has no Base32 support, and commons-codec is already a dependency of the catalyst module, so this adds no new dependency.
  • The SQL names to_base32 and from_base32 match Trino and BigQuery, and follow Spark's established to_*/from_* conversion family (for example to_json/from_json, to_binary). No major engine uses base32/unbase32.
  • from_base32 decodes leniently, ignoring characters outside the Base32 alphabet, consistent with the existing unbase64 behavior. A strict variant is out of scope, matching how unbase64 defers strict decoding to to_binary.
  • No SparkR binding is added, consistent with all recently added functions (for example to_binary, is_valid_utf8, luhn_check), since SparkR is deprecated as of Spark 4.0 and its function surface is frozen.

Why are the changes needed?

Base32 (RFC 4648) is the standard encoding for TOTP and 2FA shared secrets (RFC 6238), DNS NSEC3 records, and various identifier schemes. Spark has base16 (hex) and base64 but no base32, so users fall back to UDFs. Trino (to_base32/from_base32) and Google BigQuery (TO_BASE32/FROM_BASE32) both ship the pair, so this also improves parity with the engines Spark users migrate from.

Does this PR introduce any user-facing change?

Yes. It adds two new built in SQL functions, to_base32 and from_base32, and the corresponding Scala and PySpark DataFrame API entries. No existing behavior changes.

Example:

spark-sql> SELECT to_base32(encode('foobar', 'utf-8'));
MZXW6YTBOI======
spark-sql> SELECT cast(from_base32('MZXW6YTBOI======') as string);
foobar

How was this patch tested?

Added catalyst unit tests in StringExpressionsSuite covering the RFC 4648 test vectors, round trip encode and decode, empty input, and null propagation, and a DataFrame API test in StringFunctionsSuite. Added PySpark doctests for both functions. Regenerated sql-expression-schema.md.

Was this patch authored or co-authored using generative AI tooling?

No

@SreeramaYeshwanthGowd SreeramaYeshwanthGowd changed the title [SPARK-58295][SQL][PYTHON] Add to_base32 and from_base32 built-in… [SPARK-58295][SQL][PYTHON] Add to_base32 and from_base32 functions Jul 23, 2026
@SreeramaYeshwanthGowd

Copy link
Copy Markdown
Contributor Author

@cloud-fan Would you have a moment to review this when you get a chance? Thank you!

@SreeramaYeshwanthGowd

Copy link
Copy Markdown
Contributor Author

Noting that the Base image build check is failing with a pull access denied error on docker.io/library/root:latest, unrelated to this PR.

@cloud-fan cloud-fan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 blocking, 1 non-blocking, 0 nits.
The implementation and API integration are sound; one small hot-path allocation can be avoided.

Suggestions (1)

  • sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/stringExpressions.scala:3454: Avoid materializing a Java String for every decoded row; Commons Codec accepts the UTF8String bytes directly. -- see inline

Verification

I traced both public APIs through FunctionRegistry to the Catalyst RuntimeReplaceable expressions and compared their encode path with Base64. The added tests exercise codec semantics and each integration layer. I did not run tests locally.

private lazy val codec = new CommonsBase32()

def decode(input: UTF8String): Array[Byte] = {
codec.decode(input.toString)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conversion materializes a Java String for every non-null row. Commons Codec can decode the ASCII bytes directly, avoiding that hot-path allocation.

Suggested change
codec.decode(input.toString)
codec.decode(input.getBytes)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, confirmed this is fixed in the current revision.

@SreeramaYeshwanthGowd

SreeramaYeshwanthGowd commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@cloud-fan Thanks for the review and the suggestion, applied it exactly as proposed. Noting that the Base image build check is failing with a pull access denied error on docker.io/library/root:latest, unrelated to this PR.

@cloud-fan

Copy link
Copy Markdown
Contributor

LGTM if CI is green

@SreeramaYeshwanthGowd

Copy link
Copy Markdown
Contributor Author

@cloud-fan CI is green. Thans for the review.

@cloud-fan cloud-fan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 addressed, 0 remaining, 0 new.
0 blocking, 0 non-blocking, 0 nits.
The current implementation and API integration are sound; the prior review suggestion is addressed.

Verification

I traced both public API directions through FunctionRegistry to the Catalyst replacements and compared the encoding path with Base64. I also checked the current decoder uses UTF8String bytes directly and reviewed the added codec, SQL/DataFrame, and Connect coverage. I did not run tests locally.

@cloud-fan cloud-fan closed this in 1684ada Aug 5, 2026
cloud-fan pushed a commit that referenced this pull request Aug 5, 2026
### What changes were proposed in this pull request?

Add a matched pair of built in scalar functions:
- `to_base32(binary)` encodes bytes as an RFC 4648 Base32 string.
- `from_base32(string)` decodes an RFC 4648 Base32 string back to binary.

Spark already ships `hex`/`unhex` (base16) and `base64`/`unbase64`, so this completes the standard base16/base32/base64 encoding ladder.

API surface added:
- SQL: `to_base32(binary)` and `from_base32(string)`
- Scala DataFrame: `functions.to_base32(col)` and `functions.from_base32(col)`
- PySpark, classic and Spark Connect: `pyspark.sql.functions.to_base32` and `from_base32`

Key design choices:
- Implemented as `RuntimeReplaceable` expressions backed by a `StaticInvoke`, mirroring the existing `base64`/`unbase64` functions, so there is no hand written codegen.
- The Base32 codec delegates to `org.apache.commons.codec.binary.Base32` (RFC 4648), the same way `base64` delegates to `java.util.Base64`. `java.util.Base64` has no Base32 support, and commons-codec is already a dependency of the catalyst module, so this adds no new dependency.
- The SQL names `to_base32` and `from_base32` match Trino and BigQuery, and follow Spark's established `to_*`/`from_*` conversion family (for example `to_json`/`from_json`, `to_binary`). No major engine uses `base32`/`unbase32`.
- `from_base32` decodes leniently, ignoring characters outside the Base32 alphabet, consistent with the existing `unbase64` behavior. A strict variant is out of scope, matching how `unbase64` defers strict decoding to `to_binary`.
- No SparkR binding is added, consistent with all recently added functions (for example `to_binary`, `is_valid_utf8`, `luhn_check`), since SparkR is deprecated as of Spark 4.0 and its function surface is frozen.

### Why are the changes needed?

Base32 (RFC 4648) is the standard encoding for TOTP and 2FA shared secrets (RFC 6238), DNS NSEC3 records, and various identifier schemes. Spark has base16 (`hex`) and base64 but no base32, so users fall back to UDFs. Trino (`to_base32`/`from_base32`) and Google BigQuery (`TO_BASE32`/`FROM_BASE32`) both ship the pair, so this also improves parity with the engines Spark users migrate from.

### Does this PR introduce _any_ user-facing change?

Yes. It adds two new built in SQL functions, `to_base32` and `from_base32`, and the corresponding Scala and PySpark DataFrame API entries. No existing behavior changes.

Example:

```
spark-sql> SELECT to_base32(encode('foobar', 'utf-8'));
MZXW6YTBOI======
spark-sql> SELECT cast(from_base32('MZXW6YTBOI======') as string);
foobar
```

### How was this patch tested?

Added catalyst unit tests in `StringExpressionsSuite` covering the RFC 4648 test vectors, round trip encode and decode, empty input, and null propagation, and a DataFrame API test in `StringFunctionsSuite`. Added PySpark doctests for both functions. Regenerated `sql-expression-schema.md`.

### Was this patch authored or co-authored using generative AI tooling?

 No

Closes #57466 from SreeramaYeshwanthGowd/add-base32-functions.

Authored-by: SreeramaYeshwanthGowd <yeshwanthgowdsreerama@gmail.com>
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
(cherry picked from commit 1684ada)
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
cloud-fan pushed a commit that referenced this pull request Aug 5, 2026
### What changes were proposed in this pull request?

Add a matched pair of built in scalar functions:
- `to_base32(binary)` encodes bytes as an RFC 4648 Base32 string.
- `from_base32(string)` decodes an RFC 4648 Base32 string back to binary.

Spark already ships `hex`/`unhex` (base16) and `base64`/`unbase64`, so this completes the standard base16/base32/base64 encoding ladder.

API surface added:
- SQL: `to_base32(binary)` and `from_base32(string)`
- Scala DataFrame: `functions.to_base32(col)` and `functions.from_base32(col)`
- PySpark, classic and Spark Connect: `pyspark.sql.functions.to_base32` and `from_base32`

Key design choices:
- Implemented as `RuntimeReplaceable` expressions backed by a `StaticInvoke`, mirroring the existing `base64`/`unbase64` functions, so there is no hand written codegen.
- The Base32 codec delegates to `org.apache.commons.codec.binary.Base32` (RFC 4648), the same way `base64` delegates to `java.util.Base64`. `java.util.Base64` has no Base32 support, and commons-codec is already a dependency of the catalyst module, so this adds no new dependency.
- The SQL names `to_base32` and `from_base32` match Trino and BigQuery, and follow Spark's established `to_*`/`from_*` conversion family (for example `to_json`/`from_json`, `to_binary`). No major engine uses `base32`/`unbase32`.
- `from_base32` decodes leniently, ignoring characters outside the Base32 alphabet, consistent with the existing `unbase64` behavior. A strict variant is out of scope, matching how `unbase64` defers strict decoding to `to_binary`.
- No SparkR binding is added, consistent with all recently added functions (for example `to_binary`, `is_valid_utf8`, `luhn_check`), since SparkR is deprecated as of Spark 4.0 and its function surface is frozen.

### Why are the changes needed?

Base32 (RFC 4648) is the standard encoding for TOTP and 2FA shared secrets (RFC 6238), DNS NSEC3 records, and various identifier schemes. Spark has base16 (`hex`) and base64 but no base32, so users fall back to UDFs. Trino (`to_base32`/`from_base32`) and Google BigQuery (`TO_BASE32`/`FROM_BASE32`) both ship the pair, so this also improves parity with the engines Spark users migrate from.

### Does this PR introduce _any_ user-facing change?

Yes. It adds two new built in SQL functions, `to_base32` and `from_base32`, and the corresponding Scala and PySpark DataFrame API entries. No existing behavior changes.

Example:

```
spark-sql> SELECT to_base32(encode('foobar', 'utf-8'));
MZXW6YTBOI======
spark-sql> SELECT cast(from_base32('MZXW6YTBOI======') as string);
foobar
```

### How was this patch tested?

Added catalyst unit tests in `StringExpressionsSuite` covering the RFC 4648 test vectors, round trip encode and decode, empty input, and null propagation, and a DataFrame API test in `StringFunctionsSuite`. Added PySpark doctests for both functions. Regenerated `sql-expression-schema.md`.

### Was this patch authored or co-authored using generative AI tooling?

 No

Closes #57466 from SreeramaYeshwanthGowd/add-base32-functions.

Authored-by: SreeramaYeshwanthGowd <yeshwanthgowdsreerama@gmail.com>
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
(cherry picked from commit 1684ada)
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
@cloud-fan

Copy link
Copy Markdown
Contributor

Merge Summary:

Posted by merge_spark_pr.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants