Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mssql] Allow SQL queries as layer sources #60728

Merged
merged 9 commits into from
Feb 25, 2025
1 change: 1 addition & 0 deletions src/providers/mssql/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ set(MSSQL_SRCS
qgsmssqlsqlquerybuilder.cpp
qgsmssqltransaction.cpp
qgsmssqldatabase.cpp
qgsmssqlutils.cpp
)

if (WITH_GUI)
Expand Down
3 changes: 2 additions & 1 deletion src/providers/mssql/qgsmssqlconnection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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( " )" ) );
Expand Down
178 changes: 83 additions & 95 deletions src/providers/mssql/qgsmssqldatabase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "qgsvariantutils.h"
#include "qgsmssqlprovider.h"
#include "qgsdbquerylog.h"
#include "qgsmssqlutils.h"

#include <QCoreApplication>
#include <QtDebug>
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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;
Expand All @@ -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 );

Expand All @@ -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() ) );
Expand Down Expand Up @@ -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;
}


// -------------------

Expand Down
10 changes: 8 additions & 2 deletions src/providers/mssql/qgsmssqldatabase.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ class QgsMssqlDatabase

QSqlQuery createQuery();

static QMetaType::Type decodeSqlType( const QString &sqlTypeName );

struct FieldDetails
{
QgsFields attributeFields;
Expand All @@ -95,8 +93,16 @@ class QgsMssqlDatabase
QList<int> 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 );

Expand Down
8 changes: 4 additions & 4 deletions src/providers/mssql/qgsmssqldataitems.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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] ) );
Expand Down Expand Up @@ -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" ) );
Expand Down Expand Up @@ -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;
Expand Down
Loading