Skip to content

fix(mcp): refuse SQLite timestamp literals whose UTC instant overflows - #16536

Open
BlueX888 wants to merge 2 commits into
Arize-ai:mainfrom
BlueX888:fix/prep-sqlite-timestamp-literal-utc-overflow-crash
Open

BlueX888 wants to merge 2 commits into
Arize-ai:mainfrom
BlueX888:fix/prep-sqlite-timestamp-literal-utc-overflow-crash

Conversation

@BlueX888

Copy link
Copy Markdown

Summary

executeSql on the SQLite backend raised an unhandled OverflowError for an admitted timestamp literal whose conversion to UTC falls outside years 1-9999, instead of returning the documented error envelope.

Why

Admission accepts any date-shaped literal that resolves to an instant (parse_timestamp_literal, src/phoenix/server/mcp/sql/normalize.py:183), so 0001-01-01T00:00:00+00:30 is admitted. Re-emitting the literal for SQLite then converts the instant to UTC inside format_timestamp_for_sqlite (src/phoenix/server/mcp/sql/normalize.py:253): astimezone computes 0000-12-31 23:30, which a datetime cannot hold, and raises OverflowError: date value out of range at the call site (src/phoenix/server/mcp/sql/rewrite.py:397).

That exception is not an AnalyticsSqlError, so it bypassed the error envelope executeSql documents — "Returns either the columns, rows, and applied limits, or an error envelope when the SQL cannot be accepted" (src/phoenix/server/mcp/sql/tools.py:270) — and reached the caller as an internal failure. The same statement on PostgreSQL, which does not rewrite string literals, executes and returns rows. Every sibling path already refuses through the envelope: unreadable or naive timestamps at admission (src/phoenix/server/mcp/sql/parse.py:2079-2101), and Unix-epoch integers that cannot be read (unix_epoch_to_utc catches OSError, OverflowError, ValueError at src/phoenix/server/mcp/sql/normalize.py:56, and its caller refuses with a clean message).

Changes

  • src/phoenix/server/mcp/sql/rewrite.py: wrap the format_timestamp_for_sqlite call for string literals in _normalize_timestamp_literals in try/except OverflowError and raise AnalyticsSqlError(code=UNSUPPORTED_SYNTAX) naming the UTC boundary spellings to write instead (0001-01-01T00:00:00+00:00, 9999-12-31T23:59:59.999999+00:00).
  • tests/unit/server/mcp/sql/test_execute.py: regression tests — a literal at either end whose UTC instant falls outside years 1-9999 is refused with UNSUPPORTED_SYNTAX through the envelope rather than crashing, and the first/last UTC instants themselves are still comparable (no over-refusal).

Testing

  • New tests: 4 passed — both overflow literals refused via the error envelope on SQLite; both UTC boundary literals still compare and return [[3]].
  • pytest tests/unit/server/mcp/sql/ — sqlite: 2015 passed, 452 skipped; --db postgresql: 2219 passed, 248 skipped.
  • Full pytest tests/unit/ A/B against unpatched main at the same commit: failure lists identical (20 pre-existing, unrelated modules), +4 passed from the new tests.
  • ruff check / ruff format --check on the changed files — pass; mypy (strict, repo config) — no errors in the changed files.
  • End-to-end on both backends: SQLite returns the UNSUPPORTED_SYNTAX error envelope; PostgreSQL returns rows for the same statement.

@BlueX888
BlueX888 requested a review from a team as a code owner September 25, 2026 21:05
@github-project-automation github-project-automation Bot moved this to 📘 Todo in phoenix Sep 25, 2026
@github-actions github-actions Bot added the triage issues that need triage label Sep 25, 2026
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@BlueX888

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

@BlueX888

Copy link
Copy Markdown
Author

recheck

@cephalization cephalization added bug Something isn't working c/mcp language: python Issues with python as the primary language of use and removed triage issues that need triage labels Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Hi @BlueX888, sorry this hasn't had a review yet. Are you still interested in getting it merged? On main, _normalize_timestamp_literals in src/phoenix/server/mcp/sql/rewrite.py still calls format_timestamp_for_sqlite without guarding the OverflowError, so the fix still applies. #16879 has since changed rewrite.py on main. Your branch still merges cleanly, but a rebase and a re-run of the new test_execute.py cases would confirm nothing shifted.


Generated by Claude Code

@BlueX888

Copy link
Copy Markdown
Author

Hi @mikeldking, yes — still very much interested, thanks for taking a look! I'll rebase onto the latest main (picking up #16879's changes to rewrite.py) and re-run the new test_execute.py cases, then update the branch.

@BlueX888
BlueX888 force-pushed the fix/prep-sqlite-timestamp-literal-utc-overflow-crash branch from 67d13ab to 6d01064 Compare October 12, 2026 03:02
@BlueX888

Copy link
Copy Markdown
Author

Rebased onto latest main (picking up #16879) — the guard re-applied cleanly on top of the refactor. Re-ran the new cases: tests/unit/server/mcp/sql/test_execute.py passes (61 passed / 24 skipped, skips are the postgres-only cases), including test_a_timestamp_literal_outside_utc_years_is_refused_rather_than_crashing and test_the_first_and_last_utc_instants_are_still_comparable. The full tests/unit/server/mcp/sql/ suite is green as well (2026 passed / 456 skipped).

@BlueX888

Copy link
Copy Markdown
Author

Hi @mikeldking — yes, still interested in getting this merged.

Rebase is done: the guard re-applied cleanly on top of #16879's rewrite.py changes, and the new test_execute.py cases pass — 61 passed / 24 skipped (skips are postgres-only). The one commit since that run only retriggers the readthedocs builds and changes no files, so those numbers cover the current head, aedc775.

The change wraps the format_timestamp_for_sqlite call in _normalize_timestamp_literals in try/except OverflowError, so a literal whose UTC instant falls outside years 1–9999 is refused through the documented AnalyticsSqlError envelope instead of raising.

Is anything else needed from us before a maintainer review?

@mikeldking mikeldking added the factory-review to be reviewed by the factory label Oct 31, 2026 — with Claude
@arize-phoenix-factory

Copy link
Copy Markdown
Contributor

The factory started a review of aedc775 (session sesn_01N8SvthTSiX6ebQx7mfUxbY). It reports back here when the session ends.

@arize-phoenix-factory arize-phoenix-factory Bot left a comment

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.

Factory review of aedc775: Approve

Every criterion passes: the fix does what it claims, there are no findings, and the new regression tests fail without the fix. Review depth is raised only because the PostgreSQL test run couldn't happen in the sandbox.

Criterion Result Notes
Fit Pass does what it claims
Checks Not run --db postgresql tests couldn't run; ruff, format, mypy and SQLite tests pass
Tests Pass both overflow tests fail with OverflowError without the fix
Blocking findings 0 none
Non-blocking findings 0 none
Risk Low backwards compatibility: SQLite statements that crashed before are now refused with a message
Review depth Deep Checks Not run (PostgreSQL unavailable in sandbox)
Details

Intent: On SQLite, executeSql should refuse an admitted timestamp literal whose UTC conversion falls outside years 1-9999 (for example '0001-01-01T00:00:00+00:30') with an AnalyticsSqlError(UNSUPPORTED_SYNTAX) envelope, not crash with an uncaught OverflowError.
Fit: The PR guards the only unguarded format_timestamp_for_sqlite call that a caller literal can reach (rewrite.py:384).
Fit: The other call, the epoch path at rewrite.py:422, gets an instant that is already UTC and checked at admission, so it can't overflow.
Fit: Admission doesn't call astimezone, and PostgreSQL never re-renders the literal.
Fit: The area reviewer traced comparison, reversed, BETWEEN, IN, CAST, join ON, compact and bare-offset spellings, and each one now returns the envelope.
Fit: The diff makes no changes beyond what the PR describes.
Verified: ruff check and ruff format --check pass on the changed files and repo-wide. mypy reports no issues on the changed files.
Verified: pytest tests/unit/server/mcp/sql/ tests/unit/server/test_mcp_sql_tools.py on SQLite: 2036 passed, 464 skipped.
Verified: I reverted rewrite.py to the merge base and ran the new tests. Both overflow cases failed with OverflowError: date value out of range. The two boundary cases passed both ways, as expected, since they guard against refusing too much.
Not verified: The --db postgresql runs, because PostgreSQL won't start as root in the sandbox; CI covers them. The diff changes no code on the PostgreSQL path.
Not verified: The full repo-wide make typecheck-python. Frontend checks weren't relevant, since no js/ files changed.
Findings without a diff line: none
Risk: side effects none, performance none, backwards compatibility low, from the area reviewers

Graded against the review criteria.

@arize-phoenix-factory

Copy link
Copy Markdown
Contributor

The factory recommends Approve for aedc775. Read the review.

@arize-phoenix-factory arize-phoenix-factory Bot removed the factory-review to be reviewed by the factory label Oct 31, 2026
@mikeldking mikeldking added the factory-approved The factory review recommends approving label Oct 31, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working c/mcp factory-approved The factory review recommends approving language: python Issues with python as the primary language of use

Projects

Status: 📘 Todo

Development

Successfully merging this pull request may close these issues.

3 participants