Skip to content

Commit 564fd50

Browse files
committedMay 27, 2019
Fix spatialite handling of JSON arrays
Fixes #21986 plus: - fix multiple string keys with commas in value relation widget - more robust JSON and array (un)marshalling - uniform array representation in value relation widgets - lot of test coverage - automatic QVariant type conversions in JSON utils
1 parent d7fa028 commit 564fd50

15 files changed

+721
-181
lines changed
 

‎python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Constructor for QgsValueRelationFieldFormatter.
5959

6060
static QStringList valueToStringList( const QVariant &value );
6161
%Docstring
62-
Utility to convert an array or a string representation of an array ``value`` to a string list
62+
Utility to convert an array or a string representation of an (C style: {1,2...}) array in the form ``value`` to a string list
6363

6464
.. versionadded:: 3.2
6565
%End

‎python/core/auto_generated/qgsjsonutils.sip.in

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,12 +322,14 @@ Exports all attributes from a QgsFeature as a JSON map type.
322322
%End
323323

324324

325-
static QVariantList parseArray( const QString &json, QVariant::Type type );
325+
static QVariantList parseArray( const QString &json, QVariant::Type type = QVariant::Invalid );
326326
%Docstring
327-
Parse a simple array (depth=1).
327+
Parse a simple array (depth=1)
328328

329329
:param json: the JSON to parse
330-
:param type: the type of the elements
330+
:param type: optional variant type of the elements, if specified (and not Invalid),
331+
the array items will be converted to the type, and discarded if
332+
the conversion is not possible.
331333

332334
.. versionadded:: 3.0
333335
%End

‎src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
#include "qgsapplication.h"
2323
#include "qgsexpressioncontextutils.h"
2424

25+
26+
#include <nlohmann/json.hpp>
27+
using json = nlohmann::json;
28+
2529
#include <QSettings>
2630

2731
bool orderByKeyLessThan( const QgsValueRelationFieldFormatter::ValueRelationItem &p1, const QgsValueRelationFieldFormatter::ValueRelationItem &p2 )
@@ -164,11 +168,49 @@ QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatte
164168

165169
QStringList QgsValueRelationFieldFormatter::valueToStringList( const QVariant &value )
166170
{
171+
// Note: the recommended way to pass a value is QVariant::StringList but
172+
// for back compatibility a string representation either in the form
173+
// of a JSON array or in the form of {"quoted_str", 1, ... } is
174+
// also accepted
167175
QStringList checkList;
168176
if ( value.type() == QVariant::StringList )
177+
{
169178
checkList = value.toStringList();
179+
}
170180
else if ( value.type() == QVariant::String )
171-
checkList = value.toString().remove( QChar( '{' ) ).remove( QChar( '}' ) ).split( ',' );
181+
{
182+
// This must be an array representation
183+
auto newVal { value };
184+
if ( newVal.toString().trimmed().startsWith( '{' ) )
185+
{
186+
newVal = QVariant( newVal.toString().trimmed().mid( 1 ).chopped( 1 ).prepend( '[' ).append( ']' ) );
187+
}
188+
if ( newVal.toString().trimmed().startsWith( '[' ) )
189+
{
190+
try
191+
{
192+
for ( auto &element : json::parse( newVal.toString().toStdString() ) )
193+
{
194+
if ( element.is_number_integer() )
195+
{
196+
checkList << QString::number( element.get<int>() );
197+
}
198+
else if ( element.is_number_unsigned() )
199+
{
200+
checkList << QString::number( element.get<unsigned>() );
201+
}
202+
else if ( element.is_string() )
203+
{
204+
checkList << QString::fromStdString( element.get<std::string>() );
205+
}
206+
}
207+
}
208+
catch ( json::parse_error ex )
209+
{
210+
qDebug() << QString::fromStdString( ex.what() );
211+
}
212+
}
213+
}
172214
else if ( value.type() == QVariant::List )
173215
{
174216
QVariantList valuesList( value.toList( ) );

‎src/core/fieldformatter/qgsvaluerelationfieldformatter.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class CORE_EXPORT QgsValueRelationFieldFormatter : public QgsFieldFormatter
6666
QVariant createCache( QgsVectorLayer *layer, int fieldIndex, const QVariantMap &config ) const override;
6767

6868
/**
69-
* Utility to convert an array or a string representation of an array \a value to a string list
69+
* Utility to convert an array or a string representation of an (C style: {1,2...}) array in the form \a value to a string list
7070
* \since QGIS 3.2
7171
*/
7272
static QStringList valueToStringList( const QVariant &value );

‎src/core/qgsjsonutils.cpp

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -315,28 +315,69 @@ QString QgsJsonUtils::exportAttributes( const QgsFeature &feature, QgsVectorLaye
315315

316316
QVariantList QgsJsonUtils::parseArray( const QString &json, QVariant::Type type )
317317
{
318-
QJsonParseError error;
319-
const QJsonDocument jsonDoc = QJsonDocument::fromJson( json.toUtf8(), &error );
318+
QString errorMessage;
320319
QVariantList result;
321-
if ( error.error != QJsonParseError::NoError )
320+
try
322321
{
323-
QgsLogger::warning( QStringLiteral( "Cannot parse json (%1): %2" ).arg( error.errorString(), json ) );
324-
return result;
325-
}
326-
if ( !jsonDoc.isArray() )
327-
{
328-
QgsLogger::warning( QStringLiteral( "Cannot parse json (%1) as array: %2" ).arg( error.errorString(), json ) );
329-
return result;
322+
const auto jObj { json::parse( json.toStdString() ) };
323+
if ( ! jObj.is_array() )
324+
{
325+
throw json::parse_error::create( 0, 0, QStringLiteral( "JSON value must be an array" ).toStdString() );
326+
}
327+
for ( const auto &item : jObj )
328+
{
329+
// Create a QVariant from the array item
330+
QVariant v;
331+
if ( item.is_number_integer() )
332+
{
333+
v = item.get<int>();
334+
}
335+
else if ( item.is_number_unsigned() )
336+
{
337+
v = item.get<unsigned>();
338+
}
339+
else if ( item.is_number_float() )
340+
{
341+
// Note: it's a double and not a float on purpose
342+
v = item.get<double>();
343+
}
344+
else if ( item.is_string() )
345+
{
346+
v = QString::fromStdString( item.get<std::string>() );
347+
}
348+
else if ( item.is_null() )
349+
{
350+
// do nothing: the default is null
351+
}
352+
else
353+
{
354+
// do nothing and discard the item
355+
}
356+
357+
// If a destination type was specified (it's not invalid), try to convert
358+
if ( type != QVariant::Invalid )
359+
{
360+
if ( ! v.convert( static_cast<int>( type ) ) )
361+
{
362+
QgsLogger::warning( QStringLiteral( "Cannot convert json array element to specified type, ignoring: %1" ).arg( v.toString() ) );
363+
}
364+
else
365+
{
366+
result.push_back( v );
367+
}
368+
}
369+
else
370+
{
371+
result.push_back( v );
372+
}
373+
}
330374
}
331-
const auto constArray = jsonDoc.array();
332-
for ( const QJsonValue &cur : constArray )
375+
catch ( json::parse_error ex )
333376
{
334-
QVariant curVariant = cur.toVariant();
335-
if ( curVariant.convert( type ) )
336-
result.append( curVariant );
337-
else
338-
QgsLogger::warning( QStringLiteral( "Cannot convert json array element: %1" ).arg( cur.toString() ) );
377+
errorMessage = ex.what();
378+
QgsLogger::warning( QStringLiteral( "Cannot parse json (%1): %2" ).arg( ex.what(), json ) );
339379
}
380+
340381
return result;
341382
}
342383

‎src/core/qgsjsonutils.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -328,18 +328,20 @@ class CORE_EXPORT QgsJsonUtils
328328
const QVector<QVariant> &attributeWidgetCaches = QVector<QVariant>() ) SIP_SKIP;
329329

330330
/**
331-
* Parse a simple array (depth=1).
331+
* Parse a simple array (depth=1)
332332
* \param json the JSON to parse
333-
* \param type the type of the elements
333+
* \param type optional variant type of the elements, if specified (and not Invalid),
334+
* the array items will be converted to the type, and discarded if
335+
* the conversion is not possible.
334336
* \since QGIS 3.0
335337
*/
336-
static QVariantList parseArray( const QString &json, QVariant::Type type );
338+
static QVariantList parseArray( const QString &json, QVariant::Type type = QVariant::Invalid );
337339

338340

339341
/**
340342
* Converts a QVariant \a v to a json object
341343
* \note Not available in Python bindings
342-
* \since QGIS 3.10
344+
* \since QGIS 3.8
343345
*/
344346
static json jsonFromVariant( const QVariant &v ) SIP_SKIP;
345347

‎src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "qgsvaluerelationfieldformatter.h"
2626
#include "qgsattributeform.h"
2727
#include "qgsattributes.h"
28+
#include "qgsjsonutils.h"
2829

2930
#include <QHeaderView>
3031
#include <QComboBox>
@@ -33,6 +34,10 @@
3334
#include <QStringListModel>
3435
#include <QCompleter>
3536

37+
#include <nlohmann/json.hpp>
38+
using json = nlohmann::json;
39+
40+
3641
QgsValueRelationWidgetWrapper::QgsValueRelationWidgetWrapper( QgsVectorLayer *layer, int fieldIdx, QWidget *editor, QWidget *parent )
3742
: QgsEditorWidgetWrapper( layer, fieldIdx, editor, parent )
3843
{
@@ -70,21 +75,26 @@ QVariant QgsValueRelationWidgetWrapper::value() const
7075
}
7176
}
7277

73-
if ( layer()->fields().at( fieldIdx() ).type() == QVariant::Map )
78+
QVariantList vl;
79+
//store as QVariantList because it's json
80+
for ( const QString &s : qgis::as_const( selection ) )
7481
{
75-
QVariantList vl;
76-
//store as QVariantList because it's json
77-
for ( const QString &s : qgis::as_const( selection ) )
82+
// Convert to proper type
83+
const QVariant::Type type { fkType() };
84+
switch ( type )
7885
{
79-
vl << s;
86+
case QVariant::Type::Int:
87+
vl.push_back( s.toInt() );
88+
break;
89+
case QVariant::Type::LongLong:
90+
vl.push_back( s.toLongLong() );
91+
break;
92+
default:
93+
vl.push_back( s );
94+
break;
8095
}
81-
v = vl;
82-
}
83-
else
Code has comments. Press enter to view.
84-
{
85-
//store as hstore string
86-
v = selection.join( ',' ).prepend( '{' ).append( '}' );
8796
}
97+
v = vl;
8898
}
8999

90100
if ( mLineEdit )
@@ -289,6 +299,21 @@ int QgsValueRelationWidgetWrapper::columnCount() const
289299
return std::max( 1, config( QStringLiteral( "NofColumns" ) ).toInt() );
290300
}
291301

302+
QVariant::Type QgsValueRelationWidgetWrapper::fkType() const
303+
{
304+
QgsVectorLayer *layer = QgsProject::instance()->mapLayer<QgsVectorLayer *>( config().value( QStringLiteral( "Layer" ) ).toString() );
305+
if ( layer )
306+
{
307+
QgsFields fields = layer->fields();
308+
int idx { fields.lookupField( config().value( QStringLiteral( "Key" ) ).toString() ) };
309+
if ( idx >= 0 )
310+
{
311+
return fields.at( idx ).type();
312+
}
313+
}
314+
return QVariant::Type::Invalid;
315+
}
316+
292317
void QgsValueRelationWidgetWrapper::populate( )
293318
{
294319
// Initialize, note that signals are blocked, to avoid double signals on new features

‎src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ class GUI_EXPORT QgsValueRelationWidgetWrapper : public QgsEditorWidgetWrapper
115115
*/
116116
int columnCount() const;
117117

118+
//! Returns the variant type of the fk
119+
QVariant::Type fkType() const;
120+
118121
//! Sets the values for the widgets, re-creates the cache when required
119122
void populate( );
120123

‎src/providers/spatialite/qgsspatialitefeatureiterator.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,10 @@ QVariant QgsSpatiaLiteFeatureIterator::getFeatureAttribute( sqlite3_stmt *stmt,
581581
{
582582
// assume arrays are stored as JSON
583583
QVariant result = QVariant( QgsJsonUtils::parseArray( txt, subType ) );
584-
result.convert( type );
584+
if ( ! result.convert( static_cast<int>( type ) ) )
585+
{
586+
QgsDebugMsgLevel( QStringLiteral( "Could not convert JSON value to requested QVariant type" ).arg( txt ), 3 );
587+
}
585588
return result;
586589
}
587590
return txt;

‎src/providers/spatialite/qgsspatialiteprovider.cpp

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ email : a.furieri@lqt.it
4444
#include <QDir>
4545
#include <QRegularExpression>
4646

47+
#include <nlohmann/json.hpp>
48+
using json = nlohmann::json;
49+
4750

4851
const QString SPATIALITE_KEY = QStringLiteral( "spatialite" );
4952
const QString SPATIALITE_DESCRIPTION = QStringLiteral( "SpatiaLite data provider" );
@@ -663,6 +666,10 @@ static TypeSubType getVariantType( const QString &type )
663666
type.length() - SPATIALITE_ARRAY_PREFIX.length() - SPATIALITE_ARRAY_SUFFIX.length() ) );
664667
return TypeSubType( subType.first == QVariant::String ? QVariant::StringList : QVariant::List, subType.first );
665668
}
669+
else if ( type == QLatin1String( "jsonarray" ) )
670+
{
671+
return TypeSubType( QVariant::List, QVariant::Invalid );
672+
}
666673
else
667674
// for sure any SQLite value can be represented as SQLITE_TEXT
668675
return TypeSubType( QVariant::String, QVariant::Invalid );
@@ -4385,6 +4392,7 @@ bool QgsSpatiaLiteProvider::changeAttributeValues( const QgsChangedAttributesMap
43854392
first = false;
43864393

43874394
QVariant::Type type = fld.type();
4395+
const auto typeName { fld.typeName() };
43884396

43894397
if ( val.isNull() || !val.isValid() )
43904398
{
@@ -4398,8 +4406,27 @@ bool QgsSpatiaLiteProvider::changeAttributeValues( const QgsChangedAttributesMap
43984406
}
43994407
else if ( type == QVariant::StringList || type == QVariant::List )
44004408
{
4401-
// binding an array value
4402-
sql += QStringLiteral( "%1=%2" ).arg( QgsSqliteUtils::quotedIdentifier( fld.name() ), QgsSqliteUtils::quotedString( QgsJsonUtils::encodeValue( val ) ) );
4409+
// binding an array value, parse JSON
4410+
QString jRepr;
4411+
try
4412+
{
4413+
const auto jObj { QgsJsonUtils::jsonFromVariant( val ) };
4414+
if ( ! jObj.is_array() )
4415+
{
4416+
throw json::parse_error::create( 0, 0, tr( "JSON value must be an array" ).toStdString() );
4417+
}
4418+
jRepr = QString::fromStdString( jObj.dump( ) );
4419+
sql += QStringLiteral( "%1=%2" ).arg( QgsSqliteUtils::quotedIdentifier( fld.name() ), QgsSqliteUtils::quotedString( jRepr ) );
4420+
}
4421+
catch ( json::exception ex )
4422+
{
4423+
const auto errM { tr( "Field type is JSON but the value cannot be converted to JSON array: %1" ).arg( ex.what() ) };
4424+
auto msgPtr { static_cast<char *>( sqlite3_malloc( errM.length() + 1 ) ) };
4425+
strcpy( static_cast<char *>( msgPtr ), errM.toStdString().c_str() );
4426+
errMsg = msgPtr;
4427+
handleError( jRepr, errMsg, true );
4428+
return false;
4429+
}
44034430
}
44044431
else
44054432
{

‎tests/src/core/testqgsjsonutils.cpp

Lines changed: 170 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -34,162 +34,199 @@ class TestQgsJsonUtils : public QObject
3434
Q_ENUM( JsonAlgs )
3535

3636
Q_OBJECT
37-
private slots:
38-
void testStringList()
39-
{
40-
QStringList list;
4137

42-
{
43-
const QString json = QgsJsonUtils::encodeValue( list );
44-
QCOMPARE( json, QString( "[]" ) );
45-
const QVariant back = QgsJsonUtils::parseArray( json, QVariant::String );
46-
QCOMPARE( back.toStringList(), list );
47-
}
48-
49-
{
50-
list << QStringLiteral( "one" ) << QStringLiteral( "<',\"\\>" ) << QStringLiteral( "two" );
51-
const QString json = QgsJsonUtils::encodeValue( list );
52-
QCOMPARE( json, QString( "[\"one\",\"<',\\\"\\\\>\",\"two\"]" ) );
53-
const QVariant back = QgsJsonUtils::parseArray( json, QVariant::String );
54-
QCOMPARE( back.toStringList(), list );
55-
}
56-
}
38+
private slots:
39+
void testStringList();
40+
void testJsonArray();
41+
void testIntList();
42+
void testDoubleList();
43+
void testExportAttributesJson_data();
44+
void testExportAttributesJson();
45+
void testExportFeatureJson();
46+
void testExportGeomToJson();
47+
};
5748

58-
void testIntList()
59-
{
60-
QVariantList list;
6149

62-
{
63-
list << 1 << -2;
64-
const QString json = QgsJsonUtils::encodeValue( list );
65-
QCOMPARE( json, QString( "[1,-2]" ) );
66-
const QVariantList back = QgsJsonUtils::parseArray( json, QVariant::Int );
67-
QCOMPARE( back, list );
68-
QCOMPARE( back.at( 0 ).type(), QVariant::Int );
69-
}
7050

71-
{
72-
// check invalid entries are ignored
73-
const QVariantList back = QgsJsonUtils::parseArray( QStringLiteral( "[1,\"a\",-2]" ), QVariant::Int );
74-
QCOMPARE( back, list );
75-
}
76-
}
51+
void TestQgsJsonUtils::testStringList()
52+
{
53+
QStringList list;
54+
55+
{
56+
const QString json = QgsJsonUtils::encodeValue( list );
57+
QCOMPARE( json, QString( "[]" ) );
58+
const QVariant back = QgsJsonUtils::parseArray( json, QVariant::String );
59+
QCOMPARE( back.toStringList(), list );
60+
}
61+
62+
{
63+
list << QStringLiteral( "one" ) << QStringLiteral( "<',\"\\>" ) << QStringLiteral( "two" );
64+
const QString json = QgsJsonUtils::encodeValue( list );
65+
QCOMPARE( json, QString( "[\"one\",\"<',\\\"\\\\>\",\"two\"]" ) );
66+
const QVariant back = QgsJsonUtils::parseArray( json, QVariant::String );
67+
QCOMPARE( back.toStringList(), list );
68+
}
69+
}
70+
71+
void TestQgsJsonUtils::testJsonArray()
72+
{
73+
QCOMPARE( QgsJsonUtils::parseArray( R"([1,2,3])", QVariant::Int ), QVariantList() << 1 << 2 << 3 );
74+
QCOMPARE( QgsJsonUtils::parseArray( R"([1,2,3])" ), QVariantList() << 1 << 2 << 3 );
75+
QCOMPARE( QgsJsonUtils::parseArray( R"([1,2,3])", QVariant::Double ), QVariantList() << 1.0 << 2.0 << 3.0 );
76+
QCOMPARE( QgsJsonUtils::parseArray( R"([1.0,2.0,3.0])" ), QVariantList() << 1.0 << 2.0 << 3.0 );
77+
QCOMPARE( QgsJsonUtils::parseArray( R"([1.234567,2.00003e+4,-3.01234e-02])" ), QVariantList() << 1.234567 << 2.00003e+4 << -3.01234e-2 );
78+
// Strings
79+
QCOMPARE( QgsJsonUtils::parseArray( R"(["one", "two", "three"])" ), QVariantList() << "one" << "two" << "three" );
80+
QCOMPARE( QgsJsonUtils::parseArray( R"(["one,comma", "two[]brackets", "three\"escaped"])" ), QVariantList() << "one,comma" << "two[]brackets" << "three\"escaped" );
81+
// Nested (not implemented: discard deeper levels)
82+
//QCOMPARE( QgsJsonUtils::parseArray( R"([1.0,[2.0,5.0],3.0])" ), QVariantList() << 1.0 << 3.0 );
83+
// Mixed types
84+
QCOMPARE( QgsJsonUtils::parseArray( R"([1,"a",2.0])" ), QVariantList() << 1 << "a" << 2.0 );
85+
// discarded ...
86+
QCOMPARE( QgsJsonUtils::parseArray( R"([1,"a",2.0])", QVariant::Int ), QVariantList() << 1 << 2.0 );
87+
// Try invalid JSON
88+
QCOMPARE( QgsJsonUtils::parseArray( R"(not valid json here)" ), QVariantList() );
89+
QCOMPARE( QgsJsonUtils::parseArray( R"(not valid json here)", QVariant::Int ), QVariantList() );
90+
// Empty
91+
QCOMPARE( QgsJsonUtils::parseArray( R"([])", QVariant::Int ), QVariantList() );
92+
QCOMPARE( QgsJsonUtils::parseArray( "", QVariant::Int ), QVariantList() );
93+
// Nulls
94+
QCOMPARE( QgsJsonUtils::parseArray( R"([null, null])" ), QVariantList() << QVariant() << QVariant() );
95+
}
96+
97+
void TestQgsJsonUtils::testIntList()
98+
{
99+
QVariantList list;
100+
101+
{
102+
list << 1 << -2;
103+
const QString json = QgsJsonUtils::encodeValue( list );
104+
QCOMPARE( json, QString( "[1,-2]" ) );
105+
const QVariantList back = QgsJsonUtils::parseArray( json, QVariant::Int );
106+
QCOMPARE( back, list );
107+
QCOMPARE( back.at( 0 ).type(), QVariant::Int );
108+
}
109+
110+
{
111+
// check invalid entries are ignored
112+
const QVariantList back = QgsJsonUtils::parseArray( QStringLiteral( "[1,\"a\",-2]" ), QVariant::Int );
113+
QCOMPARE( back, list );
114+
}
115+
}
116+
117+
void TestQgsJsonUtils::testDoubleList()
118+
{
119+
QVariantList list;
77120

78-
void testDoubleList()
79-
{
80-
QVariantList list;
81-
82-
list << 1.0 << -2.2456;
83-
const QString json = QgsJsonUtils::encodeValue( list );
84-
QCOMPARE( json, QString( "[1,-2.2456]" ) );
85-
const QVariantList back = QgsJsonUtils::parseArray( json, QVariant::Double );
86-
QCOMPARE( back, list );
87-
QCOMPARE( back.at( 0 ).type(), QVariant::Double );
88-
}
121+
list << 1.0 << -2.2456;
122+
const QString json = QgsJsonUtils::encodeValue( list );
123+
QCOMPARE( json, QString( "[1,-2.2456]" ) );
124+
const QVariantList back = QgsJsonUtils::parseArray( json, QVariant::Double );
125+
QCOMPARE( back, list );
126+
QCOMPARE( back.at( 0 ).type(), QVariant::Double );
127+
}
89128

90-
void testExportAttributesJson_data()
91-
{
92-
QTest::addColumn<JsonAlgs>( "jsonAlg" );
93-
QTest::newRow( "Use json" ) << JsonAlgs::Json;
94-
QTest::newRow( "Use old string concat" ) << JsonAlgs::String;
95-
}
129+
void TestQgsJsonUtils::testExportAttributesJson_data()
130+
{
131+
QTest::addColumn<JsonAlgs>( "JsonAlgs" );
132+
QTest::newRow( "Use json" ) << JsonAlgs::Json;
133+
QTest::newRow( "Use old string concat" ) << JsonAlgs::String;
134+
}
96135

97-
void testExportAttributesJson()
98-
{
136+
void TestQgsJsonUtils::testExportAttributesJson()
137+
{
99138

100-
QFETCH( enum JsonAlgs, jsonAlg );
139+
QFETCH( enum JsonAlgs, JsonAlgs );
101140

102-
QgsVectorLayer vl { QStringLiteral( "Point?field=fldtxt:string&field=fldint:integer&field=flddbl:double" ), QStringLiteral( "mem" ), QStringLiteral( "memory" ) };
103-
QgsFeature feature { vl.fields() };
104-
feature.setAttributes( QgsAttributes() << QStringLiteral( "a value" ) << 1 << 2.0 );
141+
QgsVectorLayer vl { QStringLiteral( "Point?field=fldtxt:string&field=fldint:integer&field=flddbl:double" ), QStringLiteral( "mem" ), QStringLiteral( "memory" ) };
142+
QgsFeature feature { vl.fields() };
143+
feature.setAttributes( QgsAttributes() << QStringLiteral( "a value" ) << 1 << 2.0 );
105144

106-
if ( jsonAlg == JsonAlgs::Json ) // 0.0022
107-
{
108-
QBENCHMARK
109-
{
110-
json j { QgsJsonUtils::exportAttributesToJsonObject( feature, &vl ) };
111-
QCOMPARE( QString::fromStdString( j.dump() ), QStringLiteral( R"raw({"flddbl":2.0,"fldint":1,"fldtxt":"a value"})raw" ) );
112-
}
113-
}
114-
else // 0.0032
115-
{
116-
QBENCHMARK
117-
{
118-
const auto json { QgsJsonUtils::exportAttributes( feature, &vl ) };
119-
QCOMPARE( json, QStringLiteral( "{\"fldtxt\":\"a value\",\n\"fldint\":1,\n\"flddbl\":2}" ) );
120-
}
121-
}
145+
if ( JsonAlgs == JsonAlgs::Json ) // 0.0022
146+
{
147+
QBENCHMARK
148+
{
149+
json j { QgsJsonUtils::exportAttributesToJsonObject( feature, &vl ) };
150+
QCOMPARE( QString::fromStdString( j.dump() ), QStringLiteral( R"raw({"flddbl":2.0,"fldint":1,"fldtxt":"a value"})raw" ) );
122151
}
123-
124-
void testExportFeatureJson()
152+
}
153+
else // 0.0032
154+
{
155+
QBENCHMARK
125156
{
157+
const auto json { QgsJsonUtils::exportAttributes( feature, &vl ) };
158+
QCOMPARE( json, QStringLiteral( "{\"fldtxt\":\"a value\",\n\"fldint\":1,\n\"flddbl\":2}" ) );
159+
}
160+
}
161+
}
126162

163+
void TestQgsJsonUtils::testExportFeatureJson()
164+
{
127165

128-
QgsVectorLayer vl { QStringLiteral( "Polygon?field=fldtxt:string&field=fldint:integer&field=flddbl:double" ), QStringLiteral( "mem" ), QStringLiteral( "memory" ) };
129-
QgsFeature feature { vl.fields() };
130-
feature.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON((1.12 1.34,5.45 1.12,5.34 5.33,1.56 5.2,1.12 1.34),(2 2, 3 2, 3 3, 2 3,2 2))" ) ) );
131-
feature.setAttributes( QgsAttributes() << QStringLiteral( "a value" ) << 1 << 2.0 );
132166

133-
QgsJsonExporter exporter { &vl };
167+
QgsVectorLayer vl { QStringLiteral( "Polygon?field=fldtxt:string&field=fldint:integer&field=flddbl:double" ), QStringLiteral( "mem" ), QStringLiteral( "memory" ) };
168+
QgsFeature feature { vl.fields() };
169+
feature.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON((1.12 1.34,5.45 1.12,5.34 5.33,1.56 5.2,1.12 1.34),(2 2, 3 2, 3 3, 2 3,2 2))" ) ) );
170+
feature.setAttributes( QgsAttributes() << QStringLiteral( "a value" ) << 1 << 2.0 );
134171

135-
const auto expectedJson { QStringLiteral( "{\"bbox\":[[1.12,1.12,5.45,5.33]],\"geometry\":{\"coordinates\":"
136-
"[[[1.12,1.34],[5.45,1.12],[5.34,5.33],[1.56,5.2],[1.12,1.34]],"
137-
"[[2.0,2.0],[3.0,2.0],[3.0,3.0],[2.0,3.0],[2.0,2.0]]],\"type\":\"Polygon\"}"
138-
",\"id\":0,\"properties\":{\"flddbl\":2.0,\"fldint\":1,\"fldtxt\":\"a value\"}"
139-
",\"type\":\"Feature\"}" ) };
172+
QgsJsonExporter exporter { &vl };
140173

141-
const auto j { exporter.exportFeatureToJsonObject( feature ) };
142-
QCOMPARE( QString::fromStdString( j.dump() ), expectedJson );
143-
const auto json { exporter.exportFeature( feature ) };
144-
QCOMPARE( json, expectedJson );
145-
}
174+
const auto expectedJson { QStringLiteral( "{\"bbox\":[[1.12,1.12,5.45,5.33]],\"geometry\":{\"coordinates\":"
175+
"[[[1.12,1.34],[5.45,1.12],[5.34,5.33],[1.56,5.2],[1.12,1.34]],"
176+
"[[2.0,2.0],[3.0,2.0],[3.0,3.0],[2.0,3.0],[2.0,2.0]]],\"type\":\"Polygon\"}"
177+
",\"id\":0,\"properties\":{\"flddbl\":2.0,\"fldint\":1,\"fldtxt\":\"a value\"}"
178+
",\"type\":\"Feature\"}" ) };
146179

180+
const auto j { exporter.exportFeatureToJsonObject( feature ) };
181+
QCOMPARE( QString::fromStdString( j.dump() ), expectedJson );
182+
const auto json { exporter.exportFeature( feature ) };
183+
QCOMPARE( json, expectedJson );
184+
}
147185

148-
void testExportGeomToJson()
186+
void TestQgsJsonUtils::testExportGeomToJson()
187+
{
188+
const QMap<QString, QString> testWkts
189+
{
149190
{
150-
const QMap<QString, QString> testWkts
151191
{
152-
{
153-
{
154-
QStringLiteral( "LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)" ),
155-
QStringLiteral( R"json({"coordinates":[[-71.16,42.259],[-71.161,42.259],[-71.161,42.259]],"type":"LineString"})json" )
156-
},
157-
{
158-
QStringLiteral( "MULTILINESTRING((-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932), (-70 43.56, -67 44.68))" ),
159-
QStringLiteral( R"json({"coordinates":[[[-71.16,42.259],[-71.161,42.259],[-71.161,42.259]],[[-70.0,43.56],[-67.0,44.68]]],"type":"MultiLineString"})json" )
160-
},
161-
{ QStringLiteral( "POINT(-71.064544 42.28787)" ), QStringLiteral( R"json({"coordinates":[-71.065,42.288],"type":"Point"})json" ) },
162-
{ QStringLiteral( "MULTIPOINT(-71.064544 42.28787, -71.1776585052917 42.3902909739571)" ), QStringLiteral( R"json({"coordinates":[[-71.065,42.288],[-71.178,42.39]],"type":"MultiPoint"})json" ) },
163-
{
164-
QStringLiteral( "POLYGON((-71.1776585052917 42.3902909739571,-71.1776820268866 42.3903701743239,"
165-
"-71.1776063012595 42.3903825660754,-71.1775826583081 42.3903033653531,-71.1776585052917 42.3902909739571))" ),
166-
QStringLiteral( R"json({"coordinates":[[[-71.178,42.39],[-71.178,42.39],[-71.178,42.39],[-71.178,42.39],[-71.178,42.39]]],"type":"Polygon"})json" )
167-
},
168-
{
169-
QStringLiteral( "MULTIPOLYGON(((1 1,5 1,5 5,1 5,1 1),(2 2, 3 2, 3 3, 2 3,2 2)),((3 3,6 2,6 4,3 3)))" ),
170-
QStringLiteral( R"json({"coordinates":[[[[1.0,1.0],[5.0,1.0],[5.0,5.0],[1.0,5.0],[1.0,1.0]],[[2.0,2.0],[3.0,2.0],[3.0,3.0],[2.0,3.0],[2.0,2.0]]],[[[3.0,3.0],[6.0,2.0],[6.0,4.0],[3.0,3.0]]]],"type":"MultiPolygon"})json" )
171-
},
172-
// Note: CIRCULARSTRING json is very long, we will check first three vertices only
173-
{ QStringLiteral( "CIRCULARSTRING(220268 150415,220227 150505,220227 150406)" ), QStringLiteral( R"json({"coordinates":[[220268.0,150415.0],[220268.7,150415.535],[220269.391,150416.081])json" ) },
174-
}
175-
};
176-
177-
for ( const auto &w : testWkts.toStdMap() )
192+
QStringLiteral( "LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)" ),
193+
QStringLiteral( R"json({"coordinates":[[-71.16,42.259],[-71.161,42.259],[-71.161,42.259]],"type":"LineString"})json" )
194+
},
195+
{
196+
QStringLiteral( "MULTILINESTRING((-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932), (-70 43.56, -67 44.68))" ),
197+
QStringLiteral( R"json({"coordinates":[[[-71.16,42.259],[-71.161,42.259],[-71.161,42.259]],[[-70.0,43.56],[-67.0,44.68]]],"type":"MultiLineString"})json" )
198+
},
199+
{ QStringLiteral( "POINT(-71.064544 42.28787)" ), QStringLiteral( R"json({"coordinates":[-71.065,42.288],"type":"Point"})json" ) },
200+
{ QStringLiteral( "MULTIPOINT(-71.064544 42.28787, -71.1776585052917 42.3902909739571)" ), QStringLiteral( R"json({"coordinates":[[-71.065,42.288],[-71.178,42.39]],"type":"MultiPoint"})json" ) },
201+
{
202+
QStringLiteral( "POLYGON((-71.1776585052917 42.3902909739571,-71.1776820268866 42.3903701743239,"
203+
"-71.1776063012595 42.3903825660754,-71.1775826583081 42.3903033653531,-71.1776585052917 42.3902909739571))" ),
204+
QStringLiteral( R"json({"coordinates":[[[-71.178,42.39],[-71.178,42.39],[-71.178,42.39],[-71.178,42.39],[-71.178,42.39]]],"type":"Polygon"})json" )
205+
},
178206
{
179-
const auto g { QgsGeometry::fromWkt( w.first ) };
180-
QVERIFY( !g.isNull( ) );
181-
if ( w.first.startsWith( QStringLiteral( "CIRCULARSTRING" ) ) )
182-
{
183-
QVERIFY( g.asJson( 3 ).startsWith( w.second ) );
184-
QCOMPARE( QString::fromStdString( g.asJsonObject( 3 )["type"].dump() ), QStringLiteral( R"("LineString")" ) );
185-
}
186-
else
187-
{
188-
QCOMPARE( g.asJson( 3 ), w.second );
189-
}
190-
}
207+
QStringLiteral( "MULTIPOLYGON(((1 1,5 1,5 5,1 5,1 1),(2 2, 3 2, 3 3, 2 3,2 2)),((3 3,6 2,6 4,3 3)))" ),
208+
QStringLiteral( R"json({"coordinates":[[[[1.0,1.0],[5.0,1.0],[5.0,5.0],[1.0,5.0],[1.0,1.0]],[[2.0,2.0],[3.0,2.0],[3.0,3.0],[2.0,3.0],[2.0,2.0]]],[[[3.0,3.0],[6.0,2.0],[6.0,4.0],[3.0,3.0]]]],"type":"MultiPolygon"})json" )
209+
},
210+
// Note: CIRCULARSTRING json is very long, we will check first three vertices only
211+
{ QStringLiteral( "CIRCULARSTRING(220268 150415,220227 150505,220227 150406)" ), QStringLiteral( R"json({"coordinates":[[220268.0,150415.0],[220268.7,150415.535],[220269.391,150416.081])json" ) },
191212
}
192-
};
213+
};
214+
215+
for ( const auto &w : testWkts.toStdMap() )
216+
{
217+
const auto g { QgsGeometry::fromWkt( w.first ) };
218+
QVERIFY( !g.isNull( ) );
219+
if ( w.first.startsWith( QStringLiteral( "CIRCULARSTRING" ) ) )
220+
{
221+
QVERIFY( g.asJson( 3 ).startsWith( w.second ) );
222+
QCOMPARE( QString::fromStdString( g.asJsonObject( 3 )["type"].dump() ), QStringLiteral( R"("LineString")" ) );
223+
}
224+
else
225+
{
226+
QCOMPARE( g.asJson( 3 ), w.second );
227+
}
228+
}
229+
}
193230

194231
QGSTEST_MAIN( TestQgsJsonUtils )
195232
#include "testqgsjsonutils.moc"

‎tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp

Lines changed: 314 additions & 6 deletions
Large diffs are not rendered by default.

‎tests/src/python/test_provider_spatialite.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ def setUpClass(cls):
163163
sql += "VALUES (1, '[\"toto\",\"tutu\"]', '[1,-2,724562]', '[1.0, -232567.22]', GeomFromText('POLYGON((0 0,1 0,1 1,0 1,0 0))', 4326))"
164164
cur.execute(sql)
165165

166+
# table with different array types, stored as JSON
167+
sql = "CREATE TABLE test_arrays_write (Id INTEGER NOT NULL PRIMARY KEY, array JSONARRAY NOT NULL, strings JSONSTRINGLIST NOT NULL, ints JSONINTEGERLIST NOT NULL, reals JSONREALLIST NOT NULL)"
168+
cur.execute(sql)
169+
sql = "SELECT AddGeometryColumn('test_arrays_write', 'Geometry', 4326, 'POLYGON', 'XY')"
170+
cur.execute(sql)
171+
166172
# 2 tables with relations
167173
sql = "PRAGMA foreign_keys = ON;"
168174
cur.execute(sql)
@@ -533,6 +539,45 @@ def test_arrays(self):
533539
self.assertEqual(read_back['ints'], new_f['ints'])
534540
self.assertEqual(read_back['reals'], new_f['reals'])
535541

542+
def test_arrays_write(self):
543+
"""Test writing of layers with arrays"""
544+
l = QgsVectorLayer("dbname=%s table=test_arrays_write (geometry)" % self.dbname, "test_arrays", "spatialite")
545+
self.assertTrue(l.isValid())
546+
547+
new_f = QgsFeature(l.fields())
548+
new_f['id'] = 2
549+
new_f['array'] = ['simple', '"doubleQuote"', "'quote'", 'back\\slash']
550+
new_f['strings'] = ['simple', '"doubleQuote"', "'quote'", 'back\\slash']
551+
new_f['ints'] = [1, 2, 3, 4]
552+
new_f['reals'] = [1e67, 1e-56]
553+
r, fs = l.dataProvider().addFeatures([new_f])
554+
self.assertTrue(r)
555+
556+
read_back = l.getFeature(new_f['id'])
557+
self.assertEqual(read_back['id'], new_f['id'])
558+
self.assertEqual(read_back['array'], new_f['array'])
559+
self.assertEqual(read_back['strings'], new_f['strings'])
560+
self.assertEqual(read_back['ints'], new_f['ints'])
561+
self.assertEqual(read_back['reals'], new_f['reals'])
562+
563+
new_f = QgsFeature(l.fields())
564+
new_f['id'] = 3
565+
new_f['array'] = [1, 1.2345, '"doubleQuote"', "'quote'", 'back\\slash']
566+
new_f['strings'] = ['simple', '"doubleQuote"', "'quote'", 'back\\slash']
567+
new_f['ints'] = [1, 2, 3, 4]
568+
new_f['reals'] = [1e67, 1e-56]
569+
r, fs = l.dataProvider().addFeatures([new_f])
570+
self.assertTrue(r)
571+
572+
read_back = l.getFeature(new_f['id'])
573+
self.assertEqual(read_back['id'], new_f['id'])
574+
self.assertEqual(read_back['array'], new_f['array'])
575+
self.assertEqual(read_back['strings'], new_f['strings'])
576+
self.assertEqual(read_back['ints'], new_f['ints'])
577+
self.assertEqual(read_back['reals'], new_f['reals'])
578+
579+
read_back = l.getFeature(new_f['id'])
580+
536581
def test_discover_relation(self):
537582
artist = QgsVectorLayer("dbname=%s table=test_relation_a (geometry)" % self.dbname, "test_relation_a", "spatialite")
538583
self.assertTrue(artist.isValid())

‎tests/src/python/test_qgsfieldformatters.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,12 @@ def _test(a, b):
130130
_test([1, 2, 3], ["1", "2", "3"])
131131
_test("{1,2,3}", ["1", "2", "3"])
132132
_test(['1', '2', '3'], ["1", "2", "3"])
133-
_test('not an array', ['not an array'])
133+
_test('not an array', [])
134+
_test('[1,2,3]', ["1", "2", "3"])
135+
_test('{1,2,3}', ["1", "2", "3"])
136+
_test('{"1","2","3"}', ["1", "2", "3"])
137+
_test('["1","2","3"]', ["1", "2", "3"])
138+
_test(r'["a string,comma","a string\"quote", "another string[]"]', ['a string,comma', 'a string"quote', 'another string[]'])
134139

135140
def test_expressionRequiresFormScope(self):
136141

Binary file not shown.

0 commit comments

Comments
 (0)
Please sign in to comment.