Skip to content

Commit f5feab8

Browse files
ahmedabu98Ahmed Abualsaud
andauthored
Enhance Python Timestamp to be precision-variable up to nanos, and map it to Timestamp logical type (#39537)
* integrate logical timestamp type with python * trigger xlang ITs * Changes * Changes.md --------- Co-authored-by: Ahmed Abualsaud <ahmedabualsaud@MacBook-Pro-2.local>
1 parent 141804a commit f5feab8

14 files changed

Lines changed: 753 additions & 73 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"comment": "Modify this file in a trivial way to cause this test suite to run",
3-
"modification": 17
3+
"modification": 1
44
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"comment": "Modify this file in a trivial way to cause this test suite to run",
3-
"modification": 7
3+
"modification": 1
44
}

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
* (Java) Supported acknowledge mode for JmsIO ([#39253](https://github.com/apache/beam/issues/39253)).
7474
* (Python) Staged files directory is now automatically added to `sys.path` on the Python SDK worker at startup. This makes Python files provided via the '--files_to_stage' pipeline option importable in the pipeline code and makes it easier to initialize Python SDK harness at startup via the `--beam_plugins` pipeline option. For more information, see the [Staging Individual Files](https://beam.apache.org/documentation/sdks/python-pipeline-dependencies/#staging-files) section of the dependency management docs. This behavior can be disabled by passing the '--experiments=no_staged_dir_in_sys_path' pipeline option ([#39431](https://github.com/apache/beam/issues/39431)).
7575
* (Python) Added `equal_to_approx`, an `assert_that` matcher that compares numeric pipeline outputs with a configurable tolerance ([#18028](https://github.com/apache/beam/issues/18028)).
76+
* (Python) `Timestamp` now supports variable subsecond precision, up to nanoseconds. The portable
77+
`beam:logical_type:timestamp:v1` logical type now maps to Python's `Timestamp` ([#39344](https://github.com/apache/beam/issues/39344)).
78+
* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)).
7679

7780
## Breaking Changes
7881

model/fn-execution/src/main/resources/org/apache/beam/model/fnexecution/v1/standard_coders.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,3 +626,34 @@ examples:
626626
"\u0001\u0003\u0061\u0062\u0063" : "abc"
627627
"\u0001\u000a\u006d\u006f\u0072\u0065\u0020\u0062\u0079\u0074\u0065\u0073" : "more bytes"
628628
"\u0000" : null
629+
630+
---
631+
632+
coder:
633+
urn: "beam:coder:row:v1"
634+
# f_timestamp: logical(timestamp:v1, precision=3, representation row<seconds:int64, subseconds:int16>)
635+
payload: "\x0a\x88\x01\x0a\x0bf_timestamp\x1ay:w\x0a\x1ebeam:logical_type:timestamp:v1\x1aK2I\x0aG\x0a\x0d\x0a\x07seconds\x1a\x02\x10\x04\x0a\x10\x0a\x0asubseconds\x1a\x02\x10\x02\x12$5f2fbf6c-8b8e-4a30-a9be-e0f65f0f79c3\"\x02\x10\x03*\x04\x0a\x02\x18\x03\x12$9c7c1b6c-2a5e-4c53-9b32-91b71c1f8ac3"
636+
examples:
637+
"\x01\x00\x02\x00\xb6\x95\xd5\xf9\x05\x03\xe7": {f_timestamp: {seconds: 1597328054, subseconds: 999}}
638+
"\x01\x00\x02\x00\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01\x01\xf4": {f_timestamp: {seconds: -2, subseconds: 500}}
639+
"\x01\x00\x02\x00\xb6\x95\xd5\xf9\x05\x00\x00": {f_timestamp: {seconds: 1597328054, subseconds: 0}}
640+
641+
---
642+
643+
coder:
644+
urn: "beam:coder:row:v1"
645+
# f_timestamp: logical(timestamp:v1, precision=6, representation row<seconds:int64, subseconds:int32>)
646+
payload: "\x0a\x88\x01\x0a\x0bf_timestamp\x1ay:w\x0a\x1ebeam:logical_type:timestamp:v1\x1aK2I\x0aG\x0a\x0d\x0a\x07seconds\x1a\x02\x10\x04\x0a\x10\x0a\x0asubseconds\x1a\x02\x10\x03\x12$b8b3faf8-63f8-4b40-9f4d-3c1e83d4e07b\"\x02\x10\x03*\x04\x0a\x02\x18\x06\x12$e0a72e10-b214-4fd2-a4dc-fca0e7f7d924"
647+
examples:
648+
"\x01\x00\x02\x00\xb6\x95\xd5\xf9\x05\xc0\xc4\x07": {f_timestamp: {seconds: 1597328054, subseconds: 123456}}
649+
"\x01\x00\x02\x00\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01\xa0\xc2\x1e": {f_timestamp: {seconds: -2, subseconds: 500000}}
650+
651+
---
652+
653+
coder:
654+
urn: "beam:coder:row:v1"
655+
# f_timestamp: logical(timestamp:v1, precision=9, representation row<seconds:int64, subseconds:int32>)
656+
payload: "\x0a\x88\x01\x0a\x0bf_timestamp\x1ay:w\x0a\x1ebeam:logical_type:timestamp:v1\x1aK2I\x0aG\x0a\x0d\x0a\x07seconds\x1a\x02\x10\x04\x0a\x10\x0a\x0asubseconds\x1a\x02\x10\x03\x12$3d6f7b81-2f13-4a41-8be3-4d8cd8f30f42\"\x02\x10\x03*\x04\x0a\x02\x18\x09\x12$cf03a3c1-4a3a-4f77-8e2b-6e5b19c19f27"
657+
examples:
658+
"\x01\x00\x02\x00\xb6\x95\xd5\xf9\x05\x95\x9a\xef:": {f_timestamp: {seconds: 1597328054, subseconds: 123456789}}
659+
"\x01\x00\x02\x00\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01\xff\x93\xeb\xdc\x03": {f_timestamp: {seconds: -2, subseconds: 999999999}}

model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/schema.proto

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,19 @@ message LogicalTypes {
234234
// since the epoch.
235235
DATE = 8 [(org.apache.beam.model.pipeline.v1.beam_urn) =
236236
"beam:logical_type:date:v1"];
237+
238+
// A URN for Timestamp type
239+
// - Representation type: ROW<seconds: INT64, subseconds: INT16> for
240+
// precision < 5, otherwise ROW<seconds: INT64, subseconds: INT32>
241+
// - Argument type: INT32.
242+
// A timestamp without a timezone, represented as seconds since the
243+
// epoch plus a non-negative fraction of a second. The argument is
244+
// the precision: the number of decimal digits of the subseconds
245+
// field (e.g. 3 for milliseconds, 6 for micros, 9 for nanos).
246+
// subseconds stays non-negative even for timestamps before the epoch,
247+
// e.g. -1.5s at precision 6 is {seconds: -2, subseconds: 500000}.
248+
TIMESTAMP = 9 [(org.apache.beam.model.pipeline.v1.beam_urn) =
249+
"beam:logical_type:timestamp:v1"];
237250
}
238251
}
239252

sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaTranslation.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.apache.beam.sdk.schemas.logicaltypes.MicrosInstant;
5151
import org.apache.beam.sdk.schemas.logicaltypes.PythonCallable;
5252
import org.apache.beam.sdk.schemas.logicaltypes.SchemaLogicalType;
53+
import org.apache.beam.sdk.schemas.logicaltypes.Timestamp;
5354
import org.apache.beam.sdk.schemas.logicaltypes.UnknownLogicalType;
5455
import org.apache.beam.sdk.schemas.logicaltypes.VariableBytes;
5556
import org.apache.beam.sdk.schemas.logicaltypes.VariableString;
@@ -115,6 +116,7 @@ private static String getLogicalTypeUrn(String identifier) {
115116
.put(FixedString.IDENTIFIER, FixedString.class)
116117
.put(VariableString.IDENTIFIER, VariableString.class)
117118
.put(Date.IDENTIFIER, Date.class)
119+
.put(Timestamp.IDENTIFIER, Timestamp.class)
118120
.build();
119121

120122
public static SchemaApi.Schema schemaToProto(Schema schema, boolean serializeLogicalType) {

sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/Timestamp.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState;
2323

2424
import java.time.Instant;
25+
import org.apache.beam.model.pipeline.v1.RunnerApi;
26+
import org.apache.beam.model.pipeline.v1.SchemaApi;
2527
import org.apache.beam.sdk.schemas.Schema;
2628
import org.apache.beam.sdk.values.Row;
2729
import org.checkerframework.checker.nullness.qual.NonNull;
@@ -56,7 +58,12 @@
5658
* }</pre>
5759
*/
5860
public class Timestamp implements Schema.LogicalType<Instant, Row> {
59-
public static final String IDENTIFIER = "beam:logical_type:timestamp:v1";
61+
public static final String IDENTIFIER =
62+
SchemaApi.LogicalTypes.Enum.TIMESTAMP
63+
.getValueDescriptor()
64+
.getOptions()
65+
.getExtension(RunnerApi.beamUrn);
66+
6067
static final int MIN_PRECISION = 0;
6168
static final int MAX_PRECISION = 9;
6269

sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/SchemaTranslationTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.apache.beam.sdk.schemas.logicaltypes.PythonCallable;
5252
import org.apache.beam.sdk.schemas.logicaltypes.SchemaLogicalType;
5353
import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes;
54+
import org.apache.beam.sdk.schemas.logicaltypes.Timestamp;
5455
import org.apache.beam.sdk.schemas.logicaltypes.UnknownLogicalType;
5556
import org.apache.beam.sdk.schemas.logicaltypes.VariableBytes;
5657
import org.apache.beam.sdk.schemas.logicaltypes.VariableString;
@@ -460,6 +461,12 @@ public static Iterable<Schema.FieldType> data() {
460461
.add(FieldType.logicalType(FixedString.of(10)))
461462
.add(FieldType.logicalType(VariableString.of(10)))
462463
.add(FieldType.logicalType(FixedPrecisionNumeric.of(10)))
464+
.add(FieldType.logicalType(Timestamp.MILLIS))
465+
.add(FieldType.logicalType(Timestamp.MICROS))
466+
.add(FieldType.logicalType(Timestamp.NANOS))
467+
// INT16/INT32 subseconds representation boundary.
468+
.add(FieldType.logicalType(Timestamp.of(4)))
469+
.add(FieldType.logicalType(Timestamp.of(5)))
463470
.add(FieldType.logicalType(new PortableNullArgLogicalType()))
464471
.add(FieldType.logicalType(new NullArgumentLogicalType()))
465472
.build();

sdks/python/apache_beam/coders/row_coder_test.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,60 @@ def test_create_row_coder_from_schema(self):
203203
for test_case in self.PEOPLE:
204204
self.assertEqual(test_case, coder.decode(coder.encode(test_case)))
205205

206+
def test_create_row_coder_from_schema_with_parameterized_timestamp(self):
207+
# Simulates schemas produced by the Java SDK's parameterized Timestamp
208+
# logical type. Subseconds field uses INT16 for
209+
# precision < 5 and INT32 otherwise.
210+
for precision, subseconds_type in [(3, schema_pb2.INT16),
211+
(4, schema_pb2.INT16),
212+
(5, schema_pb2.INT32),
213+
(6, schema_pb2.INT32),
214+
(9, schema_pb2.INT32)]:
215+
schema = schema_pb2.Schema(
216+
id="timestamp_precision_%d" % precision,
217+
fields=[
218+
schema_pb2.Field(
219+
name="event_time",
220+
type=schema_pb2.FieldType(
221+
logical_type=schema_pb2.LogicalType(
222+
urn="beam:logical_type:timestamp:v1",
223+
representation=schema_pb2.FieldType(
224+
row_type=schema_pb2.RowType(
225+
schema=schema_pb2.Schema(
226+
id="timestamp_repr_%d" % precision,
227+
fields=[
228+
schema_pb2.Field(
229+
name="seconds",
230+
type=schema_pb2.FieldType(
231+
atomic_type=schema_pb2.INT64)
232+
),
233+
schema_pb2.Field(
234+
name="subseconds",
235+
type=schema_pb2.FieldType(
236+
atomic_type=subseconds_type)),
237+
]))),
238+
argument_type=schema_pb2.FieldType(
239+
atomic_type=schema_pb2.INT32),
240+
argument=schema_pb2.FieldValue(
241+
atomic_value=schema_pb2.AtomicTypeValue(
242+
int32=precision))))),
243+
])
244+
coder = RowCoder(schema)
245+
row = named_tuple_from_schema(schema)
246+
247+
subseconds = 10**precision - 1
248+
for value in [
249+
Timestamp(seconds=1500000000,
250+
subseconds=subseconds,
251+
precision=precision),
252+
Timestamp(seconds=-2, subseconds=subseconds, precision=precision),
253+
Timestamp(seconds=1500000000, precision=precision),
254+
]:
255+
test_case = row(event_time=value)
256+
decoded = coder.decode(coder.encode(test_case))
257+
self.assertEqual(test_case, decoded)
258+
self.assertEqual(decoded.event_time.precision(), precision)
259+
206260
def test_row_coder_negative_varint(self):
207261
schema = schema_pb2.Schema(
208262
id="negative",

sdks/python/apache_beam/portability/common_urns.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,4 @@
9393
fixed_char = LogicalTypes.Enum.FIXED_CHAR
9494
var_char = LogicalTypes.Enum.VAR_CHAR
9595
date = LogicalTypes.Enum.DATE
96+
timestamp = LogicalTypes.Enum.TIMESTAMP

0 commit comments

Comments
 (0)