Skip to content

fix: EXPLAIN ANALYZE fails with date/time field value out of range for binary-encoded timestamp args#24

Merged
mickamy merged 4 commits intomainfrom
copilot/fix-explain-analyze-error
Mar 5, 2026
Merged

fix: EXPLAIN ANALYZE fails with date/time field value out of range for binary-encoded timestamp args#24
mickamy merged 4 commits intomainfrom
copilot/fix-explain-analyze-error

Conversation

Copy link
Contributor

Copilot AI commented Mar 3, 2026

EXPLAIN ANALYZE crashes with SQLSTATE 22008 when a query arg was originally sent as a binary-format TIMESTAMP WITH TIME ZONE. The proxy captures binary timestamp parameters as raw int64 strings (µs since 2000-01-01 UTC, e.g. 825505830505628). When passed back to PostgreSQL as a plain Go string in a parameterized query, PG's text parser rejects it — it can't parse an integer as a timestamp literal.

Changes

  • proxy/postgres/conn.go — Fix the root cause at the protocol level:

    • handleParse now captures m.ParameterOIDs into lastParamOIDs (and preparedStmtOIDs for named prepared statements)
    • handleBind looks up the OID for each parameter position and passes it to decodeBinaryParam
    • decodeBinaryParam now checks if the OID is timestamp (1114) or timestamptz (1184): if so, decodes the 8-byte binary value as PostgreSQL microseconds since 2000-01-01 UTC and formats it as an RFC3339Nano string (e.g. "2026-02-27T14:10:30.505628Z") — a value PostgreSQL can parse back correctly in any parameterized query context, with or without an explicit ::TIMESTAMP cast
  • explain/explain.go — Retains the buildAnyArgs() / parseTimestampParams() / parsePGTimestamp() helpers as a fallback for drivers that do not send ParameterOIDs in the Parse message but do include explicit $N::TIMESTAMP... casts in the query text

  • proxy/postgres/conn_test.go and proxy/postgres/export_test.go — Unit tests for decodePGTimestampMicros covering zero, large positive, negative, and fractional-negative microsecond values

Original prompt

This section details on the original issue you should resolve

<issue_title>EXPLAIN ANALYZEfails withdate/time field value out of rangewhen args contain Unix timestamps in milliseconds/microseconds</issue_title> <issue_description>When runningEXPLAIN ANALYZEon a query that has aTIMESTAMP WITH TIME ZONE` parameter passed as a large integer (Unix timestamp in milliseconds or microseconds), the tool throws an error instead of executing the explain plan.

Steps to reproduce:

  1. Execute a query with a TIMESTAMP WITH TIME ZONE parameter bound as a raw integer (e.g. 825505830505628)
  2. Open the query in the history/log view
  3. Press x or X to run EXPLAIN / EXPLAIN ANALYZE

Error:

rpc error: code = Internal desc = explain: explain: query:
ERROR: date/time field value out of range: "825505830505628" (SQLSTATE 22008)

Expected behavior:

EXPLAIN ANALYZE should execute successfully, the same way the original query did (Duration: 1.2ms, Rows: 1).

Root cause (hypothesis):

The original query runs fine because the driver passes the argument as a native typed value. However, when sql-tap reconstructs the query for EXPLAIN ANALYZE, it substitutes the argument as a plain string literal 825505830505628 into a TIMESTAMP WITH TIME ZONE slot — PostgreSQL then tries to parse it as a date string and fails.

Environment:

  • Tool: sql-tap
  • Database: PostgreSQL
  • Argument value: 825505830505628 (likely Unix epoch in microseconds)

Yes, I used AI to generate text for issue reports :)

Image

Image</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Co-authored-by: mickamy <11856337+mickamy@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix EXPLAIN ANALYZE error with Unix timestamps fix: EXPLAIN ANALYZE fails with date/time field value out of range for binary-encoded timestamp args Mar 3, 2026
@mickamy mickamy marked this pull request as ready for review March 3, 2026 01:36
Copilot AI review requested due to automatic review settings March 3, 2026 01:36
@mickamy
Copy link
Owner

mickamy commented Mar 3, 2026

@copilot fix lint errors

Co-authored-by: mickamy <11856337+mickamy@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 3, 2026

@copilot fix lint errors

Fixed in 67deaca. Two lint issues:

  1. testpackage: Changed build_args_test.go from package explain to package explain_test, and added explain/export_test.go to re-export the internal helpers for the external test package.
  2. gofmt: Applied gofmt -w to build_args_test.go to fix the struct field alignment.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a PostgreSQL EXPLAIN ANALYZE failure (SQLSTATE 22008) when replaying captured queries whose timestamp arguments were originally sent in binary format and later re-sent as plain strings.

Changes:

  • Convert captured string args into []any via buildAnyArgs, attempting to re-type certain timestamp-cast parameters as time.Time.
  • Add parsing helpers for detecting timestamp-cast parameters and decoding PostgreSQL’s internal (2000-01-01-based) microsecond timestamp representation.
  • Add unit tests covering timestamp param detection, timestamp decoding, and mixed-argument behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
explain/explain.go Introduces arg re-typing logic for timestamp-cast parameters before running EXPLAIN.
explain/build_args_test.go Adds unit tests for the new arg-building and parsing helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +144 to +149
func buildAnyArgs(query string, args []string) []any {
tsParams := parseTimestampParams(query)
anyArgs := make([]any, len(args))
for i, a := range args {
if tsParams[i+1] {
if t, ok := parsePGTimestamp(a); ok {
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

buildAnyArgs() only converts integer args when the query text contains an explicit $N::TIMESTAMP... cast. However, in the Postgres proxy the captured query used for EXPLAIN is the raw Parse query (q := c.lastParse) and typically does not include explicit casts; parameter types are usually inferred from context (e.g., created_at > $1). In those cases this conversion will never trigger and EXPLAIN can still fail with SQLSTATE 22008 for binary-encoded timestamptz args. Consider using protocol type information instead of query-text regex (e.g., capture Parse.ParameterOIDs and/or per-parameter format codes in the event / ExplainRequest) so timestamp parameters can be detected even when there is no ::TIMESTAMP in the SQL.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

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

@copilot fix this issue

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ad49ce2 by addressing the issue at the protocol level instead of relying on query-text regex.

Changes in proxy/postgres/conn.go:

  • handleParse now captures m.ParameterOIDs into lastParamOIDs (and preparedStmtOIDs for named statements)
  • handleBind looks up OIDs per parameter and passes them to decodeBinaryParam
  • decodeBinaryParam now detects timestamp OIDs (1114 = timestamp, 1184 = timestamptz) and decodes the 8-byte binary value using decodePGTimestampMicros → stores an RFC3339Nano string (e.g. "2026-02-27T14:10:30.505628Z") that PostgreSQL can parse back correctly in both explicit-cast and implicit-cast contexts

The existing buildAnyArgs regex in explain.go is kept as a fallback for drivers that don't send OIDs but have explicit $N::TIMESTAMP casts.

…39 strings

Co-authored-by: mickamy <11856337+mickamy@users.noreply.github.com>
@mickamy mickamy merged commit 62f1ac8 into main Mar 5, 2026
1 check passed
@mickamy mickamy deleted the copilot/fix-explain-analyze-error branch March 5, 2026 08:54
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.

EXPLAIN ANALYZE fails with date/time field value out of range` when args contain Unix timestamps in milliseconds/microseconds

3 participants