Skip to content

NO-SNOW: Validate the time travel offset before emitting it into SQL - #4506

Open
sfc-gh-yixie wants to merge 2 commits into
mainfrom
yixie-time-travel-offset-validation
Open

sfc-gh-yixie wants to merge 2 commits into
mainfrom
yixie-time-travel-offset-validation

Conversation

@sfc-gh-yixie

Copy link
Copy Markdown
Collaborator
  1. Which Jira issue is this PR addressing? Make sure that there is an accompanying issue to your PR.

    NO-SNOW: a small pre-existing bug found while working on SNOW-4483962 (SNOW-4483962: Support change-tracking reads (CHANGES clause) and END #4505).

  2. Fill out the following pre-review checklist:

    • I am adding a new automated test(s) to verify correctness of my new code
      • If this test skips Local Testing mode, I'm requesting review from snowflakedb/local-testing
    • I am adding new logging messages
    • I am adding a new telemetry message
    • I am adding new credentials
    • I am adding a new dependency
    • If this is a new feature/behavior, I'm adding the Local Testing parity changes.
    • I acknowledge that I have ensured my changes to be thread-safe. Follow the link for more information: Thread-safe Developer Guidelines
    • If adding any arguments to public Snowpark APIs or creating new public Snowpark APIs, I acknowledge that I have ensured my changes include AST support. Follow the link for more information: AST Support Guidelines
  3. Please describe how your code solves the related issue.

    Bug: TimeTravelConfig.generate_sql_clause emits the time travel offset unquoted, as AT|BEFORE (OFFSET => {offset}), and nothing validated it. offset is typed int, but a string passed to Session.table, DataFrameReader.table, Table, or the offset reader option went into the query verbatim:

    session.read.option("time_travel_mode", "at").option("offset", "-1) union all select 42 --").table("T")
    # SELECT * FROM T AT (OFFSET => -1) union all select 42 --)

    Fix: a new TimeTravelConfig.normalize_offset runs before any SQL or AST is built. It accepts:

    • an int (not bool);
    • an integer string such as "-60", normalized to int;
    • an integer arithmetic expression such as "-30*60", which Snowflake documents for OFFSET and which already worked through the reader option. It is passed through unchanged.

    Anything else raises ValueError with a clear message, for example the injection string, "abc", "1.5", "--1", True or 1.5. Every other value emitted unquoted in that clause is already validated; version is checked as an int, and strings go through str_to_sql.

    AST: Table.offset / ReadTable.offset is an Int64Value. Before this change, any string offset with AST collection enabled failed with a protobuf TypeError. Now:

    • Integer strings are recorded as ints.
    • An arithmetic expression raises a clear ValueError asking for an int, rather than being silently left out of the AST, which would make a replay read the table without time travel.
    • AST collection is off by default, and existing AST expectations are unchanged.

    Testing:

    • Unit tests (tests/unit/test_utils.py): the exact SQL for each accepted form, and a ValueError for 15 rejected forms.
    • Live integration test (tests/integ/test_dataframe.py, test_time_travel_offset_validation, run with AST collection off and on): rejected values raise through session.table, session.read.table and the reader option. Arithmetic and integer-string offsets return the table's rows. With AST on, arithmetic offsets raise and integer strings work.
    • The existing time-travel, mock, AST and doctest suites pass.

🤖 Generated with Claude Code

The time travel `offset` of Session.table, DataFrameReader.table and Table
(including the `offset` reader option) was emitted unquoted into
`AT|BEFORE (OFFSET => ...)` without validation, so a string such as
"-1) union all select 42 --" was inserted into the query verbatim.

It must now be an int, an integer string (normalized to int), or an integer
arithmetic expression such as "-30*60" (passed through, as Snowflake allows).
Anything else raises ValueError before any SQL is built. With AST collection
enabled, a string offset used to fail with a protobuf TypeError; integer strings
are now recorded as ints and arithmetic expressions raise a clear ValueError,
since the AST field only holds an int.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Nov 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.03%. Comparing base (d52a28d) to head (de8c527).

Files with missing lines Patch % Lines
src/snowflake/snowpark/dataframe_reader.py 66.66% 0 Missing and 1 partial ⚠️
src/snowflake/snowpark/table.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4506      +/-   ##
==========================================
- Coverage   94.98%   92.03%   -2.96%     
==========================================
  Files         179      179              
  Lines       46942    46965      +23     
  Branches     8111     8116       +5     
==========================================
- Hits        44590    43226    -1364     
- Misses       1511     2718    +1207     
- Partials      841     1021     +180     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- Reject offsets containing '--', which starts a SQL comment ('-1--1' and
  '5\n--1' were accepted); '-1 - -1' still works.
- Match only ASCII digits, spaces and tabs (no newlines, Unicode whitespace or
  non-ASCII digits).
- Accept any numbers.Integral, such as numpy.int64, and normalize it to int, as
  such values worked before validation was added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants