Skip to content

[GH-3110] Fix STAC Client.search(datetime="YYYY-mm") raising TypeError - #3122

Merged
jiayuasu merged 3 commits into
apache:masterfrom
jiayuasu:fix/stac-expand-date-yyyy-mm
Jul 19, 2026
Merged

[GH-3110] Fix STAC Client.search(datetime="YYYY-mm") raising TypeError#3122
jiayuasu merged 3 commits into
apache:masterfrom
jiayuasu:fix/stac-expand-date-yyyy-mm

Conversation

@jiayuasu

@jiayuasu jiayuasu commented Jul 19, 2026

Copy link
Copy Markdown
Member

Did you read the Contributor Guide?

Is this PR related to a ticket?

What changes were proposed in this PR?

Calling the STAC Python Client.search() (or CollectionClient.get_dataframe() / get_items()) with a datetime argument in YYYY-mm form failed with RuntimeError: Failed to get filtered dataframe wrapping TypeError: 'NoneType' object is not callable.

Root cause: python/sedona/spark/stac/collection_client.py imported from pyspark.sql.types import dt. pyspark.sql.types exposes no dt, so the name resolved to None, and the YYYY-mm branch of _expand_date then called dt(...), raising the TypeError. The old arithmetic also overflowed for December (int(month) + 1 == 13).

This PR:

  • Removes the erroneous from pyspark.sql.types import dt import.
  • Computes the last day of the month with calendar.monthrange, which handles December and leap years correctly, and zero-pads the day so the emitted timestamp stays valid ISO 8601.
  • Makes whole-period upper bounds inclusive of the final fractional second. Sedona stores STAC datetimes as timestamps and filters them with an inclusive datetime <= end bound, so a rounded 23:59:59Z end silently drops items in the last fraction of a second. The YYYY, YYYY-mm, and YYYY-mm-dd forms now expand to end at 23:59:59.999999Z (Spark's microsecond timestamp precision), which is the bound the residual Spark filter uses.
  • Preserves that precision through the remote filter push-down. SpatialTemporalFilterPushDownForStacScan previously converted the Spark TimestampType literal with Instant.ofEpochMilli(v / 1000), discarding sub-millisecond digits; it now keeps full microseconds via a floorDiv/floorMod helper.
  • Makes the pushed remote bound a superset of the residual Spark filter for sub-microsecond timestamps. STAC allows up to nine fractional digits, but Spark keeps only microseconds, so it truncates a legal item at ...999999500Z down to ...999999, which the residual datetime <= end filter retains. StacUtils.getFilterTemporal now serializes nine fractional digits and widens the inclusive upper bound (from LessThan/LessThanOrEqual and the upper side of an equality) by 999 ns to the last nanosecond of its microsecond, so the remote catalog no longer drops items Spark would keep. The lower bound is left exact; a slightly wider remote window is always safe because the residual filter re-checks each row at microsecond precision.

_expand_date("2020-05") now returns ["2020-05-01T00:00:00Z", "2020-05-31T23:59:59.999999Z"], consistent with the docstring and the STAC tutorial.

How was this patch tested?

Python (python/tests/stac/test_collection_client.py):

  • test_expand_date covers all supported forms plus the previously broken edge cases: December (2020-12 -> 31), leap-year February (2020-02 -> 29), and non-leap February (2021-02 -> 28).
  • test_expand_date_filter_includes_final_fractional_second runs the real Spark temporal filter over sub-second timestamps and asserts rows at 23:59:59.5Z on the last day/month/year of a period are retained while the first instant of the next period is excluded.

Scala (spark/common), all run via the two suites below (38 tests, all passing locally):

  • SpatialTemporalFilterPushDownForStacScanTest (new) feeds Catalyst <=/>= predicates with microsecond TimestampType literals through the push-down and asserts the serialized datetime= request preserves microseconds and widens the inclusive upper bound to nine digits. Its covers 7-to-9 digit sub-microsecond timestamps case asserts that legal 7-, 8- and 9-digit timestamps in the bound's final microsecond fall within the pushed remote bound.
  • StacUtilsTest gains a getFilterTemporal widens the inclusive upper bound to nanosecond precision case; the existing getFilterTemporal/addFiltersToUrl expectations were updated to the nine-digit, widened bounds (exact lower bound, +999 ns upper bound).

Did this PR include necessary documentation updates?

  • No, this PR does not affect any public API so no need to change the documentation. (The Client.search datetime docstring was refreshed to show the .999999Z bounds.)

…peError

The YYYY-mm branch of CollectionClient._expand_date relied on a bogus
import `from pyspark.sql.types import dt`, which resolves to None, so
`dt(...)` raised `TypeError: 'NoneType' object is not callable`. The old
arithmetic also overflowed for December (int(month) + 1 == 13).

Drop the erroneous import and compute the last day of the month with
calendar.monthrange, which handles December and leap years correctly.

Also make whole-period upper bounds inclusive of the final fractional
second. Sedona stores STAC datetimes as timestamps and filters them with
an inclusive `datetime <= end` bound, so a rounded `23:59:59Z` end drops
items that fall in the last fraction of a second (e.g. `23:59:59.5Z`).
Expand YYYY, YYYY-mm, and YYYY-mm-dd periods to end at `23:59:59.999999Z`,
matching Spark's microsecond timestamp precision.

Add a filter-level regression test that runs the real Spark temporal
filter over sub-second timestamps, alongside the _expand_date unit test.

Closes apache#3110
@jiayuasu
jiayuasu force-pushed the fix/stac-expand-date-yyyy-mm branch from 38c807c to df3c5f7 Compare July 19, 2026 06:00
jiayuasu added 2 commits July 18, 2026 23:38
…sh-down

The in-memory temporal filter already ends whole periods at 23:59:59.999999Z,
but the remote push-down path truncated that bound to milliseconds, so a real
STAC scan could drop items in the final fraction of a second before Spark's
residual filter ran.

Two truncation points are fixed:

- SpatialTemporalFilterPushDownForStacScan converted the Spark TimestampType
  literal (microseconds since epoch) with `Instant.ofEpochMilli(v / 1000)`,
  discarding sub-millisecond precision. Replace it with a helper that keeps all
  six digits via floorDiv/floorMod (also correct for pre-epoch timestamps).
- StacUtils.getFilterTemporal serialized the pushed bound with a millisecond
  (.SSS) pattern. Emit six fractional digits (.SSSSSS) to match Spark's
  TimestampType precision, so the remote request bound equals the residual
  filter bound.

Add a push-down serialization test asserting the Catalyst-predicate to remote
URL path preserves microseconds (the in-memory .5Z test bypasses push-down),
update the getFilterTemporal expectations to six digits, and refresh the
Client.search datetime docstring to show the .999999Z bounds.
…mestamps

STAC permits timestamps with up to nine fractional digits, but Spark's
TimestampType keeps only microseconds. Spark therefore truncates a legal item
at ...999999500Z down to ...999999, which the residual `datetime <= end` filter
retains -- yet the pushed-down remote bound of ...999999Z excluded it, so the
remote catalog dropped an item Spark would have kept.

Serialize the pushed temporal bound with nine fractional digits and widen the
inclusive upper bound (from LessThan/LessThanOrEqual and the upper side of an
equality) by 999 ns to the last nanosecond of its microsecond. The lower bound
stays exact; a slightly wider remote window is always safe because Spark's
residual filter re-checks each row at microsecond precision, so the remote
request now returns a superset of the residual result.

Add a 7-to-9-digit push-down regression test and update the existing
getFilterTemporal expectations to the nine-digit, widened bounds.

Copilot AI 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.

Pull request overview

Fixes STAC datetime handling across the Python client and Spark-side STAC pushdown so that partial-date inputs (e.g. YYYY-mm) expand correctly and temporal bounds preserve microsecond precision end-to-end, avoiding dropped items at period boundaries.

Changes:

  • Python: fix _expand_date for YYYY-mm (remove erroneous dt import, compute month end via calendar.monthrange, and use 23:59:59.999999Z inclusive upper bounds).
  • Spark/Scala: preserve microseconds when converting Spark TimestampType literals for STAC temporal pushdown and widen inclusive upper bounds to the last nanosecond within the final microsecond for remote filtering.
  • Tests: add/extend Scala and Python test coverage for month-end/leap-year expansion and boundary/fractional-second inclusion.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/optimization/SpatialTemporalFilterPushDownForStacScan.scala Convert Spark timestamp literals from micros to LocalDateTime without truncating to milliseconds.
spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/io/stac/StacUtils.scala Serialize temporal filters with 9 fractional digits and widen inclusive upper bounds by +999 ns.
spark/common/src/test/scala/org/apache/spark/sql/sedona_sql/optimization/SpatialTemporalFilterPushDownForStacScanTest.scala New tests validating microsecond preservation and widened upper-bound pushdown behavior.
spark/common/src/test/scala/org/apache/spark/sql/sedona_sql/io/stac/StacUtilsTest.scala Update/add expectations to match nanosecond-precision serialization and widened upper bounds.
python/sedona/spark/stac/collection_client.py Fix _expand_date YYYY-mm expansion and make whole-period upper bounds microsecond-inclusive.
python/sedona/spark/stac/client.py Refresh Client.search datetime docstring to reflect microsecond-inclusive bounds.
python/tests/stac/test_collection_client.py Add tests for expanded date forms and for retaining items in the final fractional second of a period.

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

@jiayuasu
jiayuasu merged commit 1dfb011 into apache:master Jul 19, 2026
44 checks passed
@jiayuasu jiayuasu added this to the sedona-1.9.1 milestone Jul 19, 2026
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.

STAC Client.search(datetime="YYYY-mm") raises TypeError: NoneType object is not callable

2 participants