-
Notifications
You must be signed in to change notification settings - Fork 820
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Backport OffsetDateTime changes from 4.0 #3090
base: 3.8-dev
Are you sure you want to change the base?
Conversation
…ateTime is now the default Date type in Gremlin; added OffsetDateTime serializers to Go/JS/.NET. Made OffsetDateTime the default serializer for date types in GLVs (Date type will only deserialize).
final Date dt = DatetimeHelper.parse(removeFirstAndLastCharacters(dtString)); | ||
sb.append("datetime.datetime.utcfromtimestamp(" + dt.getTime() + " / 1000.0)"); | ||
final OffsetDateTime dt = DatetimeHelper.parse(removeFirstAndLastCharacters(dtString)); | ||
sb.append("datetime.datetime.fromtimestamp(" + dt.toEpochSecond() + ").astimezone(datetime.timezone.utc)"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we unnecessarily losing precision by using dt.toEpochSecond()
? Python's datetime.datetime supports microsecond precision: https://docs.python.org/3/library/datetime.html#datetime.datetime. Perhaps fromisoformat is preferable.
Same comment applies for the old PythonTranslator.
OffsetDateTime date = (OffsetDateTime) object; | ||
OffsetDateTime new_date; | ||
|
||
switch (dateToken) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: dateToken
and value
should arguably be converted to a Duration
in the constructor for this step so this conversion does not need to happen each time the step is iterated.
final Date dt = DatetimeHelper.parse(removeFirstAndLastCharacters(dtString)); | ||
sb.append("time.UnixMilli(" + dt.getTime() + ")"); | ||
final OffsetDateTime dt = DatetimeHelper.parse(removeFirstAndLastCharacters(dtString)); | ||
sb.append("time.UnixMilli(" + dt.toInstant().toEpochMilli() + ")"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may be able to preserve more precision as well as the timezone offset via https://pkg.go.dev/time#Date
@@ -138,6 +139,11 @@ protected String getSyntax(final Date o) { | |||
return "time.UnixMilli(" + o.getTime() + ")"; | |||
} | |||
|
|||
@Override | |||
protected String getSyntax(final OffsetDateTime o) { | |||
return "time.UnixMilli(" + o.toInstant().toEpochMilli() + ")"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may be able to preserve more precision as well as the timezone offset via https://pkg.go.dev/time#Date
...ain/java/org/apache/tinkerpop/gremlin/process/traversal/translator/JavascriptTranslator.java
Outdated
Show resolved
Hide resolved
@@ -93,45 +97,41 @@ public static String format(final Instant d) { | |||
* </ul>> | |||
* | |||
*/ | |||
public static Date parse(final String d) { | |||
public static OffsetDateTime parse(final String d) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a lot going on in this method. Based on the javadoc comment, could it simply be replaced with OffsetDateTime.parse(String) or OffsetDateTime.from(TemporalAccessor)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not quite, dates without time or time without dates cannot be transformed into OffsetDateTime
, so we'll still need the manual parsing. But you have a point that I can combine the last two cases.
final Date dt = (Date) new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDateLiteral(ctx); | ||
assertTrue(new Date().getTime() - dt.getTime() < 1000); | ||
final OffsetDateTime dt = (OffsetDateTime) new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDateLiteral(ctx); | ||
assertTrue(OffsetDateTime.now(UTC).toEpochSecond() - dt.toEpochSecond() < 1000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The < 1000 is too broad here as the units have changed from millis to seconds.
assertTrue(OffsetDateTime.now(UTC).toEpochSecond() - dt.toEpochSecond() < 1000); | |
assertTrue(OffsetDateTime.now(UTC).toInstant().toEpochMilli() - dt.toInstant().toEpochMilli() < 1000); |
offset := readIntSafe(data, i) | ||
// only way to pass offset info, timezone display is fixed to UTC as consequence (offset is calculated properly) | ||
loc := time.FixedZone("UTC", int(offset)) | ||
datetime := time.Date(int(year), time.Month(month), int(day), 0, 0, 0, int(ns), loc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
int(ns) may be an issue for times which are late in the day. According to the io docs, the "time" value can be from 0 to 86,399,999,999,999. The max size of int
is only guaranteed to be 2,147,483,647, although on most 64-bit machines it will be a 64 bit integer in practice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately the Date constructor requires int
, it's more like a caveat of go, not sure if it's avoidable.
sb.append("new Date("); | ||
sb.append(dt.getTime()); | ||
sb.append(dt.toInstant().toEpochMilli()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should work, and it seems equivalent to what was there before. As long as you've tested this with various time zones it should be fine.
However, I'd wager that if someone is using the translator to translate a query, I doubt they started with a date value as the millisecond since unix epoch. More likely they started with a string version of the date time.
Perhaps a better approach would be to use the ISO string value as the input to new Date()
. The precision should be the same (milliseconds), and it supports time zone offsets or UTC. It's just a more human friendly translation.
cal.add(DTtoCalendar.get(dateToken), value); | ||
|
||
return cal.getTime(); | ||
if (!(object instanceof OffsetDateTime)) throw new IllegalArgumentException("dateAdd accept only DateTime."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the error be:
dateAdd accept only OffsetDateTime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We changed it a general DateTime type in 4.0, but you are right it prob make more sense to be OffsetDateTime here. I will also make it accept Date as well for compatibility reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it intentional for this step to only have second precision?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original design of the step uses the dateToken
enums, which only goes to second precision. I suppose we could add support for additional precisions, but that would be outside the scope of this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I too find it strange that date add and date diff do not work with millisecond precision.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that the available precision here is odd, although this is really out of scope for this PR and should be taken as a separate discussion. I think there should be come consideration of taking a Duration as input instead of an int and DT enum, although some consideration is needed around how to make Duration's constructable in gremlin-lang.
|
||
return (((Date) object).getTime() - otherDateMs) / 1000; | ||
return (((OffsetDateTime) object).toEpochSecond() - otherDateMs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you mean to use .toEpochMilli()
?
If you did intend to use seconds instead of milliseconds, then you should change the variable name otherDateMs
to be otherDateSeconds
or something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No this step meant to return epoch time in seconds, so I'm just directly using it.
final Date dt = (Date) new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDateLiteral(ctx); | ||
assertTrue(new Date().getTime() - dt.getTime() < 1000); | ||
final OffsetDateTime dt = (OffsetDateTime) new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDateLiteral(ctx); | ||
assertTrue(OffsetDateTime.now(UTC).toEpochSecond() - dt.toEpochSecond() < 1000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like we are dropping from millisecond to second precision here.
@@ -310,6 +316,9 @@ protected Script convertToScript(final Object object) { | |||
} else if (object instanceof Date) { | |||
final Object objectOrWrapper = withParameters ? object : getSyntax((Date) object); | |||
return script.getBoundKeyOrAssign(withParameters, objectOrWrapper); | |||
} else if (object instanceof OffsetDateTime) { | |||
final Object objectOrWrapper = withParameters ? object : getSyntax((OffsetDateTime) object); | |||
return script.getBoundKeyOrAssign(withParameters, objectOrWrapper); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that this exists, should the else if (object instanceof Date)
above be removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it should stay because both Date
and OffsetDateTime
types are still supported, just now the default is OffsetDateTime
.
@@ -23,6 +23,7 @@ | |||
|
|||
import java.math.BigDecimal; | |||
import java.math.BigInteger; | |||
import java.time.OffsetDateTime; | |||
import java.util.Date; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This import is now unused
@@ -27,6 +27,7 @@ | |||
import org.apache.tinkerpop.gremlin.structure.VertexProperty; | |||
import org.apache.tinkerpop.gremlin.util.DatetimeHelper; | |||
|
|||
import java.time.OffsetDateTime; | |||
import java.util.Date; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This import is now unused
@@ -27,6 +27,7 @@ | |||
import org.apache.tinkerpop.gremlin.structure.VertexProperty; | |||
import org.apache.tinkerpop.gremlin.util.DatetimeHelper; | |||
|
|||
import java.time.OffsetDateTime; | |||
import java.util.Arrays; | |||
import java.util.Collections; | |||
import java.util.Date; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This import is now unused
@@ -25,6 +25,7 @@ | |||
import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertex; | |||
import org.apache.tinkerpop.gremlin.util.DatetimeHelper; | |||
|
|||
import java.time.OffsetDateTime; | |||
import java.util.Date; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This import is now unused
@@ -204,6 +204,7 @@ | |||
import org.apache.tinkerpop.gremlin.structure.VertexProperty; | |||
import org.apache.tinkerpop.gremlin.util.function.ConstantSupplier; | |||
|
|||
import java.time.OffsetDateTime; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Date import is now unused
@@ -37,6 +37,7 @@ | |||
import org.apache.tinkerpop.gremlin.structure.Vertex; | |||
import org.apache.tinkerpop.gremlin.structure.VertexProperty; | |||
|
|||
import java.time.OffsetDateTime; | |||
import java.util.Collection; | |||
import java.util.Date; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused
return (Date) object; | ||
throw new IllegalArgumentException("Can't parse null as OffsetDateTime."); | ||
if (object instanceof OffsetDateTime) | ||
return (OffsetDateTime) object; | ||
if (object instanceof Byte || object instanceof Short || object instanceof Integer || object instanceof Long) | ||
// numbers handled as milliseconds since January 1, 1970, 00:00:00 GMT. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment should be updated to UTC
instead of GMT
which are slightly different.
final Object object = traverser.get(); | ||
if (object == null) | ||
throw new IllegalArgumentException("Can't parse null as Date."); | ||
if (object instanceof Date) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically since Date
types are supported isn't it possible to use the asDate
step on a traversal that produces Date
? In that case should the Date
be converted to OffsetDateTime
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is part of the change I'll revert along with other changes to make it compatible with Date as type.
@@ -25,6 +25,7 @@ | |||
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil; | |||
import org.apache.tinkerpop.gremlin.structure.util.StringFactory; | |||
|
|||
import java.time.OffsetDateTime; | |||
import java.util.Collections; | |||
import java.util.Date; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused
|
||
return (((Date) object).getTime() - otherDateMs) / 1000; | ||
return (((OffsetDateTime) object).toEpochSecond() - otherDateMs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion:
return otherDate == null ? 0L : Duration.between(otherDate, (OffsetDateTime) object).getSeconds();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the simplification, not quite the right logic but I can use it, i.e. if otherDate
is null we consider it as 0L and return the epoch seconds of date
, instead of returning 0L
.
@@ -22,7 +22,11 @@ | |||
import java.time.LocalDate; | |||
import java.time.LocalDateTime; | |||
import java.time.LocalTime; | |||
import java.time.Month; | |||
import java.time.OffsetDateTime; | |||
import java.time.Year; | |||
import java.time.YearMonth; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused imports
@@ -78,6 +78,7 @@ public class DataType : IEquatable<DataType> | |||
// TODO: Support metrics and traversal metrics | |||
public static readonly DataType Char = new DataType(0x80); | |||
public static readonly DataType Duration = new DataType(0x81); | |||
public static readonly DataType OffsetDateTime = new DataType(0x88); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wondering why OffsetDateTime
was not previously supported as a type for .NET? This PR does not add the type to TInkerPop, only changes the default date type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not just .NET, it actually wasn't supported in any of the GLVs because it's an extended serialization type.
var s = value.Second; | ||
var ms = value.Millisecond; | ||
// Note there will be precision loss as microsecond and nanosecond access was added after .net 7 | ||
var ns = h * 60 * 60 * 1e9 + m * 60 * 1e9 + s * 1e9 + ms * 1e6; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q says obtaining nanoseconds is possible using Ticks
:
// Convert the time portion of DateTimeOffset to nanoseconds since midnight
static long TimeToNanosecondsSinceMidnight(DateTimeOffset dto)
{
// Get the time of day as TimeSpan
TimeSpan timeOfDay = dto.TimeOfDay;
// Convert ticks to nanoseconds (1 tick = 100 nanoseconds in .NET)
return timeOfDay.Ticks * 100;
}
…e Date as parameter to dateDiff, update date precision in GLV translators.
OffsetDateTime is now the default Date type in Gremlin.
Changes were back-ported from Java/Python. For Go/JS/.NET, the OffsetDateTime serializers were added.
Made OffsetDateTime the default serializer for dates in GLVs, the Date type will only deserialize responses.
VOTE +1.