From cc12eed34ec196d3fd10c358817d3fa538887024 Mon Sep 17 00:00:00 2001 From: Matthew Dear Date: Fri, 23 Jan 2026 14:16:25 +0000 Subject: [PATCH 1/3] feat: implement optional count for postgresql provider This work has been done to allow the result count to be enabled or disabled for the PostgreSQL provider. By disabling the count you can get improved performance on large datasets but on smaller datasets this is unlikely to have any affect. --- docs/source/publishing/ogcapi-features.rst | 3 + pygeoapi/provider/sql.py | 12 ++-- tests/provider/test_postgresql_provider.py | 70 +++++++++++++++++++++- 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/docs/source/publishing/ogcapi-features.rst b/docs/source/publishing/ogcapi-features.rst index 3a1c2e9e5..5646f1a7b 100644 --- a/docs/source/publishing/ogcapi-features.rst +++ b/docs/source/publishing/ogcapi-features.rst @@ -625,6 +625,7 @@ Must have PostGIS installed. id_field: osm_id table: hotosm_bdi_waterways geom_field: foo_geom + count: true # Optional; Default true; Enable/disable count for improved performance. A number of database connection options can be also configured in the provider in order to adjust properly the sqlalchemy engine client. These are optional and if not specified, the default from the engine will be used. Please see also `SQLAlchemy docs `_. @@ -662,6 +663,7 @@ These are optional and if not specified, the default from the engine will be use id_field: osm_id table: hotosm_bdi_waterways geom_field: foo_geom + count: true # Optional; Default true; Enable/disable count for improved performance. The PostgreSQL provider is also able to connect to Cloud SQL databases. @@ -677,6 +679,7 @@ The PostgreSQL provider is also able to connect to Cloud SQL databases. password: postgres id_field: id table: states + count: true # Optional; Default true; Enable/disable count for improved performance. This is what a configuration for `Google Cloud SQL`_ connection looks like. The ``host`` block contains the necessary socket connection information. diff --git a/pygeoapi/provider/sql.py b/pygeoapi/provider/sql.py index 631d41c99..e38051adc 100644 --- a/pygeoapi/provider/sql.py +++ b/pygeoapi/provider/sql.py @@ -128,7 +128,7 @@ def __init__( self.id_field = provider_def['id_field'] self.geom = provider_def.get('geom_field', 'geom') self.driver_name = driver_name - self.count = str2bool(provider_def.get('count', True)) + self.count = str(provider_def.get('count', 'true')).lower() == 'true' LOGGER.debug(f'Name: {self.name}') LOGGER.debug(f'Table: {self.table}') @@ -214,18 +214,18 @@ def query( .options(selected_properties) ) - matched = results.count() - - LOGGER.debug(f'Found {matched} result(s)') - LOGGER.debug('Preparing response') response = { 'type': 'FeatureCollection', 'features': [], - 'numberMatched': matched, 'numberReturned': 0 } + if self.count or resulttype == 'hits': + matched = results.count() + response['numberMatched'] = matched + LOGGER.debug(f'Found {matched} result(s)') + if resulttype == 'hits' or not results: return response diff --git a/tests/provider/test_postgresql_provider.py b/tests/provider/test_postgresql_provider.py index da3d0f99e..fecdd8814 100644 --- a/tests/provider/test_postgresql_provider.py +++ b/tests/provider/test_postgresql_provider.py @@ -85,7 +85,8 @@ def config(): }, 'id_field': 'osm_id', 'table': 'hotosm_bdi_waterways', - 'geom_field': 'foo_geom' + 'geom_field': 'foo_geom', + 'count': 'true' } @@ -908,3 +909,70 @@ def test_transaction_create_handles_invalid_input_data(pg_api_, data): headers, code, content = manage_collection_item( pg_api_, req, action='create', dataset='hot_osm_waterways') assert 'generic error' in content + + +def test_provider_count_default_value(config): + # Arrange + provider = PostgreSQLProvider(config) + + # Act + results = provider.query() + + # Assert + assert results['numberMatched'] == 14776 + + +@pytest.mark.parametrize("count_value", [ + ('true'), + ('TRUE') +]) +def test_provider_count_true(config, count_value): + # Arrange + config['count'] = count_value + provider = PostgreSQLProvider(config) + + # Act + results = provider.query() + + # Assert + assert results['numberMatched'] == 14776 + + +@pytest.mark.parametrize("count_value", [ + ('false'), + ('FALSE') +]) +def test_provider_count_false(config, count_value): + # Arrange + config['count'] = count_value + provider = PostgreSQLProvider(config) + + # Act + results = provider.query() + + # Assert + assert 'numberMatched' not in results + + +def test_provider_count_false_with_resulttype_hits(config): + # Arrange + config['count'] = 'false' + provider = PostgreSQLProvider(config) + + # Act + results = provider.query(resulttype="hits") + + # Assert + assert results['numberMatched'] == 14776 + + +def test_provider_count_number_string(config): + # Arrange + config['count'] = '1' + provider = PostgreSQLProvider(config) + + # Act + results = provider.query() + + # Assert + assert 'numberMatched' not in results From e59ea5f73a4de35e9e0bd70fc7cad2148cb420ba Mon Sep 17 00:00:00 2001 From: Matthew Dear Date: Tue, 27 Jan 2026 08:51:59 +0000 Subject: [PATCH 2/3] fix: move count to base provider This work has been done to move count to the base provider. While doing this work I also added a debug log message to state when the count had been disabled in the SQL provider. Also, I removed some tests that were no longer needed after the introduction of the str2bool function, when getting the count value from the configuration file. --- pygeoapi/provider/base.py | 3 ++ pygeoapi/provider/sql.py | 4 +-- tests/provider/test_postgresql_provider.py | 39 ++-------------------- 3 files changed, 8 insertions(+), 38 deletions(-) diff --git a/pygeoapi/provider/base.py b/pygeoapi/provider/base.py index b1ccb9c43..1765dcca0 100644 --- a/pygeoapi/provider/base.py +++ b/pygeoapi/provider/base.py @@ -57,6 +57,8 @@ def __init__(self, provider_def): :returns: pygeoapi.provider.base.BaseProvider """ + from pygeoapi.util import str2bool + try: self.name = provider_def['name'] self.type = provider_def['type'] @@ -65,6 +67,7 @@ def __init__(self, provider_def): raise RuntimeError('name/type/data are required') self.editable = provider_def.get('editable', False) + self.count = str2bool(provider_def.get('count', 'true')) self.options = provider_def.get('options') self.id_field = provider_def.get('id_field') self.uri_field = provider_def.get('uri_field') diff --git a/pygeoapi/provider/sql.py b/pygeoapi/provider/sql.py index e38051adc..a955f06db 100644 --- a/pygeoapi/provider/sql.py +++ b/pygeoapi/provider/sql.py @@ -91,7 +91,6 @@ ProviderQueryError, ProviderItemNotFoundError ) -from pygeoapi.util import str2bool LOGGER = logging.getLogger(__name__) @@ -128,7 +127,6 @@ def __init__( self.id_field = provider_def['id_field'] self.geom = provider_def.get('geom_field', 'geom') self.driver_name = driver_name - self.count = str(provider_def.get('count', 'true')).lower() == 'true' LOGGER.debug(f'Name: {self.name}') LOGGER.debug(f'Table: {self.table}') @@ -225,6 +223,8 @@ def query( matched = results.count() response['numberMatched'] = matched LOGGER.debug(f'Found {matched} result(s)') + else: + LOGGER.debug('Count disabled') if resulttype == 'hits' or not results: return response diff --git a/tests/provider/test_postgresql_provider.py b/tests/provider/test_postgresql_provider.py index fecdd8814..c27660caf 100644 --- a/tests/provider/test_postgresql_provider.py +++ b/tests/provider/test_postgresql_provider.py @@ -85,8 +85,7 @@ def config(): }, 'id_field': 'osm_id', 'table': 'hotosm_bdi_waterways', - 'geom_field': 'foo_geom', - 'count': 'true' + 'geom_field': 'foo_geom' } @@ -922,29 +921,9 @@ def test_provider_count_default_value(config): assert results['numberMatched'] == 14776 -@pytest.mark.parametrize("count_value", [ - ('true'), - ('TRUE') -]) -def test_provider_count_true(config, count_value): - # Arrange - config['count'] = count_value - provider = PostgreSQLProvider(config) - - # Act - results = provider.query() - - # Assert - assert results['numberMatched'] == 14776 - - -@pytest.mark.parametrize("count_value", [ - ('false'), - ('FALSE') -]) -def test_provider_count_false(config, count_value): +def test_provider_count_false(config): # Arrange - config['count'] = count_value + config['count'] = 'false' provider = PostgreSQLProvider(config) # Act @@ -964,15 +943,3 @@ def test_provider_count_false_with_resulttype_hits(config): # Assert assert results['numberMatched'] == 14776 - - -def test_provider_count_number_string(config): - # Arrange - config['count'] = '1' - provider = PostgreSQLProvider(config) - - # Act - results = provider.query() - - # Assert - assert 'numberMatched' not in results From a1aa0ca7d3f53b64e22852dbd8df15675af9cca2 Mon Sep 17 00:00:00 2001 From: Matthew Dear Date: Tue, 3 Feb 2026 08:59:05 +0000 Subject: [PATCH 3/3] fix: convert string true to boolean --- pygeoapi/provider/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygeoapi/provider/base.py b/pygeoapi/provider/base.py index 1765dcca0..3dd4c5a18 100644 --- a/pygeoapi/provider/base.py +++ b/pygeoapi/provider/base.py @@ -67,7 +67,7 @@ def __init__(self, provider_def): raise RuntimeError('name/type/data are required') self.editable = provider_def.get('editable', False) - self.count = str2bool(provider_def.get('count', 'true')) + self.count = str2bool(provider_def.get('count', True)) self.options = provider_def.get('options') self.id_field = provider_def.get('id_field') self.uri_field = provider_def.get('uri_field')