File indexing completed on 2024-05-12 05:26:08
0001 /* 0002 Copyright (c) 2015 Christian Mollekopf <mollekopf@kolabsys.com> 0003 0004 This library is free software; you can redistribute it and/or modify it 0005 under the terms of the GNU Library General Public License as published by 0006 the Free Software Foundation; either version 2 of the License, or (at your 0007 option) any later version. 0008 0009 This library is distributed in the hope that it will be useful, but WITHOUT 0010 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 0011 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public 0012 License for more details. 0013 0014 You should have received a copy of the GNU Library General Public License 0015 along with this library; see the file COPYING.LIB. If not, write to the 0016 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 0017 02110-1301, USA. 0018 */ 0019 #include "typeindex.h" 0020 0021 #include "log.h" 0022 #include "index.h" 0023 #include "fulltextindex.h" 0024 0025 #include <QDateTime> 0026 #include <QDataStream> 0027 0028 using namespace Sink; 0029 0030 using Storage::Identifier; 0031 0032 static QByteArray getByteArray(const QVariant &value) 0033 { 0034 if (value.type() == QVariant::DateTime) { 0035 QByteArray result; 0036 QDataStream ds(&result, QIODevice::WriteOnly); 0037 ds << value.toDateTime(); 0038 return result; 0039 } 0040 if (value.type() == QVariant::Bool) { 0041 return value.toBool() ? "t" : "f"; 0042 } 0043 if (value.canConvert<Sink::ApplicationDomain::Reference>()) { 0044 const auto ba = value.value<Sink::ApplicationDomain::Reference>().value; 0045 if (!ba.isEmpty()) { 0046 return ba; 0047 } 0048 } 0049 if (value.isValid()) { 0050 const auto ba = value.toByteArray(); 0051 if (!ba.isEmpty()) { 0052 return ba; 0053 } 0054 } 0055 // LMDB can't handle empty keys, so use something different 0056 return "toplevel"; 0057 } 0058 0059 static QByteArray toSortableByteArrayImpl(const QDateTime &date) 0060 { 0061 // Sort invalid last 0062 if (!date.isValid()) { 0063 return QByteArray::number(std::numeric_limits<unsigned int>::max()); 0064 } 0065 return padNumber(std::numeric_limits<unsigned int>::max() - date.toTime_t()); 0066 } 0067 0068 static QByteArray toSortableByteArray(const QVariant &value) 0069 { 0070 if (!value.isValid()) { 0071 // FIXME: we don't know the type, so we don't know what to return 0072 // This mean we're fixing every sorted index keys to use unsigned int 0073 return QByteArray::number(std::numeric_limits<unsigned int>::max()); 0074 } 0075 0076 if (value.canConvert<QDateTime>()) { 0077 return toSortableByteArrayImpl(value.toDateTime()); 0078 } 0079 SinkWarning() << "Not knowing how to convert a" << value.typeName() 0080 << "to a sortable key, falling back to default conversion"; 0081 return getByteArray(value); 0082 } 0083 0084 TypeIndex::TypeIndex(const QByteArray &type, const Sink::Log::Context &ctx) : mLogCtx(ctx), mType(type) 0085 { 0086 } 0087 0088 QByteArray TypeIndex::indexName(const QByteArray &property, const QByteArray &sortProperty) const 0089 { 0090 if (sortProperty.isEmpty()) { 0091 return mType + ".index." + property; 0092 } 0093 return mType + ".index." + property + ".sort." + sortProperty; 0094 } 0095 0096 QByteArray TypeIndex::sortedIndexName(const QByteArray &property) const 0097 { 0098 return mType + ".index." + property + ".sorted"; 0099 } 0100 0101 QByteArray TypeIndex::sampledPeriodIndexName(const QByteArray &rangeBeginProperty, const QByteArray &rangeEndProperty) const 0102 { 0103 return mType + ".index." + rangeBeginProperty + ".range." + rangeEndProperty; 0104 } 0105 0106 static unsigned int bucketOf(const QVariant &value) 0107 { 0108 if (value.canConvert<QDateTime>()) { 0109 return value.value<QDateTime>().date().toJulianDay() / 7; 0110 } 0111 SinkError() << "Not knowing how to get the bucket of a" << value.typeName(); 0112 return {}; 0113 } 0114 0115 static void update(TypeIndex::Action action, const QByteArray &indexName, const QByteArray &key, const QByteArray &value, Sink::Storage::DataStore::Transaction &transaction) 0116 { 0117 Index index(indexName, transaction); 0118 switch (action) { 0119 case TypeIndex::Add: 0120 index.add(key, value); 0121 break; 0122 case TypeIndex::Remove: 0123 index.remove(key, value); 0124 break; 0125 } 0126 } 0127 0128 void TypeIndex::addProperty(const QByteArray &property) 0129 { 0130 auto indexer = [=](Action action, const Identifier &identifier, const QVariant &value, Sink::Storage::DataStore::Transaction &transaction) { 0131 update(action, indexName(property), getByteArray(value), identifier.toInternalByteArray(), transaction); 0132 }; 0133 mIndexer.insert(property, indexer); 0134 mProperties << property; 0135 } 0136 0137 template <> 0138 void TypeIndex::addSortedProperty<QDateTime>(const QByteArray &property) 0139 { 0140 auto indexer = [=](Action action, const Identifier &identifier, const QVariant &value, 0141 Sink::Storage::DataStore::Transaction &transaction) { 0142 update(action, sortedIndexName(property), toSortableByteArray(value), identifier.toInternalByteArray(), transaction); 0143 }; 0144 mSortIndexer.insert(property, indexer); 0145 mSortedProperties << property; 0146 } 0147 0148 template <> 0149 void TypeIndex::addPropertyWithSorting<QByteArray, QDateTime>(const QByteArray &property, const QByteArray &sortProperty) 0150 { 0151 auto indexer = [=](Action action, const Identifier &identifier, const QVariant &value, const QVariant &sortValue, Sink::Storage::DataStore::Transaction &transaction) { 0152 const auto date = sortValue.toDateTime(); 0153 const auto propertyValue = getByteArray(value); 0154 update(action, indexName(property, sortProperty), propertyValue + toSortableByteArray(date), identifier.toInternalByteArray(), transaction); 0155 }; 0156 mGroupedSortIndexer.insert(property + sortProperty, indexer); 0157 mGroupedSortedProperties.insert(property, sortProperty); 0158 } 0159 0160 template <> 0161 void TypeIndex::addPropertyWithSorting<ApplicationDomain::Reference, QDateTime>(const QByteArray &property, const QByteArray &sortProperty) 0162 { 0163 addPropertyWithSorting<QByteArray, QDateTime>(property, sortProperty); 0164 } 0165 0166 template <> 0167 void TypeIndex::addSampledPeriodIndex<QDateTime, QDateTime>( 0168 const QByteArray &beginProperty, const QByteArray &endProperty) 0169 { 0170 auto indexer = [=](Action action, const Identifier &identifier, const QVariant &begin, 0171 const QVariant &end, Sink::Storage::DataStore::Transaction &transaction) { 0172 const auto beginDate = begin.toDateTime(); 0173 const auto endDate = end.toDateTime(); 0174 0175 auto beginBucket = bucketOf(beginDate); 0176 auto endBucket = bucketOf(endDate); 0177 0178 if (beginBucket > endBucket) { 0179 SinkError() << "End bucket greater than begin bucket"; 0180 return; 0181 } 0182 0183 Index index(sampledPeriodIndexName(beginProperty, endProperty), transaction); 0184 for (auto bucket = beginBucket; bucket <= endBucket; ++bucket) { 0185 QByteArray bucketKey = padNumber(bucket); 0186 switch (action) { 0187 case TypeIndex::Add: 0188 index.add(bucketKey, identifier.toInternalByteArray()); 0189 break; 0190 case TypeIndex::Remove: 0191 index.remove(bucketKey, identifier.toInternalByteArray(), true); 0192 break; 0193 } 0194 } 0195 }; 0196 0197 mSampledPeriodProperties.insert({ beginProperty, endProperty }); 0198 mSampledPeriodIndexer.insert({ beginProperty, endProperty }, indexer); 0199 } 0200 0201 void TypeIndex::updateIndex(Action action, const Identifier &identifier, const Sink::ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId) 0202 { 0203 for (const auto &property : mProperties) { 0204 const auto value = entity.getProperty(property); 0205 auto indexer = mIndexer.value(property); 0206 indexer(action, identifier, value, transaction); 0207 } 0208 for (const auto &properties : mSampledPeriodProperties) { 0209 auto indexer = mSampledPeriodIndexer.value(properties); 0210 auto indexRanges = entity.getProperty("indexRanges"); 0211 if (indexRanges.isValid()) { 0212 //This is to override the indexed ranges from the evenpreprocessor 0213 const auto list = indexRanges.value<QList<QPair<QDateTime, QDateTime>>>(); 0214 for (const auto &period : list) { 0215 indexer(action, identifier, period.first, period.second, transaction); 0216 } 0217 } else { 0218 //This is the regular case 0219 //NOTE Since we don't generate the ranges for removal we just end up trying to remove all possible buckets here instead. 0220 const auto beginValue = entity.getProperty(properties.first); 0221 const auto endValue = entity.getProperty(properties.second); 0222 indexer(action, identifier, beginValue, endValue, transaction); 0223 } 0224 } 0225 for (const auto &property : mSortedProperties) { 0226 const auto value = entity.getProperty(property); 0227 auto indexer = mSortIndexer.value(property); 0228 indexer(action, identifier, value, transaction); 0229 } 0230 for (auto it = mGroupedSortedProperties.constBegin(); it != mGroupedSortedProperties.constEnd(); it++) { 0231 const auto value = entity.getProperty(it.key()); 0232 const auto sortValue = entity.getProperty(it.value()); 0233 auto indexer = mGroupedSortIndexer.value(it.key() + it.value()); 0234 indexer(action, identifier, value, sortValue, transaction); 0235 } 0236 0237 } 0238 0239 void TypeIndex::commitTransaction() 0240 { 0241 for (const auto &indexer : mCustomIndexer) { 0242 indexer->commitTransaction(); 0243 } 0244 } 0245 0246 void TypeIndex::abortTransaction() 0247 { 0248 for (const auto &indexer : mCustomIndexer) { 0249 indexer->abortTransaction(); 0250 } 0251 } 0252 0253 void TypeIndex::add(const Identifier &identifier, const Sink::ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId) 0254 { 0255 updateIndex(Add, identifier, entity, transaction, resourceInstanceId); 0256 for (const auto &indexer : mCustomIndexer) { 0257 indexer->setup(this, &transaction, resourceInstanceId); 0258 indexer->add(entity); 0259 } 0260 } 0261 0262 void TypeIndex::modify(const Identifier &identifier, const Sink::ApplicationDomain::ApplicationDomainType &oldEntity, const Sink::ApplicationDomain::ApplicationDomainType &newEntity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId) 0263 { 0264 updateIndex(Remove, identifier, oldEntity, transaction, resourceInstanceId); 0265 updateIndex(Add, identifier, newEntity, transaction, resourceInstanceId); 0266 for (const auto &indexer : mCustomIndexer) { 0267 indexer->setup(this, &transaction, resourceInstanceId); 0268 indexer->modify(oldEntity, newEntity); 0269 } 0270 } 0271 0272 void TypeIndex::remove(const Identifier &identifier, const Sink::ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId) 0273 { 0274 updateIndex(Remove, identifier, entity, transaction, resourceInstanceId); 0275 for (const auto &indexer : mCustomIndexer) { 0276 indexer->setup(this, &transaction, resourceInstanceId); 0277 indexer->remove(entity); 0278 } 0279 } 0280 0281 static QVector<Identifier> indexLookup(Index &index, QueryBase::Comparator filter, 0282 std::function<QByteArray(const QVariant &)> valueToKey = getByteArray) 0283 { 0284 QVector<Identifier> keys; 0285 QByteArrayList lookupKeys; 0286 if (filter.comparator == Query::Comparator::Equals) { 0287 lookupKeys << valueToKey(filter.value); 0288 } else if (filter.comparator == Query::Comparator::In) { 0289 for (const QVariant &value : filter.value.value<QVariantList>()) { 0290 lookupKeys << valueToKey(value); 0291 } 0292 } else { 0293 Q_ASSERT(false); 0294 } 0295 0296 for (const auto &lookupKey : lookupKeys) { 0297 index.lookup(lookupKey, 0298 [&](const QByteArray &value) { 0299 keys << Identifier::fromInternalByteArray(value); 0300 return true; 0301 }, 0302 [lookupKey](const Index::Error &error) { 0303 SinkWarning() << "Lookup error in index: " << error.message << lookupKey; 0304 }, 0305 true); 0306 } 0307 return keys; 0308 } 0309 0310 static QVector<Identifier> sortedIndexLookup(Index &index, QueryBase::Comparator filter) 0311 { 0312 if (filter.comparator == Query::Comparator::In || filter.comparator == Query::Comparator::Contains) { 0313 SinkWarning() << "In and Contains comparison not supported on sorted indexes"; 0314 } 0315 0316 if (filter.comparator != Query::Comparator::Within) { 0317 return indexLookup(index, filter, toSortableByteArray); 0318 } 0319 0320 QByteArray lowerBound, upperBound; 0321 const auto bounds = filter.value.value<QVariantList>(); 0322 if (bounds[0].canConvert<QDateTime>()) { 0323 // Inverse the bounds because dates are stored newest first 0324 upperBound = toSortableByteArray(bounds[0].toDateTime()); 0325 lowerBound = toSortableByteArray(bounds[1].toDateTime()); 0326 } else { 0327 lowerBound = bounds[0].toByteArray(); 0328 upperBound = bounds[1].toByteArray(); 0329 } 0330 0331 QVector<Identifier> keys; 0332 index.rangeLookup(lowerBound, upperBound, 0333 [&](const QByteArray &value) { 0334 const auto id = Identifier::fromInternalByteArray(value); 0335 //Deduplicate because an id could be in multiple buckets 0336 if (!keys.contains(id)) { 0337 keys << id; 0338 } 0339 }, 0340 [&](const Index::Error &error) { 0341 SinkWarning() << "Lookup error in index:" << error.message 0342 << "with bounds:" << bounds[0] << bounds[1]; 0343 }); 0344 0345 return keys; 0346 } 0347 0348 static QVector<Identifier> sortedIndexLookup(Index &index, int limit) 0349 { 0350 QVector<Identifier> keys; 0351 int count = 0; 0352 index.lookup("", 0353 [&](const QByteArray &value) -> bool { 0354 keys << Identifier::fromInternalByteArray(value); 0355 count++; 0356 if (limit && count > limit) { 0357 return false; 0358 } 0359 return true; 0360 }, 0361 [](const Index::Error &error) { 0362 SinkWarning() << "Lookup error in index: " << error.message; 0363 }, 0364 true); 0365 return keys; 0366 } 0367 0368 static QVector<Identifier> sampledIndexLookup(Index &index, QueryBase::Comparator filter) 0369 { 0370 if (filter.comparator != Query::Comparator::Overlap) { 0371 SinkWarning() << "Comparisons other than Overlap not supported on sampled period indexes"; 0372 return {}; 0373 } 0374 0375 0376 const auto bounds = filter.value.value<QVariantList>(); 0377 0378 const auto lowerBucket = padNumber(bucketOf(bounds[0])); 0379 const auto upperBucket = padNumber(bucketOf(bounds[1])); 0380 0381 SinkTrace() << "Looking up from bucket:" << lowerBucket << "to:" << upperBucket; 0382 0383 QVector<Identifier> keys; 0384 index.rangeLookup(lowerBucket, upperBucket, 0385 [&](const QByteArray &value) { 0386 const auto id = Identifier::fromInternalByteArray(value); 0387 //Deduplicate because an id could be in multiple buckets 0388 if (!keys.contains(id)) { 0389 keys << id; 0390 } 0391 }, 0392 [&](const Index::Error &error) { 0393 SinkWarning() << "Lookup error in index:" << error.message 0394 << "with bounds:" << bounds[0] << bounds[1]; 0395 }); 0396 0397 return keys; 0398 } 0399 0400 QVector<Identifier> TypeIndex::query(const Sink::QueryBase &query, QSet<QByteArrayList> &appliedFilters, QByteArray &appliedSorting, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId) 0401 { 0402 const auto baseFilters = query.getBaseFilters(); 0403 for (auto it = baseFilters.constBegin(); it != baseFilters.constEnd(); it++) { 0404 if (it.value().comparator == QueryBase::Comparator::Fulltext) { 0405 appliedFilters << it.key(); 0406 appliedSorting = "date"; 0407 if (FulltextIndex::exists(resourceInstanceId)) { 0408 FulltextIndex fulltextIndex{resourceInstanceId}; 0409 const auto ids = fulltextIndex.lookup(it.value().value.toString()); 0410 SinkTraceCtx(mLogCtx) << "Fulltext index lookup found " << ids.size() << " keys."; 0411 return ids; 0412 } 0413 SinkTraceCtx(mLogCtx) << "Fulltext index doesn't exist."; 0414 return {}; 0415 } 0416 } 0417 0418 for (auto it = baseFilters.constBegin(); it != baseFilters.constEnd(); it++) { 0419 if (it.value().comparator == QueryBase::Comparator::Overlap) { 0420 if (mSampledPeriodProperties.contains({it.key()[0], it.key()[1]})) { 0421 Index index(sampledPeriodIndexName(it.key()[0], it.key()[1]), transaction); 0422 const auto keys = sampledIndexLookup(index, query.getFilter(it.key())); 0423 appliedFilters << it.key(); 0424 SinkTraceCtx(mLogCtx) << "Sampled period index lookup on" << it.key() << "found" << keys.size() << "keys."; 0425 return keys; 0426 } else { 0427 SinkWarning() << "Overlap search without sampled period index"; 0428 } 0429 } 0430 } 0431 0432 for (auto it = mGroupedSortedProperties.constBegin(); it != mGroupedSortedProperties.constEnd(); it++) { 0433 if (query.hasFilter(it.key()) && query.sortProperty() == it.value()) { 0434 Index index(indexName(it.key(), it.value()), transaction); 0435 const auto keys = indexLookup(index, query.getFilter(it.key())); 0436 appliedFilters.insert({it.key()}); 0437 appliedSorting = it.value(); 0438 SinkTraceCtx(mLogCtx) << "Grouped sorted index lookup on " << it.key() << it.value() << " found " << keys.size() << " keys."; 0439 return keys; 0440 } 0441 } 0442 0443 for (const auto &property : mSortedProperties) { 0444 if (query.hasFilter(property)) { 0445 Index index(sortedIndexName(property), transaction); 0446 const auto keys = sortedIndexLookup(index, query.getFilter(property)); 0447 appliedFilters.insert({property}); 0448 SinkTraceCtx(mLogCtx) << "Sorted index lookup on " << property << " found " << keys.size() << " keys."; 0449 return keys; 0450 } else if (query.sortProperty() == property) { 0451 Index index(sortedIndexName(property), transaction); 0452 //FIXME Setting a limit here breaks our fetchMore logic, 0453 //because our initial query will just return 0454 //as many results as queried for. We now just query for 10 times 0455 //the amount, so fetchMore works for a while, and we can avoid loading 0456 //all index results. The primary usecase for this is loading all emails sorted 0457 //by date (That's a lot of results). 0458 const auto keys = sortedIndexLookup(index, query.limit() * 10); 0459 appliedSorting = property; 0460 return keys; 0461 } 0462 } 0463 0464 for (const auto &property : mProperties) { 0465 if (query.hasFilter(property)) { 0466 Index index(indexName(property), transaction); 0467 const auto keys = indexLookup(index, query.getFilter(property)); 0468 appliedFilters.insert({property}); 0469 SinkTraceCtx(mLogCtx) << "Index lookup on " << property << " found " << keys.size() << " keys."; 0470 return keys; 0471 } 0472 } 0473 SinkTraceCtx(mLogCtx) << "No matching index"; 0474 return {}; 0475 } 0476 0477 QVector<Identifier> TypeIndex::lookup(const QByteArray &property, const QVariant &value, 0478 Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId, const QVector<Sink::Storage::Identifier> &filter) 0479 { 0480 SinkTraceCtx(mLogCtx) << "Index lookup on property: " << property << mSecondaryProperties.keys() << mProperties; 0481 if (property == "fulltext") { 0482 if (FulltextIndex::exists(resourceInstanceId)) { 0483 FulltextIndex fulltextIndex{resourceInstanceId}; 0484 const Sink::Storage::Identifier entityId = filter.isEmpty() ? Sink::Storage::Identifier{} : filter.first(); 0485 const auto ids = fulltextIndex.lookup(value.toString(), entityId); 0486 SinkTraceCtx(mLogCtx) << "Fulltext index lookup found " << ids.size() << " keys."; 0487 return ids; 0488 } 0489 SinkTraceCtx(mLogCtx) << "Fulltext index doesn't exist."; 0490 return {}; 0491 } 0492 if (mProperties.contains(property)) { 0493 QVector<Identifier> keys; 0494 Index index(indexName(property), transaction); 0495 const auto lookupKey = getByteArray(value); 0496 index.lookup(lookupKey, 0497 [&](const QByteArray &value) { 0498 keys << Identifier::fromInternalByteArray(value); 0499 return true; 0500 }, 0501 [property](const Index::Error &error) { 0502 SinkWarning() << "Error in index: " << error.message << property; 0503 }); 0504 SinkTraceCtx(mLogCtx) << "Index lookup on " << property << " found " << keys.size() << " keys."; 0505 return keys; 0506 } else if (mSecondaryProperties.contains(property)) { 0507 // Lookups on secondary indexes first lookup the key, and then lookup the results again to 0508 // resolve to entity id's 0509 QVector<Identifier> keys; 0510 auto resultProperty = mSecondaryProperties.value(property); 0511 0512 QVector<QByteArray> secondaryKeys; 0513 Index index(indexName(property + resultProperty), transaction); 0514 const auto lookupKey = getByteArray(value); 0515 index.lookup(lookupKey, [&](const QByteArray &value) { secondaryKeys << value; return true; }, 0516 [property](const Index::Error &error) { 0517 SinkWarning() << "Error in index: " << error.message << property; 0518 }); 0519 SinkTraceCtx(mLogCtx) << "Looked up secondary keys for the following lookup key: " << lookupKey 0520 << " => " << secondaryKeys; 0521 for (const auto &secondary : secondaryKeys) { 0522 keys += lookup(resultProperty, secondary, transaction); 0523 } 0524 return keys; 0525 } else { 0526 SinkWarning() << "Tried to lookup " << property << " but couldn't find value"; 0527 } 0528 return {}; 0529 } 0530 0531 template <> 0532 void TypeIndex::index<QByteArray, QByteArray>(const QByteArray &leftName, const QByteArray &rightName, const QVariant &leftValue, const QVariant &rightValue, Sink::Storage::DataStore::Transaction &transaction) 0533 { 0534 Index(indexName(leftName + rightName), transaction).add(getByteArray(leftValue), getByteArray(rightValue)); 0535 } 0536 0537 template <> 0538 void TypeIndex::index<QString, QByteArray>(const QByteArray &leftName, const QByteArray &rightName, const QVariant &leftValue, const QVariant &rightValue, Sink::Storage::DataStore::Transaction &transaction) 0539 { 0540 Index(indexName(leftName + rightName), transaction).add(getByteArray(leftValue), getByteArray(rightValue)); 0541 } 0542 0543 template <> 0544 void TypeIndex::unindex<QByteArray, QByteArray>(const QByteArray &leftName, const QByteArray &rightName, const QVariant &leftValue, const QVariant &rightValue, Sink::Storage::DataStore::Transaction &transaction) 0545 { 0546 Index(indexName(leftName + rightName), transaction).remove(getByteArray(leftValue), getByteArray(rightValue)); 0547 } 0548 0549 template <> 0550 void TypeIndex::unindex<QString, QByteArray>(const QByteArray &leftName, const QByteArray &rightName, const QVariant &leftValue, const QVariant &rightValue, Sink::Storage::DataStore::Transaction &transaction) 0551 { 0552 Index(indexName(leftName + rightName), transaction).remove(getByteArray(leftValue), getByteArray(rightValue)); 0553 } 0554 0555 template <> 0556 QVector<QByteArray> TypeIndex::secondaryLookup<QByteArray>(const QByteArray &leftName, const QByteArray &rightName, const QVariant &value) 0557 { 0558 QVector<QByteArray> keys; 0559 Index index(indexName(leftName + rightName), *mTransaction); 0560 const auto lookupKey = getByteArray(value); 0561 index.lookup( 0562 lookupKey, [&](const QByteArray &value) { keys << QByteArray{value.constData(), value.size()}; return true; }, [=](const Index::Error &error) { SinkWarning() << "Lookup error in secondary index: " << error.message << value << lookupKey; }); 0563 0564 return keys; 0565 } 0566 0567 template <> 0568 QVector<QByteArray> TypeIndex::secondaryLookup<QString>(const QByteArray &leftName, const QByteArray &rightName, const QVariant &value) 0569 { 0570 return secondaryLookup<QByteArray>(leftName, rightName, value); 0571 }