{"record":{"id":"b0b6e19cf068eb9e","repo":"apache/beam","slug":"start-time-cannot-be-after-end-time","errorCode":null,"errorMessage":"Start time cannot be after end time.","messagePattern":"Start time cannot be after end time\\.","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java","lineNumber":2258,"sourceCode":"          getChangeStreamName() != null,\n          \"SpannerIO.readChangeStream() requires the name of the change stream to be set.\");\n      checkArgument(\n          getInclusiveStartAt() != null,\n          \"SpannerIO.readChangeStream() requires the start time to be set.\");\n      // Inclusive end at is defaulted to ChangeStreamsContants.MAX_INCLUSIVE_END_AT\n      checkArgument(\n          getInclusiveEndAt() != null,\n          \"SpannerIO.readChangeStream() requires the end time to be set. If you'd like to process the stream without an end time, you can omit this parameter.\");\n      if (getMetadataInstance() != null) {\n        checkArgument(\n            getMetadataDatabase() != null,\n            \"SpannerIO.readChangeStream() requires the metadata database to be set if metadata instance is set.\");\n      }\n\n      // Start time must be before end time\n      if (getInclusiveEndAt() != null\n          && getInclusiveStartAt().toSqlTimestamp().after(getInclusiveEndAt().toSqlTimestamp())) {\n        throw new IllegalArgumentException(\"Start time cannot be after end time.\");\n      }\n\n      final DatabaseId changeStreamDatabaseId =\n          DatabaseId.of(\n              getSpannerConfig().getProjectId().get(),\n              getSpannerConfig().getInstanceId().get(),\n              getSpannerConfig().getDatabaseId().get());\n      final String partitionMetadataInstanceId =\n          MoreObjects.firstNonNull(\n              getMetadataInstance(), changeStreamDatabaseId.getInstanceId().getInstance());\n      final String partitionMetadataDatabaseId =\n          MoreObjects.firstNonNull(getMetadataDatabase(), changeStreamDatabaseId.getDatabase());\n\n      final SpannerConfig changeStreamSpannerConfig = buildChangeStreamSpannerConfig();\n      final SpannerConfig partitionMetadataSpannerConfig =\n          MetadataSpannerConfigFactory.create(\n              changeStreamSpannerConfig, partitionMetadataInstanceId, partitionMetadataDatabaseId);\n      final Dialect changeStreamDatabaseDialect =","sourceCodeStart":2240,"sourceCodeEnd":2276,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java#L2240-L2276","documentation":"SpannerIO.readChangeStream() validates that the inclusive start time is not after the inclusive end time, comparing their java.sql.Timestamp values. If inclusiveEndAt is set and start is later than end, it throws IllegalArgumentException because Spanner change streams require a monotonically ordered query window.","triggerScenarios":"Building ReadChangeStream with .changeStreamStartAt(...) later than .changeStreamEndAt(...) (or .changeStreamInclusiveEndAt(...)), e.g. computed timestamps where start is derived from a later checkpoint than end.","commonSituations":"Clock/timezone conversions producing inverted ranges; passing end from an older run while start is 'now'; mixing Instant and Timestamp conversions incorrectly.","solutions":["Swap or correct start/end so start <= end before building the transform","Validate ordering in your code before calling changeStreamStartAt/changeStreamEndAt","Normalize timezones/units so both values are converted via the same toSqlTimestamp path"],"exampleFix":"// before\n.changeStreamStartAt(laterTime).changeStreamEndAt(earlierTime)\n// after\nif (start.after(end)) { /* fix or swap */ }\n.changeStreamStartAt(earlierTime).changeStreamEndAt(laterTime)","handlingStrategy":"validation","validationCode":"if (end != null && start.toSqlTimestamp().after(end.toSqlTimestamp())) {\n  throw new IllegalArgumentException(\"Start time cannot be after end time.\");\n}","typeGuard":null,"tryCatchPattern":"try { pipeline.apply(readChangeStream); } catch (IllegalArgumentException e) { LOG.error(\"Invalid change stream window: {}\", e.getMessage()); }","preventionTips":["Compute start/end in one UTC-based code path","Validate ordering at config load time","Unit-test timestamp conversions across timezones"],"tags":["java","spanner","change-stream","date-range"],"backgroundTag":"argument-out-of-range","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-14T21:17:11.552Z"}