Skip to content

feat(python/sedonadb-geopandas): add column assignment - #1195

Open
jiayuasu wants to merge 7 commits into
apache:mainfrom
jiayuasu:feature/geopandas-assign
Open

feat(python/sedonadb-geopandas): add column assignment#1195
jiayuasu wants to merge 7 commits into
apache:mainfrom
jiayuasu:feature/geopandas-assign

Conversation

@jiayuasu

@jiayuasu jiayuasu commented Aug 27, 2026

Copy link
Copy Markdown
Member

What changes

This is the first slice of the column assignment / arithmetic / dissolve work drafted in #1184, which grew too large to review as one change. It is being split into four focused PRs — assignment, dissolve, arithmetic, temporal support — that build on each other, so each can be reviewed on its own terms.

This PR adds gdf[key] = value to the experimental sedonadb-geopandas package:

  • Same-frame Series assignment. A Series read from the same frame can be assigned back (renamed or replacing a column). A Series from a different frame, a bare SedonaDB expression, and array-likes are rejected with actionable messages: none of them records a usable row provenance, and each previously failed obscurely or would silently write wrong values. Because assignment rebinds the frame, a Series read before an assignment is stale and is rejected the same way.
  • Scalar broadcasting. Python scalars, NumPy scalars (including 0-d arrays), Arrow scalars, masked values, and pandas.NA broadcast to every row, via a shared scalar classifier/normalizer (is_scalar / normalize_scalar).
  • Geometry assignment with CRS bookkeeping. Assigning a geometry value keeps its type and CRS; a destination CRS is stamped only onto CRS-less geometry. A newly assigned geometry column becomes the active geometry only when the frame had none, and replacing the active geometry column with a non-geometry value clears the active geometry, matching GeoPandas.
  • Temporal scalars are rejected for now. Representing NumPy/pandas temporal scalars faithfully needs dedicated unit and timezone handling (a naive literal silently truncates nanoseconds or rejects most NumPy units); that arrives in the follow-up temporal PR, and until then a clear TypeError beats a subtly wrong stored value.
  • Raises the sedonadb floor to 0.4.1, the first released version with DataFrame.mutate, which assignment is built on.

Testing

80 tests (58 new), run with warnings promoted to errors against both a source-built current main and the released sedonadb 0.4.1 in a clean environment (covering pandas 2.x and 3.x). Ruff lint/format clean.

Support gdf[key] = value for same-frame Series, broadcastable scalars,
and geometry values. Assignment validates its input — a Series from
another frame, a bare expression, and array-likes are rejected with
actionable messages, since none records a usable row provenance — and
geometry assignment preserves the active-geometry bookkeeping: a
geometry column becomes active only when the frame had none, replacing
the active geometry with a non-geometry clears it, and destination CRS
is stamped only onto CRS-less geometry. The scalar surface accepts
Python scalars, NumPy scalars (0-d arrays included), Arrow scalars,
masked values, and pandas NA; NumPy and pandas temporal scalars are
rejected for now because representing them faithfully needs dedicated
unit and timezone handling, which arrives as its own change. Raises the
sedonadb floor to 0.4.1 for DataFrame.mutate.
…lization gaps

Read any geometry-typed column back as a GeoSeries rather than only the
active one, so a freshly assigned geometry column supports .area and
.buffer() immediately. Keep an explicitly cleared active geometry
cleared through materialization by applying GeoPandas' own constructor
rule instead of the engine heuristic. Inherit the destination CRS when a
CRS-less same-frame geometry Series replaces a column that has one,
mirroring the scalar path, and construct replacements for geography
columns with the destination's spatial kind so they stay geography.
Preserve NumPy scalar dtypes (and uint64 range) by rebuilding them as
typed Arrow scalars instead of .item() promotion, classify Arrow-wrapped
nulls and NaN as missing exactly like their bare payloads, broadcast
typed-null nested Arrow scalars through one-element typed arrays, reject
pandas NaT with the same temporal deferral as Timestamp and Timedelta
(and disclose the deferral in the README), and recognize Shapely 1.x
multipart geometries as single values before the sequence check.
…CRS, and scalar edge cases

Preserve the no-active-geometry marker through materialization whatever
the remaining columns are named: the GeoDataFrame constructor
auto-activates a geometry column literally named 'geometry', and there
is no public spelling for a frame with geometry columns but none
active, so the marker is cleared directly. Apply the destination CRS
unconditionally for missing and bare-Shapely geography replacements —
the geography constructors synthesize CRS84, which the CRS-absence
guard mistook for a CRS the value carried. Exempt np.void from the
typed Arrow scalar conversion, which broke its previously working
bytes broadcast. Classify the one-element-null Arrow array spelling of
a typed-null nested scalar as missing explicitly, instead of through a
pandas coincidence that a pandas-less install would lose.
…lls, and structured scalars

Route a typed spatial null (an invalid GeoArrow scalar carrying its own
kind and CRS) through the value path so its metadata survives, instead
of synthesizing a destination-kind null stamped with the destination
CRS; typed non-spatial nulls still mean missing geometry. Strip the
CRS84 that geography constructors synthesize when the destination
column is explicitly CRS-less, so replacements do not silently gain a
CRS. Clear the no-active-geometry marker in place during
materialization rather than reconstructing the frame, which let the
GeoDataFrame constructor coerce an unrelated all-null column named
geometry to geometry dtype. Broadcast structured NumPy scalars as typed
Arrow structs preserving field names and dtypes (rejecting dtypes with
no Arrow mapping) instead of flattening them to lossy lists, keeping
the bytes fallback for plain voids only. The pandas-free nested-null
classification now has a test that actually blocks the pandas import.
…s, honor GeoArrow metadata

Strip a synthesized CRS with the value-preserving st_setsrid(0) instead
of st_setcrs(NULL), which null-propagates its CRS argument and erased
every row while leaving the schema looking correct; the regression test
now asserts materialized values, not just the schema. Recognize
GeoArrow-typed scalars from their extension name rather than resolving
them — the scalar resolver drops the planar/spherical edge type and
rejects non-WKB storage outright — and resolve WKB-typed scalars, valid
and null alike, through their one-element-array spelling, which honors
the complete extension metadata; a null of non-WKB storage is rebuilt
from its own kind and CRS, and a valid one keeps the resolver's error.
Handle structured masked records before the generic mask check, which
itself raises on the mask dtype: masked fields become struct nulls and a
fully masked record is missing. The no-pandas regression test now
builds its Arrow inputs before blocking the import (PyArrow's lazy
pandas shim otherwise fails first) and was mutation-checked.
…o every destination

Recognize GeoArrow-typed scalars before the new-column fast path, so a
spherical WKB scalar assigned to a fresh or non-geometry column keeps
its geography kind (and activates a geometry-less frame with the right
semantics) and non-WKB nulls resolve there too; destination CRS
inheritance still applies only when replacing an existing geometry
column, since a new destination has no metadata to inherit. Serialize a
non-WKB GeoArrow null's own CRS with its canonical to_json() form —
stringifying the wrapper produced StringCrs(...), which ST_SetCRS
rejects as PROJJSON. Unwrap 0-d structured masked containers to their
record form before the generic mask check, which itself raises on
structured dtypes, covering direct and Literal-wrapped assignment for
all three mask states.
…askedRecords recursion

Rebuild a large-WKB GeoArrow scalar's type on Binary storage — keeping
its CRS and edge metadata — before the one-element-array resolution,
since SedonaDB's WKB importer rejects LargeBinary storage outright; the
typed routing had regressed what previously normalized through the
plain scalar path. Unwrap 0-d structured masked containers through the
base MaskedArray view: MaskedRecords' own [()] returns another 0-d
MaskedRecords and recursed until RecursionError, while the base view
yields the record form for the field/mask normalizer.
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.

1 participant