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 }