Repository: geopython/pygeoapi
Pull request #1979: Add provider for MVT tile generation from Postgres
Author: login: ThorodanBrom
Labels: name: OGC API - Tiles
description: OGC API - Tiles
Created: 2025-03-27T14:15:42Z | Merged: 2025-05-27T02:50:33Z

# Overview

This PR adds MVT tile generation from Postgres/PostGIS tables natively.  

# Related Issue / discussion

This is related to issue #1978 

<!--

Is there an existing Issue that this PR addresses?  Does this PR need a new Issue?

Non-trivial PRs are best put forth initially as an Issue so that there can be
discussion and consensus before a PR is put forth.

-->

# Additional information

# Dependency policy (RFC2)

- [x] I have ensured that this PR meets [RFC2](https://pygeoapi.io/development/rfc/2) requirements

# Updates to public demo

- [x] I have ensured that breaking changes to the [pygeoapi master demo server](https://github.com/geopython/demo.pygeoapi.io) have been addressed
  - [x] https://github.com/geopython/demo.pygeoapi.io/blob/master/services/pygeoapi_master/local.config.yml

# Contributions and licensing

(as per https://github.com/geopython/pygeoapi/blob/master/CONTRIBUTING.md#contributions-and-licensing)

- [x] I'd like to contribute this feature to pygeoapi. I confirm that my contributions to pygeoapi will be compatible with the pygeoapi license guidelines at the time of contribution
- [x] I have already previously agreed to the pygeoapi Contributions and Licensing Guidelines



Diff:
diff --git a/docs/source/data-publishing/ogcapi-tiles.rst b/docs/source/data-publishing/ogcapi-tiles.rst
index 3f669061a..66ab76c91 100644
--- a/docs/source/data-publishing/ogcapi-tiles.rst
+++ b/docs/source/data-publishing/ogcapi-tiles.rst
@@ -22,6 +22,7 @@ pygeoapi core tile providers are listed below, along with supported features.
    `MVT-elastic`_,✅,✅,✅,❌,❌,✅
    `MVT-proxy`_,❓,❓,❓,❓,❌,✅
    `WMTSFacade`_,✅,❌,✅,✅,✅,❌
+   `MVT-postgresql`_,✅,✅,✅,✅,❌,✅
 
 Below are specific connection examples based on supported providers.
 
@@ -130,6 +131,47 @@ Following code block shows how to configure pygeoapi to read Mapbox vector tiles
              name: pbf
              mimetype: application/vnd.mapbox-vector-tile
 
+MVT-postgresql
+^^^^^^^^^^^^^^
+
+.. note::
+   Requires Python packages sqlalchemy, geoalchemy2 and psycopg2-binary
+
+.. note::
+   Must have PostGIS installed with protobuf-c support 
+
+.. note::
+   Geometry must be using EPSG:4326
+
+This provider gives support to serving tiles generated using `PostgreSQL <https://www.postgresql.org/>`_ with `PostGIS <https://postgis.net/>`_.
+The tiles are rendered on-the-fly using `ST_AsMVT <https://postgis.net/docs/ST_AsMVT.html>`_ and related methods.
+
+This code block shows how to configure pygeoapi to render Mapbox vector tiles from a PostGIS table.
+
+.. code-block:: yaml
+
+   providers:
+       - type: tile
+           name: MVT-postgresql
+           data:
+               host: 127.0.0.1
+               port: 3010 # Default 5432 if not provided
+               dbname: test
+               user: postgres
+               password: postgres
+               search_path: [osm, public]
+           id_field: osm_id
+           table: hotosm_bdi_waterways
+           geom_field: foo_geom
+           options:
+               zoom:
+                   min: 0
+                   max: 15
+           format:
+               name: pbf
+               mimetype: application/vnd.mapbox-vector-tile
+
+PostgreSQL-related connection options can also be added to `options`. Please refer to the :ref:`PostgreSQL OGC Features Provider<PostgreSQL>` documentation for more information.
 
 WMTSFacade
 ^^^^^^^^^^
diff --git a/pygeoapi/plugin.py b/pygeoapi/plugin.py
index 08324cf64..620462a34 100644
--- a/pygeoapi/plugin.py
+++ b/pygeoapi/plugin.py
@@ -53,6 +53,7 @@
         'MVT-tippecanoe': 'pygeoapi.provider.mvt_tippecanoe.MVTTippecanoeProvider',  # noqa: E501
         'MVT-elastic': 'pygeoapi.provider.mvt_elastic.MVTElasticProvider',
         'MVT-proxy': 'pygeoapi.provider.mvt_proxy.MVTProxyProvider',
+        'MVT-postgresql': 'pygeoapi.provider.mvt_postgresql.MVTPostgreSQLProvider',  # noqa: E501
         'OracleDB': 'pygeoapi.provider.oracle.OracleProvider',
         'OGR': 'pygeoapi.provider.ogr.OGRProvider',
         'OpenSearch': 'pygeoapi.provider.opensearch_.OpenSearchProvider',
diff --git a/pygeoapi/provider/base_mvt.py b/pygeoapi/provider/base_mvt.py
index 12e10155f..e2d096170 100644
--- a/pygeoapi/provider/base_mvt.py
+++ b/pygeoapi/provider/base_mvt.py
@@ -95,7 +95,8 @@ def get_tiles_service(self, baseurl=None, servicepath=None,
         :returns: `dict` of item tile service
         """
 
-        url = urlparse(self.data)
+        # self.data will be a dict when using MVTPostgresProvider
+        url = urlparse(self.data) if isinstance(self.data, str) else urlparse('/') # noqa
         baseurl = baseurl or f'{url.scheme}://{url.netloc}'
         # @TODO: support multiple types
         tile_type = tile_type or self.format_type
diff --git a/pygeoapi/provider/mvt_postgresql.py b/pygeoapi/provider/mvt_postgresql.py
new file mode 100644
index 000000000..61eda81df
--- /dev/null
+++ b/pygeoapi/provider/mvt_postgresql.py
@@ -0,0 +1,264 @@
+# =================================================================
+#
+# Authors: Prajwal Amaravati <prajwal.s@satsure.co>
+#          Tanvi Prasad <tanvi.prasad@cdpg.org.in>
+#          Bryan Robert <bryan.robert@cdpg.org.in>
+#
+# Copyright (c) 2025 Prajwal Amaravati
+# Copyright (c) 2025 Tanvi Prasad
+# Copyright (c) 2025 Bryan Robert
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+from copy import deepcopy
+import logging
+
+from sqlalchemy.sql import text
+
+from pygeoapi.models.provider.base import (
+    TileSetMetadata, TileMatrixSetEnum, LinkType)
+from pygeoapi.provider.base import ProviderConnectionError
+from pygeoapi.provider.base_mvt import BaseMVTProvider
+from pygeoapi.provider.postgresql import PostgreSQLProvider
+from pygeoapi.provider.tile import ProviderTileNotFoundError
+from pygeoapi.util import url_join
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MVTPostgreSQLProvider(BaseMVTProvider):
+    """
+    MVT PostgreSQL Provider
+    Provider for serving tiles rendered on-the-fly from
+    feature tables in PostgreSQL
+    """
+
+    def __init__(self, provider_def):
+        """
+        Initialize object
+
+        :param provider_def: provider definition
+
+        :returns: pygeoapi.provider.MVT.MVTPostgreSQLProvider
+        """
+
+        super().__init__(provider_def)
+
+        pg_def = deepcopy(provider_def)
+        # delete the zoom option before initializing the PostgreSQL provider
+        # that provider breaks otherwise
+        del pg_def["options"]["zoom"]
+        self.postgres = PostgreSQLProvider(pg_def)
+
+        self.layer_name = provider_def["table"]
+        self.table = provider_def['table']
+        self.id_field = provider_def['id_field']
+        self.geom = provider_def.get('geom_field', 'geom')
+
+        LOGGER.debug(f'DB connection: {repr(self.postgres._engine.url)}')
+
+    def __repr__(self):
+        return f'<MVTPostgreSQLProvider> {self.data}'
+
+    @property
+    def service_url(self):
+        return self._service_url
+
+    @property
+    def service_metadata_url(self):
+        return self._service_metadata_url
+
+    def get_layer(self):
+        """
+        Extracts layer name from url
+
+        :returns: layer name
+        """
+
+        return self.layer_name
+
+    def get_tiling_schemes(self):
+
+        return [
+                TileMatrixSetEnum.WEBMERCATORQUAD.value,
+                TileMatrixSetEnum.WORLDCRS84QUAD.value
+            ]
+
+    def get_tiles_service(self, baseurl=None, servicepath=None,
+                          dirpath=None, tile_type=None):
+        """
+        Gets mvt service description
+
+        :param baseurl: base URL of endpoint
+        :param servicepath: base path of URL
+        :param dirpath: directory basepath (equivalent of URL)
+        :param tile_type: tile format type
+
+        :returns: `dict` of item tile service
+        """
+
+        super().get_tiles_service(baseurl, servicepath,
+                                  dirpath, tile_type)
+
+        self._service_url = servicepath
+        return self.get_tms_links()
+
+    def get_tiles(self, layer=None, tileset=None,
+                  z=None, y=None, x=None, format_=None):
+        """
+        Gets tile
+
+        :param layer: mvt tile layer
+        :param tileset: mvt tileset
+        :param z: z index
+        :param y: y index
+        :param x: x index
+        :param format_: tile format
+
+        :returns: an encoded mvt tile
+        """
+        if format_ == 'mvt':
+            format_ = self.format_type
+
+        fields_arr = self.postgres.get_fields().keys()
+        fields = ', '.join(['"' + f + '"' for f in fields_arr])
+        if len(fields) != 0:
+            fields = ',' + fields
+
+        query = ''
+        if tileset == TileMatrixSetEnum.WEBMERCATORQUAD.value.tileMatrixSet:
+            if not self.is_in_limits(TileMatrixSetEnum.WEBMERCATORQUAD.value, z, x, y): # noqa
+                raise ProviderTileNotFoundError
+
+            query = text("""
+                WITH
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :x, :y) AS boundgeom
+                    ),
+                    mvtgeom AS (
+                        SELECT ST_AsMVTGeom(ST_Transform(ST_CurveToLine({geom}), 3857), bounds.boundgeom) AS geom {fields}
+                        FROM "{table}", bounds
+                        WHERE ST_Intersects({geom}, ST_Transform(bounds.boundgeom, 4326))
+                    )
+                SELECT ST_AsMVT(mvtgeom, 'default') FROM mvtgeom;
+            """.format(geom=self.geom, table=self.table, fields=fields)) # noqa
+
+        if tileset == TileMatrixSetEnum.WORLDCRS84QUAD.value.tileMatrixSet:
+            if not self.is_in_limits(TileMatrixSetEnum.WORLDCRS84QUAD.value, z, x, y): # noqa
+                raise ProviderTileNotFoundError
+
+            query = text("""
+                WITH
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :x, :y,
+                                'SRID=4326;POLYGON((-180 -90,-180 90,180 90,180 -90,-180 -90))'::geometry) AS boundgeom
+                    ),
+                    mvtgeom AS (
+                        SELECT ST_AsMVTGeom(ST_CurveToLine({geom}), bounds.boundgeom) AS geom {fields}
+                        FROM "{table}", bounds
+                        WHERE ST_Intersects({geom}, bounds.boundgeom)
+                    )
+                SELECT ST_AsMVT(mvtgeom, 'default') FROM mvtgeom;
+            """.format(geom=self.geom, table=self.table, fields=fields)) # noqa
+
+        with self.postgres._engine.connect() as session:
+            result = session.execute(query, {
+                    'z': z,
+                    'y': y,
+                    'x': x
+            }).fetchone()
+
+            if len(bytes(result[0])) == 0:
+                return None
+            return bytes(result[0])
+
+    def get_html_metadata(self, dataset, server_url, layer, tileset,
+                          title, description, keywords, **kwargs):
+
+        service_url = url_join(
+            server_url,
+            f'collections/{dataset}/tiles/{tileset}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f=mvt')  # noqa
+        metadata_url = url_join(
+            server_url,
+            f'collections/{dataset}/tiles/{tileset}/metadata')
+
+        metadata = dict()
+        metadata['id'] = dataset
+        metadata['title'] = title
+        metadata['tileset'] = tileset
+        metadata['collections_path'] = service_url
+        metadata['json_url'] = f'{metadata_url}?f=json'
+
+        return metadata
+
+    def get_default_metadata(self, dataset, server_url, layer, tileset,
+                             title, description, keywords, **kwargs):
+
+        service_url = url_join(
+            server_url,
+            f'collections/{dataset}/tiles/{tileset}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f=mvt')  # noqa
+
+        content = {}
+        tiling_schemes = self.get_tiling_schemes()
+        # Default values
+        tileMatrixSetURI = tiling_schemes[0].tileMatrixSetURI
+        crs = tiling_schemes[0].crs
+        # Checking the selected matrix in configured tiling_schemes
+        for schema in tiling_schemes:
+            if (schema.tileMatrixSet == tileset):
+                crs = schema.crs
+                tileMatrixSetURI = schema.tileMatrixSetURI
+
+                tiling_scheme_url = url_join(
+                    server_url, f'/TileMatrixSets/{schema.tileMatrixSet}')
+                tiling_scheme_url_type = "application/json"
+                tiling_scheme_url_title = f'{schema.tileMatrixSet} tile matrix set definition' # noqa
+
+                tiling_scheme = LinkType(href=tiling_scheme_url,
+                                         rel="http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", # noqa
+                                         type_=tiling_scheme_url_type,
+                                         title=tiling_scheme_url_title)
+
+        if tiling_scheme is None:
+            msg = f'Could not identify a valid tiling schema'  # noqa
+            LOGGER.error(msg)
+            raise ProviderConnectionError(msg)
+
+        content = TileSetMetadata(title=title, description=description,
+                                  keywords=keywords, crs=crs,
+                                  tileMatrixSetURI=tileMatrixSetURI)
+
+        links = []
+        service_url_link_type = "application/vnd.mapbox-vector-tile"
+        service_url_link_title = f'{tileset} vector tiles for {layer}'
+        service_url_link = LinkType(href=service_url, rel="item",
+                                    type_=service_url_link_type,
+                                    title=service_url_link_title)
+
+        links.append(tiling_scheme)
+        links.append(service_url_link)
+
+        content.links = links
+
+        return content.dict(exclude_none=True)


Reviews:
user: login: doublebyte1
body: Thank You @ThorodanBrom ! Would you be able to add a Postgres Provider docker example at pygeoapi-examples? https://github.com/geopython/pygeoapi-examples/tree/main/docker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T10:52:55Z
user: login: doublebyte1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T11:24:16Z
user: login: ThorodanBrom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T12:05:06Z
user: login: ThorodanBrom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T12:15:51Z
user: login: doublebyte1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T12:45:38Z
user: login: doublebyte1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T12:48:19Z
user: login: ThorodanBrom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T19:06:58Z
user: login: ThorodanBrom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T19:07:38Z
user: login: doublebyte1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:21:04Z
user: login: doublebyte1
body: Please fix the format of the example in the documentation, and I think we are good to go. 

You can also click on the check-boxes that say "Updates to public demo", as this provider is not used in the configuration file of the demo.

Thank You!
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:30:05Z
user: login: doublebyte1
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T12:37:13Z
user: login: tomkralidis
body: Great work here!  Minor change requests on naming consistency.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-11T13:52:30Z
user: login: photonbit
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-21T11:29:51Z
user: login: tomkralidis
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-27T02:50:23Z

Review comments:
diff_hunk: @@ -0,0 +1,263 @@
+# =================================================================
+#
+# Authors: 
+#
+# Copyright (c)
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+import logging
+import requests
+from urllib.parse import urlparse
+
+from pygeoapi.provider.base_mvt import BaseMVTProvider
+from pygeoapi.provider.postgresql import PostgreSQLProvider
+from pygeoapi.provider.base import (ProviderConnectionError,
+                                    ProviderGenericError,
+                                    ProviderInvalidQueryError)
+from pygeoapi.provider.tile import ProviderTileNotFoundError
+from pygeoapi.models.provider.base import (
+    TileSetMetadata, TileMatrixSetEnum, LinkType)
+from pygeoapi.util import is_url, url_join
+from sqlalchemy.orm import Session, load_only
+
+from sqlalchemy.engine import URL
+from sqlalchemy.sql import text
+from sqlalchemy.exc import ConstraintColumnNotFoundError, \
+    InvalidRequestError, OperationalError
+from sqlalchemy.ext.automap import automap_base
+from sqlalchemy.sql.expression import and_
+from pygeoapi.provider.base import BaseProvider, \
+    ProviderConnectionError, ProviderInvalidDataError, ProviderQueryError, \
+    ProviderItemNotFoundError
+    
+from copy import deepcopy
+from datetime import datetime
+from decimal import Decimal
+import functools
+import logging
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MVTPostgresProvider(BaseMVTProvider):
+
+    def __init__(self, provider_def):
+        """
+        Initialize object
+
+        :param provider_def: provider definition
+
+        :returns: pygeoapi.provider.MVT.MVTPostgresProvider
+        """
+
+        super().__init__(provider_def)
+        
+        pg_def = deepcopy(provider_def)
+        del pg_def["options"]["zoom"]
+        self.postgres = PostgreSQLProvider(pg_def)
+        self.layer_name = provider_def["table"]
+        self.table = provider_def['table']
+        self.id_field = provider_def['id_field']
+        self.geom = provider_def.get('geom_field', 'geom')
+
+        LOGGER.debug(f'DB connection: {repr(self.postgres._engine.url)}')
+
+    def __repr__(self):
+        return f'<MVTPostgresProvider> {self.data}'
+
+    @property
+    def service_url(self):
+        return self._service_url
+
+    @property
+    def service_metadata_url(self):
+        return self._service_metadata_url
+
+    def get_layer(self):
+        """
+        Extracts layer name from url
+
+        :returns: layer name
+        """
+
+        return self.layer_name
+
+    def get_tiling_schemes(self):
+
+        return [
+                TileMatrixSetEnum.WEBMERCATORQUAD.value,
+                TileMatrixSetEnum.WORLDCRS84QUAD.value
+            ]
+
+    def get_tiles_service(self, baseurl=None, servicepath=None,
+                          dirpath=None, tile_type=None):
+        """
+        Gets mvt service description
+
+        :param baseurl: base URL of endpoint
+        :param servicepath: base path of URL
+        :param dirpath: directory basepath (equivalent of URL)
+        :param tile_type: tile format type
+
+        :returns: `dict` of item tile service
+        """
+
+        super().get_tiles_service(baseurl, servicepath,
+                                  dirpath, tile_type)
+
+        self._service_url = servicepath
+        return self.get_tms_links()
+
+    def get_tiles(self, layer=None, tileset=None,
+                  z=None, y=None, x=None, format_=None):
+        """
+        Gets tile
+
+        :param layer: mvt tile layer
+        :param tileset: mvt tileset
+        :param z: z index
+        :param y: y index
+        :param x: x index
+        :param format_: tile format
+
+        :returns: an encoded mvt tile
+        """
+        if format_ == 'mvt':
+            format_ = self.format_type
+
+        fields_arr = self.postgres.get_fields().keys()
+        fields = ', '.join(['"' + f + '"' for f in fields_arr])
+        if len(fields) != 0:
+            fields = ',' + fields
+        
+        query = ''
+        if tileset == TileMatrixSetEnum.WEBMERCATORQUAD.value.tileMatrixSet:
+            query = text("""
+                WITH                                                                  
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :y, :x) AS boundgeom
+                    ),
+                    mvtgeom AS (
+                        SELECT ST_AsMVTGeom(ST_Transform({geom}, 3857), bounds.boundgeom) AS geom {fields}
+                        FROM "{table}", bounds
+                        WHERE ST_Intersects({geom}, ST_Transform(bounds.boundgeom, 4326))
+                    )
+                SELECT ST_AsMVT(mvtgeom, 'default') FROM mvtgeom;
+            """.format(geom=self.geom, table=self.table, fields=fields))  
+        
+        if tileset == TileMatrixSetEnum.WORLDCRS84QUAD.value.tileMatrixSet:
+            query = text("""
+                WITH                                                                  
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :y, :x, ST_MakeEnvelope(-180, -90, 180, 90, 4326)) AS boundgeom
path: pygeoapi/provider/mvt_postgres.py
user: login: doublebyte1
body: Why the tile envelope here is based on the extent of the crs, instead of the extent of the geometry (like in the case of WebMercator)?
created_at: 2025-04-19T11:06:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,263 @@
+# =================================================================
+#
+# Authors: 
+#
+# Copyright (c)
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+import logging
+import requests
+from urllib.parse import urlparse
+
+from pygeoapi.provider.base_mvt import BaseMVTProvider
+from pygeoapi.provider.postgresql import PostgreSQLProvider
+from pygeoapi.provider.base import (ProviderConnectionError,
+                                    ProviderGenericError,
+                                    ProviderInvalidQueryError)
+from pygeoapi.provider.tile import ProviderTileNotFoundError
+from pygeoapi.models.provider.base import (
+    TileSetMetadata, TileMatrixSetEnum, LinkType)
+from pygeoapi.util import is_url, url_join
+from sqlalchemy.orm import Session, load_only
+
+from sqlalchemy.engine import URL
+from sqlalchemy.sql import text
+from sqlalchemy.exc import ConstraintColumnNotFoundError, \
+    InvalidRequestError, OperationalError
+from sqlalchemy.ext.automap import automap_base
+from sqlalchemy.sql.expression import and_
+from pygeoapi.provider.base import BaseProvider, \
+    ProviderConnectionError, ProviderInvalidDataError, ProviderQueryError, \
+    ProviderItemNotFoundError
+    
+from copy import deepcopy
+from datetime import datetime
+from decimal import Decimal
+import functools
+import logging
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MVTPostgresProvider(BaseMVTProvider):
+
+    def __init__(self, provider_def):
+        """
+        Initialize object
+
+        :param provider_def: provider definition
+
+        :returns: pygeoapi.provider.MVT.MVTPostgresProvider
+        """
+
+        super().__init__(provider_def)
+        
+        pg_def = deepcopy(provider_def)
+        del pg_def["options"]["zoom"]
+        self.postgres = PostgreSQLProvider(pg_def)
+        self.layer_name = provider_def["table"]
+        self.table = provider_def['table']
+        self.id_field = provider_def['id_field']
+        self.geom = provider_def.get('geom_field', 'geom')
+
+        LOGGER.debug(f'DB connection: {repr(self.postgres._engine.url)}')
+
+    def __repr__(self):
+        return f'<MVTPostgresProvider> {self.data}'
+
+    @property
+    def service_url(self):
+        return self._service_url
+
+    @property
+    def service_metadata_url(self):
+        return self._service_metadata_url
+
+    def get_layer(self):
+        """
+        Extracts layer name from url
+
+        :returns: layer name
+        """
+
+        return self.layer_name
+
+    def get_tiling_schemes(self):
+
+        return [
+                TileMatrixSetEnum.WEBMERCATORQUAD.value,
+                TileMatrixSetEnum.WORLDCRS84QUAD.value
+            ]
+
+    def get_tiles_service(self, baseurl=None, servicepath=None,
+                          dirpath=None, tile_type=None):
+        """
+        Gets mvt service description
+
+        :param baseurl: base URL of endpoint
+        :param servicepath: base path of URL
+        :param dirpath: directory basepath (equivalent of URL)
+        :param tile_type: tile format type
+
+        :returns: `dict` of item tile service
+        """
+
+        super().get_tiles_service(baseurl, servicepath,
+                                  dirpath, tile_type)
+
+        self._service_url = servicepath
+        return self.get_tms_links()
+
+    def get_tiles(self, layer=None, tileset=None,
+                  z=None, y=None, x=None, format_=None):
+        """
+        Gets tile
+
+        :param layer: mvt tile layer
+        :param tileset: mvt tileset
+        :param z: z index
+        :param y: y index
+        :param x: x index
+        :param format_: tile format
+
+        :returns: an encoded mvt tile
+        """
+        if format_ == 'mvt':
+            format_ = self.format_type
+
+        fields_arr = self.postgres.get_fields().keys()
+        fields = ', '.join(['"' + f + '"' for f in fields_arr])
+        if len(fields) != 0:
+            fields = ',' + fields
+        
+        query = ''
+        if tileset == TileMatrixSetEnum.WEBMERCATORQUAD.value.tileMatrixSet:
+            query = text("""
+                WITH                                                                  
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :y, :x) AS boundgeom
+                    ),
+                    mvtgeom AS (
+                        SELECT ST_AsMVTGeom(ST_Transform({geom}, 3857), bounds.boundgeom) AS geom {fields}
path: pygeoapi/provider/mvt_postgres.py
user: login: doublebyte1
body: I think converting curves to lines (ST_CurveToLine), could speed up queries in the case of more complex geometries.
created_at: 2025-04-19T11:08:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,263 @@
+# =================================================================
+#
+# Authors: 
+#
+# Copyright (c)
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+import logging
+import requests
+from urllib.parse import urlparse
+
+from pygeoapi.provider.base_mvt import BaseMVTProvider
+from pygeoapi.provider.postgresql import PostgreSQLProvider
+from pygeoapi.provider.base import (ProviderConnectionError,
+                                    ProviderGenericError,
+                                    ProviderInvalidQueryError)
+from pygeoapi.provider.tile import ProviderTileNotFoundError
+from pygeoapi.models.provider.base import (
+    TileSetMetadata, TileMatrixSetEnum, LinkType)
+from pygeoapi.util import is_url, url_join
+from sqlalchemy.orm import Session, load_only
+
+from sqlalchemy.engine import URL
+from sqlalchemy.sql import text
+from sqlalchemy.exc import ConstraintColumnNotFoundError, \
+    InvalidRequestError, OperationalError
+from sqlalchemy.ext.automap import automap_base
+from sqlalchemy.sql.expression import and_
+from pygeoapi.provider.base import BaseProvider, \
+    ProviderConnectionError, ProviderInvalidDataError, ProviderQueryError, \
+    ProviderItemNotFoundError
+    
+from copy import deepcopy
+from datetime import datetime
+from decimal import Decimal
+import functools
+import logging
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MVTPostgresProvider(BaseMVTProvider):
+
+    def __init__(self, provider_def):
+        """
+        Initialize object
+
+        :param provider_def: provider definition
+
+        :returns: pygeoapi.provider.MVT.MVTPostgresProvider
+        """
+
+        super().__init__(provider_def)
+        
+        pg_def = deepcopy(provider_def)
+        del pg_def["options"]["zoom"]
+        self.postgres = PostgreSQLProvider(pg_def)
+        self.layer_name = provider_def["table"]
+        self.table = provider_def['table']
+        self.id_field = provider_def['id_field']
+        self.geom = provider_def.get('geom_field', 'geom')
+
+        LOGGER.debug(f'DB connection: {repr(self.postgres._engine.url)}')
+
+    def __repr__(self):
+        return f'<MVTPostgresProvider> {self.data}'
+
+    @property
+    def service_url(self):
+        return self._service_url
+
+    @property
+    def service_metadata_url(self):
+        return self._service_metadata_url
+
+    def get_layer(self):
+        """
+        Extracts layer name from url
+
+        :returns: layer name
+        """
+
+        return self.layer_name
+
+    def get_tiling_schemes(self):
+
+        return [
+                TileMatrixSetEnum.WEBMERCATORQUAD.value,
+                TileMatrixSetEnum.WORLDCRS84QUAD.value
+            ]
+
+    def get_tiles_service(self, baseurl=None, servicepath=None,
+                          dirpath=None, tile_type=None):
+        """
+        Gets mvt service description
+
+        :param baseurl: base URL of endpoint
+        :param servicepath: base path of URL
+        :param dirpath: directory basepath (equivalent of URL)
+        :param tile_type: tile format type
+
+        :returns: `dict` of item tile service
+        """
+
+        super().get_tiles_service(baseurl, servicepath,
+                                  dirpath, tile_type)
+
+        self._service_url = servicepath
+        return self.get_tms_links()
+
+    def get_tiles(self, layer=None, tileset=None,
+                  z=None, y=None, x=None, format_=None):
+        """
+        Gets tile
+
+        :param layer: mvt tile layer
+        :param tileset: mvt tileset
+        :param z: z index
+        :param y: y index
+        :param x: x index
+        :param format_: tile format
+
+        :returns: an encoded mvt tile
+        """
+        if format_ == 'mvt':
+            format_ = self.format_type
+
+        fields_arr = self.postgres.get_fields().keys()
+        fields = ', '.join(['"' + f + '"' for f in fields_arr])
+        if len(fields) != 0:
+            fields = ',' + fields
+        
+        query = ''
+        if tileset == TileMatrixSetEnum.WEBMERCATORQUAD.value.tileMatrixSet:
+            query = text("""
+                WITH                                                                  
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :y, :x) AS boundgeom
+                    ),
+                    mvtgeom AS (
+                        SELECT ST_AsMVTGeom(ST_Transform({geom}, 3857), bounds.boundgeom) AS geom {fields}
path: pygeoapi/provider/mvt_postgres.py
user: login: ThorodanBrom
body: OK, I will try this out, [it looks like Martin also uses it](https://maplibre.org/martin/sources-pg-functions.html#simple-function). 

So by using `ST_CurveToLine`, will this provider also be able to support `CIRCULARSTRING`-like data types (that is what I understood from reading the [documentation](https://postgis.net/docs/ST_CurveToLine.html))? Or is it just going to be used for query performance?
created_at: 2025-04-21T12:05:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,263 @@
+# =================================================================
+#
+# Authors: 
+#
+# Copyright (c)
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+import logging
+import requests
+from urllib.parse import urlparse
+
+from pygeoapi.provider.base_mvt import BaseMVTProvider
+from pygeoapi.provider.postgresql import PostgreSQLProvider
+from pygeoapi.provider.base import (ProviderConnectionError,
+                                    ProviderGenericError,
+                                    ProviderInvalidQueryError)
+from pygeoapi.provider.tile import ProviderTileNotFoundError
+from pygeoapi.models.provider.base import (
+    TileSetMetadata, TileMatrixSetEnum, LinkType)
+from pygeoapi.util import is_url, url_join
+from sqlalchemy.orm import Session, load_only
+
+from sqlalchemy.engine import URL
+from sqlalchemy.sql import text
+from sqlalchemy.exc import ConstraintColumnNotFoundError, \
+    InvalidRequestError, OperationalError
+from sqlalchemy.ext.automap import automap_base
+from sqlalchemy.sql.expression import and_
+from pygeoapi.provider.base import BaseProvider, \
+    ProviderConnectionError, ProviderInvalidDataError, ProviderQueryError, \
+    ProviderItemNotFoundError
+    
+from copy import deepcopy
+from datetime import datetime
+from decimal import Decimal
+import functools
+import logging
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MVTPostgresProvider(BaseMVTProvider):
+
+    def __init__(self, provider_def):
+        """
+        Initialize object
+
+        :param provider_def: provider definition
+
+        :returns: pygeoapi.provider.MVT.MVTPostgresProvider
+        """
+
+        super().__init__(provider_def)
+        
+        pg_def = deepcopy(provider_def)
+        del pg_def["options"]["zoom"]
+        self.postgres = PostgreSQLProvider(pg_def)
+        self.layer_name = provider_def["table"]
+        self.table = provider_def['table']
+        self.id_field = provider_def['id_field']
+        self.geom = provider_def.get('geom_field', 'geom')
+
+        LOGGER.debug(f'DB connection: {repr(self.postgres._engine.url)}')
+
+    def __repr__(self):
+        return f'<MVTPostgresProvider> {self.data}'
+
+    @property
+    def service_url(self):
+        return self._service_url
+
+    @property
+    def service_metadata_url(self):
+        return self._service_metadata_url
+
+    def get_layer(self):
+        """
+        Extracts layer name from url
+
+        :returns: layer name
+        """
+
+        return self.layer_name
+
+    def get_tiling_schemes(self):
+
+        return [
+                TileMatrixSetEnum.WEBMERCATORQUAD.value,
+                TileMatrixSetEnum.WORLDCRS84QUAD.value
+            ]
+
+    def get_tiles_service(self, baseurl=None, servicepath=None,
+                          dirpath=None, tile_type=None):
+        """
+        Gets mvt service description
+
+        :param baseurl: base URL of endpoint
+        :param servicepath: base path of URL
+        :param dirpath: directory basepath (equivalent of URL)
+        :param tile_type: tile format type
+
+        :returns: `dict` of item tile service
+        """
+
+        super().get_tiles_service(baseurl, servicepath,
+                                  dirpath, tile_type)
+
+        self._service_url = servicepath
+        return self.get_tms_links()
+
+    def get_tiles(self, layer=None, tileset=None,
+                  z=None, y=None, x=None, format_=None):
+        """
+        Gets tile
+
+        :param layer: mvt tile layer
+        :param tileset: mvt tileset
+        :param z: z index
+        :param y: y index
+        :param x: x index
+        :param format_: tile format
+
+        :returns: an encoded mvt tile
+        """
+        if format_ == 'mvt':
+            format_ = self.format_type
+
+        fields_arr = self.postgres.get_fields().keys()
+        fields = ', '.join(['"' + f + '"' for f in fields_arr])
+        if len(fields) != 0:
+            fields = ',' + fields
+        
+        query = ''
+        if tileset == TileMatrixSetEnum.WEBMERCATORQUAD.value.tileMatrixSet:
+            query = text("""
+                WITH                                                                  
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :y, :x) AS boundgeom
+                    ),
+                    mvtgeom AS (
+                        SELECT ST_AsMVTGeom(ST_Transform({geom}, 3857), bounds.boundgeom) AS geom {fields}
+                        FROM "{table}", bounds
+                        WHERE ST_Intersects({geom}, ST_Transform(bounds.boundgeom, 4326))
+                    )
+                SELECT ST_AsMVT(mvtgeom, 'default') FROM mvtgeom;
+            """.format(geom=self.geom, table=self.table, fields=fields))  
+        
+        if tileset == TileMatrixSetEnum.WORLDCRS84QUAD.value.tileMatrixSet:
+            query = text("""
+                WITH                                                                  
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :y, :x, ST_MakeEnvelope(-180, -90, 180, 90, 4326)) AS boundgeom
path: pygeoapi/provider/mvt_postgres.py
user: login: ThorodanBrom
body: [ST_TileEnvelope](https://postgis.net/docs/ST_TileEnvelope.html) by default is configured to EPSG:3857. In order to use it for other CRSs, the docs mention that you can add an extra parameter of a geometry that has an extent of the 0th zoom level. There's an example of setting it for EPSG:4326, we just used that here. (hope that this answered your question, if not, please let me know)

We can probably use the WKB representation there so that `MakeEnvelope` isn't calculated for every tile request. 
created_at: 2025-04-21T12:15:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,263 @@
+# =================================================================
+#
+# Authors: 
+#
+# Copyright (c)
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+import logging
+import requests
+from urllib.parse import urlparse
+
+from pygeoapi.provider.base_mvt import BaseMVTProvider
+from pygeoapi.provider.postgresql import PostgreSQLProvider
+from pygeoapi.provider.base import (ProviderConnectionError,
+                                    ProviderGenericError,
+                                    ProviderInvalidQueryError)
+from pygeoapi.provider.tile import ProviderTileNotFoundError
+from pygeoapi.models.provider.base import (
+    TileSetMetadata, TileMatrixSetEnum, LinkType)
+from pygeoapi.util import is_url, url_join
+from sqlalchemy.orm import Session, load_only
+
+from sqlalchemy.engine import URL
+from sqlalchemy.sql import text
+from sqlalchemy.exc import ConstraintColumnNotFoundError, \
+    InvalidRequestError, OperationalError
+from sqlalchemy.ext.automap import automap_base
+from sqlalchemy.sql.expression import and_
+from pygeoapi.provider.base import BaseProvider, \
+    ProviderConnectionError, ProviderInvalidDataError, ProviderQueryError, \
+    ProviderItemNotFoundError
+    
+from copy import deepcopy
+from datetime import datetime
+from decimal import Decimal
+import functools
+import logging
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MVTPostgresProvider(BaseMVTProvider):
+
+    def __init__(self, provider_def):
+        """
+        Initialize object
+
+        :param provider_def: provider definition
+
+        :returns: pygeoapi.provider.MVT.MVTPostgresProvider
+        """
+
+        super().__init__(provider_def)
+        
+        pg_def = deepcopy(provider_def)
+        del pg_def["options"]["zoom"]
+        self.postgres = PostgreSQLProvider(pg_def)
+        self.layer_name = provider_def["table"]
+        self.table = provider_def['table']
+        self.id_field = provider_def['id_field']
+        self.geom = provider_def.get('geom_field', 'geom')
+
+        LOGGER.debug(f'DB connection: {repr(self.postgres._engine.url)}')
+
+    def __repr__(self):
+        return f'<MVTPostgresProvider> {self.data}'
+
+    @property
+    def service_url(self):
+        return self._service_url
+
+    @property
+    def service_metadata_url(self):
+        return self._service_metadata_url
+
+    def get_layer(self):
+        """
+        Extracts layer name from url
+
+        :returns: layer name
+        """
+
+        return self.layer_name
+
+    def get_tiling_schemes(self):
+
+        return [
+                TileMatrixSetEnum.WEBMERCATORQUAD.value,
+                TileMatrixSetEnum.WORLDCRS84QUAD.value
+            ]
+
+    def get_tiles_service(self, baseurl=None, servicepath=None,
+                          dirpath=None, tile_type=None):
+        """
+        Gets mvt service description
+
+        :param baseurl: base URL of endpoint
+        :param servicepath: base path of URL
+        :param dirpath: directory basepath (equivalent of URL)
+        :param tile_type: tile format type
+
+        :returns: `dict` of item tile service
+        """
+
+        super().get_tiles_service(baseurl, servicepath,
+                                  dirpath, tile_type)
+
+        self._service_url = servicepath
+        return self.get_tms_links()
+
+    def get_tiles(self, layer=None, tileset=None,
+                  z=None, y=None, x=None, format_=None):
+        """
+        Gets tile
+
+        :param layer: mvt tile layer
+        :param tileset: mvt tileset
+        :param z: z index
+        :param y: y index
+        :param x: x index
+        :param format_: tile format
+
+        :returns: an encoded mvt tile
+        """
+        if format_ == 'mvt':
+            format_ = self.format_type
+
+        fields_arr = self.postgres.get_fields().keys()
+        fields = ', '.join(['"' + f + '"' for f in fields_arr])
+        if len(fields) != 0:
+            fields = ',' + fields
+        
+        query = ''
+        if tileset == TileMatrixSetEnum.WEBMERCATORQUAD.value.tileMatrixSet:
+            query = text("""
+                WITH                                                                  
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :y, :x) AS boundgeom
+                    ),
+                    mvtgeom AS (
+                        SELECT ST_AsMVTGeom(ST_Transform({geom}, 3857), bounds.boundgeom) AS geom {fields}
+                        FROM "{table}", bounds
+                        WHERE ST_Intersects({geom}, ST_Transform(bounds.boundgeom, 4326))
+                    )
+                SELECT ST_AsMVT(mvtgeom, 'default') FROM mvtgeom;
+            """.format(geom=self.geom, table=self.table, fields=fields))  
+        
+        if tileset == TileMatrixSetEnum.WORLDCRS84QUAD.value.tileMatrixSet:
+            query = text("""
+                WITH                                                                  
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :y, :x, ST_MakeEnvelope(-180, -90, 180, 90, 4326)) AS boundgeom
path: pygeoapi/provider/mvt_postgres.py
user: login: doublebyte1
body: I agree, I think the WKB representation would make sense here!
created_at: 2025-04-26T12:45:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,263 @@
+# =================================================================
+#
+# Authors: 
+#
+# Copyright (c)
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+import logging
+import requests
+from urllib.parse import urlparse
+
+from pygeoapi.provider.base_mvt import BaseMVTProvider
+from pygeoapi.provider.postgresql import PostgreSQLProvider
+from pygeoapi.provider.base import (ProviderConnectionError,
+                                    ProviderGenericError,
+                                    ProviderInvalidQueryError)
+from pygeoapi.provider.tile import ProviderTileNotFoundError
+from pygeoapi.models.provider.base import (
+    TileSetMetadata, TileMatrixSetEnum, LinkType)
+from pygeoapi.util import is_url, url_join
+from sqlalchemy.orm import Session, load_only
+
+from sqlalchemy.engine import URL
+from sqlalchemy.sql import text
+from sqlalchemy.exc import ConstraintColumnNotFoundError, \
+    InvalidRequestError, OperationalError
+from sqlalchemy.ext.automap import automap_base
+from sqlalchemy.sql.expression import and_
+from pygeoapi.provider.base import BaseProvider, \
+    ProviderConnectionError, ProviderInvalidDataError, ProviderQueryError, \
+    ProviderItemNotFoundError
+    
+from copy import deepcopy
+from datetime import datetime
+from decimal import Decimal
+import functools
+import logging
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MVTPostgresProvider(BaseMVTProvider):
+
+    def __init__(self, provider_def):
+        """
+        Initialize object
+
+        :param provider_def: provider definition
+
+        :returns: pygeoapi.provider.MVT.MVTPostgresProvider
+        """
+
+        super().__init__(provider_def)
+        
+        pg_def = deepcopy(provider_def)
+        del pg_def["options"]["zoom"]
+        self.postgres = PostgreSQLProvider(pg_def)
+        self.layer_name = provider_def["table"]
+        self.table = provider_def['table']
+        self.id_field = provider_def['id_field']
+        self.geom = provider_def.get('geom_field', 'geom')
+
+        LOGGER.debug(f'DB connection: {repr(self.postgres._engine.url)}')
+
+    def __repr__(self):
+        return f'<MVTPostgresProvider> {self.data}'
+
+    @property
+    def service_url(self):
+        return self._service_url
+
+    @property
+    def service_metadata_url(self):
+        return self._service_metadata_url
+
+    def get_layer(self):
+        """
+        Extracts layer name from url
+
+        :returns: layer name
+        """
+
+        return self.layer_name
+
+    def get_tiling_schemes(self):
+
+        return [
+                TileMatrixSetEnum.WEBMERCATORQUAD.value,
+                TileMatrixSetEnum.WORLDCRS84QUAD.value
+            ]
+
+    def get_tiles_service(self, baseurl=None, servicepath=None,
+                          dirpath=None, tile_type=None):
+        """
+        Gets mvt service description
+
+        :param baseurl: base URL of endpoint
+        :param servicepath: base path of URL
+        :param dirpath: directory basepath (equivalent of URL)
+        :param tile_type: tile format type
+
+        :returns: `dict` of item tile service
+        """
+
+        super().get_tiles_service(baseurl, servicepath,
+                                  dirpath, tile_type)
+
+        self._service_url = servicepath
+        return self.get_tms_links()
+
+    def get_tiles(self, layer=None, tileset=None,
+                  z=None, y=None, x=None, format_=None):
+        """
+        Gets tile
+
+        :param layer: mvt tile layer
+        :param tileset: mvt tileset
+        :param z: z index
+        :param y: y index
+        :param x: x index
+        :param format_: tile format
+
+        :returns: an encoded mvt tile
+        """
+        if format_ == 'mvt':
+            format_ = self.format_type
+
+        fields_arr = self.postgres.get_fields().keys()
+        fields = ', '.join(['"' + f + '"' for f in fields_arr])
+        if len(fields) != 0:
+            fields = ',' + fields
+        
+        query = ''
+        if tileset == TileMatrixSetEnum.WEBMERCATORQUAD.value.tileMatrixSet:
+            query = text("""
+                WITH                                                                  
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :y, :x) AS boundgeom
+                    ),
+                    mvtgeom AS (
+                        SELECT ST_AsMVTGeom(ST_Transform({geom}, 3857), bounds.boundgeom) AS geom {fields}
path: pygeoapi/provider/mvt_postgres.py
user: login: doublebyte1
body: I think simplification, in general, yields performance; and in the case of tiles, we do not need the geometries to be so detailed.
created_at: 2025-04-26T12:48:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,263 @@
+# =================================================================
+#
+# Authors: 
+#
+# Copyright (c)
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+import logging
+import requests
+from urllib.parse import urlparse
+
+from pygeoapi.provider.base_mvt import BaseMVTProvider
+from pygeoapi.provider.postgresql import PostgreSQLProvider
+from pygeoapi.provider.base import (ProviderConnectionError,
+                                    ProviderGenericError,
+                                    ProviderInvalidQueryError)
+from pygeoapi.provider.tile import ProviderTileNotFoundError
+from pygeoapi.models.provider.base import (
+    TileSetMetadata, TileMatrixSetEnum, LinkType)
+from pygeoapi.util import is_url, url_join
+from sqlalchemy.orm import Session, load_only
+
+from sqlalchemy.engine import URL
+from sqlalchemy.sql import text
+from sqlalchemy.exc import ConstraintColumnNotFoundError, \
+    InvalidRequestError, OperationalError
+from sqlalchemy.ext.automap import automap_base
+from sqlalchemy.sql.expression import and_
+from pygeoapi.provider.base import BaseProvider, \
+    ProviderConnectionError, ProviderInvalidDataError, ProviderQueryError, \
+    ProviderItemNotFoundError
+    
+from copy import deepcopy
+from datetime import datetime
+from decimal import Decimal
+import functools
+import logging
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MVTPostgresProvider(BaseMVTProvider):
+
+    def __init__(self, provider_def):
+        """
+        Initialize object
+
+        :param provider_def: provider definition
+
+        :returns: pygeoapi.provider.MVT.MVTPostgresProvider
+        """
+
+        super().__init__(provider_def)
+        
+        pg_def = deepcopy(provider_def)
+        del pg_def["options"]["zoom"]
+        self.postgres = PostgreSQLProvider(pg_def)
+        self.layer_name = provider_def["table"]
+        self.table = provider_def['table']
+        self.id_field = provider_def['id_field']
+        self.geom = provider_def.get('geom_field', 'geom')
+
+        LOGGER.debug(f'DB connection: {repr(self.postgres._engine.url)}')
+
+    def __repr__(self):
+        return f'<MVTPostgresProvider> {self.data}'
+
+    @property
+    def service_url(self):
+        return self._service_url
+
+    @property
+    def service_metadata_url(self):
+        return self._service_metadata_url
+
+    def get_layer(self):
+        """
+        Extracts layer name from url
+
+        :returns: layer name
+        """
+
+        return self.layer_name
+
+    def get_tiling_schemes(self):
+
+        return [
+                TileMatrixSetEnum.WEBMERCATORQUAD.value,
+                TileMatrixSetEnum.WORLDCRS84QUAD.value
+            ]
+
+    def get_tiles_service(self, baseurl=None, servicepath=None,
+                          dirpath=None, tile_type=None):
+        """
+        Gets mvt service description
+
+        :param baseurl: base URL of endpoint
+        :param servicepath: base path of URL
+        :param dirpath: directory basepath (equivalent of URL)
+        :param tile_type: tile format type
+
+        :returns: `dict` of item tile service
+        """
+
+        super().get_tiles_service(baseurl, servicepath,
+                                  dirpath, tile_type)
+
+        self._service_url = servicepath
+        return self.get_tms_links()
+
+    def get_tiles(self, layer=None, tileset=None,
+                  z=None, y=None, x=None, format_=None):
+        """
+        Gets tile
+
+        :param layer: mvt tile layer
+        :param tileset: mvt tileset
+        :param z: z index
+        :param y: y index
+        :param x: x index
+        :param format_: tile format
+
+        :returns: an encoded mvt tile
+        """
+        if format_ == 'mvt':
+            format_ = self.format_type
+
+        fields_arr = self.postgres.get_fields().keys()
+        fields = ', '.join(['"' + f + '"' for f in fields_arr])
+        if len(fields) != 0:
+            fields = ',' + fields
+        
+        query = ''
+        if tileset == TileMatrixSetEnum.WEBMERCATORQUAD.value.tileMatrixSet:
+            query = text("""
+                WITH                                                                  
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :y, :x) AS boundgeom
+                    ),
+                    mvtgeom AS (
+                        SELECT ST_AsMVTGeom(ST_Transform({geom}, 3857), bounds.boundgeom) AS geom {fields}
path: pygeoapi/provider/mvt_postgres.py
user: login: ThorodanBrom
body: Addressed in [97ba024](https://github.com/geopython/pygeoapi/pull/1979/commits/97ba024f4aa8a253ff04ac718bfb3feba528080a)
created_at: 2025-05-02T19:06:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,263 @@
+# =================================================================
+#
+# Authors: 
+#
+# Copyright (c)
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+import logging
+import requests
+from urllib.parse import urlparse
+
+from pygeoapi.provider.base_mvt import BaseMVTProvider
+from pygeoapi.provider.postgresql import PostgreSQLProvider
+from pygeoapi.provider.base import (ProviderConnectionError,
+                                    ProviderGenericError,
+                                    ProviderInvalidQueryError)
+from pygeoapi.provider.tile import ProviderTileNotFoundError
+from pygeoapi.models.provider.base import (
+    TileSetMetadata, TileMatrixSetEnum, LinkType)
+from pygeoapi.util import is_url, url_join
+from sqlalchemy.orm import Session, load_only
+
+from sqlalchemy.engine import URL
+from sqlalchemy.sql import text
+from sqlalchemy.exc import ConstraintColumnNotFoundError, \
+    InvalidRequestError, OperationalError
+from sqlalchemy.ext.automap import automap_base
+from sqlalchemy.sql.expression import and_
+from pygeoapi.provider.base import BaseProvider, \
+    ProviderConnectionError, ProviderInvalidDataError, ProviderQueryError, \
+    ProviderItemNotFoundError
+    
+from copy import deepcopy
+from datetime import datetime
+from decimal import Decimal
+import functools
+import logging
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MVTPostgresProvider(BaseMVTProvider):
+
+    def __init__(self, provider_def):
+        """
+        Initialize object
+
+        :param provider_def: provider definition
+
+        :returns: pygeoapi.provider.MVT.MVTPostgresProvider
+        """
+
+        super().__init__(provider_def)
+        
+        pg_def = deepcopy(provider_def)
+        del pg_def["options"]["zoom"]
+        self.postgres = PostgreSQLProvider(pg_def)
+        self.layer_name = provider_def["table"]
+        self.table = provider_def['table']
+        self.id_field = provider_def['id_field']
+        self.geom = provider_def.get('geom_field', 'geom')
+
+        LOGGER.debug(f'DB connection: {repr(self.postgres._engine.url)}')
+
+    def __repr__(self):
+        return f'<MVTPostgresProvider> {self.data}'
+
+    @property
+    def service_url(self):
+        return self._service_url
+
+    @property
+    def service_metadata_url(self):
+        return self._service_metadata_url
+
+    def get_layer(self):
+        """
+        Extracts layer name from url
+
+        :returns: layer name
+        """
+
+        return self.layer_name
+
+    def get_tiling_schemes(self):
+
+        return [
+                TileMatrixSetEnum.WEBMERCATORQUAD.value,
+                TileMatrixSetEnum.WORLDCRS84QUAD.value
+            ]
+
+    def get_tiles_service(self, baseurl=None, servicepath=None,
+                          dirpath=None, tile_type=None):
+        """
+        Gets mvt service description
+
+        :param baseurl: base URL of endpoint
+        :param servicepath: base path of URL
+        :param dirpath: directory basepath (equivalent of URL)
+        :param tile_type: tile format type
+
+        :returns: `dict` of item tile service
+        """
+
+        super().get_tiles_service(baseurl, servicepath,
+                                  dirpath, tile_type)
+
+        self._service_url = servicepath
+        return self.get_tms_links()
+
+    def get_tiles(self, layer=None, tileset=None,
+                  z=None, y=None, x=None, format_=None):
+        """
+        Gets tile
+
+        :param layer: mvt tile layer
+        :param tileset: mvt tileset
+        :param z: z index
+        :param y: y index
+        :param x: x index
+        :param format_: tile format
+
+        :returns: an encoded mvt tile
+        """
+        if format_ == 'mvt':
+            format_ = self.format_type
+
+        fields_arr = self.postgres.get_fields().keys()
+        fields = ', '.join(['"' + f + '"' for f in fields_arr])
+        if len(fields) != 0:
+            fields = ',' + fields
+        
+        query = ''
+        if tileset == TileMatrixSetEnum.WEBMERCATORQUAD.value.tileMatrixSet:
+            query = text("""
+                WITH                                                                  
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :y, :x) AS boundgeom
+                    ),
+                    mvtgeom AS (
+                        SELECT ST_AsMVTGeom(ST_Transform({geom}, 3857), bounds.boundgeom) AS geom {fields}
+                        FROM "{table}", bounds
+                        WHERE ST_Intersects({geom}, ST_Transform(bounds.boundgeom, 4326))
+                    )
+                SELECT ST_AsMVT(mvtgeom, 'default') FROM mvtgeom;
+            """.format(geom=self.geom, table=self.table, fields=fields))  
+        
+        if tileset == TileMatrixSetEnum.WORLDCRS84QUAD.value.tileMatrixSet:
+            query = text("""
+                WITH                                                                  
+                    bounds AS (
+                        SELECT ST_TileEnvelope(:z, :y, :x, ST_MakeEnvelope(-180, -90, 180, 90, 4326)) AS boundgeom
path: pygeoapi/provider/mvt_postgres.py
user: login: ThorodanBrom
body: Addressed in [97ba024](https://github.com/geopython/pygeoapi/pull/1979/commits/97ba024f4aa8a253ff04ac718bfb3feba528080a). I used EWKT representation instead of WKB as the the WKB hex string was too long
created_at: 2025-05-02T19:07:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -130,6 +131,46 @@ Following code block shows how to configure pygeoapi to read Mapbox vector tiles
              name: pbf
              mimetype: application/vnd.mapbox-vector-tile
 
+MVT-postgres
+^^^^^^^^^^^^
+
+.. note::
+   Requires Python packages sqlalchemy, geoalchemy2 and psycopg2-binary
+
+Must have PostGIS installed with protobuf-c support. 
+
+.. note::
+   Geometry must be using EPSG:4326
+
+This provider gives support to serving tiles generated using `Postgres <https://www.postgresql.org/>`_ with `PostGIS <https://postgis.net/>`_.
+The tiles are rendered on-the-fly using `ST_AsMVT <https://postgis.net/docs/ST_AsMVT.html>`_ and related methods.
+
+This code block shows how to configure pygeoapi to render Mapbox vector tiles from a PostGIS table.
+
+.. code-block:: yaml
+
+   providers:
+       - type: tile
+           name: MVT-postgres
+           data:
+               host: 127.0.0.1
+               port: 3010 # Default 5432 if not provided
+               dbname: test
+               user: postgres
+               password: postgres
+               search_path: [osm, public]
+           id_field: osm_id
+           table: hotosm_bdi_waterways
+           geom_field: foo_geom
+           options:
+           zoom:
path: docs/source/data-publishing/ogcapi-tiles.rst
user: login: doublebyte1
body: @ThorodanBrom I think that the `zoom` should be placed under `options`
created_at: 2025-05-08T15:21:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,6 +22,7 @@ pygeoapi core tile providers are listed below, along with supported features.
    `MVT-elastic`_,✅,✅,✅,❌,❌,✅
    `MVT-proxy`_,❓,❓,❓,❓,❌,✅
    `WMTSFacade`_,✅,❌,✅,✅,✅,❌
+   `MVT-postgres`_,✅,✅,✅,✅,❌,✅
path: docs/source/data-publishing/ogcapi-tiles.rst
user: login: tomkralidis
body: Rename to `MVT-postgresql`
created_at: 2025-05-11T13:44:35Z
author_association: MEMBER
diff_hunk: @@ -130,6 +131,46 @@ Following code block shows how to configure pygeoapi to read Mapbox vector tiles
              name: pbf
              mimetype: application/vnd.mapbox-vector-tile
 
+MVT-postgres
+^^^^^^^^^^^^
+
+.. note::
+   Requires Python packages sqlalchemy, geoalchemy2 and psycopg2-binary
+
+Must have PostGIS installed with protobuf-c support. 
path: docs/source/data-publishing/ogcapi-tiles.rst
user: login: tomkralidis
body: Turn into an additional note
created_at: 2025-05-11T13:44:57Z
author_association: MEMBER
diff_hunk: @@ -130,6 +131,46 @@ Following code block shows how to configure pygeoapi to read Mapbox vector tiles
              name: pbf
              mimetype: application/vnd.mapbox-vector-tile
 
+MVT-postgres
+^^^^^^^^^^^^
+
+.. note::
+   Requires Python packages sqlalchemy, geoalchemy2 and psycopg2-binary
+
+Must have PostGIS installed with protobuf-c support. 
+
+.. note::
+   Geometry must be using EPSG:4326
+
+This provider gives support to serving tiles generated using `Postgres <https://www.postgresql.org/>`_ with `PostGIS <https://postgis.net/>`_.
path: docs/source/data-publishing/ogcapi-tiles.rst
user: login: tomkralidis
body: `s/Postgres/PostgreSQL/g`
created_at: 2025-05-11T13:45:23Z
author_association: MEMBER
diff_hunk: @@ -130,6 +131,46 @@ Following code block shows how to configure pygeoapi to read Mapbox vector tiles
              name: pbf
              mimetype: application/vnd.mapbox-vector-tile
 
+MVT-postgres
+^^^^^^^^^^^^
+
+.. note::
+   Requires Python packages sqlalchemy, geoalchemy2 and psycopg2-binary
+
+Must have PostGIS installed with protobuf-c support. 
+
+.. note::
+   Geometry must be using EPSG:4326
+
+This provider gives support to serving tiles generated using `Postgres <https://www.postgresql.org/>`_ with `PostGIS <https://postgis.net/>`_.
+The tiles are rendered on-the-fly using `ST_AsMVT <https://postgis.net/docs/ST_AsMVT.html>`_ and related methods.
+
+This code block shows how to configure pygeoapi to render Mapbox vector tiles from a PostGIS table.
+
+.. code-block:: yaml
+
+   providers:
+       - type: tile
+           name: MVT-postgres
path: docs/source/data-publishing/ogcapi-tiles.rst
user: login: tomkralidis
body: `MVT-postgresql`
created_at: 2025-05-11T13:45:52Z
author_association: MEMBER
diff_hunk: @@ -53,6 +53,7 @@
         'MVT-tippecanoe': 'pygeoapi.provider.mvt_tippecanoe.MVTTippecanoeProvider',  # noqa: E501
         'MVT-elastic': 'pygeoapi.provider.mvt_elastic.MVTElasticProvider',
         'MVT-proxy': 'pygeoapi.provider.mvt_proxy.MVTProxyProvider',
+        'MVT-postgres': 'pygeoapi.provider.mvt_postgres.MVTPostgresProvider',
path: pygeoapi/plugin.py
user: login: tomkralidis
body: `'MVT-postgresql': 'pygeoapi.provider.mvt_postgresql.MVTPostgreSQLProvider',`
created_at: 2025-05-11T13:46:49Z
author_association: MEMBER
path: pygeoapi/provider/mvt_postgres.py
user: login: tomkralidis
body: Rename to `pygeoapi/provider/mvt_postgresql.py`
created_at: 2025-05-11T13:47:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,246 @@
+# =================================================================
+#
+# Authors:
path: pygeoapi/provider/mvt_postgres.py
user: login: tomkralidis
body: Please add yourself as author.  Example in https://github.com/geopython/pygeoapi/blob/master/setup.py#L3
created_at: 2025-05-11T13:48:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,246 @@
+# =================================================================
+#
+# Authors:
+#
+# Copyright (c)
path: pygeoapi/provider/mvt_postgres.py
user: login: tomkralidis
body: Please add copyright.  Example in https://github.com/geopython/pygeoapi/blob/master/setup.py#L5
created_at: 2025-05-11T13:49:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,246 @@
+# =================================================================
+#
+# Authors:
+#
+# Copyright (c)
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+import logging
path: pygeoapi/provider/mvt_postgres.py
user: login: tomkralidis
body: Order imports by standard library, 3rd party packages, then local imports, alphabetically, separated by blank line.

```python
from copy import deepcopy
import logging

from sqlalchemy.sql import text

from pygeoapi.models.provider.base import (
    TileSetMetadata, TileMatrixSetEnum, LinkType)
from pygeoapi.provider.base import ProviderConnectionError
from pygeoapi.provider.base_mvt import BaseMVTProvider
from pygeoapi.provider.postgresql import PostgreSQLProvider
from pygeoapi.util import url_join
```
created_at: 2025-05-11T13:51:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,246 @@
+# =================================================================
+#
+# Authors:
+#
+# Copyright (c)
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+import logging
+
+from pygeoapi.provider.base_mvt import BaseMVTProvider
+from pygeoapi.provider.postgresql import PostgreSQLProvider
+from pygeoapi.provider.base import ProviderConnectionError
+from pygeoapi.models.provider.base import (
+    TileSetMetadata, TileMatrixSetEnum, LinkType)
+from pygeoapi.util import url_join
+
+from sqlalchemy.sql import text
+
+from copy import deepcopy
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MVTPostgresProvider(BaseMVTProvider):
path: pygeoapi/provider/mvt_postgres.py
user: login: tomkralidis
body: Rename to `MVTPostgreSQLProvider`
created_at: 2025-05-11T13:51:34Z
author_association: MEMBER
diff_hunk: @@ -130,6 +131,47 @@ Following code block shows how to configure pygeoapi to read Mapbox vector tiles
              name: pbf
              mimetype: application/vnd.mapbox-vector-tile
 
+MVT-postgresql
+^^^^^^^^^^^^^^
+
+.. note::
+   Requires Python packages sqlalchemy, geoalchemy2 and psycopg2-binary
+
+.. note::
+   Must have PostGIS installed with protobuf-c support 
+
+.. note::
+   Geometry must be using EPSG:4326
path: docs/source/data-publishing/ogcapi-tiles.rst
user: login: photonbit
body: I understand that this note is outdated for the postgresql provider. Is the note valid for this case?
created_at: 2025-05-21T11:29:51Z
author_association: NONE

PR comments:
user: login: doublebyte1
created_at: 2025-04-19T11:31:17Z
body: * Please check the errors when building the documentation.
* If possible, please also add a test for this provider. You can check this one for inspiration: https://github.com/geopython/pygeoapi/blob/master/tests/test_postgresql_provider.py
author_association: CONTRIBUTOR
user: login: ThorodanBrom
created_at: 2025-04-21T12:19:02Z
body: Thanks for the review,
- I'll work on adding an example provider for this in `pygeoapi-examples` as well as writing test cases
- I'll check out the errors in the documentation and flake8
- The PR needs a few updates and also needs to be rebased as well to accomodate the MVT tile fixes, is it OK if I rebase and force-push or will that break any of the CI flows?
author_association: CONTRIBUTOR
user: login: doublebyte1
created_at: 2025-04-26T12:49:53Z
body: > Thanks for the review,
> 
> * I'll work on adding an example provider for this in `pygeoapi-examples` as well as writing test cases
> * I'll check out the errors in the documentation and flake8
> * The PR needs a few updates and also needs to be rebased as well to accomodate the MVT tile fixes, is it OK if I rebase and force-push or will that break any of the CI flows?

Thank You @ThorodanBrom ! I think it is ok to rebase, but I am pinging @tomkralidis here, just in case.
author_association: CONTRIBUTOR
user: login: tomkralidis
created_at: 2025-04-27T12:54:42Z
body: +1 to rebase, given it will start CI again fresh.
author_association: MEMBER
user: login: ThorodanBrom
created_at: 2025-05-08T16:53:27Z
body: Thanks for the review @doublebyte1. I'm working on the example in `pygeoapi-examples`, but I'm not really sure on how to write tests for this provider. There aren't any tests for other MVT providers, so I don't have a reference. I can think of the following conditions to test:
- if the provider is able to create tiles for the configured table and configured zoom levels for `WebMercatorQuad` and `WorldCRS84Quad`
- if the tiles returned are valid MVT files + a `200 OK` 
    - I don't know how to test for a 'valid' MVT, is there a library or something that can parse and validate the API output 
- if tiles requested for an invalid index or where data does not exist returns a `204 No Content`

Do let me know if this sounds OK.
author_association: CONTRIBUTOR
user: login: doublebyte1
created_at: 2025-05-09T12:40:59Z
body: > Thanks for the review @doublebyte1. I'm working on the example in `pygeoapi-examples`, but I'm not really sure on how to write tests for this provider. There aren't any tests for other MVT providers, so I don't have a reference. I can think of the following conditions to test:
> 
> * if the provider is able to create tiles for the configured table and configured zoom levels for `WebMercatorQuad` and `WorldCRS84Quad`
> * if the tiles returned are valid MVT files + a `200 OK`
>   
>   * I don't know how to test for a 'valid' MVT, is there a library or something that can parse and validate the API output
> * if tiles requested for an invalid index or where data does not exist returns a `204 No Content`
> 
> Do let me know if this sounds OK.

You are right in that there are no tests for other MVT providers. Let's keep it like this, for now. Thank You for the availability!
author_association: CONTRIBUTOR
user: login: ThorodanBrom
created_at: 2025-05-19T16:50:52Z
body: Hello @tomkralidis, thanks for the review. I think I've addressed all your comments in 9a320cc. (I added a small bug fix in a034d15 as well). Please let me know if there are any other required changes.
author_association: CONTRIBUTOR