Skip to content

fix(spark): date_part SECOND returns Decimal(8,6) - #26905

Closed
ump45nose wants to merge 1 commit into
apache:mainfrom
ump45nose:fix/26336-spark-date-part-second-decimal
Closed

ump45nose wants to merge 1 commit into
apache:mainfrom
ump45nose:fix/26336-spark-date-part-second-decimal

Conversation

@ump45nose

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

In Spark, date_part('SECOND', ts) and EXTRACT(SECOND FROM ts) return DECIMAL(8, 6) that keeps the microsecond fraction (for example 45.123456). DataFusion's Spark date_part always returned Int32 and rewrote seconds to the core integer date_part, so the fraction was dropped. The standalone second(ts) function correctly stays INT and is unchanged.

What changes are included in this PR?

  • Plan-time rewrite of Spark date_part / datepart / EXTRACT for the SECOND aliases (SECOND, S, SEC, SECONDS, SECS) onto a dedicated seconds UDF that returns Decimal128(8, 6) as second + fraction.
  • The rewrite happens in SparkFunctionPlanner so the return type is fixed before analysis (a user-written 'SECOND'::string cast hides the literal from return_field_from_args).
  • Sqllogictest expectations updated for whole and fractional seconds, arrow_typeof, DESCRIBE, and EXTRACT(SECOND …).

What is the testing strategy for this PR?

  • cargo test -p datafusion-sqllogictest --test sqllogictests -- spark/datetime/date_part
  • cargo test -p datafusion-sqllogictest --test sqllogictests -- spark/datetime/second (standalone second still Int32)
  • cargo test -p datafusion-spark --lib

Are there any user-facing changes?

Yes for the Spark dialect: date_part('SECOND', …) / EXTRACT(SECOND FROM …) now return DECIMAL(8, 6) with fractional seconds, matching Spark. Standalone second(ts) is unchanged.

AI assistance

Used an AI coding assistant for the implementation and tests; I reviewed the change and wrote this description myself.

Spark's date_part('SECOND', ts) / EXTRACT(SECOND FROM ts) return
DECIMAL(8, 6) with the microsecond fraction. Rewrite those calls to a
dedicated seconds UDF at plan time so the return type is fixed before
analysis, and keep standalone second(ts) as Int32.
@github-actions github-actions Bot added sqllogictest SQL Logic Tests (.slt) spark Changes to datafusion-spark crate labels Oct 29, 2026

@harsh-ande harsh-ande 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.

Thanks @ump45nose, returning Spark's DECIMAL(8, 6) with the fractional seconds is a good fix. Added couple of minor comments.

impl ScalarUDFImpl for SparkDatePartSeconds {
fn name(&self) -> &str {
// Keep the display name as date_part so EXPLAIN/DESCRIBE stay familiar.
"date_part"

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.

I found one regression in this PR: the new seconds UDF is also named date_part but it takes only the timestamp argument. As a result the default column name is now date_part(t.ts) instead of date_part(Utf8("SECOND"),t.ts). If we have 2 seconds aliases in the same SELECT, they now collide. Example:

SELECT date_part('SECOND', ts), date_part('S', ts)
FROM (VALUES (timestamp '2020-01-01 00:00:01.5')) AS t(ts);

This runs on main but fails with the following error in this branch - Projections require unique expression names but the expression "date_part(t.ts)" at position 0 and "date_part(t.ts)" at position 1 have the same name.

Could the rewritten call keep the part literal as its first argument, and ignored at runtime, so the expression name stays the same as before? An slt case with this query would guard against it further regressing.

&& is_seconds_part(&part)
{
let date_expr = args.into_iter().nth(1).ok_or_else(|| {
datafusion_common::DataFusionError::Internal(

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.

Instead of building DataFusionError::Internal(..) directly, you can use internal_datafusion_err!(..).

&& is_seconds_part(&part)
{
let date_expr = expr.args.into_iter().nth(1).ok_or_else(|| {
datafusion_common::DataFusionError::Internal(

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.

Same here

@2010YOUY01

Copy link
Copy Markdown
Member

Closing due to

It's impossible to contribute responsibly in such PR volume, please don't do automated contribution in this project.


@harsh-ande I think it's possible that this PR is already working as is, the issue is that it's harmful to encourage fully automated contributions. If you could verify this PR's direction valid, let's open a replacement PR, would appreciate it 🙏🏼

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

Labels

spark Changes to datafusion-spark crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] date_part('SECOND', ts) drops the fractional seconds, Spark returns decimal(8,6)

3 participants