diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6e765fb..8d0adcf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,17 +1,17 @@ name: Build -on: +on: push: paths-ignore: - - '**.md' + - '**.md' pull_request: branches: - master - paths-ignore: - - '!**.md' + paths-ignore: + - '!**.md' release: types: - - released + - released jobs: flake8_py3: @@ -78,7 +78,7 @@ jobs: - name: Install sqlite and gpkg dependencies uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: libsqlite3-mod-spatialite + packages: libsqlite3-mod-spatialite version: 4.3.0a-6build1 - name: Install requirements 📦 run: | @@ -96,6 +96,7 @@ jobs: python3 tests/load_es_data.py tests/cite/ogcapi-features/canada-hydat-daily-mean-02HC003.geojson IDENTIFIER python3 tests/load_mongo_data.py tests/data/ne_110m_populated_places_simple.geojson gunzip < tests/data/hotosm_bdi_waterways.sql.gz | psql postgresql://postgres:${{ secrets.DatabasePassword || 'postgres' }}@localhost:5432/test + psql postgresql://postgres:${{ secrets.DatabasePassword || 'postgres' }}@localhost:5432/test -f tests/data/dummy_data.sql - name: run unit tests ⚙️ env: POSTGRESQL_PASSWORD: ${{ secrets.DatabasePassword || 'postgres' }} diff --git a/pygeoapi/provider/postgresql.py b/pygeoapi/provider/postgresql.py index 8a29384..41935cf 100644 --- a/pygeoapi/provider/postgresql.py +++ b/pygeoapi/provider/postgresql.py @@ -319,11 +319,31 @@ class PostgreSQLProvider(BaseProvider): raise ProviderQueryError(msg) Base = automap_base(metadata=metadata) - Base.prepare() + Base.prepare( + name_for_scalar_relationship=self._name_for_scalar_relationship, + ) TableModel = getattr(Base.classes, self.table) return TableModel + @staticmethod + def _name_for_scalar_relationship( + base, local_cls, referred_cls, constraint, + ): + """Function used when automapping classes and relationships from + database schema and fixes potential naming conflicts. + """ + name = referred_cls.__name__.lower() + local_table = local_cls.__table__ + if name in local_table.columns: + newname = name + '_' + LOGGER.debug( + f'Already detected column name {name!r} in table ' + f'{local_table!r}. Using {newname!r} for relationship name.' + ) + return newname + return name + def _sqlalchemy_to_feature(self, item): feature = { 'type': 'Feature' diff --git a/tests/data/dummy_data.sql b/tests/data/dummy_data.sql new file mode 100644 index 0000000..d475529 --- /dev/null +++ b/tests/data/dummy_data.sql @@ -0,0 +1,56 @@ +CREATE SCHEMA IF NOT EXISTS dummy AUTHORIZATION postgres; + +CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA dummy; + +-- table with multiple geometry columns +CREATE TABLE IF NOT EXISTS dummy.buildings( + gid serial PRIMARY KEY, + centroid geometry(POINT, 25833), + contours geometry(POLYGON, 25833) +); + +INSERT INTO dummy.buildings(centroid, contours) +VALUES (ST_GeomFromText('POINT (473449 7463146)', 25833), + ST_GeomFromText('POLYGON ((473447.9967755177 7463140.685534775, 473453.51980463834 7463143.029921546, 473450.0032244818 7463151.314465227, 473444.4801953612 7463148.970078456, 473447.9967755177 7463140.685534775))', 25833)), + (ST_GeomFromText('POINT (473458 7463104)', 25833), + ST_GeomFromText('POLYGON ((473460.9359104787 7463106.762323238, 473457.1106914547 7463107.931810057, 473455.06408952177 7463101.237676765, 473458.88930854574 7463100.068189946, 473460.9359104787 7463106.762323238))', 25833)), + (ST_GeomFromText('POINT (473446 7463144)', 25833), + ST_GeomFromText('POLYGON ((473446.09474694915 7463138.853056925, 473450.31999101397 7463146.79958526, 473445.9052530499 7463149.146943075, 473441.6800089851 7463141.20041474, 473446.09474694915 7463138.853056925))', 25833)), + (ST_GeomFromText('POINT (473449 7463142)', 25833), + ST_GeomFromText('POLYGON ((473452.3381955018 7463138.820935548, 473452.65221123956 7463144.812712757, 473445.6618044963 7463145.179064451, 473445.3477887586 7463139.187287242, 473452.3381955018 7463138.820935548))', 25833)), + (ST_GeomFromText('POINT (473443 7463137)', 25833), + ST_GeomFromText('POLYGON ((473447.7083111685 7463135.5571535295, 473440.9159249468 7463141.46168479, 473438.2916888306 7463138.44284647, 473445.0840750523 7463132.538315209, 473447.7083111685 7463135.5571535295))', 25833)), + (ST_GeomFromText('POINT (473433 7463125)', 25833), + ST_GeomFromText('POLYGON ((473432.73905580025 7463120.082489641, 473436.8249702975 7463128.10154836, 473433.2609442007 7463129.917510359, 473429.1750297034 7463121.898451641, 473432.73905580025 7463120.082489641))', 25833)), + (ST_GeomFromText('POINT (473451 7463140)', 25833), + ST_GeomFromText('POLYGON ((473454.99435667787 7463139.456755368, 473453.4959303038 7463143.165490787, 473447.00564332213 7463140.543244633, 473448.5040696962 7463136.834509214, 473454.99435667787 7463139.456755368))', 25833)), + (ST_GeomFromText('POINT (473438 7463144)', 25833), + ST_GeomFromText('POLYGON ((473438.99554283824 7463137.7143898895, 473444.28561010957 7463144.995542839, 473437.00445716083 7463150.28561011, 473431.7143898895 7463143.00445716, 473438.99554283824 7463137.7143898895))', 25833)), + (ST_GeomFromText('POINT (473474 7463101)', 25833), + ST_GeomFromText('POLYGON ((473474.83006438427 7463097.491297516, 473477.55805782415 7463100.416712323, 473473.1699356148 7463104.508702483, 473470.441942174 7463101.583287676, 473474.83006438427 7463097.491297516))', 25833)), + -- gid 10 + (NULL, + ST_GeomFromText('POLYGON ((473464.1495667333 7463116.574655892, 473461.1307284124 7463119.1988920085, 473457.85043326765 7463115.425344108, 473460.8692715885 7463112.8011079915, 473464.1495667333 7463116.574655892))', 25833)), + -- gid 11 + (ST_GeomFromText('POINT (473461 7463116)', 25833), + NULL), + -- gid 12 + (NULL, + NULL); + +/* Two tables which create a naming conflict + +The name of relationship or referred table is the same as the name of an +existing column. Example adapted from +https://docs-sqlalchemy.readthedocs.io/ko/latest/orm/extensions/automap.html#handling-simple-naming-conflicts +*/ +CREATE TABLE IF NOT EXISTS dummy.referred_table( + id INTEGER PRIMARY KEY +); + +CREATE TABLE IF NOT EXISTS dummy.naming_conflicts_table( + id INTEGER PRIMARY KEY, + point_geom geometry(POINT, 4326), + referred_table INTEGER, + FOREIGN KEY(referred_table) REFERENCES dummy.referred_table(id) +); diff --git a/tests/pygeoapi-test-config-postgresql.yml b/tests/pygeoapi-test-config-postgresql.yml index feeb349..df72c8d 100644 --- a/tests/pygeoapi-test-config-postgresql.yml +++ b/tests/pygeoapi-test-config-postgresql.yml @@ -134,4 +134,35 @@ resources: search_path: [osm, public] id_field: osm_id table: hotosm_bdi_waterways - geom_field: foo_geom \ No newline at end of file + geom_field: foo_geom + dummy_naming_conflicts: + type: collection + title: Dummy data + description: Dummy data creating naming conflicts + keywords: + - dummy + links: + - type: text/html + rel: canonical + title: Source instructions for loading data + href: https://github.com/geopython/pygeoapi/blob/master/pygeoapi/provider/postgresql.py + hreflang: en-UK + extents: + spatial: + bbox: [-180, -90, 180, 90] + crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 + temporal: + begin: null + end: null # or empty (either means open ended) + providers: + - type: feature + name: PostgreSQL + data: + host: localhost + dbname: test + user: postgres + password: postgres + search_path: [dummy] + id_field: id + table: naming_conflicts_table + geom_field: point_geom diff --git a/tests/test_postgresql_provider.py b/tests/test_postgresql_provider.py index 477e04a..18a7da2 100644 --- a/tests/test_postgresql_provider.py +++ b/tests/test_postgresql_provider.py @@ -570,3 +570,17 @@ def test_post_collection_items_postgresql_cql_bad_cql(pg_api_, bad_cql): error_response = json.loads(response) assert error_response['code'] == 'InvalidParameterValue' assert error_response['description'].startswith('Bad CQL string') + + +def test_get_collection_items_postgresql_automap_naming_conflicts(pg_api_): + """ + Test that PostgreSQLProvider can handle naming conflicts when automapping + classes and relationships from database schema. + """ + req = mock_request() + rsp_headers, code, response = pg_api_.get_collection_items( + req, 'dummy_naming_conflicts') + + assert code == HTTPStatus.OK + features = json.loads(response).get('features') + assert len(features) == 0