diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e491b20..15272f5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -43,27 +43,10 @@ jobs: env: PYGEOAPI_CONFIG: "$(pwd)/pygeoapi-config.yml" - services: - # Oracle service (label used to access the service container) - oracle: - # Docker Hub image (feel free to change the tag "latest" to any other available one) - image: gvenzl/oracle-xe:latest - # Provide passwords and other environment variables to container - env: - ORACLE_RANDOM_PASSWORD: true - APP_USER: geo_test - APP_USER_PASSWORD: geo_test - # Forward Oracle port - ports: - - 1521:1521 - # Provide healthcheck script options for startup - options: >- - --health-cmd healthcheck.sh - --health-interval 10s - --health-timeout 5s - --health-retries 10 - steps: + - name: Chown user + run: | + sudo chown -R $USER:$USER $GITHUB_WORKSPACE - uses: actions/checkout@v2 - uses: actions/setup-python@v2 name: Setup Python ${{ matrix.python-version }} @@ -105,6 +88,9 @@ jobs: with: packages: gdal-bin libgdal-dev version: 3.0.4 + - name: Install and run Oracle + run: | + docker run -d --name oracledb -e ORACLE_PWD=oracle -v ${{ github.workspace }}/tests/data/oracle/init-db:/opt/oracle/scripts/startup -p 1521:1521 container-registry.oracle.com/database/express:21.3.0-xe - name: Install requirements 📦 run: | pip3 install -r requirements.txt @@ -125,6 +111,7 @@ jobs: 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 + docker ps python3 tests/load_oracle_data.py - name: run unit tests ⚙️ env: diff --git a/docs/source/data-publishing/ogcapi-features.rst b/docs/source/data-publishing/ogcapi-features.rst index 15ef485..6277362 100644 --- a/docs/source/data-publishing/ogcapi-features.rst +++ b/docs/source/data-publishing/ogcapi-features.rst @@ -26,6 +26,7 @@ parameters. `GeoJSON`_,✅/✅,results/hits,❌,❌,❌,✅,❌,❌,✅ `MongoDB`_,✅/❌,results,✅,✅,✅,✅,❌,❌,✅ `OGR`_,✅/❌,results/hits,✅,❌,❌,✅,❌,❌,✅ + `Oracle`_,✅/✅,results/hits,✅,❌,✅,✅,❌,❌,✅ `PostgreSQL`_,✅/✅,results/hits,✅,✅,✅,✅,✅,❌,✅ `SQLiteGPKG`_,✅/❌,results/hits,✅,❌,❌,✅,❌,❌,✅ `SensorThings API`_,✅/✅,results/hits,✅,✅,✅,✅,❌,❌,✅ @@ -274,6 +275,8 @@ Oracle .. note:: Requires Python package oracledb +Connection +"""""""""" .. code-block:: yaml providers: @@ -295,21 +298,64 @@ Oracle table: lakes geom_field: geometry title_field: name - # sql_manipulator: tests.test_oracle_provider.SqlManipulator - # sql_manipulator_options: - # foo: bar - # mandatory_properties: - # - bbox - # source_crs: 31287 # defaults to 4326 if not provided - # target_crs: 31287 # defaults to 4326 if not provided -The provider supports connection over host and port with SID or SERVICE_NAME. For TNS naming, the system +The provider supports connection over host and port with SID, SERVICE_NAME or TNS_NAME. For TNS naming, the system environment variable TNS_ADMIN or the configuration parameter tns_admin must be set. The providers supports external authentication. At the moment only wallet authentication is implemented. Sometimes it is necessary to use the Oracle client for the connection. In this case init_oracle_client must be set to True. +SDO options +""""""""""" +.. code-block:: yaml + + providers: + - type: feature + name: OracleDB + data: + host: 127.0.0.1 + port: 1521 + service_name: XEPDB1 + user: geo_test + password: geo_test + id_field: id + table: lakes + geom_field: geometry + title_field: name + sdo_operator: sdo_relate # defaults to sdo_filter + sdo_param: mask=touch+coveredby # defaults to mask=anyinteract + +The provider supports two different SDO operators, sdo_filter and sdo_relate. When not set, the default is sdo_relate! +Further more it is possible to set the sdo_param option. When sdo_relate is used the default is anyinteraction! +`See Oracle Documentation for details `_. + +Mandatory properties +"""""""""""""""""""" +.. code-block:: yaml + + providers: + - type: feature + name: OracleDB + data: + host: 127.0.0.1 + port: 1521 + service_name: XEPDB1 + user: geo_test + password: geo_test + id_field: id + table: lakes + geom_field: geometry + title_field: name + manadory_properties: + - example_group_id + +On large tables it could be useful to disallow a query on the complete dataset. For this reason it is possible to +configure mandatory properties. When this is activated, the provoder throws an exception when the parameter +is not in the query uri. + +Custom SQL Manipulator Plugin +""""""""""""""""""""""""""""" The provider supports a SQL-Manipulator-Plugin class. With this, the SQL statement could be manipulated. This is useful e.g. for authorization at row level or manipulation of the explain plan with hints. diff --git a/pygeoapi/provider/oracle.py b/pygeoapi/provider/oracle.py index 1acb5ea..255dc31 100644 --- a/pygeoapi/provider/oracle.py +++ b/pygeoapi/provider/oracle.py @@ -30,17 +30,23 @@ import importlib import json import logging +import oracledb +import pyproj from typing import Optional -import oracledb +from pygeoapi.api import DEFAULT_STORAGE_CRS from pygeoapi.provider.base import ( BaseProvider, ProviderConnectionError, + ProviderGenericError, + ProviderInvalidQueryError, ProviderItemNotFoundError, ProviderQueryError, ) +from pygeoapi.util import get_crs_from_uri + LOGGER = logging.getLogger(__name__) @@ -97,7 +103,7 @@ class DatabaseConnection: ) if "tns_name" not in self.conn_dict: - raise Exception( + raise ProviderConnectionError( "tns_name must be set for external authentication!" ) @@ -111,7 +117,7 @@ class DatabaseConnection: ) if "host" not in self.conn_dict: - raise Exception( + raise ProviderConnectionError( "Host must be set for connection with service_name!" ) @@ -128,7 +134,7 @@ class DatabaseConnection: ) if "host" not in self.conn_dict: - raise Exception( + raise ProviderConnectionError( "Host must be set for connection with sid!" ) @@ -184,7 +190,8 @@ class DatabaseConnection: LOGGER.error(e) raise ProviderConnectionError(e) - # Check if table name has schema inside + # Check if table name has schema/owner inside + # If not, current user is set table_parts = self.table.split(".") if len(table_parts) == 2: schema = table_parts[0] @@ -196,34 +203,23 @@ class DatabaseConnection: LOGGER.debug("Schema: " + schema) LOGGER.debug("Table: " + table) - self.cur = self.conn.cursor() if self.context == "query": - # Get table column names and types, excluding geometry - query_cols = "select column_name, data_type \ - from all_tab_columns \ - where table_name = UPPER(:table_name) \ - and owner = UPPER(:owner) \ - and data_type != 'SDO_GEOMETRY'" - - self.cur.execute( - query_cols, {"table_name": table, "owner": schema} - ) - result = self.cur.fetchall() + column_list = self._get_table_columns(schema, table) # When self.properties is set, then the result would be filtered if self.properties: - result = [ - res - for res in result - if res[0].lower() + column_list = [ + col + for col in column_list + if col[0].lower() in [item.lower() for item in self.properties] ] # Concatenate column names with ', ' - self.columns = ", ".join([item[0].lower() for item in result]) + self.columns = ", ".join([item[0].lower() for item in column_list]) # Populate dictionary for columns with column type - for k, v in dict(result).items(): + for k, v in dict(column_list).items(): self.fields[k.lower()] = {"type": v} return self @@ -232,6 +228,82 @@ class DatabaseConnection: # some logic to commit/rollback self.conn.close() + def _get_table_columns(self, schema, table): + """ + Returns an array with all column names and data types + from Oracle table ALL_TAB_COLUMNS. + Lookup for public and private synonyms. + Throws ProviderGenericError when table not exist or accesable. + """ + + sql = """ + SELECT COUNT(1) + FROM all_objects + WHERE object_type IN ('VIEW','TABLE','MATERIALIZED VIEW') + AND object_name = UPPER(:table_name) + AND owner = UPPER(:owner) + """ + with self.conn.cursor() as cur: + cur.execute(sql, {"table_name": table, "owner": schema}) + result = cur.fetchone() + + if result[0] == 0: + sql = """ + SELECT COUNT(1) + FROM all_synonyms + WHERE synonym_name = UPPER(:table_name) + AND owner = UPPER(:owner) + """ + with self.conn.cursor() as cur: + cur.execute(sql, {"table_name": table, "owner": schema}) + result = cur.fetchone() + + if result[0] == 0: + sql = """ + SELECT COUNT(1) + FROM all_synonyms + WHERE synonym_name = UPPER(:table_name) + AND owner = 'PUBLIC' + """ + with self.conn.cursor() as cur: + cur.execute(sql, {"table_name": table}) + result = cur.fetchone() + + if result[0] == 0: + raise ProviderGenericError( + f"Table {schema}.{table} not found!" + ) + + else: + schema = "PUBLIC" + + sql = """ + SELECT table_owner, table_name + FROM all_synonyms + WHERE synonym_name = UPPER(:table_name) + AND owner = UPPER(:owner) + """ + with self.conn.cursor() as cur: + cur.execute(sql, {"table_name": table, "owner": schema}) + result = cur.fetchone() + + schema = result[0] + table = result[1] + + # Get table column names and types, excluding geometry + query_cols = """ + SELECT column_name, data_type + FROM all_tab_columns + WHERE table_name = UPPER(:table_name) + AND owner = UPPER(:owner) + AND data_type != 'SDO_GEOMETRY' + """ + with self.conn.cursor() as cur: + cur.execute(query_cols, {"table_name": table, "owner": schema}) + result = cur.fetchall() + + return result + class OracleProvider(BaseProvider): def __init__(self, provider_def): @@ -248,28 +320,42 @@ class OracleProvider(BaseProvider): super().__init__(provider_def) + # Table properties self.table = provider_def["table"] self.id_field = provider_def["id_field"] self.conn_dic = provider_def["data"] self.geom = provider_def["geom_field"] self.properties = [item.lower() for item in self.properties] + self.mandatory_properties = provider_def.get("mandatory_properties") + # SQL manipulator properties self.sql_manipulator = provider_def.get("sql_manipulator") self.sql_manipulator_options = provider_def.get( "sql_manipulator_options" ) - self.mandatory_properties = provider_def.get("mandatory_properties") - self.source_crs = provider_def.get("source_crs", 4326) - self.target_crs = provider_def.get("target_crs", 4326) - self.sdo_mask = provider_def.get("sdo_mask", "anyinteraction") + + # CRS properties + storage_crs_uri = provider_def.get("storage_crs", DEFAULT_STORAGE_CRS) + self.storage_crs = get_crs_from_uri(storage_crs_uri) + + # TODO See Issue #1393 + # default_crs_uri = provider_def.get("default_crs", DEFAULT_CRS) + # self.default_crs = get_crs_from_uri(default_crs_uri) + + # SDO properties + self.sdo_param = provider_def.get("sdo_param") + self.sdo_operator = provider_def.get("sdo_operator", "sdo_filter") LOGGER.debug("Setting Oracle properties:") LOGGER.debug(f"Name:{self.name}") LOGGER.debug(f"ID_field:{self.id_field}") LOGGER.debug(f"Table:{self.table}") - LOGGER.debug(f"source_crs: {self.source_crs}") - LOGGER.debug(f"target_crs: {self.target_crs}") - LOGGER.debug(f"sdo_mask: {self.sdo_mask}") + LOGGER.debug(f"sdo_param: {self.sdo_param}") + LOGGER.debug(f"sdo_operator: {self.sdo_operator}") + LOGGER.debug(f"storage_crs {self.storage_crs}") + + # TODO See Issue #1393 + # LOGGER.debug(f"default_crs: {self.default_crs}") self.get_fields() @@ -289,7 +375,12 @@ class OracleProvider(BaseProvider): return self.fields def _get_where_clauses( - self, properties, bbox, bbox_crs, sdo_mask="anyinteraction" + self, + properties, + bbox, + bbox_crs, + sdo_param=None, + sdo_operator="sdo_filter", ): """ Generarates WHERE conditions to be implemented in query. @@ -313,33 +404,72 @@ class OracleProvider(BaseProvider): if bbox: bbox_dict = {"clause": "", "properties": {}} - sdo_mask = f"mask={sdo_mask}" + if sdo_operator == "sdo_relate": + if not sdo_param: + sdo_param = "mask=anyinteract" - bbox_dict["properties"] = { - "srid": bbox_crs or 4326, - "minx": bbox[0], - "miny": bbox[1], - "maxx": bbox[2], - "maxy": bbox[3], - "sdo_mask": sdo_mask, - } + bbox_dict["properties"] = { + "srid": self._get_srid_from_crs(bbox_crs), + "minx": bbox[0], + "miny": bbox[1], + "maxx": bbox[2], + "maxy": bbox[3], + "sdo_param": sdo_param, + } - bbox_dict[ - "clause" - ] = f"sdo_relate({self.geom}, \ - mdsys.sdo_geometry(2003, \ - :srid, \ - NULL, \ - mdsys.sdo_elem_info_array(\ - 1, \ - 1003, \ - 3\ - ), \ - mdsys.sdo_ordinate_array(:minx, \ - :miny, \ - :maxx, \ - :maxy)), \ - :sdo_mask) = 'TRUE'" + bbox_query = f""" + sdo_relate({self.geom}, + mdsys.sdo_geometry(2003, + :srid, + NULL, + mdsys.sdo_elem_info_array( + 1, + 1003, + 3 + ), + mdsys.sdo_ordinate_array( + :minx, + :miny, + :maxx, + :maxy + ) + ), + :sdo_param + ) = 'TRUE' + """ + + else: + bbox_dict["properties"] = { + "srid": self._get_srid_from_crs(bbox_crs), + "minx": bbox[0], + "miny": bbox[1], + "maxx": bbox[2], + "maxy": bbox[3], + "sdo_param": sdo_param, + } + + bbox_query = f""" + sdo_filter({self.geom}, + mdsys.sdo_geometry(2003, + :srid, + NULL, + mdsys.sdo_elem_info_array( + 1, + 1003, + 3 + ), + mdsys.sdo_ordinate_array( + :minx, + :miny, + :maxx, + :maxy + ) + ), + :sdo_param + ) = 'TRUE' + """ + + bbox_dict["clause"] = bbox_query where_conditions.append(bbox_dict["clause"]) where_dict["properties"].update(bbox_dict["properties"]) @@ -381,6 +511,20 @@ class OracleProvider(BaseProvider): oracledb.DB_TYPE_LONG_RAW, arraysize=cursor.arraysize ) + def _get_srid_from_crs(self, crs): + """ + Works only for EPSG codes! + Anything else is hard coded! + """ + if crs == "OGC:CRS84": + srid = 4326 + elif crs == "OGC:CRS84h": + srid = 4326 + else: + srid = crs.to_epsg() + + return srid + def query( self, offset=0, @@ -390,9 +534,11 @@ class OracleProvider(BaseProvider): datetime_=None, properties=[], sortby=[], - select_properties=[], skip_geometry=False, + select_properties=[], + crs_transform_spec=None, q=None, + language=None, filterq=None, **kwargs, ): @@ -419,12 +565,12 @@ class OracleProvider(BaseProvider): if self.mandatory_properties: for mand_col in self.mandatory_properties: if mand_col == "bbox" and not bbox: - raise ProviderQueryError( + raise ProviderInvalidQueryError( f"Missing mandatory filter property: {mand_col}" ) else: if mand_col not in property_dict: - raise ProviderQueryError( + raise ProviderInvalidQueryError( f"Missing mandatory filter property: {mand_col}" ) @@ -440,8 +586,9 @@ class OracleProvider(BaseProvider): where_dict = self._get_where_clauses( properties=properties, bbox=bbox, - bbox_crs=self.source_crs, - sdo_mask=self.sdo_mask, + bbox_crs=self.storage_crs, + sdo_param=self.sdo_param, + sdo_operator=self.sdo_operator, ) # Not dangerous to use self.table as substitution, @@ -481,26 +628,49 @@ class OracleProvider(BaseProvider): where_dict = self._get_where_clauses( properties=properties, bbox=bbox, - bbox_crs=self.source_crs, - sdo_mask=self.sdo_mask, + bbox_crs=self.storage_crs, + sdo_param=self.sdo_param, + sdo_operator=self.sdo_operator, ) + # Get correct SRID + if crs_transform_spec is not None: + source_crs = pyproj.CRS.from_wkt( + crs_transform_spec.source_crs_wkt + ) + source_srid = self._get_srid_from_crs(source_crs) + + target_crs = pyproj.CRS.from_wkt( + crs_transform_spec.target_crs_wkt + ) + target_srid = self._get_srid_from_crs(target_crs) + else: + source_srid = self._get_srid_from_crs(self.storage_crs) + target_srid = source_srid + + # TODO See Issue #1393 + # target_srid = self._get_srid_from_crs(self.default_crs) + # If issue is not accepted, this block can be merged with + # the following block. + + LOGGER.debug(f"source_srid: {source_srid}") + LOGGER.debug(f"target_srid: {target_srid}") + # Build geometry column call # When a different output CRS is definded, the geometry # geometry column would be transformed. if skip_geometry: geom = "" - elif ( - not skip_geometry - and self.target_crs - and self.target_crs != self.source_crs - ): - geom = f", sdo_cs.transform(t1.{self.geom}, \ - :target_srid).get_geojson() \ - AS geometry " + + elif source_srid != target_srid: + geom = f""", sdo_cs.transform(t1.{self.geom}, + :target_srid).get_geojson() + AS geometry """ + where_dict["properties"].update( - {"target_srid": int(self.target_crs)} + {"target_srid": int(target_srid)} ) + else: geom = f", t1.{self.geom}.get_geojson() AS geometry " @@ -534,9 +704,19 @@ class OracleProvider(BaseProvider): sql_query, bind_variables, self.sql_manipulator_options, + offset, + limit, + resulttype, bbox, - self.source_crs, + datetime_, properties, + sortby, + skip_geometry, + select_properties, + crs_transform_spec, + q, + language, + filterq, ) # Clean up placeholders that aren't used by the @@ -622,7 +802,7 @@ class OracleProvider(BaseProvider): return id - def get(self, identifier, **kwargs): + def get(self, identifier, crs_transform_spec=None, **kwargs): """ Query the provider for a specific feature id e.g: /collections/ocrl_lakes/items/1 @@ -640,11 +820,41 @@ class OracleProvider(BaseProvider): cursor = db.conn.cursor() crs_dict = {} - if self.target_crs and self.target_crs != self.source_crs: - geom_sql = f", sdo_cs.transform(t1.{self.geom}, \ - :target_srid).get_geojson() \ - AS geometry " - crs_dict = {"target_srid": int(self.target_crs)} + + # Get correct SRIDs + if crs_transform_spec is not None: + source_crs = pyproj.CRS.from_wkt( + crs_transform_spec.source_crs_wkt + ) + source_srid = self._get_srid_from_crs(source_crs) + + target_crs = pyproj.CRS.from_wkt( + crs_transform_spec.target_crs_wkt + ) + target_srid = self._get_srid_from_crs(target_crs) + + else: + source_srid = self._get_srid_from_crs(self.storage_crs) + target_srid = source_srid + + # TODO See Issue #1393 + # target_srid = self._get_srid_from_crs(self.default_crs) + # If issue is not accepted, this block can be merged with + # the following block. + + LOGGER.debug(f"source_srid: {source_srid}") + LOGGER.debug(f"target_srid: {target_srid}") + + # Build geometry column call + # When a different output CRS is definded, the geometry + # geometry column would be transformed. + if source_srid != target_srid: + crs_dict = {"target_srid": target_srid} + + geom_sql = f""", sdo_cs.transform(t1.{self.geom}, + :target_srid).get_geojson() + AS geometry """ + else: geom_sql = f", t1.{self.geom}.get_geojson() AS geometry " @@ -779,24 +989,32 @@ class OracleProvider(BaseProvider): columns_str = ", ".join([col for col in columns]) values_str = ", ".join([f":{col}" for col in columns]) - sql_query = f"INSERT INTO {self.table} (\ - {columns_str}, \ - {self.geom}) \ - VALUES ({values_str}, :in_geometry) \ - RETURNING {self.id_field} INTO :out_id" + sql_query = f""" + INSERT INTO {self.table} ( + {columns_str}, + {self.geom} + ) + VALUES ( + {values_str}, + sdo_util.from_geojson(:in_geometry, NULL, :srid) + ) + RETURNING {self.id_field} INTO :out_id + """ # Out bind variable for the id of the created row out_id = cursor.var(int) # Bind variable for the SDO_GEOMETRY type - in_geometry = self._get_sdo_from_geojson_geometry( - db.conn, request_data.get("geometry").get("coordinates")[0] - ) + # in_geometry = self._get_sdo_from_geojson_geometry( + # db.conn, request_data.get("geometry").get("coordinates")[0] + # ) + in_geometry = request_data.get("geometry") bind_variables = { **bind_variables, "out_id": out_id, - "in_geometry": in_geometry, + "in_geometry": json.dumps(in_geometry), + "srid": self._get_srid_from_crs(self.storage_crs), } # SQL manipulation plugin @@ -872,20 +1090,28 @@ class OracleProvider(BaseProvider): set_str = ", ".join([f" {col} = :{col}" for col in columns]) - sql_query = f"UPDATE {self.table} \ - SET {set_str} \ - , {self.geom} = :in_geometry \ - WHERE {self.id_field} = :in_id" + sql_query = f""" + UPDATE {self.table} + SET {set_str} + , {self.geom} = sdo_util.from_geojson( + :in_geometry, + NULL, + :srid + ) + WHERE {self.id_field} = :in_id + """ # Bind variable for the SDO_GEOMETRY type - in_geometry = self._get_sdo_from_geojson_geometry( - db.conn, request_data.get("geometry").get("coordinates")[0] - ) + # in_geometry = self._get_sdo_from_geojson_geometry( + # db.conn, request_data.get("geometry").get("coordinates")[0] + # ) + in_geometry = json.dumps(request_data.get("geometry")) bind_variables = { **bind_variables, "in_id": identifier, "in_geometry": in_geometry, + "srid": self._get_srid_from_crs(self.storage_crs), } # SQL manipulation plugin diff --git a/tests/data/oracle/init-db/01-create-user.sql b/tests/data/oracle/init-db/01-create-user.sql new file mode 100755 index 0000000..3f3d7a8 --- /dev/null +++ b/tests/data/oracle/init-db/01-create-user.sql @@ -0,0 +1,5 @@ +CONNECT sys/oracle@XEPDB1 AS SYSDBA; + +CREATE USER geo_test IDENTIFIED BY geo_test QUOTA UNLIMITED ON USERS; + +GRANT CONNECT, RESOURCE, DBA TO geo_test; \ No newline at end of file diff --git a/tests/load_oracle_data.py b/tests/load_oracle_data.py index c3eef45..886f6b4 100644 --- a/tests/load_oracle_data.py +++ b/tests/load_oracle_data.py @@ -23,6 +23,19 @@ CREATE TABLE geo_test.lakes ( cur.execute(sql) +sql = """ +CREATE PUBLIC SYNONYM lakes_public_syn FOR geo_test.lakes +""" + +cur.execute(sql) + +sql = """ +CREATE SYNONYM geo_test.lakes_private_syn FOR geo_test.lakes +""" + +cur.execute(sql) + + sql = """ INSERT INTO lakes (area, volume, name, wiki_link, geometry) VALUES (NULL, NULL, 'Lake Baikal', diff --git a/tests/test_filesystem_provider.py b/tests/test_filesystem_provider.py index a007380..84e9164 100644 --- a/tests/test_filesystem_provider.py +++ b/tests/test_filesystem_provider.py @@ -54,7 +54,7 @@ def test_query(config): r = p.get_data_path(baseurl, urlpath, dirpath) - assert len(r['links']) == 10 + assert len(r['links']) == 11 r = p.get_data_path(baseurl, urlpath, '/poi_portugal') diff --git a/tests/test_oracle_provider.py b/tests/test_oracle_provider.py index f89e5af..7488231 100644 --- a/tests/test_oracle_provider.py +++ b/tests/test_oracle_provider.py @@ -48,9 +48,19 @@ class SqlManipulator: sql_query, bind_variables, sql_manipulator_options, + offset, + limit, + resulttype, bbox, - source_crs, + datetime_, properties, + sortby, + skip_geometry, + select_properties, + crs_transform_spec, + q, + language, + filterq, ): sql = "ID = 10 AND :foo != :bar" @@ -137,6 +147,44 @@ def config(): } +@pytest.fixture() +def config_public_synonym(): + return { + "name": "Oracle", + "type": "feature", + "data": { + "host": HOST, + "port": PORT, + "service_name": SERVICE_NAME, + "user": USERNAME, + "password": PASSWORD, + }, + "id_field": "id", + "table": "lakes_public_syn", + "geom_field": "geometry", + "editable": True, + } + + +@pytest.fixture() +def config_private_synonym(): + return { + "name": "Oracle", + "type": "feature", + "data": { + "host": HOST, + "port": PORT, + "service_name": SERVICE_NAME, + "user": USERNAME, + "password": PASSWORD, + }, + "id_field": "id", + "table": "lakes_private_syn", + "geom_field": "geometry", + "editable": True, + } + + @pytest.fixture() def config_manipulator(config): return { @@ -178,6 +226,18 @@ def create_geojson(): } +@pytest.fixture() +def create_point_geojson(): + return { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [9.603316032965449, 47.48872063967191], + }, + "properties": {"name": "Yachthafen Fischerinsel", "wiki_link": None}, + } + + @pytest.fixture() def update_geojson(): return { @@ -235,6 +295,38 @@ def test_get_fields(config): assert provider.fields == expected_fields +def test_get_fields_private_synonym(config_private_synonym): + """Test get_fields from private synonym""" + expected_fields = { + "id": {"type": "NUMBER"}, + "area": {"type": "NUMBER"}, + "volume": {"type": "NUMBER"}, + "name": {"type": "VARCHAR2"}, + "wiki_link": {"type": "VARCHAR2"}, + } + + provider = OracleProvider(config_private_synonym) + + assert provider.get_fields() == expected_fields + assert provider.fields == expected_fields + + +def test_get_fields_public_synonym(config_public_synonym): + """Test get_fields from public synonym""" + expected_fields = { + "id": {"type": "NUMBER"}, + "area": {"type": "NUMBER"}, + "volume": {"type": "NUMBER"}, + "name": {"type": "VARCHAR2"}, + "wiki_link": {"type": "VARCHAR2"}, + } + + provider = OracleProvider(config_public_synonym) + + assert provider.get_fields() == expected_fields + assert provider.fields == expected_fields + + def test_get_fields_properties(config_properties): """ Test get_fields with subset of columns. @@ -453,3 +545,15 @@ def test_delete_sql_manipulator(config_manipulator, config): down = p2.query(sortby=[{"property": "id", "order": "-"}]) assert down["features"][0]["id"] == identifier + + +def test_create_point(config, create_point_geojson): + """Test simple create""" + p = OracleProvider(config) + result = p.create(create_point_geojson) + + assert result == 28 + + data = p.get(28) + + assert data.get("geometry").get("type") == "Point"