diff --git a/src/providers/mssql/CMakeLists.txt b/src/providers/mssql/CMakeLists.txt index 6bac286e9e82..7596d32da802 100644 --- a/src/providers/mssql/CMakeLists.txt +++ b/src/providers/mssql/CMakeLists.txt @@ -11,6 +11,7 @@ set(MSSQL_SRCS qgsmssqlsqlquerybuilder.cpp qgsmssqltransaction.cpp qgsmssqldatabase.cpp + qgsmssqlutils.cpp ) if (WITH_GUI) diff --git a/src/providers/mssql/qgsmssqlconnection.cpp b/src/providers/mssql/qgsmssqlconnection.cpp index d97a5b87dfa6..5572d5f3e603 100644 --- a/src/providers/mssql/qgsmssqlconnection.cpp +++ b/src/providers/mssql/qgsmssqlconnection.cpp @@ -18,6 +18,7 @@ #include "qgsmssqlconnection.h" #include "qgsmssqlprovider.h" #include "qgsmssqldatabase.h" +#include "qgsmssqlutils.h" #include "qgslogger.h" #include "qgssettings.h" #include "qgsdatasourceuri.h" @@ -422,7 +423,7 @@ QString QgsMssqlConnection::buildQueryForTables( bool allowTablesWithNoGeometry, { QStringList quotedSchemas; for ( const QString &sch : excludedSchemaList ) - quotedSchemas.append( QgsMssqlProvider::quotedValue( sch ) ); + quotedSchemas.append( QgsMssqlUtils::quotedValue( sch ) ); notSelectedSchemas = quotedSchemas.join( ',' ); notSelectedSchemas.prepend( QStringLiteral( "( " ) ); notSelectedSchemas.append( QStringLiteral( " )" ) ); diff --git a/src/providers/mssql/qgsmssqldatabase.cpp b/src/providers/mssql/qgsmssqldatabase.cpp index 6daeb6c19621..6d8852ba9fbe 100644 --- a/src/providers/mssql/qgsmssqldatabase.cpp +++ b/src/providers/mssql/qgsmssqldatabase.cpp @@ -20,6 +20,7 @@ #include "qgsvariantutils.h" #include "qgsmssqlprovider.h" #include "qgsdbquerylog.h" +#include "qgsmssqlutils.h" #include #include @@ -165,59 +166,6 @@ QSqlQuery QgsMssqlDatabase::createQuery() return QSqlQuery( d ); } -QMetaType::Type QgsMssqlDatabase::decodeSqlType( const QString &sqlTypeName ) -{ - QMetaType::Type type = QMetaType::Type::UnknownType; - // cloned branches are intentional here for improved readability - // NOLINTBEGIN(bugprone-branch-clone) - if ( sqlTypeName.startsWith( QLatin1String( "decimal" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "numeric" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "real" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "float" ), Qt::CaseInsensitive ) ) - { - type = QMetaType::Type::Double; - } - else if ( sqlTypeName.startsWith( QLatin1String( "char" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "nchar" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "varchar" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "nvarchar" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "text" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "ntext" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "uniqueidentifier" ), Qt::CaseInsensitive ) ) - { - type = QMetaType::Type::QString; - } - else if ( sqlTypeName.startsWith( QLatin1String( "smallint" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "int" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "bit" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "tinyint" ), Qt::CaseInsensitive ) ) - { - type = QMetaType::Type::Int; - } - else if ( sqlTypeName.startsWith( QLatin1String( "bigint" ), Qt::CaseInsensitive ) ) - { - type = QMetaType::Type::LongLong; - } - else if ( sqlTypeName.startsWith( QLatin1String( "binary" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "varbinary" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "image" ), Qt::CaseInsensitive ) ) - { - type = QMetaType::Type::QByteArray; - } - else if ( sqlTypeName.startsWith( QLatin1String( "datetime" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "smalldatetime" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "datetime2" ), Qt::CaseInsensitive ) ) - { - type = QMetaType::Type::QDateTime; - } - else if ( sqlTypeName.startsWith( QLatin1String( "date" ), Qt::CaseInsensitive ) ) - { - type = QMetaType::Type::QDate; - } - else if ( sqlTypeName.startsWith( QLatin1String( "timestamp" ), Qt::CaseInsensitive ) ) - { - type = QMetaType::Type::QString; - } - else if ( sqlTypeName.startsWith( QLatin1String( "time" ), Qt::CaseInsensitive ) ) - { - type = QMetaType::Type::QTime; - } - else - { - QgsDebugError( QStringLiteral( "Unknown field type: %1" ).arg( sqlTypeName ) ); - // Everything else just dumped as a string. - type = QMetaType::Type::QString; - } - // NOLINTEND(bugprone-branch-clone) - - return type; -} - - bool QgsMssqlDatabase::loadFields( FieldDetails &details, const QString &schema, const QString &tableName, QString &error ) { error.clear(); @@ -251,7 +199,7 @@ bool QgsMssqlDatabase::loadFields( FieldDetails &details, const QString &schema, const QString sql2 { QStringLiteral( "SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC" " INNER JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE CC ON TC.CONSTRAINT_NAME = CC.CONSTRAINT_NAME" " WHERE TC.CONSTRAINT_SCHEMA = %1 AND TC.TABLE_NAME = %2 AND TC.CONSTRAINT_TYPE = 'unique'" ) - .arg( QgsMssqlProvider::quotedValue( schema ), QgsMssqlProvider::quotedValue( tableName ) ) }; + .arg( QgsMssqlUtils::quotedValue( schema ), QgsMssqlUtils::quotedValue( tableName ) ) }; if ( !LoggedExec( query, sql2 ) ) { error = query.lastError().text(); @@ -264,7 +212,7 @@ bool QgsMssqlDatabase::loadFields( FieldDetails &details, const QString &schema, } } - const QString sql3 { QStringLiteral( "exec sp_columns @table_name = %1, @table_owner = %2" ).arg( QgsMssqlProvider::quotedValue( tableName ), QgsMssqlProvider::quotedValue( schema ) ) }; + const QString sql3 { QStringLiteral( "exec sp_columns @table_name = %1, @table_owner = %2" ).arg( QgsMssqlUtils::quotedValue( tableName ), QgsMssqlUtils::quotedValue( schema ) ) }; if ( !LoggedExec( query, sql3 ) ) { error = query.lastError().text(); @@ -290,7 +238,6 @@ bool QgsMssqlDatabase::loadFields( FieldDetails &details, const QString &schema, } else { - const QMetaType::Type sqlType = decodeSqlType( sqlTypeName ); if ( sqlTypeName == QLatin1String( "int identity" ) || sqlTypeName == QLatin1String( "bigint identity" ) ) { details.primaryKeyType = PrimaryKeyType::Int; @@ -303,46 +250,14 @@ bool QgsMssqlDatabase::loadFields( FieldDetails &details, const QString &schema, pkCandidates << colName; } - QgsField field; - if ( sqlType == QMetaType::Type::QString ) - { - // Field length in chars is column 7 ("Length") of the sp_columns output, - // except for uniqueidentifiers which must use column 6 ("Precision"). - int length = query.value( sqlTypeName.startsWith( QStringLiteral( "uniqueidentifier" ), Qt::CaseInsensitive ) ? 6 : 7 ).toInt(); - if ( sqlTypeName.startsWith( QLatin1Char( 'n' ) ) ) - { - length = length / 2; - } - field = QgsField( colName, sqlType, sqlTypeName, length ); - } - else if ( sqlType == QMetaType::Type::Double ) - { - field = QgsField( colName, sqlType, sqlTypeName, query.value( QStringLiteral( "PRECISION" ) ).toInt(), sqlTypeName == QLatin1String( "decimal" ) || sqlTypeName == QLatin1String( "numeric" ) ? query.value( QStringLiteral( "SCALE" ) ).toInt() : -1 ); - } - else if ( sqlType == QMetaType::Type::QDate || sqlType == QMetaType::Type::QDateTime || sqlType == QMetaType::Type::QTime ) - { - field = QgsField( colName, sqlType, sqlTypeName, -1, -1 ); - } - else - { - field = QgsField( colName, sqlType, sqlTypeName ); - } - - // Field nullable + const int precision = query.value( 6 ).toInt(); + const int length = query.value( 7 ).toInt(); + const int scale = query.value( QStringLiteral( "SCALE" ) ).toInt(); const bool nullable = query.value( QStringLiteral( "NULLABLE" ) ).toBool(); + const bool unique = setColumnUnique.contains( colName ); + const bool readOnly = columnIsIdentity; - // Set constraints - QgsFieldConstraints constraints; - if ( !nullable ) - constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider ); - if ( setColumnUnique.contains( colName ) ) - constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider ); - field.setConstraints( constraints ); - - if ( columnIsIdentity ) - { - field.setReadOnly( true ); - } + const QgsField field = QgsMssqlUtils::createField( colName, sqlTypeName, length, precision, scale, nullable, unique, readOnly ); details.attributeFields.append( field ); @@ -367,7 +282,7 @@ bool QgsMssqlDatabase::loadFields( FieldDetails &details, const QString &schema, { query.clear(); query.setForwardOnly( true ); - const QString sql4 { QStringLiteral( "exec sp_pkeys @table_name = %1, @table_owner = %2 " ).arg( QgsMssqlProvider::quotedValue( tableName ), QgsMssqlProvider::quotedValue( schema ) ) }; + const QString sql4 { QStringLiteral( "exec sp_pkeys @table_name = %1, @table_owner = %2 " ).arg( QgsMssqlUtils::quotedValue( tableName ), QgsMssqlUtils::quotedValue( schema ) ) }; if ( !LoggedExec( query, sql4 ) ) { QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) ); @@ -429,6 +344,79 @@ bool QgsMssqlDatabase::loadFields( FieldDetails &details, const QString &schema, return true; } +bool QgsMssqlDatabase::loadQueryFields( FieldDetails &details, const QString &query, QString &error ) +{ + error.clear(); + + details.attributeFields.clear(); + details.defaultValues.clear(); + details.computedColumns.clear(); + + // get field spec + QSqlQuery dbQuery = createQuery(); + dbQuery.setForwardOnly( true ); + + // TODO SQL server >= 2012 only! + + const QString sql { QStringLiteral( R"raw( + EXEC sp_describe_first_result_set + %1 + )raw" ) + .arg( QgsMssqlUtils::quotedValue( query ) ) }; + if ( !LoggedExec( dbQuery, sql ) ) + { + error = dbQuery.lastError().text(); + return false; + } + + int fieldIndex = 0; + while ( dbQuery.next() ) + { + fieldIndex++; + + // consider all columns as computed + // NOTE: for some queries some fields are updateable. We can determine this through the "is_updateable" column in sp_describe_first_result_set + // However the provider has no way to selectively say some columns are updateable but not others, so we treat all queries as completely read-only + const int columnOrdinal = dbQuery.value( 1 ).toInt(); + if ( columnOrdinal != fieldIndex ) + { + QgsDebugError( QStringLiteral( "sp_describe_first_result_set returned out of order results!" ) ); + } + + const bool isHidden = dbQuery.value( 0 ).toInt(); + if ( isHidden ) + continue; + + QString name = dbQuery.value( 2 ).toString(); + if ( name.isEmpty() ) + name = QStringLiteral( "col%1" ).arg( fieldIndex ); + + const bool isNullable = dbQuery.value( 3 ).toInt(); + const QString systemTypeName = dbQuery.value( 5 ).toString(); + const int maxLength = dbQuery.value( 6 ).toInt(); + const int precision = dbQuery.value( 7 ).toInt(); + const int scale = dbQuery.value( 8 ).toInt(); + + + // if we don't have an explicitly set geometry column name, and this is a geometry column, then use it + // but if we DO have an explicitly set geometry column name, then load the other information if this is that column + if ( ( details.geometryColumnName.isEmpty() && ( systemTypeName == QLatin1String( "geometry" ) || systemTypeName == QLatin1String( "geography" ) ) ) + || name == details.geometryColumnName ) + { + details.geometryColumnName = name; + details.geometryColumnType = systemTypeName; + details.isGeography = systemTypeName == QLatin1String( "geography" ); + } + else + { + const QgsField field = QgsMssqlUtils::createField( name, systemTypeName, maxLength, precision, scale, isNullable, false, true ); + details.attributeFields.append( field ); + } + } + + return true; +} + // ------------------- diff --git a/src/providers/mssql/qgsmssqldatabase.h b/src/providers/mssql/qgsmssqldatabase.h index e8d667404d87..98aea5a43fc5 100644 --- a/src/providers/mssql/qgsmssqldatabase.h +++ b/src/providers/mssql/qgsmssqldatabase.h @@ -81,8 +81,6 @@ class QgsMssqlDatabase QSqlQuery createQuery(); - static QMetaType::Type decodeSqlType( const QString &sqlTypeName ); - struct FieldDetails { QgsFields attributeFields; @@ -95,8 +93,16 @@ class QgsMssqlDatabase QList primaryKeyAttrs; }; + /** + * Loads the field details corresponding to the specified \a schema and \a tableName. + */ bool loadFields( FieldDetails &details, const QString &schema, const QString &tableName, QString &error ); + /** + * Loads the field details corresponding to the specified SQL \a query. + */ + bool loadQueryFields( FieldDetails &details, const QString &query, QString &error ); + private: QgsMssqlDatabase( const QSqlDatabase &db, const QgsDataSourceUri &uri, bool transaction ); diff --git a/src/providers/mssql/qgsmssqldataitems.cpp b/src/providers/mssql/qgsmssqldataitems.cpp index 1eda011c991e..aa8519e943da 100644 --- a/src/providers/mssql/qgsmssqldataitems.cpp +++ b/src/providers/mssql/qgsmssqldataitems.cpp @@ -19,7 +19,7 @@ #include "moc_qgsmssqldataitems.cpp" #include "qgsmssqlconnection.h" #include "qgsmssqldatabase.h" - +#include "qgsmssqlutils.h" #include "qgsmssqlgeomcolumntypethread.h" #include "qgslogger.h" #include "qgsmimedatautils.h" @@ -357,7 +357,7 @@ void QgsMssqlConnectionItem::setLayerType( QgsMssqlLayerProperty layerProperty ) for ( int i = 0; i < typeList.size(); i++ ) { - Qgis::WkbType wkbType = QgsMssqlTableModel::wkbTypeFromMssql( typeList[i] ); + Qgis::WkbType wkbType = QgsMssqlUtils::wkbTypeFromGeometryType( typeList[i] ); if ( wkbType == Qgis::WkbType::Unknown ) { QgsDebugError( QStringLiteral( "unsupported geometry type:%1" ).arg( typeList[i] ) ); @@ -512,7 +512,7 @@ QString QgsMssqlLayerItem::createUri() QgsDataSourceUri uri = QgsDataSourceUri( connItem->connInfo() ); uri.setDataSource( mLayerProperty.schemaName, mLayerProperty.tableName, mLayerProperty.geometryColName, mLayerProperty.sql, pkColName ); uri.setSrid( mLayerProperty.srid ); - uri.setWkbType( QgsMssqlTableModel::wkbTypeFromMssql( mLayerProperty.type ) ); + uri.setWkbType( QgsMssqlUtils::wkbTypeFromGeometryType( mLayerProperty.type ) ); uri.setUseEstimatedMetadata( QgsMssqlConnection::useEstimatedMetadata( connItem->name() ) ); mDisableInvalidGeometryHandling = QgsMssqlConnection::isInvalidGeometryHandlingDisabled( connItem->name() ); uri.setParam( QStringLiteral( "disableInvalidGeometryHandling" ), mDisableInvalidGeometryHandling ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); @@ -560,7 +560,7 @@ void QgsMssqlSchemaItem::addLayers( QgsDataItem *newLayers ) QgsMssqlLayerItem *QgsMssqlSchemaItem::addLayer( const QgsMssqlLayerProperty &layerProperty, bool refresh ) { - Qgis::WkbType wkbType = QgsMssqlTableModel::wkbTypeFromMssql( layerProperty.type ); + Qgis::WkbType wkbType = QgsMssqlUtils::wkbTypeFromGeometryType( layerProperty.type ); QString tip = tr( "%1 as %2 in %3" ).arg( layerProperty.geometryColName, QgsWkbTypes::displayString( wkbType ), layerProperty.srid ); Qgis::BrowserLayerType layerType; diff --git a/src/providers/mssql/qgsmssqlfeatureiterator.cpp b/src/providers/mssql/qgsmssqlfeatureiterator.cpp index 62e43dff3fc7..75da1a20430a 100644 --- a/src/providers/mssql/qgsmssqlfeatureiterator.cpp +++ b/src/providers/mssql/qgsmssqlfeatureiterator.cpp @@ -19,6 +19,7 @@ #include "qgsmssqlexpressioncompiler.h" #include "qgsmssqlprovider.h" #include "qgsmssqltransaction.h" +#include "qgsmssqlutils.h" #include "qgslogger.h" #include "qgsdbquerylog.h" #include "qgsdbquerylog_p.h" @@ -35,8 +36,6 @@ QgsMssqlFeatureIterator::QgsMssqlFeatureIterator( QgsMssqlFeatureSource *source, : QgsAbstractFeatureIteratorFromSource( source, ownSource, request ) , mDisableInvalidGeometryHandling( source->mDisableInvalidGeometryHandling ) { - mClosed = false; - mParser.mIsGeography = mSource->mIsGeography; mTransform = mRequest.calculateTransform( mSource->mCrs ); @@ -124,7 +123,7 @@ QString QgsMssqlFeatureIterator::whereClauseFid( QgsFeatureId featureId ) for ( int i = 0; i < mSource->mPrimaryKeyAttrs.size(); ++i ) { const QgsField &fld = mSource->mFields.at( mSource->mPrimaryKeyAttrs[i] ); - whereClause += QStringLiteral( "%1[%2]=%3" ).arg( delim, fld.name(), QgsMssqlProvider::quotedValue( pkVals[i] ) ); + whereClause += QStringLiteral( "%1[%2]=%3" ).arg( delim, fld.name(), QgsMssqlUtils::quotedValue( pkVals[i] ) ); delim = QStringLiteral( " AND " ); } @@ -157,11 +156,10 @@ void QgsMssqlFeatureIterator::BuildStatement( const QgsFeatureRequest &request ) // build sql statement // note: 'SELECT ' is added later, to account for 'SELECT TOP...' type queries - QString delim; - for ( auto idx : mSource->mPrimaryKeyAttrs ) + QStringList selectColumns; + for ( int idx : mSource->mPrimaryKeyAttrs ) { - mStatement += QStringLiteral( "%1[%2]" ).arg( delim, mSource->mFields.at( idx ).name() ); - delim = ','; + selectColumns << QgsMssqlUtils::quotedIdentifier( mSource->mFields.at( idx ).name() ); } mAttributesToFetch << mSource->mPrimaryKeyAttrs; @@ -194,7 +192,7 @@ void QgsMssqlFeatureIterator::BuildStatement( const QgsFeatureRequest &request ) if ( mSource->mPrimaryKeyAttrs.contains( i ) ) continue; - mStatement += QStringLiteral( ",[%1]" ).arg( mSource->mFields.at( i ).name() ); + selectColumns << QgsMssqlUtils::quotedIdentifier( mSource->mFields.at( i ).name() ); mAttributesToFetch.append( i ); } @@ -206,10 +204,19 @@ void QgsMssqlFeatureIterator::BuildStatement( const QgsFeatureRequest &request ) ) && mSource->isSpatial() ) { - mStatement += QStringLiteral( ",[%1]" ).arg( mSource->mGeometryColName ); + selectColumns << QgsMssqlUtils::quotedIdentifier( mSource->mGeometryColName ); } - mStatement += QStringLiteral( " FROM [%1].[%2]" ).arg( mSource->mSchemaName, mSource->mTableName ); + mStatement = selectColumns.join( ',' ); + + if ( !mSource->mQuery.isEmpty() ) + { + mStatement += QStringLiteral( " FROM (%1) _subquery" ).arg( mSource->mQuery ); + } + else + { + mStatement += QStringLiteral( " FROM [%1].[%2]" ).arg( mSource->mSchemaName, mSource->mTableName ); + } bool filterAdded = false; // set spatial filter @@ -277,7 +284,7 @@ void QgsMssqlFeatureIterator::BuildStatement( const QgsFeatureRequest &request ) if ( QgsVariantUtils::isNull( key[i] ) ) expr = QString( "[%1] IS NULL" ).arg( colName ); else - expr = QString( "[%1]=%2" ).arg( colName, QgsMssqlProvider::quotedValue( key[i] ) ); + expr = QString( "[%1]=%2" ).arg( colName, QgsMssqlUtils::quotedValue( key[i] ) ); mStatement += QStringLiteral( "%1%2" ).arg( delim, expr ); delim = " AND "; @@ -702,6 +709,7 @@ QgsMssqlFeatureSource::QgsMssqlFeatureSource( const QgsMssqlProvider *p ) , mGeometryColType( p->mGeometryColType ) , mSchemaName( p->mSchemaName ) , mTableName( p->mTableName ) + , mQuery( p->mQuery ) , mUserName( p->mUserName ) , mPassword( p->mPassword ) , mService( p->mService ) diff --git a/src/providers/mssql/qgsmssqlfeatureiterator.h b/src/providers/mssql/qgsmssqlfeatureiterator.h index 4235c3971d36..23f25670d8c4 100644 --- a/src/providers/mssql/qgsmssqlfeatureiterator.h +++ b/src/providers/mssql/qgsmssqlfeatureiterator.h @@ -56,6 +56,7 @@ class QgsMssqlFeatureSource final : public QgsAbstractFeatureSource // current layer name QString mSchemaName; QString mTableName; + QString mQuery; // login QString mUserName; diff --git a/src/providers/mssql/qgsmssqlprovider.cpp b/src/providers/mssql/qgsmssqlprovider.cpp index 7cd99105f023..9009efb6f93a 100644 --- a/src/providers/mssql/qgsmssqlprovider.cpp +++ b/src/providers/mssql/qgsmssqlprovider.cpp @@ -20,6 +20,7 @@ #include "qgsmssqlconnection.h" #include "qgsmssqldatabase.h" #include "qgsmssqlproviderconnection.h" +#include "qgsmssqlutils.h" #include "qgsfeedback.h" #include "qgsdbquerylog.h" #include "qgsdbquerylog_p.h" @@ -122,31 +123,41 @@ QgsMssqlProvider::QgsMssqlProvider( const QString &uri, const ProviderOptions &o } // Database successfully opened; we can now issue SQL commands. - if ( !anUri.schema().isEmpty() ) - mSchemaName = anUri.schema(); - else - mSchemaName = QStringLiteral( "dbo" ); - if ( !anUri.table().isEmpty() ) + if ( mSchemaName.isEmpty() && anUri.table().startsWith( '(' ) && anUri.table().endsWith( ')' ) ) { - // the layer name has been specified - mTableName = anUri.table(); - QStringList sl = mTableName.split( '.' ); - if ( sl.length() == 2 ) - { - mSchemaName = sl[0]; - mTableName = sl[1]; - } - mTables = QStringList( mTableName ); + mIsQuery = true; + mQuery = anUri.table(); } else { - // Get a list of table - mTables = db.tables( QSql::Tables ); - if ( !mTables.isEmpty() ) - mTableName = mTables[0]; + mIsQuery = false; + if ( !anUri.schema().isEmpty() ) + mSchemaName = anUri.schema(); else - mValid = false; + mSchemaName = QStringLiteral( "dbo" ); + + if ( !anUri.table().isEmpty() ) + { + // the layer name has been specified + mTableName = anUri.table(); + QStringList sl = mTableName.split( '.' ); + if ( sl.length() == 2 ) + { + mSchemaName = sl[0]; + mTableName = sl[1]; + } + mTables = QStringList( mTableName ); + } + else + { + // Get a list of table + mTables = db.tables( QSql::Tables ); + if ( !mTables.isEmpty() ) + mTableName = mTables[0]; + else + mValid = false; + } } if ( mValid ) @@ -154,9 +165,16 @@ QgsMssqlProvider::QgsMssqlProvider( const QString &uri, const ProviderOptions &o if ( !anUri.geometryColumn().isEmpty() ) mGeometryColName = anUri.geometryColumn(); - if ( mSRId < 0 || mWkbType == Qgis::WkbType::Unknown || mGeometryColName.isEmpty() ) + if ( !mIsQuery ) { - loadMetadata(); + if ( mSRId < 0 || mWkbType == Qgis::WkbType::Unknown || mGeometryColName.isEmpty() ) + { + loadMetadata(); + } + else + { + // TODO query?? + } } loadFields(); @@ -223,6 +241,15 @@ QgsMssqlProvider::QgsMssqlProvider( const QString &uri, const ProviderOptions &o } } + + if ( mValid && mIsQuery && mPrimaryKeyAttrs.isEmpty() ) + { + const QString error = QStringLiteral( "No primary key could be found for query %1" ).arg( mQuery ); + QgsDebugError( error ); + mValid = false; + setLastError( error ); + } + //fill type names into sets setNativeTypes( QgsMssqlConnection::nativeTypes() ); } @@ -254,7 +281,7 @@ void QgsMssqlProvider::loadMetadata() QSqlQuery query = createQuery(); query.setForwardOnly( true ); - if ( !LoggedExec( query, QStringLiteral( "SELECT f_geometry_column, srid, geometry_type, coord_dimension FROM geometry_columns WHERE f_table_schema=%1 AND f_table_name=%2" ).arg( quotedValue( mSchemaName ), quotedValue( mTableName ) ) ) ) + if ( !LoggedExec( query, QStringLiteral( "SELECT f_geometry_column, srid, geometry_type, coord_dimension FROM geometry_columns WHERE f_table_schema=%1 AND f_table_name=%2" ).arg( QgsMssqlUtils::quotedValue( mSchemaName ), QgsMssqlUtils::quotedValue( mTableName ) ) ) ) { QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) ); } @@ -339,7 +366,8 @@ void QgsMssqlProvider::loadFields() details.geometryColumnName = mGeometryColName; QString error; - const bool result = conn->loadFields( details, mSchemaName, mTableName, error ); + const bool result = mIsQuery ? conn->loadQueryFields( details, mQuery, error ) + : conn->loadFields( details, mSchemaName, mTableName, error ); if ( !result ) { pushError( error ); @@ -355,7 +383,7 @@ void QgsMssqlProvider::loadFields() mAttributeFields = details.attributeFields; mDefaultValues = details.defaultValues; - if ( mPrimaryKeyAttrs.isEmpty() ) + if ( !mIsQuery && mPrimaryKeyAttrs.isEmpty() ) { const QString error = QStringLiteral( "No primary key could be found on table %1" ).arg( mTableName ); QgsDebugError( error ); @@ -364,37 +392,6 @@ void QgsMssqlProvider::loadFields() } } -QString QgsMssqlProvider::quotedValue( const QVariant &value ) -{ - if ( QgsVariantUtils::isNull( value ) ) - return QStringLiteral( "NULL" ); - - switch ( value.userType() ) - { - case QMetaType::Type::Int: - case QMetaType::Type::LongLong: - case QMetaType::Type::Double: - return value.toString(); - - case QMetaType::Type::Bool: - return QString( value.toBool() ? '1' : '0' ); - - default: - case QMetaType::Type::QString: - QString v = value.toString(); - v.replace( '\'', QLatin1String( "''" ) ); - if ( v.contains( '\\' ) ) - return v.replace( '\\', QLatin1String( "\\\\" ) ).prepend( "N'" ).append( '\'' ); - else - return v.prepend( "N'" ).append( '\'' ); - } -} - -QString QgsMssqlProvider::quotedIdentifier( const QString &value ) -{ - return QStringLiteral( "[%1]" ).arg( value ); -} - QString QgsMssqlProvider::defaultValueClause( int fieldId ) const { const QString defVal = mDefaultValues.value( fieldId, QString() ); @@ -482,14 +479,20 @@ QVariant QgsMssqlProvider::minimumValue( int index ) const // get the field name const QgsField &fld = mAttributeFields.at( index ); - QString sql = QStringLiteral( "select min([%1]) from " ) - .arg( fld.name() ); + QString sql = QStringLiteral( "SELECT min(%1) FROM " ) + .arg( QgsMssqlUtils::quotedIdentifier( fld.name() ) ); - sql += QStringLiteral( "[%1].[%2]" ).arg( mSchemaName, mTableName ); - - if ( !mSqlWhereClause.isEmpty() ) + if ( mIsQuery ) { - sql += QStringLiteral( " where (%1)" ).arg( mSqlWhereClause ); + sql += QStringLiteral( " (%1) q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() ); + } + else + { + sql += QStringLiteral( " %1.%2" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) ); + if ( !mSqlWhereClause.isEmpty() ) + { + sql += " WHERE (" + mSqlWhereClause + ')'; + } } QSqlQuery query = createQuery(); @@ -523,14 +526,20 @@ QVariant QgsMssqlProvider::maximumValue( int index ) const // get the field name const QgsField &fld = mAttributeFields.at( index ); - QString sql = QStringLiteral( "select max([%1]) from " ) - .arg( fld.name() ); - - sql += QStringLiteral( "[%1].[%2]" ).arg( mSchemaName, mTableName ); + QString sql = QStringLiteral( "SELECT max(%1) FROM " ) + .arg( QgsMssqlUtils::quotedIdentifier( fld.name() ) ); - if ( !mSqlWhereClause.isEmpty() ) + if ( mIsQuery ) + { + sql += QStringLiteral( " (%1) q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() ); + } + else { - sql += QStringLiteral( " where (%1)" ).arg( mSqlWhereClause ); + sql += QStringLiteral( " %1.%2" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) ); + if ( !mSqlWhereClause.isEmpty() ) + { + sql += " WHERE (" + mSqlWhereClause + ')'; + } } QSqlQuery query = createQuery(); @@ -565,21 +574,27 @@ QSet QgsMssqlProvider::uniqueValues( int index, int limit ) const // get the field name const QgsField &fld = mAttributeFields.at( index ); - QString sql = QStringLiteral( "select distinct " ); + QString sql = QStringLiteral( "SELECT DISTINCT " ); if ( limit > 0 ) { - sql += QStringLiteral( " top %1 " ).arg( limit ); + sql += QStringLiteral( " TOP %1 " ).arg( limit ); } - sql += QStringLiteral( "[%1] from " ) - .arg( fld.name() ); + sql += QStringLiteral( "%1 FROM " ).arg( QgsMssqlUtils::quotedIdentifier( fld.name() ) ); - sql += QStringLiteral( "[%1].[%2]" ).arg( mSchemaName, mTableName ); - if ( !mSqlWhereClause.isEmpty() ) + if ( mIsQuery ) + { + sql += QStringLiteral( " (%1) q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() ); + } + else { - sql += QStringLiteral( " where (%1)" ).arg( mSqlWhereClause ); + sql += QStringLiteral( " %1.%2" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) ); + if ( !mSqlWhereClause.isEmpty() ) + { + sql += " where (" + mSqlWhereClause + ')'; + } } QSqlQuery query = createQuery(); @@ -617,24 +632,30 @@ QStringList QgsMssqlProvider::uniqueStringsMatching( int index, const QString &s // get the field name const QgsField &fld = mAttributeFields.at( index ); - QString sql = QStringLiteral( "select distinct " ); + QString sql = QStringLiteral( "SELECT DISTINCT " ); if ( limit > 0 ) { - sql += QStringLiteral( " top %1 " ).arg( limit ); + sql += QStringLiteral( " TOP %1 " ).arg( limit ); } - sql += QStringLiteral( "[%1] from " ) - .arg( fld.name() ); + sql += QStringLiteral( "%1 FROM " ) + .arg( QgsMssqlUtils::quotedIdentifier( fld.name() ) ); - sql += QStringLiteral( "[%1].[%2] WHERE" ).arg( mSchemaName, mTableName ); - - if ( !mSqlWhereClause.isEmpty() ) + if ( mIsQuery ) { - sql += QStringLiteral( " (%1) AND" ).arg( mSqlWhereClause ); + sql += QStringLiteral( " (%1) q WHERE %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " (%1) AND " ).arg( mSqlWhereClause ) : QString() ); + } + else + { + sql += QStringLiteral( " %1.%2 WHERE " ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) ); + if ( !mSqlWhereClause.isEmpty() ) + { + sql += QStringLiteral( " (%1) AND " ).arg( mSqlWhereClause ); + } } - sql += QStringLiteral( " [%1] LIKE '%%2%'" ).arg( fld.name(), substring ); + sql += QStringLiteral( " %1 LIKE '%%2%'" ).arg( QgsMssqlUtils::quotedIdentifier( fld.name() ), substring ); QSqlQuery query = createQuery(); query.setForwardOnly( true ); @@ -680,28 +701,31 @@ void QgsMssqlProvider::UpdateStatistics( bool estimate ) const return; } - // Get the extents from the spatial index table to speed up load times. - // We have to use max() and min() because you can have more then one index but the biggest area is what we want to use. - const QString sql = "SELECT min(bounding_box_xmin), min(bounding_box_ymin), max(bounding_box_xmax), max(bounding_box_ymax)" - " FROM sys.spatial_index_tessellations WHERE object_id = OBJECT_ID('[%1].[%2]')"; + if ( !mIsQuery ) + { + // Get the extents from the spatial index table to speed up load times. + // We have to use max() and min() because you can have more then one index but the biggest area is what we want to use. + const QString sql = "SELECT min(bounding_box_xmin), min(bounding_box_ymin), max(bounding_box_xmax), max(bounding_box_ymax)" + " FROM sys.spatial_index_tessellations WHERE object_id = OBJECT_ID('[%1].[%2]')"; - statement = QString( sql ).arg( mSchemaName, mTableName ); + statement = QString( sql ).arg( mSchemaName, mTableName ); - if ( LoggedExec( query, statement ) ) - { - if ( query.next() && ( !QgsVariantUtils::isNull( query.value( 0 ) ) || !QgsVariantUtils::isNull( query.value( 1 ) ) || !QgsVariantUtils::isNull( query.value( 2 ) ) || !QgsVariantUtils::isNull( query.value( 3 ) ) ) ) + if ( LoggedExec( query, statement ) ) { - QgsDebugMsgLevel( QStringLiteral( "Found extents in spatial index" ), 2 ); - mExtent.setXMinimum( query.value( 0 ).toDouble() ); - mExtent.setYMinimum( query.value( 1 ).toDouble() ); - mExtent.setXMaximum( query.value( 2 ).toDouble() ); - mExtent.setYMaximum( query.value( 3 ).toDouble() ); - return; + if ( query.next() && ( !QgsVariantUtils::isNull( query.value( 0 ) ) || !QgsVariantUtils::isNull( query.value( 1 ) ) || !QgsVariantUtils::isNull( query.value( 2 ) ) || !QgsVariantUtils::isNull( query.value( 3 ) ) ) ) + { + QgsDebugMsgLevel( QStringLiteral( "Found extents in spatial index" ), 2 ); + mExtent.setXMinimum( query.value( 0 ).toDouble() ); + mExtent.setYMinimum( query.value( 1 ).toDouble() ); + mExtent.setXMaximum( query.value( 2 ).toDouble() ); + mExtent.setYMaximum( query.value( 3 ).toDouble() ); + return; + } + } + else + { + QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) ); } - } - else - { - QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) ); } // If we can't find the extents in the spatial index table just do what we normally do. @@ -711,16 +735,16 @@ void QgsMssqlProvider::UpdateStatistics( bool estimate ) const if ( mGeometryColType == QLatin1String( "geometry" ) ) { if ( mDisableInvalidGeometryHandling ) - statement = QStringLiteral( "select min([%1].STPointN(1).STX), min([%1].STPointN(1).STY), max([%1].STPointN(1).STX), max([%1].STPointN(1).STY)" ).arg( mGeometryColName ); + statement = QStringLiteral( "select min(%1.STPointN(1).STX), min(%1.STPointN(1).STY), max(%1.STPointN(1).STX), max(%1.STPointN(1).STY)" ).arg( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) ); else - statement = QStringLiteral( "select min(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).STX else NULL end), min(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).STY else NULL end), max(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).STX else NULL end), max(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).STY else NULL end)" ).arg( mGeometryColName ); + statement = QStringLiteral( "select min(case when (%1.STIsValid() = 1) THEN %1.STPointN(1).STX else NULL end), min(case when (%1.STIsValid() = 1) THEN %1.STPointN(1).STY else NULL end), max(case when (%1.STIsValid() = 1) THEN %1.STPointN(1).STX else NULL end), max(case when (%1.STIsValid() = 1) THEN %1.STPointN(1).STY else NULL end)" ).arg( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) ); } else { if ( mDisableInvalidGeometryHandling ) - statement = QStringLiteral( "select min([%1].STPointN(1).Long), min([%1].STPointN(1).Lat), max([%1].STPointN(1).Long), max([%1].STPointN(1).Lat)" ).arg( mGeometryColName ); + statement = QStringLiteral( "select min(%1.STPointN(1).Long), min(%1.STPointN(1).Lat), max(%1.STPointN(1).Long), max(%1.STPointN(1).Lat)" ).arg( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) ); else - statement = QStringLiteral( "select min(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).Long else NULL end), min(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).Lat else NULL end), max(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).Long else NULL end), max(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).Lat else NULL end)" ).arg( mGeometryColName ); + statement = QStringLiteral( "select min(case when (%1.STIsValid() = 1) THEN %1.STPointN(1).Long else NULL end), min(case when (%1.STIsValid() = 1) THEN %1.STPointN(1).Lat else NULL end), max(case when (%1.STIsValid() = 1) THEN %1.STPointN(1).Long else NULL end), max(case when (%1.STIsValid() = 1) THEN %1.STPointN(1).Lat else NULL end)" ).arg( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) ); } // we will first try to sample a small portion of the table/view, so the count of rows involved @@ -732,22 +756,28 @@ void QgsMssqlProvider::UpdateStatistics( bool estimate ) const if ( mGeometryColType == QLatin1String( "geometry" ) ) { if ( mDisableInvalidGeometryHandling ) - statement = QStringLiteral( "select min([%1].STEnvelope().STPointN(1).STX), min([%1].STEnvelope().STPointN(1).STY), max([%1].STEnvelope().STPointN(3).STX), max([%1].STEnvelope().STPointN(3).STY)" ).arg( mGeometryColName ); + statement = QStringLiteral( "select min(%1.STEnvelope().STPointN(1).STX), min(%1.STEnvelope().STPointN(1).STY), max(%1.STEnvelope().STPointN(3).STX), max(%1.STEnvelope().STPointN(3).STY)" ).arg( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) ); else - statement = QStringLiteral( "select min(case when ([%1].STIsValid() = 1) THEN [%1].STEnvelope().STPointN(1).STX else NULL end), min(case when ([%1].STIsValid() = 1) THEN [%1].STEnvelope().STPointN(1).STY else NULL end), max(case when ([%1].STIsValid() = 1) THEN [%1].STEnvelope().STPointN(3).STX else NULL end), max(case when ([%1].STIsValid() = 1) THEN [%1].STEnvelope().STPointN(3).STY else NULL end)" ).arg( mGeometryColName ); + statement = QStringLiteral( "select min(case when (%1.STIsValid() = 1) THEN %1.STEnvelope().STPointN(1).STX else NULL end), min(case when (%1.STIsValid() = 1) THEN %1.STEnvelope().STPointN(1).STY else NULL end), max(case when (%1.STIsValid() = 1) THEN %1.STEnvelope().STPointN(3).STX else NULL end), max(case when (%1.STIsValid() = 1) THEN %1.STEnvelope().STPointN(3).STY else NULL end)" ).arg( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) ); } else { - statement = QStringLiteral( "select [%1]" ).arg( mGeometryColName ); + statement = QStringLiteral( "select %1" ).arg( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) ); readAllGeography = true; } } - statement += QStringLiteral( " from [%1].[%2]" ).arg( mSchemaName, mTableName ); - - if ( !mSqlWhereClause.isEmpty() ) + if ( mIsQuery ) { - statement += " where (" + mSqlWhereClause + ')'; + statement += QStringLiteral( " FROM (%1) q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() ); + } + else + { + statement += QStringLiteral( " FROM %1.%2" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) ); + if ( !mSqlWhereClause.isEmpty() ) + { + statement += " where (" + mSqlWhereClause + ')'; + } } if ( estimate ) @@ -763,7 +793,7 @@ void QgsMssqlProvider::UpdateStatistics( bool estimate ) const for ( const auto idx : mPrimaryKeyAttrs ) { const QgsField &fld = mAttributeFields.at( idx ); - cols += delim + QStringLiteral( "[%1]" ).arg( fld.name() ); + cols += delim + QgsMssqlUtils::quotedIdentifier( fld.name() ); delim = QStringLiteral( "," ); } @@ -857,13 +887,22 @@ long long QgsMssqlProvider::featureCount() const QSqlQuery query = createQuery(); query.setForwardOnly( true ); - const QString statement = QStringLiteral( - "SELECT rows" - " FROM sys.tables t" - " JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0,1)" - " WHERE SCHEMA_NAME(t.schema_id) = %1 AND OBJECT_NAME(t.OBJECT_ID) = %2" - ) - .arg( quotedValue( mSchemaName ), quotedValue( mTableName ) ); + QString statement; + if ( !mIsQuery ) + { + statement = QStringLiteral( + "SELECT rows" + " FROM sys.tables t" + " JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0,1)" + " WHERE SCHEMA_NAME(t.schema_id) = %1 AND OBJECT_NAME(t.OBJECT_ID) = %2" + ) + .arg( QgsMssqlUtils::quotedValue( mSchemaName ), QgsMssqlUtils::quotedValue( mTableName ) ); + } + else + { + statement = { QStringLiteral( R"raw(SELECT COUNT(*) FROM (%1) q)raw" ) + .arg( mQuery ) }; + } if ( LoggedExec( query, statement ) && query.next() ) { @@ -873,7 +912,8 @@ long long QgsMssqlProvider::featureCount() const { // We couldn't get the rows from the sys tables. Can that ever happen? // Should just do a select count(*) here. - return -1; + QgsDebugError( QStringLiteral( "Could not retrieve feature count using %1: %2 " ).arg( statement, query.lastError().text() ) ); + return static_cast< long long >( Qgis::FeatureCountState::UnknownCount ); } } @@ -900,6 +940,9 @@ Qgis::ProviderStyleStorageCapabilities QgsMssqlProvider::styleStorageCapabilitie bool QgsMssqlProvider::addFeatures( QgsFeatureList &flist, Flags flags ) { + if ( mIsQuery ) + return false; + for ( QgsFeatureList::iterator it = flist.begin(); it != flist.end(); ++it ) { if ( it->hasGeometry() && mWkbType == Qgis::WkbType::NoGeometry ) @@ -1213,6 +1256,9 @@ bool QgsMssqlProvider::addAttributes( const QList &attributes ) if ( attributes.isEmpty() ) return true; + if ( mIsQuery ) + return false; + for ( QList::const_iterator it = attributes.begin(); it != attributes.end(); ++it ) { QString type = it->typeName(); @@ -1255,6 +1301,9 @@ bool QgsMssqlProvider::addAttributes( const QList &attributes ) bool QgsMssqlProvider::deleteAttributes( const QgsAttributeIds &attributes ) { + if ( mIsQuery ) + return false; + QString statement; for ( QgsAttributeIds::const_iterator it = attributes.begin(); it != attributes.end(); ++it ) @@ -1291,6 +1340,9 @@ bool QgsMssqlProvider::deleteAttributes( const QgsAttributeIds &attributes ) bool QgsMssqlProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_map ) { + if ( mIsQuery ) + return false; + if ( attr_map.isEmpty() ) return true; @@ -1454,6 +1506,9 @@ bool QgsMssqlProvider::changeAttributeValues( const QgsChangedAttributesMap &att bool QgsMssqlProvider::changeGeometryValues( const QgsGeometryMap &geometry_map ) { + if ( mIsQuery ) + return false; + if ( geometry_map.isEmpty() ) return true; @@ -1528,6 +1583,9 @@ bool QgsMssqlProvider::changeGeometryValues( const QgsGeometryMap &geometry_map bool QgsMssqlProvider::deleteFeatures( const QgsFeatureIds &ids ) { + if ( mIsQuery ) + return false; + if ( mPrimaryKeyAttrs.isEmpty() ) return false; @@ -1609,21 +1667,28 @@ void QgsMssqlProvider::updateExtents() Qgis::VectorProviderCapabilities QgsMssqlProvider::capabilities() const { - Qgis::VectorProviderCapabilities cap = Qgis::VectorProviderCapability::CreateAttributeIndex | Qgis::VectorProviderCapability::AddFeatures | Qgis::VectorProviderCapability::AddAttributes | Qgis::VectorProviderCapability::TransactionSupport; - bool hasGeom = false; - if ( !mGeometryColName.isEmpty() ) + Qgis::VectorProviderCapabilities cap; + const bool hasGeom = !mGeometryColName.isEmpty(); + if ( !mIsQuery ) { - hasGeom = true; - cap |= Qgis::VectorProviderCapability::CreateSpatialIndex; + cap |= Qgis::VectorProviderCapability::CreateAttributeIndex | Qgis::VectorProviderCapability::AddFeatures | Qgis::VectorProviderCapability::AddAttributes | Qgis::VectorProviderCapability::TransactionSupport; + if ( hasGeom ) + { + cap |= Qgis::VectorProviderCapability::CreateSpatialIndex; + } } if ( mPrimaryKeyAttrs.isEmpty() ) return cap; - if ( hasGeom ) + cap |= Qgis::VectorProviderCapability::SelectAtId; + + if ( hasGeom && !mIsQuery ) cap |= Qgis::VectorProviderCapability::ChangeGeometries; + if ( !mIsQuery ) + cap |= Qgis::VectorProviderCapability::DeleteFeatures | Qgis::VectorProviderCapability::ChangeAttributeValues | Qgis::VectorProviderCapability::DeleteAttributes; - return cap | Qgis::VectorProviderCapability::DeleteFeatures | Qgis::VectorProviderCapability::ChangeAttributeValues | Qgis::VectorProviderCapability::DeleteAttributes | Qgis::VectorProviderCapability::SelectAtId; + return cap; } bool QgsMssqlProvider::createSpatialIndex() @@ -1761,13 +1826,14 @@ bool QgsMssqlProvider::setSubsetString( const QString &theSQL, bool ) mSqlWhereClause = theSQL.trimmed(); - QString sql = QStringLiteral( "SELECT count(*) FROM " ); - - sql += QStringLiteral( "[%1].[%2]" ).arg( mSchemaName, mTableName ); - - if ( !mSqlWhereClause.isEmpty() ) + QString sql; + if ( mIsQuery ) { - sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause ); + sql = QStringLiteral( "SELECT count(*) FROM %1 q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() ); + } + else + { + sql = QStringLiteral( "SELECT count(*) FROM %1.%2 %3" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ), !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() ); } QSqlQuery query = createQuery(); @@ -1831,6 +1897,16 @@ QStringList QgsMssqlProvider::subLayers() const return mTables; } +Qgis::VectorLayerTypeFlags QgsMssqlProvider::vectorLayerTypeFlags() const +{ + Qgis::VectorLayerTypeFlags flags; + if ( mValid && mIsQuery ) + { + flags.setFlag( Qgis::VectorLayerTypeFlag::SqlQuery ); + } + return flags; +} + bool QgsMssqlProvider::convertField( QgsField &field ) { QString fieldType = QStringLiteral( "nvarchar(max)" ); //default to string @@ -2066,7 +2142,7 @@ Qgis::VectorExportResult QgsMssqlProvider::createEmptyLayer( const QString &uri, } sql = QStringLiteral( "IF NOT EXISTS (SELECT * FROM spatial_ref_sys WHERE srid=%1) INSERT INTO spatial_ref_sys (srid, auth_name, auth_srid, srtext, proj4text) VALUES (%1, %2, %3, %4, %5)" ) .arg( srid ) - .arg( quotedValue( auth_name ), auth_srid, quotedValue( srs.toWkt() ), quotedValue( srs.toProj() ) ); + .arg( QgsMssqlUtils::quotedValue( auth_name ), auth_srid, QgsMssqlUtils::quotedValue( srs.toWkt() ), QgsMssqlUtils::quotedValue( srs.toProj() ) ); logWrapper.reset( new QgsDatabaseQueryLogWrapper( sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), QGS_QUERY_LOG_ORIGIN ) ); @@ -2287,7 +2363,7 @@ Qgis::VectorExportResult QgsMssqlProviderMetadata::createEmptyLayer( const QStri QString buildfTableCatalogClause( const QgsDataSourceUri &dsUri ) { - return QStringLiteral( "f_table_catalog%1" ).arg( dsUri.database().isEmpty() ? QStringLiteral( " IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsMssqlProvider::quotedValue( dsUri.database() ) ) ); + return QStringLiteral( "f_table_catalog%1" ).arg( dsUri.database().isEmpty() ? QStringLiteral( " IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsMssqlUtils::quotedValue( dsUri.database() ) ) ); } @@ -2330,10 +2406,10 @@ bool QgsMssqlProviderMetadata::styleExists( const QString &uri, const QString &s " AND f_geometry_column=%4" " AND styleName=%5" ) .arg( buildfTableCatalogClause( dsUri ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) ) - .arg( QgsMssqlProvider::quotedValue( styleId.isEmpty() ? dsUri.table() : styleId ) ); + .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) ) + .arg( QgsMssqlUtils::quotedValue( styleId.isEmpty() ? dsUri.table() : styleId ) ); if ( !LoggedExecMetadata( query, checkQuery, uri ) ) { @@ -2424,16 +2500,16 @@ bool QgsMssqlProviderMetadata::saveStyle( const QString &uri, const QString &qml ") VALUES (" "%1,%2,%3,%4,%5,%6,%7,%8,%9,%10%12" ")" ) - .arg( QgsMssqlProvider::quotedValue( dsUri.database() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) ) - .arg( QgsMssqlProvider::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) ) - .arg( QgsMssqlProvider::quotedValue( qmlStyle ) ) - .arg( QgsMssqlProvider::quotedValue( sldStyle ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.database() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) ) + .arg( QgsMssqlUtils::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) ) + .arg( QgsMssqlUtils::quotedValue( qmlStyle ) ) + .arg( QgsMssqlUtils::quotedValue( sldStyle ) ) .arg( useAsDefault ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ) - .arg( QgsMssqlProvider::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.username() ) ) + .arg( QgsMssqlUtils::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.username() ) ) .arg( uiFileColumn ) .arg( uiFileValue ); @@ -2445,10 +2521,10 @@ bool QgsMssqlProviderMetadata::saveStyle( const QString &uri, const QString &qml " AND f_geometry_column=%4" " AND styleName=%5" ) .arg( buildfTableCatalogClause( dsUri ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) ) - .arg( QgsMssqlProvider::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) ); + .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) ) + .arg( QgsMssqlUtils::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) ); if ( !LoggedExecMetadata( query, checkQuery, uri ) ) { @@ -2471,15 +2547,15 @@ bool QgsMssqlProviderMetadata::saveStyle( const QString &uri, const QString &qml " AND f_geometry_column=%9" " AND styleName=%10" ) .arg( useAsDefault ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ) - .arg( QgsMssqlProvider::quotedValue( qmlStyle ) ) - .arg( QgsMssqlProvider::quotedValue( sldStyle ) ) - .arg( QgsMssqlProvider::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.username() ) ) + .arg( QgsMssqlUtils::quotedValue( qmlStyle ) ) + .arg( QgsMssqlUtils::quotedValue( sldStyle ) ) + .arg( QgsMssqlUtils::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.username() ) ) .arg( buildfTableCatalogClause( dsUri ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) ) - .arg( QgsMssqlProvider::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) ); + .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) ) + .arg( QgsMssqlUtils::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) ); } if ( useAsDefault ) { @@ -2490,9 +2566,9 @@ bool QgsMssqlProviderMetadata::saveStyle( const QString &uri, const QString &qml " AND f_table_name=%3" " AND f_geometry_column=%4" ) .arg( buildfTableCatalogClause( dsUri ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) ); + .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) ); sql = QStringLiteral( "%1; %2;" ).arg( removeDefaultSql, sql ); } @@ -2559,9 +2635,9 @@ QString QgsMssqlProviderMetadata::loadStoredStyle( const QString &uri, QString & " AND f_geometry_column=%4" " ORDER BY useAsDefault desc" ) .arg( buildfTableCatalogClause( dsUri ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) ); + .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) ); if ( !LoggedExecMetadata( query, selectQmlQuery, uri ) ) { @@ -2622,9 +2698,9 @@ int QgsMssqlProviderMetadata::listStyles( const QString &uri, QStringList &ids, " AND f_geometry_column=%4" " ORDER BY useasdefault DESC, update_time DESC" ) .arg( fTableCatalogClause ) - .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) ); + .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) ); bool queryOk = LoggedExecMetadata( query, selectRelatedQuery, uri ); @@ -2647,9 +2723,9 @@ int QgsMssqlProviderMetadata::listStyles( const QString &uri, QStringList &ids, " WHERE NOT (%1 AND f_table_schema=%2 AND f_table_name=%3 AND f_geometry_column=%4)" " ORDER BY update_time DESC" ) .arg( fTableCatalogClause ) - .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) ) - .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) ); + .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) ) + .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) ); QgsDebugMsgLevel( selectOthersQuery, 2 ); queryOk = LoggedExecMetadata( query, selectOthersQuery, uri ); @@ -2713,7 +2789,7 @@ QString QgsMssqlProviderMetadata::getStyleById( const QString &uri, const QStrin query.setForwardOnly( true ); QString style; - const QString selectQmlQuery = QStringLiteral( "SELECT styleQml FROM layer_styles WHERE id=%1" ).arg( QgsMssqlProvider::quotedValue( styleId ) ); + const QString selectQmlQuery = QStringLiteral( "SELECT styleQml FROM layer_styles WHERE id=%1" ).arg( QgsMssqlUtils::quotedValue( styleId ) ); const bool queryOk = LoggedExecMetadata( query, selectQmlQuery, uri ); if ( !queryOk ) @@ -2989,7 +3065,7 @@ QString QgsMssqlProvider::whereClauseFid( QgsFeatureId featureId ) for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i ) { const QgsField &fld = mAttributeFields.at( mPrimaryKeyAttrs[i] ); - whereClause += QStringLiteral( "%1[%2]=%3" ).arg( delim, fld.name(), quotedValue( pkVals[i] ) ); + whereClause += QStringLiteral( "%1[%2]=%3" ).arg( delim, fld.name(), QgsMssqlUtils::quotedValue( pkVals[i] ) ); delim = QStringLiteral( " AND " ); } @@ -3074,7 +3150,7 @@ bool QgsMssqlProvider::getExtentFromGeometryColumns( QgsRectangle &extent ) cons "FROM geometry_columns WHERE f_table_name = %1 AND f_table_schema = %2 " "AND NOT (qgis_xmin IS NULL OR qgis_xmax IS NULL OR qgis_ymin IS NULL OR qgis_ymax IS NULL)" ); - const QString statement = sql.arg( quotedValue( mTableName ), quotedValue( mSchemaName ) ); + const QString statement = sql.arg( QgsMssqlUtils::quotedValue( mTableName ), QgsMssqlUtils::quotedValue( mSchemaName ) ); if ( LoggedExec( query, statement ) && query.isActive() ) { @@ -3101,7 +3177,7 @@ bool QgsMssqlProvider::getPrimaryKeyFromGeometryColumns( QStringList &primaryKey const QString sql = QStringLiteral( "SELECT qgis_pkey FROM geometry_columns " "WHERE f_table_name = %1 AND f_table_schema = %2 AND NOT qgis_pkey IS NULL" ); - const QString statement = sql.arg( quotedValue( mTableName ), quotedValue( mSchemaName ) ); + const QString statement = sql.arg( QgsMssqlUtils::quotedValue( mTableName ), QgsMssqlUtils::quotedValue( mSchemaName ) ); if ( LoggedExec( query, statement ) && query.isActive() ) { diff --git a/src/providers/mssql/qgsmssqlprovider.h b/src/providers/mssql/qgsmssqlprovider.h index 2eeb685bd724..92aee96c9097 100644 --- a/src/providers/mssql/qgsmssqlprovider.h +++ b/src/providers/mssql/qgsmssqlprovider.h @@ -67,6 +67,7 @@ class QgsMssqlProvider final : public QgsVectorDataProvider void updateExtents() override; QString storageType() const override; QStringList subLayers() const override; + Qgis::VectorLayerTypeFlags vectorLayerTypeFlags() const override; QVariant minimumValue( int index ) const override; QVariant maximumValue( int index ) const override; QSet uniqueValues( int index, int limit = -1 ) const override; @@ -128,10 +129,6 @@ class QgsMssqlProvider final : public QgsVectorDataProvider // Parse type name and num coordinates as stored in geometry_columns table and returns normalized (M, Z or ZM) type name static QString typeFromMetadata( const QString &typeName, int numCoords ); - //! Convert values to quoted values for database work - static QString quotedValue( const QVariant &value ); - static QString quotedIdentifier( const QString &value ); - QString defaultValueClause( int fieldId ) const override; QVariant defaultValue( int fieldId ) const override; @@ -181,6 +178,9 @@ class QgsMssqlProvider final : public QgsVectorDataProvider bool mValid = false; + bool mIsQuery = false; + QString mQuery; + bool mUseWkb = false; bool mUseEstimatedMetadata = false; bool mSkipFailures = false; diff --git a/src/providers/mssql/qgsmssqlproviderconnection.cpp b/src/providers/mssql/qgsmssqlproviderconnection.cpp index 3d71dd8e21bd..75d280a723c8 100644 --- a/src/providers/mssql/qgsmssqlproviderconnection.cpp +++ b/src/providers/mssql/qgsmssqlproviderconnection.cpp @@ -20,6 +20,7 @@ #include "qgsmssqlproviderconnection.h" #include "qgsmssqlconnection.h" #include "qgsmssqldatabase.h" +#include "qgsmssqlutils.h" #include "qgssettings.h" #include "qgsmssqlprovider.h" #include "qgsexception.h" @@ -29,6 +30,7 @@ #include "qgsmssqlsqlquerybuilder.h" #include "qgsdbquerylog.h" #include "qgsdbquerylog_p.h" +#include "qgsvectorlayer.h" #include #include @@ -88,6 +90,7 @@ void QgsMssqlProviderConnection::setDefaultCapabilities() Capability::DropSchema, Capability::CreateSchema, Capability::ExecuteSql, + Capability::SqlLayers, Capability::Tables, Capability::Schemas, Capability::Spatial, @@ -137,8 +140,8 @@ void QgsMssqlProviderConnection::dropTablePrivate( const QString &schema, const DELETE FROM geometry_columns WHERE f_table_schema = @schema AND f_table_name = @table )raw" ) - .arg( QgsMssqlProvider::quotedValue( QStringLiteral( "master" ) ), // in my testing docker, it is 'master' instead of QgsMssqlProvider::quotedValue( QgsDataSourceUri( uri() ).database() ), - QgsMssqlProvider::quotedValue( name ), QgsMssqlProvider::quotedValue( schema ), QgsMssqlProvider::quotedIdentifier( name ), QgsMssqlProvider::quotedIdentifier( schema ) ) }; + .arg( QgsMssqlUtils::quotedValue( QStringLiteral( "master" ) ), // in my testing docker, it is 'master' instead of QgsMssqlUtils::quotedValue( QgsDataSourceUri( uri() ).database() ), + QgsMssqlUtils::quotedValue( name ), QgsMssqlUtils::quotedValue( schema ), QgsMssqlUtils::quotedIdentifier( name ), QgsMssqlUtils::quotedIdentifier( schema ) ) }; executeSqlPrivate( sql ); } @@ -193,7 +196,7 @@ void QgsMssqlProviderConnection::createSchema( const QString &schemaName ) const { checkCapability( Capability::CreateSchema ); executeSqlPrivate( QStringLiteral( "CREATE SCHEMA %1" ) - .arg( QgsMssqlProvider::quotedIdentifier( schemaName ) ) ); + .arg( QgsMssqlUtils::quotedIdentifier( schemaName ) ) ); } void QgsMssqlProviderConnection::dropSchema( const QString &schemaName, bool force ) const @@ -211,7 +214,7 @@ void QgsMssqlProviderConnection::dropSchema( const QString &schemaName, bool for } } executeSqlPrivate( QStringLiteral( "DROP SCHEMA %1" ) - .arg( QgsMssqlProvider::quotedIdentifier( schemaName ) ) ); + .arg( QgsMssqlUtils::quotedIdentifier( schemaName ) ) ); } QgsAbstractDatabaseProviderConnection::QueryResult QgsMssqlProviderConnection::execSql( const QString &sql, QgsFeedback *feedback ) const @@ -376,7 +379,7 @@ QList QgsMssqlProviderConn QString tableNameFilter; if ( !table.isEmpty() ) { - tableNameFilter = QStringLiteral( " AND sys.objects.name = %1" ).arg( QgsMssqlProvider::quotedValue( table ) ); + tableNameFilter = QStringLiteral( " AND sys.objects.name = %1" ).arg( QgsMssqlUtils::quotedValue( table ) ); } QString query { QStringLiteral( "SELECT " ) }; @@ -384,7 +387,7 @@ QList QgsMssqlProviderConn if ( useGeometryColumnsOnly ) { query += QStringLiteral( "f_table_schema, f_table_name, f_geometry_column, srid, geometry_type, 0 FROM geometry_columns WHERE f_table_schema = %1" ) - .arg( QgsMssqlProvider::quotedValue( schema ) ); + .arg( QgsMssqlUtils::quotedValue( schema ) ); } else { @@ -402,7 +405,7 @@ QList QgsMssqlProviderConn AND (sys.types.name = 'geometry' OR sys.types.name = 'geography') AND (sys.objects.type = 'U' OR sys.objects.type = 'V') )raw" ) - .arg( QgsMssqlProvider::quotedValue( schema ), tableNameFilter ); + .arg( QgsMssqlUtils::quotedValue( schema ), tableNameFilter ); } if ( allowGeometrylessTables ) @@ -422,7 +425,7 @@ QList QgsMssqlProviderConn AND sys.objects.object_id = sc1.object_id ) AND (sys.objects.type = 'U' OR sys.objects.type = 'V') )raw" ) - .arg( QgsMssqlProvider::quotedValue( schema ), tableNameFilter ); + .arg( QgsMssqlUtils::quotedValue( schema ), tableNameFilter ); } const QList results { executeSqlPrivate( query, false ).rows() }; @@ -459,7 +462,7 @@ SELECT %4 UPPER( %1.STGeometryType()), %1.STSrid, GROUP BY %1.STGeometryType(), %1.STSrid, %1.HasZ, %1.HasM )raw" ) - .arg( QgsMssqlProvider::quotedIdentifier( table.geometryColumn() ), QgsMssqlProvider::quotedIdentifier( table.schema() ), QgsMssqlProvider::quotedIdentifier( table.tableName() ), useEstimatedMetadata ? "TOP 1" : "" ); + .arg( QgsMssqlUtils::quotedIdentifier( table.geometryColumn() ), QgsMssqlUtils::quotedIdentifier( table.schema() ), QgsMssqlUtils::quotedIdentifier( table.tableName() ), useEstimatedMetadata ? "TOP 1" : "" ); } else { @@ -469,7 +472,7 @@ SELECT %4 UPPER( %1.STGeometryType()), %1.STSrid, %1.STSrid as srid, %1.HasZ as hasz, %1.HasM as hasm FROM %2.%3 WHERE %1 IS NOT NULL) AS a GROUP BY type, srid, hasz, hasm )raw" ) - .arg( QgsMssqlProvider::quotedIdentifier( table.geometryColumn() ), QgsMssqlProvider::quotedIdentifier( table.schema() ), QgsMssqlProvider::quotedIdentifier( table.tableName() ), useEstimatedMetadata ? "TOP 1" : "" ); + .arg( QgsMssqlUtils::quotedIdentifier( table.geometryColumn() ), QgsMssqlUtils::quotedIdentifier( table.schema() ), QgsMssqlUtils::quotedIdentifier( table.tableName() ), useEstimatedMetadata ? "TOP 1" : "" ); } try @@ -492,7 +495,7 @@ SELECT %4 UPPER( %1.STGeometryType()), %1.STSrid, } catch ( QgsProviderConnectionException &ex ) { - QgsMessageLog::logMessage( QObject::tr( "Error retrieving geometry type for '%1' on table %2.%3:\n%4" ).arg( table.geometryColumn(), QgsMssqlProvider::quotedIdentifier( table.schema() ), QgsMssqlProvider::quotedIdentifier( table.tableName() ), ex.what() ), QStringLiteral( "MSSQL" ), Qgis::MessageLevel::Warning ); + QgsMessageLog::logMessage( QObject::tr( "Error retrieving geometry type for '%1' on table %2.%3:\n%4" ).arg( table.geometryColumn(), QgsMssqlUtils::quotedIdentifier( table.schema() ), QgsMssqlUtils::quotedIdentifier( table.tableName() ), ex.what() ), QStringLiteral( "MSSQL" ), Qgis::MessageLevel::Warning ); } } else @@ -640,3 +643,114 @@ QgsProviderSqlQueryBuilder *QgsMssqlProviderConnection::queryBuilder() const { return new QgsMsSqlSqlQueryBuilder(); } + +QgsVectorLayer *QgsMssqlProviderConnection::createSqlVectorLayer( const SqlVectorLayerOptions &options ) const +{ + // Precondition + if ( options.sql.isEmpty() ) + { + throw QgsProviderConnectionException( QObject::tr( "Could not create a SQL vector layer: SQL expression is empty." ) ); + } + + QgsDataSourceUri tUri( uri() ); + + tUri.setSql( options.filter ); + tUri.disableSelectAtId( options.disableSelectAtId ); + + if ( !options.primaryKeyColumns.isEmpty() ) + { + tUri.setKeyColumn( options.primaryKeyColumns.join( ',' ) ); + tUri.setTable( QStringLiteral( "(%1)" ).arg( sanitizeSqlForQueryLayer( options.sql ) ) ); + } + else + { + int pkId { 0 }; + while ( options.sql.contains( QStringLiteral( "_uid%1_" ).arg( pkId ), Qt::CaseSensitivity::CaseInsensitive ) ) + { + pkId++; + } + tUri.setKeyColumn( QStringLiteral( "_uid%1_" ).arg( pkId ) ); + + int sqlId { 0 }; + while ( options.sql.contains( QStringLiteral( "_subq_%1_" ).arg( sqlId ), Qt::CaseSensitivity::CaseInsensitive ) ) + { + sqlId++; + } + tUri.setTable( QStringLiteral( "(SELECT row_number() OVER (ORDER BY (SELECT NULL)) AS _uid%1_, * FROM (%2\n) AS _subq_%3_\n)" ).arg( QString::number( pkId ), sanitizeSqlForQueryLayer( options.sql ), QString::number( sqlId ) ) ); + } + + if ( !options.geometryColumn.isEmpty() ) + { + tUri.setGeometryColumn( options.geometryColumn ); + + const QString sql = QStringLiteral( "SELECT %3" + " UPPER(%1.STGeometryType())," + " %1.STSrid," + " %1.HasZ," + " %1.HasM" + " FROM (%2) AS _subq_" + " WHERE %1 IS NOT NULL %4" + " GROUP BY %1.STGeometryType(), %1.STSrid, %1.HasZ, %1.HasM" ) + .arg( QgsMssqlUtils::quotedIdentifier( options.geometryColumn ), sanitizeSqlForQueryLayer( options.sql ), tUri.useEstimatedMetadata() ? "TOP 1" : "", options.filter.isEmpty() ? QString() : QStringLiteral( " AND %1" ).arg( options.filter ) ); + + try + { + const QList> candidates { executeSql( sql ) }; + + QStringList types; + QStringList srids; + + for ( const QList &row : std::as_const( candidates ) ) + { + const bool hasZ { row[2].toString() == '1' }; + const bool hasM { row[3].toString() == '1' }; + const int dimensions { 2 + ( ( hasZ && hasM ) ? 2 : ( ( hasZ || hasM ) ? 1 : 0 ) ) }; + QString typeName { row[0].toString().toUpper() }; + if ( typeName.isEmpty() ) + continue; + + if ( hasM && !typeName.endsWith( 'M' ) ) + { + typeName.append( 'M' ); + } + const QString type { QgsMssqlProvider::typeFromMetadata( typeName, dimensions ) }; + const QString srid = row[1].toString(); + + if ( type.isEmpty() ) + continue; + + types << type; + srids << srid; + } + + if ( !srids.isEmpty() ) + tUri.setSrid( srids.at( 0 ) ); + + if ( !types.isEmpty() ) + { + tUri.setWkbType( QgsMssqlUtils::wkbTypeFromGeometryType( types.at( 0 ) ) ); + } + } + catch ( QgsProviderConnectionException &e ) + { + QgsDebugError( e.what() ); + } + } + + QgsVectorLayer::LayerOptions vectorLayerOptions { false, true }; + vectorLayerOptions.skipCrsValidation = true; + return new QgsVectorLayer { tUri.uri( false ), options.layerName.isEmpty() ? QStringLiteral( "QueryLayer" ) : options.layerName, providerKey(), vectorLayerOptions }; +} + +QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions QgsMssqlProviderConnection::sqlOptions( const QString &layerSource ) +{ + SqlVectorLayerOptions options; + const QgsDataSourceUri tUri( layerSource ); + options.primaryKeyColumns = tUri.keyColumn().split( ',' ); + options.disableSelectAtId = tUri.selectAtIdDisabled(); + options.geometryColumn = tUri.geometryColumn(); + options.filter = tUri.sql(); + const QString trimmedTable { tUri.table().trimmed() }; + options.sql = trimmedTable.startsWith( '(' ) ? trimmedTable.mid( 1 ).chopped( 1 ) : QStringLiteral( "SELECT * FROM %1" ).arg( tUri.quotedTablename() ); + return options; +} diff --git a/src/providers/mssql/qgsmssqlproviderconnection.h b/src/providers/mssql/qgsmssqlproviderconnection.h index 478421f78e7d..c8bdbdeb19f9 100644 --- a/src/providers/mssql/qgsmssqlproviderconnection.h +++ b/src/providers/mssql/qgsmssqlproviderconnection.h @@ -68,6 +68,8 @@ class QgsMssqlProviderConnection : public QgsAbstractDatabaseProviderConnection QIcon icon() const override; QList nativeTypes() const override; QgsProviderSqlQueryBuilder *queryBuilder() const override; + QgsVectorLayer *createSqlVectorLayer( const SqlVectorLayerOptions &options ) const override; + SqlVectorLayerOptions sqlOptions( const QString &layerSource ) override; private: QgsAbstractDatabaseProviderConnection::QueryResult executeSqlPrivate( const QString &sql, bool resolveTypes = true, QgsFeedback *feedback = nullptr ) const; diff --git a/src/providers/mssql/qgsmssqlsqlquerybuilder.cpp b/src/providers/mssql/qgsmssqlsqlquerybuilder.cpp index a848a7c340eb..d99d5e73eb74 100644 --- a/src/providers/mssql/qgsmssqlsqlquerybuilder.cpp +++ b/src/providers/mssql/qgsmssqlsqlquerybuilder.cpp @@ -15,17 +15,17 @@ email : nyall dot dawson at gmail dot com ***************************************************************************/ #include "qgsmssqlsqlquerybuilder.h" -#include "qgsmssqlprovider.h" +#include "qgsmssqlutils.h" QString QgsMsSqlSqlQueryBuilder::createLimitQueryForTable( const QString &schema, const QString &name, int limit ) const { if ( schema.isEmpty() ) - return QStringLiteral( "SELECT TOP %1 * FROM %2" ).arg( limit ).arg( quoteIdentifier( name ) ); + return QStringLiteral( "SELECT TOP %1 * FROM %2" ).arg( limit ).arg( QgsMssqlUtils::quotedIdentifier( name ) ); else - return QStringLiteral( "SELECT TOP %1 * FROM %2.%3" ).arg( limit ).arg( quoteIdentifier( schema ), quoteIdentifier( name ) ); + return QStringLiteral( "SELECT TOP %1 * FROM %2.%3" ).arg( limit ).arg( QgsMssqlUtils::quotedIdentifier( schema ), QgsMssqlUtils::quotedIdentifier( name ) ); } QString QgsMsSqlSqlQueryBuilder::quoteIdentifier( const QString &identifier ) const { - return QgsMssqlProvider::quotedIdentifier( identifier ); + return QgsMssqlUtils::quotedIdentifier( identifier ); } diff --git a/src/providers/mssql/qgsmssqltablemodel.cpp b/src/providers/mssql/qgsmssqltablemodel.cpp index f4005f4fe119..ffb5124869e0 100644 --- a/src/providers/mssql/qgsmssqltablemodel.cpp +++ b/src/providers/mssql/qgsmssqltablemodel.cpp @@ -21,6 +21,7 @@ #include "qgslogger.h" #include "qgsdatasourceuri.h" #include "qgsiconutils.h" +#include "qgsmssqlutils.h" QgsMssqlTableModel::QgsMssqlTableModel( QObject *parent ) : QgsAbstractDbTableModel( parent ) @@ -90,7 +91,7 @@ void QgsMssqlTableModel::addTableEntry( const QgsMssqlLayerProperty &layerProper invisibleRootItem()->setChild( invisibleRootItem()->rowCount(), schemaItem ); } - Qgis::WkbType wkbType = QgsMssqlTableModel::wkbTypeFromMssql( layerProperty.type ); + Qgis::WkbType wkbType = QgsMssqlUtils::wkbTypeFromGeometryType( layerProperty.type ); if ( wkbType == Qgis::WkbType::Unknown && layerProperty.geometryColName.isEmpty() ) { wkbType = Qgis::WkbType::NoGeometry; @@ -298,7 +299,7 @@ void QgsMssqlTableModel::setGeometryTypesForTable( QgsMssqlLayerProperty layerPr else { // update existing row - Qgis::WkbType wkbType = QgsMssqlTableModel::wkbTypeFromMssql( typeList.at( 0 ) ); + Qgis::WkbType wkbType = QgsMssqlUtils::wkbTypeFromGeometryType( typeList.at( 0 ) ); row[DbtmType]->setIcon( QgsIconUtils::iconForWkbType( wkbType ) ); row[DbtmType]->setText( QgsWkbTypes::translatedDisplayString( wkbType ) ); @@ -416,12 +417,6 @@ QString QgsMssqlTableModel::layerURI( const QModelIndex &index, const QString &c return uri.uri(); } -Qgis::WkbType QgsMssqlTableModel::wkbTypeFromMssql( QString type ) -{ - type = type.toUpper(); - return QgsWkbTypes::parseType( type ); -} - void QgsMssqlTableModel::setConnectionName( const QString &connectionName ) { mConnectionName = connectionName; diff --git a/src/providers/mssql/qgsmssqltablemodel.h b/src/providers/mssql/qgsmssqltablemodel.h index 27a5bd310879..655c377dd9a7 100644 --- a/src/providers/mssql/qgsmssqltablemodel.h +++ b/src/providers/mssql/qgsmssqltablemodel.h @@ -87,8 +87,6 @@ class QgsMssqlTableModel : public QgsAbstractDbTableModel QString layerURI( const QModelIndex &index, const QString &connInfo, bool useEstimatedMetadata, bool disableInvalidGeometryHandling ); - static Qgis::WkbType wkbTypeFromMssql( QString dbType ); - void setConnectionName( const QString &connectionName ); private: diff --git a/src/providers/mssql/qgsmssqlutils.cpp b/src/providers/mssql/qgsmssqlutils.cpp new file mode 100644 index 000000000000..67340d6305a1 --- /dev/null +++ b/src/providers/mssql/qgsmssqlutils.cpp @@ -0,0 +1,163 @@ +/*************************************************************************** + qgsmssqlutils.cpp + -------------------------------------- + Date : February 2025 + Copyright : (C) 2025 by Nyall Dawson + Email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include "qgsmssqlutils.h" +#include "qgsvariantutils.h" +#include "qgslogger.h" +#include "qgsfield.h" +#include "qgswkbtypes.h" + +QString QgsMssqlUtils::quotedValue( const QVariant &value ) +{ + if ( QgsVariantUtils::isNull( value ) ) + return QStringLiteral( "NULL" ); + + switch ( value.userType() ) + { + case QMetaType::Type::Int: + case QMetaType::Type::LongLong: + case QMetaType::Type::Double: + return value.toString(); + + case QMetaType::Type::Bool: + return QString( value.toBool() ? '1' : '0' ); + + default: + case QMetaType::Type::QString: + QString v = value.toString(); + v.replace( '\'', QLatin1String( "''" ) ); + if ( v.contains( '\\' ) ) + return v.replace( '\\', QLatin1String( "\\\\" ) ).prepend( "N'" ).append( '\'' ); + else + return v.prepend( "N'" ).append( '\'' ); + } +} + +QString QgsMssqlUtils::quotedIdentifier( const QString &value ) +{ + return QStringLiteral( "[%1]" ).arg( value ); +} + +QMetaType::Type QgsMssqlUtils::convertSqlFieldType( const QString &systemTypeName ) +{ + QMetaType::Type type = QMetaType::Type::UnknownType; + // cloned branches are intentional here for improved readability + // NOLINTBEGIN(bugprone-branch-clone) + if ( systemTypeName.startsWith( QLatin1String( "decimal" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "numeric" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "real" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "float" ), Qt::CaseInsensitive ) ) + { + type = QMetaType::Type::Double; + } + else if ( systemTypeName.startsWith( QLatin1String( "char" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "nchar" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "varchar" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "nvarchar" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "text" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "ntext" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "uniqueidentifier" ), Qt::CaseInsensitive ) ) + { + type = QMetaType::Type::QString; + } + else if ( systemTypeName.startsWith( QLatin1String( "smallint" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "int" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "bit" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "tinyint" ), Qt::CaseInsensitive ) ) + { + type = QMetaType::Type::Int; + } + else if ( systemTypeName.startsWith( QLatin1String( "bigint" ), Qt::CaseInsensitive ) ) + { + type = QMetaType::Type::LongLong; + } + else if ( systemTypeName.startsWith( QLatin1String( "binary" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "varbinary" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "image" ), Qt::CaseInsensitive ) ) + { + type = QMetaType::Type::QByteArray; + } + else if ( systemTypeName.startsWith( QLatin1String( "datetime" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "smalldatetime" ), Qt::CaseInsensitive ) || systemTypeName.startsWith( QLatin1String( "datetime2" ), Qt::CaseInsensitive ) ) + { + type = QMetaType::Type::QDateTime; + } + else if ( systemTypeName.startsWith( QLatin1String( "date" ), Qt::CaseInsensitive ) ) + { + type = QMetaType::Type::QDate; + } + else if ( systemTypeName.startsWith( QLatin1String( "timestamp" ), Qt::CaseInsensitive ) ) + { + type = QMetaType::Type::QString; + } + else if ( systemTypeName.startsWith( QLatin1String( "time" ), Qt::CaseInsensitive ) ) + { + type = QMetaType::Type::QTime; + } + else + { + QgsDebugError( QStringLiteral( "Unknown field type: %1" ).arg( systemTypeName ) ); + // Everything else just dumped as a string. + type = QMetaType::Type::QString; + } + // NOLINTEND(bugprone-branch-clone) + + return type; +} + +QgsField QgsMssqlUtils::createField( const QString &name, const QString &systemTypeName, int length, int precision, int scale, bool nullable, bool unique, bool readOnly ) +{ + QgsField field; + const QMetaType::Type sqlType = convertSqlFieldType( systemTypeName ); + switch ( sqlType ) + { + case QMetaType::Type::QString: + { + // Field length in chars is "Length" of the sp_columns output, + // except for uniqueidentifiers which must use "Precision". + int stringLength = systemTypeName.startsWith( QStringLiteral( "uniqueidentifier" ), Qt::CaseInsensitive ) ? precision : length; + if ( systemTypeName.startsWith( QLatin1Char( 'n' ) ) ) + { + stringLength = stringLength / 2; + } + field = QgsField( name, sqlType, systemTypeName, stringLength ); + break; + } + + case QMetaType::Type::Double: + { + field = QgsField( name, sqlType, systemTypeName, precision, systemTypeName == QLatin1String( "decimal" ) || systemTypeName == QLatin1String( "numeric" ) ? scale : -1 ); + break; + } + + case QMetaType::Type::QDate: + case QMetaType::Type::QDateTime: + case QMetaType::Type::QTime: + { + field = QgsField( name, sqlType, systemTypeName, -1, -1 ); + break; + } + + default: + { + field = QgsField( name, sqlType, systemTypeName ); + break; + } + } + + // Set constraints + QgsFieldConstraints constraints; + if ( !nullable ) + constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider ); + if ( unique ) + constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider ); + field.setConstraints( constraints ); + + if ( readOnly ) + { + field.setReadOnly( true ); + } + return field; +} + +Qgis::WkbType QgsMssqlUtils::wkbTypeFromGeometryType( const QString &type ) +{ + return QgsWkbTypes::parseType( type.toUpper() ); +} diff --git a/src/providers/mssql/qgsmssqlutils.h b/src/providers/mssql/qgsmssqlutils.h new file mode 100644 index 000000000000..92205d7da8d9 --- /dev/null +++ b/src/providers/mssql/qgsmssqlutils.h @@ -0,0 +1,58 @@ +/*************************************************************************** + qgsmssqlutils.h + -------------------------------------- + Date : February 2025 + Copyright : (C) 2025 by Nyall Dawson + Email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * 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 QGSMSSQLUTILS_H +#define QGSMSSQLUTILS_H + +#include +#include +#include "qgis.h" + +class QgsField; + +/** + * Contains utility functions for working with Microsoft SQL Server databases. + */ +class QgsMssqlUtils +{ + public: + /** + * Returns a quoted string version of \a value, for safe use in a SQL query. + */ + static QString quotedValue( const QVariant &value ); + + /** + * Returns a quoted string version of a database \a identifier, for safe use in a SQL query. + */ + static QString quotedIdentifier( const QString &identifier ); + + /** + * Converts a SQL Server field system type string to the equivalent QVariant type. + */ + static QMetaType::Type convertSqlFieldType( const QString &systemTypeName ); + + /** + * Creates the equivalent QgsField corresponding to the properties of a SQL Server field. + */ + static QgsField createField( const QString &name, const QString &systemTypeName, int length, int precision, int scale, bool nullable, bool unique, bool readOnly ); + + /** + * Converts the string values from .STGeometryType() to a QGIS WKB type. + */ + static Qgis::WkbType wkbTypeFromGeometryType( const QString &type ); +}; + + +#endif // QGSMSSQLUTILS_H diff --git a/tests/src/providers/testqgsmssqlprovider.cpp b/tests/src/providers/testqgsmssqlprovider.cpp index e385b3a04030..5855b1573450 100644 --- a/tests/src/providers/testqgsmssqlprovider.cpp +++ b/tests/src/providers/testqgsmssqlprovider.cpp @@ -28,6 +28,7 @@ #include "qgsvectorlayer.h" #include "qgsproject.h" #include "qgsmssqlgeomcolumntypethread.h" +#include "qgsmssqldatabase.h" /** * \ingroup UnitTests @@ -53,6 +54,8 @@ class TestQgsMssqlProvider : public QObject void testGeomTypeResolutionValidNoWorkaround(); void testGeomTypeResolutionInvalid(); void testGeomTypeResolutionInvalidNoWorkaround(); + void testFieldsForTable(); + void testFieldsForQuery(); private: QString mDbConn; @@ -402,5 +405,94 @@ void TestQgsMssqlProvider::testGeomTypeResolutionInvalidNoWorkaround() QCOMPARE( result.isView, false ); } +void TestQgsMssqlProvider::testFieldsForTable() +{ + QgsDataSourceUri uri( mDbConn ); + + std::shared_ptr db = QgsMssqlDatabase::connectDb( uri ); + + QgsMssqlDatabase::FieldDetails details; + QString error; + QVERIFY( db->loadFields( details, QStringLiteral( "qgis_test" ), QStringLiteral( "someData" ), error ) ); + QCOMPARE( error, QString() ); + QCOMPARE( details.attributeFields.size(), 8 ); + QCOMPARE( details.attributeFields.at( 0 ).name(), QStringLiteral( "pk" ) ); + QCOMPARE( details.attributeFields.at( 0 ).type(), QVariant::Int ); + QCOMPARE( details.attributeFields.at( 0 ).typeName(), QStringLiteral( "int" ) ); + QCOMPARE( details.attributeFields.at( 1 ).name(), QStringLiteral( "cnt" ) ); + QCOMPARE( details.attributeFields.at( 1 ).type(), QVariant::Int ); + QCOMPARE( details.attributeFields.at( 1 ).typeName(), QStringLiteral( "int" ) ); + QCOMPARE( details.attributeFields.at( 2 ).name(), QStringLiteral( "name" ) ); + QCOMPARE( details.attributeFields.at( 2 ).type(), QVariant::String ); + QCOMPARE( details.attributeFields.at( 2 ).typeName(), QStringLiteral( "ntext" ) ); + QCOMPARE( details.attributeFields.at( 3 ).name(), QStringLiteral( "name2" ) ); + QCOMPARE( details.attributeFields.at( 3 ).type(), QVariant::String ); + QCOMPARE( details.attributeFields.at( 3 ).typeName(), QStringLiteral( "ntext" ) ); + QCOMPARE( details.attributeFields.at( 4 ).name(), QStringLiteral( "num_char" ) ); + QCOMPARE( details.attributeFields.at( 4 ).type(), QVariant::String ); + QCOMPARE( details.attributeFields.at( 4 ).typeName(), QStringLiteral( "ntext" ) ); + QCOMPARE( details.attributeFields.at( 5 ).name(), QStringLiteral( "dt" ) ); + QCOMPARE( details.attributeFields.at( 5 ).type(), QVariant::DateTime ); + QCOMPARE( details.attributeFields.at( 5 ).typeName(), QStringLiteral( "datetime" ) ); + QCOMPARE( details.attributeFields.at( 6 ).name(), QStringLiteral( "date" ) ); + QCOMPARE( details.attributeFields.at( 6 ).type(), QVariant::Date ); + QCOMPARE( details.attributeFields.at( 6 ).typeName(), QStringLiteral( "date" ) ); + QCOMPARE( details.attributeFields.at( 7 ).name(), QStringLiteral( "time" ) ); + QCOMPARE( details.attributeFields.at( 7 ).type(), QVariant::Time ); + QCOMPARE( details.attributeFields.at( 7 ).typeName(), QStringLiteral( "time" ) ); +} + +void TestQgsMssqlProvider::testFieldsForQuery() +{ + QgsDataSourceUri uri( mDbConn ); + + std::shared_ptr db = QgsMssqlDatabase::connectDb( uri ); + + QgsMssqlDatabase::FieldDetails details; + QString error; + QVERIFY( db->loadQueryFields( details, QStringLiteral( "SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS _uid1_, concat('a', cnt ) as b, cast(cnt as numeric)/100 as c, * FROM [qgis_test].[someData]" ), error ) ); + QCOMPARE( error, QString() ); + QCOMPARE( details.attributeFields.size(), 11 ); + + QCOMPARE( details.attributeFields.at( 0 ).name(), QStringLiteral( "_uid1_" ) ); + QCOMPARE( details.attributeFields.at( 0 ).type(), QVariant::LongLong ); + QCOMPARE( details.attributeFields.at( 0 ).typeName(), QStringLiteral( "bigint" ) ); + QCOMPARE( details.attributeFields.at( 1 ).name(), QStringLiteral( "b" ) ); + QCOMPARE( details.attributeFields.at( 1 ).type(), QVariant::String ); + QCOMPARE( details.attributeFields.at( 1 ).typeName(), QStringLiteral( "varchar(13)" ) ); + QCOMPARE( details.attributeFields.at( 2 ).name(), QStringLiteral( "c" ) ); + QCOMPARE( details.attributeFields.at( 2 ).type(), QVariant::Double ); + QCOMPARE( details.attributeFields.at( 2 ).typeName(), QStringLiteral( "numeric(24,6)" ) ); + QCOMPARE( details.attributeFields.at( 2 ).length(), 24 ); + QCOMPARE( details.attributeFields.at( 3 ).name(), QStringLiteral( "pk" ) ); + QCOMPARE( details.attributeFields.at( 3 ).type(), QVariant::Int ); + QCOMPARE( details.attributeFields.at( 3 ).typeName(), QStringLiteral( "int" ) ); + QCOMPARE( details.attributeFields.at( 4 ).name(), QStringLiteral( "cnt" ) ); + QCOMPARE( details.attributeFields.at( 4 ).type(), QVariant::Int ); + QCOMPARE( details.attributeFields.at( 4 ).typeName(), QStringLiteral( "int" ) ); + QCOMPARE( details.attributeFields.at( 5 ).name(), QStringLiteral( "name" ) ); + QCOMPARE( details.attributeFields.at( 5 ).type(), QVariant::String ); + QCOMPARE( details.attributeFields.at( 5 ).typeName(), QStringLiteral( "nvarchar(max)" ) ); + QCOMPARE( details.attributeFields.at( 6 ).name(), QStringLiteral( "name2" ) ); + QCOMPARE( details.attributeFields.at( 6 ).type(), QVariant::String ); + QCOMPARE( details.attributeFields.at( 6 ).typeName(), QStringLiteral( "nvarchar(max)" ) ); + QCOMPARE( details.attributeFields.at( 7 ).name(), QStringLiteral( "num_char" ) ); + QCOMPARE( details.attributeFields.at( 7 ).type(), QVariant::String ); + QCOMPARE( details.attributeFields.at( 7 ).typeName(), QStringLiteral( "nvarchar(max)" ) ); + QCOMPARE( details.attributeFields.at( 8 ).name(), QStringLiteral( "dt" ) ); + QCOMPARE( details.attributeFields.at( 8 ).type(), QVariant::DateTime ); + QCOMPARE( details.attributeFields.at( 8 ).typeName(), QStringLiteral( "datetime" ) ); + QCOMPARE( details.attributeFields.at( 9 ).name(), QStringLiteral( "date" ) ); + QCOMPARE( details.attributeFields.at( 9 ).type(), QVariant::Date ); + QCOMPARE( details.attributeFields.at( 9 ).typeName(), QStringLiteral( "date" ) ); + QCOMPARE( details.attributeFields.at( 10 ).name(), QStringLiteral( "time" ) ); + QCOMPARE( details.attributeFields.at( 10 ).type(), QVariant::Time ); + QCOMPARE( details.attributeFields.at( 10 ).typeName(), QStringLiteral( "time(7)" ) ); + + QCOMPARE( details.geometryColumnName, QStringLiteral( "geom" ) ); + QCOMPARE( details.geometryColumnType, QStringLiteral( "geometry" ) ); + QVERIFY( !details.isGeography ); +} + QGSTEST_MAIN( TestQgsMssqlProvider ) #include "testqgsmssqlprovider.moc" diff --git a/tests/src/python/featuresourcetestbase.py b/tests/src/python/featuresourcetestbase.py index a17bd06d908b..0084afd86c52 100644 --- a/tests/src/python/featuresourcetestbase.py +++ b/tests/src/python/featuresourcetestbase.py @@ -62,7 +62,9 @@ def testFeatureCount(self): def testFields(self): fields = self.source.fields() for f in ("pk", "cnt", "name", "name2", "num_char"): - self.assertGreaterEqual(fields.lookupField(f), 0) + self.assertGreaterEqual( + fields.lookupField(f), 0, f"Could not find field {f}" + ) def testGetFeatures( self, diff --git a/tests/src/python/test_provider_mssql.py b/tests/src/python/test_provider_mssql.py index e558a1789c7c..30eb05ed232b 100644 --- a/tests/src/python/test_provider_mssql.py +++ b/tests/src/python/test_provider_mssql.py @@ -44,93 +44,19 @@ TEST_DATA_DIR = unitTestDataPath() -class TestPyQgsMssqlProvider(QgisTestCase, ProviderTestCase): +class MssqlProviderTestBase(ProviderTestCase): - @classmethod - def setUpClass(cls): - """Run before all tests""" - super().setUpClass() - # These are the connection details for the SQL Server instance running on Travis - cls.dbconn = "service='testsqlserver' user=sa password='QGIStestSQLServer1234' " - if "QGIS_MSSQLTEST_DB" in os.environ: - cls.dbconn = os.environ["QGIS_MSSQLTEST_DB"] - # Create test layers - cls.vl = QgsVectorLayer( - cls.dbconn - + ' sslmode=disable key=\'pk\' srid=4326 type=POINT table="qgis_test"."someData" (geom) sql=', - "test", - "mssql", - ) - assert ( - cls.vl.dataProvider() is not None - ), f"No data provider for {cls.vl.source()}" - assert cls.vl.isValid(), cls.vl.dataProvider().error().message() - cls.source = cls.vl.dataProvider() - cls.poly_vl = QgsVectorLayer( - cls.dbconn - + ' sslmode=disable key=\'pk\' srid=4326 type=POLYGON table="qgis_test"."some_poly_data" (geom) sql=', - "test", - "mssql", - ) - assert cls.poly_vl.isValid(), cls.poly_vl.dataProvider().error().message() - cls.poly_provider = cls.poly_vl.dataProvider() - - # Use connections API - md = QgsProviderRegistry.instance().providerMetadata("mssql") - cls.conn_api = md.createConnection(cls.dbconn, {}) - - def setUp(self): - for t in ["new_table", "new_table_multipoint", "new_table_multipolygon"]: - self.execSQLCommand(f"DROP TABLE IF EXISTS qgis_test.[{t}]") - - def execSQLCommand(self, sql): - self.assertTrue(self.conn_api) - self.conn_api.executeSql(sql) - - def getSource(self): - # create temporary table for edit tests - self.execSQLCommand("DROP TABLE IF EXISTS qgis_test.edit_data") - self.execSQLCommand( - """CREATE TABLE qgis_test.edit_data (pk INTEGER PRIMARY KEY,cnt integer, name nvarchar(max), name2 nvarchar(max), num_char nvarchar(max), dt datetime, [date] date, [time] time, geom geometry)""" - ) - self.execSQLCommand( - "INSERT INTO [qgis_test].[edit_data] (pk, cnt, name, name2, num_char, dt, [date], [time], geom) VALUES " - "(5, -200, NULL, 'NuLl', '5', '2020-05-04T12:13:14', '2020-05-02', '12:13:01', geometry::STGeomFromText('POINT(-71.123 78.23)', 4326))," - "(3, 300, 'Pear', 'PEaR', '3', NULL, NULL, NULL, NULL)," - "(1, 100, 'Orange', 'oranGe', '1', '2020-05-03T12:13:14', '2020-05-03', '12:13:14', geometry::STGeomFromText('POINT(-70.332 66.33)', 4326))," - "(2, 200, 'Apple', 'Apple', '2', '2020-05-04T12:14:14', '2020-05-04', '12:14:14', geometry::STGeomFromText('POINT(-68.2 70.8)', 4326))," - "(4, 400, 'Honey', 'Honey', '4', '2021-05-04T13:13:14', '2021-05-04', '13:13:14', geometry::STGeomFromText('POINT(-65.32 78.3)', 4326))" - ) - - vl = QgsVectorLayer( - self.dbconn - + ' sslmode=disable key=\'pk\' srid=4326 type=POINT table="qgis_test"."edit_data" (geom) sql=', - "test", - "mssql", - ) - return vl - - def testDeleteFeaturesPktInt(self): - vl = self.getSource() - dp = vl.dataProvider() - - self.assertEqual(dp.featureCount(), 5) - - self.assertTrue(dp.deleteFeatures([1, 3, 4])) - self.assertEqual(dp.featureCount(), 2) - - self.assertFalse(dp.deleteFeatures([3])) - self.assertFalse(dp.deleteFeatures([10])) - self.assertFalse(dp.deleteFeatures([3, 10])) + def getSubsetString(self): + return "[cnt] > 100 and [cnt] < 410" - self.assertTrue(dp.deleteFeatures([5])) - self.assertEqual(dp.featureCount(), 1) + def getSubsetString2(self): + return "[cnt] > 100 and [cnt] < 400" - self.assertTrue(dp.deleteFeatures([2])) - self.assertEqual(dp.featureCount(), 0) + def getSubsetString3(self): + return "[name]='Apple'" - def getEditableLayer(self): - return self.getSource() + def getSubsetStringNoMatching(self): + return "[name]='AppleBearOrangePear'" def enableCompiler(self): QgsSettings().setValue("/qgis/compileExpressions", True) @@ -225,6 +151,95 @@ def uncompiledFilters(self): } return filters + +class TestPyQgsMssqlProvider(QgisTestCase, MssqlProviderTestBase): + + @classmethod + def setUpClass(cls): + """Run before all tests""" + super().setUpClass() + # These are the connection details for the SQL Server instance running on Travis + cls.dbconn = "service='testsqlserver' user=sa password='QGIStestSQLServer1234' " + if "QGIS_MSSQLTEST_DB" in os.environ: + cls.dbconn = os.environ["QGIS_MSSQLTEST_DB"] + # Create test layers + cls.vl = QgsVectorLayer( + cls.dbconn + + ' sslmode=disable key=\'pk\' srid=4326 type=POINT table="qgis_test"."someData" (geom) sql=', + "test", + "mssql", + ) + assert ( + cls.vl.dataProvider() is not None + ), f"No data provider for {cls.vl.source()}" + assert cls.vl.isValid(), cls.vl.dataProvider().error().message() + cls.source = cls.vl.dataProvider() + cls.poly_vl = QgsVectorLayer( + cls.dbconn + + ' sslmode=disable key=\'pk\' srid=4326 type=POLYGON table="qgis_test"."some_poly_data" (geom) sql=', + "test", + "mssql", + ) + assert cls.poly_vl.isValid(), cls.poly_vl.dataProvider().error().message() + cls.poly_provider = cls.poly_vl.dataProvider() + + # Use connections API + md = QgsProviderRegistry.instance().providerMetadata("mssql") + cls.conn_api = md.createConnection(cls.dbconn, {}) + + def setUp(self): + for t in ["new_table", "new_table_multipoint", "new_table_multipolygon"]: + self.execSQLCommand(f"DROP TABLE IF EXISTS qgis_test.[{t}]") + + def execSQLCommand(self, sql): + self.assertTrue(self.conn_api) + self.conn_api.executeSql(sql) + + def getSource(self): + # create temporary table for edit tests + self.execSQLCommand("DROP TABLE IF EXISTS qgis_test.edit_data") + self.execSQLCommand( + """CREATE TABLE qgis_test.edit_data (pk INTEGER PRIMARY KEY,cnt integer, name nvarchar(max), name2 nvarchar(max), num_char nvarchar(max), dt datetime, [date] date, [time] time, geom geometry)""" + ) + self.execSQLCommand( + "INSERT INTO [qgis_test].[edit_data] (pk, cnt, name, name2, num_char, dt, [date], [time], geom) VALUES " + "(5, -200, NULL, 'NuLl', '5', '2020-05-04T12:13:14', '2020-05-02', '12:13:01', geometry::STGeomFromText('POINT(-71.123 78.23)', 4326))," + "(3, 300, 'Pear', 'PEaR', '3', NULL, NULL, NULL, NULL)," + "(1, 100, 'Orange', 'oranGe', '1', '2020-05-03T12:13:14', '2020-05-03', '12:13:14', geometry::STGeomFromText('POINT(-70.332 66.33)', 4326))," + "(2, 200, 'Apple', 'Apple', '2', '2020-05-04T12:14:14', '2020-05-04', '12:14:14', geometry::STGeomFromText('POINT(-68.2 70.8)', 4326))," + "(4, 400, 'Honey', 'Honey', '4', '2021-05-04T13:13:14', '2021-05-04', '13:13:14', geometry::STGeomFromText('POINT(-65.32 78.3)', 4326))" + ) + + vl = QgsVectorLayer( + self.dbconn + + ' sslmode=disable key=\'pk\' srid=4326 type=POINT table="qgis_test"."edit_data" (geom) sql=', + "test", + "mssql", + ) + return vl + + def testDeleteFeaturesPktInt(self): + vl = self.getSource() + dp = vl.dataProvider() + + self.assertEqual(dp.featureCount(), 5) + + self.assertTrue(dp.deleteFeatures([1, 3, 4])) + self.assertEqual(dp.featureCount(), 2) + + self.assertFalse(dp.deleteFeatures([3])) + self.assertFalse(dp.deleteFeatures([10])) + self.assertFalse(dp.deleteFeatures([3, 10])) + + self.assertTrue(dp.deleteFeatures([5])) + self.assertEqual(dp.featureCount(), 1) + + self.assertTrue(dp.deleteFeatures([2])) + self.assertEqual(dp.featureCount(), 0) + + def getEditableLayer(self): + return self.getSource() + def testAddFeatureAllNull(self): # overridden from base test because of non-null primary key, with no default clause if not getattr(self, "getEditableLayer", None): @@ -1166,18 +1181,6 @@ def testIdentityFieldHandling(self): ) self.assertTrue(identity_field.isReadOnly()) - def getSubsetString(self): - return "[cnt] > 100 and [cnt] < 410" - - def getSubsetString2(self): - return "[cnt] > 100 and [cnt] < 400" - - def getSubsetString3(self): - return "[name]='Apple'" - - def getSubsetStringNoMatching(self): - return "[name]='AppleBearOrangePear'" - def testExtentFromGeometryTable(self): """ Check if the behavior of the mssql provider if extent is defined in the geometry_column table @@ -1323,6 +1326,273 @@ def test_nvarchar_length(self): ) self.assertEqual(vl.dataProvider().fields().at(1).length(), 12) + def test_query(self): + + uri = f"{self.dbconn} key='_uid1_' table=\"(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS _uid1_, concat('a', cnt ) as b, cast(cnt as numeric)/100 as c, * FROM [qgis_test].[someData])\"" + vl = QgsVectorLayer(uri, "", "mssql") + self.assertTrue(vl.isValid()) + self.assertEqual( + [f.name() for f in vl.dataProvider().fields()], + [ + "_uid1_", + "b", + "c", + "pk", + "cnt", + "name", + "name2", + "num_char", + "dt", + "date", + "time", + ], + ) + self.assertEqual( + [f.type() for f in vl.dataProvider().fields()], + [ + QVariant.LongLong, + QVariant.String, + QVariant.Double, + QVariant.Int, + QVariant.Int, + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.DateTime, + QVariant.Date, + QVariant.Time, + ], + ) + self.assertEqual( + [f.typeName() for f in vl.dataProvider().fields()], + [ + "bigint", + "varchar(13)", + "numeric(24,6)", + "int", + "int", + "nvarchar(max)", + "nvarchar(max)", + "nvarchar(max)", + "datetime", + "date", + "time(7)", + ], + ) + self.assertEqual( + [f.length() for f in vl.dataProvider().fields()], + [0, 13, 24, 0, 0, 0, 0, 0, -1, -1, -1], + ) + self.assertEqual( + [f.precision() for f in vl.dataProvider().fields()], + [0, 0, -1, 0, 0, 0, 0, 0, -1, -1, -1], + ) + + self.assertEqual(vl.dataProvider().featureCount(), 5) + + features = [f for f in vl.dataProvider().getFeatures()] + self.assertEqual( + [f.attributes() for f in features], + [ + [ + 1, + "a100", + 1.0, + 1, + 100, + "Orange", + "oranGe", + "1", + QDateTime(2020, 5, 3, 12, 13, 14), + QDate(2020, 5, 3), + QTime(12, 13, 14), + ], + [ + 2, + "a200", + 2.0, + 2, + 200, + "Apple", + "Apple", + "2", + QDateTime(2020, 5, 4, 12, 14, 14), + QDate(2020, 5, 4), + QTime(12, 14, 14), + ], + [3, "a300", 3.0, 3, 300, "Pear", "PEaR", "3", NULL, NULL, NULL], + [ + 4, + "a400", + 4.0, + 4, + 400, + "Honey", + "Honey", + "4", + QDateTime(2021, 5, 4, 13, 13, 14), + QDate(2021, 5, 4), + QTime(13, 13, 14), + ], + [ + 5, + "a-200", + -2.0, + 5, + -200, + NULL, + "NuLl", + "5", + QDateTime(2020, 5, 4, 12, 13, 14), + QDate(2020, 5, 2), + QTime(12, 13, 1), + ], + ], + ) + + def test_query_without_row_number(self): + + uri = f"{self.dbconn} table=\"(SELECT concat('a', cnt ) as b, cast(cnt as numeric)/100 as c, * FROM [qgis_test].[someData])\" key='pk'" + vl = QgsVectorLayer(uri, "", "mssql") + self.assertTrue(vl.isValid()) + self.assertEqual(len(vl.dataProvider().fields()), 10) + self.assertEqual( + [f.name() for f in vl.dataProvider().fields()], + ["b", "c", "pk", "cnt", "name", "name2", "num_char", "dt", "date", "time"], + ) + self.assertEqual( + [f.type() for f in vl.dataProvider().fields()], + [ + QVariant.String, + QVariant.Double, + QVariant.Int, + QVariant.Int, + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.DateTime, + QVariant.Date, + QVariant.Time, + ], + ) + self.assertEqual( + [f.typeName() for f in vl.dataProvider().fields()], + [ + "varchar(13)", + "numeric(24,6)", + "int", + "int", + "nvarchar(max)", + "nvarchar(max)", + "nvarchar(max)", + "datetime", + "date", + "time(7)", + ], + ) + self.assertEqual( + [f.length() for f in vl.dataProvider().fields()], + [13, 24, 0, 0, 0, 0, 0, -1, -1, -1], + ) + self.assertEqual( + [f.precision() for f in vl.dataProvider().fields()], + [0, -1, 0, 0, 0, 0, 0, -1, -1, -1], + ) + + self.assertEqual(vl.dataProvider().featureCount(), 5) + + features = [f for f in vl.dataProvider().getFeatures()] + self.assertEqual( + [f.attributes() for f in features], + [ + [ + "a100", + 1.0, + 1, + 100, + "Orange", + "oranGe", + "1", + QDateTime(2020, 5, 3, 12, 13, 14), + QDate(2020, 5, 3), + QTime(12, 13, 14), + ], + [ + "a200", + 2.0, + 2, + 200, + "Apple", + "Apple", + "2", + QDateTime(2020, 5, 4, 12, 14, 14), + QDate(2020, 5, 4), + QTime(12, 14, 14), + ], + ["a300", 3.0, 3, 300, "Pear", "PEaR", "3", NULL, NULL, NULL], + [ + "a400", + 4.0, + 4, + 400, + "Honey", + "Honey", + "4", + QDateTime(2021, 5, 4, 13, 13, 14), + QDate(2021, 5, 4), + QTime(13, 13, 14), + ], + [ + "a-200", + -2.0, + 5, + -200, + NULL, + "NuLl", + "5", + QDateTime(2020, 5, 4, 12, 13, 14), + QDate(2020, 5, 2), + QTime(12, 13, 1), + ], + ], + ) + + +class TestPyQgsMssqlProviderQuery(QgisTestCase, MssqlProviderTestBase): + + @classmethod + def setUpClass(cls): + """Run before all tests""" + super().setUpClass() + # These are the connection details for the SQL Server instance running on Travis + cls.dbconn = "service='testsqlserver' user=sa password='QGIStestSQLServer1234' " + if "QGIS_MSSQLTEST_DB" in os.environ: + cls.dbconn = os.environ["QGIS_MSSQLTEST_DB"] + # Create test layers + cls.vl = QgsVectorLayer( + cls.dbconn + + " sslmode=disable srid=4326 type=POINT key='pk' table=\"(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS pk, cnt as cnt, name as name, name2 as name2, num_char as num_char, dt as dt, date as date, time as time, geom as geom FROM [qgis_test].[someData])\" (geom) sql=", + "test", + "mssql", + ) + assert ( + cls.vl.dataProvider() is not None + ), f"No data provider for {cls.vl.source()}" + assert cls.vl.isValid(), cls.vl.dataProvider().error().message() + cls.source = cls.vl.dataProvider() + cls.poly_vl = QgsVectorLayer( + cls.dbconn + + " sslmode=disable srid=4326 type=POINT key='pk' table=\"(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS pk, geom as geometry FROM [qgis_test].[some_poly_data])\" (geometry) sql=", + "test", + "mssql", + ) + assert cls.poly_vl.isValid(), cls.poly_vl.dataProvider().error().message() + cls.poly_provider = cls.poly_vl.dataProvider() + + # Use connections API + md = QgsProviderRegistry.instance().providerMetadata("mssql") + cls.conn_api = md.createConnection(cls.dbconn, {}) + if __name__ == "__main__": unittest.main() diff --git a/tests/src/python/test_qgsproviderconnection_mssql.py b/tests/src/python/test_qgsproviderconnection_mssql.py index 9071d4c1dbbb..5021817eeba1 100644 --- a/tests/src/python/test_qgsproviderconnection_mssql.py +++ b/tests/src/python/test_qgsproviderconnection_mssql.py @@ -20,6 +20,7 @@ Qgis, QgsCoordinateReferenceSystem, QgsFields, + QgsAbstractDatabaseProviderConnection, ) from qgis.testing import unittest @@ -242,6 +243,82 @@ def test_invalid_geometries_disable_workaround(self): # due to the invalid geometries in this table self.assertEqual(len(tb.geometryColumnTypes()), 0) + def test_create_vector_layer(self): + """Test query layers""" + + md = QgsProviderRegistry.instance().providerMetadata("mssql") + conn = md.createConnection(self.uri, {}) + + options = QgsAbstractDatabaseProviderConnection.SqlVectorLayerOptions() + options.sql = "SELECT pk as pk, name as my_name, geom as geometry FROM qgis_test.someData WHERE pk < 3" + options.primaryKeyColumns = ["pk"] + options.geometryColumn = "geometry" + vl = conn.createSqlVectorLayer(options) + self.assertTrue(vl.isValid()) + self.assertTrue(vl.isSqlQuery()) + # Test flags + self.assertTrue(vl.vectorLayerTypeFlags() & Qgis.VectorLayerTypeFlag.SqlQuery) + self.assertEqual(vl.geometryType(), Qgis.GeometryType.Point) + features = [f for f in vl.getFeatures()] + self.assertEqual(len(features), 2) + self.assertEqual(vl.dataProvider().geometryColumnName(), "geometry") + self.assertEqual( + vl.dataProvider().crs(), QgsCoordinateReferenceSystem("EPSG:4326") + ) + + # Wrong calls + options.primaryKeyColumns = ["DOES_NOT_EXIST"] + vl = conn.createSqlVectorLayer(options) + self.assertFalse(vl.isValid()) + self.assertFalse(vl.vectorLayerTypeFlags() & Qgis.VectorLayerTypeFlag.SqlQuery) + self.assertFalse(vl.isSqlQuery()) + + options.primaryKeyColumns = ["id"] + options.geometryColumn = "DOES_NOT_EXIST" + vl = conn.createSqlVectorLayer(options) + self.assertFalse(vl.isValid()) + self.assertFalse(vl.isSqlQuery()) + + # No geometry and no PK + options.sql = "SELECT pk as pk, geom as geometry FROM qgis_test.someData" + options.primaryKeyColumns = [] + options.geometryColumn = "" + vl = conn.createSqlVectorLayer(options) + self.assertTrue(vl.isValid()) + self.assertTrue(vl.isSqlQuery()) + self.assertEqual(vl.geometryType(), Qgis.GeometryType.Unknown) + self.assertEqual(vl.dataProvider().pkAttributeIndexes(), [0]) + features = [f for f in vl.getFeatures()] + self.assertEqual(len(features), 5) + + # No PKs + options.primaryKeyColumns = [] + options.geometryColumn = "geometry" + vl = conn.createSqlVectorLayer(options) + self.assertTrue(vl.isSqlQuery()) + self.assertTrue(vl.isValid()) + self.assertEqual(vl.geometryType(), Qgis.GeometryType.Point) + self.assertEqual(vl.dataProvider().pkAttributeIndexes(), [0]) + features = [f for f in vl.getFeatures()] + self.assertEqual(len(features), 5) + + # changing geometry type + options.sql = ( + "SELECT pk as pk, geom.STBuffer(1) as geometry FROM qgis_test.someData" + ) + options.primaryKeyColumns = ["pk"] + options.geometryColumn = "geometry" + vl = conn.createSqlVectorLayer(options) + self.assertTrue(vl.isSqlQuery()) + self.assertTrue(vl.isValid()) + self.assertEqual(vl.geometryType(), Qgis.GeometryType.Polygon) + self.assertEqual(vl.dataProvider().pkAttributeIndexes(), [0]) + features = [f for f in vl.getFeatures()] + self.assertEqual(len(features), 5) + self.assertEqual( + vl.dataProvider().crs(), QgsCoordinateReferenceSystem("EPSG:4326") + ) + if __name__ == "__main__": unittest.main()