From ca7f8fc1f6eef84dc191d22f067f9dddcb5d5fac Mon Sep 17 00:00:00 2001 From: totycro Date: Wed, 27 Sep 2023 13:07:23 +0200 Subject: [PATCH] Oracle Provider (#1329) * Added Oracle Provider * Changed author * Modified formatting with Black * Adapt Python Flake8 style * Adapted line length * Flake8 * Line length <= 79 * Added Oracle provider into CI/CD * Changed code style to flake8 * style: tabs to spaces * style: line length * style: trailing whitespaces * Changed dictionary concat to old style * Fixed skip geometry error. * Added first set of unit tests * Deleted whitespaces * Added Oracle provider documentation * First version Part 4 (CRUD) * First version OGC API Feature Part 4 (CRUD) * Changed style for flake8 * Style: trailing whitespaces * style: line too long * style: line too long * CRUD: Added update * flake nervt * CRUD: update + delete * Added tests + fixed errors * Updated docs * Added test_get.. + Error fixing * Worked reviews in * Added pull request comments --------- Co-authored-by: Andreas Kosubek Co-authored-by: xkosubek <133005275+xkosubek@users.noreply.github.com> --- .github/workflows/main.yml | 24 +- .../data-publishing/ogcapi-features.rst | 48 + pygeoapi/plugin.py | 1 + pygeoapi/provider/oracle.py | 1023 +++++++ requirements-provider.txt | 1 + tests/load_oracle_data.py | 2342 +++++++++++++++++ tests/test_oracle_provider.py | 455 ++++ 7 files changed, 3893 insertions(+), 1 deletion(-) create mode 100644 pygeoapi/provider/oracle.py create mode 100644 tests/load_oracle_data.py create mode 100644 tests/test_oracle_provider.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 38fe44a..047a172 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,7 +42,27 @@ jobs: - python-version: 3.7 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: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 @@ -105,6 +125,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 + python3 tests/load_oracle_data.py - name: run unit tests ⚙️ env: POSTGRESQL_PASSWORD: ${{ secrets.DatabasePassword || 'postgres' }} @@ -126,6 +147,7 @@ jobs: pytest tests/test_ogr_sqlite_provider.py pytest tests/test_ogr_wfs_provider.py pytest tests/test_openapi.py + pytest tests/test_oracle_provider.py pytest tests/test_postgresql_provider.py pytest tests/test_rasterio_provider.py pytest tests/test_sensorthings_provider.py diff --git a/docs/source/data-publishing/ogcapi-features.rst b/docs/source/data-publishing/ogcapi-features.rst index 139094c..c9bb993 100644 --- a/docs/source/data-publishing/ogcapi-features.rst +++ b/docs/source/data-publishing/ogcapi-features.rst @@ -266,6 +266,54 @@ Here `test` is the name of database , `points` is the target collection name. data: mongodb://localhost:27017/testdb collection: testplaces +.. _Oracle: + +Oracle +^^^^^^ + +.. note:: + Requires Python package oracledb + +.. code-block:: yaml + + providers: + - type: feature + name: OracleDB + data: + host: 127.0.0.1 + port: 1521 # defaults to 1521 if not provided + service_name: XEPDB1 + # sid: XEPDB1 + user: geo_test + password: geo_test + # external_auth: wallet + # tns_name: XEPDB1 + # tns_admin /opt/oracle/client/network/admin + # init_oracle_client: True + + id_field: id + 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 +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. + +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. + +An example an more informations about that feature you can find in the test class in tests/test_oracle_provider.py. .. _PostgreSQL: diff --git a/pygeoapi/plugin.py b/pygeoapi/plugin.py index bb0383e..632d908 100644 --- a/pygeoapi/plugin.py +++ b/pygeoapi/plugin.py @@ -50,6 +50,7 @@ PLUGINS = { 'MapScript': 'pygeoapi.provider.mapscript_.MapScriptProvider', 'MongoDB': 'pygeoapi.provider.mongo.MongoProvider', 'MVT': 'pygeoapi.provider.mvt.MVTProvider', + 'OracleDB': 'pygeoapi.provider.oracle.OracleProvider', 'OGR': 'pygeoapi.provider.ogr.OGRProvider', 'PostgreSQL': 'pygeoapi.provider.postgresql.PostgreSQLProvider', 'rasterio': 'pygeoapi.provider.rasterio_.RasterioProvider', diff --git a/pygeoapi/provider/oracle.py b/pygeoapi/provider/oracle.py new file mode 100644 index 0000000..1acb5ea --- /dev/null +++ b/pygeoapi/provider/oracle.py @@ -0,0 +1,1023 @@ +# ================================================================= +# +# Authors: Andreas Kosubek +# +# Copyright (c) 2023 Andreas Kosubek +# +# 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 importlib +import json +import logging +from typing import Optional + +import oracledb + +from pygeoapi.provider.base import ( + BaseProvider, + ProviderConnectionError, + ProviderItemNotFoundError, + ProviderQueryError, +) + +LOGGER = logging.getLogger(__name__) + + +class DatabaseConnection: + """Database connection class to be used as 'with' statement. + The class returns a connection object. + """ + + def __init__(self, conn_dic, table, properties=[], context="query"): + """ + OracleProvider Class constructor + + :param conn_dic: dictionary with connection parameters + service_name - the service name of the database instance + sid - sid of the database instance + tns_name - name of the tnsnames.ora entry + external_auth - External authentication e.g. wallet + tns_admin - path to tnsnames.ora configuration file + user - user name used to authenticate + password - password used to authenticate + host - database host address + port - connection port number + (defaults to 1521 if not provided) + init_oracle_client - + + :param table: table name containing the data. This variable is used to + assemble column information + :param properties: User-specified subset of column names to expose + :param context: query or hits, if query then it will determine + table column otherwise will not do it + :returns: DatabaseConnection + """ + + self.conn_dict = conn_dic + self.table = table + self.context = context + self.columns = ( + None # Comma sepparated string with column names (for SQL query) + ) + self.properties = [item.lower() for item in properties] + self.fields = {} # Dict of columns. Key is col name, value is type + self.conn = None + + def __enter__(self): + try: + if self.conn_dict.get("init_oracle_client", False): + oracledb.init_oracle_client() + + # Connect with tnsnames.ora entry and Login with Oracle Wallet + if self.conn_dict.get("external_auth") == "wallet": + LOGGER.debug( + "Oracle connect with tnsnames.ora entry \ + and login with Oracle Wallet" + ) + + if "tns_name" not in self.conn_dict: + raise Exception( + "tns_name must be set for external authentication!" + ) + + dsn = self.conn_dict["tns_name"] + + # Connect with SERVICE_NAME + if "service_name" in self.conn_dict: + LOGGER.debug( + f"Oracle connect with service_name: \ + {self.conn_dict['service_name']}" + ) + + if "host" not in self.conn_dict: + raise Exception( + "Host must be set for connection with service_name!" + ) + + dsn = oracledb.makedsn( + self.conn_dict["host"], + self.conn_dict.get("port", 1521), + service_name=self.conn_dict["service_name"], + ) + + # Connect with SID + elif "sid" in self.conn_dict: + LOGGER.debug( + f"Oracle connect with sid: {self.conn_dict['sid']}" + ) + + if "host" not in self.conn_dict: + raise Exception( + "Host must be set for connection with sid!" + ) + + dsn = oracledb.makedsn( + self.conn_dict["host"], + self.conn_dict.get("port", 1521), + sid=self.conn_dict["sid"], + ) + + # Connect with tnsnames.ora entry + elif "tns_name" in self.conn_dict: + LOGGER.debug( + f"Oracle connect with tns_name: \ + {self.conn_dict['tns_name']}" + ) + dsn = self.conn_dict["tns_name"] + + else: + raise ProviderConnectionError( + "One of service_name, sid or tns_name must be specified!" + ) + + LOGGER.debug(f"Oracle DSN string: {dsn}") + + # Connect with tnsnames.ora entry and Login with Oracle Wallet + if self.conn_dict.get("external_auth") == "wallet": + self.conn = oracledb.connect(externalauth=True, dsn=dsn) + + # Connect with tnsnames.ora entry, + # TNS_ADMIN is set via configuration + if "tns_admin" in self.conn_dict: + self.conn = oracledb.connect( + user=self.conn_dict["user"], + password=self.conn_dict["password"], + dsn=dsn, + config_dir=self.conn_dict["tns_admin"], + ) + + # Connect with user / password via dsn string + # When dsn is a TNS name, the environment variable TNS_ADMIN must + # be set (Path to tnsnames.ora file) + else: + self.conn = oracledb.connect( + user=self.conn_dict["user"], + password=self.conn_dict["password"], + dsn=dsn, + ) + + except oracledb.DatabaseError as e: + LOGGER.error( + f"Couldn't connect to Oracle using:{str(self.conn_dict)}" + ) + LOGGER.error(e) + raise ProviderConnectionError(e) + + # Check if table name has schema inside + table_parts = self.table.split(".") + if len(table_parts) == 2: + schema = table_parts[0] + table = table_parts[1] + else: + schema = self.conn_dict["user"] + table = self.table + + 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() + + # When self.properties is set, then the result would be filtered + if self.properties: + result = [ + res + for res in result + if res[0].lower() + in [item.lower() for item in self.properties] + ] + + # Concatenate column names with ', ' + self.columns = ", ".join([item[0].lower() for item in result]) + + # Populate dictionary for columns with column type + for k, v in dict(result).items(): + self.fields[k.lower()] = {"type": v} + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # some logic to commit/rollback + self.conn.close() + + +class OracleProvider(BaseProvider): + def __init__(self, provider_def): + """ + OracleProvider Class constructor + + :param provider_def: provider definitions from yml pygeoapi-config. + data,id_field, name set in parent class + data contains the connection information + for class DatabaseCursor + + :returns: pygeoapi.provider.base.OracleProvider + """ + + super().__init__(provider_def) + + 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.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") + + 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}") + + self.get_fields() + + def get_fields(self): + """ + Get fields from Oracle table (columns are field) + + :returns: dict of fields + """ + LOGGER.debug("Get available fields/properties") + + if not self.fields: + with DatabaseConnection( + self.conn_dic, self.table, properties=self.properties + ) as db: + self.fields = db.fields + return self.fields + + def _get_where_clauses( + self, properties, bbox, bbox_crs, sdo_mask="anyinteraction" + ): + """ + Generarates WHERE conditions to be implemented in query. + Private method mainly associated with query method + :param properties: list of tuples (name, value) + :param bbox: bounding box [minx,miny,maxx,maxy] + + :returns: Dictionary with sql where clause and bind variables + """ + LOGGER.debug("Get where clause with bind variables as dictionary") + + where_dict = {"clause": "", "properties": {}} + + where_conditions = [] + + if properties: + prop_clauses = [f"{key} = :{key}" for key, value in properties] + where_conditions += prop_clauses + where_dict["properties"] = dict(properties) + + if bbox: + bbox_dict = {"clause": "", "properties": {}} + + sdo_mask = f"mask={sdo_mask}" + + 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[ + "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'" + + where_conditions.append(bbox_dict["clause"]) + where_dict["properties"].update(bbox_dict["properties"]) + + if where_conditions: + where_dict["clause"] = f" WHERE {' AND '.join(where_conditions)}" + + LOGGER.debug(where_dict) + + return where_dict + + def _get_orderby(self, sortby): + """ + Private function: Get ORDER BY clause + + :param sortby: list of dicts (property, order) + + :returns: STA $orderby string + """ + sort_map = {"+": "ASC", "-": "DESC"} + ret = [ + f"{sort['property']} {sort_map[sort['order']]}" for sort in sortby + ] + + return f"ORDER BY {','.join(ret)}" + + def _output_type_handler( + self, cursor, name, default_type, size, precision, scale + ): + """ + Output type handler for Oracle LOB datatypes + """ + if default_type == oracledb.DB_TYPE_CLOB: + return cursor.var( + oracledb.DB_TYPE_LONG, arraysize=cursor.arraysize + ) + if default_type == oracledb.DB_TYPE_BLOB: + return cursor.var( + oracledb.DB_TYPE_LONG_RAW, arraysize=cursor.arraysize + ) + + def query( + self, + offset=0, + limit=10, + resulttype="results", + bbox=None, + datetime_=None, + properties=[], + sortby=[], + select_properties=[], + skip_geometry=False, + q=None, + filterq=None, + **kwargs, + ): + """ + Query Oracle for all the content. + + :param offset: starting record to return (default 0) + :param limit: number of records to return (default 10) + :param resulttype: return results or hit limit (default results) + :param bbox: bounding box [minx,miny,maxx,maxy] + :param datetime_: temporal (datestamp or extent) + :param properties: list of tuples (name, value) + :param sortby: list of dicts (property, order) + :param select_properties: list of property names + :param skip_geometry: bool of whether to skip geometry (default False) + :param q: full-text search term(s) + :param filterq: CQL query as text string + + :returns: GeoJSON FeaturesCollection + """ + + # Check mandatory filter properties + property_dict = dict(properties) + if self.mandatory_properties: + for mand_col in self.mandatory_properties: + if mand_col == "bbox" and not bbox: + raise ProviderQueryError( + f"Missing mandatory filter property: {mand_col}" + ) + else: + if mand_col not in property_dict: + raise ProviderQueryError( + f"Missing mandatory filter property: {mand_col}" + ) + + if resulttype == "hits": + with DatabaseConnection( + self.conn_dic, + self.table, + properties=self.properties, + context="hits", + ) as db: + cursor = db.conn.cursor() + + where_dict = self._get_where_clauses( + properties=properties, + bbox=bbox, + bbox_crs=self.source_crs, + sdo_mask=self.sdo_mask, + ) + + # Not dangerous to use self.table as substitution, + # because of getFields ... + sql_query = f"SELECT COUNT(1) AS hits \ + FROM {self.table} \ + {where_dict['clause']}" + try: + cursor.execute(sql_query, where_dict["properties"]) + except oracledb.Error as err: + LOGGER.error( + f"Error executing sql_query: {sql_query}: {err}" + ) + raise ProviderQueryError() + + hits = cursor.fetchone()[0] + LOGGER.debug(f"hits: {str(hits)}") + + return self._response_feature_hits(hits) + + with DatabaseConnection( + self.conn_dic, self.table, properties=self.properties + ) as db: + db.conn.outputtypehandler = self._output_type_handler + + cursor = db.conn.cursor() + + # Create column list. + # Uses columns field that was generated in the Connection class + # or the configured columns from the Yaml file. + props = ( + db.columns + if select_properties == [] + else ", ".join([p for p in select_properties]) + ) + + where_dict = self._get_where_clauses( + properties=properties, + bbox=bbox, + bbox_crs=self.source_crs, + sdo_mask=self.sdo_mask, + ) + + # 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 " + where_dict["properties"].update( + {"target_srid": int(self.target_crs)} + ) + else: + geom = f", t1.{self.geom}.get_geojson() AS geometry " + + orderby = self._get_orderby(sortby) if sortby else "" + + # Create paging and add placeholders for the + # SQL manipulation class + paging_bind = {} + if limit > 0: + sql_query = f"SELECT #HINTS# {props} {geom} \ + FROM {self.table} t1 #JOIN# \ + {where_dict['clause']} #WHERE# \ + {orderby} \ + OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY" + paging_bind = {"offset": offset, "limit": limit} + else: + sql_query = f"SELECT #HINTS# {props} {geom} \ + FROM {self.table} t1 #JOIN# \ + {where_dict['clause']} #WHERE# \ + {orderby}" + + # Create dictionary for sql bind variables + bind_variables = {**where_dict["properties"], **paging_bind} + + # SQL manipulation plugin + if self.sql_manipulator: + LOGGER.debug("sql_manipulator: " + self.sql_manipulator) + manipulation_class = _class_factory(self.sql_manipulator) + sql_query, bind_variables = manipulation_class.process_query( + db, + sql_query, + bind_variables, + self.sql_manipulator_options, + bbox, + self.source_crs, + properties, + ) + + # Clean up placeholders that aren't used by the + # manipulation class. + sql_query = sql_query.replace("#HINTS#", "") + sql_query = sql_query.replace("#JOIN#", "") + sql_query = sql_query.replace("#WHERE#", "") + + LOGGER.debug(f"SQL Query: {sql_query}") + LOGGER.debug(f"Bind variables: {bind_variables}") + + try: + cursor.execute(sql_query, bind_variables) + except oracledb.Error as err: + LOGGER.error(f"Error executing sql_query: {sql_query}") + LOGGER.error(err) + raise ProviderQueryError() + + # Convert row resultset to dictionary + columns = [col[0] for col in cursor.description] + cursor.rowfactory = lambda *args: dict(zip(columns, args)) + + row_data = cursor.fetchall() + + # Generate feature JSON + features = [self._response_feature(rd) for rd in row_data] + feature_collection = { + "type": "FeatureCollection", + "features": features, + } + + return feature_collection + + def _get_previous(self, cursor, identifier): + """ + Query previous ID given current ID + + :param identifier: feature id + + :returns: feature id + """ + sql = f"SELECT {self.id_field} AS id \ + FROM {self.table} \ + WHERE ROWNUM = 1 \ + AND {self.id_field} < :{self.id_field} \ + ORDER BY {self.id_field} DESC" + + bind_variables = {self.id_field: identifier} + + LOGGER.debug(f"SQL Query: {sql}") + LOGGER.debug(f"Bind variables: {str(bind_variables)}") + + cursor.execute(sql, bind_variables) + + item = cursor.fetchall() + id = item[0][0] if item else None + + return id + + def _get_next(self, cursor, identifier): + """ + Query next ID given current ID + + :param identifier: feature id + + :returns: feature id + """ + sql = f"SELECT {self.id_field} AS id \ + FROM {self.table} \ + WHERE ROWNUM = 1 \ + AND {self.id_field} > :{self.id_field} \ + ORDER BY {self.id_field} ASC" + + bind_variables = {self.id_field: identifier} + + LOGGER.debug(f"SQL Query: {sql}") + LOGGER.debug(f"Bind variables: {str(bind_variables)}") + + cursor.execute(sql, bind_variables) + + item = cursor.fetchall() + id = item[0][0] if item else None + + return id + + def get(self, identifier, **kwargs): + """ + Query the provider for a specific + feature id e.g: /collections/ocrl_lakes/items/1 + + :param identifier: feature id + + :returns: GeoJSON FeaturesCollection + """ + + with DatabaseConnection( + self.conn_dic, self.table, properties=self.properties + ) as db: + db.conn.outputtypehandler = self._output_type_handler + + 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)} + else: + geom_sql = f", t1.{self.geom}.get_geojson() AS geometry " + + sql_query = f"SELECT {db.columns} {geom_sql} \ + FROM {self.table} t1 \ + WHERE {self.id_field} = :in_id" + + bind_variables = {"in_id": identifier, **crs_dict} + + # SQL manipulation plugin + if self.sql_manipulator: + LOGGER.debug("sql_manipulator: " + self.sql_manipulator) + manipulation_class = _class_factory(self.sql_manipulator) + sql_query, bind_variables = manipulation_class.process_get( + db, + sql_query, + bind_variables, + self.sql_manipulator_options, + identifier, + ) + + LOGGER.debug(f"SQL Query: {sql_query}") + LOGGER.debug(f"Identifier: {identifier}") + + try: + cursor.execute(sql_query, bind_variables) + except oracledb.Error as err: + LOGGER.error(f"Error executing sql_query: {sql_query}") + LOGGER.error(err) + raise ProviderQueryError() + + # Convert row resultset to dictionary + columns = [col[0] for col in cursor.description] + cursor.rowfactory = lambda *args: dict(zip(columns, args)) + + results = cursor.fetchall() + + row_data = None + if results: + row_data = results[0] + feature = self._response_feature(row_data) + + if feature: + previous_id = self._get_previous(cursor, identifier) + if previous_id: + feature["prev"] = previous_id + next_id = self._get_next(cursor, identifier) + if next_id: + feature["next"] = self._get_next(cursor, identifier) + return feature + else: + err = f"item identifier {identifier} not found" + LOGGER.error(err) + raise ProviderItemNotFoundError(err) + + def _response_feature(self, row_data): + """ + Assembles GeoJSON output from DB query + + :param row_data: DB row result + + :returns: `dict` of GeoJSON Feature + """ + + if row_data: + feature = {"type": "Feature"} + + if row_data.get("GEOMETRY"): + feature["geometry"] = json.loads(row_data["GEOMETRY"]) + else: + feature["geometry"] = None + + feature["properties"] = { + key.lower(): value + for (key, value) in row_data.items() + if key != "GEOMETRY" + } + feature["id"] = feature["properties"].pop(self.id_field) + + return feature + else: + return None + + def _response_feature_hits(self, hits): + """Assembles GeoJSON/Feature number + e.g: http://localhost:5000/collections/lakes/items?resulttype=hits + + :returns: GeoJSON FeaturesCollection + """ + + feature_collection = {"features": [], "type": "FeatureCollection"} + feature_collection["numberMatched"] = hits + + return feature_collection + + def create(self, request_data): + """ + Creates on record with the given data. + + :param request_data: Data of the record as Geojson + :returns: ID of the created record + """ + LOGGER.debug(f"Request data: {str(request_data)}") + + with DatabaseConnection( + self.conn_dic, self.table, properties=self.properties + ) as db: + cursor = db.conn.cursor() + + columns = [*request_data.get("properties")] + + # Filter properties to get only columns who are + # in the column list + columns = [ + col + for col in columns + if col.lower() in [field.lower() for field in self.fields] + ] + + # Flter function to get only properties who are + # in the column list + def filter_binds(pair): + return pair[0].lower() in [ + field.lower() for field in self.fields + ] + + # Filter bind variables + bind_variables = dict( + filter(filter_binds, request_data.get("properties").items()) + ) + + 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" + + # 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] + ) + + bind_variables = { + **bind_variables, + "out_id": out_id, + "in_geometry": in_geometry, + } + + # SQL manipulation plugin + if self.sql_manipulator: + LOGGER.debug("sql_manipulator: " + self.sql_manipulator) + manipulation_class = _class_factory(self.sql_manipulator) + sql_query, bind_variables = manipulation_class.process_create( + db, + sql_query, + bind_variables, + self.sql_manipulator_options, + request_data, + ) + + LOGGER.debug(f"SQL Query: {sql_query}") + LOGGER.debug(f"Bind variables: {bind_variables}") + + try: + cursor.execute(sql_query, bind_variables) + db.conn.commit() + except oracledb.Error as err: + LOGGER.error(f"Error executing sql_query: {sql_query}") + LOGGER.error(err) + + db.conn.rollback() + + raise ProviderQueryError() + + identifier = out_id.getvalue() + + return identifier[0] + + def update(self, identifier, request_data): + """ + Updates the record with the given identifier. + + :param identifier: ID of the record + :param request_data: Data of the record as Geojson + :returns: True + """ + LOGGER.debug(f"Identifier: {identifier}") + LOGGER.debug(f"Request data: {str(request_data)}") + + with DatabaseConnection( + self.conn_dic, self.table, properties=self.properties + ) as db: + cursor = db.conn.cursor() + + columns = [*request_data.get("properties")] + + # Filter properties to get only columns who are + # in the column list + columns = [ + col + for col in columns + if col.lower() in [field.lower() for field in self.fields] + ] + + # Flter function to get only properties who are + # in the column list + def filter_binds(pair): + return pair[0].lower() in [ + field.lower() for field in self.fields + ] + + # Filter bind variables + bind_variables = dict( + filter( + filter_binds, + request_data.get("properties").items(), + ) + ) + + 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" + + # Bind variable for the SDO_GEOMETRY type + in_geometry = self._get_sdo_from_geojson_geometry( + db.conn, request_data.get("geometry").get("coordinates")[0] + ) + + bind_variables = { + **bind_variables, + "in_id": identifier, + "in_geometry": in_geometry, + } + + # SQL manipulation plugin + if self.sql_manipulator: + LOGGER.debug("sql_manipulator: " + self.sql_manipulator) + manipulation_class = _class_factory(self.sql_manipulator) + sql_query, bind_variables = manipulation_class.process_update( + db, + sql_query, + bind_variables, + self.sql_manipulator_options, + identifier, + request_data, + ) + + LOGGER.debug(sql_query) + LOGGER.debug(bind_variables) + + try: + cursor.execute(sql_query, bind_variables) + rowcount = cursor.rowcount + db.conn.commit() + except oracledb.Error as err: + LOGGER.error(f"Error executing sql_query: {sql_query}") + LOGGER.error(err) + + db.conn.rollback() + + raise ProviderQueryError() + + return rowcount == 1 + + def delete(self, identifier): + """ + Deletes the record with the given identifier. + + :param identifier: ID of the record + :returns: True + """ + + LOGGER.debug(f"Identifier: {identifier}") + + with DatabaseConnection( + self.conn_dic, self.table, properties=self.properties + ) as db: + cursor = db.conn.cursor() + + sql_query = f"DELETE FROM {self.table} \ + WHERE {self.id_field} = :in_id" + + bind_variables = { + "in_id": identifier, + } + + # SQL manipulation plugin + if self.sql_manipulator: + LOGGER.debug("sql_manipulator: " + self.sql_manipulator) + manipulation_class = _class_factory(self.sql_manipulator) + sql_query, bind_variables = manipulation_class.process_delete( + db, + sql_query, + bind_variables, + self.sql_manipulator_options, + identifier, + ) + + LOGGER.debug(sql_query) + LOGGER.debug(bind_variables) + + try: + cursor.execute(sql_query, bind_variables) + rowcount = cursor.rowcount + db.conn.commit() + except oracledb.Error as err: + LOGGER.error(f"Error executing sql_query: {sql_query}") + LOGGER.error(err) + + db.conn.rollback() + + raise ProviderQueryError() + + return rowcount == 1 + + def _get_sdo_from_geojson_geometry(self, conn, geometry, srid=4326): + """ + Get an filled Python object for Oracle Type SDO_GEOMETRY. + + :param conn: oracledb connection instance + :param geometry: Ordinate Array from Geojson + :param srid: SRID defaults to 4326 when not provided + :return Python object instance: + """ + gtype = 2003 + elemInfo = [1, 1003, 1] + + # Get Oracle types + obj_type = conn.gettype("MDSYS.SDO_GEOMETRY") + element_info_type_obj = conn.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") + ordinate_type_obj = conn.gettype("MDSYS.SDO_ORDINATE_ARRAY") + + obj = obj_type.newobject() + obj.SDO_GTYPE = gtype + obj.SDO_SRID = srid or 4326 + obj.SDO_ELEM_INFO = element_info_type_obj.newobject() + obj.SDO_ELEM_INFO.extend(elemInfo) + obj.SDO_ORDINATES = ordinate_type_obj.newobject() + for coord in geometry: + obj.SDO_ORDINATES.extend(coord) + + return obj + + +def _class_factory( + module_class_string, super_cls: Optional[type] = None, **kwargs +): + """ + Factory function for class instances. + Used for dynamic loading of the SQL manipulation class. + + :param module_class_string: full name of the class to create an object of + :param super_cls: expected super class for validity, None if bypass + :param kwargs: parameters to pass + :return instance of the given class: + """ + module_name, class_name = module_class_string.rsplit(".", 1) + module = importlib.import_module(module_name) + LOGGER.debug(f"reading class {class_name} from module {module_name}") + cls = getattr(module, class_name) + if super_cls is not None: + assert issubclass( + cls, super_cls + ), f"class {class_name} should inherit from {super_cls.__name__}" + LOGGER.debug(f"initialising {class_name} with params {kwargs}") + obj = cls(**kwargs) + return obj diff --git a/requirements-provider.txt b/requirements-provider.txt index 2dc6fa4..c83a7d8 100644 --- a/requirements-provider.txt +++ b/requirements-provider.txt @@ -6,6 +6,7 @@ fiona #GDAL>=3.0.0 geoalchemy netCDF4 +oracledb pandas; python_version < '3.7' pandas==1.2.5; python_version >= '3.7' psycopg2 diff --git a/tests/load_oracle_data.py b/tests/load_oracle_data.py new file mode 100644 index 0000000..c3eef45 --- /dev/null +++ b/tests/load_oracle_data.py @@ -0,0 +1,2342 @@ +import oracledb + +oracle_user = "geo_test" +oracle_pwd = "geo_test" +oracle_tns = "XEPDB1" + +dsn = oracledb.makedsn("127.0.0.1", 1521, service_name=oracle_tns) + +conn = oracledb.connect(user=oracle_user, password=oracle_pwd, dsn=dsn) + +cur = conn.cursor() + +sql = """ +CREATE TABLE geo_test.lakes ( + id NUMBER GENERATED AS IDENTITY PRIMARY KEY, + area NUMBER, + volume NUMBER, + name VARCHAR2(255 CHAR), + wiki_link VARCHAR2(1024 CHAR), + geometry MDSYS.SDO_GEOMETRY +) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Baikal', + 'https://en.wikipedia.org/wiki/Lake_Baikal', + SDO_GEOMETRY( + 2003, + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), + SDO_ORDINATE_ARRAY( + 106.57998579307912, + 52.79998159444554 + , + 106.53998823448521, + 52.93999888774037 + , + 107.0800069519353, + 53.18001007751998 + , + 107.2999935242018, + 53.37999787048953 + , + 107.59997521365611, + 53.51998932556822 + , + 108.03994835818912, + 53.859968573616456 + , + 108.37997928266967, + 54.25999583598784 + , + 109.05270307824526, + 55.027597561251326 + , + 109.19346967980832, + 55.53560272889659 + , + 109.50699059452313, + 55.73091380474372 + , + 109.92980716353523, + 55.7129562445223 + , + 109.70000206913326, + 54.980003567110515 + , + 109.66000451053935, + 54.71999359803395 + , + 109.47996382043448, + 54.33999095317566 + , + 109.31997358605884, + 53.81999685323869 + , + 109.22003136600637, + 53.619983222052994 + , + 108.99999311730755, + 53.78002513286093 + , + 108.60001753136845, + 53.4399942083804 + , + 108.800005324338, + 53.37999787048953 + , + 108.76000776574409, + 53.200008856816936 + , + 108.45997439985749, + 53.14001251892607 + , + 108.17999148970011, + 52.79998159444554 + , + 107.79996300662566, + 52.579995022179034 + , + 107.31999230349876, + 52.42000478780339 + , + 106.64003380740229, + 52.32001089131862 + , + 106.1000150899522, + 52.03997630472897 + , + 105.740037062607, + 51.759993394571595 + , + 105.24001590375084, + 51.52000804300813 + , + 104.81998986208251, + 51.46001170511727 + , + 104.30002160036167, + 51.50000926371118 + , + 103.7600028829116, + 51.60000316019595 + , + 103.6200114278329, + 51.73999461527464 + , + 103.85999677939637, + 51.85998729105637 + , + 104.39996382041414, + 51.85998729105637 + , + 105.05997521364597, + 52.0000045843512 + , + 105.4800012553143, + 52.28001333272471 + , + 105.98002241417046, + 52.51999868428817 + , + 106.26000532432784, + 52.619992580772944 + , + 106.57998579307912, + 52.79998159444554 + ) + ) +) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Winnipeg', + 'https://en.wikipedia.org/wiki/Lake_Winnipeg', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -98.95540137408423, + 53.92978343364277 + , + -- + -97.95799455441879, + 54.337097072967325 + , + -- + -97.8050064766187, + 54.05938792583079 + , + -- + -97.64380184608419, + 53.42521474874492 + , + -- + -96.39258622921722, + 51.39730235453108 + , + -- + -96.23789282915149, + 50.6910147161819 + , + -- + -96.72558915890605, + 50.448910630894474 + , + -- + -96.92110694048235, + 50.75405996357799 + , + -- + -97.23568722205913, + 51.49778717712263 + , + -- + -98.20097713905517, + 52.18456696228162 + , + -- + -99.23680538612963, + 53.21628693298888 + , + -- + -98.95540137408423, + 53.92978343364277 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Great Slave Lake', + 'https://en.wikipedia.org/wiki/Great_Slave_Lake', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -115.00000342492967, + 61.9723932969562 + , + -- + -115.80669837122196, + 62.54171255151576 + , + -- + -116.0585951404287, + 62.77880402287089 + , + -- + -115.84279435917783, + 62.7503044704619 + , + -- + -114.45259497766185, + 62.42820526798667 + , + -- + -113.35341142459757, + 62.04448192000336 + , + -- + -111.77890804731261, + 62.44360484480964 + , + -- + -111.04001258018727, + 62.92000987410843 + , + -- + -110.20001217328286, + 63.08000010848407 + , + -- + -109.40000932497237, + 62.87817780216814 + , + -- + -109.09010576051801, + 62.814099026126215 + , + -- + -109.11651241741916, + 62.6928144395372 + , + -- + -110.10089677614705, + 62.51561595320837 + , + -- + -111.27599300824811, + 62.34911448836395 + , + -- + -112.63050981326654, + 61.55991201440246 + , + -- + -113.64000891808828, + 61.07999298770784 + , + -- + -115.3409903634076, + 60.87659455020702 + , + -- + -116.43999304895887, + 60.86000641544133 + , + -- + -118.06110856817108, + 61.31196849226605 + , + -- + -118.3474735177165, + 61.36137116153708 + , + -- + -118.3849906075604, + 61.52110301375126 + , + -- + -118.17960262741634, + 61.55629466414203 + , + -- + -116.8028132800801, + 61.32589529076871 + , + -- + -115.67879920129957, + 61.69179026961132 + , + -- + -115.00000342492967, + 61.9723932969562 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'L. Ontario', + 'https://en.wikipedia.org/wiki/Lake_Ontario', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -79.05630591502026, + 43.25410431576152 + , + -- + -79.36168779164908, + 43.20237620703736 + , + -- + -79.76047481964547, + 43.29720246029295 + , + -- + -79.46116492381094, + 43.639197089200565 + , + -- + -79.1561706204243, + 43.75743276628437 + , + -- + -78.45052893747877, + 43.9031861435636 + , + -- + -77.60536088734519, + 44.039327704436545 + , + -- + -77.16148617217414, + 43.850140285815996 + , + -- + -76.88269181995948, + 44.0694550644627 + , + -- + -76.56555355498425, + 44.20802541765336 + , + -- + -76.35303422718391, + 44.134670722015045 + , + -- + -76.23926856149336, + 43.979150499032656 + , + -- + -76.17999569365458, + 43.5900011256587 + , + -- + -76.9300015937227, + 43.2599954290428 + , + -- + -77.74915056019732, + 43.342832750006664 + , + -- + -78.53499406605984, + 43.379988104824534 + , + -- + -79.05630591502026, + 43.25410431576152 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'L. Erie', + 'https://en.wikipedia.org/wiki/Lake_Erie', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -83.12001135937246, + 42.08001577409016 + , + -- + -82.57123348664891, + 42.01702220312636 + , + -- + -81.82918575715374, + 42.33552989355732 + , + -- + -81.39226152212595, + 42.61517690690481 + , + -- + -81.09496700715081, + 42.66075552018623 + , + -- + -80.54515560578142, + 42.560089830081665 + , + -- + -80.27917700877515, + 42.71566172949635 + , + -- + -79.79135148793986, + 42.84203644466612 + , + -- + -78.92000932485044, + 42.96500051530464 + , + -- + -78.90057898630869, + 42.8667119410855 + , + -- + -79.76220598012725, + 42.269616604169045 + , + -- + -80.51644934764329, + 41.980331936199136 + , + -- + -81.03119828970264, + 41.845508124349635 + , + -- + -81.62351355663209, + 41.568935858723535 + , + -- + -82.34744869660895, + 41.435920722004255 + , + -- + -82.84610043000939, + 41.48710622818935 + , + -- + -83.46283281119673, + 41.69396698665372 + , + -- + -83.12001135937246, + 42.08001577409016 + , + -- + -83.12001135937246, + 42.08001577409016 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Superior', + 'https://en.wikipedia.org/wiki/Lake_Superior', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -89.60000342482806, + 48.00998973244721 + , + -- + -89.19405921095924, + 48.40546946877693 + , + -- + -88.62641944044917, + 48.56251414651193 + , + -- + -88.40423661981026, + 48.80632355406499 + , + -- + -88.17895321323383, + 48.93670319273738 + , + -- + -87.24903581414156, + 48.73511343036678 + , + -- + -86.56000810417788, + 48.71108388935106 + , + -- + -86.32103044304411, + 48.57729360614741 + , + -- + -85.98652889681881, + 48.01035146747326 + , + -- + -84.8642201403039, + 47.86007640236849 + , + -- + -85.04061764193222, + 47.575700995466306 + , + -- + -84.64500871452178, + 47.28220469826462 + , + -- + -84.81528255892351, + 46.90233124448706 + , + -- + -84.39559241406505, + 46.77683502866624 + , + -- + -84.60490780306328, + 46.439594631529474 + , + -- + -84.9100054593145, + 46.48000560158172 + , + -- + -85.11999264193253, + 46.76001434995524 + , + -- + -86.1026200019625, + 46.67265534116582 + , + -- + -86.99000769727856, + 46.45000743263628 + , + -- + -87.69427995476835, + 46.83104360614041 + , + -- + -88.2612220934425, + 46.958581041036766 + , + -- + -87.93992387566777, + 47.485913194359185 + , + -- + -88.82260901564527, + 47.154796454449 + , + -- + -89.62498897984119, + 46.83083690041124 + , + -- + -90.39703487828177, + 46.57648550067064 + , + -- + -91.00999487991183, + 46.92000458433087 + , + -- + -92.01192338740282, + 46.71167104754619 + , + -- + -92.00877112503301, + 46.85843211525511 + , + -- + -91.33000118687926, + 47.28000844989221 + , + -- + -90.61999284540505, + 47.68000987404746 + , + -- + -89.60000342482806, + 48.00998973244721 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Victoria', + 'https://en.wikipedia.org/wiki/Lake_Victoria', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + 33.85036827976734, + 0.128157863766091 + , + -- + 33.85036827976734, + 0.128157863766091 + , + -- + 34.13624230320599, + -0.319308363449238 + , + -- + 34.0726286150547, + -1.059831638191795 + , + -- + 33.579428745261055, + -1.506005954599821 + , + -- + 33.251748488098286, + -1.957968031424549 + , + -- + 33.64717654799571, + -2.30089283611342 + , + -- + 33.07672041192572, + -2.547131035984201 + , + -- + 32.95176679864397, + -2.43044565186915 + , + -- + 32.37309410983957, + -2.489925225437091 + , + -- + 31.926558058405476, + -2.714511000177573 + , + -- + 31.648022088352292, + -2.32921152100937 + , + -- + 31.836020949030114, + -1.629305922048232 + , + -- + 31.866199985488578, + -1.027378838712494 + , + -- + 31.815143670384202, + -0.64042571371094 + , + -- + 32.27284183119332, + -0.056120293786734 + , + -- + 32.906136508930246, + 0.0867650415003 + , + -- + 33.33184695815069, + 0.324993394365833 + , + -- + 33.85036827976734, + 0.128157863766091 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Ladoga', + 'https://en.wikipedia.org/wiki/Lake_Ladoga', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + 29.836711460089845, + 61.22608226179696 + , + -- + 29.836711460089845, + 61.22608226179696 + , + -- + 30.85199832532828, + 61.77504100203353 + , + -- + 32.52688317234018, + 61.11751007755173 + , + -- + 32.94432539239392, + 60.64260366478942 + , + -- + 32.81575442885176, + 60.481889960361684 + , + -- + 32.599901971168606, + 60.533514716221276 + , + -- + 32.583882277158125, + 60.20893504499601 + , + -- + 31.699440138482714, + 60.23565176049091 + , + -- + 31.50973595553924, + 59.92034800886205 + , + -- + 31.106246372204282, + 59.92768606224749 + , + -- + 31.10893354668346, + 60.14645823835514 + , + -- + 30.533878208139498, + 60.63009796817478 + , + -- + 30.502045525847706, + 60.843211574946466 + , + -- + 29.836711460089845, + 61.22608226179696 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Balkhash', + 'https://en.wikipedia.org/wiki/Lake_Balkhash', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + 78.99076541536459, + 46.748619696634876 + , + -- + 78.99076541536459, + 46.748619696634876 + , + -- + 79.2451684915375, + 46.64516347918655 + , + -- + 78.88989301953086, + 46.369934800800024 + , + -- + 78.44506229036242, + 46.29717438413307 + , + -- + 77.20606814973246, + 46.3955663112168 + , + -- + 75.61128177277294, + 46.50718740496724 + , + -- + 75.46333214712132, + 46.6706141220903 + , + -- + 75.40566124868357, + 46.47080719663377 + , + -- + 74.91122114451494, + 46.404661363300164 + , + -- + 74.77138471873312, + 46.107831936215646 + , + -- + 74.27802981964263, + 46.004065660173566 + , + -- + 74.0999528339648, + 44.98893382423201 + , + -- + 73.4367375019309, + 45.60946442319282 + , + -- + 73.4399931171653, + 45.80586070411809 + , + -- + 73.73713260284359, + 46.012747300798594 + , + -- + 73.67925499867667, + 46.18307282163262 + , + -- + 74.02068118682908, + 46.20490611427593 + , + -- + 74.09432010284502, + 46.42781240496693 + , + -- + 74.939384800114, + 46.81678091082786 + , + -- + 76.20313195181177, + 46.7800131292522 + , + -- + 77.18131513866464, + 46.64340648048862 + , + -- + 77.85993004752152, + 46.64785065366573 + , + -- + 78.29700931184618, + 46.46853343361292 + , + -- + 78.39710656119556, + 46.657669175801175 + , + -- + 78.99076541536459, + 46.748619696634876 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Tanganyika', + 'https://en.wikipedia.org/wiki/Lake_Tanganyika', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + 30.806006300588507, + -8.578339125201033 + , + -- + 30.46442508313922, + -8.498188978716335 + , + -- + 30.566847771941724, + -8.1150082332721 + , + -- + 30.27735639824263, + -7.848357842646031 + , + -- + 30.14702843600253, + -7.299244073112582 + , + -- + 29.536523064906334, + -6.7541610652979 + , + -- + 29.19318484875913, + -6.038029066597119 + , + -- + 29.371985304489016, + -5.616452731960017 + , + -- + 29.101717563602506, + -5.054006442895265 + , + -- + 29.281034783655315, + -3.455499362810748 + , + -- + 29.652588331833897, + -4.420143324403149 + , + -- + 29.600085076625334, + -4.896393324405054 + , + -- + 29.79195966972506, + -5.040880629093131 + , + -- + 29.758421665167646, + -5.466901136907339 + , + -- + 29.951226434048635, + -5.860985609565162 + , + -- + 29.722041456834177, + -6.244114678577112 + , + -- + 30.52803877129051, + -6.922729587433992 + , + -- + 30.604158156056457, + -7.541916599155222 + , + -- + 31.189032016735865, + -8.729906101113095 + , + -- + 31.022117140433124, + -8.786543470904988 + , + -- + 30.806006300588507, + -8.578339125201033 + , + -- + 30.806006300588507, + -8.578339125201033 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Malawi', + 'https://en.wikipedia.org/wiki/Lake_Malawi', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + 35.2602047055542, + -14.277474460510291 + , + -- + 35.236226840970915, + -14.401291192281633 + , + -- + 34.881209751125766, + -14.012012627826891 + , + -- + 34.706543409979076, + -14.262023207255027 + , + -- + 34.54675988133266, + -14.047669366108266 + , + -- + 34.55110070164528, + -13.67208505621096 + , + -- + 34.3209338722171, + -13.379389743709737 + , + -- + 34.32997724786827, + -12.944584242405995 + , + -- + 34.032527703596315, + -12.208556817272836 + , + -- + 34.322690870915096, + -11.652983493702834 + , + -- + 34.25990400568048, + -10.447579034062642 + , + -- + 33.906592238100984, + -9.801726983278854 + , + -- + 33.99557905450757, + -9.495440769084837 + , + -- + 34.52422895685345, + -10.03013681400887 + , + -- + 34.60789310073403, + -11.080511976773494 + , + -- + 34.93702029800096, + -11.463434340056267 + , + -- + 34.69388268406766, + -12.422393894096615 + , + -- + 34.86756717300062, + -13.701127211159019 + , + -- + 35.05597944513687, + -13.742933444883136 + , + -- + 35.2602047055542, + -14.277474460510291 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Aral Sea', + 'https://en.wikipedia.org/wiki/Aral_Sea', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + 60.05285159692946, + 44.264636949229114 + , + -- + 59.77002648299603, + 44.15999217383806 + , + -- + 59.06288618351405, + 44.36928172462015 + , + -- + 59.34571129744745, + 44.99720205339871 + , + -- + 59.35930219914022, + 45.190006822279685 + , + -- + 58.96139367049281, + 45.37500844988459 + , + -- + 58.92144778833119, + 45.101846828789746 + , + -- + 58.92144778833119, + 44.73556427670495 + , + -- + 58.497132602782614, + 44.212340399749735 + , + -- + 58.285000848224456, + 44.473952338227335 + , + -- + 58.285000848224456, + 44.89255727800766 + , + -- + 58.689989048095896, + 45.50001373959863 + , + -- + 58.78000939314833, + 45.88673432065487 + , + -- + 59.20427290226462, + 45.93905670835039 + , + -- + 59.535002068932585, + 45.70501414650049 + , + -- + 59.55784305200561, + 46.30533926043519 + , + -- + 59.77002648299603, + 46.25299103452352 + , + -- + 60.12359663273702, + 46.096023871436955 + , + -- + 60.12359663273702, + 45.88673432065487 + , + -- + 59.94675988143425, + 45.808211981787366 + , + -- + 59.84071984237133, + 45.520477606786216 + , + -- + 60.12359663273702, + 45.572774156265595 + , + -- + 60.23997195825834, + 44.78403677019473 + , + -- + 60.05285159692946, + 44.264636949229114 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Vänern', + 'https://en.wikipedia.org/wiki/V%C3%A4nern', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + 13.979281447005576, + 59.20491364199721 + , + -- + 13.979281447005576, + 59.20491364199721 + , + -- + 13.984449090234762, + 59.086212877022774 + , + -- + 13.91892337408865, + 58.902503160225166 + , + -- + 13.28268313971111, + 58.608670966213566 + , + -- + 12.830100945698888, + 58.50903880475484 + , + -- + 12.460149366921371, + 58.50619660097878 + , + -- + 12.537767368223768, + 58.77594757754237 + , + -- + 12.522161085671598, + 58.880282294339665 + , + -- + 12.697085808979608, + 58.953843695707135 + , + -- + 13.027039829163215, + 58.993531195707305 + , + -- + 13.195298292705559, + 59.12900096296045 + , + -- + 13.59144982265505, + 59.336481838612315 + , + -- + 13.979281447005576, + 59.20491364199721 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Okeechobee', + 'https://en.wikipedia.org/wiki/Lake_Okeechobee', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -80.70643775096435, + 26.788959458924822 + , + -- + -80.93244462759287, + 26.823272609966622 + , + -- + -80.91970638703292, + 27.068916530866048 + , + -- + -80.69369951040441, + 27.034629218040394 + , + -- + -80.70643775096435, + 26.788959458924822 + , + -- + -80.70643775096435, + 26.788959458924822 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lago de Nicaragua', + 'https://en.wikipedia.org/wiki/Lake_Nicaragua', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -84.85548682324658, + 11.147898667846633 + , + -- + -85.29013729525353, + 11.176165676310276 + , + -- + -85.79132117383625, + 11.509737046754324 + , + -- + -85.8851655748783, + 11.900100816287136 + , + -- + -85.5653401354239, + 11.940330918826362 + , + -- + -85.03684526237491, + 11.5216484643976 + , + -- + -84.85548682324658, + 11.147898667846633 + , + -- + -84.85548682324658, + 11.147898667846633 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Tana', + 'https://en.wikipedia.org/wiki/Lake_Tana', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + 37.14370730972843, + 11.850594794151519 + , + -- + 37.01482628759251, + 12.035596421756424 + , + -- + 37.24401126480697, + 12.233878892460353 + , + -- + 37.518361443844526, + 12.160601711470477 + , + -- + 37.48187788264647, + 11.825092474815477 + , + -- + 37.33635704931254, + 11.713393866416595 + , + -- + 37.14370730972843, + 11.850594794151519 + , + -- + 37.14370730972843, + 11.850594794151519 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lago Titicaca', + 'https://en.wikipedia.org/wiki/Lake_Titicaca', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -69.40673987494259, + -16.126198825752063 + , + -- + -69.7290974595793, + -15.928794854397104 + , + -- + -69.98365556504908, + -15.73717864345884 + , + -- + -69.8770212470148, + -15.669844252182529 + , + -- + -69.8679778713637, + -15.546079196843493 + , + -- + -69.88559953477524, + -15.35425628017606 + , + -- + -69.59675411647981, + -15.410480238509614 + , + -- + -68.98697221543571, + -15.885903415594846 + , + -- + -68.9596870591856, + -15.91329192470954 + , + -- + -68.74623755560401, + -16.356003920154023 + , + -- + -68.90524593776611, + -16.506640720284835 + , + -- + -69.00115739609983, + -16.536406345284952 + , + -- + -69.09084184434238, + -16.461992282784657 + , + -- + -69.18205074733753, + -16.40116912197712 + , + -- + -69.25098710801488, + -16.227536309476427 + , + -- + -69.40673987494259, + -16.126198825752063 + , + -- + -69.40673987494259, + -16.126198825752063 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Winnipegosis', + 'https://en.wikipedia.org/wiki/Lake_Winnipegosis', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -99.40377193886468, + 53.1258531764781 + , + -- + -99.54846594928192, + 53.120427151087455 + , + -- + -99.80498775917877, + 53.14290639913442 + , + -- + -100.43146114785316, + 53.2841897650204 + , + -- + -100.60685095905177, + 53.531616522833886 + , + -- + -100.33487789589965, + 53.745246893928496 + , + -- + -100.42673275429846, + 53.90740753846039 + , + -- + -100.4044860501968, + 53.94507965760117 + , + -- + -100.32629960813921, + 54.094011135466346 + , + -- + -100.23586585162842, + 54.23033356385231 + , + -- + -99.99549292682278, + 54.215864162810576 + , + -- + -100.04778947630214, + 54.10013479269293 + , + -- + -100.18356930214904, + 53.930351874397985 + , + -- + -99.87128862180926, + 53.928465684619326 + , + -- + -99.91066606321566, + 53.821314602262134 + , + -- + -100.05649695514333, + 53.44332733826322 + , + -- + -99.66551306842301, + 53.29581696228607 + , + -- + -99.39317827024485, + 53.26791168884846 + , + -- + -99.40377193886468, + 53.1258531764781 + , + -- + -99.40377193886468, + 53.1258531764781 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Onega', + 'https://en.wikipedia.org/wiki/Lake_Onega', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + 35.71464725112901, + 62.2802298041189 + , + -- + 36.0541614112866, + 61.716310736733874 + , + -- + 36.391401808423325, + 61.27605337182331 + , + -- + 36.10945519383887, + 61.01508738874924 + , + -- + 35.35074181492962, + 60.948579820389625 + , + -- + 34.866843702948586, + 61.11637319604125 + , + -- + 35.207288038887384, + 61.114435329830485 + , + -- + 35.57832482274313, + 61.08634918887975 + , + -- + 35.16000410334027, + 61.39428904890701 + , + -- + 34.85733523940689, + 61.55179881453273 + , + -- + 34.48691857273877, + 61.86697337508076 + , + -- + 34.265019972477376, + 62.21914826114994 + , + -- + 34.289824659977455, + 62.29774811466581 + , + -- + 34.66561567560399, + 62.22979360620195 + , + -- + 34.62613488133297, + 62.45223480900245 + , + -- + 34.8356311378443, + 62.29676626245225 + , + -- + 35.080267368314026, + 62.1411943630377 + , + -- + 35.21658979669991, + 62.193284206787894 + , + -- + 35.463706495919666, + 62.2560193955901 + , + -- + 35.13969526544963, + 62.48776235620312 + , + -- + 34.614352654770414, + 62.762448432050576 + , + -- + 34.99541466649072, + 62.748469957115674 + , + -- + 35.23395307795005, + 62.675347805422575 + , + -- + 35.71464725112901, + 62.2802298041189 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Great Salt Lake', + 'https://en.wikipedia.org/wiki/Great_Salt_Lake', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -112.18405127648089, + 41.34124949804567 + , + -- + -112.13875688357706, + 41.142346910154174 + , + -- + -112.17193315310845, + 40.851460272783186 + , + -- + -112.67882727745939, + 41.130487168943205 + , + -- + -112.70549231652205, + 41.16753917089636 + , + -- + -112.87814327680917, + 41.62815705013003 + , + -- + -112.58955624067522, + 41.43891795507716 + , + -- + -112.40532975955472, + 41.33801972102742 + , + -- + -112.21901038292629, + 41.42855683040267 + , + -- + -112.18405127648089, + 41.34124949804567 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Great Bear Lake', + 'https://en.wikipedia.org/wiki/Great_Bear_Lake', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -117.7592923653704, + 66.22368419052789 + , + -- + -117.97374955938167, + 65.85489533147692 + , + -- + -118.10710059291085, + 65.76691620550001 + , + -- + -119.7200520432652, + 65.73479930283061 + , + -- + -119.74568355368197, + 65.65436493596833 + , + -- + -119.6648099371452, + 65.52742178004333 + , + -- + -119.70219783590836, + 65.36794830999061 + , + -- + -121.35509436098008, + 64.87999359807459 + , + -- + -121.33610327211281, + 64.99461192489797 + , + -- + -120.94527441468938, + 65.37774099390991 + , + -- + -121.05728308168202, + 65.4463414577774 + , + -- + -122.56450374412293, + 65.0310696478799 + , + -- + -123.232705851873, + 65.18041453720339 + , + -- + -123.17963415590926, + 65.31937246363624 + , + -- + -122.32599117087979, + 65.79378795029179 + , + -- + -122.35622188377054, + 65.90184337021411 + , + -- + -124.95363440005697, + 66.04925039332666 + , + -- + -124.89753963280414, + 66.15115631780624 + , + -- + -119.48724971579031, + 66.96929759385118 + , + -- + -119.35743771042887, + 66.87519481064767 + , + -- + -120.1769492193738, + 66.46527151149239 + , + -- + -117.60545162643749, + 66.55934845647975 + , + -- + -117.61278967982294, + 66.41997711858856 + , + -- + -117.7592923653704, + 66.22368419052789 + , + -- + -117.7592923653704, + 66.22368419052789 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Athabasca', + 'https://en.wikipedia.org/wiki/Lake_Athabasca', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -109.65330135785099, + 59.03763703066841 + , + -- + -111.08626298708849, + 58.56017263450765 + , + -- + -111.19948605023998, + 58.685565497463884 + , + -- + -111.160005255969, + 58.75985036888345 + , + -- + -109.09672034385136, + 59.55042226830068 + , + -- + -106.54517066122398, + 59.31968699811746 + , + -- + -106.54695349813804, + 59.292815253325685 + , + -- + -109.65330135785099, + 59.03763703066841 + , + -- + -109.65330135785099, + 59.03763703066841 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Reindeer Lake', + 'https://en.wikipedia.org/wiki/Reindeer_Lake', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -101.89514441608819, + 58.01403025983099 + , + -- + -101.89514441608819, + 58.01403025983099 + , + -- + -101.54384802936804, + 57.86809601503873 + , + -- + -101.97090206582807, + 57.34867035585697 + , + -- + -101.93403093138781, + 57.23066722271848 + , + -- + -103.20416012247362, + 56.34539826112639 + , + -- + -103.2825015938281, + 56.40994212505895 + , + -- + -103.1487371488406, + 56.70411021588043 + , + -- + -103.07832800984292, + 56.71080231386223 + , + -- + -103.01440426309787, + 56.56510061301529 + , + -- + -102.57680823445028, + 56.938281968811054 + , + -- + -102.81322791218561, + 57.28714956321349 + , + -- + -102.81369300007623, + 57.46434804954232 + , + -- + -102.12874772826359, + 58.01914622662788 + , + -- + -101.89514441608819, + 58.01403025983099 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Huron', + 'https://en.wikipedia.org/wiki/Lake_Huron', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -80.41056433787725, + 45.590137437515665 + , + -- + -79.76292945017934, + 44.824602769543844 + , + -- + -80.08500281443844, + 44.493925279308144 + , + -- + -80.89769222687659, + 44.63164297136599 + , + -- + -81.40233842642287, + 45.25005483660284 + , + -- + -81.27567949087549, + 44.6201708033972 + , + -- + -81.75265296092948, + 44.06464915625956 + , + -- + -81.70017554393709, + 43.590052802090995 + , + -- + -81.78272864452336, + 43.310845038417995 + , + -- + -82.43000179719522, + 42.98001251888543 + , + -- + -82.4799987454376, + 43.39001333268915 + , + -- + -82.6599877591102, + 43.970003770516996 + , + -- + -83.02999101432, + 44.06999766700177 + , + -- + -83.6500048489579, + 43.62999868425261 + , + -- + -83.84870073112015, + 43.63831858985161 + , + -- + -83.89998959016984, + 43.88998281511303 + , + -- + -83.34999732128743, + 44.29001007748441 + , + -- + -83.25878841829228, + 44.74574453386644 + , + -- + -83.33999793163895, + 45.200006211928155 + , + -- + -84.08000444205858, + 45.58998240821879 + , + -- + -84.93000423861146, + 45.789996039404485 + , + -- + -84.75355506055087, + 45.924483954444085 + , + -- + -84.71999121777729, + 45.91998810483469 + , + -- + -83.83919226757845, + 46.010215155616294 + , + -- + -84.33670711946846, + 46.40876963966737 + , + -- + -84.14870825879063, + 46.55576325132161 + , + -- + -83.95257036002683, + 46.334278062518635 + , + -- + -83.20251278352643, + 46.21015127215355 + , + -- + -82.44199072948692, + 46.19963511818216 + , + -- + -81.63136837434045, + 46.09754832618957 + , + -- + -80.73679765493583, + 45.90381338152733 + , + -- + -80.41056433787725, + 45.590137437515665 + ) + ) + ) +""" + +cur.execute(sql) + +sql = """ +INSERT INTO geo_test.lakes (area, volume, name, wiki_link, geometry) + VALUES (NULL, NULL, 'Lake Michigan', + 'https://en.wikipedia.org/wiki/Lake_Michigan', + SDO_GEOMETRY( + 2003, -- two-dimensional polygon + 4326, + NULL, + SDO_ELEM_INFO_ARRAY(1,1003,1), -- one polygon (exterior polygon ring) + SDO_ORDINATE_ARRAY( + -- + -85.53999284538475, + 46.03000722918408 + , + -- + -84.75355506055087, + 45.924483954444085 + , + -- + -84.93000423861146, + 45.789996039404485 + , + -- + -85.06999569369015, + 45.40999339454619 + , + -- + -85.29044735384727, + 45.30824249936349 + , + -- + -85.46710323763705, + 44.81457754167923 + , + -- + -85.55999162468169, + 45.150009263685774 + , + -- + -85.95983801954006, + 44.91059235287753 + , + -- + -86.20935767286137, + 44.574798895844935 + , + -- + -86.47027197950304, + 44.08423452409818 + , + -- + -86.52001054558397, + 43.65999685319804 + , + -- + -86.18842871778315, + 43.04140412044818 + , + -- + -86.21604977084317, + 42.38170278581012 + , + -- + -86.62191647006355, + 41.8944198675139 + , + -- + -86.8244364082154, + 41.75618541113313 + , + -- + -87.09444576694042, + 41.64616628678374 + , + -- + -87.4342183092595, + 41.640714423176945 + , + -- + -87.52617652052288, + 41.70851390234388 + , + -- + -87.79569495314115, + 42.23411489518453 + , + -- + -87.80344641798493, + 42.49399567318035 + , + -- + -87.77672970249003, + 42.740853990238634 + , + -- + -87.90214840366241, + 43.23051402442029 + , + -- + -87.71221167677363, + 43.79650014909703 + , + -- + -87.486359829442, + 44.49335683855294 + , + -- + -86.9674767727993, + 45.26287059181122 + , + -- + -87.11806189649782, + 45.25933075619923 + , + -- + -87.85282324903983, + 44.61505483660031 + , + -- + -87.98831885450912, + 44.73331635190026 + , + -- + -87.5964306302237, + 45.093707790703775 + , + -- + -87.00000708692704, + 45.73999909116209 + , + -- + -86.31999691439827, + 45.829993597998396 + , + -- + -85.53999284538475, + 46.03000722918408 + ) + ) + ) +""" + +cur.execute(sql) + +conn.commit() diff --git a/tests/test_oracle_provider.py b/tests/test_oracle_provider.py new file mode 100644 index 0000000..f89e5af --- /dev/null +++ b/tests/test_oracle_provider.py @@ -0,0 +1,455 @@ +# ================================================================= +# +# Authors: Andreas Kosubek +# +# Copyright (c) 2023 Andreas Kosubek +# +# 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. +# +# ================================================================= + +# Needs to be run like: python3 -m pytest +# Create testdata: python3 load_oracle_data.py + +import os +import pytest +from pygeoapi.provider.oracle import OracleProvider + +USERNAME = os.environ.get("PYGEOAPI_ORACLE_USER", "geo_test") +PASSWORD = os.environ.get("PYGEOAPI_ORACLE_PASSWD", "geo_test") +SERVICE_NAME = os.environ.get("PYGEOAPI_ORACLE_SERVICE_NAME", "XEPDB1") +HOST = os.environ.get("PYGEOAPI_ORACLE_HOST", "127.0.0.1") +PORT = os.environ.get("PYGEOAPI_ORACLE_PORT", "1521") + + +class SqlManipulator: + def process_query( + self, + db, + sql_query, + bind_variables, + sql_manipulator_options, + bbox, + source_crs, + properties, + ): + sql = "ID = 10 AND :foo != :bar" + + if sql_query.find(" WHERE ") == -1: + sql_query = sql_query.replace("#WHERE#", f" WHERE {sql}") + else: + sql_query = sql_query.replace("#WHERE#", f" AND {sql}") + + bind_variables = { + **bind_variables, + "foo": "foo", + "bar": sql_manipulator_options.get("foo"), + } + + return sql_query, bind_variables + + def process_get( + self, + db, + sql_query, + bind_variables, + sql_manipulator_options, + identifier, + ): + sql_query = f"{sql_query} AND 'auth' = 'you arent allowed'" + + return sql_query, bind_variables + + def process_create( + self, + db, + sql_query, + bind_variables, + sql_manipulator_options, + request_data, + ): + bind_variables["name"] = "overwritten" + + return sql_query, bind_variables + + def process_update( + self, + db, + sql_query, + bind_variables, + sql_manipulator_options, + identifier, + request_data, + ): + bind_variables["area"] = 42 + bind_variables["volume"] = 42 + + return sql_query, bind_variables + + def process_delete( + self, + db, + sql_query, + bind_variables, + sql_manipulator_options, + identifier, + ): + sql_query = f"{sql_query} AND 'auth' = 'you arent allowed'" + + return sql_query, bind_variables + + +@pytest.fixture() +def config(): + return { + "name": "Oracle", + "type": "feature", + "data": { + "host": HOST, + "port": PORT, + "service_name": SERVICE_NAME, + "user": USERNAME, + "password": PASSWORD, + }, + "id_field": "id", + "table": "lakes", + "geom_field": "geometry", + "editable": True, + } + + +@pytest.fixture() +def config_manipulator(config): + return { + **config, + "sql_manipulator": "tests.test_oracle_provider.SqlManipulator", + "sql_manipulator_options": {"foo": "bar"}, + } + + +@pytest.fixture() +def config_properties(config): + return { + **config, + "properties": ["id", "name", "wiki_link"], + } + + +@pytest.fixture() +def create_geojson(): + return { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [9.012050, 47.841512], + [9.803470, 47.526461], + [9.476940, 47.459178], + [8.918151, 47.693253], + [9.012050, 47.841512], + ] + ], + }, + "properties": { + "name": "Lake Constance", + "wiki_link": "https://en.wikipedia.org/wiki/Lake_Constance", + "foo": "bar", + }, + } + + +@pytest.fixture() +def update_geojson(): + return { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [9.012050, 47.841512], + [9.803470, 47.526461], + [9.476940, 47.459178], + [8.918151, 47.693253], + [9.012050, 47.841512], + ] + ], + }, + "properties": { + "name": "Lake Constance", + "wiki_link": "https://en.wikipedia.org/wiki/Lake_Constance", + "foo": "bar", + "area": 536000, + "volume": 48000, + }, + "id": 26, + } + + +def test_query(config): + """Test query for a valid JSON object with geometry""" + p = OracleProvider(config) + feature_collection = p.query() + assert feature_collection.get("type") == "FeatureCollection" + features = feature_collection.get("features") + assert features is not None + feature = features[0] + properties = feature.get("properties") + assert properties is not None + geometry = feature.get("geometry") + assert geometry is not None + + +def test_get_fields(config): + """Test get_fields""" + expected_fields = { + "id": {"type": "NUMBER"}, + "area": {"type": "NUMBER"}, + "volume": {"type": "NUMBER"}, + "name": {"type": "VARCHAR2"}, + "wiki_link": {"type": "VARCHAR2"}, + } + + provider = OracleProvider(config) + + 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. + Test of property configuration. + """ + expected_fields = { + "id": {"type": "NUMBER"}, + "name": {"type": "VARCHAR2"}, + "wiki_link": {"type": "VARCHAR2"}, + } + + provider = OracleProvider(config_properties) + provided_fields = provider.get_fields() + print(provided_fields) + + assert provided_fields == expected_fields + assert provider.fields == expected_fields + + +def test_query_with_property_filter(config): + """Test query valid features when filtering by property""" + p = OracleProvider(config) + feature_collection = p.query(properties=[("name", "Aral Sea")]) + features = feature_collection.get("features") + + assert len(features) == 1 + assert features[0].get("id") == 12 + + +def test_query_bbox(config): + """Test query with a specified bounding box""" + p = OracleProvider(config) + feature_collection = p.query(bbox=[50, 40, 60, 50]) + features = feature_collection.get("features") + + assert len(features) == 1 + assert features[0]["properties"]["name"] == "Aral Sea" + + +def test_query_sortby(config): + """Test query with sorting""" + p = OracleProvider(config) + up = p.query(sortby=[{"property": "id", "order": "+"}]) + assert up["features"][0]["id"] == 1 + down = p.query(sortby=[{"property": "id", "order": "-"}]) + assert down["features"][0]["id"] == 25 + + name = p.query(sortby=[{"property": "name", "order": "+"}]) + assert name["features"][0]["properties"]["name"] == "Aral Sea" + name = p.query(sortby=[{"property": "name", "order": "-"}]) + assert name["features"][0]["properties"]["name"] == "Vänern" + + +def test_query_skip_geometry(config): + """Test query without geometry""" + p = OracleProvider(config) + result = p.query(skip_geometry=True) + feature = result["features"][0] + + assert feature.get("geometry") is None + + +def test_query_hits(config): + """Test query number of hits""" + p = OracleProvider(config) + result = p.query(bbox=[0, 0, 70, 60], resulttype="hits") + + assert result.get("numberMatched") == 5 + + +def test_get(config): + """Test simple get""" + p = OracleProvider(config) + result = p.get(5) + + assert result.get("id") == 5 + assert result.get("prev") == 4 + assert result.get("next") == 6 + + +def test_create(config, create_geojson): + """Test simple create""" + p = OracleProvider(config) + result = p.create(create_geojson) + + assert result == 26 + + data = p.get(26) + + assert data.get("properties").get("name") == "Lake Constance" + + +def test_update(config, update_geojson): + """Test simple update""" + p = OracleProvider(config) + identifier = 26 + result = p.update(identifier, update_geojson) + + assert result + + data = p.get(identifier) + + assert data.get("properties").get("area") == 536000 + assert data.get("properties").get("volume") == 48000 + + +def test_update_properties(config_properties, config, update_geojson): + """ + Test update with filtered columnlist in configuration + In this case, the columns area and volume shouldn't be updated! + """ + p = OracleProvider(config_properties) + identifier = 26 + + update_geojson["properties"]["area"] = 42 + update_geojson["properties"]["volume"] = 42 + + result = p.update(identifier, update_geojson) + + assert result + + p2 = OracleProvider(config) + data = p2.get(identifier) + + assert data.get("properties").get("area") == 536000 + assert data.get("properties").get("volume") == 48000 + + +def test_delete(config): + """Test simple delete""" + p = OracleProvider(config) + identifier = 26 + + result = p.delete(identifier) + + assert result + + down = p.query(sortby=[{"property": "id", "order": "-"}]) + assert down["features"][0]["id"] == 25 + + +def test_query_sql_manipulator(config_manipulator): + """Test SQL manipulator""" + p = OracleProvider(config_manipulator) + feature_collection = p.query() + features = feature_collection.get("features") + + assert len(features) == 1 + assert features[0].get("id") == 10 + + +def test_get_sql_manipulator(config_manipulator): + """ + Test get with SQL manipulator that throws + an authorization error. + """ + p = OracleProvider(config_manipulator) + + with pytest.raises(Exception): + p.get(5) + + +def test_create_sql_manipulator(config_manipulator, config, create_geojson): + """ + Test create with SQL Manipulator call. + Field name should be overwritten with the string "overwritten" + """ + expected_identifier = 27 + + p = OracleProvider(config_manipulator) + result = p.create(create_geojson) + + assert result == expected_identifier + + p2 = OracleProvider(config) + data = p2.get(expected_identifier) + + assert data.get("properties").get("name") == "overwritten" + + +def test_update_sql_manipulator(config_manipulator, config, update_geojson): + """ + Test update with SQL Manipulator call + Field names area and volume should be overwritten with the answer to + life the universe and everything + """ + identifier = 27 + + p = OracleProvider(config_manipulator) + result = p.update(identifier, update_geojson) + + assert result + + p2 = OracleProvider(config) + data = p2.get(identifier) + + assert data.get("properties").get("area") == 42 + assert data.get("properties").get("volume") == 42 + + +def test_delete_sql_manipulator(config_manipulator, config): + """ + Test for delete with SQL Manipulator call + Where clause is overwritten by the manipulator to not + match to any record. No record should be deleted. + """ + identifier = 27 + + p = OracleProvider(config_manipulator) + + result = p.delete(identifier) + + assert not result + + p2 = OracleProvider(config) + + down = p2.query(sortby=[{"property": "id", "order": "-"}]) + assert down["features"][0]["id"] == identifier