1 /***************************************************************************
2 testqgsvectorlayerjoinbuffer.cpp
3 --------------------------------------
4 Date : September 2014
5 Copyright : (C) 2014 Martin Dobias
6 Email : wonder.sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16
17 #include "qgstest.h"
18 #include <QObject>
19
20 //qgis includes...
21 #include <qgsvectorlayer.h>
22 #include "qgsfeatureiterator.h"
23 #include "qgslayertreegroup.h"
24 #include "qgsreadwritecontext.h"
25 #include <qgsvectordataprovider.h>
26 #include <qgsapplication.h>
27 #include <qgsvectorlayerjoinbuffer.h>
28 #include <qgslayerdefinition.h>
29 #include <qgsproject.h>
30 #include "qgslayertree.h"
31
32 /**
33 * @ingroup UnitTests
34 * This is a unit test for the vector layer join buffer
35 *
36 * \see QgsVectorLayerJoinBuffer
37 */
38 class TestVectorLayerJoinBuffer : public QObject
39 {
40 Q_OBJECT
41
42 public:
TestVectorLayerJoinBuffer()43 TestVectorLayerJoinBuffer()
44 : mLayers( QMap<QPair<QString, QString>, QgsVectorLayer*>() )
45 {}
46
47 private slots:
48 void initTestCase(); // will be called before the first testfunction is executed.
49 void cleanupTestCase(); // will be called after the last testfunction was executed.
50 void init(); // will be called before each testfunction is executed.
51 void cleanup(); // will be called after every testfunction.
52
53 void testJoinBasic_data();
54 void testJoinBasic();
55 void testJoinTransitive_data();
56 void testJoinTransitive();
57 void testJoinDetectCycle_data();
58 void testJoinDetectCycle();
59 void testJoinSubset_data();
60 void testJoinSubset();
61 void testJoinTwoTimes_data();
62 void testJoinTwoTimes();
63 void testJoinLayerDefinitionFile();
64 void testCacheUpdate_data();
65 void testCacheUpdate();
66 void testRemoveJoinOnLayerDelete();
67 void testResolveReferences();
68 void testSignals();
69 void testChangeAttributeValues();
70 void testCollidingNameColumnCached();
71
72 private:
73 QgsProject mProject;
74 QList<QString> mProviders;
75 // map of layers. First key is the name of the layer A, B or C and second key is the provider memory or PG.
76 QMap<QPair<QString, QString>, QgsVectorLayer *> mLayers;
77 };
78
79 // runs before all tests
initTestCase()80 void TestVectorLayerJoinBuffer::initTestCase()
81 {
82 QgsApplication::init();
83 QgsApplication::initQgis();
84
85 // Set up the QgsSettings environment
86 QCoreApplication::setOrganizationName( QStringLiteral( "QGIS" ) );
87 QCoreApplication::setOrganizationDomain( QStringLiteral( "qgis.org" ) );
88 QCoreApplication::setApplicationName( QStringLiteral( "QGIS-TEST" ) );
89
90 mProviders = QList<QString>() << QStringLiteral( "memory" );
91
92 // Create memory layers
93 // LAYER A //
94 QgsVectorLayer *vlA = new QgsVectorLayer( QStringLiteral( "Point?field=id_a:integer" ), QStringLiteral( "A" ), QStringLiteral( "memory" ) );
95 QVERIFY( vlA->isValid() );
96 QVERIFY( vlA->fields().count() == 1 );
97 // LAYER B //
98 QgsVectorLayer *vlB = new QgsVectorLayer( QStringLiteral( "Point?field=id_b:integer&field=value_b" ), QStringLiteral( "B" ), QStringLiteral( "memory" ) );
99 QVERIFY( vlB->isValid() );
100 QVERIFY( vlB->fields().count() == 2 );
101 // LAYER C //
102 QgsVectorLayer *vlC = new QgsVectorLayer( QStringLiteral( "Point?field=id_c:integer&field=value_c" ), QStringLiteral( "C" ), QStringLiteral( "memory" ) );
103 QVERIFY( vlC->isValid() );
104 QVERIFY( vlC->fields().count() == 2 );
105 // LAYER X //
106 QgsVectorLayer *vlX = new QgsVectorLayer( QStringLiteral( "Point?field=id_x:integer&field=value_x1:integer&field=value_x2" ), QStringLiteral( "X" ), QStringLiteral( "memory" ) );
107 QVERIFY( vlX->isValid() );
108 QVERIFY( vlX->fields().count() == 3 );
109
110 mLayers = QMap<QPair<QString, QString>, QgsVectorLayer *>();
111 mLayers.insert( QPair<QString, QString>( QStringLiteral( "A" ), QStringLiteral( "memory" ) ), vlA );
112 mLayers.insert( QPair<QString, QString>( QStringLiteral( "B" ), QStringLiteral( "memory" ) ), vlB );
113 mLayers.insert( QPair<QString, QString>( QStringLiteral( "C" ), QStringLiteral( "memory" ) ), vlC );
114 mLayers.insert( QPair<QString, QString>( QStringLiteral( "X" ), QStringLiteral( "memory" ) ), vlX );
115
116 // Add PG layers
117 #ifdef ENABLE_PGTEST
118 QString dbConn = getenv( "QGIS_PGTEST_DB" );
119 if ( dbConn.isEmpty() )
120 {
121 dbConn = "service=qgis_test";
122 }
123 QgsVectorLayer *vlA_PG = new QgsVectorLayer( QString( "%1 sslmode=disable key='id_a' table=\"qgis_test\".\"table_a\" sql=" ).arg( dbConn ), "A_PG", "postgres" );
124 QgsVectorLayer *vlB_PG = new QgsVectorLayer( QString( "%1 sslmode=disable key='id_b' table=\"qgis_test\".\"table_b\" sql=" ).arg( dbConn ), "B_PG", "postgres" );
125 QgsVectorLayer *vlC_PG = new QgsVectorLayer( QString( "%1 sslmode=disable key='id_c' table=\"qgis_test\".\"table_c\" sql=" ).arg( dbConn ), "C_PG", "postgres" );
126 QgsVectorLayer *vlX_PG = new QgsVectorLayer( QString( "%1 sslmode=disable key='id_x' table=\"qgis_test\".\"table_x\" sql=" ).arg( dbConn ), "X_PG", "postgres" );
127 QVERIFY( vlA_PG->isValid() );
128 QVERIFY( vlB_PG->isValid() );
129 QVERIFY( vlC_PG->isValid() );
130 QVERIFY( vlX_PG->isValid() );
131 QVERIFY( vlA_PG->fields().count() == 1 );
132 QVERIFY( vlB_PG->fields().count() == 2 );
133 QVERIFY( vlC_PG->fields().count() == 2 );
134 QVERIFY( vlX_PG->fields().count() == 3 );
135 mLayers.insert( QPair<QString, QString>( "A", "PG" ), vlA_PG );
136 mLayers.insert( QPair<QString, QString>( "B", "PG" ), vlB_PG );
137 mLayers.insert( QPair<QString, QString>( "C", "PG" ), vlC_PG );
138 mLayers.insert( QPair<QString, QString>( "X", "PG" ), vlX_PG );
139 mProviders << "PG";
140 #endif
141
142 // Create features
143 QgsFeature fA1( vlA->dataProvider()->fields(), 1 );
144 fA1.setAttribute( QStringLiteral( "id_a" ), 1 );
145 QgsFeature fA2( vlA->dataProvider()->fields(), 2 );
146 fA2.setAttribute( QStringLiteral( "id_a" ), 2 );
147 QgsFeature fB1( vlB->dataProvider()->fields(), 1 );
148 fB1.setAttribute( QStringLiteral( "id_b" ), 1 );
149 fB1.setAttribute( QStringLiteral( "value_b" ), 11 );
150 QgsFeature fB2( vlB->dataProvider()->fields(), 2 );
151 fB2.setAttribute( QStringLiteral( "id_b" ), 2 );
152 fB2.setAttribute( QStringLiteral( "value_b" ), 12 );
153 QgsFeature fC1( vlC->dataProvider()->fields(), 1 );
154 fC1.setAttribute( QStringLiteral( "id_c" ), 1 );
155 fC1.setAttribute( QStringLiteral( "value_c" ), 101 );
156 QgsFeature fX1( vlX->dataProvider()->fields(), 1 );
157 fX1.setAttribute( QStringLiteral( "id_x" ), 1 );
158 fX1.setAttribute( QStringLiteral( "value_x1" ), 111 );
159 fX1.setAttribute( QStringLiteral( "value_x2" ), 222 );
160
161 // Commit features and layers to qgis
162 Q_FOREACH ( const QString provider, mProviders )
163 {
164 QgsVectorLayer *vl = mLayers.value( QPair<QString, QString>( QStringLiteral( "A" ), provider ) );
165 vl->dataProvider()->addFeatures( QgsFeatureList() << fA1 << fA2 );
166 QVERIFY( vl->featureCount() == 2 );
167 mProject.addMapLayer( vl );
168 }
169
170 Q_FOREACH ( const QString provider, mProviders )
171 {
172 QgsVectorLayer *vl = mLayers.value( QPair<QString, QString>( QStringLiteral( "B" ), provider ) );
173 vl->dataProvider()->addFeatures( QgsFeatureList() << fB1 << fB2 );
174 QVERIFY( vl->featureCount() == 2 );
175 mProject.addMapLayer( vl );
176 }
177
178 Q_FOREACH ( const QString provider, mProviders )
179 {
180 QgsVectorLayer *vl = mLayers.value( QPair<QString, QString>( QStringLiteral( "C" ), provider ) );
181 vl->dataProvider()->addFeatures( QgsFeatureList() << fC1 );
182 QVERIFY( vl->featureCount() == 1 );
183 mProject.addMapLayer( vl );
184 }
185
186 Q_FOREACH ( const QString provider, mProviders )
187 {
188 QgsVectorLayer *vl = mLayers.value( QPair<QString, QString>( QStringLiteral( "X" ), provider ) );
189 vl->dataProvider()->addFeatures( QgsFeatureList() << fX1 );
190 QVERIFY( vl->featureCount() == 1 );
191 mProject.addMapLayer( vl );
192 }
193
194 QVERIFY( mProject.mapLayers().count() == 4 * mProviders.count() );
195 }
196
init()197 void TestVectorLayerJoinBuffer::init()
198 {
199 }
200
cleanup()201 void TestVectorLayerJoinBuffer::cleanup()
202 {
203 }
204
cleanupTestCase()205 void TestVectorLayerJoinBuffer::cleanupTestCase()
206 {
207 QgsApplication::exitQgis();
208 }
209
testJoinBasic_data()210 void TestVectorLayerJoinBuffer::testJoinBasic_data()
211 {
212 QTest::addColumn<QString>( "provider" );
213 QTest::addColumn<bool>( "memoryCache" );
214
215 QTest::newRow( "memory with cache" ) << "memory" << true ;
216 QTest::newRow( "memory without cache" ) << "memory" << false;
217
218 #ifdef ENABLE_PGTEST
219 QTest::newRow( "postgresql with cache" ) << "PG" << true ;
220 QTest::newRow( "postgresql without cache" ) << "PG" << false;
221 #endif
222 }
223
testJoinBasic()224 void TestVectorLayerJoinBuffer::testJoinBasic()
225 {
226 QFETCH( bool, memoryCache );
227 QFETCH( QString, provider );
228
229 QgsVectorLayer *vlA = mLayers.value( QPair<QString, QString>( QStringLiteral( "A" ), provider ) );
230 QgsVectorLayer *vlB = mLayers.value( QPair<QString, QString>( QStringLiteral( "B" ), provider ) );
231
232 QVERIFY( vlA->fields().count() == 1 );
233
234 QgsVectorLayerJoinInfo joinInfo;
235 joinInfo.setTargetFieldName( QStringLiteral( "id_a" ) );
236 joinInfo.setJoinLayer( vlB );
237 joinInfo.setJoinFieldName( QStringLiteral( "id_b" ) );
238 joinInfo.setUsingMemoryCache( memoryCache );
239 joinInfo.setPrefix( QStringLiteral( "B_" ) );
240 vlA->addJoin( joinInfo );
241
242 QVERIFY( vlA->fields().count() == 2 );
243
244 QgsFeatureIterator fi = vlA->getFeatures();
245 QgsFeature fA1, fA2;
246 fi.nextFeature( fA1 );
247 QCOMPARE( fA1.attribute( "id_a" ).toInt(), 1 );
248 QCOMPARE( fA1.attribute( "B_value_b" ).toInt(), 11 );
249 fi.nextFeature( fA2 );
250 QCOMPARE( fA2.attribute( "id_a" ).toInt(), 2 );
251 QCOMPARE( fA2.attribute( "B_value_b" ).toInt(), 12 );
252
253 vlA->removeJoin( vlB->id() );
254
255 QVERIFY( vlA->fields().count() == 1 );
256 }
257
testJoinTransitive_data()258 void TestVectorLayerJoinBuffer::testJoinTransitive_data()
259 {
260 QTest::addColumn<QString>( "provider" );
261 QTest::newRow( "memory" ) << "memory";
262 #ifdef ENABLE_PGTEST
263 QTest::newRow( "postgresql" ) << "PG";
264 #endif
265 }
266
testJoinTransitive()267 void TestVectorLayerJoinBuffer::testJoinTransitive()
268 {
269
270 QFETCH( QString, provider );
271
272 QgsVectorLayer *vlA = mLayers.value( QPair<QString, QString>( QStringLiteral( "A" ), provider ) );
273 QgsVectorLayer *vlB = mLayers.value( QPair<QString, QString>( QStringLiteral( "B" ), provider ) );
274 QgsVectorLayer *vlC = mLayers.value( QPair<QString, QString>( QStringLiteral( "C" ), provider ) );
275
276 // test join A -> B -> C
277 // first we join A -> B and after that B -> C
278 // layer A should automatically update to include joined data from C
279
280 QVERIFY( vlA->fields().count() == 1 ); // id_a
281
282 // add join A -> B
283
284 QgsVectorLayerJoinInfo joinInfo1;
285 joinInfo1.setTargetFieldName( QStringLiteral( "id_a" ) );
286 joinInfo1.setJoinLayer( vlB );
287 joinInfo1.setJoinFieldName( QStringLiteral( "id_b" ) );
288 joinInfo1.setUsingMemoryCache( true );
289 joinInfo1.setPrefix( QStringLiteral( "B_" ) );
290 vlA->addJoin( joinInfo1 );
291 QVERIFY( vlA->fields().count() == 2 ); // id_a, B_value_b
292
293 // add join B -> C
294
295 QgsVectorLayerJoinInfo joinInfo2;
296 joinInfo2.setTargetFieldName( QStringLiteral( "id_b" ) );
297 joinInfo2.setJoinLayer( vlC );
298 joinInfo2.setJoinFieldName( QStringLiteral( "id_c" ) );
299 joinInfo2.setUsingMemoryCache( true );
300 joinInfo2.setPrefix( QStringLiteral( "C_" ) );
301 vlB->addJoin( joinInfo2 );
302 QVERIFY( vlB->fields().count() == 3 ); // id_b, value_b, C_value_c
303
304 // now layer A must include also data from layer C
305 QVERIFY( vlA->fields().count() == 3 ); // id_a, B_value_b, B_C_value_c
306
307 QgsFeatureIterator fi = vlA->getFeatures();
308 QgsFeature fA1;
309 fi.nextFeature( fA1 );
310 QCOMPARE( fA1.attribute( "id_a" ).toInt(), 1 );
311 QCOMPARE( fA1.attribute( "B_value_b" ).toInt(), 11 );
312 QCOMPARE( fA1.attribute( "B_C_value_c" ).toInt(), 101 );
313
314 // test that layer A gets updated when layer C changes its fields
315 vlC->addExpressionField( QStringLiteral( "123" ), QgsField( QStringLiteral( "dummy" ), QVariant::Int ) );
316 QVERIFY( vlA->fields().count() == 4 ); // id_a, B_value_b, B_C_value_c, B_C_dummy
317 vlC->removeExpressionField( 0 );
318
319 // cleanup
320 vlA->removeJoin( vlB->id() );
321 vlB->removeJoin( vlC->id() );
322 }
323
testJoinDetectCycle_data()324 void TestVectorLayerJoinBuffer::testJoinDetectCycle_data()
325 {
326 QTest::addColumn<QString>( "provider" );
327 QTest::newRow( "memory" ) << "memory";
328 #ifdef ENABLE_PGTEST
329 QTest::newRow( "postgresql" ) << "PG";
330 #endif
331 }
332
testJoinDetectCycle()333 void TestVectorLayerJoinBuffer::testJoinDetectCycle()
334 {
335 QFETCH( QString, provider );
336
337 QgsVectorLayer *vlA = mLayers.value( QPair<QString, QString>( QStringLiteral( "A" ), provider ) );
338 QgsVectorLayer *vlB = mLayers.value( QPair<QString, QString>( QStringLiteral( "B" ), provider ) );
339
340 // if A joins B and B joins A, we may get to an infinite loop if the case is not handled properly
341
342 QgsVectorLayerJoinInfo joinInfo;
343 joinInfo.setTargetFieldName( QStringLiteral( "id_a" ) );
344 joinInfo.setJoinLayer( vlB );
345 joinInfo.setJoinFieldName( QStringLiteral( "id_b" ) );
346 joinInfo.setUsingMemoryCache( true );
347 joinInfo.setPrefix( QStringLiteral( "B_" ) );
348 vlA->addJoin( joinInfo );
349
350 QgsVectorLayerJoinInfo joinInfo2;
351 joinInfo2.setTargetFieldName( QStringLiteral( "id_b" ) );
352 joinInfo2.setJoinLayer( vlA );
353 joinInfo2.setJoinFieldName( QStringLiteral( "id_a" ) );
354 joinInfo2.setUsingMemoryCache( true );
355 joinInfo2.setPrefix( QStringLiteral( "A_" ) );
356 bool res = vlB->addJoin( joinInfo2 );
357
358 QVERIFY( !res );
359
360 // the join in layer B must be rejected
361 QVERIFY( vlB->vectorJoins().isEmpty() );
362
363 vlA->removeJoin( vlB->id() );
364 }
365
366
testJoinSubset_data()367 void TestVectorLayerJoinBuffer::testJoinSubset_data()
368 {
369 QTest::addColumn<QString>( "provider" );
370 QTest::addColumn<bool>( "memoryCache" );
371
372 QTest::newRow( "memory with cache" ) << "memory" << true ;
373 QTest::newRow( "memory without cache" ) << "memory" << false;
374
375 #ifdef ENABLE_PGTEST
376 QTest::newRow( "postgresql with cache" ) << "PG" << true ;
377 QTest::newRow( "postgresql without cache" ) << "PG" << false;
378 #endif
379 }
380
381
testJoinSubset()382 void TestVectorLayerJoinBuffer::testJoinSubset()
383 {
384 QFETCH( bool, memoryCache );
385 QFETCH( QString, provider );
386
387 QVERIFY( mProject.mapLayers().count() == 4 * mProviders.count() );
388
389 QgsVectorLayer *vlA = mLayers.value( QPair<QString, QString>( QStringLiteral( "A" ), provider ) );
390 QgsVectorLayer *vlX = mLayers.value( QPair<QString, QString>( QStringLiteral( "X" ), provider ) );
391
392 // case 1: join without subset
393
394 QgsVectorLayerJoinInfo joinInfo;
395 joinInfo.setTargetFieldName( QStringLiteral( "id_a" ) );
396 joinInfo.setJoinLayer( vlX );
397 joinInfo.setJoinFieldName( QStringLiteral( "id_x" ) );
398 joinInfo.setUsingMemoryCache( memoryCache );
399 joinInfo.setPrefix( QStringLiteral( "X_" ) );
400 bool res = vlA->addJoin( joinInfo );
401 QVERIFY( res );
402
403 QCOMPARE( vlA->fields().count(), 3 ); // id_a, X_value_x1, X_value_x2
404 QgsFeatureIterator fi = vlA->getFeatures();
405 QgsFeature fAX;
406 fi.nextFeature( fAX );
407 QCOMPARE( fAX.attribute( "id_a" ).toInt(), 1 );
408 QCOMPARE( fAX.attribute( "X_value_x1" ).toInt(), 111 );
409 QCOMPARE( fAX.attribute( "X_value_x2" ).toInt(), 222 );
410
411 vlA->removeJoin( vlX->id() );
412
413 // case 2: join with subset
414
415 QStringList *subset = new QStringList;
416 *subset << QStringLiteral( "value_x2" );
417 joinInfo.setJoinFieldNamesSubset( subset );
418 vlA->addJoin( joinInfo );
419
420 QCOMPARE( vlA->fields().count(), 2 ); // id_a, X_value_x2
421
422 fi = vlA->getFeatures();
423 fi.nextFeature( fAX );
424 QCOMPARE( fAX.attribute( "id_a" ).toInt(), 1 );
425 QCOMPARE( fAX.attribute( "X_value_x2" ).toInt(), 222 );
426
427 vlA->removeJoin( vlX->id() );
428 }
429
testJoinTwoTimes_data()430 void TestVectorLayerJoinBuffer::testJoinTwoTimes_data()
431 {
432 QTest::addColumn<QString>( "provider" );
433 QTest::newRow( "memory" ) << "memory";
434 #ifdef ENABLE_PGTEST
435 QTest::newRow( "postgresql" ) << "PG";
436 #endif
437 }
438
testJoinTwoTimes()439 void TestVectorLayerJoinBuffer::testJoinTwoTimes()
440 {
441
442 QFETCH( QString, provider );
443
444 QgsVectorLayer *vlA = mLayers.value( QPair<QString, QString>( QStringLiteral( "A" ), provider ) );
445 QgsVectorLayer *vlB = mLayers.value( QPair<QString, QString>( QStringLiteral( "B" ), provider ) );
446
447 QVERIFY( vlA->fields().count() == 1 );
448
449 QgsVectorLayerJoinInfo joinInfo1;
450 joinInfo1.setTargetFieldName( QStringLiteral( "id_a" ) );
451 joinInfo1.setJoinLayer( vlB );
452 joinInfo1.setJoinFieldName( QStringLiteral( "id_b" ) );
453 joinInfo1.setUsingMemoryCache( true );
454 joinInfo1.setPrefix( QStringLiteral( "j1_" ) );
455 vlA->addJoin( joinInfo1 );
456
457 QgsVectorLayerJoinInfo joinInfo2;
458 joinInfo2.setTargetFieldName( QStringLiteral( "id_a" ) );
459 joinInfo2.setJoinLayer( vlB );
460 joinInfo2.setJoinFieldName( QStringLiteral( "id_b" ) );
461 joinInfo2.setUsingMemoryCache( true );
462 joinInfo2.setPrefix( QStringLiteral( "j2_" ) );
463 vlA->addJoin( joinInfo2 );
464
465 QCOMPARE( vlA->vectorJoins().count(), 2 );
466
467 QVERIFY( vlA->fields().count() == 3 );
468
469 QgsFeatureIterator fi = vlA->getFeatures();
470 QgsFeature fA1; //, fA2;
471 fi.nextFeature( fA1 );
472 QCOMPARE( fA1.attribute( "id_a" ).toInt(), 1 );
473 QCOMPARE( fA1.attribute( "j1_value_b" ).toInt(), 11 );
474 QCOMPARE( fA1.attribute( "j2_value_b" ).toInt(), 11 );
475
476 vlA->removeJoin( vlB->id() );
477 vlA->removeJoin( vlB->id() );
478
479 QCOMPARE( vlA->vectorJoins().count(), 0 );
480 }
481
testJoinLayerDefinitionFile()482 void TestVectorLayerJoinBuffer::testJoinLayerDefinitionFile()
483 {
484 bool r;
485
486 mProject.removeAllMapLayers();
487
488 // Create two layers
489 QgsVectorLayer *layerA = new QgsVectorLayer( QStringLiteral( "Point?crs=epsg:4326&field=key:integer&field=value:double&index=yes" ), QStringLiteral( "layerA" ), QStringLiteral( "memory" ) );
490 QVERIFY( layerA );
491 mProject.addMapLayer( layerA );
492
493 QgsVectorLayer *layerB = new QgsVectorLayer( QStringLiteral( "Point?crs=epsg:4326&field=id:integer&index=yes" ), QStringLiteral( "layerB" ), QStringLiteral( "memory" ) );
494 QVERIFY( layerB );
495 mProject.addMapLayer( layerB );
496
497 // Create vector join
498 QgsVectorLayerJoinInfo joinInfo;
499 joinInfo.setTargetFieldName( QStringLiteral( "id" ) );
500 joinInfo.setJoinLayer( layerA );
501 joinInfo.setJoinFieldName( QStringLiteral( "key" ) );
502 joinInfo.setUsingMemoryCache( true );
503 joinInfo.setPrefix( QStringLiteral( "joined_" ) );
504 r = layerB->addJoin( joinInfo );
505 QVERIFY( r );
506
507 // Generate QLR
508 QDomDocument qlrDoc( QStringLiteral( "qgis-layer-definition" ) );
509 QString errorMessage;
510 r = QgsLayerDefinition::exportLayerDefinition( qlrDoc, mProject.layerTreeRoot()->children(), errorMessage, QgsReadWriteContext() );
511 QVERIFY2( r, errorMessage.toUtf8().constData() );
512
513 // Clear
514 mProject.removeAllMapLayers();
515
516 // Load QLR
517 QgsReadWriteContext context = QgsReadWriteContext();
518 r = QgsLayerDefinition::loadLayerDefinition( qlrDoc, &mProject, mProject.layerTreeRoot(), errorMessage, context );
519 QVERIFY2( r, errorMessage.toUtf8().constData() );
520
521 // Get layer
522 QList<QgsMapLayer *> mapLayers = mProject.mapLayersByName( QStringLiteral( "layerB" ) );
523 QCOMPARE( mapLayers.count(), 1 );
524
525 QgsVectorLayer *vLayer = dynamic_cast<QgsVectorLayer *>( mapLayers.value( 0 ) );
526 QVERIFY( vLayer );
527
528 // Check for vector join
529 QCOMPARE( vLayer->vectorJoins().count(), 1 );
530
531 // Check for joined field
532 QVERIFY( vLayer->fields().lookupField( joinInfo.prefix() + "value" ) >= 0 );
533 }
534
testCacheUpdate_data()535 void TestVectorLayerJoinBuffer::testCacheUpdate_data()
536 {
537 QTest::addColumn<bool>( "useCache" );
538 QTest::newRow( "cache" ) << true;
539 QTest::newRow( "no cache" ) << false;
540 }
541
testCacheUpdate()542 void TestVectorLayerJoinBuffer::testCacheUpdate()
543 {
544 QFETCH( bool, useCache );
545
546 QgsVectorLayer *vlA = new QgsVectorLayer( QStringLiteral( "Point?field=id_a:integer" ), QStringLiteral( "cacheA" ), QStringLiteral( "memory" ) );
547 QVERIFY( vlA->isValid() );
548 QgsVectorLayer *vlB = new QgsVectorLayer( QStringLiteral( "Point?field=id_b:integer&field=value_b" ), QStringLiteral( "cacheB" ), QStringLiteral( "memory" ) );
549 QVERIFY( vlB->isValid() );
550 mProject.addMapLayer( vlA );
551 mProject.addMapLayer( vlB );
552
553 QgsFeature fA1( vlA->dataProvider()->fields(), 1 );
554 fA1.setAttribute( QStringLiteral( "id_a" ), 1 );
555 QgsFeature fA2( vlA->dataProvider()->fields(), 2 );
556 fA2.setAttribute( QStringLiteral( "id_a" ), 2 );
557
558 vlA->dataProvider()->addFeatures( QgsFeatureList() << fA1 << fA2 );
559
560 QgsFeature fB1( vlB->dataProvider()->fields(), 1 );
561 fB1.setAttribute( QStringLiteral( "id_b" ), 1 );
562 fB1.setAttribute( QStringLiteral( "value_b" ), 11 );
563 QgsFeature fB2( vlB->dataProvider()->fields(), 2 );
564 fB2.setAttribute( QStringLiteral( "id_b" ), 2 );
565 fB2.setAttribute( QStringLiteral( "value_b" ), 12 );
566
567 vlB->dataProvider()->addFeatures( QgsFeatureList() << fB1 << fB2 );
568
569 QgsVectorLayerJoinInfo joinInfo;
570 joinInfo.setTargetFieldName( QStringLiteral( "id_a" ) );
571 joinInfo.setJoinLayer( vlB );
572 joinInfo.setJoinFieldName( QStringLiteral( "id_b" ) );
573 joinInfo.setUsingMemoryCache( useCache );
574 joinInfo.setPrefix( QStringLiteral( "B_" ) );
575 vlA->addJoin( joinInfo );
576
577 QgsFeatureIterator fi = vlA->getFeatures();
578 fi.nextFeature( fA1 );
579 QCOMPARE( fA1.attribute( "id_a" ).toInt(), 1 );
580 QCOMPARE( fA1.attribute( "B_value_b" ).toInt(), 11 );
581 fi.nextFeature( fA2 );
582 QCOMPARE( fA2.attribute( "id_a" ).toInt(), 2 );
583 QCOMPARE( fA2.attribute( "B_value_b" ).toInt(), 12 );
584
585 // change value in join target layer
586 vlB->startEditing();
587 vlB->changeAttributeValue( 1, 1, 111 );
588 vlB->changeAttributeValue( 2, 0, 3 );
589 vlB->commitChanges();
590
591 fi = vlA->getFeatures();
592 fi.nextFeature( fA1 );
593 QCOMPARE( fA1.attribute( "id_a" ).toInt(), 1 );
594 QCOMPARE( fA1.attribute( "B_value_b" ).toInt(), 111 );
595 fi.nextFeature( fA2 );
596 QCOMPARE( fA2.attribute( "id_a" ).toInt(), 2 );
597 QVERIFY( fA2.attribute( "B_value_b" ).isNull() );
598
599 // change value in joined layer
600 vlA->startEditing();
601 vlA->changeAttributeValue( 2, 0, 3 );
602 vlA->commitChanges();
603
604 fi = vlA->getFeatures();
605 fi.nextFeature( fA1 );
606 QCOMPARE( fA1.attribute( "id_a" ).toInt(), 1 );
607 QCOMPARE( fA1.attribute( "B_value_b" ).toInt(), 111 );
608 fi.nextFeature( fA2 );
609 QCOMPARE( fA2.attribute( "id_a" ).toInt(), 3 );
610 QCOMPARE( fA2.attribute( "B_value_b" ).toInt(), 12 );
611 }
612
testRemoveJoinOnLayerDelete()613 void TestVectorLayerJoinBuffer::testRemoveJoinOnLayerDelete()
614 {
615 QgsVectorLayer *vlA = new QgsVectorLayer( QStringLiteral( "Point?field=id_a:integer" ), QStringLiteral( "cacheA" ), QStringLiteral( "memory" ) );
616 QVERIFY( vlA->isValid() );
617 QgsVectorLayer *vlB = new QgsVectorLayer( QStringLiteral( "Point?field=id_b:integer&field=value_b" ), QStringLiteral( "cacheB" ), QStringLiteral( "memory" ) );
618 QVERIFY( vlB->isValid() );
619
620 QgsVectorLayerJoinInfo joinInfo;
621 joinInfo.setTargetFieldName( QStringLiteral( "id_a" ) );
622 joinInfo.setJoinLayer( vlB );
623 joinInfo.setJoinFieldName( QStringLiteral( "id_b" ) );
624 joinInfo.setUsingMemoryCache( true );
625 joinInfo.setPrefix( QStringLiteral( "B_" ) );
626 vlA->addJoin( joinInfo );
627
628 QCOMPARE( vlA->vectorJoins().count(), 1 );
629 QCOMPARE( vlA->vectorJoins()[0].joinLayer(), vlB );
630 QCOMPARE( vlA->vectorJoins()[0].joinLayerId(), vlB->id() );
631 QCOMPARE( vlA->fields().count(), 2 );
632
633 delete vlB;
634
635 QCOMPARE( vlA->vectorJoins().count(), 0 );
636 QCOMPARE( vlA->fields().count(), 1 );
637
638 delete vlA;
639 }
640
testResolveReferences()641 void TestVectorLayerJoinBuffer::testResolveReferences()
642 {
643 QgsVectorLayer *vlA = new QgsVectorLayer( QStringLiteral( "Point?field=id_a:integer" ), QStringLiteral( "cacheA" ), QStringLiteral( "memory" ) );
644 QVERIFY( vlA->isValid() );
645 QgsVectorLayer *vlB = new QgsVectorLayer( QStringLiteral( "Point?field=id_b:integer&field=value_b" ), QStringLiteral( "cacheB" ), QStringLiteral( "memory" ) );
646 QVERIFY( vlB->isValid() );
647
648 QgsVectorLayerJoinInfo joinInfo;
649 joinInfo.setTargetFieldName( QStringLiteral( "id_a" ) );
650 joinInfo.setJoinLayerId( vlB->id() );
651 joinInfo.setJoinFieldName( QStringLiteral( "id_b" ) );
652 joinInfo.setUsingMemoryCache( true );
653 joinInfo.setPrefix( QStringLiteral( "B_" ) );
654 vlA->addJoin( joinInfo );
655
656 QCOMPARE( vlA->fields().count(), 1 );
657 QCOMPARE( vlA->vectorJoins()[0].joinLayer(), ( QgsVectorLayer * ) nullptr );
658 QCOMPARE( vlA->vectorJoins()[0].joinLayerId(), vlB->id() );
659
660 QgsProject project;
661 project.addMapLayer( vlB );
662
663 vlA->resolveReferences( &project );
664
665 QCOMPARE( vlA->fields().count(), 2 );
666 QCOMPARE( vlA->vectorJoins()[0].joinLayer(), vlB );
667 QCOMPARE( vlA->vectorJoins()[0].joinLayerId(), vlB->id() );
668
669 delete vlA;
670 }
671
testSignals()672 void TestVectorLayerJoinBuffer::testSignals()
673 {
674 mProject.clear();
675 QgsVectorLayer *vlA = new QgsVectorLayer( QStringLiteral( "Point?field=id_a:integer" ), QStringLiteral( "cacheA" ), QStringLiteral( "memory" ) );
676 QVERIFY( vlA->isValid() );
677 QgsVectorLayer *vlB = new QgsVectorLayer( QStringLiteral( "Point?field=id_b:integer&field=value_b" ), QStringLiteral( "cacheB" ), QStringLiteral( "memory" ) );
678 QVERIFY( vlB->isValid() );
679 mProject.addMapLayer( vlA );
680 mProject.addMapLayer( vlB );
681
682 QgsFeature fA1( vlA->dataProvider()->fields(), 1 );
683 fA1.setAttribute( QStringLiteral( "id_a" ), 1 );
684 QgsFeature fA2( vlA->dataProvider()->fields(), 2 );
685 fA2.setAttribute( QStringLiteral( "id_a" ), 2 );
686
687 vlA->dataProvider()->addFeatures( QgsFeatureList() << fA1 << fA2 );
688
689 QgsVectorLayerJoinInfo joinInfo;
690 joinInfo.setTargetFieldName( QStringLiteral( "id_a" ) );
691 joinInfo.setJoinLayer( vlB );
692 joinInfo.setJoinFieldName( QStringLiteral( "id_b" ) );
693 joinInfo.setPrefix( QStringLiteral( "B_" ) );
694 joinInfo.setEditable( true );
695 joinInfo.setUpsertOnEdit( true );
696 vlA->addJoin( joinInfo );
697
698 QgsFeatureIterator fi = vlA->getFeatures();
699 fi.nextFeature( fA1 );
700 QCOMPARE( fA1.attribute( "id_a" ).toInt(), 1 );
701 QVERIFY( !fA1.attribute( "B_value_b" ).isValid() );
702 fi.nextFeature( fA2 );
703 QCOMPARE( fA2.attribute( "id_a" ).toInt(), 2 );
704 QVERIFY( !fA2.attribute( "B_value_b" ).isValid() );
705
706 // change value in join target layer, check for signals
707 QSignalSpy spy( vlA, &QgsVectorLayer::attributeValueChanged );
708 vlA->startEditing();
709 vlB->startEditing();
710 // adds new feature to second layer
711 QVERIFY( vlA->changeAttributeValue( 1, 1, 111 ) );
712 fi = vlA->getFeatures();
713 fi.nextFeature( fA1 );
714 QCOMPARE( fA1.attribute( "id_a" ).toInt(), 1 );
715 QCOMPARE( fA1.attribute( "B_value_b" ).toInt(), 111 );
716 fi.nextFeature( fA2 );
717 QCOMPARE( fA2.attribute( "id_a" ).toInt(), 2 );
718 QVERIFY( !fA2.attribute( "B_value_b" ).isValid() );
719 QCOMPARE( spy.count(), 1 );
720 QVERIFY( vlA->changeAttributeValue( 2, 1, 222 ) );
721 fi = vlA->getFeatures();
722 fi.nextFeature( fA1 );
723 QCOMPARE( fA1.attribute( "id_a" ).toInt(), 1 );
724 QCOMPARE( fA1.attribute( "B_value_b" ).toInt(), 111 );
725 fi.nextFeature( fA2 );
726 QCOMPARE( fA2.attribute( "id_a" ).toInt(), 2 );
727 QCOMPARE( fA2.attribute( "B_value_b" ).toInt(), 222 );
728 QCOMPARE( spy.count(), 2 );
729 // changes existing feature in second layer
730 QVERIFY( vlA->changeAttributeValue( 1, 1, 112 ) );
731 fi = vlA->getFeatures();
732 fi.nextFeature( fA1 );
733 QCOMPARE( fA1.attribute( "id_a" ).toInt(), 1 );
734 QCOMPARE( fA1.attribute( "B_value_b" ).toInt(), 112 );
735 fi.nextFeature( fA2 );
736 QCOMPARE( fA2.attribute( "id_a" ).toInt(), 2 );
737 QCOMPARE( fA2.attribute( "B_value_b" ).toInt(), 222 );
738 QCOMPARE( spy.count(), 3 );
739 }
740
testChangeAttributeValues()741 void TestVectorLayerJoinBuffer::testChangeAttributeValues()
742 {
743 // change attribute values in a vector layer which includes joins
744 mProject.clear();
745 QgsVectorLayer *vlA = new QgsVectorLayer( QStringLiteral( "Point?field=id_a:integer&field=value_a1:string&field=value_a2:string" ), QStringLiteral( "cacheA" ), QStringLiteral( "memory" ) );
746 QVERIFY( vlA->isValid() );
747 QgsVectorLayer *vlB = new QgsVectorLayer( QStringLiteral( "Point?field=id_b:integer&field=value_b1:string&field=value_b2:string" ), QStringLiteral( "cacheB" ), QStringLiteral( "memory" ) );
748 QVERIFY( vlB->isValid() );
749 mProject.addMapLayer( vlA );
750 mProject.addMapLayer( vlB );
751
752 QgsFeature fA1( vlA->dataProvider()->fields(), 1 );
753 fA1.setAttribute( QStringLiteral( "id_a" ), 1 );
754 fA1.setAttribute( QStringLiteral( "value_a1" ), QStringLiteral( "a_1_1" ) );
755 fA1.setAttribute( QStringLiteral( "value_a2" ), QStringLiteral( "a_1_2" ) );
756 QgsFeature fA2( vlA->dataProvider()->fields(), 2 );
757 fA2.setAttribute( QStringLiteral( "id_a" ), 2 );
758 fA2.setAttribute( QStringLiteral( "value_a1" ), QStringLiteral( "a_2_1" ) );
759 fA2.setAttribute( QStringLiteral( "value_a2" ), QStringLiteral( "a_2_2" ) );
760
761 QVERIFY( vlA->dataProvider()->addFeatures( QgsFeatureList() << fA1 << fA2 ) );
762
763 QCOMPARE( vlA->getFeature( 1 ).attributes().size(), 3 );
764 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 0 ).toInt(), 1 );
765 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 1 ).toString(), QStringLiteral( "a_1_1" ) );
766 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 2 ).toString(), QStringLiteral( "a_1_2" ) );
767
768 QgsVectorLayerJoinInfo joinInfo;
769 joinInfo.setTargetFieldName( QStringLiteral( "id_a" ) );
770 joinInfo.setJoinLayer( vlB );
771 joinInfo.setJoinFieldName( QStringLiteral( "id_b" ) );
772 joinInfo.setPrefix( QStringLiteral( "B_" ) );
773 joinInfo.setEditable( true );
774 joinInfo.setUpsertOnEdit( true );
775 vlA->addJoin( joinInfo );
776
777 QVERIFY( vlA->startEditing() );
778 QVERIFY( vlB->startEditing() );
779
780 QCOMPARE( vlA->getFeature( 1 ).attributes().size(), 5 );
781 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 0 ).toInt(), 1 );
782 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 1 ).toString(), QStringLiteral( "a_1_1" ) );
783 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 2 ).toString(), QStringLiteral( "a_1_2" ) );
784 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 3 ).toString(), QString() );
785 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 4 ).toString(), QString() );
786
787 // change a provider field
788 QVERIFY( vlA->changeAttributeValue( 1, 1, QStringLiteral( "new_a_1_1" ) ) );
789 // change a join field
790 QVERIFY( vlA->changeAttributeValue( 1, 3, QStringLiteral( "new_b_1_1" ) ) );
791
792 QCOMPARE( vlA->getFeature( 1 ).attributes().size(), 5 );
793 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 0 ).toInt(), 1 );
794 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 1 ).toString(), QStringLiteral( "new_a_1_1" ) );
795 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 2 ).toString(), QStringLiteral( "a_1_2" ) );
796 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 3 ).toString(), QStringLiteral( "new_b_1_1" ) );
797 QCOMPARE( vlA->getFeature( 1 ).attributes().at( 4 ).toString(), QString() );
798
799 QgsFeature joinFeature;
800 vlB->getFeatures().nextFeature( joinFeature );
801 QVERIFY( joinFeature.isValid() );
802 QCOMPARE( joinFeature.attributes().size(), 3 );
803 QCOMPARE( joinFeature.attributes().at( 0 ).toInt(), 1 );
804 QCOMPARE( joinFeature.attributes().at( 1 ).toString(), QStringLiteral( "new_b_1_1" ) );
805 QCOMPARE( joinFeature.attributes().at( 2 ).toString(), QString() );
806
807 // change a combination of provider and joined fields at once
808 QVERIFY( vlA->changeAttributeValues( 2, QgsAttributeMap{ { 1, QStringLiteral( "new_a_2_1" ) },
809 { 2, QStringLiteral( "new_a_2_2" ) },
810 { 3, QStringLiteral( "new_b_2_1" ) },
811 { 4, QStringLiteral( "new_b_2_2" ) }} ) );
812
813 QCOMPARE( vlA->getFeature( 2 ).attributes().size(), 5 );
814 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 0 ).toInt(), 2 );
815 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 1 ).toString(), QStringLiteral( "new_a_2_1" ) );
816 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 2 ).toString(), QStringLiteral( "new_a_2_2" ) );
817 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 3 ).toString(), QStringLiteral( "new_b_2_1" ) );
818 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 4 ).toString(), QStringLiteral( "new_b_2_2" ) );
819
820 // change only provider fields
821 QVERIFY( vlA->changeAttributeValues( 2, QgsAttributeMap{ { 1, QStringLiteral( "new_a_2_1b" ) },
822 { 2, QStringLiteral( "new_a_2_2b" ) }} ) );
823
824 QCOMPARE( vlA->getFeature( 2 ).attributes().size(), 5 );
825 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 0 ).toInt(), 2 );
826 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 1 ).toString(), QStringLiteral( "new_a_2_1b" ) );
827 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 2 ).toString(), QStringLiteral( "new_a_2_2b" ) );
828 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 3 ).toString(), QStringLiteral( "new_b_2_1" ) );
829 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 4 ).toString(), QStringLiteral( "new_b_2_2" ) );
830
831 // change only joined fields
832 QVERIFY( vlA->changeAttributeValues( 2, QgsAttributeMap{ { 3, QStringLiteral( "new_b_2_1b" ) },
833 { 4, QStringLiteral( "new_b_2_2b" ) }} ) );
834
835 QCOMPARE( vlA->getFeature( 2 ).attributes().size(), 5 );
836 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 0 ).toInt(), 2 );
837 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 1 ).toString(), QStringLiteral( "new_a_2_1b" ) );
838 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 2 ).toString(), QStringLiteral( "new_a_2_2b" ) );
839 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 3 ).toString(), QStringLiteral( "new_b_2_1b" ) );
840 QCOMPARE( vlA->getFeature( 2 ).attributes().at( 4 ).toString(), QStringLiteral( "new_b_2_2b" ) );
841
842 }
843
testCollidingNameColumnCached()844 void TestVectorLayerJoinBuffer::testCollidingNameColumnCached()
845 {
846 mProject.clear();
847 QgsVectorLayer *vlA = new QgsVectorLayer( QStringLiteral( "Point?field=id_a:integer&field=name" ), QStringLiteral( "cacheA" ), QStringLiteral( "memory" ) );
848 QVERIFY( vlA->isValid() );
849 QgsVectorLayer *vlB = new QgsVectorLayer( QStringLiteral( "Point?field=id_b:integer&field=name&field=value_b&field=value_c" ), QStringLiteral( "cacheB" ), QStringLiteral( "memory" ) );
850 QVERIFY( vlB->isValid() );
851 mProject.addMapLayer( vlA );
852 mProject.addMapLayer( vlB );
853
854 QgsFeature fA1( vlA->dataProvider()->fields(), 1 );
855 fA1.setAttribute( QStringLiteral( "id_a" ), 1 );
856 fA1.setAttribute( QStringLiteral( "name" ), QStringLiteral( "name_a" ) );
857
858 vlA->dataProvider()->addFeatures( QgsFeatureList() << fA1 );
859
860 QgsFeature fB1( vlB->dataProvider()->fields(), 1 );
861 fB1.setAttribute( QStringLiteral( "id_b" ), 1 );
862 fB1.setAttribute( QStringLiteral( "name" ), QStringLiteral( "name_b" ) );
863 fB1.setAttribute( QStringLiteral( "value_b" ), QStringLiteral( "value_b" ) );
864 fB1.setAttribute( QStringLiteral( "value_c" ), QStringLiteral( "value_c" ) );
865
866 vlB->dataProvider()->addFeatures( QgsFeatureList() << fB1 );
867
868 QgsVectorLayerJoinInfo joinInfo;
869 joinInfo.setTargetFieldName( QStringLiteral( "id_a" ) );
870 joinInfo.setJoinLayer( vlB );
871 joinInfo.setJoinFieldName( QStringLiteral( "id_b" ) );
872 joinInfo.setPrefix( QStringLiteral( "" ) );
873 joinInfo.setEditable( true );
874 joinInfo.setUpsertOnEdit( false );
875 joinInfo.setUsingMemoryCache( true );
876 vlA->addJoin( joinInfo );
877
878 QgsFeatureIterator fi1 = vlA->getFeatures();
879 fi1.nextFeature( fA1 );
880 QCOMPARE( fA1.fields().names(), QStringList( {"id_a", "name", "value_b", "value_c"} ) );
881 QCOMPARE( fA1.attribute( "id_a" ).toInt(), 1 );
882 QCOMPARE( fA1.attribute( "name" ).toString(), QStringLiteral( "name_a" ) );
883 QCOMPARE( fA1.attribute( "value_b" ).toString(), QStringLiteral( "value_b" ) );
884 QCOMPARE( fA1.attribute( "value_c" ).toString(), QStringLiteral( "value_c" ) );
885 }
886
887 QGSTEST_MAIN( TestVectorLayerJoinBuffer )
888 #include "testqgsvectorlayerjoinbuffer.moc"
889
890
891