Skip to content

feat(csharp): convert geospatial query results - #700

Open
cathleeny wants to merge 5 commits into
mainfrom
feat/csharp-geospatial-results
Open

cathleeny wants to merge 5 commits into
mainfrom
feat/csharp-geospatial-results

Conversation

@cathleeny

@cathleeny cathleeny commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

What's Changed

  • Add the client-side geospatial representation option, defaulting to GeoArrow WKB; disabled mode returns WKT or EWKT strings.
  • Convert Reyden's internal SRID plus WKB struct to public geoarrow.wkb binary values.
  • Convert legacy WKT or EWKT text results to the same GeoArrow output.
  • Preserve fixed column SRIDs in GeoArrow metadata and per-row ANY SRIDs in EWKB.
  • Mark GEOGRAPHY fields with spherical edges.
  • Apply conversion on SEA/Reyden and Thrift paths without forwarding the option to the server.

Stack: 2 of 3. Depends on #699.

Testing

  • Top-level geospatial conversion tests: 19 passed.
  • Stack-wide C# unit suite: 981 passed.
  • CI is running on the rewritten stack.

@cathleeny
cathleeny added this pull request to stack #702 October 8, 2026 22:46

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good — the geospatial conversion is well-structured and the data flow is consistent across both the Thrift (MaybeWrapComplexTypes) and SEA paths: geo columns are detected via Spark:DataType:SqlName metadata, are correctly excluded from ComplexTypeSerializingStream (which only matches ARRAY/MAP/STRUCT names), and schema/batch types stay in agreement in both native-struct and string modes; sliced-struct offset handling is explicitly tested. One low-confidence coverage gap on WKT formatting (F1). One behavior worth reviewer awareness, not filed as a defect since it's documented and intentional: with the default config (enable_complex_datatype_support=false, enable_geospatial_support=true), GEOMETRY/GEOGRAPHY columns are returned as native Arrow struct<srid,wkb> while all other complex types are JSON-stringified — consumers that can't consume nested Arrow structs will need to set enable_geospatial_support=false.

Comment thread csharp/src/GeospatialWkb.cs Outdated
@cathleeny
cathleeny marked this pull request as draft October 8, 2026 23:02
@cathleeny
cathleeny force-pushed the feat/csharp-geospatial-results branch from fd9e5dd to 7e325c9 Compare October 9, 2026 01:17
@cathleeny
cathleeny force-pushed the feat/csharp-geospatial-results branch from 7e325c9 to a9c053f Compare October 14, 2026 18:27
@cathleeny
cathleeny marked this pull request as ready for review October 14, 2026 19:01

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium · 1 Low

Solid, well-tested PR — the WKB↔EWKB codec and the geo-transform stream handle byte order, ISO→EWKB flag conversion, nested collections, SRID validation, null children, large/sliced arrays, and both native-struct and legacy-text inputs correctly, with thorough unit coverage. The batch-rewrite/non-dispose pattern matches the existing sibling streams, and the feature flag is correctly kept client-side (not forwarded to SEA). Two non-blocking notes: a medium heads-up that the default-on flag changes GEOMETRY/GEOGRAPHY result columns from WKT strings to binary GeoArrow WKB (a wire-format change for existing consumers), and a low perf note that the stream fully parses every metadata-tagged column's type text — including nested complex types — even for non-geo results.

Comment thread csharp/src/DatabricksConnection.cs
Comment thread csharp/src/GeospatialTransformingStream.cs
@cathleeny
cathleeny force-pushed the feat/csharp-geospatial-results branch from a9c053f to 9613cfa Compare October 14, 2026 22:47

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium · 1 Low

Solid, well-tested PR. The WKB/EWKB conversion math (ISO dimension offsets → EWKB flags, endianness, nested-collection recursion, trailing-byte checks) and the fixed-vs-ANY SRID handling are careful and match the extensive unit tests; the new stream is correctly composed before ComplexTypeSerializingStream and preserves the Spark:DataType:SqlName metadata so geo columns aren't re-serialized as JSON. Two substantive notes posted inline: a backward-compatibility concern around the true default flipping existing geometry output from WKT strings to binary (F1, medium), and a validation asymmetry on the fixed-SRID native branch (F2, low).

Nits / observations not posted inline: (1) DatabricksStatement.MaybeWrapComplexTypes lost its XML-doc rationale for the PECO-3032/D3 complex-type JSON fix — the comment is now a single generic sentence while the serializer behavior it documented still exists. (2) Geospatial values nested inside complex t

[...truncated to keep verdict scannable]

Comment thread csharp/src/DatabricksConnection.cs
Comment thread csharp/src/GeospatialTransformingStream.cs

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium · 1 Low

Solid, well-tested feature (WKB/EWKB rewriting, SRID validation, GeoArrow metadata, and SEA/Thrift wiring all look correct and have good unit coverage). Two things worth a look: (1) a medium gap — nested geospatial values inside STRUCT/ARRAY/MAP are parsed into tagged transport structs but never converted, so they leak the internal struct<srid,wkb> representation (raw or JSON) rather than WKB/WKT; and (2) a low note that defaulting EnableGeospatialSupport to true changes the existing GEOMETRY/GEOGRAPHY result type from string to binary for current consumers. The client-side flag correctly stays out of SEA session_confs (verified against the ServerSidePropertyPrefix filter and the new SSP test).

Comment thread csharp/src/GeospatialTransformingStream.cs
Comment thread csharp/src/DatabricksConnection.cs
@cathleeny
cathleeny force-pushed the feat/csharp-geospatial-results branch from 9221940 to 34b555c Compare October 16, 2026 19:08

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium · 1 Low

Solid, well-tested addition — the WKB↔EWKB rewriter and WKT rendering carry byte-exact unit coverage that checks out, SRID validation is thorough, and the batch-ownership/schema-wrapping pattern matches the existing ComplexTypeSerializingStream/IntervalSerializingStream. Two things worth a look: nested GEOMETRY/GEOGRAPHY inside complex types is mapped to the internal struct<srid,wkb> transport type but never converted by GeospatialTransformingStream, so that internal shape leaks to callers (medium, possibly deferred to a later stack); and the default-on EnableGeospatialSupport=true changes the output type of existing geospatial columns without opt-in, unlike the default-off EnableComplexDatatypeSupport (low — confirm intent).

Nit (not inlined): MaybeWrapComplexTypes in DatabricksStatement.cs lost its doc comment explaining the PECO-3032 / malformed-MAP-JSON rationale for ComplexTypeSerializingStream; the terser replacement no longer records why that wrapper exists.

Comment thread csharp/src/GeospatialTransformingStream.cs
Comment thread csharp/src/StatementExecution/StatementExecutionConnection.cs

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Solid, well-tested PR — the WKB/EWKB codec and GeospatialTransformingStream have thorough round-trip and edge-case coverage, the flag is wired consistently across Thrift/SEA with a true default, and the setting is correctly kept client-side (verified via the SSP test and telemetry mapping). One low-severity scoping note: nested GEOMETRY/GEOGRAPHY columns (inside ARRAY/MAP/STRUCT) are not converted and leak the internal struct<srid,wkb> transport type when EnableComplexDatatypeSupport=true. Also note (no action needed) that defaulting enable_geospatial_support=true changes the default geo output representation to binary GeoArrow WKB — intended per the PR description.

Comment thread csharp/src/GeospatialTransformingStream.cs
@cathleeny
cathleeny added this pull request to the merge queue Oct 19, 2026
@eric-wang-1990
eric-wang-1990 removed this pull request from the merge queue due to a manual request Oct 19, 2026
Comment thread csharp/src/Telemetry/ConnectionTelemetry.cs Outdated
Comment thread csharp/src/GeospatialTransformingStream.cs
hasLogicalTag = GeospatialArrowType.ResolveTag(logicalType, out logicalTag);
}

bool hasPhysicalTag = GeospatialArrowType.ResolveTag(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two related questions on how geo columns are detected:

  1. Which physical types can the server send? The code handles a tagged struct<srid,wkb> (Reyden) and String/LargeString EWKT (legacy; main's GeospatialDataColumnReportsStringEwkt shows strings on both Thrift and SEA), and throws on anything else. Could you document which protocol/server versions/configs produce which representation, and whether it can vary within one result (inline vs CloudFetch, chunk to chunk)? If it can, a test mixing string and struct batches would be good, especially for feat(csharp): support nested geospatial results #701, which plans from the first batch only. Also, can the server ever send plain Binary WKB or an untagged struct? Today that would fail the whole query.

  2. Detection convention. Since a legacy string column can only be recognized via Spark:DataType:SqlName, that key is the real source of truth here, as in ComplexTypeSerializingStream / IntervalSerializingStream / NullColumnSerializingStream. But a geo-tagged physical struct also counts as geo on its own, even when Spark:DataType:SqlName says STRUCT or STRING. Unless there's a real case for that, could the physical tag only validate (throw InvalidData when Spark:DataType:SqlName says non-geo but the payload is tagged, as with the SRID mismatch), and be used alone only when Spark:DataType:SqlName is missing? Same for the nested ResolveTag(source.DataType) fallback in feat(csharp): support nested geospatial results #701's RewriteData.

Related: the SEA legacy case, where the manifest-derived schema declares the tagged struct<srid,wkb> but the IPC batches contain EWKT strings, works because RewriteGeo dispatches on the batch's array type. But I don't see a unit test for it; the existing tests declare StringType in the schema. Could you add one (declared tagged struct + StringArray batch, both modes)? Also, in string mode legacy text is passed through verbatim while native values are rendered locally. Is the formatting guaranteed to match (spacing, coordinate precision)?

}
else
{
output.Append(wkb);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perf question: this stream now wraps every result (including non-geo, which is fine since it's a pass-through), but each geo column is rebuilt row by row. A few suggestions:

  1. Fixed SRID (default path): the output is byte-identical to the wkb child, so it could reuse the child's offsets/data buffers and only combine the struct validity, which is roughly zero-copy.
  2. ANY → EWKB: WkbToEwkbRewriter copies through a List<byte> one byte at a time and allocates a byte[4] per uint32. Writing into a pre-sized byte[] (input length + 4) with Span copies would cut this to about one copy.
  3. String mode: NTS parsing into a full Geometry per row is the heaviest path. Since you already have a custom WKT writer, could it read WKB directly without building a Geometry?

Could you add a quick benchmark (e.g. 1M rows of points and of 1K-vertex polygons, per mode) so we know the overhead before this ships with the default flipped to true?

private bool _enablePKFK = true;
private bool _runAsyncInThrift = true;
private bool _enableComplexDatatypeSupport = false;
private bool _enableGeospatialSupport = true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Echoing the bot's comment with a human +1: before this stack, geo columns came back as WKT/EWKT strings on both Thrift and SEA (GeospatialDataColumnReportsStringEwkt on main asserts this). Defaulting enable_geospatial_support to true silently changes the column type from String to Binary for existing users. Could we default to false (opt-in, like enable_complex_datatype_support), or if true is intentional, call it out as a behavior change in the CHANGELOG (#701 currently lists it under "Added")?

Also, have we tested this behavior change in Power BI already? Power BI is one of the main consumers of this driver, so it would be good to confirm how it handles a geo column that arrives as geoarrow.wkb binary instead of a string, both for existing reports/models and for new ones.

Base automatically changed from feat/csharp-geospatial-arrow-types to main October 19, 2026 18:44
Signed-off-by: Cathleen Yan <58714163+cathleeny@users.noreply.github.com>
Signed-off-by: Cathleen Yan <58714163+cathleeny@users.noreply.github.com>
Signed-off-by: Cathleen Yan <58714163+cathleeny@users.noreply.github.com>
Signed-off-by: Cathleen Yan <58714163+cathleeny@users.noreply.github.com>
@cathleeny
cathleeny force-pushed the feat/csharp-geospatial-results branch from cb722f5 to 98eca4c Compare October 19, 2026 18:44

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks solid — the WKB↔WKT/EWKB codec and the SEA/Thrift stream wiring are carefully implemented and backed by thorough round-trip tests (dimensional markers, empty members, byte-order, sliced-struct offset, fixed-vs-ANY SRID validation, GeoArrow metadata). No correctness defects found in the top-level conversion path; the flag is correctly kept client-side (not forwarded to SEA) and defaults are consistent across both connection classes.

Two things for the human reviewer to confirm rather than treat as bugs: (1) EnableGeospatialSupport defaults to true, so merging this flips the on-the-wire representation of existing top-level GEOMETRY/GEOGRAPHY results from server WKT strings to GeoArrow WKB binary — this is the PR's stated intent but is a behavioral break for current consumers; (2) one inline Low on nested-geo handling.

Comment thread csharp/src/GeospatialTransformingStream.cs
Signed-off-by: Cathleen Yan <58714163+cathleeny@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Solid, well-tested PR. The WKB↔WKT/EWKB codec (GeospatialWkb) and the per-column plan/convert logic in GeospatialTransformingStream are carefully implemented and backed by extensive unit tests (dimensional markers, empty members, byte order, sliced-offset structs, fixed/ANY SRID, legacy text). Struct-child access is guarded by GeospatialArrowType.IsStructShape (exact 2-field shape), and the undisposed-original-batch rewrite pattern matches the existing ComplexTypeSerializingStream. One low-severity asymmetry noted inline. Two things to confirm (not code bugs): (1) EnableGeospatialSupport defaults to true on both paths, so GEOMETRY/GEOGRAPHY results that previously surfaced as StringType/WKT now become geoarrow.wkb binary by default — ensure this default-on output-format change is intended and called out in release notes; (2) geospatial columns nested inside complex ARRAY/MAP/STRUCT values are not converted to GeoArrow (only to

[...truncated to keep verdict scannable]


private static ArrayData RewriteGeo(
ArrayData source,
IArrowType outputType,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — Fixed-SRID validation is asymmetric between the two disabled-mode (string) input paths.

When outputType.TypeId == String and the source is a native struct, RewriteGeo routes to ConvertToEwkt, which calls ValidateFixedSrid for every row — so a GEOMETRY(4326) column carrying a row whose SRID is 3857 is rejected with InvalidData (covered by FixedSrid_StringModeRejectsMismatchedRowSrid).

But when the source is already text (legacy WKT/EWKT), this early branch returns the string verbatim without ever parsing/validating the embedded SRID=...:

if (outputType.TypeId == ArrowTypeId.String)
{
    return source.DataType.TypeId == ArrowTypeId.String
        ? source
        : ConvertLargeStringToString(source).Data;
}

So for a fixed-SRID column, a legacy-text row like SRID=3857;POINT(1 2) on a GEOMETRY(4326) column passes through silently in disabled mode, whereas the identical mismatch is thrown in native mode (ConvertTextToGeoArrow → ValidateFixedSrid) and in disabled-struct mode. This may be intentional (render legacy text as-is), but the inconsistency means the same bad data is accepted or rejected depending only on the server's physical encoding and the flag. Worth a comment at minimum so the divergence is deliberate rather than accidental.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants