From 2520a9b6024e13d5248802e2559ca1b89587afdd Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Tue, 20 Jan 2026 19:29:55 -0500 Subject: [PATCH 1/4] Implement optional count in Feature Providers --- docs/source/configuration.rst | 1 + pygeoapi/provider/csv_.py | 5 +-- pygeoapi/provider/esri.py | 8 +++-- pygeoapi/provider/geojson.py | 3 +- pygeoapi/provider/mongo.py | 34 +++++++++++-------- pygeoapi/provider/socrata.py | 8 +++-- pygeoapi/provider/sql.py | 1 + pygeoapi/provider/tinydb_.py | 5 ++- .../schemas/config/pygeoapi-config-0.x.yml | 4 +++ tests/provider/test_csv__provider.py | 13 +++++++ tests/provider/test_esri_provider.py | 13 +++++++ tests/provider/test_geojson_provider.py | 14 ++++++++ tests/provider/test_mongo_provider.py | 14 ++++++++ tests/provider/test_mysql_provider.py | 14 ++++++++ tests/provider/test_postgresql_provider.py | 14 ++++++++ tests/provider/test_socrata_provider.py | 14 ++++++++ tests/provider/test_tinydb_provider.py | 14 ++++++++ tests/pygeoapi-test-config.yml | 1 + 18 files changed, 158 insertions(+), 22 deletions(-) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index fd75e0284b..8cd1b10d59 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -273,6 +273,7 @@ default. include_extra_query_parameters: false # include extra query parameters that are not part of the collection properties (default: false) # editable transactions: DO NOT ACTIVATE unless you have setup access control beyond pygeoapi editable: true # optional: if backend is writable, default is false + count: true # optional: perform a count query for collection queries, default is true # coordinate reference systems (CRS) section is optional # default CRSs are http://www.opengis.net/def/crs/OGC/1.3/CRS84 (coordinates without height) # and http://www.opengis.net/def/crs/OGC/1.3/CRS84h (coordinates with ellipsoidal height) diff --git a/pygeoapi/provider/csv_.py b/pygeoapi/provider/csv_.py index cffa547347..d66d6fd1b5 100644 --- a/pygeoapi/provider/csv_.py +++ b/pygeoapi/provider/csv_.py @@ -194,8 +194,9 @@ def _load(self, offset=0, limit=10, resulttype='results', feature_collection['features'].append(feature) - feature_collection['numberMatched'] = \ - len(feature_collection['features']) + if self.count: + feature_collection['numberMatched'] = \ + len(feature_collection['features']) if identifier is not None and not found: return None diff --git a/pygeoapi/provider/esri.py b/pygeoapi/provider/esri.py index c84b6e1f1a..f41171e439 100644 --- a/pygeoapi/provider/esri.py +++ b/pygeoapi/provider/esri.py @@ -156,9 +156,13 @@ def query(self, offset=0, limit=10, resulttype='results', fc = { 'type': 'FeatureCollection', 'features': [], - 'numberMatched': self._get_count(params) } + if self.count or resulttype == 'hits': + matched = self._get_count(params) + LOGGER.debug(f'Found {matched} result(s)') + fc['numberMatched'] = matched + if resulttype == 'hits': return fc @@ -168,7 +172,7 @@ def query(self, offset=0, limit=10, resulttype='results', params['resultOffset'] = offset params['resultRecordCount'] = limit - hits_ = min(limit, fc['numberMatched']) + hits_ = min(limit, matched) if self.count else limit fc['features'] = self._get_all(params, hits_) fc['numberReturned'] = len(fc['features']) diff --git a/pygeoapi/provider/geojson.py b/pygeoapi/provider/geojson.py index 9109c8a15e..5062182fe6 100644 --- a/pygeoapi/provider/geojson.py +++ b/pygeoapi/provider/geojson.py @@ -188,7 +188,8 @@ def query(self, offset=0, limit=10, resulttype='results', properties=properties, select_properties=select_properties) - data['numberMatched'] = len(data['features']) + if self.count or resulttype == 'hits': + data['numberMatched'] = len(data['features']) if resulttype == 'hits': data['features'] = [] diff --git a/pygeoapi/provider/mongo.py b/pygeoapi/provider/mongo.py index 6c1bab729e..049174f3f9 100644 --- a/pygeoapi/provider/mongo.py +++ b/pygeoapi/provider/mongo.py @@ -102,7 +102,6 @@ def _get_feature_list(self, filterObj, sortList=[], skip=0, maxitems=1, if sortList: featurecursor = featurecursor.sort(sortList) - matchCount = self.featuredb[self.collection].count_documents(filterObj) featurecursor.skip(skip) featurecursor.limit(maxitems) featurelist = list(featurecursor) @@ -111,7 +110,7 @@ def _get_feature_list(self, filterObj, sortList=[], skip=0, maxitems=1, if skip_geometry: item['geometry'] = None - return featurelist, matchCount + return featurelist @crs_transform def query(self, offset=0, limit=10, resulttype='results', @@ -144,20 +143,28 @@ def query(self, offset=0, limit=10, resulttype='results', ASCENDING if (sort['order'] == '+') else DESCENDING) for sort in sortby] - featurelist, matchcount = self._get_feature_list( - filterobj, sortList=sort_list, skip=offset, maxitems=limit, - skip_geometry=skip_geometry) - - if resulttype == 'hits': - featurelist = [] - feature_collection = { 'type': 'FeatureCollection', - 'features': featurelist, - 'numberMatched': matchcount, - 'numberReturned': len(featurelist) + 'features': [] } + if self.count or resulttype == 'hits': + matched = self.featuredb[self.collection].count_documents( + filterobj) + LOGGER.debug(f'Found {matched} result(s)') + feature_collection['numberMatched'] = matched + + if resulttype == 'hits': + return feature_collection + + featurelist = self._get_feature_list( + filterobj, sortList=sort_list, skip=offset, maxitems=limit, + skip_geometry=skip_geometry + ) + + feature_collection['features'] = featurelist + feature_collection['numberReturned'] = len(featurelist) + return feature_collection @crs_transform @@ -168,8 +175,7 @@ def get(self, identifier, **kwargs): :param identifier: feature id :returns: dict of single GeoJSON feature """ - featurelist, matchcount = self._get_feature_list( - {'_id': ObjectId(identifier)}) + featurelist = self._get_feature_list({'_id': ObjectId(identifier)}) if featurelist: return featurelist[0] else: diff --git a/pygeoapi/provider/socrata.py b/pygeoapi/provider/socrata.py index 4ffdd4f587..c96c207cca 100644 --- a/pygeoapi/provider/socrata.py +++ b/pygeoapi/provider/socrata.py @@ -123,10 +123,14 @@ def query(self, offset=0, limit=10, resulttype='results', fc = { 'type': 'FeatureCollection', - 'features': [], - 'numberMatched': self._get_count(params) + 'features': [] } + if self.count or resulttype == 'hits': + matched = self._get_count(params) + LOGGER.debug(f'Found {matched} result(s)') + fc['numberMatched'] = matched + if resulttype == 'hits': # Return hits LOGGER.debug('Returning hits') diff --git a/pygeoapi/provider/sql.py b/pygeoapi/provider/sql.py index 410f57c309..868ee4f884 100644 --- a/pygeoapi/provider/sql.py +++ b/pygeoapi/provider/sql.py @@ -220,6 +220,7 @@ def query( crs_transform_out = get_transform_from_spec(crs_transform_spec) + response['numberReturned'] = 0 for item in ( results.order_by(*order_by_clauses).offset(offset).limit(limit) ): diff --git a/pygeoapi/provider/tinydb_.py b/pygeoapi/provider/tinydb_.py index c121af8386..abad7c5191 100644 --- a/pygeoapi/provider/tinydb_.py +++ b/pygeoapi/provider/tinydb_.py @@ -229,7 +229,10 @@ def query(self, offset=0, limit=10, resulttype='results', LOGGER.error(f'{msg}: {err}') raise ProviderInvalidQueryError(msg) - feature_collection['numberMatched'] = len(results) + if self.count or resulttype == 'hits': + matched = len(results) + LOGGER.debug(f'Found {matched} result(s)') + feature_collection['numberMatched'] = matched if resulttype == 'hits': return feature_collection diff --git a/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml b/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml index aaee641ec1..4d0b77dd8c 100644 --- a/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml +++ b/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml @@ -546,6 +546,10 @@ properties: type: boolean description: whether the resource is editable default: false + count: + type: boolean + description: whether to perform a count query for collection queries + default: true table: type: string description: table name for RDBMS-based providers diff --git a/tests/provider/test_csv__provider.py b/tests/provider/test_csv__provider.py index d4427e85cc..481d197e9e 100644 --- a/tests/provider/test_csv__provider.py +++ b/tests/provider/test_csv__provider.py @@ -144,6 +144,19 @@ def test_query(config): assert len(results['features'][0]['properties']) == 2 +def test_no_count(config): + p = CSVProvider(config) + results = p.query() + assert results['numberMatched'] == 5 + assert results['numberReturned'] == 5 + + config['count'] = False + p = CSVProvider(config) + results = p.query() + assert 'numberMatched' not in results + assert results['numberReturned'] == 5 + + def test_get_invalid_property(config): """Testing query for an invalid property name""" p = CSVProvider(config) diff --git a/tests/provider/test_esri_provider.py b/tests/provider/test_esri_provider.py index 65870de79d..1be9bddb40 100644 --- a/tests/provider/test_esri_provider.py +++ b/tests/provider/test_esri_provider.py @@ -85,6 +85,19 @@ def test_query(config): assert results['numberMatched'] == 406 +def test_no_count(config): + p = ESRIServiceProvider(config) + results = p.query() + assert results['numberMatched'] == 406 + assert results['numberReturned'] == 10 + + config['count'] = False + p = ESRIServiceProvider(config) + results = p.query() + assert 'numberMatched' not in results + assert results['numberReturned'] == 10 + + def test_geometry(config): p = ESRIServiceProvider(config) diff --git a/tests/provider/test_geojson_provider.py b/tests/provider/test_geojson_provider.py index 7db4dd0fde..4961e3e5fd 100644 --- a/tests/provider/test_geojson_provider.py +++ b/tests/provider/test_geojson_provider.py @@ -117,6 +117,20 @@ def test_get(fixture, config): assert 'Dinagat' in results['properties']['name'] +def test_no_count(fixture, config): + p = GeoJSONProvider(config) + + results = p.query() + assert results['numberMatched'] == 1 + assert results['numberReturned'] == 1 + + config['count'] = False + p = GeoJSONProvider(config) + results = p.query() + assert 'numberMatched' not in results + assert results['numberReturned'] == 1 + + def test_get_not_existing_item_raise_exception( fixture, config ): diff --git a/tests/provider/test_mongo_provider.py b/tests/provider/test_mongo_provider.py index 279f672d38..b4a7eeda01 100644 --- a/tests/provider/test_mongo_provider.py +++ b/tests/provider/test_mongo_provider.py @@ -111,6 +111,20 @@ def test_get(config): assert 'Reykjavik' in result['properties']['ls_name'] +def test_no_count(config): + p = MongoProvider(config) + + results = p.query() + assert results['numberMatched'] == 243 + assert results['numberReturned'] == 10 + + config['count'] = False + p = MongoProvider(config) + results = p.query() + assert 'numberMatched' not in results + assert results['numberReturned'] == 10 + + def test_get_not_existing_item_raise_exception(config): """Testing query for a not existing object""" p = MongoProvider(config) diff --git a/tests/provider/test_mysql_provider.py b/tests/provider/test_mysql_provider.py index c92ec01fdc..6812c4c354 100644 --- a/tests/provider/test_mysql_provider.py +++ b/tests/provider/test_mysql_provider.py @@ -134,6 +134,20 @@ def test_query_with_paging(config): assert feature_collection['numberReturned'] == ALL_ITEMS_IN_DB - 3 +def test_no_count(config): + """Test query with no count""" + p = MySQLProvider(config) + feature_collection = p.query() + assert feature_collection['numberMatched'] == 5 + assert feature_collection['numberReturned'] == 5 + + config['count'] = False + p = MySQLProvider(config) + feature_collection = p.query() + assert 'numberMatched' not in feature_collection + assert feature_collection['numberReturned'] == 5 + + def test_query_bbox(config): """Test query with a specified bounding box""" p = MySQLProvider(config) diff --git a/tests/provider/test_postgresql_provider.py b/tests/provider/test_postgresql_provider.py index b2e5ae0ae4..b725999058 100644 --- a/tests/provider/test_postgresql_provider.py +++ b/tests/provider/test_postgresql_provider.py @@ -246,6 +246,20 @@ def test_query_with_property_filter(config): assert feature_collection['numberReturned'] == 50 +def test_no_count(config): + """Test query with count disabled""" + p = PostgreSQLProvider(config) + results = p.query() + assert results['numberMatched'] == 14776 + assert results['numberReturned'] == 10 + + config['count'] = False + p = PostgreSQLProvider(config) + results = p.query() + assert 'numberMatched' not in results + assert results['numberReturned'] == 10 + + def test_query_with_paging(config): """Test query valid features with paging""" p = PostgreSQLProvider(config) diff --git a/tests/provider/test_socrata_provider.py b/tests/provider/test_socrata_provider.py index 51667ef4bf..bfe8b99799 100644 --- a/tests/provider/test_socrata_provider.py +++ b/tests/provider/test_socrata_provider.py @@ -191,6 +191,20 @@ def test_query(config, mock_socrata): assert results['numberMatched'] == 1006 +def test_no_count(config, mock_socrata): + p = SODAServiceProvider(config) + + results = p.query() + assert results['numberMatched'] == 1006 + assert results['numberReturned'] == 10 + + config['count'] = False + p = SODAServiceProvider(config) + results = p.query() + assert 'numberMatched' not in results + assert results['numberReturned'] == 10 + + def test_geometry(config, mock_socrata): p = SODAServiceProvider(config) diff --git a/tests/provider/test_tinydb_provider.py b/tests/provider/test_tinydb_provider.py index 19d8d8f4d2..109dbf4ab8 100644 --- a/tests/provider/test_tinydb_provider.py +++ b/tests/provider/test_tinydb_provider.py @@ -194,6 +194,20 @@ def test_get(config): assert result['properties']['FLOW'] == 2.059999942779541 +def test_no_count(config): + p = TinyDBProvider(config) + + results = p.query() + assert results['numberMatched'] == 50 + assert results['numberReturned'] == 10 + + config['count'] = False + p = TinyDBProvider(config) + results = p.query() + assert 'numberMatched' not in results + assert results['numberReturned'] == 10 + + def test_get_not_existing_item_raise_exception(config): """Testing query for a not existing object""" p = TinyDBProvider(config) diff --git a/tests/pygeoapi-test-config.yml b/tests/pygeoapi-test-config.yml index e64afbf286..44cf8f6c6d 100644 --- a/tests/pygeoapi-test-config.yml +++ b/tests/pygeoapi-test-config.yml @@ -244,6 +244,7 @@ resources: - type: feature name: GeoJSON data: tests/data/ne_110m_lakes.geojson + count: false id_field: id crs: - http://www.opengis.net/def/crs/OGC/1.3/CRS84 From 703a805af9c7eb9d6a473df83675ca9f5c95c41d Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Tue, 20 Jan 2026 20:28:47 -0500 Subject: [PATCH 2/4] Revert collection entry --- tests/pygeoapi-test-config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/pygeoapi-test-config.yml b/tests/pygeoapi-test-config.yml index 44cf8f6c6d..e64afbf286 100644 --- a/tests/pygeoapi-test-config.yml +++ b/tests/pygeoapi-test-config.yml @@ -244,7 +244,6 @@ resources: - type: feature name: GeoJSON data: tests/data/ne_110m_lakes.geojson - count: false id_field: id crs: - http://www.opengis.net/def/crs/OGC/1.3/CRS84 From 4fb0ed5c27798b1d553529b11006558bdc564f7d Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Thu, 7 May 2026 22:52:15 -0400 Subject: [PATCH 3/4] Add count for SQLite.GPKG --- pygeoapi/provider/sqlite.py | 41 +++++++++---------- .../test_sqlite_geopackage_provider.py | 14 +++++++ 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/pygeoapi/provider/sqlite.py b/pygeoapi/provider/sqlite.py index 340071af26..d3c9d19dc0 100644 --- a/pygeoapi/provider/sqlite.py +++ b/pygeoapi/provider/sqlite.py @@ -169,18 +169,6 @@ def __response_feature(self, row_data, skip_geometry=False): else: return None - def __response_feature_hits(self, hits): - """Assembles GeoJSON/Feature number - - :returns: GeoJSON FeaturesCollection - """ - - feature_collection = {"features": [], - "type": "FeatureCollection"} - feature_collection['numberMatched'] = hits - - return feature_collection - def __load(self): """ Private method for loading spatiallite, @@ -305,14 +293,25 @@ def query(self, offset=0, limit=10, resulttype='results', where_clause, where_values = self.__get_where_clauses( properties=properties, bbox=bbox) - if resulttype == 'hits': + feature_collection = { + 'type': 'FeatureCollection', + 'features': [] + } + + if self.count or resulttype == 'hits': - sql_query = f"SELECT COUNT(*) as hits FROM {self.table} {where_clause} " # noqa + sql_query = ( + 'SELECT COUNT(*) as hits ' + f'FROM {self.table} {where_clause}' + ) res = self.cursor.execute(sql_query, where_values) - hits = res.fetchone()["hits"] - return self.__response_feature_hits(hits) + hits = res.fetchone()['hits'] + feature_collection['numberMatched'] = hits + + if resulttype == 'hits': + return feature_collection sql_query = f"SELECT DISTINCT {self.columns} from \ {self.table} {where_clause} limit ? offset ?" @@ -326,14 +325,12 @@ def query(self, offset=0, limit=10, resulttype='results', row_data = self.cursor.execute( sql_query, where_values + (limit, offset)) - feature_collection = { - 'type': 'FeatureCollection', - 'features': [] - } - + feature_collection['numberReturned'] = 0 for rd in row_data: feature_collection['features'].append( - self.__response_feature(rd, skip_geometry=skip_geometry)) + self.__response_feature(rd, skip_geometry=skip_geometry) + ) + feature_collection['numberReturned'] += 1 return feature_collection diff --git a/tests/provider/test_sqlite_geopackage_provider.py b/tests/provider/test_sqlite_geopackage_provider.py index c5a26ebceb..28fcac3c9b 100644 --- a/tests/provider/test_sqlite_geopackage_provider.py +++ b/tests/provider/test_sqlite_geopackage_provider.py @@ -164,6 +164,20 @@ def test_query_bbox_sqlite_geopackage(config_sqlite): boxed_feature_collection['features'][0]['properties']['name'] +def test_no_count(config_sqlite): + """Test query with count disabled""" + p = SQLiteGPKGProvider(config_sqlite) + results = p.query() + assert results['numberMatched'] == 177 + assert results['numberReturned'] == 10 + + config_sqlite['count'] = False + p = SQLiteGPKGProvider(config_sqlite) + results = p.query() + assert 'numberMatched' not in results + assert results['numberReturned'] == 10 + + def test_get_sqlite(config_sqlite): p = SQLiteGPKGProvider(config_sqlite) result = p.get(118) From 11c81828a1f7e29783b76d5142536531098f13e9 Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Thu, 7 May 2026 23:35:53 -0400 Subject: [PATCH 4/4] Update docs --- docs/source/configuration.rst | 4 ++- docs/source/publishing/ogcapi-features.rst | 40 +++++++++++++--------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 8cd1b10d59..cc3798c094 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -273,7 +273,9 @@ default. include_extra_query_parameters: false # include extra query parameters that are not part of the collection properties (default: false) # editable transactions: DO NOT ACTIVATE unless you have setup access control beyond pygeoapi editable: true # optional: if backend is writable, default is false - count: true # optional: perform a count query for collection queries, default is true + # count: include `numberMatched` in collection results responses, for providers + # that require an additional query to calculate this value (e.g. a SQL COUNT query). + count: true # optional: perform additional count on queries, default is true # coordinate reference systems (CRS) section is optional # default CRSs are http://www.opengis.net/def/crs/OGC/1.3/CRS84 (coordinates without height) # and http://www.opengis.net/def/crs/OGC/1.3/CRS84h (coordinates with ellipsoidal height) diff --git a/docs/source/publishing/ogcapi-features.rst b/docs/source/publishing/ogcapi-features.rst index 8a38e0b984..b2a9c1f061 100644 --- a/docs/source/publishing/ogcapi-features.rst +++ b/docs/source/publishing/ogcapi-features.rst @@ -16,30 +16,36 @@ parameters. .. csv-table:: - :header: Provider, property filters/display, resulttype, bbox, datetime, sortby, skipGeometry, domains, CQL, transactions, crs + :header: Provider, property filters/display, resulttype hits/count, bbox, datetime, sortby, skipGeometry, domains, CQL, transactions, crs :align: left - `CSV`_,✅/✅,results/hits,✅,❌,❌,✅,❌,❌,❌,✅ - `Elasticsearch`_,✅/✅,results/hits,✅,✅,✅,✅,✅,✅,✅,✅ - `ERDDAP Tabledap Service`_,❌/❌,results/hits,✅,✅,❌,❌,❌,❌,❌,✅ - `ESRI Feature Service`_,✅/✅,results/hits,✅,✅,✅,✅,❌,❌,❌,✅ - `GeoJSON`_,✅/✅,results/hits,✅,❌,❌,✅,❌,❌,❌,✅ - `MongoDB`_,✅/❌,results,✅,✅,✅,✅,❌,❌,❌,✅ - `MySQL`_,✅/✅,results/hits,✅,✅,✅,✅,❌,✅,✅,✅ - `OGR`_,✅/❌,results/hits,✅,❌,❌,✅,❌,❌,❌,✅ - `OpenSearch`_,✅/✅,results/hits,✅,✅,✅,✅,❌,✅,✅,✅ - `Oracle`_,✅/✅,results/hits,✅,❌,✅,✅,❌,❌,❌,✅ - `Parquet`_,✅/✅,results/hits,✅,✅,❌,✅,❌,❌,❌,✅ - `PostgreSQL`_,✅/✅,results/hits,✅,✅,✅,✅,❌,✅,✅,✅ - `SQLiteGPKG`_,✅/❌,results/hits,✅,❌,❌,✅,❌,❌,❌,✅ - `SensorThings API`_,✅/✅,results/hits,✅,✅,✅,✅,❌,❌,✅,✅ - `Socrata`_,✅/✅,results/hits,✅,✅,✅,✅,❌,❌,❌,✅ - `TinyDB`_,✅/✅,results/hits,✅,✅,✅,✅,✅,❌,✅,✅ + `CSV`_,✅/✅,✅/✅,✅,❌,❌,✅,❌,❌,❌,✅ + `Elasticsearch`_,✅/✅,✅/❌,✅,✅,✅,✅,✅,✅,✅,✅ + `ERDDAP Tabledap Service`_,❌/❌,❌/❌,✅,✅,❌,❌,❌,❌,❌,✅ + `ESRI Feature Service`_,✅/✅,✅/✅,✅,✅,✅,✅,❌,❌,❌,✅ + `GeoJSON`_,✅/✅,✅/✅,✅,❌,❌,✅,❌,❌,❌,✅ + `MongoDB`_,✅/❌,✅/✅,✅,✅,✅,✅,❌,❌,❌,✅ + `MySQL`_,✅/✅,✅/✅,✅,✅,✅,✅,❌,✅,✅,✅ + `OGR`_,✅/❌,✅/❌,✅,❌,❌,✅,❌,❌,❌,✅ + `OpenSearch`_,✅/✅,✅/❌,✅,✅,✅,✅,❌,✅,✅,✅ + `Oracle`_,✅/✅,✅/✅,✅,❌,✅,✅,❌,❌,❌,✅ + `Parquet`_,✅/✅,✅/❌,✅,✅,❌,✅,❌,❌,❌,✅ + `PostgreSQL`_,✅/✅,✅/✅,✅,✅,✅,✅,❌,✅,✅,✅ + `SQLiteGPKG`_,✅/❌,✅/✅,✅,❌,❌,✅,❌,❌,❌,✅ + `SensorThings API`_,✅/✅,✅/❌,✅,✅,✅,✅,❌,❌,✅,✅ + `Socrata`_,✅/✅,✅/✅,✅,✅,✅,✅,❌,❌,❌,✅ + `TinyDB`_,✅/✅,✅/✅,✅,✅,✅,✅,✅,❌,✅,✅ .. note:: For more information on CRS transformations, see :ref:`crs`. +.. note:: + Providers that support the query parameter ``resulttype=hits`` will also + include ``numberMatched`` in the default ``resulttype=results`` response. + Providers that support the configuration option ``count: false`` will + not include the ``numberMatched`` property in the ``results`` response. + Connection examples -------------------