Skip to content

feat(bindings): tgeometry + tgeography + tgeogpoint parity — cumulative (consolidates #90, #91)#92

Closed
estebanzimanyi wants to merge 18 commits intomainfrom
feat/parity-tgeogpoint
Closed

feat(bindings): tgeometry + tgeography + tgeogpoint parity — cumulative (consolidates #90, #91)#92
estebanzimanyi wants to merge 18 commits intomainfrom
feat/parity-tgeogpoint

Conversation

@estebanzimanyi
Copy link
Copy Markdown
Member

Summary

Adds the geographic-coordinate-system point-restricted temporal type tgeogpoint, completing the (geometric / geographic) × (generic / point-restricted) cross-product alongside the already-shipped tgeompoint, tgeometry, and tgeography (PR #91).

The MEOS C functions backing this surface are subtype-agnostic between tgeography and tgeogpoint, so this PR is largely a search-and-replace mirror of PR #91 (tgeography): TGeographyTypesTGeogpointType, TGEOGRAPHY()TGEOGPOINT(), tgeography_*tgeogpoint_* in MEOS calls.

What's included

Layer Source What it adds
Type registration + constructors + I/O src/geo/tgeogpoint.cpp + tgeogpoint_in_out.cpp The tgeogpoint type, tgeogpointInst / tgeogpointSeq constructors, asText / asEWKT
Foundational accessors / restrict / modifiers / comparison src/geo/tgeogpoint.cpp Same surface as tgeography's foundational layer (~50 names)
Cross-type predicates (box / position / spatial-rels / temporal-spatial-rels / distance) src/geo/tgeogpoint_ops.cpp Same surface as tgeography's cross-type layer (~100 names)
Spatial functions src/geo/tgeogpoint_ops.cpp SRID, setSRID, transform, stbox, tgeography(tgeogpoint) / tgeogpoint(tgeography) coercions (the pair that PR #91 deferred), centroid, convexHull, traversedArea
Aggregate wiring src/temporal/span_aggregates.cpp, src/temporal/temporal_aggregates.cpp extent(tgeogpoint) → stbox; TcountAgg(tgeogpoint) → tint; MergeAgg(tgeogpoint); AppendInstantAgg(tgeogpoint); AppendSequenceAgg(tgeogpoint); TcentroidAgg(tgeogpoint) (tgeogpoint is a point type, so the temporal centroid aggregate applies)
Tile / box emitters + analytics src/geo/tgeogpoint_ops.cpp spaceBoxes, spaceTimeBoxes, the four *Simplify registrations

Stacked on top of #91

This PR is targeted to merge into feat/parity-tgeography (PR #91). It depends on the tgeography type being registered so the tgeogpoint <-> tgeography coercion pair can land here.

Closes the deferred coercion gap

PR #91's body called out that the tgeogpoint coercion pair couldn't be registered until tgeogpoint was a registered type. This PR adds them:

  • tgeography(tgeogpoint) → tgeography via tgeogpoint_to_tgeography
  • tgeogpoint(tgeography) → tgeogpoint via tgeography_to_tgeogpoint

Defaults / quirks

Same as tgeography: SRID defaults to 4326 (WGS84), extent(tgeogpoint) emits SRID=4326;GEODSTBOX, encoded payloads start with 0101000020E6100000….

Out of scope

Test plan

  • test/sql/parity/042_tgeogpoint_parity.test — 19 assertions covering accessors, time-domain restrict, modifiers, comparison, box predicates, position predicates, spatial functions (SRID + coercions), and aggregate wiring (including TcentroidAgg).
  • All 162 cumulative-base assertions (001_set, 025_temporal_tile_getters, 026_single_tile_getters, 030_aggregates_extent, 031_aggregates_skiplist, 040_tgeometry_parity, 041_tgeography_parity) still pass.

🤖 Generated with Claude Code

estebanzimanyi and others added 18 commits May 1, 2026 00:41
…ueTimeTiles)

Adds the single-bin getter family and the LIST<TBOX> emitters that are
the scalar counterparts of the prior bins() / tboxes() table functions.

  getBin(integer, integer, integer)        -> intspan
  getBin(bigint, bigint, bigint)           -> bigintspan
  getBin(double, double, double)           -> floatspan
  getBin(date, interval, date)             -> datespan
  getBin(timestamptz, interval, timestamptz) -> tstzspan

  valueTiles(tbox, vsize [, vorigin])              -> LIST<tbox>
  timeTiles(tbox, duration [, torigin])            -> LIST<tbox>
  valueTimeTiles(tbox, vsize, duration
                  [, vorigin, torigin])            -> LIST<tbox>

The tbox emitters dispatch on tbox->span.spantype so the same SQL
function transparently handles TBOXINT and TBOXFLOAT inputs without
the user having to pick a numeric variant. Defaults match MobilityDB's
SQL: vorigin = 0, torigin = '2000-01-03'.

Test 025_temporal_tile_getters.test exercises all five getBin variants
and the three emitters across both int and float tboxes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous PR landed extent() for the 5 span types only. This expands
the same aggregate to cover the full MobilityDB SQL surface for fixed-
size-state extent variants (still avoiding the SkipList-state aggregates
like tcount / tmin / tcentroid / merge / appendInstant — those need a
larger lift).

Added (15 new overloads on the same `extent` aggregate set):

- extent(integer | bigint | double | date | timestamptz)  -> typed span
- extent(intset | bigintset | floatset | dateset | tstzset) -> typed span
- extent(intspanset | bigintspanset | floatspanset
        | datespanset | tstzspanset)                       -> typed span
- extent(tbox)                                              -> tbox
- extent(stbox)                                             -> stbox
- extent(tint | tfloat)                                     -> tbox
- extent(tbool | ttext)                                     -> tstzspan
- extent(tgeompoint)                                        -> stbox

All variants share a small set of templated state types — Span, TBox,
STBox — and dispatch on the appropriate MEOS *_extent_transfn for the
input. Combine functions use span_extent_transfn / tbox_expand /
stbox_expand so parallel-agg merge works.

Tests in 030_aggregates_extent.test cover one assertion per category
plus NULL handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, tsum, tavg, tcentroid)

Adds the harness for MEOS SkipList-state temporal aggregates and ships
seven of them. SkipList aggregates differ from the fixed-state extent()
family in two ways:

  1. State holds a `SkipList *` pointer; the skiplist owns variable-size
     heap-allocated Temporal* values, so the aggregate needs a destructor
     (registered via UnaryAggregateDestructor with LEGACY destructor type).

  2. Combine merges two skiplists via MEOS's `temporal_tagg_combinefn`,
     which is exported from libmeos but not declared in the public
     headers, so we forward-declare it here. Same goes for the per-
     aggregate `datum_*` merge functors.

The MEOS combine semantics share Temporal* pointers between the two
input skiplists when both are non-empty: the returned skiplist holds
the merged elements while the "loser" skiplist still references the
same pointers. To avoid double-frees we walk the loser's elements and
NULL out keys/values before calling skiplist_free (which then only
releases the SkipList struct + freed[] array + elem array).

Implemented aggregates (single-input only — window aggregates and
extra base-type overloads are deferred):

  tand(tbool)             -> tbool
  tor(tbool)              -> tbool
  tcount(tbool|tint|tfloat|ttext|tgeompoint) -> tint
  tsum(tint|tfloat)       -> same input type
  tavg(tint|tfloat)       -> tfloat
  tcentroid(tgeompoint)   -> tgeompoint

Deferred this PR:
  - tmin / tmax — collide with the existing scalar Tmin(tbox|stbox) /
    Tmax(tbox|stbox) under DuckDB's case-insensitive name resolution.
    DuckDB rejects mixing scalar+aggregate overloads on the same
    canonical name (raises "GetAlterInfo not implemented for this type").
    Resolving needs either renaming the box scalars or aggregate
    suffixing — out of scope for this PR.
  - merge / appendInstant / appendSequence
  - spanUnion / setUnion (need different state shape — span/set
    skiplist instead of temporal)
  - Window aggregates (wmin / wmax / wsum / wcount / wavg) — need an
    extra interval argument
  - tcentroid 3D dispatch — currently always uses datum_sum_double3 (2D)
  - tcount over scalar timestamptz / tstzset / tstzspan / tstzspanset
    inputs (use timestamptz_tcount_transfn etc.)

Test plan: 031_aggregates_skiplist.test (14 assertions) — one per
registered aggregate plus NULL/empty handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…g/TmaxAgg

Rename every registered SkipList aggregate to its Pascal-cased *Agg
identifier per the proposal in MobilityDB issue #827 / PR #828:

  tand        -> TandAgg
  tor         -> TorAgg
  tcount      -> TcountAgg
  tsum        -> TsumAgg
  tavg        -> TavgAgg
  tcentroid   -> TcentroidAgg

The new naming convention disambiguates SkipList aggregates from the
same-stem scalar accessors (e.g. `Tmin(tbox)`) under DuckDB's case-
folding catalog. With the rename in place the previously-blocked
TminAgg / TmaxAgg now drop in cleanly, so this commit also adds them:

  TminAgg(tint | tfloat | ttext)  -> same-typed temporal min
  TmaxAgg(tint | tfloat | ttext)  -> same-typed temporal max

The MobilityDB upstream PR #828 lands the matching aliases on the PG
side; the original lowercase / camelCase names remain valid in PG for
backward compat but collide in DuckDB and are intentionally not
registered here.

Test 031_aggregates_skiplist.test exercises all 11 aggregates (4
TcountAgg overloads + TandAgg + TorAgg + 3 TminAgg + 3 TmaxAgg + 2
TsumAgg + 2 TavgAgg + TcentroidAgg) plus NULL handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tValueTimeTile + spatial

Adds the per-input lookup form of the tile family as a counterpart to
the LIST<TBOX>/LIST<STBOX> emitters previously shipped as
valueTiles / timeTiles / valueTimeTiles / spaceTiles / etc.

Temporal (3 names, 5 overloads):
  getValueTile(double, double [, double=0.0])           -> tbox
  getTBoxTimeTile(timestamptz, interval
                  [, timestamptz='2000-01-03'])         -> tbox
  getValueTimeTile(double, timestamptz, double, interval
                  [, double=0.0, timestamptz='2000-01-03']) -> tbox

Spatial (3 names, 6 overloads):
  getSpaceTile(geom, xsize)                             -> stbox  (uniform xyz)
  getSpaceTile(geom, xsize, ysize, zsize [, sorigin])   -> stbox
  getStboxTimeTile(timestamptz, interval
                  [, timestamptz='2000-01-03'])         -> stbox
  getSpaceTimeTile(geom, t, xsize, ysize, zsize, interval
                  [, sorigin, torigin])                 -> stbox

All temporal variants funnel through the MEOS internal
`tbox_get_value_time_tile`. The basetype/spantype hint to MEOS comes
from the first argument: T_FLOAT8/T_FLOATSPAN for value-bearing
variants, T_TIMESTAMPTZ/T_TSTZSPAN for the time-only getter (this
detail is what mirrors how the MobilityDB PG layer derives the basetype
from the first SQL arg in `Tbox_get_value_time_tile_common`).

Spatial variants call `stbox_get_space_tile` / `stbox_get_time_tile` /
`stbox_get_space_time_tile` directly with a default-cached
`Point(0 0 0)` GSERIALIZED used when no explicit sorigin is supplied.

Test 026_single_tile_getters.test exercises every shape (10 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dow aggs / time-only TcountAgg / TcentroidAgg 3D dispatch

Completes the SkipList-aggregate surface in scope for this PR.

Added (24 new aggregate overloads on top of the previous 11):

  MergeAgg(tbool|tint|tfloat|ttext|tgeompoint)             -> same input
  AppendInstantAgg(...)  / AppendSequenceAgg(...)          -> same input
    (Temporal-state harness, distinct from the SkipList-state harness;
     state holds a Temporal* directly and combine merges via temporal_merge.)
  SpanUnionAgg(<typed-span> | <typed-spanset>)             -> typed spanset
    (SpanSet-state harness via span_union_transfn / spanset_union_transfn /
     spanset_union_finalfn.)
  SetUnionAgg(<scalar> | <typed-set>)                      -> typed set
    (Set-state harness — value_union_transfn for scalars, set_union_transfn
     for sets. Forward-declares value_union_transfn from meos_internal.h.)
  WminAgg / WmaxAgg / WsumAgg / WavgAgg(tnumber, interval) -> same numeric type
  WcountAgg(tnumber, interval)                             -> tint
    (Binary-aggregate harness over (Temporal, Interval). WcountAgg uses
     temporal_wagg_transform_transfn + temporal_transform_wcount,
     forward-declared since they're not in installed public headers.)
  TcountAgg(timestamptz | tstzset | tstzspan | tstzspanset) -> tint
    (Distinct transfns: timestamptz_tcount_transfn / tstzset_*tcount_transfn /
     tstzspan_tcount_transfn / tstzspanset_tcount_transfn.)

Fixed: TcentroidAgg's 3D dispatch was reading the wrong byte of
SkipList::extra (treated the LSB of `srid` as `hasz`). Now reads
GeoAggregateState{int32 srid; bool hasz;} via a mirror struct.

Test 031_aggregates_skiplist.test grew from 18 to 35 assertions covering
every shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… / modifiers / comparison

Mirrors the temporal-generic portion of the tgeompoint surface for
tgeometry. Most MEOS C functions reached here are subtype-agnostic
(they take Temporal *), so the registrations reuse the same generic
TemporalFunctions::* handlers that tgeompoint uses, with TGEOMPOINT()
swapped for TGEOMETRY() and the value type swapped from POINT to
GEOMETRY.

Functions added (~50 overloads on top of the existing 22):

- Accessors: valueSet, valueN, valueAtTimestamp, getTime, duration,
  lowerInc, upperInc, numInstants, instants, numSequences, sequences,
  startSequence, endSequence, sequenceN, numTimestamps, timestamps,
  startTimestamp, endTimestamp, timestampN, segments
- Time-domain restrict: atTime / minusTime over (timestamptz, tstzset,
  tstzspan, tstzspanset); beforeTimestamp / afterTimestamp
- Value restrict: atValues / minusValues over (geometry, geomset)
- Spatial restrict: atGeometry, minusGeometry, minusStbox (reusing the
  tgeompoint-side Tgeo_* handlers — they already operate on Temporal *)
- Modifiers: shiftTime, scaleTime, shiftScaleTime, appendInstant,
  appendSequence, insert, update, deleteTime over multiple time
  argument shapes
- Comparison: temporal_eq / ne / lt / le / gt / ge / cmp + the matching
  =, <>, <, <=, >, >= operators

Out of scope for this PR (planned follow-ups):

- tgeometry comparison ops with geometry arg, distance / spatialrels
  / temporal-spatialrels (the bigger 060 / 064 / 070 / 072 SQL files)
- tgeometry tile family + analytics + aggregates + GiST / SP-GiST
- tgeography parity (entirely separate type registration)

Test: 040_tgeometry_foundations.test — 19 assertions covering each
category. Geometry values are emitted in EWKB-hex display rather than
WKT, mirroring how tgeompoint test outputs are compared.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ps / spatial-rels / temporal-rels / distance)

Extends the foundational tgeometry surface from the previous commit
with the cross-type predicate family that mirrors what MobilityDB
ships in 060 / 062 / 070 / 072 / 064_tgeo_*.in.sql:

  Box predicates (named function + operator):
    temporal_overlaps  / &&    over (tgeometry × stbox / tstzspan / tgeometry)
    temporal_contains  / @>    over the same cross-product
    temporal_contained / <@
    temporal_same      / ~=
    temporal_adjacent  / -|-

  Position predicates (16 names — left / right / below / above / front /
  back / before / after and their over-* variants) over (tgeometry ×
  stbox / tgeometry); the four time-axis ones (before / after /
  overbefore / overafter) also accept tstzspan.

  Spatial relationships:
    eContains / aContains          (tgeometry, geometry, tgeometry)
    eDisjoint / aDisjoint          commutative (geo, tgeo) reuses (tgeo, geo)
    eIntersects / aIntersects      same
    eTouches / aTouches            same
    eDwithin / aDwithin            with float distance argument

  Temporal spatial relationships (return tbool):
    tContains, tDisjoint, tIntersects, tTouches over (geo, tgeo) /
    (tgeo, geo) / (tgeo, tgeo); tDwithin with float distance.

  Distance:
    tdistance(tgeometry, geometry|tgeometry) -> tfloat
    <-> operator equivalents.

The MEOS exports reached here all take Temporal * (subtype-agnostic),
so this file is purely registration glue — templated executors decode
the input blobs, call the MEOS function, and re-encode the result.

Test 040_tgeometry_parity.test grew from 19 to 33 assertions and was
renamed from 040_tgeometry_foundations.test to reflect the broader
scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…orm / coercions / centroid / convexHull / traversedArea

Adds the spatial-functions surface from MobilityDB's
056_tgeo_spatialfuncs.in.sql for tgeometry:

  SRID(tgeometry)               -> integer
  setSRID(tgeometry, integer)   -> tgeometry
  transform(tgeometry, integer) -> tgeometry
  stbox(tgeometry)              -> stbox

  tgeompoint(tgeometry)  -> tgeompoint
  tgeometry(tgeompoint)  -> tgeometry

  centroid(tgeometry)               -> tgeometry  (temporal centroid path)
  convexHull(tgeometry)             -> geometry
  traversedArea(tgeometry [, bool]) -> geometry

All MEOS exports reached here (tspatial_srid / tspatial_set_srid /
tspatial_transform / tspatial_to_stbox / tgeometry_to_tgeompoint /
tgeompoint_to_tgeometry / tgeo_centroid / tgeo_convex_hull /
tgeo_traversed_area) take a Temporal *, so the new exec helpers in
tgeometry_ops.cpp follow the same decode/call/encode pattern as the
predicate executors above.

Tests: 040_tgeometry_parity.test grew from 33 to 39 assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the comprehensive tgeometry parity sweep with three more
categories on top of the cumulative base (which already includes
the foundational, predicate, and spatial-functions commits plus the
prerequisite work from PRs #85 / #86 / #87 / #88):

Aggregate wiring (in span_aggregates.cpp + temporal_aggregates.cpp):
  extent(tgeometry)         -> stbox    (new tgeometry input via
                                          tspatial_extent_transfn)
  TcountAgg(tgeometry)       -> tint
  MergeAgg(tgeometry)        -> tgeometry
  AppendInstantAgg(tgeometry)  -> tgeometry
  AppendSequenceAgg(tgeometry) -> tgeometry

Tile / box emitters (in tgeometry_ops.cpp):
  spaceBoxes(tgeometry, x, y, z)                  -> LIST<stbox>
  spaceTimeBoxes(tgeometry, x, y, z, interval)    -> LIST<stbox>

Analytics (in tgeometry_ops.cpp) — registered for parity even though
MEOS internally rejects non-numeric / non-point temporal inputs at
runtime (same constraint as MobilityDB's PG-side):
  minDistSimplify(tgeometry, double)
  minTimeDeltaSimplify(tgeometry, interval)
  maxDistSimplify(tgeometry, double[, bool])
  douglasPeuckerSimplify(tgeometry, double[, bool])

Test 040_tgeometry_parity.test grew from 39 to 45 assertions
covering aggregates and tile emitters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the geographic-coordinate-system temporal type tgeography. The MEOS
C functions backing this surface are subtype-agnostic, so the bulk of
this commit is a mechanical mirror of the tgeometry registrations:
search-and-replace TGeometryTypes -> TGeographyTypes, TGEOMETRY()
-> TGEOGRAPHY(), tgeometry_* -> tgeography_*, plus a small number of
case-by-case fixes for the tgeometry <-> tgeography coercions
(tgeogpoint coercions are deferred until tgeogpoint is registered).

Files added:

  src/include/geo/tgeography.hpp
  src/include/geo/tgeography_ops.hpp
  src/geo/tgeography.cpp           — type registration, constructors,
                                      foundational accessors, time/value
                                      restrict, modifiers, comparison
  src/geo/tgeography_in_out.cpp    — text I/O (asText, asEWKT) and
                                      string casts
  src/geo/tgeography_ops.cpp       — cross-type predicates (boxops,
                                      posops, spatial-rels, temporal-
                                      spatial-rels), distance,
                                      spatial functions, tile / box
                                      emitters, analytics
  test/sql/parity/041_tgeography_parity.test — 19 assertions

Aggregate wiring extended in span_aggregates.cpp /
temporal_aggregates.cpp to register tgeography overloads alongside
tgeometry / tgeompoint for extent / TcountAgg / MergeAgg /
AppendInstantAgg / AppendSequenceAgg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the geographic-coordinate-system point-restricted temporal type
tgeogpoint, completing the cross-product of (geometric, geographic) ×
(generic, point-restricted) temporal types alongside the already-shipped
tgeompoint / tgeometry / tgeography. Like tgeography → tgeometry, this
PR is largely a mechanical mirror: search-and-replace TGeographyTypes ->
TGeogpointType, TGEOGRAPHY() -> TGEOGPOINT(), tgeography_* ->
tgeogpoint_* in MEOS calls. The MEOS C functions backing this surface
are subtype-agnostic between tgeography and tgeogpoint.

Files added:

  src/include/geo/tgeogpoint.hpp
  src/include/geo/tgeogpoint_ops.hpp
  src/geo/tgeogpoint.cpp           — type registration, constructors,
                                      foundational accessors, time/value
                                      restrict, modifiers, comparison
  src/geo/tgeogpoint_in_out.cpp    — text I/O (asText, asEWKT) and casts
  src/geo/tgeogpoint_ops.cpp       — cross-type predicates (boxops,
                                      posops, spatial-rels, temporal-
                                      spatial-rels), distance, spatial
                                      functions, tile / box emitters,
                                      analytics, plus the
                                      tgeogpoint <-> tgeography coercion
                                      pair (uses
                                      tgeogpoint_to_tgeography and
                                      tgeography_to_tgeogpoint MEOS
                                      exports)
  test/sql/parity/042_tgeogpoint_parity.test — 19 assertions

Aggregate wiring extended in span_aggregates.cpp /
temporal_aggregates.cpp to register tgeogpoint overloads alongside
tgeography / tgeometry / tgeompoint for extent / TcountAgg / MergeAgg /
AppendInstantAgg / AppendSequenceAgg, plus TcentroidAgg(tgeogpoint)
since tgeogpoint is point-typed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Wasm CI (wasm32-emscripten) tripped on
`static_assert(sizeof(Datum) == sizeof(double))` in Float8ToDatum.
Datum is `uintptr_t` — 8 bytes on 64-bit native, 4 bytes on Wasm32 —
so the bit-pattern reinterpret can't store a double on Wasm.

Mirror PostgreSQL's `Float8GetDatum` shape: bit-cast on 64-bit Datum
builds, heap-allocate-and-pass-pointer on 32-bit Datum builds (MEOS'
set machinery copies the pointed-at value into the Set on insert
and frees it via set_free).

The `if` is a constant expression; the unused branch compiles away
on each target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@estebanzimanyi estebanzimanyi changed the base branch from feat/parity-tgeography to main May 1, 2026 18:55
@estebanzimanyi estebanzimanyi changed the title feat(bindings): tgeogpoint parity — full surface, mirrors tgeography feat(bindings): tgeometry + tgeography + tgeogpoint parity — cumulative (consolidates #90, #91) May 1, 2026
@estebanzimanyi estebanzimanyi marked this pull request as ready for review May 1, 2026 19:12
@estebanzimanyi
Copy link
Copy Markdown
Member Author

Superseded by the consolidated PR branch consolidate/geo-types-parity. All changes from this PR are included in that branch as a single squashed commit. Please review and merge the consolidated branch instead.

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