feat(function): add S2 and H3 geospatial cell functions - #25065
Conversation
Implement the S2_* and H3_* SQL functions described in docs/design/s2h3_funcs.md. S2 (github.com/golang/geo/s2, already a dependency): s2_cellid(POINT), s2_cellid_level, s2_cellid_center, s2_cellid_area, s2_cellid_parent, s2_cellid_edgeneighbours, s2_cellid_allneighbours, s2_cellid_areneighbours H3 (github.com/uber/h3-go/v4, newly added): h3_h3index(POINT[, res]), h3_h3index_resolution, h3_h3index_center, h3_h3index_boundary, h3_h3index_parent[ , res], h3_h3index_neighbours, h3_h3index_areneighbours A CellId / H3Index is BIGINT UNSIGNED (uint64). Neighbour lists return a JSON array of uint64 encoded with bytejson's native uint64 type code so the full 64-bit id is preserved. Both British and American spellings of "neighbour(s)" are accepted. Decisions vs. the design note (see doc): - s2_cellid_parent returns a CellId, not a POINT (spec typo). - h3_h3index(POINT) defaults to resolution 15 and gained an optional explicit-resolution overload (H3 needs a resolution to pick a cell). - s2_cellid_area returns square metres rather than S2 steradians. BVT: test/distributed/cases/geo/geo_s2h3.sql (34 statements, all pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
The S2/H3 id additions left the H3_* const block misaligned, which the SCA (golangci-lint gofmt) check flagged. gofmt-only change, no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`INSERT INTO t VALUES (st_point(116.3975, 39.9087))` failed with "invalid argument function st_point, bad value [GEOMETRY GEOMETRY]" (only with decimal/float literal args; integers and INSERT ... SELECT worked). Root cause: in a VALUES clause the DefaultBinder carries the destination column type and pushes it down to nested literals, so bindNumVal received typ = GEOMETRY for st_point's float64 arguments and cast each literal to GEOMETRY — breaking the function's overload resolution. Guard bindNumVal so a numeric literal is never coerced to a geometry target type (string literals are left alone, since a WKT string may legitimately cast to geometry). Also add the missing GEOMETRY32 source overload to cast_geometry_to_subtype, so a geometry32 value (e.g. st_point32(...)) can be stored into a point32 / subtype-constrained column. The eval already handles float32 via the "32:" subtype prefix; only the overload was missing. BVT: geo_stpoint.sql extended to cover INSERT ... VALUES with decimal args into point / point32 columns (the previous .result had the error baked in). Full geo suite passes; func unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the geometry-specific bindNumVal guard from the previous commit
with the correct, general fix. The root problem is that buildValueScan
binds every VALUES item with a DefaultBinder carrying the destination
column type, and that type is pushed down to nested literals. That target
type is only meaningful for a bare literal value, not for the literal
arguments of a function call: st_point(116.3975, 39.9087) bound the float
args against the GEOMETRY column type, breaking overload resolution
("bad value [GEOMETRY GEOMETRY]") — and the same class of bug could affect
any column type, not just geometry.
Now a compound value expression (function/operator) is bound with a
binder that has no column type, so its literal arguments bind by their own
type; the result is cast to the column type afterward (funcCastFor*Type /
forceCastExpr2). A bare literal value still binds against the column type
as before. bindNumVal is reverted to its original behavior.
Regression: geo, dml/insert, dml/update, dml/replace, expression and
operator BVT suites all pass 100%; geo_stpoint.result is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous commit bound every non-bare-literal VALUES expression without the column type, which regressed inserts where the column type is legitimately needed by a nested literal: a negative literal like -999.995 (a UnaryExpr) and a cast like 123.45::int (a CastExpr) lost the destination decimal/precision and failed dtype/decimal and pg_cast BVT. Narrow the rule to actual function calls (*tree.FuncExpr), matching the real problem: st_point(116.3975, 39.9087) must bind its float arguments by their own types, not the GEOMETRY column type. Unary/cast/other value expressions keep binding against the column type as before. Verified locally: dtype/decimal, pg_cast (the CI failures), plus geo, dml/insert, dml/update, expression, operator all pass 100%. Remaining local dtype/function failures are pre-existing stage/datalink/wasm environment cases (green in CI), unrelated to this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
XuPeng-SH
left a comment
There was a problem hiding this comment.
I re-checked the latest head and still see two substantive correctness issues.
-
h3_h3index_areneighboursdoes not match the documented mixed-resolution semantics.
The docs now explicitly say mixed-level comparisons should returnfalse, not error. But the implementation still forwards directly toca.IsNeighbor(cb), and with the H3 v4.5.0 library used by this PR that call returnsErrResolutionMismatchwhen the two cells are valid but at different resolutions.I verified this against the dependency version in the PR: a cell and its parent do not produce
(false, nil); they produce a resolution-mismatch error. So the current implementation contradicts the documented/user-facing semantics.Suggestion: detect differing H3 resolutions before calling
IsNeighborand returnfalse; add an H3 ancestor/descendant SQL regression alongside the existing S2 cross-level case. -
The INSERT binder fix still misses wrapped
st_point(...)expressions.
pkg/sql/plan/bind_insert.gonow switches to the function-call binder only when the top-level VALUES AST node is exactly*tree.FuncExpr. Parenthesized or cast-wrapped constructors like(st_point(...))orcast(st_point(...) as point)still go through the destination-column binder, andbase_binder.gorecursively binds their inner expression with that same binder. So the original overload-resolution failure can still survive under transparent wrappers.Suggestion: unwrap at least
ParenExprandCastExprbefore deciding whether to use the function-call binder, and add regression cases for wrapped constructors inINSERT ... VALUESintopoint/point32columns.
Non-blocking follow-up suggestions:
- add alias-call coverage for the US spelling variants (
...neighbors), - add an H3 pentagon neighbor/boundary edge-case test, since that is a real H3 shape difference even though the safe
GridDiskpath itself did not reproduce a zero-cell bug in my local probe.
…nstructors Two correctness fixes from PR review (XuPeng-SH): 1. h3_h3index_areneighbours: h3-go v4.5.0's IsNeighbor returns a resolution-mismatch error for two valid cells at different resolutions, contradicting the documented "cross-resolution returns false" semantics. Detect differing resolutions before IsNeighbor and return false, matching the S2 cross-level behavior. Added an H3 ancestor/descendant regression. 2. INSERT ... VALUES binder only used the function-call binder when the top-level node was exactly *tree.FuncExpr, so wrapped constructors — (st_point(...)) and cast(st_point(...) as point) — still bound their arguments against the destination column type and could fail overload resolution. Unwrap ParenExpr/CastExpr before deciding. Added regression cases for parenthesized and cast-wrapped constructors into point/point32. A cast wrapping a bare literal (123.45::int) is unaffected. Also added American-spelling alias coverage (s2_cellid_allneighbors, s2_cellid_edgeneighbors, h3_h3index_neighbors, *_areneighbors). Verified: geo, dtype/decimal and pg_cast (the prior CI failures) all 100%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @XuPeng-SH — both addressed in 1. 2. Wrapped Non-blocking:
Verified locally: |
# Conflicts: # pkg/sql/plan/function/function_id.go # pkg/sql/plan/function/function_id_test.go
Implements
S2_*andH3_*SQL functions for geospatial cell indexing.Functions
S2 (via
github.com/golang/geo/s2, already a dependency):s2_cellid(POINT),s2_cellid_level,s2_cellid_center,s2_cellid_area,s2_cellid_parent,s2_cellid_edgeneighbours,s2_cellid_allneighbours,s2_cellid_areneighboursH3 (via
github.com/uber/h3-go/v4, newly added — cgo-backed, bundles the H3 C library):h3_h3index(POINT[, res]),h3_h3index_resolution,h3_h3index_center,h3_h3index_boundary,h3_h3index_parent[, res],h3_h3index_neighbours,h3_h3index_areneighboursA
CellId/H3IndexisBIGINT UNSIGNED(uint64). APOINTargument carries(longitude, latitude). Neighbour lists return a JSON array of uint64 encoded with bytejson's native uint64 type code so the full 64-bit id is preserved (above 2^53 a JSON float would lose precision). Both British and American spellings of "neighbour(s)" are accepted.Behavioural notes
s2_cellid_parentreturns a CellId (BIGINT UNSIGNED), mirroringh3_h3index_parent.h3_h3index(POINT)defaults to resolution 15 and has an optional explicit-resolution overload — H3 needs a resolution to pick a cell, unlike S2 where a point maps to a unique leaf cell.s2_cellid_areareturns square metres rather than S2's native unit-sphere steradians, so it is directly usable and comparable to H3.CellId/H3Index(e.g.0) or a non-POINT geometry argument raises aninvalid inputerror; NULL inputs yield NULL.Files
pkg/sql/plan/function/func_s2h3.go(new — implementations + helpers)pkg/sql/plan/function/function_id.go,function_id_test.go,list_builtIn.go(ids + registration)go.mod/go.sum(h3-go/v4)docs/design/s2h3_funcs.md(design note + implementation details)test/distributed/cases/geo/geo_s2h3.{sql,result}(BVT)Testing
mo-servicebuild + link clean.Test_funidsID-registration unit test passes.geo_s2h3.sql: 34/34 statements pass (100%), covering every function, the storedBIGINT UNSIGNEDcolumn path, and invalid-input error cases.Note for reviewers
h3-go/v4is cgo-backed and bundles the H3 C library — please confirm it builds in CI / musl-static configurations.🤖 Generated with Claude Code