Skip to content

Commit 61173c2

Browse files
author
Hugo Mercier
committedJan 11, 2016
Allow to call qgis expression functions in virtual layers
1 parent 80fe7e2 commit 61173c2

File tree

3 files changed

+187
-9
lines changed

3 files changed

+187
-9
lines changed
 

‎src/providers/virtual/qgsvirtuallayersqlitemodule.cpp

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ email : hugo dot mercier at oslandia dot com
2020
#include <stdexcept>
2121

2222
#include <QCoreApplication>
23+
#include <QBuffer>
2324

2425
#include <qgsapplication.h>
2526
#include <qgsvectorlayer.h>
@@ -636,6 +637,178 @@ void module_destroy( void* )
636637
}
637638
}
638639

640+
// the expression context used for calling qgis functions
641+
QgsExpressionContext qgisFunctionExpressionContext;
642+
643+
void qgisFunctionWrapper( sqlite3_context* ctxt, int nArgs, sqlite3_value** args )
644+
{
645+
// convert from sqlite3 value to QVariant and then call the qgis expression function
646+
// the 3 basic sqlite3 types (int, float, text) are converted to their QVariant equivalent
647+
// Expression::Interval is handled specifically
648+
// geometries are converted between spatialite and QgsGeometry
649+
// other data types (datetime mainly) are represented as BLOBs thanks to QVariant serializing functions
650+
651+
QgsExpression::Function* foo = reinterpret_cast<QgsExpression::Function*>( sqlite3_user_data( ctxt ) );
652+
653+
QVariantList variants;
654+
for ( int i = 0; i < nArgs; i++ )
655+
{
656+
int t = sqlite3_value_type( args[i] );
657+
switch ( t )
658+
{
659+
case SQLITE_INTEGER:
660+
variants << QVariant( sqlite3_value_int64( args[i] ) );
661+
break;
662+
case SQLITE_FLOAT:
663+
variants << QVariant( sqlite3_value_double( args[i] ) );
664+
break;
665+
case SQLITE_TEXT:
666+
{
667+
int n = sqlite3_value_bytes( args[i] );
668+
const char* t = reinterpret_cast<const char*>( sqlite3_value_text( args[i] ) );
669+
QString str( QByteArray::fromRawData( t, n ) ); // don't copy data
670+
variants << QVariant( str );
671+
break;
672+
}
673+
case SQLITE_BLOB:
674+
{
675+
int n = sqlite3_value_bytes( args[i] );
676+
const char* blob = reinterpret_cast<const char*>( sqlite3_value_blob( args[i] ) );
677+
// spatialite blobs start with a 0 byte
678+
if ( n > 0 && blob[0] == 0 )
679+
{
680+
QgsGeometry geom = spatialiteBlobToQgsGeometry( blob, n );
681+
variants << QVariant::fromValue( geom );
682+
}
683+
else
684+
{
685+
// else it is another type
686+
QByteArray ba = QByteArray::fromRawData( blob + 1, n - 1 );
687+
QBuffer buffer( &ba );
688+
buffer.open( QIODevice::ReadOnly );
689+
QDataStream ds( &buffer );
690+
QVariant v;
691+
ds >> v;
692+
buffer.close();
693+
variants << v;
694+
}
695+
break;
696+
}
697+
default:
698+
variants << QVariant(); // null
699+
break;
700+
};
701+
}
702+
703+
QgsExpression parentExpr( "" );
704+
QVariant ret = foo->func( variants, &qgisFunctionExpressionContext, &parentExpr );
705+
if ( parentExpr.hasEvalError() )
706+
{
707+
QByteArray ba = parentExpr.evalErrorString().toUtf8();
708+
sqlite3_result_error( ctxt, ba.constData(), ba.size() );
709+
return;
710+
}
711+
712+
if ( ret.isNull() )
713+
{
714+
sqlite3_result_null( ctxt );
715+
return;
716+
}
717+
718+
switch ( ret.type() )
719+
{
720+
case QVariant::Bool:
721+
case QVariant::Int:
722+
case QVariant::UInt:
723+
case QVariant::LongLong:
724+
sqlite3_result_int64( ctxt, ret.toLongLong() );
725+
break;
726+
case QVariant::Double:
727+
sqlite3_result_double( ctxt, ret.toDouble() );
728+
break;
729+
case QVariant::String:
730+
{
731+
QByteArray ba( ret.toByteArray() );
732+
sqlite3_result_text( ctxt, ba.constData(), ba.size(), SQLITE_TRANSIENT );
733+
break;
734+
}
735+
case QVariant::UserType:
736+
{
737+
if ( ret.canConvert<QgsGeometry>() )
738+
{
739+
char* blob = nullptr;
740+
int size = 0;
741+
qgsGeometryToSpatialiteBlob( ret.value<QgsGeometry>(), /*srid*/0, blob, size );
742+
sqlite3_result_blob( ctxt, blob, size, deleteGeometryBlob );
743+
}
744+
else if ( ret.canConvert<QgsExpression::Interval>() )
745+
{
746+
sqlite3_result_double( ctxt, ret.value<QgsExpression::Interval>().seconds() );
747+
}
748+
break;
749+
}
750+
default:
751+
{
752+
QBuffer buffer;
753+
buffer.open( QBuffer::ReadWrite );
754+
QDataStream ds( &buffer );
755+
// something different from 0 (to distinguish from the first byte of a geometry blob)
756+
char type = 1;
757+
buffer.write( &type, 1 );
758+
// then the serialized version of the variant
759+
ds << ret;
760+
buffer.close();
761+
sqlite3_result_blob( ctxt, buffer.buffer().constData(), buffer.buffer().size(), SQLITE_TRANSIENT );
762+
}
763+
};
764+
}
765+
766+
void registerQgisFunctions( sqlite3* db )
767+
{
768+
QStringList excludedFunctions;
769+
excludedFunctions << "min" << "max" << "coalesce" << "get_feature" << "getFeature" << "attribute";
770+
QStringList reservedFunctions;
771+
reservedFunctions << "left" << "right" << "union";
772+
// register QGIS expression functions
773+
foreach ( QgsExpression::Function* foo, QgsExpression::Functions() )
774+
{
775+
if ( foo->usesgeometry() || foo->lazyEval() )
776+
{
777+
// there is no "current" feature here, so calling functions that access "the" geometry does not make sense
778+
// also, we can't pass Node values for lazy evaluations
779+
continue;
780+
}
781+
if ( excludedFunctions.contains( foo->name() ) )
782+
continue;
783+
784+
QStringList names;
785+
names << foo->name();
786+
names << foo->aliases();
787+
788+
foreach ( QString name, names ) // for each alias
789+
{
790+
if ( reservedFunctions.contains( name ) ) // reserved keyword
791+
name = "_" + name;
792+
if ( name.startsWith( "$" ) )
793+
continue;
794+
795+
// register the function and pass the pointer to the Function* as user data
796+
int r = sqlite3_create_function( db, name.toUtf8().constData(), foo->params(), SQLITE_UTF8, foo, qgisFunctionWrapper, nullptr, nullptr );
797+
if ( r != SQLITE_OK )
798+
{
799+
// is it because a function of the same name already exist (in Spatialite for instance ?)
800+
// we then try to recreate it with a prefix
801+
name = "qgis_" + name;
802+
r = sqlite3_create_function( db, name.toUtf8().constData(), foo->params(), SQLITE_UTF8, foo, qgisFunctionWrapper, nullptr, nullptr );
803+
}
804+
}
805+
}
806+
807+
// initialize the expression context
808+
qgisFunctionExpressionContext << QgsExpressionContextUtils::globalScope();
809+
qgisFunctionExpressionContext << QgsExpressionContextUtils::projectScope();
810+
}
811+
639812
int qgsvlayer_module_init( sqlite3 *db, char **pzErrMsg, void * unused /*const sqlite3_api_routines *pApi*/ )
640813
{
641814
Q_UNUSED( pzErrMsg );
@@ -682,5 +855,7 @@ int qgsvlayer_module_init( sqlite3 *db, char **pzErrMsg, void * unused /*const s
682855

683856
sqlite3_create_module_v2( db, "QgsVLayer", &module, nullptr, module_destroy );
684857

858+
registerQgisFunctions( db );
859+
685860
return rc;
686861
}

‎src/providers/virtual/qgsvirtuallayersqlitemodule.h

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,6 @@ extern "C"
5353

5454
#include <qgsgeometry.h>
5555

56-
/**
57-
* Convert a spatialite geometry blob to a QgsGeometry
58-
*
59-
* \param blob Pointer to the raw blob memory
60-
* \param size Size in bytes of the blob
61-
* \returns a QgsGeometry (that are implicitly shared)
62-
*/
63-
QgsGeometry spatialite_blob_to_qgsgeometry( const unsigned char* blob, size_t size );
64-
6556
/**
6657
* Init the SQLite file with proper metadata tables
6758
*/

‎tests/src/python/test_provider_virtual.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,5 +703,17 @@ def test_ProjectDependencies(self):
703703
# make sure the 3 layers are loaded back
704704
self.assertEqual(len(QgsMapLayerRegistry.instance().mapLayers()), 3)
705705

706+
def test_qgisExpressionFunctions(self):
707+
QgsProject.instance().setTitle('project')
708+
self.assertEqual(QgsProject.instance().title(), 'project')
709+
df = QgsVirtualLayerDefinition()
710+
df.setQuery("SELECT format('hello %1', 'world') as a, year(todate('2016-01-02')) as b, title('This') as t, var('project_title') as c")
711+
l = QgsVectorLayer(df.toString(), "testq", "virtual")
712+
self.assertEqual(l.isValid(), True)
713+
714+
for f in l.getFeatures():
715+
self.assertEqual(f.attributes(), ['hello world', 2016, u'This', u'project'])
716+
717+
706718
if __name__ == '__main__':
707719
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.