diff --git a/.github/workflows/asan/start.sh b/.github/workflows/asan/start.sh index 5473c55bec6d..936956ae4990 100755 --- a/.github/workflows/asan/start.sh +++ b/.github/workflows/asan/start.sh @@ -31,11 +31,16 @@ fi sudo apt-get update -sudo apt-get install -y --allow-unauthenticated libpng-dev libjpeg-dev libgif-dev liblzma-dev libgeos-dev libcurl4-gnutls-dev libproj-dev libxml2-dev libexpat1-dev libxerces-c-dev libnetcdf-dev netcdf-bin libpoppler-dev libpoppler-private-dev libsqlite3-dev gpsbabel swig libhdf4-alt-dev libhdf5-dev libpodofo-dev poppler-utils libfreexl-dev unixodbc-dev libwebp-dev libepsilon-dev liblcms2-2 libpcre3-dev libcrypto++-dev libdap-dev libfyba-dev libmysqlclient-dev libogdi-dev libcfitsio-dev openjdk-8-jdk libzstd-dev ccache curl autoconf automake sqlite3 libspatialite-dev make g++ libssl-dev libsfcgal-dev libgeotiff-dev libopenjp2-7-dev libcairo2-dev python3-dev python3-setuptools python3-numpy python3-pip clang +sudo apt-get install -y --allow-unauthenticated libpng-dev libjpeg-dev libgif-dev liblzma-dev libgeos-dev libcurl4-gnutls-dev libproj-dev libxml2-dev libexpat1-dev libxerces-c-dev libnetcdf-dev netcdf-bin libpoppler-dev libpoppler-private-dev libsqlite3-dev gpsbabel swig libhdf4-alt-dev libhdf5-dev libpodofo-dev poppler-utils libfreexl-dev unixodbc-dev libwebp-dev libepsilon-dev liblcms2-2 libpcre3-dev libcrypto++-dev libdap-dev libfyba-dev libmysqlclient-dev libogdi-dev libcfitsio-dev openjdk-8-jdk libzstd-dev ccache curl autoconf automake sqlite3 libspatialite-dev make g++ libssl-dev libsfcgal-dev libgeotiff-dev libopenjp2-7-dev libcairo2-dev python3-dev python3-setuptools python3-numpy python3-pip clang git # Workaround bug in ogdi packaging sudo ln -s /usr/lib/ogdi/libvrf.so /usr/lib +# Build odbc-cpp library for HANA +(wget https://github.com/Kitware/CMake/releases/download/v3.12.4/cmake-3.12.4-Linux-x86_64.sh -O cmake.sh) +(sudo sh cmake.sh --prefix=/usr/local/ --exclude-subdir) +(git clone https://github.com/SAP/odbc-cpp-wrapper.git && mkdir odbc-cpp-wrapper/build && cd odbc-cpp-wrapper/build && cmake .. && make -j 2 && make install) + wget https://github.com/Esri/file-geodatabase-api/raw/master/FileGDB_API_1.5/FileGDB_API_1_5_64gcc51.tar.gz tar xzf FileGDB_API_1_5_64gcc51.tar.gz sudo cp FileGDB_API-64gcc51/lib/* /usr/lib @@ -66,7 +71,7 @@ ccache -s ./autogen.sh SANITIZE_FLAGS="-DMAKE_SANITIZE_HAPPY -fsanitize=undefined -fsanitize=address -fsanitize=unsigned-integer-overflow" -CFLAGS=$SANITIZE_FLAGS CXXFLAGS=$SANITIZE_FLAGS LDFLAGS="-fsanitize=undefined -fsanitize=address -lstdc++" ./configure --prefix=/usr --without-libtool --enable-debug --with-jpeg12 --with-poppler --without-podofo --with-spatialite --with-mysql --with-liblzma --with-webp --with-epsilon --with-libtiff=internal --with-rename-internal-libtiff-symbols --with-hide-internal-symbols --with-gnm --with-fgdb=$PWD/FileGDB_API-64gcc51 +CFLAGS=$SANITIZE_FLAGS CXXFLAGS=$SANITIZE_FLAGS LDFLAGS="-fsanitize=undefined -fsanitize=address -lstdc++" ./configure --prefix=/usr --without-libtool --enable-debug --with-jpeg12 --with-poppler --without-podofo --with-spatialite --with-mysql --with-liblzma --with-webp --with-epsilon --with-libtiff=internal --with-rename-internal-libtiff-symbols --with-hide-internal-symbols --with-gnm --with-fgdb=$PWD/FileGDB_API-64gcc51 --with-hana sed -i "s/-fsanitize=address/-fsanitize=address -shared-libasan/g" GDALmake.opt sed -i "s/-fsanitize=unsigned-integer-overflow/-fsanitize=unsigned-integer-overflow -fno-sanitize-recover=unsigned-integer-overflow/g" GDALmake.opt make USER_DEFS="-Werror" -j$NPROC @@ -101,6 +106,7 @@ rm -f ogr/ogr_fgdb.py ogr/ogr_pgeo.py # install test dependencies sudo python3 -m pip install -U -r ./requirements.txt +sudo python3 -m pip install -U hdbcli # Run each module in its own pytest process. # This makes sure the output from the address sanitizer is relevant diff --git a/.github/workflows/cmake_builds.yml b/.github/workflows/cmake_builds.yml index 45d629fafd95..af93a78bb77d 100644 --- a/.github/workflows/cmake_builds.yml +++ b/.github/workflows/cmake_builds.yml @@ -64,6 +64,24 @@ jobs: curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | sudo tee /etc/apt/sources.list.d/msprod.list sudo apt-get update sudo ACCEPT_EULA=Y apt-get install -y msodbcsql17 unixodbc-dev + # HANA: client side + # Install hdbsql tool + curl -v -j -k -s -L -H "Cookie: eula_3_1_agreed=tools.hana.ondemand.com/developer-license-3_1.txt" https://tools.hana.ondemand.com/additional/hanaclient-latest-linux-x64.tar.gz --output hanaclient-latest-linux-x64.tar.gz \ + && tar -xvf hanaclient-latest-linux-x64.tar.gz \ + && sudo mkdir /usr/sap \ + && sudo ./client/hdbinst -a client --sapmnt=/usr/sap \ + && rm -rf client \ + && rm hanaclient* + export PATH=/usr/sap/hdbclient:$PATH + # Download and compile odbc-cpp-wrapper + sudo apt-get install -y -q cmake + git clone https://github.com/SAP/odbc-cpp-wrapper.git \ + && mkdir odbc-cpp-wrapper/build \ + && cd odbc-cpp-wrapper/build \ + && /usr/bin/cmake .. \ + && make -j 2 \ + && sudo make install + sudo ldconfig # # Workaround bug in ogdi packaging sudo ln -s /usr/lib/ogdi/libvrf.so /usr/lib diff --git a/.github/workflows/ubuntu_20.04.yml b/.github/workflows/ubuntu_20.04.yml index 357c39aebe9a..63a6a0d4d084 100644 --- a/.github/workflows/ubuntu_20.04.yml +++ b/.github/workflows/ubuntu_20.04.yml @@ -17,6 +17,8 @@ jobs: ubuntu_20_04_build: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" + env: + HANA_CONNECTION_STR: "DRIVER=/usr/sap/hdbclient/libodbcHDB.so;HOST=917df316-4e01-4a10-be54-eac1b6ab15fb.hana.prod-us10.hanacloud.ondemand.com;PORT=443;USER=GDALCI;PASSWORD=Z2!.S-bN]vh62\\G" steps: - name: Checkout uses: actions/checkout@v2 @@ -81,7 +83,7 @@ jobs: - name: Run pytest # --security-opt seccomp=unconfined, so that the userfaulfd syscall is available - run: docker run -e CI -e TRAVIS=yes -e TRAVIS_BRANCH=ubuntu_2004 -e GITHUB_WORKFLOW --security-opt seccomp=unconfined --rm gdal sh -c "cd /build/autotest && pip3 install -U -r requirements.txt && pytest" + run: docker run -e CI -e TRAVIS=yes -e TRAVIS_BRANCH=ubuntu_2004 -e GITHUB_WORKFLOW -e OGR_HANA_CONNECTION_STRING=${HANA_CONNECTION_STR} --security-opt seccomp=unconfined --rm gdal sh -c "cd /build/autotest && pip3 install -U -r requirements.txt && pip3 install -U hdbcli && pytest" - name: Build fake ossfuzz fuzzers run: docker run --rm gdal sh -c "cd /build/fuzzers && make dummyfuzzers_dynamic" diff --git a/.github/workflows/ubuntu_20.04/build-deps.sh b/.github/workflows/ubuntu_20.04/build-deps.sh index 17da5679ee83..d883f5beae1d 100755 --- a/.github/workflows/ubuntu_20.04/build-deps.sh +++ b/.github/workflows/ubuntu_20.04/build-deps.sh @@ -125,4 +125,22 @@ wget -q https://github.com/rouault/pdfium_build_gdal_3_5/releases/download/v1_pd && apt-get install -y --fix-missing --no-install-recommends liblcms2-dev \ && rm -rf /var/lib/apt/lists/* +# HANA: client side +# Install hdbsql tool +curl -v -j -k -s -L -H "Cookie: eula_3_1_agreed=tools.hana.ondemand.com/developer-license-3_1.txt" https://tools.hana.ondemand.com/additional/hanaclient-latest-linux-x64.tar.gz --output hanaclient-latest-linux-x64.tar.gz \ + && tar -xvf hanaclient-latest-linux-x64.tar.gz \ + && mkdir /usr/sap \ + && ./client/hdbinst -a client --sapmnt=/usr/sap \ + && rm -rf client \ + && rm hanaclient* +export PATH=/usr/sap/hdbclient:$PATH + +# Download and compile odbc-cpp-wrapper +git clone https://github.com/SAP/odbc-cpp-wrapper.git \ + && mkdir odbc-cpp-wrapper/build \ + && cd odbc-cpp-wrapper/build \ + && cmake .. \ + && make -j 2 \ + && make install + ldconfig diff --git a/.github/workflows/ubuntu_20.04/build.sh b/.github/workflows/ubuntu_20.04/build.sh index 69e23f49a6dc..f63f4e75fef7 100755 --- a/.github/workflows/ubuntu_20.04/build.sh +++ b/.github/workflows/ubuntu_20.04/build.sh @@ -36,6 +36,7 @@ cd /build --with-poppler \ --with-spatialite \ --with-mysql \ + --with-hana \ --with-liblzma \ --with-webp \ --with-epsilon \ diff --git a/GDALmake.opt.in b/GDALmake.opt.in index dd858a421e4e..d414e0baac4f 100644 --- a/GDALmake.opt.in +++ b/GDALmake.opt.in @@ -162,6 +162,14 @@ MYSQL_LIB = @MYSQL_LIB@ MYSQL_INC = @MYSQL_INC@ LIBS += $(MYSQL_LIB) +# +# SAP HANA support +# +HAVE_HANA = @HAVE_HANA@ +ODBCCPP_LIB = @ODBCCPP_LIB@ +ODBCCPP_INC = @ODBCCPP_INC@ +LIBS += $(ODBCCPP_LIB) + # # HDF4 Support. # diff --git a/autotest/ogr/ogr_hana.py b/autotest/ogr/ogr_hana.py new file mode 100644 index 000000000000..9c09d8970694 --- /dev/null +++ b/autotest/ogr/ogr_hana.py @@ -0,0 +1,950 @@ +#!/usr/bin/env pytest +# -*- coding: utf-8 -*- +############################################################################### +# $Id$ +# +# Project: GDAL/OGR Test Suite +# Purpose: Test SAP HANA driver functionality. +# Author: Maxim Rylov +# +############################################################################### +# Copyright (c) 2020, SAP SE +# +# 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. +############################################################################### +from os import environ + +import gdaltest +import ogrtest +import pytest + +from osgeo import ogr +from osgeo import osr +from osgeo import gdal + +try: + from hdbcli import dbapi +except ImportError: + pytest.skip("hdbcli not available", allow_module_level=True) + +pytestmark = pytest.mark.require_driver('HANA') + + +@pytest.fixture(scope="module", autouse=True) +def setup_driver(): + driver = ogr.GetDriverByName('HANA') + if driver is None: + pytest.skip("HANA driver not available", allow_module_level=True) + + conn = create_connection() + + uid = execute_sql_scalar(conn, "SELECT REPLACE(CURRENT_UTCDATE, '-', '') || '_' || BINTOHEX(SYSUUID) FROM DUMMY;") + gdaltest.hana_schema_name = '{}_{}'.format('gdal_test', uid) + + execute_sql(conn, f'CREATE SCHEMA "{gdaltest.hana_schema_name}"') + + ds = open_datasource(1) + create_tpoly_table(ds) + + yield + + execute_sql(conn, f'DROP SCHEMA "{gdaltest.hana_schema_name}" CASCADE') + + +@pytest.fixture() +def ogrsf_path(): + import test_cli_utilities + path = test_cli_utilities.get_test_ogrsf_path() + if path is None: + pytest.skip('ogrsf test utility not found') + return path + + +############################################################################### +# Test data source capabilities + +def test_ogr_hana_1(): + def test_capabilities(update, capabilities): + ds = open_datasource(update) + assert ds is not None, 'Data source is none' + + for capability in capabilities: + assert ds.TestCapability(capability[0]) == capability[1] + + test_capabilities(0, [[ogr.ODsCCreateLayer, False], + [ogr.ODsCDeleteLayer, False], + [ogr.ODsCCreateGeomFieldAfterCreateLayer, False], + [ogr.ODsCMeasuredGeometries, True], + [ogr.ODsCCurveGeometries, False], + [ogr.ODsCTransactions, True]]) + + test_capabilities(1, [[ogr.ODsCCreateLayer, True], + [ogr.ODsCDeleteLayer, True], + [ogr.ODsCCreateGeomFieldAfterCreateLayer, True], + [ogr.ODsCMeasuredGeometries, True], + [ogr.ODsCCurveGeometries, False], + [ogr.ODsCTransactions, True]]) + + +############################################################################### +# Verify data in TPOLY table + +def test_ogr_hana_2(): + ds = open_datasource() + layer = ds.GetLayerByName('TPOLY') + + shp_ds = ogr.Open('data/poly.shp') + shp_layer = shp_ds.GetLayer(0) + + assert layer.GetFeatureCount() == shp_layer.GetFeatureCount(), \ + 'feature count does not match' + assert layer.GetSpatialRef().GetAuthorityCode(None) == shp_layer.GetSpatialRef().GetAuthorityCode(None), \ + 'spatial ref does not match' + + layer.SetAttributeFilter(None) + field_count = layer.GetLayerDefn().GetFieldCount() + orig_feat = shp_layer.GetNextFeature() + + while orig_feat is not None: + read_feat = layer.GetNextFeature() + + assert read_feat.GetFieldCount() == field_count, 'Field count does not match' + + assert (ogrtest.check_feature_geometry(read_feat, orig_feat.GetGeometryRef(), + max_error=0.001) == 0) + for fld in range(field_count - 1): + assert orig_feat.GetField(fld) == read_feat.GetField(fld), \ + ('Attribute %d does not match' % fld) + + orig_feat = shp_layer.GetNextFeature() + + +############################################################################### +# Test attribute filter + +def test_ogr_hana_3(): + ds = open_datasource() + layer = ds.GetLayerByName('tpoly') + + layer.SetAttributeFilter('EAS_ID > 160 AND EAS_ID < 170') + tr = ogrtest.check_features_against_list(layer, 'EAS_ID', [168, 169, 166, 165]) + + assert layer.GetFeatureCount() == 4, \ + 'GetFeatureCount() returned %d instead of 4' % layer.GetFeatureCount() + + assert tr + + +############################################################################### +# Test spatial filter + +def test_ogr_hana_4(): + ds = open_datasource() + layer = ds.GetLayerByName('TPOLY') + + geom = ogr.CreateGeometryFromWkt('LINESTRING(479505 4763195,480526 4762819)') + layer.SetSpatialFilter(geom) + + assert layer.GetFeatureCount() == 1, \ + 'GetFeatureCount() returned %d instead of 1' % layer.GetFeatureCount() + + assert ogrtest.check_features_against_list(layer, 'EAS_ID', [158]) + + +############################################################################### +# Test reading a layer extent + +def test_ogr_hana_5(): + ds = open_datasource() + layer = ds.GetLayerByName('tpoly') + assert layer is not None, 'did not get tpoly layer' + + check_bboxes(layer.GetExtent(), (478315.53125, 481645.3125, 4762880.5, 4765610.5), 0.0001) + + +############################################################################### +# Test reading a SQL result layer extent + +def test_ogr_hana_6(): + ds = open_datasource() + layer = ds.ExecuteSQL('SELECT * FROM TPOLY') + check_bboxes(layer.GetExtent(), (478315.53125, 481645.3125, 4762880.5, 4765610.5), 0.0001) + + +############################################################################### +# Test returned spatial reference + +def test_ogr_hana_7(): + ds = open_datasource() + layer = ds.ExecuteSQL('SELECT * FROM TPOLY') + assert layer.GetSpatialRef().GetAuthorityCode(None) == '27700', \ + 'returned wrong spatial reference id' + ds.ReleaseResultSet(layer) + + +############################################################################### +# Test returned geometry type + +def test_ogr_hana_8(): + ds = open_datasource() + layer = ds.ExecuteSQL('SELECT * FROM TPOLY') + assert layer.GetLayerDefn().GetGeomFieldDefn(0).GetType() == ogr.wkbPolygon, \ + 'Returned wrong geometry type' + ds.ReleaseResultSet(layer) + + +############################################################################### +# Write new features with geometries and verify them + +def test_ogr_hana_9(): + layer_name = get_test_name() + ds = open_datasource(1) + create_tpoly_table(ds, layer_name) + + layer = ds.GetLayerByName(layer_name) + + dst_feat = ogr.Feature(feature_def=layer.GetLayerDefn()) + wkt_list = ['10', '2', '1', '3d_1', '4', '5', '6'] + + for item in wkt_list: + wkt = open('data/wkb_wkt/' + item + '.wkt').read() + geom = ogr.CreateGeometryFromWkt(wkt) + + ###################################################################### + # Write geometry as a new feature. + + dst_feat.SetGeometryDirectly(geom) + dst_feat.SetField('PRFEDEA', item) + dst_feat.SetFID(-1) + layer.CreateFeature(dst_feat) + + ###################################################################### + # Read back the feature and get the geometry. + + layer.SetAttributeFilter("PRFEDEA = '%s'" % item) + + feat_read = layer.GetNextFeature() + + if ogrtest.check_feature_geometry(feat_read, geom) != 0: + print(item) + print(wkt) + pytest.fail(geom) + + layer.ResetReading() + + +############################################################################### +# Test ExecuteSQL() without geometry + +def test_ogr_hana_10(): + ds = open_datasource() + layer = ds.ExecuteSQL('SELECT EAS_ID FROM tpoly WHERE EAS_ID IN (158, 170) ') + assert layer.GetFeatureCount() == 2, \ + 'GetFeatureCount() returned %d instead of 2' % layer.GetFeatureCount() + assert ogrtest.check_features_against_list(layer, 'EAS_ID', [158, 170]) + + +############################################################################### +# Test ExecuteSQL() results layers without geometry + +def test_ogr_hana_11(): + ds = open_datasource() + layer = ds.ExecuteSQL('SELECT DISTINCT EAS_ID FROM TPOLY ORDER BY EAS_ID DESC') + assert layer.GetFeatureCount() == 10 + + expected = [179, 173, 172, 171, 170, 169, 168, 166, 165, 158] + tr = ogrtest.check_features_against_list(layer, 'EAS_ID', expected) + ds.ReleaseResultSet(layer) + + assert tr + + +############################################################################### +# Test ExecuteSQL() with empty result set + +def test_ogr_hana_12(): + ds = open_datasource() + + layer = ds.ExecuteSQL('SELECT * FROM TPOLY WHERE EAS_ID = 7892342') + assert layer is not None, 'Expected a non None layer' + + feat = layer.GetNextFeature() + assert feat is None, 'Expected no features' + + ds.ReleaseResultSet(layer) + + +############################################################################### +# Test ExecuteSQL() with quoted table name + +def test_ogr_hana_13(): + ds = open_datasource() + layer = ds.ExecuteSQL('SELECT EAS_ID FROM "TPOLY" WHERE EAS_ID IN (158, 170) ') + assert ogrtest.check_features_against_list(layer, 'EAS_ID', [158, 170]) + + +############################################################################### +# Test GetFeature() method with an invalid id + +def test_ogr_hana_14(): + ds = open_datasource() + layer = ds.GetLayerByName('tpoly') + assert layer.GetFeature(0) is None + + +############################################################################### +# Test inserting features without geometry + +def test_ogr_hana_15(): + layer_name = get_test_name() + ds = open_datasource(1) + create_tpoly_table(ds, layer_name) + layer = ds.GetLayerByName(layer_name) + feat_count = layer.GetFeatureCount() + + dst_feat = ogr.Feature(feature_def=layer.GetLayerDefn()) + dst_feat.SetField('PRFEDEA', '7777') + dst_feat.SetField('EAS_ID', 2000) + dst_feat.SetFID(-1) + layer.CreateFeature(dst_feat) + + assert (feat_count + 1) == layer.GetFeatureCount(), \ + ('Feature count %d is not as expected %d' % (layer.GetFeatureCount(), feat_count + 1)) + + +############################################################################### +# Test reading features without geometry + +def test_ogr_hana_16(): + layer_name = get_test_name() + ds = open_datasource(1) + create_tpoly_table(ds, layer_name) + layer = ds.GetLayerByName(layer_name) + + feat = ogr.Feature(feature_def=layer.GetLayerDefn()) + feat.SetField('PRFEDEA', '7777') + feat.SetField('EAS_ID', 2000) + feat.SetFID(-1) + layer.CreateFeature(feat) + + layer.SetAttributeFilter("PRFEDEA = '7777'") + feat = layer.GetNextFeature() + assert feat.GetGeometryRef() is None, 'NULL geometry is expected' + + +############################################################################### +# Write a feature with too long a text value for a fixed length text field. +# The driver should now truncate this (but with a debug message). Also, +# put some crazy stuff in the value to verify that quoting and escaping +# is working smoothly. +# +# No geometry in this test. + +def test_ogr_hana_17(): + layer_name = get_test_name() + ds = open_datasource(1) + create_tpoly_table(ds, layer_name) + layer = ds.GetLayerByName(layer_name) + dst_feat = ogr.Feature(feature_def=layer.GetLayerDefn()) + + dst_feat.SetField('PRFEDEA', 'CrazyKey') + dst_feat.SetField('SHORTNAME', 'Crazy"\'Long') + layer.CreateFeature(dst_feat) + + layer.SetAttributeFilter("PRFEDEA = 'CrazyKey'") + feat_read = layer.GetNextFeature() + + assert feat_read is not None, 'creating crazy feature failed!' + + assert feat_read.GetField('shortname') == 'Crazy"\'L', \ + ('Value not properly escaped or truncated:' + feat_read.GetField('shortname')) + + +############################################################################### +# Verify inplace update of a feature with SetFeature() + +def test_ogr_hana_18(): + layer_name = get_test_name() + ds = open_datasource(1) + create_tpoly_table(ds, layer_name) + layer = ds.GetLayerByName(layer_name) + + feat_new = ogr.Feature(feature_def=layer.GetLayerDefn()) + feat_new.SetField('PRFEDEA', '9999') + layer.CreateFeature(feat_new) + feat_new.Destroy() + + layer.SetAttributeFilter("PRFEDEA = '9999'") + feat = layer.GetNextFeature() + layer.SetAttributeFilter(None) + + feat.SetField('SHORTNAME', 'Reset') + point = ogr.Geometry(ogr.wkbPoint25D) + point.SetPoint(0, 5, 6, 7) + feat.SetGeometryDirectly(point) + + assert layer.SetFeature(feat) == 0, 'SetFeature() method failed.' + + fid = feat.GetFID() + feat.Destroy() + + feat = layer.GetFeature(fid) + assert feat is not None, ('GetFeature(%d) failed.' % fid) + + shortname = feat.GetField('SHORTNAME') + assert shortname[:5] == 'Reset', ('SetFeature() did not update SHORTNAME, got %s.' + % shortname) + + if ogrtest.check_feature_geometry(feat, 'POINT(5 6 7)') != 0: + print(feat.GetGeometryRef()) + pytest.fail('Geometry update failed') + + feat.SetGeometryDirectly(None) + assert layer.SetFeature(feat) == 0, 'SetFeature() method failed.' + feat = layer.GetFeature(fid) + assert feat.GetGeometryRef() is None, 'Geometry update failed. null geometry expected' + + feat.SetFieldNull('SHORTNAME') + layer.SetFeature(feat) + feat = layer.GetFeature(fid) + assert feat.IsFieldNull('SHORTNAME'), 'SHORTNAME update failed. null value expected' + + # Test updating non-existing feature + feat.SetFID(-10) + assert layer.SetFeature(feat) == ogr.OGRERR_NON_EXISTING_FEATURE, \ + 'Expected failure of SetFeature().' + + +############################################################################### +# Verify that DeleteFeature() works properly + +def test_ogr_hana_19(): + layer_name = get_test_name() + ds = open_datasource(1) + create_tpoly_table(ds, layer_name) + layer = ds.GetLayerByName(layer_name) + layer.SetAttributeFilter("PRFEDEA = '35043411'") + feat = layer.GetNextFeature() + layer.SetAttributeFilter(None) + + fid = feat.GetFID() + assert layer.DeleteFeature(fid) == 0, 'DeleteFeature() method failed.' + + layer.SetAttributeFilter("PRFEDEA = '35043411'") + feat = layer.GetNextFeature() + layer.SetAttributeFilter(None) + + assert feat is None, 'DeleteFeature() seems to have had no effect.' + + # Test deleting non-existing feature + assert layer.DeleteFeature(-10) == ogr.OGRERR_NON_EXISTING_FEATURE, \ + 'Expected failure of DeleteFeature().' + + +############################################################################### +# Test default values + +def test_ogr_hana_20(): + ds = open_datasource(1) + + layer_name = get_test_name() + srs = osr.SpatialReference() + srs.ImportFromEPSG(4326) + layer = ds.CreateLayer(layer_name, srs, options=[]) + + field_values = [999, 1, 6823, 623445, 78912394123, 12.253, 534.23749234, 7234.89732, "'2018/04/25'", "'21:15:47'", + "'2018/04/25 21:15:47.987'", "'hello'", None, '74657374737472696e67', None, [], [], [], [], + 'POINT (10 10)'] + field_values_expected = [999, 1, 6823, 623445, 78912394123, 12.253, 534.23749234, 7234.89732, '2018/04/25', + '21:15:47', + '2018/04/25 21:15:47.987', 'hello', None, b'74657374737472696e67', None, [], [], [], [], + None] + + field_defns = [] + + field_defn = ogr.FieldDefn('FIELD_BOOLEAN', ogr.OFTInteger) + field_defn.SetSubType(ogr.OFSTBoolean) + field_defn.SetDefault(str(field_values[1])) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_SHORT', ogr.OFTInteger) + field_defn.SetSubType(ogr.OFSTInt16) + field_defn.SetDefault(str(field_values[2])) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_INT', ogr.OFTInteger) + field_defn.SetDefault(str(field_values[3])) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_INT64', ogr.OFTInteger64) + field_defn.SetDefault(str(field_values[4])) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_FLOAT', ogr.OFTReal) + field_defn.SetSubType(ogr.OFSTFloat32) + field_defn.SetDefault(str(field_values[5])) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_DOUBLE', ogr.OFTReal) + field_defn.SetDefault(str(field_values[6])) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_DECIMAL', ogr.OFTReal) + field_defn.SetDefault(str(field_values[7])) + field_defn.SetWidth(16) + field_defn.SetPrecision(5) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_DATE', ogr.OFTDate) + field_defn.SetDefault(str(field_values[8])) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_TIME', ogr.OFTTime) + field_defn.SetDefault(str(field_values[9])) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_TIMESTAMP', ogr.OFTDateTime) + field_defn.SetDefault(str(field_values[10])) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_STRING', ogr.OFTString) + field_defn.SetDefault(str(field_values[11])) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_STRING_NULL', ogr.OFTString) + field_defn.SetDefault(field_values[12]) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_BINARY', ogr.OFTBinary) + field_defn.SetDefault(str(field_values[13])) + field_defn.SetNullable(1) + field_defn.SetWidth(500) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_NO_DEFAULT', ogr.OFTInteger) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_INT_LIST', ogr.OFTIntegerList) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_INT64_LIST', ogr.OFTInteger64List) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_REAL_LIST', ogr.OFTRealList) + field_defns.append(field_defn) + + field_defn = ogr.FieldDefn('FIELD_STRING_LIST', ogr.OFTStringList) + field_defns.append(field_defn) + + for field_defn in field_defns: + assert layer.CreateField(field_defn) == ogr.OGRERR_NONE, \ + ('CreateField failed for %s' % field_defn.GetNameRef()) + + new_feat = ogr.Feature(layer.GetLayerDefn()) + new_feat.SetFID(field_values[0]) + new_feat.SetFieldNull('FIELD_STRING_NULL') + new_feat.SetGeometryDirectly(ogr.CreateGeometryFromWkt(field_values[19])) + assert layer.CreateFeature(new_feat) == ogr.OGRERR_NONE + + layer.ResetReading() + + layer_defn = layer.GetLayerDefn() + + ds = open_datasource(0) + layer_new = ds.GetLayerByName(layer_name) + layer_defn_new = layer_new.GetLayerDefn() + feat = layer_new.GetNextFeature() + + for i in range(layer_defn.GetFieldCount()): + field_defn = layer_defn.GetFieldDefn(i) + field_name = field_defn.GetNameRef() + field_defn_new = layer_defn_new.GetFieldDefn(layer_defn_new.GetFieldIndex(field_name)) + assert field_defn.GetDefault() == field_defn_new.GetDefault() + if field_defn.GetType() in [ogr.OFTIntegerList, ogr.OFTInteger64List, ogr.OFTRealList, ogr.OFTStringList]: + continue + expected_value = field_values_expected[i + 1] + actual = feat.GetFieldAsBinary(field_name) if field_defn.GetType() == ogr.OFTBinary else feat.GetField( + field_name) + assert check_values(actual, expected_value, 0.01), \ + pytest.fail('Values in field %s do not match (actual: %s, expected: %s)' % + (field_name, actual, expected_value)) + + +############################################################################### +# Test creating a field with the fid name + +def test_ogr_hana_21(): + ds = open_datasource(1) + layer = ds.CreateLayer(get_test_name(), geom_type=ogr.wkbNone, options=['FID=fid', 'LAUNDER=NO']) + + with gdaltest.error_handler(): + assert layer.CreateField(ogr.FieldDefn('str', ogr.OFTString)) == 0 + assert layer.CreateField(ogr.FieldDefn('fid', ogr.OFTString)) != 0 + assert layer.CreateField(ogr.FieldDefn('fid', ogr.OFTInteger)) != 0 + + layer.ResetReading() + + +############################################################################### +# Test very large query + +def test_ogr_hana_22(): + ds = open_datasource() + + query = 'eas_id = 169' + for eid in range(1000): + query = query + (' or eas_id = %d' % (eid + 1000)) + + layer = ds.GetLayerByName('TPOLY') + layer.SetAttributeFilter(query) + assert ogrtest.check_features_against_list(layer, 'eas_id', [169]) + + +############################################################################### +# Test COLUMN_TYPES layer creation option + +def test_ogr_hana_23(): + ds = open_datasource(1) + + layer_name = get_test_name() + layer = ds.CreateLayer(layer_name, + options=['COLUMN_TYPES=SINT=SMALLINT,DEC1=DECIMAL(10,5),DEC2=DECIMAL(20,0)']) + layer.CreateField(ogr.FieldDefn('SINT', ogr.OFTString)) + layer.CreateField(ogr.FieldDefn('DEC1', ogr.OFTString)) + layer.CreateField(ogr.FieldDefn('DEC2', ogr.OFTString)) + + ds = open_datasource() + layer = ds.GetLayerByName(layer_name) + layer_defn = layer.GetLayerDefn() + field_SINT = layer_defn.GetFieldDefn(layer_defn.GetFieldIndex('SINT')) + field_DEC1 = layer_defn.GetFieldDefn(layer_defn.GetFieldIndex('DEC1')) + field_DEC2 = layer_defn.GetFieldDefn(layer_defn.GetFieldIndex('DEC2')) + assert field_SINT.GetType() == ogr.OFTInteger + assert field_SINT.GetWidth() == 0 + assert field_DEC1.GetType() == ogr.OFTReal + assert field_DEC1.GetWidth() == 10 + assert field_DEC1.GetPrecision() == 5 + assert field_DEC2.GetType() == ogr.OFTReal + assert field_DEC2.GetWidth() == 20 + assert field_DEC2.GetPrecision() == 0 + + +############################################################################### +# Run test_ogrsf + +def test_ogr_hana_24(ogrsf_path): + conn_str = get_connection_str() + ';SCHEMA=' + gdaltest.hana_schema_name + ret = gdaltest.runexternal(ogrsf_path + ' "' + 'HANA:' + conn_str + '" TPOLY') + + assert ret.find('INFO') != -1 and ret.find('ERROR') == -1 + + +############################################################################### +# Run test_ogrsf with -sql + +def test_ogr_hana_25(ogrsf_path): + conn_str = get_connection_str() + ';SCHEMA=' + gdaltest.hana_schema_name + ret = gdaltest.runexternal(ogrsf_path + ' "' + 'HANA:' + conn_str + + '" -sql "SELECT * FROM TPOLY"') + + assert ret.find('INFO') != -1 and ret.find('ERROR') == -1 + + +############################################################################### +# Test retrieving an error from ExecuteSQL() + +def test_ogr_hana_26(): + ds = open_datasource() + gdal.ErrorReset() + with gdaltest.error_handler(): + layer = ds.ExecuteSQL('SELECT FROM') + assert gdal.GetLastErrorMsg() != '' + assert layer is None + + +############################################################################### +# Test array types + +def test_ogr_hana_27(): + conn = create_connection() + + def get_str(s): + if s is None: + return "NULL" + if isinstance(s, str): + return "'{}'".format(s) + return str(s) + + def test_array_type(arr_type, arr_values, expected_type, expected_sub_type = ogr.OFSTNone): + layer_name = get_test_name() + table_name = f'"{gdaltest.hana_schema_name}"."{layer_name}"' + execute_sql(conn, f'CREATE COLUMN TABLE {table_name} ( COL1 INT PRIMARY KEY, COL2 {arr_type} ARRAY )') + str_values = ', '.join(get_str(e) for e in arr_values) + execute_sql(conn, f"INSERT INTO {table_name} VALUES ( 1, ARRAY ( {str_values} ) )") + + ds = open_datasource(0) + layer = ds.GetLayerByName(layer_name) + layer_defn = layer.GetLayerDefn() + field_defn = layer_defn.GetFieldDefn(layer_defn.GetFieldIndex('COL2')) + assert field_defn.GetType() == expected_type + if field_defn.GetSubType() != ogr.OFSTNone: + assert field_defn.GetSubType() == expected_sub_type + + feat = layer.GetNextFeature() + values = feat['COL2'] + assert len(values) == len(arr_values) + for i in range(0, len(arr_values)): + assert values[i] == arr_values[i] + + execute_sql(conn, f'DROP TABLE {table_name}') + + test_array_type('BOOLEAN', [True, False], ogr.OFTIntegerList, ogr.OFSTBoolean) + test_array_type('SMALLINT', [-1, 7982], ogr.OFTIntegerList, ogr.OFSTInt16) + test_array_type('INT', [-1, 0, 1, 2, 2147483647, 4], ogr.OFTIntegerList) + test_array_type('BIGINT', [-1, 0, 9223372036854775807], ogr.OFTInteger64List) + test_array_type('DOUBLE', [-1.0002, 0.0, 4.6828734], ogr.OFTRealList) + test_array_type('NVARCHAR(300)', ['str1', '', 'str2'], ogr.OFTStringList) + test_array_type('VARCHAR(100)', ['str1', '', 'str2'], ogr.OFTStringList) + + +############################################################################### +# Test DETECT_GEOMETRY_TYPE open options + +def test_ogr_hana_28(): + ds_with_gt = open_datasource(0, 'DETECT_GEOMETRY_TYPE=YES') + layer = ds_with_gt.GetLayerByName('TPOLY') + assert layer.GetLayerDefn().GetGeomFieldDefn(0).GetType() == ogr.wkbPolygon, \ + 'Returned wrong geometry type' + ds_without_gt = open_datasource(0, 'DETECT_GEOMETRY_TYPE=NO') + layer = ds_without_gt.GetLayerByName('TPOLY') + assert layer.GetLayerDefn().GetGeomFieldDefn(0).GetType() == ogr.wkbUnknown, \ + 'Returned wrong geometry type' + + +############################################################################### +# Test CreateGeomField + +def test_ogr_hana_29(): + ds_ro = open_datasource(0) + layer = ds_ro.GetLayerByName('TPOLY') + with gdaltest.error_handler(): + assert layer.CreateGeomField(ogr.GeomFieldDefn('GEOM_FIELD', ogr.wkbPoint)) == ogr.OGRERR_FAILURE + + layer_name = get_test_name() + ds_rw = open_datasource(1) + create_tpoly_table(ds_rw, layer_name) + + layer = ds_rw.GetLayerByName(layer_name) + with gdaltest.error_handler(): + # unsupported geometry type + assert layer.CreateGeomField(ogr.GeomFieldDefn('GEOM_FIELD', ogr.wkbCompoundCurve)) == ogr.OGRERR_FAILURE + # duplicate field name + assert layer.CreateGeomField(ogr.GeomFieldDefn('OGR_GEOMETRY', ogr.wkbPoint)) == ogr.OGRERR_FAILURE + # undefined srs + assert layer.CreateGeomField(ogr.GeomFieldDefn('GEOM_FIELD', ogr.wkbPoint)) == ogr.OGRERR_FAILURE + + gfld_defn = ogr.GeomFieldDefn('', ogr.wkbPolygon) + srs = osr.SpatialReference() + srs.ImportFromEPSG(4326) + gfld_defn.SetSpatialRef(srs) + assert layer.CreateGeomField(gfld_defn) == ogr.OGRERR_NONE + layer_defn = layer.GetLayerDefn() + assert layer_defn.GetGeomFieldCount() == 2 + assert layer_defn.GetGeomFieldDefn(1).GetType() == ogr.wkbPolygon, 'Returned wrong geometry type' + assert layer_defn.GetGeomFieldDefn(1).GetName() == 'OGR_GEOMETRY_1', 'Returned wrong geometry field name' + + +############################################################################### +# Test DateTime time zones + +def test_ogr_hana_30(): + ds = open_datasource(1) + + layer_name = get_test_name() + layer = ds.CreateLayer(layer_name) + layer.CreateField(ogr.FieldDefn('DT', ogr.OFTDateTime)) + for val in ['2020/01/01 01:34:56', '2020/01/01 01:34:56+00', '2020/01/01 01:34:56.789+02']: + feat = ogr.Feature(layer.GetLayerDefn()) + feat.SetField('DT', val) + layer.CreateFeature(feat) + + ds = open_datasource(0) + layer = ds.GetLayerByName(layer_name) + for val in ['2020/01/01 01:34:56', '2020/01/01 01:34:56', '2019/12/31 23:34:56.789']: + feat = layer.GetNextFeature() + assert feat.GetField('DT') == val + + +############################################################################### +# Create a table from data/poly.shp + +def create_tpoly_table(ds, layer_name='TPOLY'): + with gdaltest.error_handler(): + ds.ExecuteSQL('DELLAYER:%s' % layer_name) + + shp_ds = ogr.Open('data/poly.shp') + shp_layer = shp_ds.GetLayer(0) + + ###################################################### + # Create Layer + layer = ds.CreateLayer(layer_name, srs=shp_layer.GetSpatialRef()) + + ###################################################### + # Check layer name + + assert layer.GetName() == layer_name, \ + pytest.fail('GetName() returned %s instead of %s' % (layer.GetName(), layer_name)) + + ###################################################### + # Check capabilities + + for capabilities in [ogr.OLCFastFeatureCount, + ogr.OLCFastSpatialFilter, + ogr.OLCFastGetExtent, + ogr.OLCCreateField, + ogr.OLCCreateGeomField, + ogr.OLCDeleteFeature, + ogr.OLCAlterFieldDefn, + ogr.OLCRandomWrite, + ogr.OLCTransactions]: + assert layer.TestCapability(capabilities) + + ###################################################### + # Setup Schema + ogrtest.quick_create_layer_def(layer, + [('AREA', ogr.OFTReal), + ('EAS_ID', ogr.OFTInteger64), + ('PRFEDEA', ogr.OFTString), + ('SHORTNAME', ogr.OFTString, 8)]) + + ###################################################### + # Check fields + + feat_def = layer.GetLayerDefn() + assert feat_def.GetGeomFieldCount() == 1, 'geometry field not found' + assert feat_def.GetFieldCount() == 4, \ + 'GetFieldCount() returned %d instead of %d' % (4, feat_def.GetFieldCount()) + + ###################################################### + # Copy in poly.shp + + dst_feat = ogr.Feature(feature_def=layer.GetLayerDefn()) + + feat = shp_layer.GetNextFeature() + while feat is not None: + dst_feat.SetFrom(feat) + layer.CreateFeature(dst_feat) + feat = shp_layer.GetNextFeature() + + +############################################################################### +# Helper methods + +def get_connection_str(): + uri = gdal.GetConfigOption('OGR_HANA_CONNECTION_STRING', None) + if uri is not None: + conn_str = uri + ';ENCRYPT=YES;SSL_VALIDATE_CERTIFICATE=false;CHAR_AS_UTF8=1' + else: + conn_str = 'HANA:autotest' + + return conn_str + + +def create_connection(): + conn_str = get_connection_str() + conn_params = dict(item.split("=") for item in conn_str.split(";")) + + with gdaltest.error_handler(): + conn = dbapi.connect(address=conn_params['HOST'], port=conn_params['PORT'], user=conn_params['USER'], + password=conn_params['PASSWORD'], ENCRYPT=conn_params['ENCRYPT'], + sslValidateCertificate=conn_params['SSL_VALIDATE_CERTIFICATE'], CHAR_AS_UTF8=1) + + if conn is None: + pytest.skip() + conn.setautocommit(False) + return conn + + +def get_test_name(): + name = environ.get('PYTEST_CURRENT_TEST').split(':')[-1].split(' ')[0] + return name.replace('test_', '').upper() + + +def execute_sql(conn, sql): + cursor = conn.cursor() + assert cursor + cursor.execute(sql) + cursor.close() + conn.commit() + + +def execute_sql_scalar(conn, sql): + cursor = conn.cursor() + assert cursor + cursor.execute(sql) + res = cursor.fetchone()[0] + cursor.close() + conn.commit() + return res + + +def open_datasource(update=0, open_opts=''): + return ogr.Open('HANA:' + get_connection_str() + ';SCHEMA=' + gdaltest.hana_schema_name + ';' + open_opts, update=update) + + +def check_bboxes(actual, expected, max_error=0.001): + minx = abs(actual[0] - expected[0]) + maxx = abs(actual[1] - expected[1]) + miny = abs(actual[2] - expected[2]) + maxy = abs(actual[3] - expected[3]) + + if max(minx, maxx, miny, maxy) > max_error: + print(actual) + pytest.fail('Extents do not match') + + +def check_values(actual, expected, max_error=0.001): + if actual is None and expected is None: + return 1 + + if isinstance(actual, float): + dif = abs(actual - expected) + if dif > max_error: + return 0 + else: + if actual != expected: + return 0 + + return 1 + + +############################################################################### +# Reverse the vertex order + +def reverse_points(poly): + for geom_index in range(poly.GetGeometryCount()): + ring = poly.GetGeometryRef(geom_index) + point_count = ring.GetPointCount() + + for point_index in range(point_count // 2): + point_rev_index = point_count - point_index - 1 + p1 = (ring.GetX(point_index), ring.GetY(point_index), ring.GetZ(point_index)) + ring.SetPoint(point_index, ring.GetX(point_rev_index), ring.GetY(point_rev_index), + ring.GetZ(point_rev_index)) + ring.SetPoint(point_rev_index, p1[0], p1[1], p1[2]) diff --git a/ci/travis/ubuntu_1804/script.sh b/ci/travis/ubuntu_1804/script.sh index 770553d7950b..ea795e0387cc 100755 --- a/ci/travis/ubuntu_1804/script.sh +++ b/ci/travis/ubuntu_1804/script.sh @@ -32,6 +32,9 @@ rm autotest/ogr/ogr_fgdb.py (cd autotest/ogr && $PYTEST ogr_mssqlspatial.py) +# HANA tests +(cd autotest/ogr && OGR_HANA_CONNECTION_STRING="DRIVER={\'/usr/sap/hdbclient/libodbcHDB.so\'};HOST=917df316-4e01-4a10-be54-eac1b6ab15fb.hana.prod-us10.hanacloud.ondemand.com;PORT=443;USER=QGISCI;PASSWORD=tQ&7W3Klr9!p" $PYTEST ogr_hana.py) + # Fails with ERROR 1: OGDI DataSource Open Failed: Could not find the dynamic library "vrf" rm autotest/ogr/ogr_ogdi.py diff --git a/cmake/helpers/CheckDependentLibraries.cmake b/cmake/helpers/CheckDependentLibraries.cmake index f6e3fd1d3b2d..e04e462a1afb 100644 --- a/cmake/helpers/CheckDependentLibraries.cmake +++ b/cmake/helpers/CheckDependentLibraries.cmake @@ -245,6 +245,7 @@ if (WIN32) else () gdal_check_package(ODBC "Enable DB support through ODBC" COMPONENTS ODBCINST CAN_DISABLE) endif () +gdal_check_package(ODBCCPP "odbc-cpp library (external)" CAN_DISABLE) gdal_check_package(MSSQL_NCLI "MSSQL Native Client to enable bulk copy" CAN_DISABLE) gdal_check_package(MSSQL_ODBC "MSSQL ODBC driver to enable bulk copy" CAN_DISABLE) diff --git a/cmake/modules/packages/FindODBCCPP.cmake b/cmake/modules/packages/FindODBCCPP.cmake new file mode 100644 index 000000000000..b04632233d86 --- /dev/null +++ b/cmake/modules/packages/FindODBCCPP.cmake @@ -0,0 +1,72 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file COPYING-CMAKE-SCRIPTS or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: +FindODBCCPP +-------------- + +Find the odbc-cpp-wrapper includes and library. + +IMPORTED Targets +^^^^^^^^^^^^^^^^ + +This module defines :prop_tgt:`IMPORTED` target ``ODBCCPP::ODBCCPP``, if +the odbc-cpp-wrapper has been found. + +Result Variables +^^^^^^^^^^^^^^^^ + +This module defines the following variables: + +:: + + ODBCCPP_INCLUDE_DIRS - where to find the odbc-cpp-wrapper headers. + ODBCCPP_LIBRARIES - List of libraries when using the odbc-cpp-wrapper. + ODBCCPP_FOUND - True if the odbc-cpp-wrapper found. + +#]=======================================================================] + +if(NOT ODBCCPP_INCLUDE_DIR) + find_path(ODBCCPP_INCLUDE_DIR odbc/Environment.h + PATHS + /usr/include + /usr/local/include + c:/msys/local/include + "$ENV{LIB_DIR}/include" + "$ENV{INCLUDE}" + "$ENV{ODBCCPP_PATH}/include") +endif(NOT ODBCCPP_INCLUDE_DIR) + +if(NOT ODBCCPP_LIBRARY) + find_library(ODBCCPP_LIBRARY odbccpp + PATHS + /usr/lib + /usr/local/lib + c:/msys/local/lib + "$ENV{LIB_DIR}/lib" + "$ENV{LIB}" + "$ENV{ODBCCPP_PATH}/lib") +endif(NOT ODBCCPP_LIBRARY) + +if(ODBCCPP_INCLUDE_DIR AND ODBCCPP_LIBRARY) + set(ODBCCPP_FOUND TRUE) +endif(ODBCCPP_INCLUDE_DIR AND ODBCCPP_LIBRARY) + +mark_as_advanced(ODBCCPP_LIBRARY ODBCCPP_INCLUDE_DIR) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(ODBCCPP + FOUND_VAR ODBCCPP_FOUND + REQUIRED_VARS ODBCCPP_LIBRARY ODBCCPP_INCLUDE_DIR) + +if(ODBCCPP_FOUND) + set(ODBCCPP_INCLUDE_DIRS ${ODBCCPP_INCLUDE_DIR}) + set(ODBCCPP_LIBRARIES ${ODBCCPP_LIBRARY}) + if(NOT TARGET ODBCCPP::ODBCCPP) + add_library(ODBCCPP::ODBCCPP UNKNOWN IMPORTED) + set_target_properties(ODBCCPP::ODBCCPP PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${ODBCCPP_INCLUDE_DIRS}" + IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" + IMPORTED_LOCATION "${ODBCCPP_LIBRARY}") + endif() +endif() diff --git a/cmake/template/gdal_def.h.in b/cmake/template/gdal_def.h.in index 2bd4a8dfe6fa..60e42cc1bcf3 100644 --- a/cmake/template/gdal_def.h.in +++ b/cmake/template/gdal_def.h.in @@ -12,6 +12,9 @@ /* Define if gdal used odbc */ #cmakedefine GDAL_USE_ODBC +/* Define if gdal used odbc-cpp */ +#cmakedefine GDAL_USE_ODBCCPP + /* Define if pam enable in gdal */ #cmakedefine ENABLE_PAM diff --git a/configure.ac b/configure.ac index cd71bf547b8b..f06bf0139cc9 100644 --- a/configure.ac +++ b/configure.ac @@ -3896,6 +3896,62 @@ fi AC_SUBST(ODBC_SETTING,$ODBC_SETTING) +dnl --------------------------------------------------------------------------- +dnl Check for SAP HANA support. +dnl --------------------------------------------------------------------------- + +AC_ARG_WITH(hana, AS_HELP_STRING([--with-hana[=ARG]] Include SAP HANA (ARG=no(default), yes or path to odbc-cpp-wrapper)),,) + +AC_MSG_CHECKING([for SAP HANA]) + +if test "x$with_hana" = "xno" -o "x$with_hana" = "x" ; then + HAVE_HANA=no + ODBCCPP_LIB= + ODBCCPP_INC= + AC_MSG_RESULT([no]) +else + if test "$ODBC_SETTING" = "no" ; then + AC_MSG_ERROR([SAP HANA driver requires odbc driver to be installed.]) + fi + + if test "$with_hana" = "yes" ; then + ODBCCPP_PATH=/usr/local + else + ODBCCPP_PATH=$with_hana + fi + + if test -d ${ODBCCPP_PATH} ; then + AC_LANG_PUSH(C++) + LIBS="-lodbccpp $LIBS" + AC_CHECK_HEADER(odbc/PreparedStatement.h) + AC_LINK_IFELSE( + [AC_LANG_PROGRAM( + [#include ], + [ + odbc::EnvironmentRef env = odbc::Environment::create(); + ] + )], + [HAVE_HANA="yes"], + [AC_MSG_WARN([Cannot find odbccpp.])]) + AC_LANG_POP(C++) + + if test "${HAVE_HANA}" = "yes" ; then + ODBCCPP_LIB=-L"$ODBCCPP_PATH/lib" + ODBCCPP_INC=-I"$ODBCCPP_PATH/include" + AC_MSG_RESULT([yes]) + else + AC_MSG_ERROR([--with-hana argument points to a directory which does not contain the odbc-cpp-wrapper library.]) + fi + else + AC_MSG_RESULT([no]) + AC_MSG_ERROR([--with-hana argument is not a directory. It should be the path to the odbc-cpp-wrapper library, often somewhere like /usr/local.]) + fi +fi + +AC_SUBST(HAVE_HANA,$HAVE_HANA) +AC_SUBST(ODBCCPP_INC,$ODBCCPP_INC) +AC_SUBST(ODBCCPP_LIB,$ODBCCPP_LIB) + dnl --------------------------------------------------------------------------- dnl Check for OCI support. dnl --------------------------------------------------------------------------- @@ -5864,6 +5920,7 @@ LOC_MSG([ QHull support: ${QHULL_SETTING}]) LOC_MSG([ Rasdaman support: ${RASDAMAN_ENABLED}]) LOC_MSG([ RasterLite2 support: ${HAVE_RASTERLITE2}]) LOC_MSG([ RDB support: ${RDB_SETTING}]) +LOC_MSG([ SAP HANA support: ${HAVE_HANA}]) LOC_MSG([ SFCGAL support: ${HAVE_SFCGAL}]) LOC_MSG([ SOSI support: ${SOSI_ENABLED}]) LOC_MSG([ SpatiaLite support: ${HAVE_SPATIALITE}]) diff --git a/doc/source/build_hints.rst b/doc/source/build_hints.rst index 404696b708d7..01e35e3fbedf 100644 --- a/doc/source/build_hints.rst +++ b/doc/source/build_hints.rst @@ -1129,8 +1129,8 @@ The ``nc-config`` program can be used to detect it. ODBC **** -ODBC is required for various drivers: :ref:`vector.odbc`, :ref:`vector.pgeo` -and :ref:`vector.mssqlspatial`. +ODBC is required for various drivers: :ref:`vector.odbc`, :ref:`vector.pgeo`, +:ref:`vector.hana` and :ref:`vector.mssqlspatial`. It is normally automatically found in system directories on Unix and Windows. .. option:: ODBC_INCLUDE_DIR @@ -1146,6 +1146,25 @@ It is normally automatically found in system directories on Unix and Windows. Control whether to use ODBC. Defaults to ON when ODBC is found. +ODBC-CPP +******** + +The `odbc-cpp-wrapper library `_ is required for +the :ref:`vector.hana` driver. + +.. option:: ODBCCPP_INCLUDE_DIR + + Path to an include directory with the ``odbc/Environment.h`` header file. + +.. option:: ODBCCPP_LIBRARY + + Path to a shared or static library file. + +.. option:: GDAL_USE_ODBCCPP=ON/OFF + + Control whether to use ODBC-CPP. Defaults to ON when ODBC-CPP is found. + + OGDI **** diff --git a/doc/source/drivers/vector/hana.rst b/doc/source/drivers/vector/hana.rst new file mode 100644 index 000000000000..415c54104b57 --- /dev/null +++ b/doc/source/drivers/vector/hana.rst @@ -0,0 +1,230 @@ +.. _vector.hana: + +SAP HANA +==================== + +.. shortname:: HANA + +.. build_dependencies:: odbc-cpp-wrapper + +This driver implements read and write access for spatial data stored in +an `SAP HANA `__ database. + +Driver capabilities +------------------- + +.. supports_create:: + +.. supports_georeferencing:: + +Connecting to a database +------------------------ + +| To connect to an SAP HANA database, use a connection string + specifying the database name, with additional parameters as necessary. + The HANA: prefix is used to mark the name as a HANA connection string. + + :: + + HANA:"DRIVER={HDBODBC};DATABASE=HAN;HOST=localhost;PORT=30015;USER=mylogin;PASSWORD=mypassword;SCHEMA=MYSCHEMA" + + In this syntax each parameter setting is in the form keyword = value. + Spaces around the equal sign are optional. To write an empty value, or a + value containing spaces, surround it with single quotes, e.g., + keyword = 'a value'. Single quotes and backslashes within the value must + be escaped with a backslash, i.e., \' and \\. + + +SQL statements +-------------- + +The HANA driver passes SQL statements directly to HANA by +default, rather than evaluating them internally when using the +ExecuteSQL() call on the OGRDataSource, or the -sql command option to +ogr2ogr. Attribute query expressions are also passed directly through to +HANA. It's also possible to request the OGR HANA driver to handle +SQL commands with the :ref:`OGR SQL ` engine, by +passing **"OGRSQL"** string to the ExecuteSQL() method, as the name of +the SQL dialect. + +The HANA driver in OGR supports the OGRDataSource::StartTransaction(), +OGRDataSource::CommitTransaction() and OGRDataSource::RollbackTransaction() +calls in the normal SQL sense. + +Creation Issues +--------------- + +The HANA driver does not support creation of new schemas, but it +does allow creation of new layers (tables) within an existing schema. + +Dataset Open options +~~~~~~~~~~~~~~~~~~~~ + +- **DSN**\ =string: Data source name. +- **DRIVER**\ =string: Name or a path to a driver. For example, + DRIVER={HDBODBC} (Windows) or DRIVER=/usr/sap/hdbclient/libodbcHDB.so + (Linux/MacOS). +- **HOST**\ =string: Server host name. +- **PORT**\ =integer: Port number. +- **USER**\ =string: User name. +- **PASSWORD**\ =string: User password. +- **DATABASE**\ =string: Database name. +- **SCHEMA**\ =string: Specifies schema used for tables listed in TABLES + option. +- **TABLES**\ =string: Restricted set of tables to list (comma separated). +- **ENCRYPT**\ =boolean: Enables or disables TLS/SSL encryption. The default + value is "NO". +- **SSL_CRYPTO_PROVIDER**\ =string: Cryptographic library provider used for + SSL communication (commoncrypto| sapcrypto | openssl). +- **SSL_KEY_STORE**\ =string: Path to the keystore file that contains the + server's private key. +- **SSL_TRUST_STORE**\ =string: Path to trust store file that contains the + server's public certificate(s) (OpenSSL only). +- **SSL_VALIDATE_CERTIFICATE**\ =string: If set to true, the server's + certificate is validated. The default value is "YES". +- **SSL_HOST_NAME_IN_CERTIFICATE**\ =string: Host name used to verify server's + identity validated. +- **CONNECTION_TIMEOUT**\ =integer: Connection timeout measured in + milliseconds. The default value is 0 (disabled). +- **PACKET_SIZE**\ =integer: Sets the maximum size of a request packet sent + from the client to the server, in bytes. The minimum is 1 MB. The default + value is 1 MB. +- **SPLIT_BATCH_COMMANDS**\ =boolean: Allows split and parallel execution of + batch commands on partitioned tables. The default value is "YES". +- **DETECT_GEOMETRY_TYPE**\ =boolean: Specifies whether to detect the type of + geometry columns. Note, the detection may take a significant amount of time + for large tables. The default value is "YES". + +Dataset Creation Options +~~~~~~~~~~~~~~~~~~~~~~~~ + +None + +Layer Creation Options +~~~~~~~~~~~~~~~~~~~~~~ + +- **OVERWRITE**: This may be "YES" to force an existing layer of the + desired name to be destroyed before creating the requested layer. + The default value is "NO". +- **LAUNDER**: This may be "YES" to force new fields created on this + layer to have their field names "laundered" into a form more + compatible with HANA. This converts to upper case and converts + some special characters like "-" and "#" to "_". If "NO" exact names + are preserved. The default value is "YES". If enabled the table + (layer) name will also be laundered. +- **PRECISION**: This may be "YES" to force new fields created on this + layer to try and represent the width and precision information, if + available using DECIMAL(width,precision) or CHAR(width) types. If + "NO" then the types REAL, INTEGER and VARCHAR will be used instead. + The default is "YES". +- **DEFAULT_STRING_SIZE**: Specifies default string column size. The + default value is 256. +- **GEOMETRY_NAME**: Specifies the name of the geometry column in new + table. If omitted it defaults to *GEOMETRY*. +- **GEOMETRY_NULLABLE**: Specifies whether the values of the geometry + column can be NULL or not. The default value is "YES". +- **SRID**: Specifies the SRID of the layer. +- **FID**: Specifies the name of the FID column to create. The default + value is 'OGR_FID'. +- **FID64**: Specifies whether to create the FID column with BIGINT + type to handle 64bit wide ids. The default value is NO. +- **COLUMN_TYPES**: Specifies a comma-separated list of strings in + the format field_name=hana_field_type that define column types. +- **BATCH_SIZE**: Specifies the number of bytes to be written per one + batch. The default value is 4194304 (4MB). + +Multitenant Database Containers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to connect to a tenant database, you need to specify a port number +assigned exactly to a desired instance. This port number can be determined +by executing the following query from the tenant database. + + :: + + SELECT SQL_PORT FROM SYS.M_SERVICES WHERE ((SERVICE_NAME='indexserver' and COORDINATOR_TYPE= 'MASTER') or (SERVICE_NAME='xsengine')) + +For more details, see **Section 2.9 Connections for Multitenant Database Containers** +in `SAP HANA Multitenant Database Containers `__. + + +Examples +-------- + +- This example shows how to list HANA layers on a specified host using + :ref:`ogrinfo` command. + + :: + + ogrinfo -ro HANA:"DRIVER={HDBODBC};DATABASE=HAN;HOST=localhost;PORT=30015;USER=mylogin;PASSWORD=mypassword;SCHEMA=MYSCHEMA" + + or + + :: + + ogrinfo -ro HANA:"DSN=MYHANADB;USER=mylogin;PASSWORD=mypassword;SCHEMA=MYSCHEMA" + +- This example shows how to print summary information about a given layer, + i.e. 'planet_osm_line', using :ref:`ogrinfo`. + + :: + + ogrinfo -ro HANA:"DRIVER={HDBODBC};DATABASE=HAN;HOST=localhost;PORT=30015;USER=mylogin;PASSWORD=mypassword;SCHEMA=MYSCHEMA" -so "planet_osm_line" + + Layer name: planet_osm_line + Geometry: Line String + Feature Count: 81013 + Extent: (732496.086304, 6950959.464783) - (1018694.144531, 7204272.976379) + Layer SRS WKT: + PROJCS["WGS 84 / Pseudo-Mercator", + GEOGCS["WGS 84", + DATUM["WGS_1984", + SPHEROID["WGS 84",6378137,298.257223563, AHORITY["EPSG","7030"]], + AUTHORITY["EPSG","6326"]], + PRIMEM["Greenwich",0, AUTHORITY["EPSG","8901"]], + UNIT["degree",0.0174532925199433, AUTHORITY["EPSG","9122"]], + AUTHORITY["EPSG","4326"]], + PROJECTION["Mercator_1SP"], + PARAMETER["central_meridian",0], + PARAMETER["scale_factor",1], + PARAMETER["false_easting",0], + PARAMETER["false_northing",0], + UNIT["metre",1,AUTHORITY["EPSG","9001"]], + AXIS["X",EAST], + AXIS["Y",NORTH], + AUTHORITY["EPSG","3857"]] + Geometry Column = way + osm_id: Integer64 (0.0) + access: String (4000.0) + addr:housename: String (4000.0) + addr:housenumber: String (4000.0) + addr:interpolation: String (4000.0) + admin_level: String (4000.0) + aerialway: String (4000.0) + aeroway: String (4000.0) + +- This example shows how to export data from the 'points' table to a shapefile called 'points_output.shp'. + + :: + + ogr2ogr -f "ESRI Shapefile" "D:\\points_output.shp" HANA:"DRIVER={HDBODBC};DATABASE=HAN;HOST=localhost;PORT=30015;USER=mylogin;PASSWORD=mypassword;SCHEMA=GIS;TABLES=points" + +- This example shows how to create and populate a table with data taken from a shapefile. + + :: + + ogr2ogr -f HANA HANA:"DRIVER={HDBODBC};DATABASE=HAN;HOST=localhost;PORT=30015;USER=mylogin;PASSWORD=mypassword;SCHEMA=MYSCHEMA" myshapefile.shp + + +For developers +-------------- + +To compile the SAP HANA driver, `odbc-cpp-wrapper `__ library needs to be linked or installed. +For more details, see comments in nmake.opt or configure.ac files to build the driver for Windows or Linux/MacOS correspondingly. + +See Also +-------- + +- `SAP HANA Home Page `__ +- `SAP HANA Spatial Reference `__ +- `SAP HANA ODBC Connection Properties `__ diff --git a/doc/source/drivers/vector/index.rst b/doc/source/drivers/vector/index.rst index bc010685be27..bfbe398839ec 100644 --- a/doc/source/drivers/vector/index.rst +++ b/doc/source/drivers/vector/index.rst @@ -55,6 +55,7 @@ Vector drivers gpsbabel gpx grass + hana idb idrisi ili diff --git a/doc/source/user/ogr_sql_dialect.rst b/doc/source/user/ogr_sql_dialect.rst index 245b960366b2..0246e874cb7e 100644 --- a/doc/source/user/ogr_sql_dialect.rst +++ b/doc/source/user/ogr_sql_dialect.rst @@ -705,7 +705,7 @@ Non-OGR SQL All OGR drivers for database systems: :ref:`vector.mysql`, :ref:`vector.pg`, :ref:`vector.oci`, :ref:`vector.sqlite`, :ref:`vector.odbc`, :ref:`vector.pgeo`, -and :ref:`vector.mssqlspatial`, +:ref:`vector.hana` and :ref:`vector.mssqlspatial`, override the :cpp:func:`GDALDataset::ExecuteSQL` function with dedicated implementation and, by default, pass the SQL statements directly to the underlying RDBMS. In these cases the SQL syntax varies in some particulars from OGR SQL. diff --git a/frmts/drivers.ini b/frmts/drivers.ini index 6a8647d69abb..d043efde6853 100644 --- a/frmts/drivers.ini +++ b/frmts/drivers.ini @@ -252,6 +252,7 @@ GMLAS MVT NGW MapML +HANA # Put TIGER and AVCBIN at end since they need poOpenInfo->GetSiblingFiles() Tiger AVCBin diff --git a/ogr/ogrsf_frmts/CMakeLists.txt b/ogr/ogrsf_frmts/CMakeLists.txt index 4b280f279100..782cf0f231c2 100644 --- a/ogr/ogrsf_frmts/CMakeLists.txt +++ b/ogr/ogrsf_frmts/CMakeLists.txt @@ -110,6 +110,7 @@ ogr_dependent_driver(idb "IDB" "GDAL_USE_IDB") ogr_dependent_driver(ods ODS "GDAL_USE_EXPAT") ogr_dependent_driver(ogdi "OGDI" "GDAL_USE_OGDI") ogr_dependent_driver(lvbag "LVBAG" "GDAL_USE_EXPAT") +ogr_dependent_driver(hana "SAP HANA" "GDAL_USE_ODBCCPP;GDAL_USE_ODBC") # Add generic in last line add_subdirectory(generic) diff --git a/ogr/ogrsf_frmts/GNUmakefile b/ogr/ogrsf_frmts/GNUmakefile index 81d151c465f5..44e873918be3 100644 --- a/ogr/ogrsf_frmts/GNUmakefile +++ b/ogr/ogrsf_frmts/GNUmakefile @@ -8,6 +8,7 @@ SUBDIRS-$(HAVE_TEIGHA) += dwg SUBDIRS-$(HAVE_GRASS) += grass SUBDIRS-$(HAVE_IDB) += idb SUBDIRS-$(HAVE_MYSQL) += mysql +SUBDIRS-$(HAVE_HANA) += hana SUBDIRS-$(HAVE_OGDI) += ogdi SUBDIRS-$(HAVE_OCI) += oci SUBDIRS-$(HAVE_OGR_PG) += pg diff --git a/ogr/ogrsf_frmts/generic/GNUmakefile b/ogr/ogrsf_frmts/generic/GNUmakefile index 251c80cad741..1099ec7b46d1 100644 --- a/ogr/ogrsf_frmts/generic/GNUmakefile +++ b/ogr/ogrsf_frmts/generic/GNUmakefile @@ -35,6 +35,10 @@ ifeq ($(HAVE_MYSQL),yes) CXXFLAGS := $(CXXFLAGS) -DMYSQL_ENABLED endif +ifeq ($(HAVE_HANA),yes) +CXXFLAGS := $(CXXFLAGS) -DHANA_ENABLED +endif + ifeq ($(PCIDSK_SETTING),internal) CXXFLAGS := $(CXXFLAGS) -DPCIDSK_ENABLED endif diff --git a/ogr/ogrsf_frmts/generic/ogrregisterall.cpp b/ogr/ogrsf_frmts/generic/ogrregisterall.cpp index 4c946096fe30..42a274a1fa46 100644 --- a/ogr/ogrsf_frmts/generic/ogrregisterall.cpp +++ b/ogr/ogrsf_frmts/generic/ogrregisterall.cpp @@ -253,6 +253,9 @@ void OGRRegisterAllInternal() #ifdef MAPML_ENABLED RegisterOGRMapML(); #endif +#ifdef HANA_ENABLED + RegisterOGRHANA(); +#endif // NOTE: you need to generally insert your own driver before that line. diff --git a/ogr/ogrsf_frmts/hana/CMakeLists.txt b/ogr/ogrsf_frmts/hana/CMakeLists.txt new file mode 100644 index 000000000000..684d5a145c6b --- /dev/null +++ b/ogr/ogrsf_frmts/hana/CMakeLists.txt @@ -0,0 +1,13 @@ +add_gdal_driver( + TARGET ogr_HANA + SOURCES ogrhanadatasource.cpp + ogrhanadriver.cpp + ogrhanafeaturereader.cpp + ogrhanafeaturewriter.cpp + ogrhanalayer.cpp + ogrhanaresultlayer.cpp + ogrhanatablelayer.cpp + ogrhanautils.cpp + PLUGIN_CAPABLE) +gdal_standard_includes(ogr_HANA) +gdal_target_link_libraries(ogr_HANA PRIVATE ODBCCPP::ODBCCPP) diff --git a/ogr/ogrsf_frmts/hana/GNUmakefile b/ogr/ogrsf_frmts/hana/GNUmakefile new file mode 100644 index 000000000000..7fd05ca77527 --- /dev/null +++ b/ogr/ogrsf_frmts/hana/GNUmakefile @@ -0,0 +1,27 @@ +# $Id$ +# +# Makefile to build OGR SAP HANA driver +# + +include ../../../GDALmake.opt + +OBJ = ogrhanadriver.o ogrhanadatasource.o ogrhanautils.o \ + ogrhanatablelayer.o ogrhanalayer.o ogrhanaresultlayer.o \ + ogrhanafeaturereader.o ogrhanafeaturewriter.o + +CPPFLAGS := -I.. -I../.. $(ODBCCPP_INC) -DODBC_STATIC $(CPPFLAGS) + +default: $(O_OBJ:.o=.$(OBJ_EXT)) + +clean: + rm -f *.o $(O_OBJ) + +$(O_OBJ): ogr_hana.h ogrhanautils.h ogrhanafeaturereader.h ogrhanafeaturewriter.h + +PLUGIN_SO = ogr_HANA.$(SO_EXT) + +plugin: $(PLUGIN_SO) + +$(PLUGIN_SO): $(OBJ) + $(LD_SHARED) $(LNK_FLAGS) $(OBJ) $(CONFIG_LIBS_INS) $(LIBS) \ + -o $(PLUGIN_SO) diff --git a/ogr/ogrsf_frmts/hana/ogr_hana.h b/ogr/ogrsf_frmts/hana/ogr_hana.h new file mode 100644 index 000000000000..1e913cf25d8b --- /dev/null +++ b/ogr/ogrsf_frmts/hana/ogr_hana.h @@ -0,0 +1,365 @@ +/****************************************************************************** + * + * Project: SAP HANA Spatial Driver + * Purpose: SAP HANA Spatial OGR Driver Declarations. + * Author: Maxim Rylov + * + ****************************************************************************** + * Copyright (c) 2020, SAP SE + * + * 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. + ****************************************************************************/ + +#ifndef OGR_HANA_H_INCLUDED +#define OGR_HANA_H_INCLUDED + +#include "ogrsf_frmts.h" +#include "cpl_string.h" + +#include +#include +#include +#include + +#include "odbc/Forwards.h" + +class OGRHanaDataSource; + +namespace OGRHANA { + +constexpr static int DEFAULT_BATCH_SIZE = 4 * 1024 * 1024; +constexpr static int DEFAULT_STRING_SIZE = 256; + +constexpr static int UNDETERMINED_SRID = -1; + +/************************************************************************/ +/* Internal struct definitions */ +/************************************************************************/ + +struct ColumnDefinition +{ + CPLString name; + CPLString typeDef; +}; + +struct AttributeColumnDescription +{ + CPLString name; + short type = -1; + CPLString typeName; + int length = 0; // the same type as in OGRFieldDefn.GetWidth + unsigned short precision = 0; + unsigned short scale = 0; + bool isFeatureID = false; + bool isArray = false; + bool isAutoIncrement = false; + bool isNullable = false; + CPLString defaultValue; +}; + +struct GeometryColumnDescription +{ + CPLString name; + OGRwkbGeometryType type; + int srid; + bool isNullable; +}; + +struct ColumnDescription +{ + bool isGeometry; + AttributeColumnDescription attributeDescription; + GeometryColumnDescription geometryDescription; +}; + +struct ColumnTypeInfo +{ + CPLString name; + short type; + int width; + int precision; +}; + +struct Binary +{ + GByte* data; + std::size_t size; +}; + +/************************************************************************/ +/* OGRHanaLayer */ +/************************************************************************/ + +class OGRHanaLayer : public OGRLayer +{ +protected: + OGRHanaDataSource* dataSource_ = nullptr; + OGRFeatureDefn* featureDefn_ = nullptr; + GIntBig nextFeatureId_ = 0; + std::vector attrColumns_; + std::vector geomColumns_; + int fidFieldIndex_ = OGRNullFID; + CPLString fidFieldName_; + CPLString rawQuery_; + CPLString queryStatement_; + CPLString whereClause_; + CPLString attrFilter_; + bool rebuildQueryStatement_ = true; + odbc::ResultSetRef resultSet_; + std::vector dataBuffer_; + + void BuildQueryStatement(); + void BuildWhereClause(); + void EnsureBufferCapacity(std::size_t size); + virtual OGRFeature* GetNextFeatureInternal(); + int GetGeometryColumnSrid(int columnIndex) const; + virtual OGRFeature* ReadFeature(); + OGRErr ReadFeatureDefinition( + const CPLString& schemaName, + const CPLString& tableName, + const CPLString& query, + const char* featureDefName); + void ReadGeometryExtent(int geomField, OGREnvelope* extent); + +public: + explicit OGRHanaLayer(OGRHanaDataSource* datasource); + ~OGRHanaLayer() override; + + void ResetReading() override; + + OGRErr GetExtent(OGREnvelope* extent, int force = TRUE) override + { + return GetExtent(0, extent, force); + } + OGRErr GetExtent(int geomField, OGREnvelope* extent, int force) override; + GIntBig GetFeatureCount(int force) override; + OGRFeature* GetNextFeature() override; + OGRFeatureDefn* GetLayerDefn() override { return featureDefn_; } + const char* GetFIDColumn() override; + + OGRErr SetAttributeFilter( const char *pszQuery ) override; + + void SetSpatialFilter(OGRGeometry* poGeom) override + { + SetSpatialFilter(0, poGeom); + } + void SetSpatialFilter(int iGeomField, OGRGeometry* poGeom) override; +}; + +/************************************************************************/ +/* OGRHanaTableLayer */ +/************************************************************************/ + +class OGRHanaTableLayer final : public OGRHanaLayer +{ +private: + CPLString schemaName_; + CPLString tableName_; + bool updateMode_ = false; + + odbc::PreparedStatementRef currentIdentityValueStmt_; + odbc::PreparedStatementRef insertFeatureStmtWithFID_; + odbc::PreparedStatementRef insertFeatureStmtWithoutFID_; + odbc::PreparedStatementRef deleteFeatureStmt_; + odbc::PreparedStatementRef updateFeatureStmt_; + + std::size_t batchSize_ = DEFAULT_BATCH_SIZE; + std::size_t defaultStringSize_ = DEFAULT_STRING_SIZE; + bool launderColumnNames_ = true; + bool preservePrecision_ = true; + std::vector customColumnDefs_; + bool parseFunctionsChecked_ = false; + + OGRErr ReadTableDefinition(); + + std::pair ExecuteUpdate( + odbc::PreparedStatement& statement, bool withBatch, const char* functionName); + odbc::PreparedStatementRef CreateDeleteFeatureStatement(); + odbc::PreparedStatementRef CreateInsertFeatureStatement(bool withFID); + odbc::PreparedStatementRef CreateUpdateFeatureStatement(); + void ResetPreparedStatements(); + OGRErr SetStatementParameters( + odbc::PreparedStatement& statement, + OGRFeature* feature, + bool newFeature, + bool withFID, + const char* functionName); + + void FlushPendingFeatures(); + bool HasPendingFeatures() const; + ColumnTypeInfo GetColumnTypeInfo(const OGRFieldDefn& field) const; + OGRErr GetGeometryWkb(OGRFeature* feature, int fieldIndex, Binary& binary); + void ClearBatches(); + +public: + OGRHanaTableLayer(OGRHanaDataSource* datasource, int update); + ~OGRHanaTableLayer() override; + + OGRErr Initialize(const char* schemaName, const char* tableName); + + OGRErr DropTable(); + + void ResetReading() override; + const char* GetName() override { return tableName_.c_str(); } + int TestCapability(const char* capabilities) override; + + OGRErr ICreateFeature(OGRFeature* feature) override; + OGRErr DeleteFeature(GIntBig nFID) override; + OGRErr ISetFeature(OGRFeature* feature) override; + + OGRErr CreateField(OGRFieldDefn* field, int approxOK = TRUE) override; + OGRErr CreateGeomField( + OGRGeomFieldDefn* geomField, int approxOK = TRUE) override; + OGRErr DeleteField(int field) override; + OGRErr AlterFieldDefn( + int field, OGRFieldDefn* newFieldDefn, int flagsIn) override; + + void SetBatchSize(std::size_t size) { batchSize_ = size; } + void SetDefaultStringSize(std::size_t size) { defaultStringSize_ = size; } + void SetLaunderFlag(bool flag) { launderColumnNames_ = flag; } + void SetCustomColumnTypes(const char* columnTypes); + void SetPrecisionFlag(bool flag) { preservePrecision_ = flag; } + + OGRErr StartTransaction() override; + OGRErr CommitTransaction() override; + OGRErr RollbackTransaction() override; +}; + +/************************************************************************/ +/* OGRHanaResultLayer */ +/************************************************************************/ + +class OGRHanaResultLayer final : public OGRHanaLayer +{ +public: + explicit OGRHanaResultLayer(OGRHanaDataSource* datasource); + + OGRErr Initialize(const char* query, OGRGeometry* spatialFilter); + + int TestCapability(const char* capabilities) override; +}; + +} /* end of OGRHANA namespace */ + +/************************************************************************/ +/* OGRHanaDataSource */ +/************************************************************************/ + +class OGRHanaDataSource final : public GDALDataset +{ +private: + friend class OGRHANA::OGRHanaLayer; + friend class OGRHANA::OGRHanaTableLayer; + friend class OGRHANA::OGRHanaResultLayer; + + using SrsCache = std::unordered_map; + + CPLString schemaName_; + bool updateMode_ = false; + bool detectGeometryType_ = true; + bool isTransactionStarted_ = false; + std::vector> layers_; + SrsCache srsCache_; + odbc::EnvironmentRef connEnv_; + odbc::ConnectionRef conn_; + int majorVersion_ = 0; + +private: + void CreateTable( + const CPLString& tableName, + const CPLString& fidName, + const CPLString& fidType, + const CPLString& geomColumnName, + OGRwkbGeometryType geomType, + bool geomColumnNullable, + const CPLString& geomColumnIndexType, + int geomSrid); + +protected: + std::pair FindSchemaAndTableNames(const char* query); + int FindLayerByName(const char* name); + CPLString FindSchemaName(const char* objectName); + + odbc::StatementRef CreateStatement(); + odbc::PreparedStatementRef PrepareStatement(const char* sql); + void Commit(); + void ExecuteSQL(const char* sql); + + OGRSpatialReference* GetSrsById(int srid); + int GetSrsId(OGRSpatialReference* srs); + bool IsSrsRoundEarth(int srid); + bool HasSrsPlanarEquivalent(int srid); + OGRErr GetQueryColumns( + const CPLString& schemaName, + const CPLString& query, + std::vector& columDescriptions); + std::vector GetTablePrimaryKeys( + const char* schemaName, const char* tableName); + + void InitializeLayers( + const char* schemaName, + const char* tableNames); + void CreateSpatialReferenceSystem( + const OGRSpatialReference& srs, + int srid, + const char* authorityName, + int authorityCode, + const CPLString& wkt, + const CPLString& proj4); + + bool IsTransactionStarted() const { return isTransactionStarted_; } + + void CreateParseArrayFunctions(const char* schemaName); + bool ParseArrayFunctionsExist(const char* schemaName); + +public: + static const char* GetPrefix(); + static const char* GetLayerCreationOptions(); + static const char* GetOpenOptions(); + static const char* GetSupportedDataTypes(); + +public: + OGRHanaDataSource(); + ~OGRHanaDataSource() override; + + int Open(const char* newName, char** options, int update); + + uint GetMajorVersion() const { return majorVersion_; } + OGRErr DeleteLayer(int index) override; + int GetLayerCount() override { return static_cast(layers_.size()); } + OGRLayer* GetLayer(int index) override; + OGRLayer* GetLayerByName(const char*) override; + OGRLayer* ICreateLayer( + const char* layerName, + OGRSpatialReference* srs = nullptr, + OGRwkbGeometryType geomType = wkbUnknown, + char** options = nullptr) override; + int TestCapability(const char* capabilities) override; + + OGRLayer* ExecuteSQL( + const char* sqlCommand, + OGRGeometry* spatialFilter, + const char* dialect) override; + + OGRErr StartTransaction(int bForce = FALSE) override; + OGRErr CommitTransaction() override; + OGRErr RollbackTransaction() override; +}; + +#endif /* ndef OGR_HANA_H_INCLUDED */ diff --git a/ogr/ogrsf_frmts/hana/ogrhanadatasource.cpp b/ogr/ogrsf_frmts/hana/ogrhanadatasource.cpp new file mode 100644 index 000000000000..c6b4c949efc3 --- /dev/null +++ b/ogr/ogrsf_frmts/hana/ogrhanadatasource.cpp @@ -0,0 +1,1784 @@ +/****************************************************************************** + * + * Project: SAP HANA Spatial Driver + * Purpose: OGRHanaDataSource class implementation + * Author: Maxim Rylov + * + ****************************************************************************** + * Copyright (c) 2020, SAP SE + * + * 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. + ****************************************************************************/ + +#include "ogr_hana.h" +#include "ogrhanautils.h" + +#include "odbc/Connection.h" +#include "odbc/Environment.h" +#include "odbc/Exception.h" +#include "odbc/DatabaseMetaData.h" +#include "odbc/PreparedStatement.h" +#include "odbc/ResultSet.h" +#include "odbc/ResultSetMetaData.h" +#include "odbc/Statement.h" + +#include +#include +#include +#include +#include + +CPL_CVSID("$Id$") + +using namespace OGRHANA; + +namespace +{ + +class LayerCreationOptionsConstants +{ +public: + LayerCreationOptionsConstants() = delete; + +public: + static constexpr const char* OVERWRITE = "OVERWRITE"; + static constexpr const char* LAUNDER = "LAUNDER"; + static constexpr const char* PRECISION = "PRECISION"; + static constexpr const char* DEFAULT_STRING_SIZE = "DEFAULT_STRING_SIZE"; + static constexpr const char* GEOMETRY_NAME = "GEOMETRY_NAME"; + static constexpr const char* GEOMETRY_NULLABLE = "GEOMETRY_NULLABLE"; + static constexpr const char* GEOMETRY_INDEX = "GEOMETRY_INDEX"; + static constexpr const char* SRID = "SRID"; + static constexpr const char* FID = "FID"; + static constexpr const char* FID64 = "FID64"; + static constexpr const char* COLUMN_TYPES = "COLUMN_TYPES"; + static constexpr const char* BATCH_SIZE = "BATCH_SIZE"; + + // clang-format off + static const char* GetList() + { + return + "" + " "; + } + // clang-format on +}; + +class OpenOptionsConstants +{ +public: + OpenOptionsConstants() = delete; + +public: + static constexpr const char* DSN = "DSN"; + static constexpr const char* DRIVER = "DRIVER"; + static constexpr const char* HOST = "HOST"; + static constexpr const char* PORT = "PORT"; + static constexpr const char* DATABASE = "DATABASE"; + static constexpr const char* USER = "USER"; + static constexpr const char* PASSWORD = "PASSWORD"; + static constexpr const char* SCHEMA = "SCHEMA"; + static constexpr const char* TABLES = "TABLES"; + + static constexpr const char* ENCRYPT = "ENCRYPT"; + static constexpr const char* SSL_CRYPTO_PROVIDER = "SSL_CRYPTO_PROVIDER"; + static constexpr const char* SSL_KEY_STORE = "SSL_KEY_STORE"; + static constexpr const char* SSL_TRUST_STORE = "SSL_TRUST_STORE"; + static constexpr const char* SSL_VALIDATE_CERTIFICATE = + "SSL_VALIDATE_CERTIFICATE"; + static constexpr const char* SSL_HOST_NAME_CERTIFICATE = + "SSL_HOST_NAME_CERTIFICATE"; + + static constexpr const char* CONNECTION_TIMEOUT = "CONNECTION_TIMEOUT"; + static constexpr const char* PACKET_SIZE = "PACKET_SIZE"; + static constexpr const char* SPLIT_BATCH_COMMANDS = "SPLIT_BATCH_COMMANDS"; + + static constexpr const char* DETECT_GEOMETRY_TYPE = "DETECT_GEOMETRY_TYPE"; + + // clang-format off + static const char* GetList() + { + return + "" + " "; + } + // clang-format on +}; + +CPLString BuildConnectionString(char** openOptions) +{ + std::vector params; + auto addParameter = [&](const char* optionName, const char* paramName) { + const char* paramValue = + CSLFetchNameValueDef(openOptions, optionName, nullptr); + if (paramValue == nullptr) + return; + params.push_back(CPLString(paramName) + "=" + CPLString(paramValue)); + }; + + const char* paramDSN = + CSLFetchNameValueDef(openOptions, OpenOptionsConstants::DSN, nullptr); + const char* paramDriver = + CSLFetchNameValueDef(openOptions, OpenOptionsConstants::DRIVER, ""); + const char* paramHost = + CSLFetchNameValueDef(openOptions, OpenOptionsConstants::HOST, ""); + const char* paramPort = + CSLFetchNameValueDef(openOptions, OpenOptionsConstants::PORT, ""); + const char* paramDatabase = + CSLFetchNameValueDef(openOptions, OpenOptionsConstants::DATABASE, ""); + const char* paramUser = + CSLFetchNameValueDef(openOptions, OpenOptionsConstants::USER, ""); + const char* paramPassword = + CSLFetchNameValueDef(openOptions, OpenOptionsConstants::PASSWORD, ""); + const char* paramSchema = + CSLFetchNameValueDef(openOptions, OpenOptionsConstants::SCHEMA, ""); + + if (CPLFetchBool(openOptions, OpenOptionsConstants::ENCRYPT, "NO")) + { + params.push_back("encrypt=true"); + addParameter( + OpenOptionsConstants::SSL_CRYPTO_PROVIDER, "sslCryptoProvider"); + addParameter(OpenOptionsConstants::SSL_KEY_STORE, "sslKeyStore"); + addParameter(OpenOptionsConstants::SSL_TRUST_STORE, "sslTrustStore"); + addParameter( + OpenOptionsConstants::SSL_VALIDATE_CERTIFICATE, + "sslValidateCertificate"); + addParameter( + OpenOptionsConstants::SSL_HOST_NAME_CERTIFICATE, + "sslHostNameInCertificate"); + } + + addParameter(OpenOptionsConstants::PACKET_SIZE, "PACKETSIZE"); + addParameter( + OpenOptionsConstants::SPLIT_BATCH_COMMANDS, "SPLITBATCHCOMMANDS"); + + // For more details on how to escape special characters in passwords, + // see + // https://stackoverflow.com/questions/55150362/maybe-illegal-character-in-odbc-sql-server-connection-string-pwd + if (paramDSN != nullptr) + return CPLString().Printf( + "DSN=%s;UID=%s;PWD={%s};CURRENTSCHEMA=\"%s\";CHAR_AS_UTF8=1;%s", + paramDSN, paramUser, paramPassword, paramSchema, + JoinStrings(params, ";").c_str()); + else + return CPLString().Printf( + "DRIVER={%s};SERVERNODE=%s:%s;DATABASENAME=%s;UID=%s;" + "PWD={%s};CURRENTSCHEMA=\"%s\";CHAR_AS_UTF8=1;%s", + paramDriver, paramHost, paramPort, paramDatabase, paramUser, + paramPassword, paramSchema, JoinStrings(params, ";").c_str()); +} + +int CPLFetchInt(CSLConstList papszStrList, const char *pszKey, int defaultValue) +{ + const char * const pszValue = + CSLFetchNameValue( papszStrList, pszKey ); + if( pszValue == nullptr ) + return defaultValue; + return atoi( pszValue ); +} + +int GetSrid(odbc::ResultSet& resultSet) +{ + int srid = UNDETERMINED_SRID; + while (resultSet.next()) + { + odbc::Int val = resultSet.getInt(1); + if (!val.isNull()) + { + srid = *val; + break; + } + } + resultSet.close(); + return srid; +} + +int GetColumnSrid( + odbc::Connection& conn, + const CPLString& schemaName, + const CPLString& tableName, + const CPLString& columnName) +{ + CPLString sql = + "SELECT SRS_ID FROM SYS.ST_GEOMETRY_COLUMNS WHERE SCHEMA_NAME = ?" + " AND TABLE_NAME = ? AND COLUMN_NAME = ?"; + odbc::PreparedStatementRef stmt = conn.prepareStatement(sql.c_str()); + stmt->setString(1, odbc::String(schemaName)); + stmt->setString(2, odbc::String(tableName)); + if (columnName != nullptr) + stmt->setString(3, odbc::String(columnName)); + return GetSrid(*stmt->executeQuery()); +} + +int GetColumnSrid( + odbc::Connection& conn, const CPLString& query, const CPLString& columnName) +{ + CPLString clmName = QuotedIdentifier(columnName); + + CPLString sql = CPLString().Printf( + "SELECT %s.ST_SRID() FROM (%s) WHERE %s IS NOT NULL", clmName.c_str(), + query.c_str(), clmName.c_str()); + + odbc::StatementRef stmt = conn.createStatement(); + return GetSrid(*stmt->executeQuery(sql.c_str())); +} + +int GetSridWithFilter(odbc::Connection& conn, const CPLString& whereCondition) +{ + CPLAssert(whereCondition != nullptr); + + int ret = UNDETERMINED_SRID; + + odbc::StatementRef stmt = conn.createStatement(); + CPLString sql = CPLString().Printf( + "SELECT SRS_ID FROM SYS.ST_SPATIAL_REFERENCE_SYSTEMS WHERE %s", + whereCondition.c_str()); + odbc::ResultSetRef rsSrs = stmt->executeQuery(sql.c_str()); + while (rsSrs->next()) + { + odbc::Int val = rsSrs->getInt(1); + if (!val.isNull()) + { + ret = *val; + break; + } + } + rsSrs->close(); + + return ret; +} + +CPLString GetSrsWktById(odbc::Connection& conn, int srid) +{ + CPLString ret; + const char* sql = "SELECT DEFINITION FROM " + "SYS.ST_SPATIAL_REFERENCE_SYSTEMS WHERE SRS_ID = ?"; + odbc::PreparedStatementRef stmt = conn.prepareStatement(sql); + stmt->setInt(1, odbc::Int(srid)); + odbc::ResultSetRef rs = stmt->executeQuery(); + while (rs->next()) + { + odbc::String wkt = rs->getString(1); + if (!wkt.isNull()) + { + ret = *wkt; + if (!ret.empty()) + break; + } + } + rs->close(); + + return ret; +} + +OGRwkbGeometryType GetGeometryType( + odbc::Connection& conn, + const CPLString& query, + const CPLString& columnName) +{ + CPLString clmName = QuotedIdentifier(columnName); + + CPLString sql = CPLString().Printf( + "SELECT DISTINCT UPPER(%s.ST_GeometryType()), %s.ST_Is3D(), " + "%s.ST_IsMeasured() FROM %s WHERE %s IS NOT NULL", + clmName.c_str(), clmName.c_str(), clmName.c_str(), query.c_str(), + clmName.c_str()); + + odbc::StatementRef stmt = conn.createStatement(); + odbc::ResultSetRef rsGeomInfo = stmt->executeQuery(sql.c_str()); + OGRwkbGeometryType ret = wkbUnknown; + std::size_t i = 0; + while (rsGeomInfo->next()) + { + ++i; + auto typeName = rsGeomInfo->getString(1); + auto hasZ = rsGeomInfo->getInt(2); + auto hasM = rsGeomInfo->getInt(3); + OGRwkbGeometryType geomType = + ToWkbType(typeName->c_str(), *hasZ == 1, *hasM == 1); + if (geomType == OGRwkbGeometryType::wkbUnknown) + continue; + if (ret == OGRwkbGeometryType::wkbUnknown) + ret = geomType; + else if (ret != geomType) + { + ret = OGRwkbGeometryType::wkbUnknown; + break; + } + } + rsGeomInfo->close(); + + if (i == 0) + ret = OGRwkbGeometryType::wkbUnknown; + return ret; +} + +GeometryColumnDescription GetGeometryColumnDescription( + odbc::Connection& conn, + const CPLString& schemaName, + const CPLString& tableName, + const CPLString& columnName, + bool detectGeometryType) +{ + OGRwkbGeometryType type = detectGeometryType ? + GetGeometryType(conn, GetFullTableNameQuoted(schemaName, tableName), columnName) : + OGRwkbGeometryType::wkbUnknown; + int srid = GetColumnSrid(conn, schemaName, tableName, columnName); + + return {columnName, type, srid, false}; +} + +GeometryColumnDescription GetGeometryColumnDescription( + odbc::Connection& conn, + const CPLString& query, + const CPLString& columnName, + bool detectGeometryType) +{ + // For some queries like this SELECT ST_GeomFROMWKT('POINT(0 0)') FROM DUMMY + // we need to have a proper column name. + bool needColumnName = false; + std::vector specialChars = {'(', ')', '\'', ' '}; + for (const char c : specialChars) + { + if (columnName.find(c) != CPLString::npos) + { + needColumnName = true; + break; + } + } + + CPLString preparedQuery = query; + CPLString clmName = columnName; + if (needColumnName) + { + auto it = std::search( + preparedQuery.begin(), preparedQuery.end(), columnName.begin(), + columnName.end(), [](char ch1, char ch2) { + return std::toupper(ch1) == std::toupper(ch2); + }); + + if (it != preparedQuery.end()) + { + auto pos = it - preparedQuery.begin(); + CPLString newName = columnName + " AS \"tmp_geom_field\""; + preparedQuery.replace( + static_cast(pos), columnName.length(), newName, 0, + newName.length()); + clmName = "tmp_geom_field"; + } + } + + OGRwkbGeometryType type = detectGeometryType ? + GetGeometryType(conn, "(" + preparedQuery + ")", clmName) : + OGRwkbGeometryType::wkbUnknown; + int srid = GetColumnSrid(conn, preparedQuery, clmName); + + return {columnName, type, srid, false}; +} + +CPLString FormatDefaultValue(const char* value, short dataType) +{ + /* + The values that can be set as default values are : + - literal string values enclosed in single-quote characters and properly + escaped like: 'Nice weather. Isn''t it ?' + - numeric values (unquoted) + - reserved keywords (unquoted): CURRENT_TIMESTAMP, CURRENT_DATE, + CURRENT_TIME, NULL + - datetime literal values enclosed in single-quote characters with the + following defined format: ‘YYYY/MM/DD HH:MM:SS[.sss]’ + - any other driver specific expression. e.g. for SQLite: + (strftime(‘%Y-%m-%dT%H:%M:%fZ’,’now’)) + */ + + if (EQUAL(value, "NULL")) + return value; + + switch (dataType) + { + case odbc::SQLDataTypes::Bit: + case odbc::SQLDataTypes::Boolean: + return value; + case odbc::SQLDataTypes::TinyInt: + case odbc::SQLDataTypes::SmallInt: + case odbc::SQLDataTypes::Integer: + case odbc::SQLDataTypes::BigInt: + case odbc::SQLDataTypes::Real: + case odbc::SQLDataTypes::Float: + case odbc::SQLDataTypes::Double: + case odbc::SQLDataTypes::Decimal: + case odbc::SQLDataTypes::Numeric: + return value; + case odbc::SQLDataTypes::Char: + case odbc::SQLDataTypes::VarChar: + case odbc::SQLDataTypes::LongVarChar: + case odbc::SQLDataTypes::WChar: + case odbc::SQLDataTypes::WVarChar: + case odbc::SQLDataTypes::WLongVarChar: + return Literal(value); + case odbc::SQLDataTypes::Binary: + case odbc::SQLDataTypes::VarBinary: + case odbc::SQLDataTypes::LongVarBinary: + return value; + case odbc::SQLDataTypes::Date: + case odbc::SQLDataTypes::TypeDate: + if (EQUAL(value, "CURRENT_DATE")) + return value; + return Literal(value); + case odbc::SQLDataTypes::Time: + case odbc::SQLDataTypes::TypeTime: + if (EQUAL(value, "CURRENT_TIME")) + return value; + return Literal(value); + case odbc::SQLDataTypes::Timestamp: + case odbc::SQLDataTypes::TypeTimestamp: + if (EQUAL(value, "CURRENT_TIMESTAMP")) + return value; + return Literal(value); + default: + return value; + } +} + +short GetArrayDataType(const CPLString& typeName) +{ + if (typeName == "BOOLEAN ARRAY") + return odbc::SQLDataTypes::Boolean; + else if (typeName == "TINYINT ARRAY") + return odbc::SQLDataTypes::TinyInt; + else if (typeName == "SMALLINT ARRAY") + return odbc::SQLDataTypes::SmallInt; + else if (typeName == "INTEGER ARRAY") + return odbc::SQLDataTypes::Integer; + else if (typeName == "BIGINT ARRAY") + return odbc::SQLDataTypes::BigInt; + else if (typeName == "DOUBLE ARRAY") + return odbc::SQLDataTypes::Double; + else if (typeName == "REAL ARRAY") + return odbc::SQLDataTypes::Float; + else if (typeName == "DECIMAL ARRAY" || typeName == "SMALLDECIMAL ARRAY") + return odbc::SQLDataTypes::Decimal; + else if (typeName == "CHAR ARRAY") + return odbc::SQLDataTypes::Char; + else if (typeName == "VARCHAR ARRAY") + return odbc::SQLDataTypes::VarChar; + else if (typeName == "NCHAR ARRAY") + return odbc::SQLDataTypes::WChar; + else if (typeName == "NVARCHAR ARRAY") + return odbc::SQLDataTypes::WVarChar; + else if (typeName == "DATE ARRAY") + return odbc::SQLDataTypes::Date; + else if (typeName == "TIME ARRAY") + return odbc::SQLDataTypes::Time; + else if (typeName == "TIMESTAMP ARRAY" || typeName == "SECONDDATE ARRAY") + return odbc::SQLDataTypes::Timestamp; + + return odbc::SQLDataTypes::Unknown; +} + +std::vector GetSupportedArrayTypes() +{ + return {"TINYINT", "SMALLINT", "INT", "BIGINT", "REAL", "DOUBLE", "STRING"}; +} + +bool IsKnownDataType(short dataType) +{ + return dataType == odbc::SQLDataTypes::Bit + || dataType == odbc::SQLDataTypes::Boolean + || dataType == odbc::SQLDataTypes::TinyInt + || dataType == odbc::SQLDataTypes::SmallInt + || dataType == odbc::SQLDataTypes::Integer + || dataType == odbc::SQLDataTypes::BigInt + || dataType == odbc::SQLDataTypes::Double + || dataType == odbc::SQLDataTypes::Real + || dataType == odbc::SQLDataTypes::Float + || dataType == odbc::SQLDataTypes::Decimal + || dataType == odbc::SQLDataTypes::Numeric + || dataType == odbc::SQLDataTypes::Char + || dataType == odbc::SQLDataTypes::VarChar + || dataType == odbc::SQLDataTypes::LongVarChar + || dataType == odbc::SQLDataTypes::WChar + || dataType == odbc::SQLDataTypes::WVarChar + || dataType == odbc::SQLDataTypes::WLongVarChar + || dataType == odbc::SQLDataTypes::Date + || dataType == odbc::SQLDataTypes::TypeDate + || dataType == odbc::SQLDataTypes::Time + || dataType == odbc::SQLDataTypes::TypeTime + || dataType == odbc::SQLDataTypes::Timestamp + || dataType == odbc::SQLDataTypes::TypeTimestamp + || dataType == odbc::SQLDataTypes::Binary + || dataType == odbc::SQLDataTypes::VarBinary + || dataType == odbc::SQLDataTypes::LongVarBinary; +} + +} // anonymous namespace + +/************************************************************************/ +/* GetPrefix() */ +/************************************************************************/ + +const char* OGRHanaDataSource::GetPrefix() +{ + return "HANA:"; +} + +/************************************************************************/ +/* GetLayerCreationOptions() */ +/************************************************************************/ + +const char* OGRHanaDataSource::GetLayerCreationOptions() +{ + return LayerCreationOptionsConstants::GetList(); +} + +/************************************************************************/ +/* GetOpenOptions() */ +/************************************************************************/ + +const char* OGRHanaDataSource::GetOpenOptions() +{ + return OpenOptionsConstants::GetList(); +} + +/************************************************************************/ +/* GetSupportedDataTypes() */ +/************************************************************************/ + +const char* OGRHanaDataSource::GetSupportedDataTypes() +{ + return "Integer Integer64 Real String Date DateTime Time IntegerList " + "Integer64List RealList StringList Binary"; +} + +/************************************************************************/ +/* OGRHanaDataSource() */ +/************************************************************************/ + +OGRHanaDataSource::OGRHanaDataSource() +{ +} + +/************************************************************************/ +/* ~OGRHanaDataSource() */ +/************************************************************************/ + +OGRHanaDataSource::~OGRHanaDataSource() +{ + layers_.clear(); + + for (const auto& kv : srsCache_) + { + OGRSpatialReference* srs = kv.second; + if (srs != nullptr) + srs->Release(); + } + srsCache_.clear(); +} + +/************************************************************************/ +/* Open() */ +/************************************************************************/ + +int OGRHanaDataSource::Open(const char* newName, char** options, int update) +{ + CPLAssert(layers_.size() == 0); + + if (!STARTS_WITH_CI(newName, GetPrefix())) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "%s does not conform to HANA driver naming convention," + " %s*\n", + newName, GetPrefix()); + return FALSE; + } + + updateMode_ = update; + + std::size_t prefixLength = strlen(GetPrefix()); + char** openOptions; + if (newName[prefixLength] == '\0') + openOptions = options; + else + openOptions = + CSLTokenizeStringComplex(newName + prefixLength, ";", TRUE, FALSE); + + detectGeometryType_ = + CPLFetchBool(openOptions, OpenOptionsConstants::DETECT_GEOMETRY_TYPE, "YES"); + + connEnv_ = odbc::Environment::create(); + conn_ = connEnv_->createConnection(); + conn_->setAutoCommit(false); + + const char* paramConnTimeout = CSLFetchNameValueDef( + openOptions, OpenOptionsConstants::CONNECTION_TIMEOUT, nullptr); + if (paramConnTimeout != nullptr) + conn_->setConnectionTimeout( + static_cast(atoi(paramConnTimeout))); + + try + { + CPLString connectionStr = BuildConnectionString(openOptions); + conn_->connect(connectionStr.c_str()); + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, "HANA connection failed: %s\n", + ex.what()); + } + + int ret = FALSE; + + if (conn_->connected()) + { + const char* paramSchema = CSLFetchNameValueDef( + openOptions, OpenOptionsConstants::SCHEMA, nullptr); + if (paramSchema == nullptr) + CPLError( + CE_Failure, CPLE_AppDefined, + "HANA parameter '%s' is missing:\n", "SCHEMA"); + schemaName_.assign(paramSchema); + + odbc::DatabaseMetaDataRef dbmd = conn_->getDatabaseMetaData(); + CPLString dbVersion(dbmd->getDBMSVersion()); + majorVersion_ = atoi(dbVersion.substr(0u, dbVersion.find('.')).c_str()); + + const char* paramTables = + CSLFetchNameValueDef(openOptions, OpenOptionsConstants::TABLES, ""); + InitializeLayers(paramSchema, paramTables); + ret = TRUE; + } + + if (openOptions != options) + CSLDestroy(openOptions); + + return ret; +} + +/************************************************************************/ +/* DeleteLayer() */ +/************************************************************************/ + +OGRErr OGRHanaDataSource::DeleteLayer(int index) +{ + if (index < 0 || static_cast(index) >= layers_.size()) + return OGRERR_FAILURE; + + const std::unique_ptr& layer = + layers_[static_cast(index)]; + CPLDebug("HANA", "DeleteLayer(%s)", layer->GetName()); + + if (auto tableLayer = dynamic_cast(layer.get())) + { + OGRErr err = tableLayer->DropTable(); + if (OGRERR_NONE == err) + return err; + } + + layers_.erase(layers_.begin() + index); + + return OGRERR_NONE; +} + +void OGRHanaDataSource::CreateTable( + const CPLString& tableName, + const CPLString& fidName, + const CPLString& fidType, + const CPLString& geomColumnName, + OGRwkbGeometryType geomType, + bool geomColumnNullable, + const CPLString& geomColumnIndexType, + int geomSrid) +{ + CPLString sql; + if (wkbFlatten(geomType) == OGRwkbGeometryType::wkbNone || !(!geomColumnName.empty() && geomSrid >= 0)) + { + sql = "CREATE COLUMN TABLE " + + GetFullTableNameQuoted(schemaName_, tableName) + " (" + + QuotedIdentifier(fidName) + " " + fidType + + " GENERATED BY DEFAULT AS IDENTITY, PRIMARY KEY ( " + + QuotedIdentifier(fidName) + "));"; + } + else + { + sql = "CREATE COLUMN TABLE " + + GetFullTableNameQuoted(schemaName_, tableName) + " (" + + QuotedIdentifier(fidName) + " " + fidType + + " GENERATED BY DEFAULT AS IDENTITY, " + + QuotedIdentifier(geomColumnName) + " ST_GEOMETRY (" + + std::to_string(geomSrid) + ")" + (geomColumnNullable ? "" : " NOT NULL") + " SPATIAL INDEX PREFERENCE " + geomColumnIndexType + + ", PRIMARY KEY ( " + QuotedIdentifier(fidName) + "));"; + } + + ExecuteSQL(sql.c_str()); +} + +/************************************************************************/ +/* FindSchemaAndTableNames() */ +/************************************************************************/ + +std::pair OGRHanaDataSource::FindSchemaAndTableNames( + const char* query) +{ + odbc::PreparedStatementRef stmt = PrepareStatement(query); + if (stmt.get() == nullptr) + return {"", ""}; + + odbc::ResultSetMetaDataRef rsmd = stmt->getMetaData(); + + // Note, getTableName returns correct table name also in the case + // when the original sql query uses a view + CPLString tableName = rsmd->getTableName(1); + if (tableName == "M_DATABASE_") + tableName = "M_DATABASE"; + CPLString schemaName = rsmd->getSchemaName(1); + if (schemaName.empty() && !tableName.empty()) + schemaName = FindSchemaName(tableName.c_str()); + return {schemaName, tableName}; +} + +/************************************************************************/ +/* FindLayerByName() */ +/************************************************************************/ + +int OGRHanaDataSource::FindLayerByName(const char* name) +{ + for (size_t i = 0; i < layers_.size(); ++i) + { + if (EQUAL(name, layers_[i]->GetName())) + return static_cast(i); + } + return -1; +} + +/************************************************************************/ +/* FindSchemaName() */ +/************************************************************************/ + +CPLString OGRHanaDataSource::FindSchemaName(const char* objectName) +{ + auto getSchemaName = [&](const char* sql) { + odbc::PreparedStatementRef stmt = conn_->prepareStatement(sql); + stmt->setString(1, odbc::String(objectName)); + odbc::ResultSetRef rsEntries = stmt->executeQuery(); + CPLString ret; + while (rsEntries->next()) + { + // return empty string if there is more than one schema. + if (!ret.empty()) + { + ret.clear(); + break; + } + ret = *rsEntries->getString(1); + } + rsEntries->close(); + + return ret; + }; + + CPLString ret = getSchemaName( + "SELECT SCHEMA_NAME FROM SYS.TABLES WHERE TABLE_NAME = ?"); + if (ret.empty()) + ret = getSchemaName( + "SELECT SCHEMA_NAME FROM SYS.VIEWS WHERE VIEW_NAME = ?"); + + return ret; +} + +/************************************************************************/ +/* CreateStatement() */ +/************************************************************************/ + +odbc::StatementRef OGRHanaDataSource::CreateStatement() +{ + return conn_->createStatement(); +} + +/************************************************************************/ +/* PrepareStatement() */ +/************************************************************************/ + +odbc::PreparedStatementRef OGRHanaDataSource::PrepareStatement(const char* sql) +{ + try + { + CPLDebug("HANA", "Prepare statement %s.", sql); + return conn_->prepareStatement(sql); + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, "Failed to prepare statement: %s", + ex.what()); + } + return nullptr; +} + +/************************************************************************/ +/* Commit() */ +/************************************************************************/ + +void OGRHanaDataSource::Commit() +{ + conn_->commit(); +} + +/************************************************************************/ +/* ExecuteSQL() */ +/************************************************************************/ + +void OGRHanaDataSource::ExecuteSQL(const char* sql) +{ + odbc::StatementRef stmt = conn_->createStatement(); + stmt->execute(sql); + conn_->commit(); +} + +/************************************************************************/ +/* GetSrsById() */ +/* */ +/* Return a SRS corresponding to a particular id. Note that */ +/* reference counting should be honoured on the returned */ +/* OGRSpatialReference, as handles may be cached. */ +/************************************************************************/ + +OGRSpatialReference* OGRHanaDataSource::GetSrsById(int srid) +{ + if (srid < 0) + return nullptr; + + auto it = srsCache_.find(srid); + if (it != srsCache_.end()) + return it->second; + + std::unique_ptr srs; + + CPLString wkt = GetSrsWktById(*conn_, srid); + if (!wkt.empty()) + { + srs = cpl::make_unique(); + OGRErr err = srs->importFromWkt(wkt.c_str()); + if (OGRERR_NONE != err) + srs.reset(nullptr); + } + + srsCache_.insert({srid, srs.get()}); + + return srs.release(); +} + +/************************************************************************/ +/* GetSrsId() */ +/************************************************************************/ + +int OGRHanaDataSource::GetSrsId(OGRSpatialReference* srs) +{ + if (srs == nullptr) + return UNDETERMINED_SRID; + + /* -------------------------------------------------------------------- */ + /* Try to find srs id using authority name and code (EPSG:3857). */ + /* -------------------------------------------------------------------- */ + OGRSpatialReference srsLocal(*srs); + + const char* authorityName = srsLocal.GetAuthorityName(nullptr); + if (authorityName == nullptr || strlen(authorityName) == 0) + { + srsLocal.AutoIdentifyEPSG(); + authorityName = srsLocal.GetAuthorityName(nullptr); + if (authorityName != nullptr && EQUAL(authorityName, "EPSG")) + { + const char* authorityCode = srsLocal.GetAuthorityCode(nullptr); + if (authorityCode != nullptr && strlen(authorityCode) > 0) + { + srsLocal.importFromEPSG(atoi(authorityCode)); + authorityName = srsLocal.GetAuthorityName(nullptr); + } + } + } + + int authorityCode = 0; + if (authorityName != nullptr) + { + authorityCode = atoi(srsLocal.GetAuthorityCode(nullptr)); + if (authorityCode > 0) + { + int ret = GetSridWithFilter( + *conn_, CPLString().Printf( + "SRS_ID = %d AND ORGANIZATION = '%s'", authorityCode, + authorityName)); + if (ret != UNDETERMINED_SRID) + return ret; + } + } + + /* -------------------------------------------------------------------- */ + /* Try to find srs id using wkt content. */ + /* -------------------------------------------------------------------- */ + + char* wkt = nullptr; + OGRErr err = srsLocal.exportToWkt(&wkt); + CPLString strWkt(wkt); + CPLFree(wkt); + + if (OGRERR_NONE != err) + return UNDETERMINED_SRID; + + int srid = GetSridWithFilter( + *conn_, CPLString().Printf("DEFINITION = '%s'", strWkt.c_str())); + if (srid != UNDETERMINED_SRID) + return srid; + + /* -------------------------------------------------------------------- */ + /* Try to add a new spatial reference system to the database */ + /* -------------------------------------------------------------------- */ + + char* proj4 = nullptr; + err = srsLocal.exportToProj4(&proj4); + CPLString strProj4(proj4); + CPLFree(proj4); + + if (OGRERR_NONE != err) + return srid; + + if (authorityName != nullptr && authorityCode > 0) + { + srid = authorityCode; + } + else + { + odbc::StatementRef stmt = conn_->createStatement(); + const char* sql = "SELECT MAX(SRS_ID) FROM SYS.ST_SPATIAL_REFERENCE_SYSTEMS WHERE SRS_ID >= 10000000 AND SRS_ID < 20000000"; + odbc::ResultSetRef rsSrid = stmt->executeQuery(sql); + while (rsSrid->next()) + { + odbc::Int val = rsSrid->getInt(1); + srid = val.isNull() ? 10000000 : *val + 1; + } + rsSrid->close(); + } + + try + { + CreateSpatialReferenceSystem( + srsLocal, srid, + authorityName, authorityCode, + strWkt, strProj4); + return srid; + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "Unable to create an SRS in the database: %s.\n", ex.what()); + } + + return UNDETERMINED_SRID; +} + +/************************************************************************/ +/* IsSrsRoundEarth() */ +/************************************************************************/ + +bool OGRHanaDataSource::IsSrsRoundEarth(int srid) +{ + const char* sql = + "SELECT ROUND_EARTH FROM SYS.ST_SPATIAL_REFERENCE_SYSTEMS " + "WHERE SRS_ID = ?"; + odbc::PreparedStatementRef stmt = conn_->prepareStatement(sql); + stmt->setInt(1, odbc::Int(srid)); + odbc::ResultSetRef rs = stmt->executeQuery(); + bool ret = false; + if (rs->next()) + ret = (*rs->getString(1) == "TRUE"); + rs->close(); + return ret; +} + +/************************************************************************/ +/* HasSrsPlanarEquivalent() */ +/************************************************************************/ + +bool OGRHanaDataSource::HasSrsPlanarEquivalent(int srid) +{ + const char* sql = "SELECT COUNT(*) FROM SYS.ST_SPATIAL_REFERENCE_SYSTEMS " + "WHERE SRS_ID = ?"; + odbc::PreparedStatementRef stmt = conn_->prepareStatement(sql); + stmt->setInt(1, ToPlanarSRID(srid)); + odbc::ResultSetRef rs = stmt->executeQuery(); + std::int64_t count = 0; + if (rs->next()) + count = *rs->getLong(1); + rs->close(); + return count > 0; +} + +/************************************************************************/ +/* GetQueryColumns() */ +/************************************************************************/ + +OGRErr OGRHanaDataSource::GetQueryColumns( + const CPLString& schemaName, + const CPLString& query, + std::vector& columDescriptions) +{ + columDescriptions.clear(); + + odbc::PreparedStatementRef stmtQuery; + + try + { + stmtQuery = conn_->prepareStatement(query); + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, "Unable to prepare statement: %s", + ex.what()); + return OGRERR_FAILURE; + } + + odbc::ResultSetMetaDataRef rsmd = stmtQuery->getMetaData(); + std::size_t numColumns = rsmd->getColumnCount(); + if (numColumns == 0) + return OGRERR_NONE; + + columDescriptions.reserve(numColumns); + + CPLString tableName = rsmd->getTableName(1); + odbc::DatabaseMetaDataRef dmd = conn_->getDatabaseMetaData(); + odbc::PreparedStatementRef stmtArrayTypeInfo = + conn_->prepareStatement("SELECT DATA_TYPE_NAME FROM " + "SYS.TABLE_COLUMNS_ODBC WHERE SCHEMA_NAME = ? " + "AND TABLE_NAME = ? AND COLUMN_NAME = ? AND " + "DATA_TYPE_NAME LIKE '% ARRAY'"); + + for (unsigned short clmIndex = 1; clmIndex <= numColumns; ++clmIndex) + { + CPLString typeName = rsmd->getColumnTypeName(clmIndex); + + if (typeName.empty()) + continue; + + bool isArray = false; + bool isGeometry = false; + CPLString columnName = rsmd->getColumnName(clmIndex); + CPLString defaultValue; + short dataType = rsmd->getColumnType(clmIndex); + + if (!schemaName.empty() && !tableName.empty()) + { + // Retrieve information about default value in column + odbc::ResultSetRef rsColumns = dmd->getColumns( + nullptr, schemaName.c_str(), tableName.c_str(), + columnName.c_str()); + if (rsColumns->next()) + { + odbc::String defaultValueStr = + rsColumns->getString(13 /*COLUMN_DEF*/); + if (!defaultValueStr.isNull()) + defaultValue = + FormatDefaultValue(defaultValueStr->c_str(), dataType); + } + rsColumns->close(); + + // Retrieve information about array type + stmtArrayTypeInfo->setString(1, schemaName); + stmtArrayTypeInfo->setString(2, tableName); + stmtArrayTypeInfo->setString(3, columnName); + odbc::ResultSetRef rsArrayTypes = stmtArrayTypeInfo->executeQuery(); + if (rsArrayTypes->next()) + { + typeName = *rsArrayTypes->getString(1); + dataType = GetArrayDataType(typeName); + + if (dataType == odbc::SQLDataTypes::Unknown) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "GetQueryColumns(): Unsupported type of array (%s)", + typeName.c_str()); + return OGRERR_FAILURE; + } + + isArray = true; + } + rsArrayTypes->close(); + } + + if (!isArray && !IsKnownDataType(dataType)) + { + odbc::ResultSetRef rsTypeInfo = dmd->getTypeInfo(dataType); + if (rsTypeInfo->next()) + { + odbc::String name = rsTypeInfo->getString(1); + if (name.isNull()) + continue; + if (name->compare("SHORTTEXT") == 0 + || name->compare("ALPHANUM") == 0) + { + dataType = odbc::SQLDataTypes::WVarChar; + } + else if ( + name->compare("ST_GEOMETRY") == 0 + || name->compare("ST_POINT") == 0) + { + isGeometry = true; + } + } + rsTypeInfo->close(); + } + + if (isGeometry) + { + GeometryColumnDescription geometryColumnDesc; + if (schemaName.empty() || tableName.empty()) + geometryColumnDesc = + GetGeometryColumnDescription(*conn_, query, columnName, detectGeometryType_); + else + geometryColumnDesc = GetGeometryColumnDescription( + *conn_, schemaName, tableName, columnName, detectGeometryType_); + geometryColumnDesc.isNullable = rsmd->isNullable(clmIndex); + + columDescriptions.push_back( + {true, AttributeColumnDescription(), geometryColumnDesc}); + } + else + { + AttributeColumnDescription attributeColumnDesc; + attributeColumnDesc.name = columnName; + attributeColumnDesc.type = dataType; + attributeColumnDesc.typeName = typeName; + attributeColumnDesc.isArray = isArray; + attributeColumnDesc.isNullable = rsmd->isNullable(clmIndex); + attributeColumnDesc.isAutoIncrement = + rsmd->isAutoIncrement(clmIndex); + attributeColumnDesc.length = + static_cast(rsmd->getColumnLength(clmIndex)); + attributeColumnDesc.precision = rsmd->getPrecision(clmIndex); + attributeColumnDesc.scale = rsmd->getScale(clmIndex); + attributeColumnDesc.defaultValue = defaultValue; + + columDescriptions.push_back( + {false, attributeColumnDesc, GeometryColumnDescription()}); + } + } + + return OGRERR_NONE; +} + +/************************************************************************/ +/* GetTablePrimaryKeys() */ +/************************************************************************/ + +std::vector OGRHanaDataSource::GetTablePrimaryKeys( + const char* schemaName, const char* tableName) +{ + std::vector ret; + + odbc::DatabaseMetaDataRef dmd = conn_->getDatabaseMetaData(); + odbc::ResultSetRef rsPrimaryKeys = + dmd->getPrimaryKeys(nullptr, schemaName, tableName); + while (rsPrimaryKeys->next()) + { + ret.push_back(*rsPrimaryKeys->getString(4)); + } + rsPrimaryKeys->close(); + + return ret; +} + +/************************************************************************/ +/* InitializeLayers() */ +/************************************************************************/ + +void OGRHanaDataSource::InitializeLayers( + const char* schemaName, const char* tableNames) +{ + std::vector tables = SplitStrings(tableNames, ","); + + auto addLayersFromQuery = [&](const char* query, bool updatable) { + odbc::PreparedStatementRef stmt = conn_->prepareStatement(query); + stmt->setString(1, odbc::String(schemaName)); + odbc::ResultSetRef rsTables = stmt->executeQuery(); + while (rsTables->next()) + { + odbc::String tableName = rsTables->getString(1); + if (tableName.isNull()) + continue; + auto pos = std::find(tables.begin(), tables.end(), *tableName); + if (pos != tables.end()) + tables.erase(pos); + + auto layer = cpl::make_unique(this, updatable); + OGRErr err = + layer->Initialize(schemaName_.c_str(), tableName->c_str()); + if (OGRERR_NONE == err) + layers_.push_back(std::move(layer)); + } + rsTables->close(); + }; + + // Look for layers in Tables + std::ostringstream osTables; + osTables << "SELECT TABLE_NAME FROM SYS.TABLES WHERE SCHEMA_NAME = ?"; + if (!tables.empty()) + osTables << " AND TABLE_NAME IN (" << JoinStrings(tables, ",", Literal) + << ")"; + + addLayersFromQuery(osTables.str().c_str(), updateMode_); + + // Look for layers in Views + std::ostringstream osViews; + osViews << "SELECT VIEW_NAME FROM SYS.VIEWS WHERE SCHEMA_NAME = ?"; + if (!tables.empty()) + osViews << " AND VIEW_NAME IN (" << JoinStrings(tables, ",", Literal) + << ")"; + + addLayersFromQuery(osViews.str().c_str(), false); + + // Report about tables that could not be found + for (const auto& tableName : tables) + { + const char* layerName = tableName.c_str(); + if (GetLayerByName(layerName) == nullptr) + CPLDebug( + "HANA", + "Table '%s' not found or does not " + "have any geometry column.", + layerName); + } +} + +/************************************************************************/ +/* CreateSpatialReference() */ +/************************************************************************/ + +void OGRHanaDataSource::CreateSpatialReferenceSystem( + const OGRSpatialReference& srs, + int srid, + const char* authorityName, + int authorityCode, + const CPLString& wkt, + const CPLString& proj4) +{ + CPLString refName( + (srs.IsProjected()) ? srs.GetAttrValue("PROJCS") + : srs.GetAttrValue("GEOGCS")); + if (refName.empty() || EQUAL(refName.c_str(), "UNKNOWN")) + refName = "OGR_PROJECTION_" + std::to_string(srid); + + OGRErr err = OGRERR_NONE; + CPLString ellipsoidParams; + const double semiMajor = srs.GetSemiMajor(&err); + if (OGRERR_NONE == err) + ellipsoidParams += " SEMI MAJOR AXIS " + std::to_string(semiMajor); + const double semiMinor = srs.GetSemiMinor(&err); + const double invFlattening = srs.GetInvFlattening(&err); + if (OGRERR_NONE == err) + ellipsoidParams += + " INVERSE FLATTENING " + std::to_string(invFlattening); + else + ellipsoidParams += " SEMI MINOR AXIS " + std::to_string(semiMinor); + + const char *linearUnits = nullptr; + srs.GetLinearUnits(&linearUnits); + const char *angularUnits = nullptr; + srs.GetAngularUnits(&angularUnits); + + CPLString xRange, yRange; + double dfWestLongitudeDeg, dfSouthLatitudeDeg, + dfEastLongitudeDeg, dfNorthLatitudeDeg; + if (srs.GetAreaOfUse(&dfWestLongitudeDeg, &dfSouthLatitudeDeg, + &dfEastLongitudeDeg, &dfNorthLatitudeDeg, nullptr)) + { + xRange = CPLString().Printf("%s BETWEEN %f AND %f", + srs.IsGeographic() ? "LONGITUDE" : "X", + dfWestLongitudeDeg, dfEastLongitudeDeg); + yRange = CPLString().Printf("%s BETWEEN %f AND %f", + srs.IsGeographic() ? "LATITUDE" : "Y", + dfSouthLatitudeDeg, dfNorthLatitudeDeg); + } + else + { + xRange = CPLString().Printf("%s UNBOUNDED", + srs.IsGeographic() ? "LONGITUDE" : "X"); + yRange = CPLString().Printf("%s UNBOUNDED ", + srs.IsGeographic() ? "LATITUDE" : "Y"); + } + + CPLString organization; + if (authorityName != nullptr && authorityCode > 0) + { + organization = CPLString().Printf( + "ORGANIZATION %s IDENTIFIED BY %d", + QuotedIdentifier(authorityName).c_str(), + authorityCode); + } + + CPLString sql = CPLString().Printf( + "CREATE SPATIAL REFERENCE SYSTEM %s " + "IDENTIFIED BY %d " + "TYPE %s " + "LINEAR UNIT OF MEASURE %s " + "ANGULAR UNIT OF MEASURE %s " + "%s " // ELLIPSOID + "COORDINATE %s " + "COORDINATE %s " + "%s " // ORGANIZATION + "DEFINITION %s " + "TRANSFORM DEFINITION %s", + QuotedIdentifier(refName).c_str(), + srid, + srs.IsGeographic() ? "ROUND EARTH" : "PLANAR", + QuotedIdentifier((linearUnits == nullptr || EQUAL(linearUnits, "unknown")) ? "metre" : linearUnits).tolower().c_str(), + QuotedIdentifier((angularUnits == nullptr || EQUAL(angularUnits, "unknown")) ? "degree" : angularUnits).tolower().c_str(), + (ellipsoidParams.empty() ? "" : ("ELLIPSOID" + ellipsoidParams).c_str()), + xRange.c_str(), yRange.c_str(), + organization.c_str(), + Literal(wkt).c_str(), + Literal(proj4).c_str()); + + ExecuteSQL(sql.c_str()); +} + +/************************************************************************/ +/* CreateParseArrayFunctions() */ +/************************************************************************/ + +void OGRHanaDataSource::CreateParseArrayFunctions(const char* schemaName) +{ + auto replaceAll = [](const CPLString& str, const CPLString& before, + const CPLString& after) { + CPLString res = str; + return res.replaceAll(before, after); + }; + + // clang-format off + const CPLString parseStringArrayFunc = + "CREATE OR REPLACE FUNCTION {SCHEMA}.OGR_PARSE_STRING_ARRAY(IN str NCLOB, IN delimiter NVARCHAR(10))\n" + "RETURNS TABLE(VALUE NVARCHAR(512))\n" + "LANGUAGE SQLSCRIPT\n" + "SQL SECURITY INVOKER AS\n" + "BEGIN\n" + "DECLARE arrValues NVARCHAR(512) ARRAY;\n" + "DECLARE idx INTEGER = 1;\n" + "DECLARE curPos INTEGER = 1;\n" + "DECLARE lastPos INTEGER = 1;\n" + "DECLARE delimiterLength INTEGER = LENGTH(delimiter);\n" + + "IF(NOT(:str IS NULL)) THEN\n" + "WHILE(:curPos > 0) DO\n" + "curPos = LOCATE(:str, :delimiter, :lastPos);\n" + "IF :curPos = 0 THEN\n" + "BREAK;\n" + "END IF;\n" + + "arrValues[:idx] = SUBSTRING(:str, :lastPos, :curPos - :lastPos);\n" + "lastPos = :curPos + :delimiterLength;\n" + "idx = :idx + 1;\n" + "END WHILE;\n" + + "arrValues[:idx] = SUBSTRING(:str, :lastPos, LENGTH(:str));\n" + "END IF;\n" + + "ret = UNNEST(:arrValues) AS(\"VALUE\");\n" + "RETURN SELECT * FROM :ret;\n" + "END;\n"; + // clang-format on + + CPLString sql = replaceAll( + parseStringArrayFunc, "{SCHEMA}", QuotedIdentifier(schemaName)); + ExecuteSQL(sql.c_str()); + + // clang-format off + const CPLString parseTypeArrayFunc = + "CREATE OR REPLACE FUNCTION {SCHEMA}.OGR_PARSE_{TYPE}_ARRAY(IN str NCLOB, IN delimiter NVARCHAR(10))\n" + "RETURNS TABLE(VALUE {TYPE})\n" + "LANGUAGE SQLSCRIPT\n" + "SQL SECURITY INVOKER AS\n" + "BEGIN\n" + "DECLARE arrValues {TYPE} ARRAY;\n" + "DECLARE elemValue STRING;\n" + "DECLARE idx INTEGER = 1;\n" + "DECLARE CURSOR cursor_values FOR\n" + "SELECT * FROM OGR_PARSE_STRING_ARRAY(:str, :delimiter);\n" + + "FOR row_value AS cursor_values DO\n" + "elemValue = TRIM(row_value.VALUE);\n" + "IF(UPPER(elemValue) = 'NULL') THEN\n" + "arrValues[:idx] = CAST(NULL AS {TYPE});\n" + "ELSE\n" + "arrValues[:idx] = CAST(:elemValue AS {TYPE});\n" + "END IF;\n" + "idx = :idx + 1;\n" + "END FOR;\n" + + "ret = UNNEST(:arrValues) AS(\"VALUE\");\n" + "RETURN SELECT * FROM :ret;\n" + "END;\n"; + // clang-format on + + sql = replaceAll( + parseTypeArrayFunc, "{SCHEMA}", QuotedIdentifier(schemaName)); + + for (const CPLString& type : GetSupportedArrayTypes()) + { + if (type == "STRING") + continue; + ExecuteSQL(replaceAll(sql, "{TYPE}", type).c_str()); + } +} + +/************************************************************************/ +/* ParseArrayFunctionsExist() */ +/************************************************************************/ + +bool OGRHanaDataSource::ParseArrayFunctionsExist(const char* schemaName) +{ + const char* sql = + "SELECT COUNT(*) FROM FUNCTIONS WHERE SCHEMA_NAME = ? AND " + "FUNCTION_NAME LIKE 'OGR_PARSE_%_ARRAY'"; + odbc::PreparedStatementRef stmt = conn_->prepareStatement(sql); + stmt->setString(1, odbc::String(schemaName)); + odbc::ResultSetRef rsFunctions = stmt->executeQuery(); + rsFunctions->next(); + auto numFunctions = *rsFunctions->getLong(1); + rsFunctions->close(); + return ( + static_cast(numFunctions) + == GetSupportedArrayTypes().size()); +} + +/************************************************************************/ +/* GetLayer() */ +/************************************************************************/ + +OGRLayer* OGRHanaDataSource::GetLayer(int index) +{ + if (index < 0 || static_cast(index) >= layers_.size()) + return nullptr; + return layers_[static_cast(index)].get(); +} + +/************************************************************************/ +/* GetLayerByName() */ +/************************************************************************/ + +OGRLayer* OGRHanaDataSource::GetLayerByName(const char* name) +{ + return GetLayer(FindLayerByName(name)); +} + +/************************************************************************/ +/* ICreateLayer() */ +/************************************************************************/ + +OGRLayer* OGRHanaDataSource::ICreateLayer( + const char* layerNameIn, + OGRSpatialReference* srs, + OGRwkbGeometryType geomType, + char** options) +{ + // Check if we are allowed to create new objects in the database + odbc::DatabaseMetaDataRef dmd = conn_->getDatabaseMetaData(); + if (dmd->isReadOnly()) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "Unable to create Layer %s.\n" + "Database %s is read only.", + layerNameIn, dmd->getDatabaseName().c_str()); + return nullptr; + } + + bool launderNames = + CPLFetchBool(options, LayerCreationOptionsConstants::LAUNDER, true); + CPLString layerName = + launderNames ? LaunderName(layerNameIn) : CPLString(layerNameIn); + + CPLDebug("HANA", "Creating layer %s.", layerName.c_str()); + + int layerIndex = FindLayerByName(layerName.c_str()); + if (layerIndex >= 0) + { + bool overwriteLayer = CPLFetchBool(options, LayerCreationOptionsConstants::OVERWRITE, false); + if (!overwriteLayer) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "Layer %s already exists, CreateLayer failed.\n" + "Use the layer creation option OVERWRITE=YES to " + "replace it.", + layerName.c_str()); + return nullptr; + } + + DeleteLayer(layerIndex); + } + + int batchSize = CPLFetchInt(options, LayerCreationOptionsConstants::BATCH_SIZE, + DEFAULT_BATCH_SIZE); + if (batchSize <= 0) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "Unable to create layer %s. The value of %s parameter must be " + "greater than 0.", + layerName.c_str(), LayerCreationOptionsConstants::BATCH_SIZE); + return nullptr; + } + + int defaultStringSize = CPLFetchInt(options, LayerCreationOptionsConstants::DEFAULT_STRING_SIZE, + DEFAULT_STRING_SIZE); + if (defaultStringSize <= 0) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "Unable to create layer %s. The value of %s parameter must be " + "greater than 0.", + layerName.c_str(), + LayerCreationOptionsConstants::DEFAULT_STRING_SIZE); + return nullptr; + } + + CPLString geomColumnName(CSLFetchNameValueDef(options, LayerCreationOptionsConstants::GEOMETRY_NAME, "OGR_GEOMETRY")); + const bool geomColumnNullable = CPLFetchBool(options, LayerCreationOptionsConstants::GEOMETRY_NULLABLE, true); + CPLString geomColumnIndexType(CSLFetchNameValueDef(options, LayerCreationOptionsConstants::GEOMETRY_INDEX, "DEFAULT")); + + const char* paramFidName = CSLFetchNameValueDef(options, LayerCreationOptionsConstants::FID, "OGR_FID"); + CPLString fidName(launderNames ? LaunderName(paramFidName).c_str() : paramFidName); + CPLString fidType = CPLFetchBool(options, LayerCreationOptionsConstants::FID64, false) ? "BIGINT" : "INTEGER"; + + CPLDebug("HANA", "Geometry Column Name %s.", geomColumnName.c_str()); + CPLDebug("HANA", "FID Column Name %s, Type %s.", fidName.c_str(), + fidType.c_str()); + + int srid = CPLFetchInt(options, LayerCreationOptionsConstants::SRID, UNDETERMINED_SRID); + if (srid < 0 && srs != nullptr) + srid = GetSrsId(srs); + + try + { + CreateTable( + layerName, fidName, fidType, + geomColumnName, geomType, geomColumnNullable, geomColumnIndexType, srid); + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "Unable to create layer %s. CreateLayer failed:%s\n", + layerName.c_str(), ex.what()); + return nullptr; + } + + // Create new layer object + auto layer = cpl::make_unique(this, true); + OGRErr err = layer->Initialize(schemaName_.c_str(), layerName.c_str()); + if (err == OGRERR_FAILURE) + return nullptr; + + if (geomType != wkbNone && layer->GetLayerDefn()->GetGeomFieldCount() > 0) + layer->GetLayerDefn()->GetGeomFieldDefn(0)->SetNullable(FALSE); + if (batchSize > 0) + layer->SetBatchSize(static_cast(batchSize)); + if (defaultStringSize > 0) + layer->SetDefaultStringSize( + static_cast(defaultStringSize)); + layer->SetLaunderFlag(launderNames); + layer->SetPrecisionFlag( + CPLFetchBool(options, LayerCreationOptionsConstants::PRECISION, true)); + layer->SetCustomColumnTypes(CSLFetchNameValue( + options, LayerCreationOptionsConstants::COLUMN_TYPES)); + + layers_.push_back(std::move(layer)); + + return layers_[layers_.size() - 1].get(); +} + +/************************************************************************/ +/* TestCapability() */ +/************************************************************************/ + +int OGRHanaDataSource::TestCapability(const char* capabilities) +{ + if (EQUAL(capabilities, ODsCCreateLayer)) + return updateMode_; + else if (EQUAL(capabilities, ODsCDeleteLayer)) + return updateMode_; + else if (EQUAL(capabilities, ODsCCreateGeomFieldAfterCreateLayer)) + return updateMode_; + else if (EQUAL(capabilities, ODsCMeasuredGeometries)) + return TRUE; + else if (EQUAL(capabilities, ODsCRandomLayerWrite)) + return updateMode_; + else if (EQUAL(capabilities, ODsCTransactions)) + return TRUE; + else + return FALSE; +} + +/************************************************************************/ +/* ExecuteSQL() */ +/************************************************************************/ + +OGRLayer* OGRHanaDataSource::ExecuteSQL( + const char* sqlCommand, OGRGeometry* spatialFilter, const char* dialect) +{ + sqlCommand = SkipLeadingSpaces(sqlCommand); + + if (IsGenericSQLDialect(dialect)) + return GDALDataset::ExecuteSQL(sqlCommand, spatialFilter, dialect); + + if (STARTS_WITH_CI(sqlCommand, "DELLAYER:")) + { + const char* layerName = SkipLeadingSpaces(sqlCommand + 9); + int layerIndex = FindLayerByName(layerName); + if (layerIndex >= 0) + DeleteLayer(layerIndex); + return nullptr; + } + if (STARTS_WITH_CI(sqlCommand, "SELECT")) + { + auto layer = cpl::make_unique(this); + OGRErr err = layer->Initialize(sqlCommand, spatialFilter); + if (OGRERR_NONE == err) + return layer.release(); + return nullptr; + } + + try + { + ExecuteSQL(sqlCommand); + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "Unable to execute SQL statement '%s': %s", sqlCommand, ex.what()); + } + + return nullptr; +} + +/************************************************************************/ +/* StartTransaction() */ +/************************************************************************/ + +OGRErr OGRHanaDataSource::StartTransaction(CPL_UNUSED int bForce) +{ + if (isTransactionStarted_) + { + CPLError( + CE_Failure, CPLE_AppDefined, "Transaction already established"); + return OGRERR_FAILURE; + } + + isTransactionStarted_ = true; + return OGRERR_NONE; +} + +/************************************************************************/ +/* CommitTransaction() */ +/************************************************************************/ + +OGRErr OGRHanaDataSource::CommitTransaction() +{ + if (!isTransactionStarted_) + { + CPLError(CE_Failure, CPLE_AppDefined, "Transaction not established"); + return OGRERR_FAILURE; + } + + isTransactionStarted_ = false; + + try + { + conn_->commit(); + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, "Failed to commit transaction: %s", + ex.what()); + return OGRERR_FAILURE; + } + return OGRERR_NONE; +} + +/************************************************************************/ +/* RollbackTransaction() */ +/************************************************************************/ + +OGRErr OGRHanaDataSource::RollbackTransaction() +{ + if (!isTransactionStarted_) + { + CPLError(CE_Failure, CPLE_AppDefined, "Transaction not established"); + return OGRERR_FAILURE; + } + + isTransactionStarted_ = false; + + try + { + conn_->rollback(); + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, "Failed to roll back transaction: %s", + ex.what()); + return OGRERR_FAILURE; + } + return OGRERR_NONE; +} diff --git a/ogr/ogrsf_frmts/hana/ogrhanadriver.cpp b/ogr/ogrsf_frmts/hana/ogrhanadriver.cpp new file mode 100644 index 000000000000..c3389d1a05f0 --- /dev/null +++ b/ogr/ogrsf_frmts/hana/ogrhanadriver.cpp @@ -0,0 +1,125 @@ +/****************************************************************************** + * + * Project: SAP HANA Spatial Driver + * Purpose: OGRHanaDriver functions implementation + * Author: Maxim Rylov + * + ****************************************************************************** + * Copyright (c) 2020, SAP SE + * + * 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. + ****************************************************************************/ + +#include "cpl_conv.h" +#include "ogr_hana.h" + +#include + +CPL_CVSID("$Id$") + +/************************************************************************/ +/* OGRHanaDriverIdentify() */ +/************************************************************************/ + +static int OGRHanaDriverIdentify(GDALOpenInfo* openInfo) +{ + return STARTS_WITH_CI( + openInfo->pszFilename, OGRHanaDataSource::GetPrefix()); +} + +/************************************************************************/ +/* OGRHanaDriverOpen() */ +/************************************************************************/ + +static GDALDataset* OGRHanaDriverOpen(GDALOpenInfo* openInfo) +{ + if (!OGRHanaDriverIdentify(openInfo)) + return nullptr; + + auto ds = cpl::make_unique(); + if (!ds->Open( + openInfo->pszFilename, openInfo->papszOpenOptions, + openInfo->eAccess == GA_Update)) + return nullptr; + return ds.release(); +} + +/************************************************************************/ +/* OGRHanaDriverCreate() */ +/************************************************************************/ + +static GDALDataset* OGRHanaDriverCreate( + const char* name, + CPL_UNUSED int nBands, + CPL_UNUSED int nXSize, + CPL_UNUSED int nYSize, + CPL_UNUSED GDALDataType eDT, + CPL_UNUSED char** options) +{ + auto ds = cpl::make_unique(); + if (!ds->Open(name, options, TRUE)) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "HANA driver doesn't currently support database creation.\n" + "Please create a database with SAP HANA tools before using."); + + return nullptr; + } + return ds.release(); +} + +/************************************************************************/ +/* RegisterOGRHANA() */ +/************************************************************************/ + +void RegisterOGRHANA() +{ + if (!GDAL_CHECK_VERSION("SAP HANA driver")) + return; + + if (GDALGetDriverByName("HANA") != nullptr) + return; + + auto driver = cpl::make_unique(); + driver->SetDescription("HANA"); + driver->SetMetadataItem(GDAL_DMD_LONGNAME, "SAP HANA"); + driver->SetMetadataItem(GDAL_DCAP_VECTOR, "YES"); + driver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/vector/hana.html"); + driver->SetMetadataItem( + GDAL_DMD_CONNECTION_PREFIX, OGRHanaDataSource::GetPrefix()); + driver->SetMetadataItem( + GDAL_DMD_OPENOPTIONLIST, OGRHanaDataSource::GetOpenOptions()); + driver->SetMetadataItem( + GDAL_DMD_CREATIONOPTIONLIST, ""); + driver->SetMetadataItem( + GDAL_DS_LAYER_CREATIONOPTIONLIST, + OGRHanaDataSource::GetLayerCreationOptions()); + driver->SetMetadataItem( + GDAL_DMD_CREATIONFIELDDATATYPES, + OGRHanaDataSource::GetSupportedDataTypes()); + driver->SetMetadataItem(GDAL_DCAP_NOTNULL_FIELDS, "YES"); + driver->SetMetadataItem(GDAL_DCAP_DEFAULT_FIELDS, "YES"); + + driver->pfnOpen = OGRHanaDriverOpen; + driver->pfnIdentify = OGRHanaDriverIdentify; + driver->pfnCreate = OGRHanaDriverCreate; + + GetGDALDriverManager()->RegisterDriver(driver.release()); +} diff --git a/ogr/ogrsf_frmts/hana/ogrhanafeaturereader.cpp b/ogr/ogrsf_frmts/hana/ogrhanafeaturereader.cpp new file mode 100644 index 000000000000..74567caae36b --- /dev/null +++ b/ogr/ogrsf_frmts/hana/ogrhanafeaturereader.cpp @@ -0,0 +1,535 @@ +/****************************************************************************** + * + * Project: SAP HANA Spatial Driver + * Purpose: OGRHanaFeatureReader class implementation + * Author: Maxim Rylov + * + ****************************************************************************** + * Copyright (c) 2020, SAP SE + * + * 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. + ****************************************************************************/ + +#include "ogrhanafeaturereader.h" +#include "ogrhanautils.h" + +#include "cpl_time.h" + +#include +#include +#include +#include + +#include "odbc/Types.h" + +CPL_CVSID("$Id$") + +namespace OGRHANA { +namespace { + +template +odbc::String CreateStringFromValues( + const T* elements, int numElements, std::string (*toString)(T e)) +{ + if (numElements == 0) + return odbc::String(); + + std::ostringstream os; + for (int i = 0; i < numElements; ++i) + { + if (i > 0) + os << ARRAY_VALUES_DELIMITER; + os << toString(elements[i]); + } + return odbc::String(os.str()); +} + +template +T castInt(int value) +{ + if (!(std::numeric_limits::min() >= value + || value <= std::numeric_limits::max())) + throw std::overflow_error("Integer value lies outside of the range"); + return static_cast(value); +} + +template +T strToInt(const char* value) +{ + return castInt(std::stoi(value)); +} + +} // anonymous namespace + +OGRHanaFeatureReader::OGRHanaFeatureReader(OGRFeature& feature) + : feature_(feature) +{ +} + +odbc::Boolean OGRHanaFeatureReader::GetFieldAsBoolean(int fieldIndex) const +{ + if (IsFieldSet(fieldIndex)) + return feature_.GetFieldAsInteger(fieldIndex) == 1; + + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return odbc::Boolean(); + + return (EQUAL(defaultValue, "1") || EQUAL(defaultValue, "'t'")); +} + +odbc::Byte OGRHanaFeatureReader::GetFieldAsByte(int fieldIndex) const +{ + if (IsFieldSet(fieldIndex)) + return odbc::Byte( + castInt(feature_.GetFieldAsInteger(fieldIndex))); + + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return odbc::Byte(); + return odbc::Byte(strToInt(defaultValue)); +} + +odbc::Short OGRHanaFeatureReader::GetFieldAsShort(int fieldIndex) const +{ + if (IsFieldSet(fieldIndex)) + return odbc::Short( + castInt(feature_.GetFieldAsInteger(fieldIndex))); + + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return odbc::Short(); + return odbc::Short(strToInt(defaultValue)); +} + +odbc::Int OGRHanaFeatureReader::GetFieldAsInt(int fieldIndex) const +{ + if (IsFieldSet(fieldIndex)) + return odbc::Int(feature_.GetFieldAsInteger(fieldIndex)); + + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return odbc::Int(); + return odbc::Int(strToInt(defaultValue)); +} + +odbc::Long OGRHanaFeatureReader::GetFieldAsLong(int fieldIndex) const +{ + if (IsFieldSet(fieldIndex)) + return odbc::Long(feature_.GetFieldAsInteger64(fieldIndex)); + + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return odbc::Long(); + return odbc::Long(std::stol(defaultValue)); +} + +odbc::Float OGRHanaFeatureReader::GetFieldAsFloat(int fieldIndex) const +{ + if (IsFieldSet(fieldIndex)) + { + double dValue = feature_.GetFieldAsDouble(fieldIndex); + return odbc::Float(static_cast(dValue)); + } + + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return odbc::Float(); + return odbc::Float(std::stof(defaultValue)); +} + +odbc::Double OGRHanaFeatureReader::GetFieldAsDouble(int fieldIndex) const +{ + if (IsFieldSet(fieldIndex)) + return odbc::Double(feature_.GetFieldAsDouble(fieldIndex)); + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return odbc::Double(); + return odbc::Double(std::stod(defaultValue)); +} + +odbc::String OGRHanaFeatureReader::GetFieldAsString( + int fieldIndex, int maxCharLength) const +{ + auto getString = [&](const char* str) { + if (str == nullptr) + return odbc::String(); + + if (maxCharLength > 0 + && std::strlen(str) > static_cast(maxCharLength)) + return odbc::String( + std::string(str, static_cast(maxCharLength))); + return odbc::String(str); + }; + + if (IsFieldSet(fieldIndex)) + return getString(feature_.GetFieldAsString(fieldIndex)); + + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return odbc::String(); + + if (defaultValue[0] == '\'' + && defaultValue[strlen(defaultValue) - 1] == '\'') + { + CPLString str(defaultValue + 1); + str.resize(str.size() - 1); + char* tmp = CPLUnescapeString(str, nullptr, CPLES_SQL); + odbc::String ret = getString(tmp); + CPLFree(tmp); + return ret; + } + + return odbc::String(defaultValue); +} + +odbc::String OGRHanaFeatureReader::GetFieldAsNString( + int fieldIndex, int maxCharLength) const +{ + auto getString = [&](const char* str) { + if (str == nullptr) + return odbc::String(); + + if (maxCharLength <= 0) + return odbc::String(std::string(str)); + + int nSrcLen = static_cast(std::strlen(str)); + int nSrcLenUTF = CPLStrlenUTF8(str); + + if (maxCharLength > 0 && nSrcLenUTF > maxCharLength) + { + CPLDebug("HANA", + "Truncated field value '%s' at index %d to %d characters.", + str, fieldIndex, maxCharLength); + + int iUTF8Char = 0; + for (int iChar = 0; iChar < nSrcLen; ++iChar) + { + if ((str[iChar] & 0xc0) != 0x80) + { + if (iUTF8Char == maxCharLength) + { + nSrcLen = iChar; + break; + } + ++iUTF8Char; + } + } + } + + return odbc::String(std::string(str, static_cast(nSrcLen))); + }; + + if (IsFieldSet(fieldIndex)) + return getString(feature_.GetFieldAsString(fieldIndex)); + + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return odbc::String(); + + if (defaultValue[0] == '\'' + && defaultValue[strlen(defaultValue) - 1] == '\'') + { + CPLString str(defaultValue + 1); + str.resize(str.size() - 1); + char* tmp = CPLUnescapeString(str, nullptr, CPLES_SQL); + odbc::String ret = getString(tmp); + CPLFree(tmp); + return ret; + } + + return odbc::String(defaultValue); +} + +odbc::Date OGRHanaFeatureReader::GetFieldAsDate(int fieldIndex) const +{ + if (IsFieldSet(fieldIndex)) + { + int year = 0; + int month = 0; + int day = 0; + int hour = 0; + int minute = 0; + int timeZoneFlag = 0; + float second = 0.0f; + feature_.GetFieldAsDateTime( + fieldIndex, &year, &month, &day, &hour, &minute, &second, + &timeZoneFlag); + + return odbc::makeNullable(year, month, day); + } + + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return odbc::Date(); + + if (EQUAL(defaultValue, "CURRENT_DATE")) + { + std::time_t t = std::time(nullptr); + tm* now = std::localtime(&t); + if (now == nullptr) + return odbc::Date(); + return odbc::makeNullable( + now->tm_year + 1900, now->tm_mon + 1, now->tm_mday); + } + + int year, month, day; + sscanf(defaultValue, "'%04d/%02d/%02d'", &year, &month, &day); + + return odbc::makeNullable(year, month, day); +} + +odbc::Time OGRHanaFeatureReader::GetFieldAsTime(int fieldIndex) const +{ + if (IsFieldSet(fieldIndex)) + { + int year = 0; + int month = 0; + int day = 0; + int hour = 0; + int minute = 0; + int timeZoneFlag = 0; + float second = 0.0f; + feature_.GetFieldAsDateTime( + fieldIndex, &year, &month, &day, &hour, &minute, &second, + &timeZoneFlag); + return odbc::makeNullable( + hour, minute, static_cast(round(second))); + } + + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return odbc::Time(); + + if (EQUAL(defaultValue, "CURRENT_TIME")) + { + std::time_t t = std::time(nullptr); + tm* now = std::localtime(&t); + if (now == nullptr) + return odbc::Time(); + return odbc::makeNullable( + now->tm_hour, now->tm_min, now->tm_sec); + } + + int hour = 0; + int minute = 0; + int second = 0; + sscanf(defaultValue, "'%02d:%02d:%02d'", &hour, &minute, &second); + return odbc::makeNullable(hour, minute, second); +} + +odbc::Timestamp OGRHanaFeatureReader::GetFieldAsTimestamp(int fieldIndex) const +{ + if (IsFieldSet(fieldIndex)) + { + int year = 0; + int month = 0; + int day = 0; + int hour = 0; + int minute = 0; + float secondWithMillisecond = 0.0f; + int timeZoneFlag = 0; + feature_.GetFieldAsDateTime( + fieldIndex, &year, &month, &day, &hour, &minute, + &secondWithMillisecond, &timeZoneFlag); + double seconds = 0.0; + double milliseconds = std::modf(static_cast(secondWithMillisecond), &seconds); + int second = static_cast(std::floor(seconds)); + int millisecond = static_cast(std::floor(milliseconds * 1000)); + + if (!(timeZoneFlag == 0 || timeZoneFlag == 100 || timeZoneFlag == 1)) + { + struct tm time; + time.tm_year = year - 1900; + time.tm_mon = month - 1; + time.tm_mday = day; + time.tm_hour = hour; + time.tm_min = minute; + time.tm_sec = second; + GIntBig dt = CPLYMDHMSToUnixTime(&time); + const int tzoffset = std::abs(timeZoneFlag - 100) * 15; + dt -= tzoffset * 60; + CPLUnixTimeToYMDHMS(dt, &time); + year = time.tm_year + 1900; + month = time.tm_mon + 1; + day = time.tm_mday; + hour = time.tm_hour; + minute = time.tm_min; + second = time.tm_sec; + } + + return odbc::makeNullable( + year, month, day, hour, minute, second, millisecond); + } + + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return odbc::Timestamp(); + + if (EQUAL(defaultValue, "CURRENT_TIMESTAMP")) + { + time_t t = std::time(nullptr); + tm* now = std::localtime(&t); + if (now == nullptr) + return odbc::Timestamp(); + return odbc::makeNullable( + now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour, + now->tm_min, now->tm_sec, 0); + } + + int year = 0; + int month = 0; + int day = 0; + int hour = 0; + int minute = 0; + int second = 0; + int millisecond = 0; + + if (strchr(defaultValue, '.') == nullptr) + sscanf( + defaultValue, "'%04d/%02d/%02d %02d:%02d:%02d'", &year, &month, + &day, &hour, &minute, &second); + else + sscanf( + defaultValue, "'%04d/%02d/%02d %02d:%02d:%02d.%03d'", &year, &month, + &day, &hour, &minute, &second, &millisecond); + + return odbc::makeNullable( + year, month, day, hour, minute, second, millisecond); +} + +Binary OGRHanaFeatureReader::GetFieldAsBinary(int fieldIndex) const +{ + if (IsFieldSet(fieldIndex)) + { + int size = 0; + GByte* data = feature_.GetFieldAsBinary(fieldIndex, &size); + return {data, static_cast(size)}; + } + + const char* defaultValue = GetDefaultValue(fieldIndex); + if (defaultValue == nullptr) + return {nullptr, 0U}; + + return {const_cast(reinterpret_cast(defaultValue)), + std::strlen(defaultValue)}; +} + +odbc::String OGRHanaFeatureReader::GetFieldAsIntArray(int fieldIndex) const +{ + if (!IsFieldSet(fieldIndex)) + return odbc::String(); + + int numElements; + const int* values = + feature_.GetFieldAsIntegerList(fieldIndex, &numElements); + return CreateStringFromValues(values, numElements, &std::to_string); +} + +odbc::String OGRHanaFeatureReader::GetFieldAsBigIntArray(int fieldIndex) const +{ + if (!IsFieldSet(fieldIndex)) + return odbc::String(); + + int numElements; + const GIntBig* values = + feature_.GetFieldAsInteger64List(fieldIndex, &numElements); + return CreateStringFromValues( + values, numElements, &std::to_string); +} + +odbc::String OGRHanaFeatureReader::GetFieldAsRealArray(int fieldIndex) const +{ + if (!IsFieldSet(fieldIndex)) + return odbc::String(); + + int numElements; + const double* values = + feature_.GetFieldAsDoubleList(fieldIndex, &numElements); + return CreateStringFromValues( + values, numElements, [](double value) { + return std::isnan(value) + ? "NULL" + : std::to_string(static_cast(value)); + }); +} + +odbc::String OGRHanaFeatureReader::GetFieldAsDoubleArray(int fieldIndex) const +{ + if (!IsFieldSet(fieldIndex)) + return odbc::String(); + + int numElements; + const double* values = + feature_.GetFieldAsDoubleList(fieldIndex, &numElements); + return CreateStringFromValues( + values, numElements, [](double value) { + return std::isnan(value) ? "NULL" : std::to_string(value); + }); +} + +odbc::String OGRHanaFeatureReader::GetFieldAsStringArray(int fieldIndex) const +{ + if (!IsFieldSet(fieldIndex)) + return odbc::String(); + + char** items = feature_.GetFieldAsStringList(fieldIndex); + if (items == nullptr) + return odbc::String(); + + std::ostringstream os; + bool firstItem = true; + while (items && *items) + { + if (!firstItem) + os << ARRAY_VALUES_DELIMITER; + + char* itemValue = *items; + if (*itemValue != '\0') + { + os << '\''; + while (*itemValue) + { + if (*itemValue == '\'') + os << "'"; + os << *itemValue; + ++itemValue; + } + os << '\''; + } + + ++items; + firstItem = false; + } + + return odbc::String(os.str()); +} + +const char* OGRHanaFeatureReader::GetDefaultValue(int fieldIndex) const +{ + const OGRFieldDefn* fieldDef = feature_.GetFieldDefnRef(fieldIndex); + return fieldDef->GetDefault(); +} + +bool OGRHanaFeatureReader::IsFieldSet(int fieldIndex) const +{ + return feature_.IsFieldSet(fieldIndex) && !feature_.IsFieldNull(fieldIndex); +} + +} /* end of OGRHANA namespace */ diff --git a/ogr/ogrsf_frmts/hana/ogrhanafeaturereader.h b/ogr/ogrsf_frmts/hana/ogrhanafeaturereader.h new file mode 100644 index 000000000000..3a08a68eb3ed --- /dev/null +++ b/ogr/ogrsf_frmts/hana/ogrhanafeaturereader.h @@ -0,0 +1,73 @@ +/****************************************************************************** + * + * Project: SAP HANA Spatial Driver + * Purpose: OGRHanaFeatureReader class declaration + * Author: Maxim Rylov + * + ****************************************************************************** + * Copyright (c) 2020, SAP SE + * + * 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. + ****************************************************************************/ + +#ifndef OGRHANAFEATUREREADER_H_INCLUDED +#define OGRHANAFEATUREREADER_H_INCLUDED + +#include "ogr_hana.h" +#include "gdal_priv.h" +#include "odbc/Types.h" + +namespace OGRHANA { + +class OGRHanaFeatureReader +{ +public: + explicit OGRHanaFeatureReader(OGRFeature& feature); + + odbc::Boolean GetFieldAsBoolean(int fieldIndex) const; + odbc::Byte GetFieldAsByte(int fieldIndex) const; + odbc::Short GetFieldAsShort(int fieldIndex) const; + odbc::Int GetFieldAsInt(int fieldIndex) const; + odbc::Long GetFieldAsLong(int fieldIndex) const; + odbc::Float GetFieldAsFloat(int fieldIndex) const; + odbc::Double GetFieldAsDouble(int fieldIndex) const; + odbc::String GetFieldAsString(int fieldIndex, int maxCharLength) const; + odbc::String GetFieldAsNString(int fieldIndex, int maxCharLength) const; + odbc::Date GetFieldAsDate(int fieldIndex) const; + odbc::Time GetFieldAsTime(int fieldIndex) const; + odbc::Timestamp GetFieldAsTimestamp(int fieldIndex) const; + Binary GetFieldAsBinary(int fieldIndex) const; + + odbc::String GetFieldAsIntArray(int fieldIndex) const; + odbc::String GetFieldAsBigIntArray(int fieldIndex) const; + odbc::String GetFieldAsRealArray(int fieldIndex) const; + odbc::String GetFieldAsDoubleArray(int fieldIndex) const; + odbc::String GetFieldAsStringArray(int fieldIndex) const; + +private: + const char* GetDefaultValue(int fieldIndex) const; + bool IsFieldSet(int fieldIndex) const; + +private: + const OGRFeature& feature_; +}; + +} /* end of OGRHANA namespace */ + +#endif // OGRHANAFEATUREREADER_H_INCLUDED diff --git a/ogr/ogrsf_frmts/hana/ogrhanafeaturewriter.cpp b/ogr/ogrsf_frmts/hana/ogrhanafeaturewriter.cpp new file mode 100644 index 000000000000..d7ae5c4c0551 --- /dev/null +++ b/ogr/ogrsf_frmts/hana/ogrhanafeaturewriter.cpp @@ -0,0 +1,222 @@ +/****************************************************************************** + * + * Project: SAP HANA Spatial Driver + * Purpose: OGRHanaFeatureWriter class implementation + * Author: Maxim Rylov + * + ****************************************************************************** + * Copyright (c) 2020, SAP SE + * + * 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. + ****************************************************************************/ + +#include "ogrhanafeaturewriter.h" +#include + +CPL_CVSID("$Id$") + +namespace OGRHANA { +namespace { + +enum DataLengthIndicator +{ + MAX_ONE_BYTE = 245, + TWO_BYTE = 246, + FOUR_BYTE = 247, + DEFAULT_VALUE = 254, + NULL_VALUE = 255 +}; + +} // anonymous namespace + +OGRHanaFeatureWriter::OGRHanaFeatureWriter(OGRFeature& feature) + : feature_(feature) +{ +} + +void OGRHanaFeatureWriter::SetFieldValue( + int fieldIndex, const odbc::Long& value) +{ + if (value.isNull()) + feature_.SetFieldNull(fieldIndex); + else + feature_.SetField(fieldIndex, static_cast(*value)); +} + +void OGRHanaFeatureWriter::SetFieldValue( + int fieldIndex, const odbc::Float& value) +{ + if (value.isNull()) + feature_.SetFieldNull(fieldIndex); + else + feature_.SetField(fieldIndex, static_cast(*value)); +} + +void OGRHanaFeatureWriter::SetFieldValue( + int fieldIndex, const odbc::Decimal& value) +{ + if (value.isNull()) + feature_.SetFieldNull(fieldIndex); + else + feature_.SetField(fieldIndex, value->toString().c_str()); +} + +void OGRHanaFeatureWriter::SetFieldValue( + int fieldIndex, const odbc::String& value) +{ + if (value.isNull()) + feature_.SetFieldNull(fieldIndex); + else + feature_.SetField(fieldIndex, value->c_str()); +} + +void OGRHanaFeatureWriter::SetFieldValue( + int fieldIndex, const odbc::Date& value) +{ + if (value.isNull()) + feature_.SetFieldNull(fieldIndex); + else + feature_.SetField( + fieldIndex, value->year(), value->month(), value->day(), 0, 0, 0, + 0); +} + +void OGRHanaFeatureWriter::SetFieldValue( + int fieldIndex, const odbc::Time& value) +{ + if (value.isNull()) + feature_.SetFieldNull(fieldIndex); + else + feature_.SetField( + fieldIndex, 0, 0, 0, value->hour(), value->minute(), + static_cast(value->second()), 0); +} + +void OGRHanaFeatureWriter::SetFieldValue( + int fieldIndex, const odbc::Timestamp& value) +{ + if (value.isNull()) + feature_.SetFieldNull(fieldIndex); + else + feature_.SetField( + fieldIndex, value->year(), value->month(), value->day(), + value->hour(), value->minute(), + static_cast( + value->second() + value->milliseconds() / 1000.0), + 0); +} + +void OGRHanaFeatureWriter::SetFieldValue( + int fieldIndex, const odbc::Binary& value) +{ + if (value.isNull()) + feature_.SetFieldNull(fieldIndex); + else + SetFieldValue(fieldIndex, value->data(), value->size()); +} + +void OGRHanaFeatureWriter::SetFieldValue(int fieldIndex, const char* value) +{ + if (value == nullptr) + feature_.SetFieldNull(fieldIndex); + else + feature_.SetField(fieldIndex, value); +} + +void OGRHanaFeatureWriter::SetFieldValue( + int fieldIndex, const void* value, std::size_t size) +{ + if (size > static_cast(std::numeric_limits::max())) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "Data size is larger than maximum integer value"); + return; + } + + if (value == nullptr) + feature_.SetFieldNull(fieldIndex); + else + feature_.SetField(fieldIndex, static_cast(size), value); +} + +void OGRHanaFeatureWriter::SetFieldValueAsStringArray( + int fieldIndex, const odbc::Binary& value) +{ + if ( value.isNull() || value->size() == 0 ) + { + feature_.SetFieldNull(fieldIndex); + return; + } + + const char* ptr = value->data(); + const uint32_t numElements = *reinterpret_cast(ptr); + ptr += sizeof(uint32_t); + + char** values = nullptr; + + for (uint32_t i = 0; i < numElements; ++i) + { + uint8_t indicator = *ptr; + ++ptr; + + int32_t len = 0; + if (indicator <= DataLengthIndicator::MAX_ONE_BYTE) + { + len = indicator; + } + else if (indicator == DataLengthIndicator::TWO_BYTE) + { + len = *reinterpret_cast(ptr); + ptr += sizeof(int16_t); + } + else + { + len = *reinterpret_cast(ptr); + ptr += sizeof(int32_t); + } + + if (len == 0) + { + values = CSLAddString(values, ""); + } + else + { + if (ptr[0] == '\0') + { + values = CSLAddString(values, ptr); + } + else + { + char* val = static_cast(CPLMalloc(len + 1)); + memcpy(val, ptr, len); + val[len] = '\0'; + values = CSLAddString(values, val); + CPLFree(val); + } + } + + ptr += len; + } + + feature_.SetField(fieldIndex, values); + CSLDestroy(values); +} + +} /* end of OGRHANA namespace */ diff --git a/ogr/ogrsf_frmts/hana/ogrhanafeaturewriter.h b/ogr/ogrsf_frmts/hana/ogrhanafeaturewriter.h new file mode 100644 index 000000000000..6683d02e8187 --- /dev/null +++ b/ogr/ogrsf_frmts/hana/ogrhanafeaturewriter.h @@ -0,0 +1,117 @@ +/****************************************************************************** + * + * Project: SAP HANA Spatial Driver + * Purpose: OGRHanaFeatureWriter class declaration + * Author: Maxim Rylov + * + ****************************************************************************** + * Copyright (c) 2020, SAP SE + * + * 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. + ****************************************************************************/ + +#ifndef OGRHANAFEATUREWRITER_H_INCLUDED +#define OGRHANAFEATUREWRITER_H_INCLUDED + +#include "gdal_priv.h" +#include "odbc/Types.h" + +namespace OGRHANA { + +class OGRHanaFeatureWriter +{ +public: + explicit OGRHanaFeatureWriter(OGRFeature& feature); + + template + void SetFieldValue(int fieldIndex, const odbc::Nullable& value); + + void SetFieldValue(int fieldIndex, const odbc::Long& value); + void SetFieldValue(int fieldIndex, const odbc::Float& value); + void SetFieldValue(int fieldIndex, const odbc::Decimal& value); + void SetFieldValue(int fieldIndex, const odbc::String& value); + void SetFieldValue(int fieldIndex, const odbc::Date& value); + void SetFieldValue(int fieldIndex, const odbc::Time& value); + void SetFieldValue(int fieldIndex, const odbc::Timestamp& value); + void SetFieldValue(int fieldIndex, const odbc::Binary& value); + void SetFieldValue(int fieldIndex, const char* value); + void SetFieldValue(int fieldIndex, const void* value, std::size_t size); + + template + void SetFieldValueAsArray(int fieldIndex, const odbc::Binary& value); + void SetFieldValueAsStringArray(int fieldIndex, const odbc::Binary& value); + +private: + OGRFeature& feature_; +}; + +template +void OGRHanaFeatureWriter::SetFieldValue( + int fieldIndex, const odbc::Nullable& value) +{ + if (value.isNull()) + feature_.SetFieldNull(fieldIndex); + else + feature_.SetField(fieldIndex, *value); +} + +template +void OGRHanaFeatureWriter::SetFieldValueAsArray( + int fieldIndex, const odbc::Binary& value) +{ + if ( value.isNull() || value->size() == 0 ) + { + feature_.SetFieldNull(fieldIndex); + return; + } + + const uint8_t* ptr = reinterpret_cast(value->data()); + const uint32_t numElements = *reinterpret_cast(ptr); + ptr += sizeof(uint32_t); + + std::vector values; + values.reserve(numElements); + + bool elemHasLength = numElements * sizeof(InputT) != (value->size() - sizeof(uint32_t)); + + for (uint32_t i = 0; i < numElements; ++i) + { + if (elemHasLength) + { + uint8_t len = *ptr; + ++ptr; + + if (len > 0) + values.push_back(*reinterpret_cast(ptr)); + else + values.push_back(ResultT()); + } + else + values.push_back(*reinterpret_cast(ptr)); + + ptr += sizeof(InputT); + } + + feature_.SetField( + fieldIndex, static_cast(values.size()), values.data()); +} + +} /* end of OGRHANA namespace */ + +#endif // OGRHANAFEATUREWRITER_H_INCLUDED diff --git a/ogr/ogrsf_frmts/hana/ogrhanalayer.cpp b/ogr/ogrsf_frmts/hana/ogrhanalayer.cpp new file mode 100644 index 000000000000..f0fab809cf47 --- /dev/null +++ b/ogr/ogrsf_frmts/hana/ogrhanalayer.cpp @@ -0,0 +1,919 @@ +/****************************************************************************** + * + * Project: SAP HANA Spatial Driver + * Purpose: OGRHanaLayer class implementation + * Author: Maxim Rylov + * + ****************************************************************************** + * Copyright (c) 2020, SAP SE + * + * 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. + ****************************************************************************/ + +#include "ogr_hana.h" +#include "ogrhanafeaturewriter.h" +#include "ogrhanautils.h" + +#include +#include +#include +#include + +#include "odbc/Exception.h" +#include "odbc/ResultSet.h" +#include "odbc/Statement.h" +#include "odbc/Types.h" + +CPL_CVSID("$Id$") + +namespace OGRHANA { +namespace { +/************************************************************************/ +/* Helper methods */ +/************************************************************************/ + +CPLString BuildQuery( + const char* source, + const char* columns, + const char* where, + const char* orderBy, + int limit) +{ + std::ostringstream os; + os << "SELECT " << columns << " FROM (" << source << ")"; + if (where != nullptr && strlen(where) > 0) + os << " WHERE " << where; + if (orderBy != nullptr && strlen(orderBy) > 0) + os << " ORDER BY " << orderBy; + if (limit >= 0) + os << " LIMIT " << std::to_string(limit); + return os.str(); +} + +CPLString BuildQuery(const char* source, const char* columns) +{ + return BuildQuery(source, columns, nullptr, nullptr, -1); +} + +CPLString BuildSpatialFilter(uint dbVersion, const OGRGeometry& geom, const CPLString& clmName, int srid) +{ + OGREnvelope env; + geom.getEnvelope(&env); + + if( (CPLIsInf(env.MinX) || CPLIsInf(env.MinY) || + CPLIsInf(env.MaxX) || CPLIsInf(env.MaxY)) ) + return ""; + + auto clampValue = []( double v) + { + constexpr double MAX_VALUE = 1e+150; + if (v < -MAX_VALUE) + return -MAX_VALUE; + else if (v > MAX_VALUE) + return MAX_VALUE; + return v; + }; + + double minX = clampValue(env.MinX); + double minY = clampValue(env.MinY); + double maxX = clampValue(env.MaxX); + double maxY = clampValue(env.MaxY); + + // TODO: add support for non-rectangular filter, see m_bFilterIsEnvelope flag. + if ( dbVersion == 1 ) + return CPLString().Printf("\"%s\".ST_IntersectsRect(ST_GeomFromText('POINT(%.18g %.18g)', %d), ST_GeomFromText('POINT(%.18g %.18g)', %d)) = 1", + clmName.c_str(), minX, minY, srid, maxX, maxY, srid); + else + return CPLString().Printf("\"%s\".ST_IntersectsRectPlanar(ST_GeomFromText('POINT(%.18g %.18g)', %d), ST_GeomFromText('POINT(%.18g %.18g)', %d)) = 1", + clmName.c_str(), minX, minY, srid, maxX, maxY, srid); +} + +std::unique_ptr CreateFieldDefn(const AttributeColumnDescription& columnDesc) +{ + bool setFieldSize = false; + bool setFieldPrecision = false; + + OGRFieldType ogrFieldType = OFTString; + OGRFieldSubType ogrFieldSubType = OGRFieldSubType::OFSTNone; + + switch (columnDesc.type) + { + case odbc::SQLDataTypes::Bit: + case odbc::SQLDataTypes::Boolean: + ogrFieldType = columnDesc.isArray ? OFTIntegerList : OFTInteger; + ogrFieldSubType = OGRFieldSubType::OFSTBoolean; + break; + case odbc::SQLDataTypes::TinyInt: + case odbc::SQLDataTypes::SmallInt: + ogrFieldType = columnDesc.isArray ? OFTIntegerList : OFTInteger; + ogrFieldSubType = OGRFieldSubType::OFSTInt16; + break; + case odbc::SQLDataTypes::Integer: + ogrFieldType = columnDesc.isArray ? OFTIntegerList : OFTInteger; + break; + case odbc::SQLDataTypes::BigInt: + ogrFieldType = columnDesc.isArray ? OFTInteger64List : OFTInteger64; + break; + case odbc::SQLDataTypes::Double: + case odbc::SQLDataTypes::Real: + case odbc::SQLDataTypes::Float: + ogrFieldType = columnDesc.isArray ? OFTRealList : OFTReal; + if (columnDesc.type != odbc::SQLDataTypes::Double) + ogrFieldSubType = OGRFieldSubType::OFSTFloat32; + break; + case odbc::SQLDataTypes::Decimal: + case odbc::SQLDataTypes::Numeric: + ogrFieldType = columnDesc.isArray ? OFTRealList : OFTReal; + setFieldPrecision = true; + break; + case odbc::SQLDataTypes::Char: + case odbc::SQLDataTypes::VarChar: + case odbc::SQLDataTypes::LongVarChar: + case odbc::SQLDataTypes::WChar: + case odbc::SQLDataTypes::WVarChar: + case odbc::SQLDataTypes::WLongVarChar: + // Note: OFTWideString is deprecated + ogrFieldType = columnDesc.isArray ? OFTStringList : OFTString; + setFieldSize = true; + break; + case odbc::SQLDataTypes::Date: + case odbc::SQLDataTypes::TypeDate: + ogrFieldType = OFTDate; + break; + case odbc::SQLDataTypes::Time: + case odbc::SQLDataTypes::TypeTime: + ogrFieldType = OFTTime; + break; + case odbc::SQLDataTypes::Timestamp: + case odbc::SQLDataTypes::TypeTimestamp: + ogrFieldType = OFTDateTime; + break; + case odbc::SQLDataTypes::Binary: + case odbc::SQLDataTypes::VarBinary: + case odbc::SQLDataTypes::LongVarBinary: + ogrFieldType = OFTBinary; + setFieldSize = true; + break; + default: + break; + } + + if (columnDesc.isArray && !IsArrayField(ogrFieldType)) + CPLError(CE_Failure, CPLE_AppDefined, + "Array of type %s in column %s is not supported", + columnDesc.typeName.c_str(), columnDesc.name.c_str()); + + auto field = cpl::make_unique(columnDesc.name.c_str(), ogrFieldType); + field->SetSubType(ogrFieldSubType); + field->SetNullable(columnDesc.isNullable); + if (!columnDesc.isArray) + { + if (setFieldSize) + field->SetWidth(columnDesc.length); + if (setFieldPrecision) + { + field->SetWidth(columnDesc.precision); + field->SetPrecision(columnDesc.scale); + } + } + if (columnDesc.defaultValue.empty()) + field->SetDefault(nullptr); + else + field->SetDefault(columnDesc.defaultValue.c_str()); + return field; +} + +OGRGeometry* CreateGeometryFromWkb(const void* data, std::size_t size) +{ + if (size > static_cast(std::numeric_limits::max())) + CPLError( + CE_Failure, CPLE_AppDefined, "createFromWkb(): %s", + "Geometry size is larger than maximum integer value"); + + int len = static_cast(size); + + OGRGeometry* geom = nullptr; + OGRErr err = OGRGeometryFactory::createFromWkb(data, nullptr, &geom, len); + + if (OGRERR_NONE == err) + return geom; + + auto cplError = [](const char* message) { + CPLError(CE_Failure, CPLE_AppDefined, "ReadFeature(): %s", message); + }; + + switch (err) + { + case OGRERR_NOT_ENOUGH_DATA: + cplError("Not enough data to deserialize"); + return nullptr; + case OGRERR_UNSUPPORTED_GEOMETRY_TYPE: + cplError("Unsupported geometry type"); + return nullptr; + case OGRERR_CORRUPT_DATA: + cplError("Corrupt data"); + return nullptr; + default: + cplError("Unrecognized error"); + return nullptr; + } +} + +} // anonymous namespace + +/************************************************************************/ +/* OGRHanaLayer() */ +/************************************************************************/ + +OGRHanaLayer::OGRHanaLayer(OGRHanaDataSource* datasource) + : dataSource_(datasource) + , rawQuery_("") + , queryStatement_("") + , whereClause_("") +{ +} + +/************************************************************************/ +/* ~OGRHanaLayer() */ +/************************************************************************/ + +OGRHanaLayer::~OGRHanaLayer() +{ + if (featureDefn_) + featureDefn_->Release(); +} + +/************************************************************************/ +/* BuildQueryStatement() */ +/************************************************************************/ + +void OGRHanaLayer::BuildQueryStatement() +{ + if (!rebuildQueryStatement_) + return; + + if (!geomColumns_.empty()) + { + std::vector columns; + for (const GeometryColumnDescription& geometryColumnDesc : geomColumns_) + columns.push_back( + QuotedIdentifier(geometryColumnDesc.name) + ".ST_AsBinary() AS " + + QuotedIdentifier(geometryColumnDesc.name)); + + for (const AttributeColumnDescription& attributeColumnDesc : + attrColumns_) + columns.push_back(QuotedIdentifier(attributeColumnDesc.name)); + + queryStatement_ = CPLString().Printf( + "SELECT %s FROM (%s) %s", JoinStrings(columns, ", ").c_str(), + rawQuery_.c_str(), whereClause_.c_str()); + } + else + { + if (whereClause_.empty()) + queryStatement_ = rawQuery_; + else + queryStatement_ = CPLString().Printf( + "SELECT * FROM (%s) %s", rawQuery_.c_str(), + whereClause_.c_str()); + } + + rebuildQueryStatement_ = false; +} + +/************************************************************************/ +/* BuildWhereClause() */ +/************************************************************************/ + +void OGRHanaLayer::BuildWhereClause() +{ + whereClause_ = ""; + + CPLString spatialFilter; + if (m_poFilterGeom != nullptr) + { + OGRGeomFieldDefn* geomFieldDefn = nullptr; + if( featureDefn_->GetGeomFieldCount() != 0 ) + geomFieldDefn = featureDefn_->GetGeomFieldDefn(m_iGeomFieldFilter); + + if ( geomFieldDefn != nullptr) + { + const GeometryColumnDescription& geomClmDesc = + geomColumns_[static_cast(m_iGeomFieldFilter)]; + spatialFilter = BuildSpatialFilter(dataSource_->GetMajorVersion(), *m_poFilterGeom, geomClmDesc.name, geomClmDesc.srid); + } + } + + if (!attrFilter_.empty()) + { + whereClause_ = " WHERE " + attrFilter_; + if (!spatialFilter.empty()) + whereClause_ += " AND " + spatialFilter; + } + else if (!spatialFilter.empty()) + whereClause_ = " WHERE " + spatialFilter; +} + +/************************************************************************/ +/* EnsureBufferCapacity() */ +/************************************************************************/ + +void OGRHanaLayer::EnsureBufferCapacity(std::size_t size) +{ + if (size > dataBuffer_.size()) + dataBuffer_.resize(size); +} + +/************************************************************************/ +/* GetNextFeatureInternal() */ +/************************************************************************/ + +OGRFeature* OGRHanaLayer::GetNextFeatureInternal() +{ + if (nextFeatureId_ == 0) + { + CPLAssert(!queryStatement_.empty()); + + try + { + odbc::StatementRef stmt = dataSource_->CreateStatement(); + resultSet_ = stmt->executeQuery(queryStatement_.c_str()); + } + catch (const odbc::Exception& ex) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Fail to execute query : %s", ex.what()); + return nullptr; + } + } + + OGRFeature* feature = ReadFeature(); + ++nextFeatureId_; + + return feature; +} + +/************************************************************************/ +/* GetGeometryColumnSrid() */ +/************************************************************************/ + +int OGRHanaLayer::GetGeometryColumnSrid(int columnIndex) const +{ + if (columnIndex < 0 + || static_cast(columnIndex) >= geomColumns_.size()) + return -1; + return geomColumns_[static_cast(columnIndex)].srid; +} + +/************************************************************************/ +/* ReadFeature() */ +/************************************************************************/ + +OGRFeature* OGRHanaLayer::ReadFeature() +{ + if (!resultSet_->next()) + return nullptr; + + auto feature = cpl::make_unique(featureDefn_); + feature->SetFID(nextFeatureId_); + + unsigned short paramIndex = 0; + + // Read geometries + for (std::size_t i = 0; i < geomColumns_.size(); ++i) + { + ++paramIndex; + int geomIndex = static_cast(i); + + OGRGeomFieldDefn* geomFieldDef = + featureDefn_->GetGeomFieldDefn(geomIndex); + + if (geomFieldDef->IsIgnored()) + continue; + + std::size_t bufLength = resultSet_->getBinaryLength(paramIndex); + if (bufLength == 0 || bufLength == odbc::ResultSet::NULL_DATA) + { + feature->SetGeomFieldDirectly(geomIndex, nullptr); + continue; + } + + OGRGeometry* geom = nullptr; + if (bufLength != odbc::ResultSet::UNKNOWN_LENGTH) + { + EnsureBufferCapacity(bufLength); + resultSet_->getBinaryData( + paramIndex, dataBuffer_.data(), bufLength); + geom = + CreateGeometryFromWkb(dataBuffer_.data(), dataBuffer_.size()); + } + else + { + odbc::Binary wkb = resultSet_->getBinary(paramIndex); + if (!wkb.isNull() && wkb->size() > 0) + geom = CreateGeometryFromWkb( + static_cast(wkb->data()), wkb->size()); + } + + if (geom != nullptr) + geom->assignSpatialReference(geomFieldDef->GetSpatialRef()); + feature->SetGeomFieldDirectly(geomIndex, geom); + } + + // Read feature attributes + OGRHanaFeatureWriter featWriter(*feature); + int fieldIndex = -1; + for (const AttributeColumnDescription& clmDesc : attrColumns_) + { + ++paramIndex; + + if (clmDesc.isFeatureID) + { + if (clmDesc.type == odbc::SQLDataTypes::Integer) + { + odbc::Int val = resultSet_->getInt(paramIndex); + if (!val.isNull()) + feature->SetFID(static_cast(*val)); + } + else if (clmDesc.type == odbc::SQLDataTypes::BigInt) + { + odbc::Long val = resultSet_->getLong(paramIndex); + if (!val.isNull()) + feature->SetFID(static_cast(*val)); + } + continue; + } + + ++fieldIndex; + + OGRFieldDefn* fieldDefn = featureDefn_->GetFieldDefn(fieldIndex); + if (fieldDefn->IsIgnored()) + continue; + + if (clmDesc.isArray) + { + odbc::Binary val = resultSet_->getBinary(paramIndex); + if (val.isNull()) + { + feature->SetFieldNull(fieldIndex); + continue; + } + + switch (clmDesc.type) + { + case odbc::SQLDataTypes::Boolean: + featWriter.SetFieldValueAsArray(fieldIndex, val); + break; + case odbc::SQLDataTypes::TinyInt: + featWriter.SetFieldValueAsArray(fieldIndex, val); + break; + case odbc::SQLDataTypes::SmallInt: + featWriter.SetFieldValueAsArray(fieldIndex, val); + break; + case odbc::SQLDataTypes::Integer: + featWriter.SetFieldValueAsArray(fieldIndex, val); + break; + case odbc::SQLDataTypes::BigInt: + featWriter.SetFieldValueAsArray(fieldIndex, val); + break; + case odbc::SQLDataTypes::Float: + case odbc::SQLDataTypes::Real: + featWriter.SetFieldValueAsArray(fieldIndex, val); + break; + case odbc::SQLDataTypes::Double: + featWriter.SetFieldValueAsArray(fieldIndex, val); + break; + case odbc::SQLDataTypes::Char: + case odbc::SQLDataTypes::VarChar: + case odbc::SQLDataTypes::LongVarChar: + case odbc::SQLDataTypes::WChar: + case odbc::SQLDataTypes::WVarChar: + case odbc::SQLDataTypes::WLongVarChar: + featWriter.SetFieldValueAsStringArray(fieldIndex, val); + break; + } + + continue; + } + + switch (clmDesc.type) + { + case odbc::SQLDataTypes::Bit: + case odbc::SQLDataTypes::Boolean: { + odbc::Boolean val = resultSet_->getBoolean(paramIndex); + featWriter.SetFieldValue(fieldIndex, val); + } + break; + case odbc::SQLDataTypes::TinyInt: { + odbc::Byte val = resultSet_->getByte(paramIndex); + featWriter.SetFieldValue(fieldIndex, val); + } + break; + case odbc::SQLDataTypes::SmallInt: { + odbc::Short val = resultSet_->getShort(paramIndex); + featWriter.SetFieldValue(fieldIndex, val); + } + break; + case odbc::SQLDataTypes::Integer: { + odbc::Int val = resultSet_->getInt(paramIndex); + featWriter.SetFieldValue(fieldIndex, val); + } + break; + case odbc::SQLDataTypes::BigInt: { + odbc::Long val = resultSet_->getLong(paramIndex); + featWriter.SetFieldValue(fieldIndex, val); + } + break; + case odbc::SQLDataTypes::Real: + case odbc::SQLDataTypes::Float: { + odbc::Float val = resultSet_->getFloat(paramIndex); + featWriter.SetFieldValue(fieldIndex, val); + } + break; + case odbc::SQLDataTypes::Double: { + odbc::Double val = resultSet_->getDouble(paramIndex); + featWriter.SetFieldValue(fieldIndex, val); + } + break; + case odbc::SQLDataTypes::Decimal: + case odbc::SQLDataTypes::Numeric: { + odbc::Decimal val = resultSet_->getDecimal(paramIndex); + featWriter.SetFieldValue(fieldIndex, val); + } + break; + case odbc::SQLDataTypes::Char: + case odbc::SQLDataTypes::VarChar: + case odbc::SQLDataTypes::LongVarChar: + // Note: NVARCHAR data type is converted to UTF-8 on the HANA side + // when using a connection setting CHAR_AS_UTF8=1. + case odbc::SQLDataTypes::WChar: + case odbc::SQLDataTypes::WVarChar: + case odbc::SQLDataTypes::WLongVarChar: { + std::size_t len = resultSet_->getStringLength(paramIndex); + if (len == odbc::ResultSet::NULL_DATA) + feature->SetFieldNull(fieldIndex); + else if (len == 0) + feature->SetField(fieldIndex, ""); + else if (len != odbc::ResultSet::UNKNOWN_LENGTH) + { + EnsureBufferCapacity(len + 1); + resultSet_->getStringData( + paramIndex, dataBuffer_.data(), len + 1); + featWriter.SetFieldValue( + fieldIndex, dataBuffer_.data()); + } + else + { + odbc::String data = resultSet_->getString(paramIndex); + featWriter.SetFieldValue(fieldIndex, data); + } + } + break; + case odbc::SQLDataTypes::Binary: + case odbc::SQLDataTypes::VarBinary: + case odbc::SQLDataTypes::LongVarBinary: { + std::size_t len = resultSet_->getBinaryLength(paramIndex); + if (len == 0) + feature->SetField(fieldIndex, 0, static_cast(nullptr)); + else if (len == odbc::ResultSet::NULL_DATA) + feature->SetFieldNull(fieldIndex); + else if (len != odbc::ResultSet::UNKNOWN_LENGTH) + { + EnsureBufferCapacity(len); + resultSet_->getBinaryData(paramIndex, dataBuffer_.data(), len); + featWriter.SetFieldValue(fieldIndex, dataBuffer_.data(), len); + } + else + { + odbc::Binary binData = resultSet_->getBinary(paramIndex); + featWriter.SetFieldValue(fieldIndex, binData); + } + } + break; + case odbc::SQLDataTypes::Date: + case odbc::SQLDataTypes::TypeDate: { + odbc::Date date = resultSet_->getDate(paramIndex); + featWriter.SetFieldValue(fieldIndex, date); + } + break; + case odbc::SQLDataTypes::Time: + case odbc::SQLDataTypes::TypeTime: { + odbc::Time time = resultSet_->getTime(paramIndex); + featWriter.SetFieldValue(fieldIndex, time); + } + break; + case odbc::SQLDataTypes::Timestamp: + case odbc::SQLDataTypes::TypeTimestamp: { + odbc::Timestamp timestamp = resultSet_->getTimestamp(paramIndex); + featWriter.SetFieldValue(fieldIndex, timestamp); + } + break; + default: + break; + } + } + + return feature.release(); +} + +/************************************************************************/ +/* ReadFeatureDefinition() */ +/************************************************************************/ + +OGRErr OGRHanaLayer::ReadFeatureDefinition( + const CPLString& schemaName, + const CPLString& tableName, + const CPLString& query, + const char* featureDefName) +{ + attrColumns_.clear(); + fidFieldIndex_ = OGRNullFID; + fidFieldName_.clear(); + auto featureDef = cpl::make_unique(featureDefName); + featureDef->Reference(); + + std::vector columnDescriptions; + OGRErr err = + dataSource_->GetQueryColumns(schemaName, query, columnDescriptions); + if (err != OGRERR_NONE) + return err; + + std::vector primKeys = + dataSource_->GetTablePrimaryKeys(schemaName, tableName); + + if (featureDef->GetGeomFieldCount() == 1) + featureDef->DeleteGeomFieldDefn(0); + + for (const ColumnDescription& clmDesc : columnDescriptions) + { + if (clmDesc.isGeometry) + { + const GeometryColumnDescription& geometryColumnDesc = + clmDesc.geometryDescription; + + auto geomFieldDefn = cpl::make_unique( + geometryColumnDesc.name.c_str(), geometryColumnDesc.type); + geomFieldDefn->SetNullable(geometryColumnDesc.isNullable); + + if (geometryColumnDesc.srid >= 0) + { + OGRSpatialReference* srs = + dataSource_->GetSrsById(geometryColumnDesc.srid); + geomFieldDefn->SetSpatialRef(srs); + } + geomColumns_.push_back(geometryColumnDesc); + featureDef->AddGeomFieldDefn(std::move(geomFieldDefn)); + continue; + } + + AttributeColumnDescription attributeColumnDesc = + clmDesc.attributeDescription; + auto field = CreateFieldDefn(attributeColumnDesc); + + if ((field->GetType() == OFTInteger || field->GetType() == OFTInteger64) + && (fidFieldIndex_ == OGRNullFID && primKeys.size() > 0)) + { + for (const CPLString& key : primKeys) + { + if (key.compare(attributeColumnDesc.name) == 0) + { + fidFieldIndex_ = static_cast(attrColumns_.size()); + fidFieldName_ = field->GetNameRef(); + attributeColumnDesc.isFeatureID = true; + break; + } + } + } + + if (!attributeColumnDesc.isFeatureID) + featureDef->AddFieldDefn(field.get()); + attrColumns_.push_back(attributeColumnDesc); + } + + featureDefn_ = featureDef.release(); + + return OGRERR_NONE; +} + +/************************************************************************/ +/* ReadGeometryExtent() */ +/************************************************************************/ + +void OGRHanaLayer::ReadGeometryExtent(int geomField, OGREnvelope* extent) +{ + OGRGeomFieldDefn* geomFieldDef = featureDefn_->GetGeomFieldDefn(geomField); + const char* clmName = geomFieldDef->GetNameRef(); + int srid = GetGeometryColumnSrid(geomField); + CPLString sql; + if (dataSource_->IsSrsRoundEarth(srid)) + { + CPLString quotedClmName = QuotedIdentifier(clmName); + bool hasSrsPlanarEquivalent = dataSource_->HasSrsPlanarEquivalent(srid); + CPLString geomColumn = !hasSrsPlanarEquivalent + ? quotedClmName + : CPLString().Printf( + "%s.ST_SRID(%d)", quotedClmName.c_str(), + ToPlanarSRID(srid)); + CPLString columns = CPLString().Printf( + "MIN(%s.ST_XMin()), MIN(%s.ST_YMin()), MAX(%s.ST_XMax()), " + "MAX(%s.ST_YMax())", + geomColumn.c_str(), geomColumn.c_str(), geomColumn.c_str(), + geomColumn.c_str()); + sql = BuildQuery(rawQuery_.c_str(), columns.c_str()); + } + else + { + CPLString columns = CPLString().Printf( + "ST_EnvelopeAggr(%s) AS ext", QuotedIdentifier(clmName).c_str()); + CPLString subQuery = BuildQuery(rawQuery_.c_str(), columns); + sql = CPLString().Printf( + "SELECT ext.ST_XMin(),ext.ST_YMin(),ext.ST_XMax(),ext.ST_YMax() " + "FROM (%s)", + subQuery.c_str()); + } + + extent->MinX = 0.0; + extent->MaxX = 0.0; + extent->MinY = 0.0; + extent->MaxY = 0.0; + + odbc::StatementRef stmt = dataSource_->CreateStatement(); + odbc::ResultSetRef rsExtent = stmt->executeQuery(sql.c_str()); + if (rsExtent->next()) + { + odbc::Double val = rsExtent->getDouble(1); + if (!val.isNull()) + { + extent->MinX = *val; + extent->MinY = *rsExtent->getDouble(2); + extent->MaxX = *rsExtent->getDouble(3); + extent->MaxY = *rsExtent->getDouble(4); + } + } + rsExtent->close(); +} + +/************************************************************************/ +/* ResetReading() */ +/************************************************************************/ + +void OGRHanaLayer::ResetReading() +{ + nextFeatureId_ = 0; + BuildQueryStatement(); +} + +/************************************************************************/ +/* GetExtent() */ +/************************************************************************/ + +OGRErr OGRHanaLayer::GetExtent(int iGeomField, OGREnvelope* extent, int force) +{ + if( iGeomField < 0 || iGeomField >= GetLayerDefn()->GetGeomFieldCount() || + GetLayerDefn()->GetGeomFieldDefn(iGeomField)->GetType() == wkbNone ) + { + extent->MinX = 0.0; + extent->MaxX = 0.0; + extent->MinY = 0.0; + extent->MaxY = 0.0; + + if( iGeomField != 0 ) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Invalid geometry field index : %d", iGeomField); + } + return OGRERR_FAILURE; + } + + try + { + ReadGeometryExtent(iGeomField, extent); + return OGRERR_NONE; + } + catch (const std::exception& ex) + { + CPLString clmName = + (iGeomField < static_cast(geomColumns_.size())) + ? geomColumns_[static_cast( + geomColumns_.size())].name + : "unknown column"; + CPLError(CE_Failure, CPLE_AppDefined, + "Unable to query extent of '%s' using fast method: %s", + clmName.c_str(), ex.what()); + } + + if( iGeomField == 0 ) + return OGRLayer::GetExtent( extent, force ); + else + return OGRLayer::GetExtent( iGeomField, extent, force); +} + +/************************************************************************/ +/* GetFeatureCount() */ +/************************************************************************/ + +GIntBig OGRHanaLayer::GetFeatureCount(CPL_UNUSED int force) +{ + GIntBig ret = 0; + CPLString sql = CPLString().Printf( + "SELECT COUNT(*) FROM (%s) AS tmp", queryStatement_.c_str()); + odbc::StatementRef stmt = dataSource_->CreateStatement(); + odbc::ResultSetRef rs = stmt->executeQuery(sql.c_str()); + if (rs->next()) + ret = *rs->getLong(1); + rs->close(); + return ret; +} + +/************************************************************************/ +/* GetNextFeature() */ +/************************************************************************/ + +OGRFeature* OGRHanaLayer::GetNextFeature() +{ + while (true) + { + OGRFeature* feature = GetNextFeatureInternal(); + if (feature == nullptr) + return nullptr; + + if ((m_poFilterGeom == nullptr + || FilterGeometry(feature->GetGeometryRef())) + && (m_poAttrQuery == nullptr || m_poAttrQuery->Evaluate(feature))) + return feature; + + delete feature; + } +} + +/************************************************************************/ +/* GetFIDColumn() */ +/************************************************************************/ + +const char* OGRHanaLayer::GetFIDColumn() +{ + return fidFieldName_.c_str(); +} + +/************************************************************************/ +/* SetAttributeFilter() */ +/************************************************************************/ + +OGRErr OGRHanaLayer::SetAttributeFilter(const char* pszQuery) +{ + CPLFree(m_pszAttrQueryString); + m_pszAttrQueryString = pszQuery ? CPLStrdup(pszQuery) : nullptr; + + if (pszQuery == nullptr || strlen(pszQuery) == 0) + attrFilter_ = ""; + else + attrFilter_.assign(pszQuery, strlen(pszQuery)); + + rebuildQueryStatement_ = true; + BuildWhereClause(); + ResetReading(); + + return OGRERR_NONE; +} + +/************************************************************************/ +/* SetSpatialFilter() */ +/************************************************************************/ + +void OGRHanaLayer::SetSpatialFilter(int iGeomField, OGRGeometry* poGeom) +{ + m_iGeomFieldFilter = 0; + + if (iGeomField < 0 || iGeomField >= GetLayerDefn()->GetGeomFieldCount()) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Invalid geometry field index : %d", iGeomField); + return; + } + m_iGeomFieldFilter = iGeomField; + + if (!InstallFilter(poGeom)) + return; + + rebuildQueryStatement_ = true; + BuildWhereClause(); + ResetReading(); +} + +} /* end of OGRHANA namespace */ diff --git a/ogr/ogrsf_frmts/hana/ogrhanaresultlayer.cpp b/ogr/ogrsf_frmts/hana/ogrhanaresultlayer.cpp new file mode 100644 index 000000000000..e42c2f608031 --- /dev/null +++ b/ogr/ogrsf_frmts/hana/ogrhanaresultlayer.cpp @@ -0,0 +1,88 @@ +/****************************************************************************** + * + * Project: SAP HANA Spatial Driver + * Purpose: OGRHanaResultLayer class implementation + * Author: Maxim Rylov + * + ****************************************************************************** + * Copyright (c) 2020, SAP SE + * + * 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. + ****************************************************************************/ + +#include "ogr_hana.h" + +#include + +#include "odbc/PreparedStatement.h" +#include "odbc/ResultSetMetaData.h" + +CPL_CVSID("$Id$") + +namespace OGRHANA { + +/************************************************************************/ +/* OGRHanaResultLayer() */ +/************************************************************************/ + +OGRHanaResultLayer::OGRHanaResultLayer(OGRHanaDataSource* datasource) + : OGRHanaLayer(datasource) +{ +} + +/************************************************************************/ +/* Initialize() */ +/************************************************************************/ + +OGRErr OGRHanaResultLayer::Initialize(const char* query, OGRGeometry* spatialFilter) +{ + rawQuery_ = (query == nullptr) ? "" : query; + + auto names = dataSource_->FindSchemaAndTableNames(query); + OGRErr err = ReadFeatureDefinition( + names.first, names.second, query, "sql_statement"); + if (err != OGRERR_NONE) + return err; + + SetDescription(featureDefn_->GetName()); + + if (spatialFilter != nullptr) + SetSpatialFilter(spatialFilter); + BuildQueryStatement(); + + return OGRERR_NONE; +} + +/************************************************************************/ +/* TestCapability() */ +/************************************************************************/ + +int OGRHanaResultLayer::TestCapability(const char* capabilities) +{ + if (EQUAL(capabilities, OLCFastFeatureCount) + || EQUAL(capabilities, OLCFastSpatialFilter) + || EQUAL(capabilities, OLCFastGetExtent)) + return (geomColumns_.size() > 0); + if (EQUAL(capabilities, OLCStringsAsUTF8)) + return TRUE; + + return FALSE; +} + +} /* end of OGRHANA namespace */ diff --git a/ogr/ogrsf_frmts/hana/ogrhanatablelayer.cpp b/ogr/ogrsf_frmts/hana/ogrhanatablelayer.cpp new file mode 100644 index 000000000000..e2d88ca3e592 --- /dev/null +++ b/ogr/ogrsf_frmts/hana/ogrhanatablelayer.cpp @@ -0,0 +1,1804 @@ +/****************************************************************************** + * + * Project: SAP HANA Spatial Driver + * Purpose: OGRHanaTableLayer class implementation + * Author: Maxim Rylov + * + ****************************************************************************** + * Copyright (c) 2020, SAP SE + * + * 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. + ****************************************************************************/ + +#include "ogr_hana.h" +#include "ogrhanafeaturereader.h" +#include "ogrhanautils.h" + +#include +#include +#include +#include +#include + +#include "odbc/Exception.h" +#include "odbc/ResultSet.h" +#include "odbc/PreparedStatement.h" +#include "odbc/Types.h" + +CPL_CVSID("$Id$") + +namespace OGRHANA { +namespace { + +constexpr const char* UNSUPPORTED_OP_READ_ONLY = + "%s : unsupported operation on a read-only datasource."; + +const char* GetColumnDefaultValue(const OGRFieldDefn& field) +{ + const char* defaultValue = field.GetDefault(); + if (field.GetType() == OFTInteger && field.GetSubType() == OFSTBoolean) + return (EQUAL(defaultValue, "1") || EQUAL(defaultValue, "'t'")) + ? "TRUE" + : "FALSE"; + return defaultValue; +} + +CPLString FindGeomFieldName(const OGRFeatureDefn& featureDefn) +{ + if (featureDefn.GetGeomFieldCount() == 0) + return "OGR_GEOMETRY"; + + int numGeomFields = featureDefn.GetGeomFieldCount(); + for (int i = 1; i <= 2 * numGeomFields; ++i) + { + CPLString name = CPLSPrintf("OGR_GEOMETRY_%d", i); + if (featureDefn.GetGeomFieldIndex(name) < 0) + return name; + } + + return "OGR_GEOMETRY"; +} + +CPLString GetParameterValue(short type, const CPLString& typeName, bool isArray) +{ + if (isArray) + { + CPLString arrayType = "STRING"; + switch (type) + { + case odbc::SQLDataTypes::TinyInt: + arrayType = "TINYINT"; + break; + case odbc::SQLDataTypes::SmallInt: + arrayType = "SMALLINT"; + break; + case odbc::SQLDataTypes::Integer: + arrayType = "INT"; + break; + case odbc::SQLDataTypes::BigInt: + arrayType = "BIGINT"; + break; + case odbc::SQLDataTypes::Float: + case odbc::SQLDataTypes::Real: + arrayType = "REAL"; + break; + case odbc::SQLDataTypes::Double: + arrayType = "DOUBLE"; + break; + case odbc::SQLDataTypes::WVarChar: + arrayType = "STRING"; + break; + } + return "ARRAY(SELECT * FROM OGR_PARSE_" + arrayType + "_ARRAY(?, '" + + ARRAY_VALUES_DELIMITER + "'))"; + } + else if (typeName.compare("NCLOB") == 0) + return "TO_NCLOB(?)"; + else if (typeName.compare("CLOB") == 0) + return "TO_CLOB(?)"; + else if (typeName.compare("BLOB") == 0) + return "TO_BLOB(?)"; + else + return "?"; +} + +std::vector ParseIntValues(const char* str) +{ + std::vector values; + std::stringstream stream(str); + while (stream.good()) + { + std::string value; + getline(stream, value, ','); + values.push_back(std::atoi(value.c_str())); + } + return values; +} + +ColumnTypeInfo ParseColumnTypeInfo(const CPLString& typeDef) +{ + auto incorrectFormatErr = [&]() { + CPLError( + CE_Failure, CPLE_NotSupported, + "Column type '%s' has incorrect format.", typeDef.c_str()); + }; + + CPLString typeName; + std::vector typeSize; + + if (std::strstr(typeDef, "(") == nullptr) + { + typeName = typeDef; + } + else + { + const auto regex = std::regex(R"((\w+)+\((\d+(,\d+)*)\)$)"); + std::smatch match; + std::regex_search(typeDef, match, regex); + + if (match.size() != 0) + { + typeName.assign(match[1]); + typeSize = ParseIntValues(match[2].str().c_str()); + } + + if (typeSize.empty() || typeSize.size() > 2) + { + incorrectFormatErr(); + return {"", odbc::SQLDataTypes::Unknown, 0, 0}; + } + } + + if (EQUAL(typeName.c_str(), "BOOLEAN")) + return {typeName, odbc::SQLDataTypes::Boolean, 0, 0}; + else if (EQUAL(typeName.c_str(), "TINYINT")) + return {typeName, odbc::SQLDataTypes::TinyInt, 0, 0}; + else if (EQUAL(typeName.c_str(), "SMALLINT")) + return {typeName, odbc::SQLDataTypes::SmallInt, 0, 0}; + else if (EQUAL(typeName.c_str(), "INTEGER")) + return {typeName, odbc::SQLDataTypes::Integer, 0, 0}; + else if (EQUAL(typeName.c_str(), "DECIMAL")) + { + switch (typeSize.size()) + { + case 0: + return {typeName, odbc::SQLDataTypes::Decimal, 0, 0}; + case 1: + return {typeName, odbc::SQLDataTypes::Decimal, typeSize[0], 0}; + case 2: + return {typeName, odbc::SQLDataTypes::Decimal, typeSize[0], + typeSize[1]}; + } + } + else if (EQUAL(typeName.c_str(), "FLOAT")) + { + switch (typeSize.size()) + { + case 0: + return {typeName, odbc::SQLDataTypes::Float, 10, 0}; + case 1: + return {typeName, odbc::SQLDataTypes::Float, typeSize[0], 0}; + default: + incorrectFormatErr(); + return {"", odbc::SQLDataTypes::Unknown, 0, 0}; + } + } + else if (EQUAL(typeName.c_str(), "REAL")) + return {typeName, odbc::SQLDataTypes::Real, 0, 0}; + else if (EQUAL(typeName.c_str(), "DOUBLE")) + return {typeName, odbc::SQLDataTypes::Double, 0, 0}; + else if (EQUAL(typeName.c_str(), "VARCHAR")) + { + switch (typeSize.size()) + { + case 0: + return {typeName, odbc::SQLDataTypes::VarChar, 1, 0}; + case 1: + return {typeName, odbc::SQLDataTypes::VarChar, typeSize[0], 0}; + default: + incorrectFormatErr(); + return {"", odbc::SQLDataTypes::Unknown, 0, 0}; + } + } + else if (EQUAL(typeName.c_str(), "NVARCHAR")) + { + switch (typeSize.size()) + { + case 0: + return {typeName, odbc::SQLDataTypes::WVarChar, 1, 0}; + case 1: + return {typeName, odbc::SQLDataTypes::WVarChar, typeSize[0], 0}; + case 2: + incorrectFormatErr(); + return {"", odbc::SQLDataTypes::Unknown, 0, 0}; + } + } + else if (EQUAL(typeName.c_str(), "NCLOB")) + return {typeName, odbc::SQLDataTypes::WLongVarChar, 0, 0}; + else if (EQUAL(typeName.c_str(), "DATE")) + return {typeName, odbc::SQLDataTypes::Date, 0, 0}; + else if (EQUAL(typeName.c_str(), "TIME")) + return {typeName, odbc::SQLDataTypes::Time, 0, 0}; + else if (EQUAL(typeName.c_str(), "TIMESTAMP")) + return {typeName, odbc::SQLDataTypes::Timestamp, 0, 0}; + else if (EQUAL(typeName.c_str(), "VARBINARY")) + { + switch (typeSize.size()) + { + case 0: + return {typeName, odbc::SQLDataTypes::VarBinary, 1, 0}; + case 1: + return {typeName, odbc::SQLDataTypes::VarBinary, typeSize[0], 0}; + case 2: + incorrectFormatErr(); + return {"", odbc::SQLDataTypes::Unknown, 0, 0}; + } + } + else if (EQUAL(typeName.c_str(), "BLOB")) + return {typeName, odbc::SQLDataTypes::LongVarBinary, 0, 0}; + + CPLError( + CE_Failure, CPLE_NotSupported, "Unknown column type '%s'.", + typeName.c_str()); + return {typeName, odbc::SQLDataTypes::Unknown, 0, 0}; +} + +CPLString GetColumnDefinition(const ColumnTypeInfo& typeInfo) +{ + bool isArray = std::strstr(typeInfo.name, "ARRAY") != nullptr; + + if (isArray) + { + switch (typeInfo.type) + { + case odbc::SQLDataTypes::SmallInt: + return "SMALLINT ARRAY"; + case odbc::SQLDataTypes::Integer: + return "INTEGER ARRAY"; + case odbc::SQLDataTypes::BigInt: + return "BIGINT ARRAY"; + case odbc::SQLDataTypes::Real: + return "REAL ARRAY"; + case odbc::SQLDataTypes::Double: + return "DOUBLE ARRAY"; + case odbc::SQLDataTypes::WVarChar: + return "NVARCHAR(512) ARRAY"; + default: + return "UNKNOWN"; + } + } + + switch (typeInfo.type) + { + case odbc::SQLDataTypes::Boolean: + case odbc::SQLDataTypes::TinyInt: + case odbc::SQLDataTypes::SmallInt: + case odbc::SQLDataTypes::Integer: + case odbc::SQLDataTypes::BigInt: + case odbc::SQLDataTypes::Float: + case odbc::SQLDataTypes::Real: + case odbc::SQLDataTypes::Double: + case odbc::SQLDataTypes::Date: + case odbc::SQLDataTypes::TypeDate: + case odbc::SQLDataTypes::Time: + case odbc::SQLDataTypes::TypeTime: + case odbc::SQLDataTypes::Timestamp: + case odbc::SQLDataTypes::TypeTimestamp: + case odbc::SQLDataTypes::Char: + case odbc::SQLDataTypes::WChar: + case odbc::SQLDataTypes::LongVarChar: + case odbc::SQLDataTypes::LongVarBinary: + return typeInfo.name; + case odbc::SQLDataTypes::Decimal: + case odbc::SQLDataTypes::Numeric: + return CPLString().Printf("DECIMAL(%d,%d)", typeInfo.width, typeInfo.precision); + case odbc::SQLDataTypes::VarChar: + case odbc::SQLDataTypes::WVarChar: + case odbc::SQLDataTypes::Binary: + case odbc::SQLDataTypes::VarBinary: + case odbc::SQLDataTypes::WLongVarChar: + return (typeInfo.width == 0) + ? typeInfo.name + : CPLString().Printf("%s(%d)", typeInfo.name.c_str(), typeInfo.width); + default: + return "UNKNOWN"; + } +} + +void SetFieldDefn(OGRFieldDefn& field, const ColumnTypeInfo& typeInfo) +{ + auto isArray = [&typeInfo]() { + return std::strstr(typeInfo.name, "ARRAY") != nullptr; + }; + + switch (typeInfo.type) + { + case odbc::SQLDataTypes::Bit: + case odbc::SQLDataTypes::Boolean: + field.SetType(OFTInteger); + field.SetSubType(OFSTBoolean); + break; + case odbc::SQLDataTypes::TinyInt: + case odbc::SQLDataTypes::SmallInt: + field.SetType(isArray() ? OFTIntegerList : OFTInteger); + field.SetSubType(OFSTInt16); + break; + case odbc::SQLDataTypes::Integer: + field.SetType(isArray() ? OFTIntegerList : OFTInteger); + break; + case odbc::SQLDataTypes::BigInt: + field.SetType(isArray() ? OFTInteger64List : OFTInteger64); + break; + case odbc::SQLDataTypes::Double: + case odbc::SQLDataTypes::Real: + case odbc::SQLDataTypes::Float: + field.SetType(isArray() ? OFTRealList : OFTReal); + if (typeInfo.type != odbc::SQLDataTypes::Double) + field.SetSubType(OFSTFloat32); + break; + case odbc::SQLDataTypes::Decimal: + case odbc::SQLDataTypes::Numeric: + field.SetType(isArray() ? OFTRealList : OFTReal); + break; + case odbc::SQLDataTypes::Char: + case odbc::SQLDataTypes::VarChar: + case odbc::SQLDataTypes::LongVarChar: + field.SetType(isArray() ? OFTStringList : OFTString); + break; + case odbc::SQLDataTypes::WChar: + case odbc::SQLDataTypes::WVarChar: + case odbc::SQLDataTypes::WLongVarChar: + field.SetType(isArray() ? OFTStringList : OFTString); + break; + case odbc::SQLDataTypes::Date: + case odbc::SQLDataTypes::TypeDate: + field.SetType(OFTDate); + break; + case odbc::SQLDataTypes::Time: + case odbc::SQLDataTypes::TypeTime: + field.SetType(OFTTime); + break; + case odbc::SQLDataTypes::Timestamp: + case odbc::SQLDataTypes::TypeTimestamp: + field.SetType(OFTDateTime); + break; + case odbc::SQLDataTypes::Binary: + case odbc::SQLDataTypes::VarBinary: + case odbc::SQLDataTypes::LongVarBinary: + field.SetType(OFTBinary); + break; + default: + break; + } + + field.SetWidth(typeInfo.width); + field.SetPrecision(typeInfo.precision); +} + +} // anonymous namespace + +/************************************************************************/ +/* OGRHanaTableLayer() */ +/************************************************************************/ + +OGRHanaTableLayer::OGRHanaTableLayer(OGRHanaDataSource* datasource, int update) + : OGRHanaLayer(datasource) + , updateMode_(update) +{ +} + +/************************************************************************/ +/* ~OGRHanaTableLayer() */ +/************************************************************************/ + +OGRHanaTableLayer::~OGRHanaTableLayer() +{ + FlushPendingFeatures(); +} + +/************************************************************************/ +/* ReadTableDefinition() */ +/************************************************************************/ + +OGRErr OGRHanaTableLayer::ReadTableDefinition() +{ + OGRErr err = ReadFeatureDefinition( + schemaName_, tableName_, rawQuery_, tableName_.c_str()); + if (err != OGRERR_NONE) + return err; + + if (fidFieldIndex_ != OGRNullFID) + CPLDebug( + "HANA", "table %s has FID column %s.", tableName_.c_str(), + fidFieldName_.c_str()); + else + CPLDebug( + "HANA", "table %s has no FID column, FIDs will not be reliable!", + tableName_.c_str()); + + return OGRERR_NONE; +} + +/* -------------------------------------------------------------------- */ +/* ExecuteUpdate() */ +/* -------------------------------------------------------------------- */ + +std::pair OGRHanaTableLayer::ExecuteUpdate( + odbc::PreparedStatement& statement, bool withBatch, const char* functionName) +{ + std::size_t ret = 0; + + try + { + if (withBatch) + { + if (statement.getBatchDataSize() >= batchSize_) + statement.executeBatch(); + ret = 1; + } + else + { + ret = statement.executeUpdate(); + } + + if (!dataSource_->IsTransactionStarted()) + dataSource_->Commit(); + } + catch (odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, "Failed to execute %s: %s", + functionName, ex.what()); + return {OGRERR_FAILURE, 0}; + } + + return {OGRERR_NONE, ret}; +} + +/* -------------------------------------------------------------------- */ +/* CreateDeleteFeatureStatement() */ +/* -------------------------------------------------------------------- */ + +odbc::PreparedStatementRef OGRHanaTableLayer::CreateDeleteFeatureStatement() +{ + CPLString sql = CPLString().Printf( + "DELETE FROM %s WHERE %s = ?", + GetFullTableNameQuoted(schemaName_, tableName_).c_str(), + QuotedIdentifier(GetFIDColumn()).c_str()); + return dataSource_->PrepareStatement(sql.c_str()); +} + +/* -------------------------------------------------------------------- */ +/* CreateInsertFeatureStatement() */ +/* -------------------------------------------------------------------- */ + +odbc::PreparedStatementRef OGRHanaTableLayer::CreateInsertFeatureStatement(bool withFID) +{ + std::vector columns; + std::vector values; + bool hasArray = false; + for (const AttributeColumnDescription& clmDesc : attrColumns_) + { + if (clmDesc.isFeatureID && !withFID) + { + if (clmDesc.isAutoIncrement) + continue; + } + + columns.push_back(QuotedIdentifier(clmDesc.name)); + values.push_back( + GetParameterValue(clmDesc.type, clmDesc.typeName, clmDesc.isArray)); + if (clmDesc.isArray) + hasArray = true; + } + + for (const GeometryColumnDescription& geomClmDesc : geomColumns_) + { + columns.push_back(QuotedIdentifier(geomClmDesc.name)); + values.push_back( + "ST_GeomFromWKB(? , " + std::to_string(geomClmDesc.srid) + ")"); + } + + if (hasArray && !parseFunctionsChecked_) + { + // Create helper functions if needed. + if (!dataSource_->ParseArrayFunctionsExist(schemaName_.c_str())) + dataSource_->CreateParseArrayFunctions(schemaName_.c_str()); + parseFunctionsChecked_ = true; + } + + const CPLString sql = CPLString().Printf( + "INSERT INTO %s (%s) VALUES(%s)", + GetFullTableNameQuoted(schemaName_, tableName_).c_str(), + JoinStrings(columns, ", ").c_str(), JoinStrings(values, ", ").c_str()); + + return dataSource_->PrepareStatement(sql.c_str()); +} + +/* -------------------------------------------------------------------- */ +/* CreateUpdateFeatureStatement() */ +/* -------------------------------------------------------------------- */ + +odbc::PreparedStatementRef OGRHanaTableLayer::CreateUpdateFeatureStatement() +{ + std::vector values; + values.reserve(attrColumns_.size()); + bool hasArray = false; + + for (const AttributeColumnDescription& clmDesc : attrColumns_) + { + if (clmDesc.isFeatureID) + { + if (clmDesc.isAutoIncrement) + continue; + } + values.push_back( + QuotedIdentifier(clmDesc.name) + " = " + + GetParameterValue( + clmDesc.type, clmDesc.typeName, clmDesc.isArray)); + if (clmDesc.isArray) + hasArray = true; + } + + for (const GeometryColumnDescription& geomClmDesc : geomColumns_) + { + values.push_back( + QuotedIdentifier(geomClmDesc.name) + " = " + "ST_GeomFromWKB(?, " + + std::to_string(geomClmDesc.srid) + ")"); + } + + if (hasArray && !parseFunctionsChecked_) + { + // Create helper functions if needed. + if (!dataSource_->ParseArrayFunctionsExist(schemaName_.c_str())) + dataSource_->CreateParseArrayFunctions(schemaName_.c_str()); + parseFunctionsChecked_ = true; + } + + const CPLString sql = CPLString().Printf( + "UPDATE %s SET %s WHERE %s = ?", + GetFullTableNameQuoted(schemaName_, tableName_).c_str(), + JoinStrings(values, ", ").c_str(), + QuotedIdentifier(GetFIDColumn()).c_str()); + + return dataSource_->PrepareStatement(sql.c_str()); +} + +/* -------------------------------------------------------------------- */ +/* ResetPreparedStatements() */ +/* -------------------------------------------------------------------- */ + +void OGRHanaTableLayer::ResetPreparedStatements() +{ + if (!currentIdentityValueStmt_.isNull()) + currentIdentityValueStmt_ = nullptr; + if (!insertFeatureStmtWithFID_.isNull()) + insertFeatureStmtWithFID_ = nullptr; + if (!insertFeatureStmtWithoutFID_.isNull()) + insertFeatureStmtWithoutFID_ = nullptr; + if (!deleteFeatureStmt_.isNull()) + deleteFeatureStmt_ = nullptr; + if (!updateFeatureStmt_.isNull()) + updateFeatureStmt_ = nullptr; +} + +/************************************************************************/ +/* SetStatementParameters() */ +/************************************************************************/ + +OGRErr OGRHanaTableLayer::SetStatementParameters( + odbc::PreparedStatement& statement, + OGRFeature* feature, + bool newFeature, + bool withFID, + const char* functionName) +{ + OGRHanaFeatureReader featReader(*feature); + + unsigned short paramIndex = 0; + int fieldIndex = -1; + for (const AttributeColumnDescription& clmDesc : attrColumns_) + { + if (clmDesc.isFeatureID) + { + if (!withFID && clmDesc.isAutoIncrement) + continue; + + ++paramIndex; + + switch (clmDesc.type) + { + case odbc::SQLDataTypes::Integer: + if (feature->GetFID() == OGRNullFID) + statement.setInt(paramIndex, odbc::Int()); + else + { + if (std::numeric_limits::min() > feature->GetFID() || + std::numeric_limits::max() < feature->GetFID()) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "%s: Feature id with value %s cannot " + "be stored in a column of type INTEGER", + functionName, + std::to_string(feature->GetFID()).c_str()); + return OGRERR_FAILURE; + } + + statement.setInt( + paramIndex, + odbc::Int(static_cast(feature->GetFID()))); + } + break; + case odbc::SQLDataTypes::BigInt: + if (feature->GetFID() == OGRNullFID) + statement.setLong(paramIndex, odbc::Long()); + else + statement.setLong( + paramIndex, + odbc::Long(static_cast(feature->GetFID()))); + break; + default: + CPLError( + CE_Failure, CPLE_AppDefined, + "%s: Unexpected type ('%s') in the field " + "'%s'", + functionName, std::to_string(clmDesc.type).c_str(), + clmDesc.name.c_str()); + return OGRERR_FAILURE; + } + continue; + } + else + ++paramIndex; + + ++fieldIndex; + + switch (clmDesc.type) + { + case odbc::SQLDataTypes::Bit: + case odbc::SQLDataTypes::Boolean: + statement.setBoolean( + paramIndex, featReader.GetFieldAsBoolean(fieldIndex)); + break; + case odbc::SQLDataTypes::TinyInt: + if (clmDesc.isArray) + statement.setString( + paramIndex, featReader.GetFieldAsIntArray(fieldIndex)); + else + statement.setByte( + paramIndex, featReader.GetFieldAsByte(fieldIndex)); + break; + case odbc::SQLDataTypes::SmallInt: + if (clmDesc.isArray) + statement.setString( + paramIndex, featReader.GetFieldAsIntArray(fieldIndex)); + else + statement.setShort( + paramIndex, featReader.GetFieldAsShort(fieldIndex)); + break; + case odbc::SQLDataTypes::Integer: + if (clmDesc.isArray) + statement.setString( + paramIndex, featReader.GetFieldAsIntArray(fieldIndex)); + else + statement.setInt( + paramIndex, featReader.GetFieldAsInt(fieldIndex)); + break; + case odbc::SQLDataTypes::BigInt: + if (clmDesc.isArray) + statement.setString( + paramIndex, featReader.GetFieldAsBigIntArray(fieldIndex)); + else + statement.setLong( + paramIndex, featReader.GetFieldAsLong(fieldIndex)); + break; + case odbc::SQLDataTypes::Float: + case odbc::SQLDataTypes::Real: + if (clmDesc.isArray) + statement.setString( + paramIndex, featReader.GetFieldAsRealArray(fieldIndex)); + else + statement.setFloat( + paramIndex, featReader.GetFieldAsFloat(fieldIndex)); + break; + case odbc::SQLDataTypes::Double: + if (clmDesc.isArray) + statement.setString( + paramIndex, featReader.GetFieldAsDoubleArray(fieldIndex)); + else + statement.setDouble( + paramIndex, featReader.GetFieldAsDouble(fieldIndex)); + break; + case odbc::SQLDataTypes::Decimal: + case odbc::SQLDataTypes::Numeric: + if ((!feature->IsFieldSet(fieldIndex) + || feature->IsFieldNull(fieldIndex)) + && feature->GetFieldDefnRef(fieldIndex)->GetDefault() + == nullptr) + statement.setDecimal(paramIndex, odbc::Decimal()); + else + statement.setDouble( + paramIndex, featReader.GetFieldAsDouble(fieldIndex)); + break; + case odbc::SQLDataTypes::Char: + case odbc::SQLDataTypes::VarChar: + case odbc::SQLDataTypes::LongVarChar: + if (clmDesc.isArray) + statement.setString( + paramIndex, featReader.GetFieldAsStringArray(fieldIndex)); + else + statement.setString( + paramIndex, + featReader.GetFieldAsString(fieldIndex, clmDesc.length)); + break; + case odbc::SQLDataTypes::WChar: + case odbc::SQLDataTypes::WVarChar: + case odbc::SQLDataTypes::WLongVarChar: + if (clmDesc.isArray) + statement.setString( + paramIndex, featReader.GetFieldAsStringArray(fieldIndex)); + else + statement.setString( + paramIndex, + featReader.GetFieldAsNString(fieldIndex, clmDesc.length)); + break; + case odbc::SQLDataTypes::Binary: + case odbc::SQLDataTypes::VarBinary: + case odbc::SQLDataTypes::LongVarBinary: { + Binary bin = featReader.GetFieldAsBinary(fieldIndex); + statement.setBytes(paramIndex, bin.data, bin.size); + } + break; + case odbc::SQLDataTypes::DateTime: + case odbc::SQLDataTypes::TypeDate: + statement.setDate( + paramIndex, featReader.GetFieldAsDate(fieldIndex)); + break; + case odbc::SQLDataTypes::Time: + case odbc::SQLDataTypes::TypeTime: + statement.setTime( + paramIndex, featReader.GetFieldAsTime(fieldIndex)); + break; + case odbc::SQLDataTypes::Timestamp: + case odbc::SQLDataTypes::TypeTimestamp: + statement.setTimestamp( + paramIndex, featReader.GetFieldAsTimestamp(fieldIndex)); + break; + } + } + + for (std::size_t i = 0; i < geomColumns_.size(); ++i) + { + ++paramIndex; + Binary wkb{nullptr, 0}; + OGRErr err = GetGeometryWkb(feature, static_cast(i), wkb); + if (OGRERR_NONE != err) + return err; + statement.setBytes(paramIndex, wkb.data, wkb.size); + } + + if (!newFeature) + { + ++paramIndex; + + statement.setLong( + paramIndex, + odbc::Long(static_cast(feature->GetFID()))); + } + + return OGRERR_NONE; +} + +/* -------------------------------------------------------------------- */ +/* DropTable() */ +/* -------------------------------------------------------------------- */ + +OGRErr OGRHanaTableLayer::DropTable() +{ + CPLString sql = + "DROP TABLE " + GetFullTableNameQuoted(schemaName_, tableName_); + try + { + dataSource_->ExecuteSQL(sql.c_str()); + CPLDebug("HANA", "Dropped table %s.", GetName()); + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, "Unable to delete layer '%s': %s", + tableName_.c_str(), ex.what()); + return OGRERR_FAILURE; + } + + return OGRERR_NONE; +} + +/* -------------------------------------------------------------------- */ +/* FlushPendingFeatures() */ +/* -------------------------------------------------------------------- */ + +void OGRHanaTableLayer::FlushPendingFeatures() +{ + if (HasPendingFeatures()) + dataSource_->Commit(); +} + +/* -------------------------------------------------------------------- */ +/* HasPendingFeatures() */ +/* -------------------------------------------------------------------- */ + +bool OGRHanaTableLayer::HasPendingFeatures() const +{ + return (!deleteFeatureStmt_.isNull() + && deleteFeatureStmt_->getBatchDataSize() > 0) + || (!insertFeatureStmtWithFID_.isNull() + && insertFeatureStmtWithFID_->getBatchDataSize() > 0) + || (!insertFeatureStmtWithoutFID_.isNull() + && insertFeatureStmtWithoutFID_->getBatchDataSize() > 0) + || (!updateFeatureStmt_.isNull() + && updateFeatureStmt_->getBatchDataSize() > 0); +} + +/* -------------------------------------------------------------------- */ +/* GetColumnTypeInfo() */ +/* -------------------------------------------------------------------- */ + +ColumnTypeInfo OGRHanaTableLayer::GetColumnTypeInfo(const OGRFieldDefn& field) const +{ + for (const auto& clmType : customColumnDefs_) + { + if (EQUAL(clmType.name.c_str(), field.GetNameRef())) + return ParseColumnTypeInfo(clmType.typeDef); + } + + switch (field.GetType()) + { + case OFTInteger: + if (preservePrecision_ && field.GetWidth() > 10) + { + return {"DECIMAL", odbc::SQLDataTypes::Decimal, field.GetWidth(), 0}; + } + else + { + if (field.GetSubType() == OFSTBoolean) + return {"BOOLEAN", odbc::SQLDataTypes::Boolean, + field.GetWidth(), 0}; + else if (field.GetSubType() == OFSTInt16) + return {"SMALLINT", odbc::SQLDataTypes::SmallInt, + field.GetWidth(), 0}; + else + return {"INTEGER", odbc::SQLDataTypes::Integer, + field.GetWidth(), 0}; + } + break; + case OFTInteger64: + if (preservePrecision_ && field.GetWidth() > 20) + { + return {"DECIMAL", odbc::SQLDataTypes::Decimal, + field.GetWidth(), 0}; + } + else + return {"BIGINT", odbc::SQLDataTypes::BigInt, + field.GetWidth(), 0}; + break; + case OFTReal: + if (preservePrecision_ && field.GetWidth() != 0) + { + return {"DECIMAL", odbc::SQLDataTypes::Decimal, + field.GetWidth(), field.GetPrecision()}; + } + else + { + if (field.GetSubType() == OFSTFloat32) + return {"REAL", odbc::SQLDataTypes::Real, field.GetWidth(), + field.GetPrecision()}; + else + return {"DOUBLE", odbc::SQLDataTypes::Double, field.GetWidth(), + field.GetPrecision()}; + } + case OFTString: + if (field.GetWidth() == 0 || !preservePrecision_) + { + int width = static_cast(defaultStringSize_); + return {"NVARCHAR", odbc::SQLDataTypes::WLongVarChar, width, 0}; + } + else + { + if (field.GetWidth() <= 5000) + return {"NVARCHAR", odbc::SQLDataTypes::WLongVarChar, + field.GetWidth(), 0}; + else + return {"NCLOB", odbc::SQLDataTypes::WLongVarChar, + 0, 0}; + } + case OFTBinary: + if (field.GetWidth() <= 5000) + return {"VARBINARY", odbc::SQLDataTypes::VarBinary, + field.GetWidth(), 0}; + else + return {"BLOB", odbc::SQLDataTypes::LongVarBinary, field.GetWidth(), + 0}; + case OFTDate: + return {"DATE", odbc::SQLDataTypes::TypeDate, field.GetWidth(), 0}; + case OFTTime: + return {"TIME", odbc::SQLDataTypes::TypeTime, field.GetWidth(), 0}; + case OFTDateTime: + return {"TIMESTAMP", odbc::SQLDataTypes::TypeTimestamp, + field.GetWidth(), 0}; + case OFTIntegerList: + if (field.GetSubType() == OGRFieldSubType::OFSTInt16) + return {"ARRAY", odbc::SQLDataTypes::SmallInt, + field.GetWidth(), 0}; + else + return {"ARRAY", odbc::SQLDataTypes::Integer, + field.GetWidth(), 0}; + case OFTInteger64List: + return {"ARRAY", odbc::SQLDataTypes::BigInt, field.GetWidth(), + 0}; + case OFTRealList: + if (field.GetSubType() == OGRFieldSubType::OFSTFloat32) + return {"ARRAY", odbc::SQLDataTypes::Real, field.GetWidth(), + 0}; + else + return {"ARRAY", odbc::SQLDataTypes::Double, + field.GetWidth(), 0}; + break; + case OFTStringList: + return {"ARRAY", odbc::SQLDataTypes::WVarChar, 512, 0}; + default: + break; + } + + return {"", odbc::SQLDataTypes::Unknown, 0, 0}; +} + +/* -------------------------------------------------------------------- */ +/* GetGeometryWkb() */ +/* -------------------------------------------------------------------- */ +OGRErr OGRHanaTableLayer::GetGeometryWkb( + OGRFeature* feature, int fieldIndex, Binary& binary) +{ + OGRGeometry* geom = feature->GetGeomFieldRef(fieldIndex); + if (geom == nullptr || !IsGeometryTypeSupported(geom->getIsoGeometryType())) + return OGRERR_NONE; + + // Rings must be closed, otherwise HANA throws an exception + geom->closeRings(); + std::size_t size = static_cast(geom->WkbSize()); + EnsureBufferCapacity(size); + unsigned char* data = reinterpret_cast(dataBuffer_.data()); + OGRErr err = geom->exportToWkb( + OGRwkbByteOrder::wkbNDR, data, OGRwkbVariant::wkbVariantIso); + if (OGRERR_NONE == err) + { + binary.data = data; + binary.size = size; + } + return err; +} + +/* -------------------------------------------------------------------- */ +/* Initialize() */ +/* -------------------------------------------------------------------- */ + +OGRErr OGRHanaTableLayer::Initialize( + const char* schemaName, const char* tableName) +{ + schemaName_ = schemaName; + tableName_ = tableName; + rawQuery_ = + "SELECT * FROM " + GetFullTableNameQuoted(schemaName, tableName); + + OGRErr err = ReadTableDefinition(); + if (err != OGRERR_NONE) + return err; + + SetDescription(featureDefn_->GetName()); + + ResetReading(); + return OGRERR_NONE; +} + +/************************************************************************/ +/* ResetReading() */ +/************************************************************************/ + +void OGRHanaTableLayer::ResetReading() +{ + FlushPendingFeatures(); + + OGRHanaLayer::ResetReading(); +} + +/************************************************************************/ +/* TestCapability() */ +/************************************************************************/ + +int OGRHanaTableLayer::TestCapability(const char* capabilities) +{ + if (EQUAL(capabilities, OLCRandomRead)) + return fidFieldIndex_ != OGRNullFID; + if (EQUAL(capabilities, OLCFastFeatureCount)) + return TRUE; + if (EQUAL(capabilities, OLCFastSpatialFilter)) + return !geomColumns_.empty(); + if (EQUAL(capabilities, OLCFastGetExtent)) + return !geomColumns_.empty(); + if (EQUAL(capabilities, OLCCreateField)) + return updateMode_; + if (EQUAL(capabilities, OLCCreateGeomField) + || EQUAL(capabilities, ODsCCreateGeomFieldAfterCreateLayer)) + return updateMode_; + if (EQUAL(capabilities, OLCDeleteField)) + return updateMode_; + if (EQUAL(capabilities, OLCDeleteFeature)) + return updateMode_ && fidFieldIndex_ != OGRNullFID; + if (EQUAL(capabilities, OLCAlterFieldDefn)) + return updateMode_; + if (EQUAL(capabilities, OLCRandomWrite)) + return updateMode_; + if (EQUAL(capabilities, OLCMeasuredGeometries)) + return TRUE; + if (EQUAL(capabilities, OLCSequentialWrite)) + return updateMode_; + if (EQUAL(capabilities, OLCTransactions)) + return updateMode_; + if (EQUAL(capabilities, OLCStringsAsUTF8)) + return TRUE; + + return FALSE; +} + +/************************************************************************/ +/* ICreateFeature() */ +/************************************************************************/ + +OGRErr OGRHanaTableLayer::ICreateFeature(OGRFeature* feature) +{ + if (!updateMode_) + { + CPLError( + CE_Failure, CPLE_NotSupported, UNSUPPORTED_OP_READ_ONLY, + "CreateFeature"); + return OGRERR_FAILURE; + } + + if( nullptr == feature ) + { + CPLError( CE_Failure, CPLE_AppDefined, + "NULL pointer to OGRFeature passed to CreateFeature()." ); + return OGRERR_FAILURE; + } + + GIntBig nFID = feature->GetFID(); + bool withFID = nFID != OGRNullFID; + bool withBatch = withFID && dataSource_->IsTransactionStarted(); + + try + { + odbc::PreparedStatementRef& stmt = withFID ? insertFeatureStmtWithFID_ : insertFeatureStmtWithoutFID_; + + if (stmt.isNull()) + { + stmt = CreateInsertFeatureStatement(withFID); + if (stmt.isNull()) + return OGRERR_FAILURE; + } + + OGRErr err = SetStatementParameters(*stmt, feature, true, withFID, "CreateFeature"); + + if (OGRERR_NONE != err) + return err; + + if (withBatch) + stmt->addBatch(); + + auto ret = ExecuteUpdate(*stmt, withBatch, "CreateFeature"); + + err = ret.first; + if (OGRERR_NONE != err) + return err; + + if (!withFID) + { + const CPLString sql = CPLString().Printf( + "SELECT CURRENT_IDENTITY_VALUE() \"current identity value\" FROM %s", + GetFullTableNameQuoted(schemaName_, tableName_).c_str()); + + if (currentIdentityValueStmt_.isNull()) + currentIdentityValueStmt_ = dataSource_->PrepareStatement(sql.c_str()); + + odbc::ResultSetRef rsIdentity = currentIdentityValueStmt_->executeQuery(); + if ( rsIdentity->next() ) + { + odbc::Long id = rsIdentity->getLong( 1 ); + if ( !id.isNull() ) + feature->SetFID(static_cast( *id ) ); + } + rsIdentity->close(); + } + + return err; + } + catch (const std::exception& ex) + { + CPLError( CE_Failure, CPLE_NotSupported, + "Unable to create feature: %s", ex.what()); + return OGRERR_FAILURE; + } + return OGRERR_NONE; +} + +/************************************************************************/ +/* DeleteFeature() */ +/************************************************************************/ + +OGRErr OGRHanaTableLayer::DeleteFeature(GIntBig nFID) +{ + if (!updateMode_) + { + CPLError( + CE_Failure, CPLE_NotSupported, UNSUPPORTED_OP_READ_ONLY, + "DeleteFeature"); + return OGRERR_FAILURE; + } + + if (nFID == OGRNullFID) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "DeleteFeature(" CPL_FRMT_GIB + ") failed. Unable to delete features " + "in tables without\n a recognised FID column.", + nFID); + return OGRERR_FAILURE; + } + + if (OGRNullFID == fidFieldIndex_) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "DeleteFeature(" CPL_FRMT_GIB + ") failed. Unable to delete features " + "in tables without\n a recognised FID column.", + nFID); + return OGRERR_FAILURE; + } + + if (deleteFeatureStmt_.isNull()) + { + deleteFeatureStmt_ = CreateDeleteFeatureStatement(); + if (deleteFeatureStmt_.isNull()) + return OGRERR_FAILURE; + } + + deleteFeatureStmt_->setLong(1, odbc::Long(static_cast(nFID))); + bool withBatch = dataSource_->IsTransactionStarted(); + if (withBatch) + deleteFeatureStmt_->addBatch(); + + auto ret = ExecuteUpdate(*deleteFeatureStmt_, withBatch, "DeleteFeature"); + return (OGRERR_NONE == ret.first && ret.second != 1) + ? OGRERR_NON_EXISTING_FEATURE + : ret.first; +} + +/************************************************************************/ +/* ISetFeature() */ +/************************************************************************/ + +OGRErr OGRHanaTableLayer::ISetFeature(OGRFeature* feature) +{ + if (!updateMode_) + { + CPLError( + CE_Failure, CPLE_NotSupported, UNSUPPORTED_OP_READ_ONLY, + "SetFeature"); + return OGRERR_FAILURE; + } + + if( nullptr == feature ) + { + CPLError( CE_Failure, CPLE_AppDefined, + "NULL pointer to OGRFeature passed to SetFeature()." ); + return OGRERR_FAILURE; + } + + if (feature->GetFID() == OGRNullFID) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "FID required on features given to SetFeature()."); + return OGRERR_FAILURE; + } + + if (OGRNullFID == fidFieldIndex_) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "Unable to update features in tables without\n" + "a recognised FID column."); + return OGRERR_FAILURE; + } + + if (updateFeatureStmt_.isNull()) + { + updateFeatureStmt_ = CreateUpdateFeatureStatement(); + if (updateFeatureStmt_.isNull()) + return OGRERR_FAILURE; + } + + try + { + OGRErr err = SetStatementParameters( + *updateFeatureStmt_, feature, false, false, "SetFeature"); + + if (OGRERR_NONE != err) + return err; + + bool withBatch = dataSource_->IsTransactionStarted(); + if (withBatch) + updateFeatureStmt_->addBatch(); + + auto ret = ExecuteUpdate(*updateFeatureStmt_, withBatch, "SetFeature"); + return (OGRERR_NONE == ret.first && ret.second != 1) + ? OGRERR_NON_EXISTING_FEATURE + : ret.first; + } + catch (const std::exception& ex) + { + CPLError(CE_Failure, CPLE_NotSupported, + "Unable to create feature: %s", ex.what()); + return OGRERR_FAILURE; + } +} + +/************************************************************************/ +/* CreateField() */ +/************************************************************************/ + +OGRErr OGRHanaTableLayer::CreateField(OGRFieldDefn* srsField, int approxOK) +{ + if (!updateMode_) + { + CPLError( + CE_Failure, CPLE_NotSupported, UNSUPPORTED_OP_READ_ONLY, + "CreateField"); + return OGRERR_FAILURE; + } + + OGRFieldDefn dstField(srsField); + + if (launderColumnNames_) + { + CPLString launderName = LaunderName(dstField.GetNameRef()); + dstField.SetName(launderName.c_str()); + } + + if (fidFieldIndex_ != OGRNullFID + && EQUAL(dstField.GetNameRef(), GetFIDColumn()) + && dstField.GetType() != OFTInteger + && dstField.GetType() != OFTInteger64) + { + CPLError( + CE_Failure, CPLE_AppDefined, "Wrong field type for %s", + dstField.GetNameRef()); + return OGRERR_FAILURE; + } + + ColumnTypeInfo columnTypeInfo = GetColumnTypeInfo(dstField); + CPLString columnDef = GetColumnDefinition(columnTypeInfo); + + if (columnTypeInfo.type == odbc::SQLDataTypes::Unknown) + { + if (columnTypeInfo.name.empty()) + return OGRERR_FAILURE; + + if (approxOK) + { + dstField.SetDefault(nullptr); + CPLError( + CE_Warning, CPLE_NotSupported, + "Unable to create field %s with type %s on HANA layers. " + "Creating as VARCHAR.", + dstField.GetNameRef(), + OGRFieldDefn::GetFieldTypeName(dstField.GetType())); + columnTypeInfo.name = "VARCHAR"; + columnTypeInfo.width = static_cast(defaultStringSize_); + columnDef = "VARCHAR(" + std::to_string(defaultStringSize_) + ")"; + } + else + { + CPLError( + CE_Failure, CPLE_NotSupported, + "Unable to create field %s with type %s on HANA layers.", + dstField.GetNameRef(), + OGRFieldDefn::GetFieldTypeName(dstField.GetType())); + + return OGRERR_FAILURE; + } + } + + CPLString clmClause = + QuotedIdentifier(dstField.GetNameRef()) + " " + columnDef; + if (!dstField.IsNullable()) + clmClause += " NOT NULL"; + if (dstField.GetDefault() != nullptr && !dstField.IsDefaultDriverSpecific()) + { + if (IsArrayField(dstField.GetType()) + || columnTypeInfo.type == odbc::SQLDataTypes::LongVarBinary) + { + CPLError( + CE_Failure, CPLE_NotSupported, + "Default value cannot be created on column of data type %s: " + "%s.", + columnTypeInfo.name.c_str(), dstField.GetNameRef()); + + return OGRERR_FAILURE; + } + + clmClause += + CPLString().Printf(" DEFAULT %s", GetColumnDefaultValue(dstField)); + } + + const CPLString sql = CPLString().Printf( + "ALTER TABLE %s ADD(%s)", + GetFullTableNameQuoted(schemaName_, tableName_).c_str(), + clmClause.c_str()); + + try + { + dataSource_->ExecuteSQL(sql.c_str()); + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "Failed to execute create attribute field %s: %s", + dstField.GetNameRef(), ex.what()); + return OGRERR_FAILURE; + } + + // columnTypeInfo might contain a different defintion due to custom column types + SetFieldDefn(dstField, columnTypeInfo); + + AttributeColumnDescription clmDesc; + clmDesc.name = dstField.GetNameRef(); + clmDesc.type = columnTypeInfo.type; + clmDesc.typeName = columnTypeInfo.name; + clmDesc.isArray = IsArrayField(dstField.GetType()); + clmDesc.length = columnTypeInfo.width; + clmDesc.isNullable = dstField.IsNullable(); + clmDesc.isAutoIncrement = false; // TODO + clmDesc.scale = static_cast(columnTypeInfo.precision); + clmDesc.precision = static_cast(columnTypeInfo.width); + if (dstField.GetDefault() != nullptr) + clmDesc.defaultValue = dstField.GetDefault(); + + attrColumns_.push_back(clmDesc); + featureDefn_->AddFieldDefn(&dstField); + + rebuildQueryStatement_ = true; + ResetPreparedStatements(); + ResetReading(); + + return OGRERR_NONE; +} + +/************************************************************************/ +/* CreateGeomField() */ +/************************************************************************/ + +OGRErr OGRHanaTableLayer::CreateGeomField(OGRGeomFieldDefn* geomField, int) +{ + if (!updateMode_) + { + CPLError( + CE_Failure, CPLE_AppDefined, UNSUPPORTED_OP_READ_ONLY, + "CreateGeomField"); + return OGRERR_FAILURE; + } + + if (!IsGeometryTypeSupported(geomField->GetType())) + { + CPLError( + CE_Failure, CPLE_NotSupported, + "Geometry field '%s' in layer '%s' has unsupported type %s", + geomField->GetNameRef(), tableName_.c_str(), + OGRGeometryTypeToName(geomField->GetType())); + return OGRERR_FAILURE; + } + + if (featureDefn_->GetGeomFieldIndex(geomField->GetNameRef()) >= 0) + { + CPLError(CE_Failure, CPLE_AppDefined, + "CreateGeomField() called with an already existing field name: %s", + geomField->GetNameRef()); + return OGRERR_FAILURE; + } + + int srid = dataSource_->GetSrsId(geomField->GetSpatialRef()); + if (srid == UNDETERMINED_SRID) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Unable to determine the srs-id for field name: %s", + geomField->GetNameRef()); + return OGRERR_FAILURE; + } + + CPLString clmName(launderColumnNames_ + ? LaunderName(geomField->GetNameRef()).c_str() + : geomField->GetNameRef()); + + if (clmName.empty()) + clmName = FindGeomFieldName(*featureDefn_); + + CPLString sql = CPLString().Printf( + "ALTER TABLE %s ADD(%s ST_GEOMETRY(%d))", + GetFullTableNameQuoted(schemaName_, tableName_).c_str(), + QuotedIdentifier(clmName).c_str(), srid); + + try + { + dataSource_->ExecuteSQL(sql.c_str()); + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "Failed to execute CreateGeomField() with field name '%s': %s", + geomField->GetNameRef(), ex.what()); + return OGRERR_FAILURE; + } + + auto newGeomField = cpl::make_unique( + clmName.c_str(), geomField->GetType()); + newGeomField->SetNullable(geomField->IsNullable()); + newGeomField->SetSpatialRef(geomField->GetSpatialRef()); + geomColumns_.push_back( + {newGeomField->GetNameRef(), newGeomField->GetType(), + srid, newGeomField->IsNullable() == TRUE}); + featureDefn_->AddGeomFieldDefn(std::move(newGeomField)); + + ResetPreparedStatements(); + + return OGRERR_NONE; +} + +/************************************************************************/ +/* DeleteField() */ +/************************************************************************/ + +OGRErr OGRHanaTableLayer::DeleteField(int field) +{ + if (!updateMode_) + { + CPLError( + CE_Failure, CPLE_NotSupported, UNSUPPORTED_OP_READ_ONLY, + "DeleteField"); + return OGRERR_FAILURE; + } + + if (field < 0 || field >= featureDefn_->GetFieldCount()) + { + CPLError(CE_Failure, CPLE_NotSupported, "Field index is out of range"); + return OGRERR_FAILURE; + } + + CPLString clmName = featureDefn_->GetFieldDefn(field)->GetNameRef(); + CPLString sql = CPLString().Printf( + "ALTER TABLE %s DROP (%s)", + GetFullTableNameQuoted(schemaName_, tableName_).c_str(), + QuotedIdentifier(clmName).c_str()); + + try + { + dataSource_->ExecuteSQL(sql.c_str()); + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, "Failed to drop column %s: %s", + clmName.c_str(), ex.what()); + return OGRERR_FAILURE; + } + + auto it = std::find_if( + attrColumns_.begin(), attrColumns_.end(), + [&](const AttributeColumnDescription& cd) { + return cd.name == clmName; + }); + attrColumns_.erase(it); + OGRErr ret = featureDefn_->DeleteFieldDefn(field); + + ResetPreparedStatements(); + + return ret; +} + +/************************************************************************/ +/* AlterFieldDefn() */ +/************************************************************************/ + +OGRErr OGRHanaTableLayer::AlterFieldDefn( + int field, OGRFieldDefn* newFieldDefn, int flagsIn) +{ + if (!updateMode_) + { + CPLError( + CE_Failure, CPLE_NotSupported, UNSUPPORTED_OP_READ_ONLY, + "AlterFieldDefn"); + return OGRERR_FAILURE; + } + + if (field < 0 || field >= featureDefn_->GetFieldCount()) + { + CPLError(CE_Failure, CPLE_NotSupported, "Field index is out of range"); + return OGRERR_FAILURE; + } + + OGRFieldDefn* fieldDefn = featureDefn_->GetFieldDefn(field); + + int64_t columnDescIdx = -1; + for (size_t i = 0; i < attrColumns_.size(); ++i) + { + if (EQUAL(attrColumns_[i].name.c_str(), fieldDefn->GetNameRef())) + { + columnDescIdx = i; + break; + } + } + + if (columnDescIdx < 0) + { + CPLError(CE_Failure, CPLE_NotSupported, "Column description cannot be found"); + return OGRERR_FAILURE; + } + + CPLString clmName = launderColumnNames_ + ? LaunderName(newFieldDefn->GetNameRef()) + : CPLString(newFieldDefn->GetNameRef()); + + ColumnTypeInfo columnTypeInfo = GetColumnTypeInfo(*newFieldDefn); + + try + { + if ((flagsIn & ALTER_NAME_FLAG) + && (strcmp(fieldDefn->GetNameRef(), newFieldDefn->GetNameRef()) + != 0)) + { + CPLString sql = CPLString().Printf( + "RENAME COLUMN %s TO %s", + GetFullColumnNameQuoted( + schemaName_, tableName_, fieldDefn->GetNameRef()) + .c_str(), + QuotedIdentifier(clmName).c_str()); + dataSource_->ExecuteSQL(sql.c_str()); + } + + if ((flagsIn & ALTER_TYPE_FLAG) + || (flagsIn & ALTER_WIDTH_PRECISION_FLAG) + || (flagsIn & ALTER_NULLABLE_FLAG) + || (flagsIn & ALTER_DEFAULT_FLAG)) + { + CPLString fieldTypeDef = GetColumnDefinition(columnTypeInfo); + if ((flagsIn & ALTER_NULLABLE_FLAG) + && fieldDefn->IsNullable() != newFieldDefn->IsNullable()) + { + if (fieldDefn->IsNullable()) + fieldTypeDef += " NULL"; + else + fieldTypeDef += " NOT NULL"; + } + + if ((flagsIn & ALTER_DEFAULT_FLAG) + && ((fieldDefn->GetDefault() == nullptr + && newFieldDefn->GetDefault() != nullptr) + || (fieldDefn->GetDefault() != nullptr + && newFieldDefn->GetDefault() == nullptr) + || (fieldDefn->GetDefault() != nullptr + && newFieldDefn->GetDefault() != nullptr + && strcmp( + fieldDefn->GetDefault(), + newFieldDefn->GetDefault()) + != 0))) + { + fieldTypeDef += + " DEFAULT " + + ((fieldDefn->GetType() == OFTString) + ? Literal(newFieldDefn->GetDefault()) + : CPLString(newFieldDefn->GetDefault())); + } + + CPLString sql = CPLString().Printf( + "ALTER TABLE %s ALTER(%s %s)", + GetFullTableNameQuoted(schemaName_, tableName_).c_str(), + QuotedIdentifier(clmName).c_str(), fieldTypeDef.c_str()); + + dataSource_->ExecuteSQL(sql.c_str()); + } + } + catch (const odbc::Exception& ex) + { + CPLError( + CE_Failure, CPLE_AppDefined, "Failed to alter field %s: %s", + fieldDefn->GetNameRef(), ex.what()); + return OGRERR_FAILURE; + } + + // Update field definition and column description + + AttributeColumnDescription& attrClmDesc = attrColumns_[columnDescIdx]; + + if (flagsIn & ALTER_NAME_FLAG) + { + fieldDefn->SetName(clmName.c_str()); + attrClmDesc.name.assign(clmName.c_str()); + } + + if (flagsIn & ALTER_TYPE_FLAG) + { + fieldDefn->SetSubType(OFSTNone); + fieldDefn->SetType(newFieldDefn->GetType()); + fieldDefn->SetSubType(newFieldDefn->GetSubType()); + attrClmDesc.isArray = IsArrayField(newFieldDefn->GetType()); + attrClmDesc.type = columnTypeInfo.type; + attrClmDesc.typeName = columnTypeInfo.name; + } + + if (flagsIn & ALTER_WIDTH_PRECISION_FLAG) + { + fieldDefn->SetWidth(newFieldDefn->GetWidth()); + fieldDefn->SetPrecision(newFieldDefn->GetPrecision()); + attrClmDesc.length = newFieldDefn->GetWidth(); + attrClmDesc.scale = newFieldDefn->GetWidth(); + attrClmDesc.precision = newFieldDefn->GetPrecision(); + } + + if (flagsIn & ALTER_NULLABLE_FLAG) + { + fieldDefn->SetNullable(newFieldDefn->IsNullable()); + attrClmDesc.isNullable = newFieldDefn->IsNullable(); + } + + if (flagsIn & ALTER_DEFAULT_FLAG) + { + fieldDefn->SetDefault(newFieldDefn->GetDefault()); + attrClmDesc.name.assign(newFieldDefn->GetDefault()); + } + + rebuildQueryStatement_ = true; + ResetReading(); + ResetPreparedStatements(); + + return OGRERR_NONE; +} + +/************************************************************************/ +/* ClearBatches() */ +/************************************************************************/ + +void OGRHanaTableLayer::ClearBatches() +{ + if (!insertFeatureStmtWithFID_.isNull()) + insertFeatureStmtWithFID_->clearBatch(); + if (!insertFeatureStmtWithoutFID_.isNull()) + insertFeatureStmtWithoutFID_->clearBatch(); + if (!updateFeatureStmt_.isNull()) + updateFeatureStmt_->clearBatch(); +} + +/************************************************************************/ +/* SetCustomColumnTypes() */ +/************************************************************************/ + +void OGRHanaTableLayer::SetCustomColumnTypes(const char* columnTypes) +{ + if (columnTypes == nullptr) + return; + + const char* ptr = columnTypes; + const char* start = ptr; + while (*ptr != '\0') + { + if (*ptr == '(') + { + // Skip commas inside brackets, for example decimal(20,5) + while (*ptr != '\0' && *ptr != ')') + { + ++ptr; + } + } + + ++ptr; + + if (*ptr == ',' || *ptr == '\0') + { + std::size_t len = static_cast(ptr - start); + const char* sep = std::find(start, start + len, '='); + if (sep != nullptr) + { + std::size_t pos = static_cast(sep - start); + customColumnDefs_.push_back( + {CPLString(start, pos), + CPLString(start + pos + 1, len - pos - 1)}); + } + + start = ptr + 1; + } + } +} + +/************************************************************************/ +/* StartTransaction() */ +/************************************************************************/ + +OGRErr OGRHanaTableLayer::StartTransaction() +{ + return dataSource_->StartTransaction(); +} + +/************************************************************************/ +/* CommitTransaction() */ +/************************************************************************/ + +OGRErr OGRHanaTableLayer::CommitTransaction() +{ + try + { + if (!deleteFeatureStmt_.isNull() + && deleteFeatureStmt_->getBatchDataSize() > 0) + deleteFeatureStmt_->executeBatch(); + if (!insertFeatureStmtWithFID_.isNull() + && insertFeatureStmtWithFID_->getBatchDataSize() > 0) + insertFeatureStmtWithFID_->executeBatch(); + if (!insertFeatureStmtWithoutFID_.isNull() + && insertFeatureStmtWithoutFID_->getBatchDataSize() > 0) + insertFeatureStmtWithoutFID_->executeBatch(); + if (!updateFeatureStmt_.isNull() + && updateFeatureStmt_->getBatchDataSize() > 0) + updateFeatureStmt_->executeBatch(); + + ClearBatches(); + } + catch (const odbc::Exception& ex) + { + ClearBatches(); + CPLError( + CE_Failure, CPLE_AppDefined, "Failed to execute batch insert: %s", + ex.what()); + return OGRERR_FAILURE; + } + + dataSource_->CommitTransaction(); + return OGRERR_NONE; +} + +/************************************************************************/ +/* RollbackTransaction() */ +/************************************************************************/ + +OGRErr OGRHanaTableLayer::RollbackTransaction() +{ + ClearBatches(); + return dataSource_->RollbackTransaction(); +} + +} /* end of OGRHANA namespace */ diff --git a/ogr/ogrsf_frmts/hana/ogrhanautils.cpp b/ogr/ogrsf_frmts/hana/ogrhanautils.cpp new file mode 100644 index 000000000000..285020ecba48 --- /dev/null +++ b/ogr/ogrsf_frmts/hana/ogrhanautils.cpp @@ -0,0 +1,186 @@ +/****************************************************************************** + * + * Project: SAP HANA Spatial Driver + * Purpose: OGRHanaUtils class implementation + * Author: Maxim Rylov + * + ****************************************************************************** + * Copyright (c) 2020, SAP SE + * + * 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. + ****************************************************************************/ + +#include "ogrhanautils.h" + +#include + +CPL_CVSID("$Id$") + +namespace OGRHANA { + +const char* SkipLeadingSpaces(const char* value) +{ + while (*value == ' ') + value++; + return value; +} + +CPLString JoinStrings( + const std::vector& strs, + const char* delimiter, + CPLString (*decorator)(const CPLString& str)) +{ + CPLString ret; + for (std::size_t i = 0; i < strs.size(); ++i) + { + ret += ((decorator != nullptr) ? decorator(strs[i]) : strs[i]); + if (i != strs.size() - 1) + ret += delimiter; + } + return ret; +} + +std::vector SplitStrings(const char* str, const char* delimiter) +{ + std::vector ret; + if (str != nullptr) + { + char** items = CSLTokenizeString2(str, delimiter, CSLT_HONOURSTRINGS); + for (int i = 0; items[i] != nullptr; ++i) + { + CPLString item(items[i]); + ret.push_back(item.Trim()); + } + + CSLDestroy(items); + } + + return ret; +} + +CPLString GetFullTableName( + const CPLString& schemaName, const CPLString& tableName) +{ + if (schemaName.empty()) + return tableName; + return schemaName + "." + tableName; +} + +CPLString GetFullTableNameQuoted( + const CPLString& schemaName, const CPLString& tableName) +{ + if (schemaName.empty()) + return QuotedIdentifier(tableName); + return QuotedIdentifier(schemaName) + "." + QuotedIdentifier(tableName); +} + +CPLString GetFullColumnNameQuoted( + const CPLString& schemaName, + const CPLString& tableName, + const CPLString& columnName) +{ + return GetFullTableNameQuoted(schemaName, tableName) + "." + + QuotedIdentifier(columnName); +} + +CPLString LaunderName(const char* name) +{ + if (name == nullptr) + return nullptr; + + CPLString newName(name); + for (std::size_t i = 0; newName[i] != '\0'; ++i) + { + char c = static_cast(toupper(newName[i])); + if (c == '-' || c == '#') + c = '_'; + newName[i] = c; + } + return newName; +} + +CPLString Literal(const CPLString& value) +{ + CPLString ret("'"); + char* tmp = CPLEscapeString(value, -1, CPLES_SQL); + ret += tmp; + CPLFree(tmp); + ret += "'"; + return ret; +} + +CPLString QuotedIdentifier(const CPLString& value) +{ + return "\"" + value + "\""; +} + +bool IsArrayField(OGRFieldType fieldType) +{ + return ( + fieldType == OFTIntegerList || fieldType == OFTInteger64List + || fieldType == OFTRealList || fieldType == OFTStringList || fieldType == OFTWideStringList); +} + +bool IsGeometryTypeSupported(OGRwkbGeometryType wkbType) +{ + switch (wkbFlatten(wkbType)) + { + case OGRwkbGeometryType::wkbPoint: + case OGRwkbGeometryType::wkbLineString: + case OGRwkbGeometryType::wkbPolygon: + case OGRwkbGeometryType::wkbMultiPoint: + case OGRwkbGeometryType::wkbMultiLineString: + case OGRwkbGeometryType::wkbMultiPolygon: + case OGRwkbGeometryType::wkbCircularString: + case OGRwkbGeometryType::wkbGeometryCollection: + return true; + default: + return false; + } +} + +OGRwkbGeometryType ToWkbType(const char* type, bool hasZ, bool hasM) +{ + if (strcmp(type, "ST_POINT") == 0) + return OGR_GT_SetModifier(OGRwkbGeometryType::wkbPoint, hasZ, hasM); + else if (strcmp(type, "ST_MULTIPOINT") == 0) + return OGR_GT_SetModifier(OGRwkbGeometryType::wkbMultiPoint, hasZ, hasM); + else if (strcmp(type, "ST_LINESTRING") == 0) + return OGR_GT_SetModifier(OGRwkbGeometryType::wkbLineString, hasZ, hasM); + else if (strcmp(type, "ST_MULTILINESTRING") == 0) + return OGR_GT_SetModifier(OGRwkbGeometryType::wkbMultiLineString, hasZ, hasM); + else if (strcmp(type, "ST_POLYGON") == 0) + return OGR_GT_SetModifier(OGRwkbGeometryType::wkbPolygon, hasZ, hasM); + else if (strcmp(type, "ST_MULTIPOLYGON") == 0) + return OGR_GT_SetModifier(OGRwkbGeometryType::wkbMultiPolygon, hasZ, hasM); + else if (strcmp(type, "ST_CIRCULARSTRING") == 0) + return OGR_GT_SetModifier(OGRwkbGeometryType::wkbCircularString, hasZ, hasM); + else if (strcmp(type, "ST_GEOMETRYCOLLECTION") == 0) + return OGR_GT_SetModifier(OGRwkbGeometryType::wkbGeometryCollection, hasZ, hasM); + return OGRwkbGeometryType::wkbUnknown; +} + +constexpr int PLANAR_SRID_OFFSET = 1000000000; + +int ToPlanarSRID(int srid) +{ + return srid < PLANAR_SRID_OFFSET ? PLANAR_SRID_OFFSET + srid : srid; +} + +} // namespace hana_utils diff --git a/ogr/ogrsf_frmts/hana/ogrhanautils.h b/ogr/ogrsf_frmts/hana/ogrhanautils.h new file mode 100644 index 000000000000..e14ea4d20a46 --- /dev/null +++ b/ogr/ogrsf_frmts/hana/ogrhanautils.h @@ -0,0 +1,70 @@ +/****************************************************************************** + * + * Project: SAP HANA Spatial Driver + * Purpose: OGRHanaUtils class declaration + * Author: Maxim Rylov + * + ****************************************************************************** + * Copyright (c) 2020, SAP SE + * + * 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. + ****************************************************************************/ + +#ifndef OGRHANAUTILS_H_INCLUDED +#define OGRHANAUTILS_H_INCLUDED + +#include "ogr_core.h" +#include "ogr_feature.h" + +#include "cpl_string.h" + +#include +#include + +namespace OGRHANA { + +constexpr const char* ARRAY_VALUES_DELIMITER = "^%^"; + +const char* SkipLeadingSpaces(const char* value); +CPLString JoinStrings( + const std::vector& strs, + const char* delimiter, + CPLString (*decorator)(const CPLString& str) = nullptr); +std::vector SplitStrings(const char* str, const char* delimiter); + +CPLString GetFullColumnNameQuoted( + const CPLString& schemaName, + const CPLString& tableName, + const CPLString& columnName); +CPLString GetFullTableName( + const CPLString& schemaName, const CPLString& tableName); +CPLString GetFullTableNameQuoted( + const CPLString& schemaName, const CPLString& tableName); +CPLString LaunderName(const char* name); +CPLString Literal(const CPLString& value); +CPLString QuotedIdentifier(const CPLString& value); + +bool IsArrayField(OGRFieldType fieldType); +bool IsGeometryTypeSupported(OGRwkbGeometryType wkbType); +OGRwkbGeometryType ToWkbType(const char* type, bool hasZ, bool hasM); +int ToPlanarSRID(int srid); + +} /* end of OGRHANA namespace */ + +#endif /* ndef OGRHANAUTILS_H_INCLUDED */ diff --git a/ogr/ogrsf_frmts/ogrsf_frmts.h b/ogr/ogrsf_frmts/ogrsf_frmts.h index e8c196c8f5a0..343be7ef142b 100644 --- a/ogr/ogrsf_frmts/ogrsf_frmts.h +++ b/ogr/ogrsf_frmts/ogrsf_frmts.h @@ -550,6 +550,7 @@ void CPL_DLL RegisterOGRMVT(); void CPL_DLL RegisterOGRNGW(); void CPL_DLL RegisterOGRMapML(); void CPL_DLL RegisterOGRLVBAG(); +void CPL_DLL RegisterOGRHANA(); // @endcond CPL_C_END