Skip to content

[CALCITE-7529] Casts between literals and TIME/TIMESTAMP can lose precision beyond milliseconds#4942

Open
zzwqqq wants to merge 9 commits into
apache:mainfrom
zzwqqq:fix_time_literal
Open

[CALCITE-7529] Casts between literals and TIME/TIMESTAMP can lose precision beyond milliseconds#4942
zzwqqq wants to merge 9 commits into
apache:mainfrom
zzwqqq:fix_time_literal

Conversation

@zzwqqq
Copy link
Copy Markdown
Contributor

@zzwqqq zzwqqq commented May 14, 2026

Jira Link

CALCITE-7529

Changes Proposed

Preserve precision for casts involving high-precision TIME and TIMESTAMP literals.

Generated constant reduction represents TIME and TIMESTAMP values at millisecond precision. When a custom RelDataTypeSystem allows precision greater than 3, literal casts such as CAST('12:34:56.123456' AS TIME(6)) can lose digits beyond milliseconds.

The change keeps directly representable temporal literal casts as Rex literals, and avoids re-reducing existing RexLiterals.

Regression tests cover TIME(6) and TIMESTAMP(6) literal casts, existing temporal literals, and casts to VARCHAR.

@zzwqqq zzwqqq force-pushed the fix_time_literal branch from 575b5fd to 19b41c4 Compare May 15, 2026 02:16
Comment thread core/src/main/java/org/apache/calcite/rex/RexExecutorImpl.java
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@mihaibudiu mihaibudiu left a comment

Choose a reason for hiding this comment

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

The restricted scope of this PR looks more reasonable, but I do have some questions

Comment thread core/src/main/java/org/apache/calcite/rex/RexExecutorImpl.java
final SqlTimeLiteral timeLiteral =
SqlParserUtil.parseTimeLiteral(value, pos);
if (!isSubMillisecondLiteral(timeLiteral, value)) {
if (timeLiteral.getPrec() <= 0
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 understand what 0 means, but negative precisions are less clear.
There's "PRECISION_NOT_SPECIFIED", but I'd rather see an explicit check for that if that's what you are looking for.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Switched this to check PRECISION_NOT_SPECIFIED explicitly.

return timestamp == null || !hasSubMillisecondPrecision(timestamp)
? null
: timestamp.toString(precision(literal.getType()));
if (timestamp == null || literal.getType().getPrecision() <= 3) {
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.

Why is 3 hardwired here?

Copy link
Copy Markdown
Contributor Author

@zzwqqq zzwqqq May 23, 2026

Choose a reason for hiding this comment

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

that 3 was just the millisecond cutoff from the old version. Removed it and now format using the literal type's precision.

I also checked PostgreSQL: concat_ws(',', timestamp '2024-07-06 12:15:48.678') keeps .678(https://onecompiler.com/postgresql/44q5pytxk). After updating the expectation, CalciteSqlOperatorTest.testConcatWSFunc exposed that the VALUES-based path was still dropping it, so I updated the generated TIME/TIMESTAMP-to-character cast to pass through the source precision.

default:
return false;
}
return SqlTypeUtil.comparePrecision(type.getPrecision(), value.length()) >= 0
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.

please add a comment explaining what happens here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a note here about when this can replace the CAST with a literal.

}

private static RelDataTypeFactory highPrecisionTemporalTypeFactory() {
return new JavaTypeFactoryImpl(
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.

Can you run tests with multiple precisions, checking for precisions 1..9 for example in a loop?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Expanded these to run through precisions 1..9. I did the same for the temporal-to-VARCHAR cases

@zzwqqq zzwqqq changed the title [CALCITE-7529] RexExecutor constant reduction loses sub-millisecond precision for TIME/TIMESTAMP literals [CALCITE-7529] Casts between literals and TIME/TIMESTAMP can lose precision beyond milliseconds May 23, 2026
String.class, int.class),
UNIX_DATE_TO_STRING(DateTimeUtils.class, "unixDateToString", int.class),
UNIX_TIME_TO_STRING(DateTimeUtils.class, "unixTimeToString", int.class),
UNIX_TIME_TO_STRING_WITH_PRECISION(DateTimeUtils.class, "unixTimeToString",
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.

Both these methods assume that the actual data value is expressed as an integer number of milliseconds, so they won't work for precisions higher than 3. Perhaps this is not worsening the problem, but it looks to me like it will need to be fixed at some point again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right. I ran into this while debugging CalciteSqlOperatorTest.testConcatWSFunc. This is still limited by the runtime representation; a lot of the enumerable path uses milliseconds / Calendar internally, so this only preserves up to 3 fractional digits today.

: null;
}

private static boolean isExactFractionalSecondLiteral(
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.

can you document what this function is supposed to do?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a short JavaDoc. This checks that the parsed literal has explicit fractional-second precision and still formats back to the original string.

return temporalLiteral;
}

private @Nullable RexNode makeCastFromTemporalLiteralToCharacter(
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 needs JavaDoc, particularly because it returns null on many paths.
You would not expect that result from the function name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added JavaDoc here, including the null-return cases where the caller keeps the regular CAST.

default:
return null;
}
// Replace the CAST with a character literal only if the formatted temporal
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 don't see any cast in this function, so this comment is misleading.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I reworded this to describe what the helper does directly, without referring to the surrounding CAST.

/** Converts a TIME or TIMESTAMP literal to a character literal.
*
* <p>Returns null if the literal type is not supported, the literal value is
* unavailable, or the formatted temporal value does not fit in the target
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.

Please do not document what the caller does, it's not the job of the callee. Other callers may be added in the future.

What is "value is unavailable"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I removed the caller behavior from the JavaDoc and made the null-return cases more specific.

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