Skip to content

Connection.set_type_codec example for PostGIS geometry/geography types #245

@adamrothman

Description

@adamrothman

I imagine there are at least a few people (like me, lol) using PostgreSQL with PostGIS wondering if there's an elegant, precise way to encode geometry/geography types for queries.

A naive approach might be to use the string-based functions:

await conn.fetchrow(
    'INSERT INTO locations (name, coords) VALUES ($1, ST_GeographyFromText($2))',
    'Empire State Building',
    'Point(-73.985661 40.748447)',
)

These string-based functions have the potential for precision loss (due to string conversions), and there are faster alternatives available, like ST_MakePoint:

ST_MakePoint while not being OGC compliant is generally faster and more precise than ST_GeomFromText and ST_PointFromText. It is also easier to use if you have raw coordinates rather than WKT.

You could do something like this:

await conn.fetchrow(
    'INSERT INTO locations (name, coords) VALUES ($1, ST_MakePoint($2, $3))',
    'Empire State Building',
    -73.985661,
    40.748447,
)

Better, but specific to the point type.

A fully general solution that suffers no loss of precision can be implemented using Connection.set_type_codec. Geometry/geography objects are encoded in the well-known binary format. It works for any Python object that conforms to the geo interface specification, and relies on the excellent Shapely library (but anything that speaks WKB will do).

from typing import Any

import shapely.geometry
import shapely.wkb
from shapely.geometry.base import BaseGeometry


def encode_geometry(geometry: Any) -> bytes:
    if not hasattr(geometry, '__geo_interface__'):
        raise TypeError(f'{geometry} does not conform to geo interface')
    shape = shapely.geometry.asShape(geometry)
    return shapely.wkb.dumps(shape)


def decode_geometry(wkb: bytes) -> BaseGeometry:
    return shapely.wkb.loads(wkb)


await conn.set_type_codec(
    'geometry',  # also works for 'geography'
    encoder=encode_geometry,
    decoder=decode_geometry,
    format='binary',
)

With a connection configured as above, the query is as simple as:

await conn.fetchrow(
    'INSERT INTO locations (name, coords) VALUES ($1, $2)',
    'Empire State Building',
    shapely.geometry.Point(-73.985661, 40.748447),  # or any other geo-interface compliant type
)

And it works for polygons and line strings too!

This may be rather niche but it took me a good deal of trial-and-error to figure out. It would be really cool to see an example like the above in the documentation. Maybe alongside the JSON example under Custom Type Conversions?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions