From a6639d965b7f5cfa7e0d2cf19144bf67e4218d46 Mon Sep 17 00:00:00 2001 From: Maxim Rylov Date: Fri, 12 Apr 2024 08:15:27 +0200 Subject: [PATCH] HANA: Add support for REAL_VECTOR type --- src/providers/hana/CMakeLists.txt | 1 + src/providers/hana/qgshanaconnection.cpp | 93 +++++++++++--------- src/providers/hana/qgshanaconnection.h | 2 - src/providers/hana/qgshanadatatypes.h | 91 ++++++++++++++++++++ src/providers/hana/qgshanaprovider.cpp | 81 ++++++++++-------- src/providers/hana/qgshanaresultset.cpp | 104 ++++++++++++++++------- src/providers/hana/qgshanautils.cpp | 30 +++++-- tests/src/python/test_hana_utils.py | 3 +- tests/src/python/test_provider_hana.py | 102 +++++++++++++++++++++- 9 files changed, 387 insertions(+), 120 deletions(-) create mode 100644 src/providers/hana/qgshanadatatypes.h diff --git a/src/providers/hana/CMakeLists.txt b/src/providers/hana/CMakeLists.txt index 898386b6eac1..7391f14ad485 100644 --- a/src/providers/hana/CMakeLists.txt +++ b/src/providers/hana/CMakeLists.txt @@ -34,6 +34,7 @@ set(HANA_HDRS qgshanaconnection.h qgshanaconnectionpool.h qgshanaconnectionstringbuilder.h + qgshanadatatypes.h qgshanadriver.h qgshanaexception.h qgshanaexpressioncompiler.h diff --git a/src/providers/hana/qgshanaconnection.cpp b/src/providers/hana/qgshanaconnection.cpp index bdf4564200d2..4d35c54bb0e7 100644 --- a/src/providers/hana/qgshanaconnection.cpp +++ b/src/providers/hana/qgshanaconnection.cpp @@ -18,6 +18,7 @@ #include "qgsdatasourceuri.h" #include "qgshanaconnection.h" #include "qgshanaconnectionstringbuilder.h" +#include "qgshanadatatypes.h" #include "qgshanadriver.h" #include "qgshanaexception.h" #include "qgshanaresultset.h" @@ -99,62 +100,66 @@ QgsField AttributeField::toQgsField() const QVariant::Type fieldType; switch ( type ) { - case SQLDataTypes::Bit: - case SQLDataTypes::Boolean: + case QgsHanaDataTypes::Bit: + case QgsHanaDataTypes::Boolean: fieldType = QVariant::Bool; break; - case SQLDataTypes::TinyInt: - case SQLDataTypes::SmallInt: - case SQLDataTypes::Integer: + case QgsHanaDataTypes::TinyInt: + case QgsHanaDataTypes::SmallInt: + case QgsHanaDataTypes::Integer: fieldType = isSigned ? QVariant::Int : QVariant::UInt; break; - case SQLDataTypes::BigInt: + case QgsHanaDataTypes::BigInt: fieldType = isSigned ? QVariant::LongLong : QVariant::ULongLong; break; - case SQLDataTypes::Numeric: - case SQLDataTypes::Decimal: + case QgsHanaDataTypes::Numeric: + case QgsHanaDataTypes::Decimal: fieldType = QVariant::Double; break; - case SQLDataTypes::Double: - case SQLDataTypes::Float: - case SQLDataTypes::Real: + case QgsHanaDataTypes::Double: + case QgsHanaDataTypes::Float: + case QgsHanaDataTypes::Real: fieldType = QVariant::Double; break; - case SQLDataTypes::Char: - case SQLDataTypes::WChar: + case QgsHanaDataTypes::Char: + case QgsHanaDataTypes::WChar: fieldType = ( size == 1 ) ? QVariant::Char : QVariant::String; break; - case SQLDataTypes::VarChar: - case SQLDataTypes::WVarChar: - case SQLDataTypes::LongVarChar: - case SQLDataTypes::WLongVarChar: + case QgsHanaDataTypes::VarChar: + case QgsHanaDataTypes::WVarChar: + case QgsHanaDataTypes::LongVarChar: + case QgsHanaDataTypes::WLongVarChar: fieldType = QVariant::String; break; - case SQLDataTypes::Binary: - case SQLDataTypes::VarBinary: - case SQLDataTypes::LongVarBinary: + case QgsHanaDataTypes::Binary: + case QgsHanaDataTypes::VarBinary: + case QgsHanaDataTypes::LongVarBinary: fieldType = QVariant::ByteArray; break; - case SQLDataTypes::Date: - case SQLDataTypes::TypeDate: + case QgsHanaDataTypes::Date: + case QgsHanaDataTypes::TypeDate: fieldType = QVariant::Date; break; - case SQLDataTypes::Time: - case SQLDataTypes::TypeTime: + case QgsHanaDataTypes::Time: + case QgsHanaDataTypes::TypeTime: fieldType = QVariant::Time; break; - case SQLDataTypes::Timestamp: - case SQLDataTypes::TypeTimestamp: + case QgsHanaDataTypes::Timestamp: + case QgsHanaDataTypes::TypeTimestamp: fieldType = QVariant::DateTime; break; + case QgsHanaDataTypes::Geometry: + // There are two options how to treat ST_GEOMETRY columns that are attributes: + // 1. Type is QVariant::String. The value is provided as WKT and editable. + // 2. Type is QVariant::ByteArray. The value is provided as BLOB and uneditable. + fieldType = QVariant::String; + break; + case QgsHanaDataTypes::RealVector: + // Controls how REAL_VECTOR type is treated, either as QVariant::ByteArray or QVariant::String. + fieldType = QVariant::String; + break; default: - if ( isGeometry() ) - // There are two options how to treat geometry columns that are attributes: - // 1. Type is QVariant::String. The value is provided as WKT and editable. - // 2. Type is QVariant::ByteArray. The value is provided as BLOB and uneditable. - fieldType = QVariant::String; - else - throw QgsHanaException( QString( "Field type '%1' is not supported" ).arg( QString::number( type ) ) ); + throw QgsHanaException( QString( "Field type '%1' is not supported" ).arg( QString::number( type ) ) ); break; } @@ -168,6 +173,10 @@ QgsField AttributeField::toQgsField() const constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider ); field.setConstraints( constraints ); } + + if ( type == QgsHanaDataTypes::Geometry ) + field.setMetadata( Qgis::FieldMetadataProperty::CustomProperty, srid ); + return field; } @@ -824,19 +833,19 @@ void QgsHanaConnection::readTableFields( const QString &schemaName, const QStrin field.name = rsColumns->getString( 4/*COLUMN_NAME*/ ); field.type = rsColumns->getShort( 5/*DATA_TYPE*/ ); field.typeName = rsColumns->getString( 6/*TYPE_NAME*/ ); - if ( field.type == SQLDataTypes::Unknown ) + if ( field.type == QgsHanaDataTypes::Unknown ) throw QgsHanaException( QString( "Type of the column '%1' is unknown" ).arg( field.name ) ); field.size = rsColumns->getInt( 7/*COLUMN_SIZE*/ ); field.precision = static_cast( rsColumns->getShort( 9/*DECIMAL_DIGITS*/ ) ); - field.isSigned = field.type == SQLDataTypes::SmallInt || field.type == SQLDataTypes::Integer || - field.type == SQLDataTypes::BigInt || field.type == SQLDataTypes::Decimal || - field.type == SQLDataTypes::Numeric || field.type == SQLDataTypes::Real || - field.type == SQLDataTypes::Float || field.type == SQLDataTypes::Double; + field.isSigned = field.type == QgsHanaDataTypes::SmallInt || field.type == QgsHanaDataTypes::Integer || + field.type == QgsHanaDataTypes::BigInt || field.type == QgsHanaDataTypes::Decimal || + field.type == QgsHanaDataTypes::Numeric || field.type == QgsHanaDataTypes::Real || + field.type == QgsHanaDataTypes::Float || field.type == QgsHanaDataTypes::Double; QString isNullable = rsColumns->getString( 18/*IS_NULLABLE*/ ); field.isNullable = ( isNullable == QLatin1String( "YES" ) || isNullable == QLatin1String( "TRUE" ) ); field.isAutoIncrement = isColumnAutoIncrement( field.name ); field.isUnique = isColumnUnique( field.name ); - if ( field.isGeometry() ) + if ( field.type == QgsHanaDataTypes::Geometry ) field.srid = getColumnSrid( schemaName, tableName, field.name ); field.comment = rsColumns->getString( 12/*REMARKS*/ ); @@ -912,9 +921,9 @@ QStringList QgsHanaConnection::getPrimaryKeyCandidates( const QgsHanaLayerProper while ( rsColumns->next() ) { int dataType = rsColumns->getValue( 5/*DATA_TYPE */ ).toInt(); - // We exclude GEOMETRY and LOB columns - if ( dataType == 29812 /* GEOMETRY TYPE */ || dataType == SQLDataTypes::LongVarBinary || - dataType == SQLDataTypes::LongVarChar || dataType == SQLDataTypes::WLongVarChar ) + // We exclude ST_GEOMETRY, REAL_VECTOR and LOB columns + if ( dataType == QgsHanaDataTypes::Geometry || dataType == QgsHanaDataTypes::RealVector || + dataType == QgsHanaDataTypes::LongVarBinary || dataType == QgsHanaDataTypes::LongVarChar || dataType == QgsHanaDataTypes::WLongVarChar ) continue; ret << rsColumns->getValue( 4/*COLUMN_NAME */ ).toString(); } diff --git a/src/providers/hana/qgshanaconnection.h b/src/providers/hana/qgshanaconnection.h index 89c68308774c..c2ad0b88b159 100644 --- a/src/providers/hana/qgshanaconnection.h +++ b/src/providers/hana/qgshanaconnection.h @@ -41,8 +41,6 @@ struct AttributeField bool isUnique = false; QString comment; - bool isGeometry() const { return type == 29812; /* ST_GEOMETRY, ST_POINT */ } - QgsField toQgsField() const; }; diff --git a/src/providers/hana/qgshanadatatypes.h b/src/providers/hana/qgshanadatatypes.h new file mode 100644 index 000000000000..94739fdc6a09 --- /dev/null +++ b/src/providers/hana/qgshanadatatypes.h @@ -0,0 +1,91 @@ +/*************************************************************************** + qgshanadatatypes.h + -------------------------------------- + Date : 10-04-2024 + Copyright : (C) SAP SE + Author : Maxim Rylov + ***************************************************************************/ + +/*************************************************************************** + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + ***************************************************************************/ +#ifndef QGSHANADATATYPES_H +#define QGSHANADATATYPES_H + +#include "odbc/Types.h" + +using namespace NS_ODBC; + +class QgsHanaDataTypes +{ + QgsHanaDataTypes() = delete; + + public: + /// Unknown data type. + static constexpr int Unknown = SQLDataTypes::Unknown; + /// 64-bit integer value. + static constexpr int BigInt = SQLDataTypes::BigInt; + /// Binary data of fixed length. + static constexpr int Binary = SQLDataTypes::Binary; + /// Single bit binary data. + static constexpr int Bit = SQLDataTypes::Bit; + /// Boolean value. + static constexpr int Boolean = SQLDataTypes::Boolean; + /// Character string of fixed string length. + static constexpr int Char = SQLDataTypes::Char; + /// Year, month, and day fields. + static constexpr int Date = SQLDataTypes::Date; + /// Year, month, and day fields. + static constexpr int DateTime = SQLDataTypes::DateTime; + /// Signed, exact, numeric value. + static constexpr int Decimal = SQLDataTypes::Decimal; + /// Double-precision floating point number. + static constexpr int Double = SQLDataTypes::Double; + /// Floating point number with driver-specific precision. + static constexpr int Float = SQLDataTypes::Float; + /// 32-bit integer value. + static constexpr int Integer = SQLDataTypes::Integer; + /// Variable length binary data. + static constexpr int LongVarBinary = SQLDataTypes::LongVarBinary; + /// Variable length character data. + static constexpr int LongVarChar = SQLDataTypes::LongVarChar; + /// Signed, exact, numeric value. + static constexpr int Numeric = SQLDataTypes::Numeric; + /// Single-precision floating point number. + static constexpr int Real = SQLDataTypes::Real; + /// 16-bit integer value. + static constexpr int SmallInt = SQLDataTypes::SmallInt; + /// Hour, minute, and second fields. + static constexpr int Time = SQLDataTypes::Time; + /// Year, month, day, hour, minute, and second fields. + static constexpr int Timestamp = SQLDataTypes::Timestamp; + /// 8-bit integer value. + static constexpr int TinyInt = SQLDataTypes::TinyInt; + /// Year, month, and day fields. + static constexpr int TypeDate = SQLDataTypes::TypeDate; + /// Hour, minute, and second fields. + static constexpr int TypeTime = SQLDataTypes::TypeTime; + /// Year, month, day, hour, minute, and second fields. + static constexpr int TypeTimestamp = SQLDataTypes::TypeTimestamp; + /// Variable length binary data. + static constexpr int VarBinary = SQLDataTypes::VarBinary; + /// Variable-length character string. + static constexpr int VarChar = SQLDataTypes::VarChar; + /// Unicode character string of fixed string length. + static constexpr int WChar = SQLDataTypes::WChar; + /// Unicode variable-length character data. + static constexpr int WLongVarChar = SQLDataTypes::WLongVarChar; + /// Unicode variable-length character string. + static constexpr int WVarChar = SQLDataTypes::WVarChar; + /// ST_GEOMETRY/ST_POINT value. + static constexpr int Geometry = 29812; + /// REAL_VECTOR value. + static constexpr int RealVector = 29814; +}; + +#endif // QGSHANADATATYPES_H diff --git a/src/providers/hana/qgshanaprovider.cpp b/src/providers/hana/qgshanaprovider.cpp index aed7ee195693..2c1026f49e5e 100644 --- a/src/providers/hana/qgshanaprovider.cpp +++ b/src/providers/hana/qgshanaprovider.cpp @@ -23,6 +23,7 @@ #include "qgsfields.h" #include "qgsgeometry.h" #include "qgshanaconnectionpool.h" +#include "qgshanadatatypes.h" #include "qgshanaexception.h" #include "qgshanadriver.h" #include "qgshanafeatureiterator.h" @@ -185,36 +186,36 @@ namespace switch ( field.type ) { - case SQLDataTypes::Bit: - case SQLDataTypes::Boolean: + case QgsHanaDataTypes::Bit: + case QgsHanaDataTypes::Boolean: stmt->setBoolean( paramIndex, isNull ? Boolean() : Boolean( value.toBool() ) ); break; - case SQLDataTypes::TinyInt: + case QgsHanaDataTypes::TinyInt: if ( field.isSigned ) stmt->setByte( paramIndex, isNull ? Byte() : Byte( static_cast( value.toInt() ) ) ); else stmt->setUByte( paramIndex, isNull ? UByte() : UByte( static_cast( value.toUInt() ) ) ); break; - case SQLDataTypes::SmallInt: + case QgsHanaDataTypes::SmallInt: if ( field.isSigned ) stmt->setShort( paramIndex, isNull ? Short() : Short( static_cast( value.toInt() ) ) ); else stmt->setUShort( paramIndex, isNull ? UShort() : UShort( static_cast( value.toUInt() ) ) ); break; - case SQLDataTypes::Integer: + case QgsHanaDataTypes::Integer: if ( field.isSigned ) stmt->setInt( paramIndex, isNull ? Int() : Int( value.toInt() ) ); else stmt->setUInt( paramIndex, isNull ? UInt() : UInt( value.toUInt() ) ); break; - case SQLDataTypes::BigInt: + case QgsHanaDataTypes::BigInt: if ( field.isSigned ) stmt->setLong( paramIndex, isNull ? Long() : Long( value.toLongLong() ) ); else stmt->setULong( paramIndex, isNull ? ULong() : ULong( value.toULongLong() ) ); break; - case SQLDataTypes::Numeric: - case SQLDataTypes::Decimal: + case QgsHanaDataTypes::Numeric: + case QgsHanaDataTypes::Decimal: if ( isNull ) stmt->setDouble( paramIndex, Double() ); else @@ -223,15 +224,15 @@ namespace stmt->setDouble( paramIndex, Double( dvalue ) ); } break; - case SQLDataTypes::Real: + case QgsHanaDataTypes::Real: stmt->setFloat( paramIndex, isNull ? Float() : Float( value.toFloat() ) ); break; - case SQLDataTypes::Float: - case SQLDataTypes::Double: + case QgsHanaDataTypes::Float: + case QgsHanaDataTypes::Double: stmt->setDouble( paramIndex, isNull ? Double() : Double( value.toDouble() ) ); break; - case SQLDataTypes::Date: - case SQLDataTypes::TypeDate: + case QgsHanaDataTypes::Date: + case QgsHanaDataTypes::TypeDate: if ( isNull ) stmt->setDate( paramIndex, Date() ); else @@ -240,8 +241,8 @@ namespace stmt->setDate( paramIndex, makeNullable( d.year(), d.month(), d.day() ) ); } break; - case SQLDataTypes::Time: - case SQLDataTypes::TypeTime: + case QgsHanaDataTypes::Time: + case QgsHanaDataTypes::TypeTime: if ( isNull ) stmt->setTime( paramIndex, Time() ); else @@ -250,8 +251,8 @@ namespace stmt->setTime( paramIndex, makeNullable( t.hour(), t.minute(), t.second() ) ); } break; - case SQLDataTypes::Timestamp: - case SQLDataTypes::TypeTimestamp: + case QgsHanaDataTypes::Timestamp: + case QgsHanaDataTypes::TypeTimestamp: if ( isNull ) stmt->setTimestamp( paramIndex, Timestamp() ); else @@ -263,19 +264,19 @@ namespace d.month(), d.day(), t.hour(), t.minute(), t.second(), t.msec() ) ); } break; - case SQLDataTypes::Char: - case SQLDataTypes::VarChar: - case SQLDataTypes::LongVarChar: + case QgsHanaDataTypes::Char: + case QgsHanaDataTypes::VarChar: + case QgsHanaDataTypes::LongVarChar: stmt->setString( paramIndex, isNull ? String() : String( value.toString().toStdString() ) ); break; - case SQLDataTypes::WChar: - case SQLDataTypes::WVarChar: - case SQLDataTypes::WLongVarChar: + case QgsHanaDataTypes::WChar: + case QgsHanaDataTypes::WVarChar: + case QgsHanaDataTypes::WLongVarChar: stmt->setNString( paramIndex, isNull ? NString() : NString( value.toString().toStdU16String() ) ); break; - case SQLDataTypes::Binary: - case SQLDataTypes::VarBinary: - case SQLDataTypes::LongVarBinary: + case QgsHanaDataTypes::Binary: + case QgsHanaDataTypes::VarBinary: + case QgsHanaDataTypes::LongVarBinary: if ( isNull ) stmt->setBinary( paramIndex, Binary() ); else @@ -284,20 +285,24 @@ namespace stmt->setBinary( paramIndex, Binary( vector( arr.begin(), arr.end() ) ) ); } break; - default: - if ( field.isGeometry() ) + case QgsHanaDataTypes::Geometry: + case QgsHanaDataTypes::RealVector: + if ( isNull ) + stmt->setString( paramIndex, String() ); + else { if ( value.type() == QVariant::String ) - stmt->setString( paramIndex, isNull ? String() : String( value.toString().toStdString() ) ); + stmt->setString( paramIndex, String( value.toString().toStdString() ) ); else if ( value.type() == QVariant::ByteArray ) { QByteArray arr = value.toByteArray(); - stmt->setBinary( paramIndex, isNull ? Binary() : Binary( vector( arr.begin(), arr.end() ) ) ); + stmt->setBinary( paramIndex, Binary( vector( arr.begin(), arr.end() ) ) ); } } - else - QgsDebugError( QStringLiteral( "Unknown value type ('%1') for parameter %2" ) - .arg( QString::number( field.type ), QString::number( paramIndex ) ) ); + break; + default: + QgsDebugError( QStringLiteral( "Unknown value type ('%1') for parameter %2" ) + .arg( QString::number( field.type ), QString::number( paramIndex ) ) ); break; } } @@ -673,8 +678,11 @@ bool QgsHanaProvider::addFeatures( QgsFeatureList &flist, Flags flags ) } columnNames << QgsHanaUtils::quotedIdentifier( field.name ); - if ( field.isGeometry() && mFields.at( idx ).type() == QVariant::String ) + auto qType = mFields.at( idx ).type(); + if ( field.type == QgsHanaDataTypes::Geometry && qType == QVariant::String ) values << QStringLiteral( "ST_GeomFromWKT(?, %1)" ).arg( QString::number( field.srid ) ); + else if ( field.type == QgsHanaDataTypes::RealVector && qType == QVariant::String ) + values << QStringLiteral( "TO_REAL_VECTOR(?)" ); else values << QStringLiteral( "?" ); fieldIds << idx; @@ -1169,9 +1177,12 @@ bool QgsHanaProvider::changeAttributeValues( const QgsChangedAttributesMap &attr continue; pkChanged = pkChanged || mPrimaryKeyAttrs.contains( fieldIndex ); - if ( field.isGeometry() && mFields.at( fieldIndex ).type() == QVariant::String ) + auto qType = mFields.at( fieldIndex ).type(); + if ( field.type == QgsHanaDataTypes::Geometry && qType == QVariant::String ) attrs << QStringLiteral( "%1=ST_GeomFromWKT(?, %2)" ).arg( QgsHanaUtils::quotedIdentifier( field.name ), QString::number( field.srid ) ); + else if ( field.type == QgsHanaDataTypes::RealVector && qType == QVariant::String ) + attrs << QStringLiteral( "%1=TO_REAL_VECTOR(?)" ).arg( QgsHanaUtils::quotedIdentifier( field.name ) ); else attrs << QStringLiteral( "%1=?" ).arg( QgsHanaUtils::quotedIdentifier( field.name ) ); } diff --git a/src/providers/hana/qgshanaresultset.cpp b/src/providers/hana/qgshanaresultset.cpp index 7c6f34b0034d..66f37bb9a6e5 100644 --- a/src/providers/hana/qgshanaresultset.cpp +++ b/src/providers/hana/qgshanaresultset.cpp @@ -14,6 +14,7 @@ * (at your option) any later version. * ***************************************************************************/ +#include "qgshanadatatypes.h" #include "qgshanaexception.h" #include "qgshanaresultset.h" #include "qgshanautils.h" @@ -24,8 +25,30 @@ #include "odbc/PreparedStatement.h" #include "odbc/Statement.h" +#include + using namespace NS_ODBC; +namespace +{ + QString fvecsToString( const char *data, size_t ) + { + uint32_t numElements; + memcpy( &numElements, data, sizeof( numElements ) ); + + const char *ptr = static_cast( data ) + sizeof( numElements ); + const float *elements = reinterpret_cast( ptr ); + + QString res; + res += QStringLiteral( "[" ) + QString::number( elements[0], 'g', 7 ); + for ( uint32_t i = 1; i < numElements; ++i ) + res += QStringLiteral( "," ) + QString::number( elements[i], 'g', 7 ); + res += QStringLiteral( "]" ); + + return res; + } +} + QgsHanaResultSet::QgsHanaResultSet( ResultSetRef &&resultSet ) : mResultSet( std::move( resultSet ) ) , mMetadata( mResultSet->getMetaDataUnicode() ) @@ -104,12 +127,16 @@ QString QgsHanaResultSet::getString( unsigned short columnIndex ) QVariant QgsHanaResultSet::getValue( unsigned short columnIndex ) { - switch ( mMetadata->getColumnType( columnIndex ) ) + int type = mMetadata->getColumnType( columnIndex ); + if ( type == QgsHanaDataTypes::VarBinary && mMetadata->getColumnTypeName( columnIndex ) == QLatin1String( "REAL_VECTOR" ) ) + type = QgsHanaDataTypes::RealVector; + + switch ( type ) { - case SQLDataTypes::Bit: - case SQLDataTypes::Boolean: + case QgsHanaDataTypes::Bit: + case QgsHanaDataTypes::Boolean: return QgsHanaUtils::toVariant( mResultSet->getBoolean( columnIndex ) ); - case SQLDataTypes::Char: + case QgsHanaDataTypes::Char: { String str = mResultSet->getString( columnIndex ); if ( mMetadata->getColumnLength( columnIndex ) == 1 ) @@ -122,7 +149,7 @@ QVariant QgsHanaResultSet::getValue( unsigned short columnIndex ) else return QgsHanaUtils::toVariant( str ); } - case SQLDataTypes::WChar: + case QgsHanaDataTypes::WChar: { NString str = mResultSet->getNString( columnIndex ); if ( mMetadata->getColumnLength( columnIndex ) == 1 ) @@ -135,56 +162,73 @@ QVariant QgsHanaResultSet::getValue( unsigned short columnIndex ) else return QgsHanaUtils::toVariant( str ); } - case SQLDataTypes::TinyInt: + case QgsHanaDataTypes::TinyInt: if ( mMetadata ->isSigned( columnIndex ) ) return QgsHanaUtils::toVariant( mResultSet->getByte( columnIndex ) ); else return QgsHanaUtils::toVariant( mResultSet->getUByte( columnIndex ) ); - case SQLDataTypes::SmallInt: + case QgsHanaDataTypes::SmallInt: if ( mMetadata ->isSigned( columnIndex ) ) return QgsHanaUtils::toVariant( mResultSet->getShort( columnIndex ) ); else return QgsHanaUtils::toVariant( mResultSet->getUShort( columnIndex ) ); - case SQLDataTypes::Integer: + case QgsHanaDataTypes::Integer: if ( mMetadata ->isSigned( columnIndex ) ) return QgsHanaUtils::toVariant( mResultSet->getInt( columnIndex ) ); else return QgsHanaUtils::toVariant( mResultSet->getUInt( columnIndex ) ); - case SQLDataTypes::BigInt: + case QgsHanaDataTypes::BigInt: if ( mMetadata ->isSigned( columnIndex ) ) return QgsHanaUtils::toVariant( mResultSet->getLong( columnIndex ) ); else return QgsHanaUtils::toVariant( mResultSet->getULong( columnIndex ) ); - case SQLDataTypes::Real: + case QgsHanaDataTypes::Real: return QgsHanaUtils::toVariant( mResultSet->getFloat( columnIndex ) ); - case SQLDataTypes::Double: - case SQLDataTypes::Decimal: - case SQLDataTypes::Float: - case SQLDataTypes::Numeric: + case QgsHanaDataTypes::Double: + case QgsHanaDataTypes::Decimal: + case QgsHanaDataTypes::Float: + case QgsHanaDataTypes::Numeric: return QgsHanaUtils::toVariant( mResultSet->getDouble( columnIndex ) ); - case SQLDataTypes::Date: - case SQLDataTypes::TypeDate: + case QgsHanaDataTypes::Date: + case QgsHanaDataTypes::TypeDate: return QgsHanaUtils::toVariant( mResultSet->getDate( columnIndex ) ); - case SQLDataTypes::Time: - case SQLDataTypes::TypeTime: + case QgsHanaDataTypes::Time: + case QgsHanaDataTypes::TypeTime: return QgsHanaUtils::toVariant( mResultSet->getTime( columnIndex ) ); - case SQLDataTypes::Timestamp: - case SQLDataTypes::TypeTimestamp: + case QgsHanaDataTypes::Timestamp: + case QgsHanaDataTypes::TypeTimestamp: return QgsHanaUtils::toVariant( mResultSet->getTimestamp( columnIndex ) ); - case SQLDataTypes::VarChar: - case SQLDataTypes::LongVarChar: + case QgsHanaDataTypes::VarChar: + case QgsHanaDataTypes::LongVarChar: return QgsHanaUtils::toVariant( mResultSet->getString( columnIndex ) ); - case SQLDataTypes::WVarChar: - case SQLDataTypes::WLongVarChar: + case QgsHanaDataTypes::WVarChar: + case QgsHanaDataTypes::WLongVarChar: return QgsHanaUtils::toVariant( mResultSet->getNString( columnIndex ) ); - case SQLDataTypes::Binary: - case SQLDataTypes::VarBinary: - case SQLDataTypes::LongVarBinary: - return QgsHanaUtils::toVariant( mResultSet->getBinary( columnIndex ) ); - case 29812: /* ST_GEOMETRY, ST_POINT */ + case QgsHanaDataTypes::Binary: + case QgsHanaDataTypes::VarBinary: + case QgsHanaDataTypes::LongVarBinary: + case QgsHanaDataTypes::Geometry: return QgsHanaUtils::toVariant( mResultSet->getBinary( columnIndex ) ); + case QgsHanaDataTypes::RealVector: + { + const size_t bufLength = mResultSet->getBinaryLength( columnIndex ); + if ( bufLength == ResultSet::UNKNOWN_LENGTH ) + { + Binary vec = mResultSet->getBinary( columnIndex ); + if ( !vec.isNull() && vec->size() > 0 ) + return fvecsToString( vec->data(), vec->size() ); + } + else if ( bufLength != 0 && bufLength != ResultSet::NULL_DATA ) + { + QByteArray vec( bufLength, '0' ); + mResultSet->getBinaryData( columnIndex, vec.data(), bufLength ); + return fvecsToString( vec.data(), vec.size() ); + } + + return QVariant(); + } default: - QgsDebugError( QStringLiteral( "Unhandled HANA type %1" ).arg( QString::fromStdU16String( mMetadata->getColumnTypeName( columnIndex ) ) ) ); + QgsDebugError( QStringLiteral( "Unhandled HANA data type %1" ).arg( QString::fromStdU16String( mMetadata->getColumnTypeName( columnIndex ) ) ) ); return QVariant(); } } diff --git a/src/providers/hana/qgshanautils.cpp b/src/providers/hana/qgshanautils.cpp index e917c43530ae..afdfd3847fd6 100644 --- a/src/providers/hana/qgshanautils.cpp +++ b/src/providers/hana/qgshanautils.cpp @@ -486,16 +486,34 @@ bool QgsHanaUtils::convertField( QgsField &field ) fieldPrec = 0; break; case QVariant::String: - if ( fieldSize > 0 ) + if ( field.typeName() == QLatin1String( "REAL_VECTOR" ) ) { - if ( fieldSize <= 5000 ) - fieldType = QStringLiteral( "NVARCHAR(%1)" ).arg( QString::number( fieldSize ) ); + if ( fieldSize > 0 ) + fieldType = QStringLiteral( "REAL_VECTOR(%1)" ).arg( QString::number( fieldSize ) ); else - fieldType = QStringLiteral( "NCLOB" ); + fieldType = QStringLiteral( "REAL_VECTOR" ); + } + else if ( field.typeName() == QLatin1String( "ST_GEOMETRY" ) ) + { + QVariant srid = field.metadata( Qgis::FieldMetadataProperty::CustomProperty ); + if ( srid.isValid() && srid.toInt() >= 0 ) + fieldType = QStringLiteral( "ST_GEOMETRY(%1)" ).arg( QString::number( srid.toInt() ) ); + else + fieldType = QStringLiteral( "ST_GEOMETRY" ); } else - fieldType = QStringLiteral( "NVARCHAR(5000)" ); - fieldPrec = -1; + { + if ( fieldSize > 0 ) + { + if ( fieldSize <= 5000 ) + fieldType = QStringLiteral( "NVARCHAR(%1)" ).arg( QString::number( fieldSize ) ); + else + fieldType = QStringLiteral( "NCLOB" ); + } + else + fieldType = QStringLiteral( "NVARCHAR(5000)" ); + fieldPrec = -1; + } break; case QVariant::ByteArray: if ( fieldSize >= 1 && fieldSize <= 5000 ) diff --git a/tests/src/python/test_hana_utils.py b/tests/src/python/test_hana_utils.py index 81137241df5e..e88857da8de7 100644 --- a/tests/src/python/test_hana_utils.py +++ b/tests/src/python/test_hana_utils.py @@ -20,8 +20,9 @@ class QgsHanaProviderUtils: @staticmethod def createConnection(uri): ds_uri = QgsDataSourceUri(uri) + encrypt = ds_uri.param("ENCRYPT") if ds_uri.hasParam("ENCRYPT") else False conn = dbapi.connect(address=ds_uri.host(), port=ds_uri.port(), user=ds_uri.username(), - password=ds_uri.password(), ENCRYPT=True, sslValidateCertificate=False, CHAR_AS_UTF8=1) + password=ds_uri.password(), ENCRYPT=encrypt, sslValidateCertificate=False, CHAR_AS_UTF8=1) conn.setautocommit(False) return conn diff --git a/tests/src/python/test_provider_hana.py b/tests/src/python/test_provider_hana.py index ba077ac5fe52..92e7eb61f8ae 100644 --- a/tests/src/python/test_provider_hana.py +++ b/tests/src/python/test_provider_hana.py @@ -424,6 +424,56 @@ def testBinaryTypeEdit(self): expected = {1: QByteArray(b'bbbvx'), 2: QByteArray(b'dddd')} self.assertEqual(values, expected) + def testRealVectorType(self): + table_name = "real_vector_type" + create_sql = f'CREATE TABLE "{self.schemaName}"."{table_name}" ( ' \ + '"id" INTEGER NOT NULL PRIMARY KEY,' \ + '"emb" REAL_VECTOR(3))' + insert_sql = f'INSERT INTO "{self.schemaName}"."{table_name}" ("id", "emb") VALUES (?, TO_REAL_VECTOR(?))' + insert_args = [[1, '[0.1,0.2,0.1]'], [2, None]] + self.prepareTestTable('real_vector_type', create_sql, insert_sql, insert_args) + + vl = self.createVectorLayer(f'table="{self.schemaName}"."{table_name}" sql=', 'testrealvector') + + fields = vl.dataProvider().fields() + self.assertEqual(fields.at(fields.indexFromName('emb')).type(), QVariant.String) + self.assertEqual(fields.at(fields.indexFromName('emb')).length(), 3) + + values = {feat['id']: feat['emb'] for feat in vl.getFeatures()} + expected = {1: '[0.1,0.2,0.1]', 2: QVariant()} + self.assertEqual(values, expected) + + def testRealVectorTypeEdit(self): + table_name = "real_vector_type_edit" + create_sql = f'CREATE TABLE "{self.schemaName}"."{table_name}" ( ' \ + '"id" INTEGER NOT NULL PRIMARY KEY,' \ + '"emb" REAL_VECTOR)' + insert_sql = f'INSERT INTO "{self.schemaName}"."{table_name}" ("id", "emb") VALUES (?, TO_REAL_VECTOR(?))' + insert_args = [[1, '[0.1,0.2,0.3]']] + self.prepareTestTable(table_name, create_sql, insert_sql, insert_args) + + vl = self.createVectorLayer(f'key=\'id\' table="{self.schemaName}"."{table_name}" sql=', 'testrealvectoredit') + + def check_values(expected): + actual = {feat['id']: feat['emb'] for feat in vl.getFeatures()} + self.assertEqual(actual, expected) + + check_values({1: '[0.1,0.2,0.3]'}) + + # change attribute value + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {1: '[0.82,0.5,1]'}})) + check_values({1: '[0.82,0.5,1]'}) + + # add feature + f = QgsFeature() + f.setAttributes([2, '[1,1,1]']) + self.assertTrue(vl.dataProvider().addFeature(f)) + check_values({1: '[0.82,0.5,1]', 2: '[1,1,1]'}) + + # change feature + self.assertTrue(vl.dataProvider().changeFeatures({2: {1: '[2,2,2]'}}, {})) + check_values({1: '[0.82,0.5,1]', 2: '[2,2,2]'}) + def testGeometryAttributes(self): create_sql = f'CREATE TABLE "{self.schemaName}"."geometry_attribute" ( ' \ 'ID INTEGER NOT NULL PRIMARY KEY,' \ @@ -487,12 +537,11 @@ def runTest(crs, primaryKey, attributeNames, attributeValues): layer.commitChanges() QgsHanaProviderUtils.dropTableIfExists(self.conn, self.schemaName, 'import_data') - uri = self.uri + f' key=\'{primaryKey}\' table="{self.schemaName}"."import_data" (geom) sql=' - error, message = QgsVectorLayerExporter.exportLayer(layer, uri, 'hana', crs) + params = f' key=\'{primaryKey}\' table="{self.schemaName}"."import_data" (geom) sql=' + error, message = QgsVectorLayerExporter.exportLayer(layer, self.uri + params, 'hana', crs) self.assertEqual(error, QgsVectorLayerExporter.ExportError.NoError) - import_layer = self.createVectorLayer( - f'key=\'{primaryKey}\' table="{self.schemaName}"."import_data" (geom) sql=', 'testimportedlayer') + import_layer = self.createVectorLayer(params, 'testimportedlayer') self.assertEqual(import_layer.wkbType(), QgsWkbTypes.Type.Point) self.assertEqual([f.name() for f in import_layer.fields()], attributeNames) @@ -531,6 +580,51 @@ def is_crs_installed(srid): QgsHanaProviderUtils.executeSQL(self.conn, f'DROP SPATIAL REFERENCE SYSTEM "{crs.description()}"') # QgsHanaProviderUtils.executeSQL(self.conn, 'DROP SPATIAL UNIT OF MEASURE degree_qgis') + def testCreateLayerViaExportWithSpecialTypesHandledAsString(self): + table_name = 'binary_types_as_string' + create_sql = f'''CREATE COLUMN TABLE "{self.schemaName}"."{table_name}" ( + "pk" INTEGER NOT NULL PRIMARY KEY, + "vec" REAL_VECTOR(3), + "geom1" ST_GEOMETRY(4326), + "geom2" ST_GEOMETRY(4326))''' + + insert_sql = f'INSERT INTO "{self.schemaName}"."{table_name}" ("pk", "vec", "geom1", "geom2") ' \ + 'VALUES (?, TO_REAL_VECTOR(?), ST_GeomFromWKT(?, 4326), ST_GeomFromWKT(?, 4326)) ' + insert_args = [ + [1, '[0.1,0.3,0.2]', None, 'POINT (-71.123 78.23)'], + [2, None, None, None], + [3, '[0.5,0.8,0.4]', 'POINT (-70.332 66.33)', 'POINT (-71.123 78.23)']] + + self.prepareTestTable(table_name, create_sql, insert_sql, insert_args) + + layer = self.createVectorLayer(f'table="{self.schemaName}"."{table_name}" (geom1) sql=', + 'testbinaryattributes') + crs = QgsCoordinateReferenceSystem('EPSG:4326') + params = f' key=\'pk\' table="{self.schemaName}"."import_data_binaries" (geom1) sql=' + error, message = QgsVectorLayerExporter.exportLayer(layer, self.uri + params, 'hana', crs) + self.assertEqual(error, QgsVectorLayerExporter.ExportError.NoError) + + import_layer = self.createVectorLayer(params, 'testimportedlayer') + fields = import_layer.dataProvider().fields() + self.assertEqual(fields.size(), 3) + pk_field = fields.at(fields.indexFromName('pk')) + self.assertEqual(pk_field.type(), QVariant.Int) + self.assertEqual(pk_field.typeName(), 'INTEGER') + vec_field = fields.at(fields.indexFromName('vec')) + self.assertEqual(vec_field.type(), QVariant.String) + self.assertEqual(vec_field.typeName(), 'REAL_VECTOR') + self.assertEqual(vec_field.length(), 3) + geom2_field = fields.at(fields.indexFromName('geom2')) + self.assertEqual(geom2_field.type(), QVariant.String) + self.assertEqual(geom2_field.typeName(), 'ST_GEOMETRY') + + i = 0 + for feat in import_layer.getFeatures(): + self.assertEqual(feat['pk'], insert_args[i][0]) + self.assertEqual(feat['vec'], insert_args[i][1]) + self.assertEqual(feat['geom2'], insert_args[i][3]) + i += 1 + def testFilterRectOutsideSrsExtent(self): """Test filterRect which partially lies outside of the srs extent""" self.source.setSubsetString(None)