From 28dee405258c6bffb79038c7880f37102aa6347c Mon Sep 17 00:00:00 2001 From: Igor Malinovskiy Date: Thu, 3 Feb 2022 17:28:25 +0200 Subject: [PATCH] Add TairHash basic support --- src/app/models/key-models/abstractkey.h | 2 +- src/app/models/key-models/keyfactory.cpp | 4 + src/app/models/key-models/tairhashkey.cpp | 189 ++++++++++++++++++++++ src/app/models/key-models/tairhashkey.h | 36 +++++ src/qml/value-editor/ValueTable.qml | 38 ++++- src/qml/value-editor/ValueTableCell.qml | 20 ++- src/qml/value-editor/editors/editor.js | 2 +- 7 files changed, 279 insertions(+), 12 deletions(-) create mode 100644 src/app/models/key-models/tairhashkey.cpp create mode 100644 src/app/models/key-models/tairhashkey.h diff --git a/src/app/models/key-models/abstractkey.h b/src/app/models/key-models/abstractkey.h index 4725465aa..2dfdb6c6f 100644 --- a/src/app/models/key-models/abstractkey.h +++ b/src/app/models/key-models/abstractkey.h @@ -146,7 +146,7 @@ class KeyModel : public ValueEditor::Model { virtual void loadRows(QVariant rowStart, unsigned long count, LoadRowsCallback callback) override { - if (m_rowsLoadCmd.mid(1, 4).toLower() == "scan") { + if (m_rowsLoadCmd.right(4).toLower() == "scan") { QList cmdParts = {m_rowsLoadCmd, m_keyFullPath, QString::number(m_scanCursor).toLatin1(), "COUNT", QString::number(count).toLatin1()}; diff --git a/src/app/models/key-models/keyfactory.cpp b/src/app/models/key-models/keyfactory.cpp index 98fe2df00..cdf9f3146 100644 --- a/src/app/models/key-models/keyfactory.cpp +++ b/src/app/models/key-models/keyfactory.cpp @@ -13,6 +13,7 @@ #include "sortedsetkey.h" #include "stream.h" #include "stringkey.h" +#include "tairhashkey.h" KeyFactory::KeyFactory() {} @@ -158,6 +159,9 @@ QSharedPointer KeyFactory::createModel( } else if (type == "stream") { return QSharedPointer( new StreamKeyModel(connection, keyFullPath, dbIndex, ttl)); + } else if (type == "tairhash-") { + return QSharedPointer( + new TairHashKeyModel(connection, keyFullPath, dbIndex, ttl)); } return QSharedPointer(); diff --git a/src/app/models/key-models/tairhashkey.cpp b/src/app/models/key-models/tairhashkey.cpp new file mode 100644 index 000000000..dea39830d --- /dev/null +++ b/src/app/models/key-models/tairhashkey.cpp @@ -0,0 +1,189 @@ +#include "tairhashkey.h" +#include +#include + +TairHashKeyModel::TairHashKeyModel(QSharedPointer connection, + QByteArray fullPath, int dbIndex, long long ttl) + : KeyModel(connection, fullPath, dbIndex, ttl, "EXHLEN", "EXHSCAN") {} + +QString TairHashKeyModel::type() { return "tairhash"; } + +QStringList TairHashKeyModel::getColumnNames() { + return QStringList() << "rowNumber" + << "key" + << "value" + << "metadata"; +} + +QHash TairHashKeyModel::getRoles() { + QHash roles; + roles[Roles::RowNumber] = "rowNumber"; + roles[Roles::Key] = "key"; + roles[Roles::Value] = "value"; + roles[Roles::TTL] = "ttl"; + roles[Roles::Version] = "version"; + return roles; +} + +QVariant TairHashKeyModel::getData(int rowIndex, int dataRole) { + if (!isRowLoaded(rowIndex)) return QVariant(); + + auto row = m_rowsCache[rowIndex]; + + if (dataRole == Roles::Key) + return row.first; + else if (dataRole == Roles::Value) + return row.second.value; + else if (dataRole == Roles::TTL) + return row.second.ttl; + else if (dataRole == Roles::Version) + return row.second.version; + else if (dataRole == Roles::RowNumber) + return rowIndex; + + return QVariant(); +} + +void TairHashKeyModel::updateRow(int rowIndex, const QVariantMap &row, Callback c) { + if (!isRowLoaded(rowIndex) || !isRowValid(row)) { + c(QCoreApplication::translate("RESP", "Invalid row")); + return; + } + + auto cachedRow = m_rowsCache[rowIndex]; + + bool keyChanged = cachedRow.first != row["key"].toByteArray(); + bool valueChanged = cachedRow.second.value != row["value"].toByteArray(); + + TairHashValue newValue; + + if (valueChanged) { + newValue = {row["value"].toByteArray(), cachedRow.second.ttl, + cachedRow.second.version}; + } else { + newValue = cachedRow.second; + } + + QPair newRow( + (keyChanged) ? row["key"].toByteArray() : cachedRow.first, newValue + ); + + auto afterValueUpdate = [this, c, rowIndex, newRow](const QString &err) { + if (err.isEmpty()) { + m_rowsCache.replace(rowIndex, newRow); + loadMetadata(newRow.first, rowIndex); + } + + return c(err); + }; + + if (keyChanged) { + deleteHashRow(cachedRow.first, + [this, c, newRow, afterValueUpdate](const QString &err) { + if (err.size() > 0) return c(err); + + setHashRow(newRow.first, newRow.second.value, afterValueUpdate); + }); + } else { + setHashRow(newRow.first, newRow.second.value, afterValueUpdate); + } +} + +void TairHashKeyModel::addRow(const QVariantMap &row, Callback c) { + if (!isRowValid(row)) { + c(QCoreApplication::translate("RESP", "Invalid row")); + return; + } + + setHashRow( + row["key"].toByteArray(), row["value"].toByteArray(), + [this, c](const QString &err) { + if (err.isEmpty()) m_rowCount++; + return c(err); + }, + false); +} + +void TairHashKeyModel::removeRow(int i, Callback c) { + if (!isRowLoaded(i)) return; + + auto row = m_rowsCache[i]; + + deleteHashRow(row.first, [this, i, c](const QString &err) { + if (err.isEmpty()) { + m_rowCount--; + m_rowsCache.removeAt(i); + setRemovedIfEmpty(); + } + + return c(err); + }); +} + +void TairHashKeyModel::setHashRow(const QByteArray &hashKey, + const QByteArray &hashValue, Callback c, + bool updateIfNotExist) { + QList rawCmd{(updateIfNotExist) ? "EXHSET" : "EXHSETNX", + m_keyFullPath, hashKey, hashValue}; + + executeCmd(rawCmd, c, + [updateIfNotExist](RedisClient::Response r, Callback c) { + if (updateIfNotExist == false && r.value().toInt() == 0) { + return c(QCoreApplication::translate( + "RESP", "Value with the same key already exists")); + } else { + return c(QString()); + } + }); +} + +void TairHashKeyModel::deleteHashRow(const QByteArray &hashKey, Callback c) { + executeCmd({"EXHDEL", m_keyFullPath, hashKey}, c); +} + +void TairHashKeyModel::loadMetadata(const QByteArray &hashKey, int rowIndex) { + executeCmd({"EXHVER", m_keyFullPath, hashKey}, Callback{}, + [rowIndex, this](RedisClient::Response r, Callback) { + auto row = m_rowsCache.getRow(rowIndex); + row.second.version = r.value().toLongLong(); + m_rowsCache.replace(rowIndex, row); + }); + + executeCmd({"EXHTTL", m_keyFullPath, hashKey}, Callback{}, + [rowIndex, this](RedisClient::Response r, Callback) { + auto row = m_rowsCache.getRow(rowIndex); + row.second.ttl = r.value().toLongLong(); + m_rowsCache.replace(rowIndex, row); + }); +} + +int TairHashKeyModel::addLoadedRowsToCache(const QVariantList &rows, + QVariant rowStartId) { + QList> result; + + for (QVariantList::const_iterator item = rows.begin(); item != rows.end(); + ++item) { + QPair value; + QByteArray hashKey = item->toByteArray(); + value.first = hashKey; + ++item; + + int currentIndex = result.size(); + + if (item == rows.end()) { + emit m_notifier->error(QCoreApplication::translate( + "RESP", "Data was loaded from server partially.")); + return 0; + } + + value.second = TairHashValue{item->toByteArray(), -1, -1}; + result.push_back(value); + + loadMetadata(value.first, currentIndex); + } + + auto rowStart = rowStartId.toLongLong(); + m_rowsCache.addLoadedRange({rowStart, rowStart + result.size() - 1}, result); + + return result.size(); +} diff --git a/src/app/models/key-models/tairhashkey.h b/src/app/models/key-models/tairhashkey.h new file mode 100644 index 000000000..2f0367a6a --- /dev/null +++ b/src/app/models/key-models/tairhashkey.h @@ -0,0 +1,36 @@ +#pragma once +#include "abstractkey.h" + +struct TairHashValue { + QByteArray value; + qlonglong ttl; + qlonglong version; +}; + +class TairHashKeyModel : public KeyModel> { + public: + TairHashKeyModel(QSharedPointer connection, + QByteArray fullPath, int dbIndex, long long ttl); + + QString type() override; + QStringList getColumnNames() override; + QHash getRoles() override; + + QVariant getData(int rowIndex, int dataRole) override; + void addRow(const QVariantMap &, Callback) override; + virtual void updateRow(int rowIndex, const QVariantMap &, Callback) override; + void removeRow(int, Callback) override; + + protected: + int addLoadedRowsToCache(const QVariantList &list, + QVariant rowStart) override; + + private: + enum Roles { RowNumber = Qt::UserRole + 1, Key, Value, TTL, Version }; + + void setHashRow(const QByteArray &hashKey, const QByteArray &hashValue, + Callback c, bool updateIfNotExist = true); + void deleteHashRow(const QByteArray &hashKey, Callback c); + + void loadMetadata(const QByteArray& hashKey, int row); +}; diff --git a/src/qml/value-editor/ValueTable.qml b/src/qml/value-editor/ValueTable.qml index c234081a8..aa9285ea1 100644 --- a/src/qml/value-editor/ValueTable.qml +++ b/src/qml/value-editor/ValueTable.qml @@ -198,8 +198,9 @@ Item { property int totalPages: keyTab.keyModel ? Math.ceil(keyTab.keyModel.totalRowCount / maxItemsOnPage) : 0 property bool forceLoading: false property int firstColumnWidth: 75 - property int valueColumnWidth: keyTab.keyModel && keyTab.keyModel.columnNames.length == 2? root.width - 200 - table.firstColumnWidth - table.columnSpacing - : (root.width - 200 - table.firstColumnWidth - table.columnSpacing) / 2 + property int valueColumnWidth: keyTab.keyModel && table.columns > 2 ? (root.width - 200 - table.firstColumnWidth - table.columnSpacing) / (table.columns - 1) + : root.width - 200 - table.firstColumnWidth - table.columnSpacing + property var valueColumnWidthOverrides: QtObject {} Keys.onUpPressed: { @@ -239,7 +240,7 @@ Item { if (keyTab.keyModel.columnNames.length == 2) { table.valueColumnWidthOverrides[index] = width; } else { - for (var i=1; i < 3; i++) + for (var i=1; i < keyTab.keyModel.columnNames.length; i++) { if (i === index) { table.valueColumnWidthOverrides[i] = width; @@ -333,6 +334,37 @@ Item { } } } + + DelegateChoice { + column: 3 + + ValueTableCell { + objectName: "rdm_value_table_cell_col4" + implicitWidth: table.valueColumnWidth + implicitHeight: 30 + + selected: table.currentRow === row + onClicked: { + table.currentRow = row + table.forceActiveFocus() + } + + textDelegate: Component { + TextEdit { + wrapMode: Text.WrapAnywhere + color: root.selected ? sysPalette.highlightedText : sysPalette.text + readOnly: true + selectByMouse: true + text: { + if (display === "" || !isMultiRow) { + return "" + } + return "TTL:" + model.ttl + "\nVersion:" + model.version + } + } + } + } + } } OkDialogOverlay { diff --git a/src/qml/value-editor/ValueTableCell.qml b/src/qml/value-editor/ValueTableCell.qml index 1d428b0e1..9617e2afe 100644 --- a/src/qml/value-editor/ValueTableCell.qml +++ b/src/qml/value-editor/ValueTableCell.qml @@ -4,7 +4,8 @@ import QtQuick.Controls 2.0 Item { id: root - property alias text: textItem.text + property string text + property alias textDelegate: textItem.sourceComponent property alias color: background.color property bool selected: false @@ -18,14 +19,19 @@ Item { color: root.selected ? sysPalette.highlight : sysPalette.base clip: true - TextInput { + Loader { id: textItem anchors.centerIn: parent - wrapMode: Text.WrapAnywhere - color: root.selected ? sysPalette.highlightedText : sysPalette.text - readOnly: true - selectByMouse: true - autoScroll: false + sourceComponent: Component { + TextInput { + wrapMode: Text.WrapAnywhere + color: root.selected ? sysPalette.highlightedText : sysPalette.text + readOnly: true + selectByMouse: true + text: root.text + autoScroll: false + } + } } } diff --git a/src/qml/value-editor/editors/editor.js b/src/qml/value-editor/editors/editor.js index f26be6cbc..05644216c 100644 --- a/src/qml/value-editor/editors/editor.js +++ b/src/qml/value-editor/editors/editor.js @@ -12,7 +12,7 @@ function getEditorByTypeString(keyType) { return "./editors/SingleItemEditor.qml" } else if (keyType === "zset") { return "./editors/SortedSetItemEditor.qml" - } else if (keyType === "hash") { + } else if (keyType === "hash" || keyType === "tairhash") { return "./editors/HashItemEditor.qml" } else if (keyType === "stream") { return "./editors/StreamItemEditor.qml"