mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-19 10:35:50 +00:00
Simplify. Rewrite SHAMap::dirtyUp.
This commit is contained in:
185
SHAMap.cpp
185
SHAMap.cpp
@@ -4,35 +4,30 @@
|
||||
|
||||
#include <boost/foreach.hpp>
|
||||
|
||||
SHAMap::SHAMap(int leafDataSize, int leafDataOffset) : mLeafDataSize(leafDataSize), mLeafDataOffset(leafDataOffset)
|
||||
SHAMap::SHAMap()
|
||||
{
|
||||
;
|
||||
}
|
||||
|
||||
void SHAMap::dirtyUp(const uint256& id, const std::vector<SHAMapInnerNode::pointer>& path)
|
||||
void SHAMap::dirtyUp(const uint256& id)
|
||||
{ // walk the tree up from through the inner nodes to the root
|
||||
// update linking hashes and add nodes to dirty list
|
||||
if(mDirtyInnerNodes==NULL) return;
|
||||
SHAMapLeafNode::pointer leaf=mLeafByID[SHAMapNode(SHAMapNode::leafDepth, id)];
|
||||
if(!leaf) throw SHAMapException(MissingNode);
|
||||
|
||||
std::vector<SHAMapInnerNode::pointer>::const_reverse_iterator it;
|
||||
uint256 hVal=leaf->getNodeHash();
|
||||
if(mDirtyLeafNodes) (*mDirtyLeafNodes)[*leaf]=leaf;
|
||||
if(!hVal) mLeafByID.erase(*leaf);
|
||||
|
||||
it = path.rbegin();
|
||||
if(it == path.rend()) return;
|
||||
|
||||
(*mDirtyInnerNodes)[**it]=*it;
|
||||
uint256 hVal=(*it)->getNodeHash();
|
||||
if(hVal==0)
|
||||
mInnerNodeByID.erase(**it);
|
||||
|
||||
for(++it; it<path.rend(); ++it)
|
||||
{
|
||||
if(!(*it)->setChildHash((*it)->selectBranch(id), hVal))
|
||||
return;
|
||||
(*mDirtyInnerNodes)[**it]=*it;
|
||||
hVal = (*it)->getNodeHash();
|
||||
if(hVal == 0) mInnerNodeByID.erase(**it);
|
||||
for(int depth=SHAMapNode::leafDepth-1; depth>=0; depth--)
|
||||
{ // walk up the tree to the root updating nodes
|
||||
SHAMapInnerNode::pointer node=mInnerNodeByID[SHAMapNode(depth, leaf->getNodeID())];
|
||||
if(!node) throw SHAMapException(MissingNode);
|
||||
if(!node->setChildHash(node->selectBranch(id), hVal)) return;
|
||||
if(mDirtyInnerNodes) (*mDirtyInnerNodes)[*node]=node;
|
||||
hVal=node->getNodeHash();
|
||||
if(!hVal) mInnerNodeByID.erase(*node);
|
||||
}
|
||||
assert(**it == *root);
|
||||
}
|
||||
|
||||
SHAMapLeafNode::pointer SHAMap::checkCacheLeaf(const SHAMapNode& iNode)
|
||||
@@ -42,8 +37,7 @@ SHAMapLeafNode::pointer SHAMap::checkCacheLeaf(const SHAMapNode& iNode)
|
||||
}
|
||||
|
||||
|
||||
SHAMapLeafNode::pointer SHAMap::walkToLeaf(const uint256& id, bool create,
|
||||
std::vector<SHAMapInnerNode::pointer>& path)
|
||||
SHAMapLeafNode::pointer SHAMap::walkToLeaf(const uint256& id, bool create)
|
||||
{ // walk down to the leaf that would contain this ID
|
||||
// is leaf node in cache
|
||||
SHAMapLeafNode::pointer ln=checkCacheLeaf(SHAMapNode(SHAMapNode::leafDepth, id));
|
||||
@@ -51,7 +45,6 @@ SHAMapLeafNode::pointer SHAMap::walkToLeaf(const uint256& id, bool create,
|
||||
|
||||
// walk tree to leaf
|
||||
SHAMapInnerNode::pointer inNode=root;
|
||||
path.push_back(inNode);
|
||||
|
||||
for(int i=1; i<SHAMapNode::leafDepth; i++)
|
||||
{
|
||||
@@ -61,13 +54,12 @@ SHAMapLeafNode::pointer SHAMap::walkToLeaf(const uint256& id, bool create,
|
||||
if(inNode->isEmptyBranch(branch))
|
||||
{ // no nodes below this one
|
||||
if(!create) return SHAMapLeafNode::pointer();
|
||||
return createLeaf(*inNode, id, path);
|
||||
return createLeaf(*inNode, id);
|
||||
}
|
||||
if(i!=(SHAMapNode::leafDepth)-1)
|
||||
{ // child is another inner node
|
||||
inNode=getInner(inNode->getChildNodeID(branch), inNode->getChildHash(branch));
|
||||
if(inNode==NULL) throw SHAMapException(InvalidNode);
|
||||
path.push_back(inNode);
|
||||
}
|
||||
else // child is leaf node
|
||||
{
|
||||
@@ -75,7 +67,7 @@ SHAMapLeafNode::pointer SHAMap::walkToLeaf(const uint256& id, bool create,
|
||||
if(ln==NULL)
|
||||
{
|
||||
if(!create) return SHAMapLeafNode::pointer();
|
||||
return createLeaf(*inNode, id, path);
|
||||
return createLeaf(*inNode, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,30 +78,23 @@ SHAMapLeafNode::pointer SHAMap::walkToLeaf(const uint256& id, bool create,
|
||||
SHAMapLeafNode::pointer SHAMap::getLeaf(const SHAMapNode &id, const uint256& hash)
|
||||
{ // retrieve a leaf whose node hash is known
|
||||
assert(!!hash);
|
||||
if(!id.isLeaf()) return SHAMapLeafNode::pointer();
|
||||
|
||||
SHAMapLeafNode::pointer leaf=mLeafByID[id]; // is the leaf in memory
|
||||
if(leaf) return leaf;
|
||||
|
||||
std::vector<unsigned char> rawNode; // is it in backing store
|
||||
if(!fetchNode(hash, id, rawNode)) return SHAMapLeafNode::pointer();
|
||||
std::vector<SHAMapItem::pointer> leafData; // is it in backing store
|
||||
if(!fetchLeafNode(hash, id, leafData))
|
||||
throw SHAMapException(MissingNode);
|
||||
|
||||
Serializer s(rawNode);
|
||||
leaf=SHAMapLeafNode::pointer(new SHAMapLeafNode(id));
|
||||
|
||||
BOOST_FOREACH(SHAMapItem::pointer& item, leafData)
|
||||
leaf->addUpdateItem(item);
|
||||
|
||||
for(int i=0; i<s.getLength(); i+=mLeafDataSize)
|
||||
{
|
||||
uint256 tag;
|
||||
if(mLeafDataOffset<0)
|
||||
{
|
||||
Serializer inner;
|
||||
inner.addRaw(s.getRaw(i, mLeafDataSize));
|
||||
tag=inner.getSHA512Half();
|
||||
}
|
||||
else tag=s.get256(i+mLeafDataOffset);
|
||||
leaf->addUpdateItem(SHAMapItem::pointer(new SHAMapItem(tag, s.getRaw(i, mLeafDataSize))));
|
||||
}
|
||||
leaf->updateHash();
|
||||
mLeafByID[*leaf]=leaf;
|
||||
assert(leaf->getNodeHash()==hash);
|
||||
if(leaf->getNodeHash()!=hash) throw SHAMapException(InvalidNode);
|
||||
mLeafByID[id]=leaf;
|
||||
return leaf;
|
||||
}
|
||||
|
||||
@@ -119,14 +104,11 @@ SHAMapInnerNode::pointer SHAMap::getInner(const SHAMapNode &id, const uint256& h
|
||||
if(node) return node;
|
||||
|
||||
std::vector<unsigned char> rawNode;
|
||||
if(!fetchNode(hash, id, rawNode)) return SHAMapInnerNode::pointer();
|
||||
if(!fetchInnerNode(hash, id, rawNode)) throw SHAMapException(MissingNode);
|
||||
|
||||
node=SHAMapInnerNode::pointer(new SHAMapInnerNode(id, rawNode));
|
||||
if(node->getNodeHash()!=hash)
|
||||
{
|
||||
badNode(hash, id);
|
||||
return SHAMapInnerNode::pointer();
|
||||
}
|
||||
if(node->getNodeHash()!=hash) throw SHAMapException(InvalidNode);
|
||||
|
||||
mInnerNodeByID[id]=node;
|
||||
return node;
|
||||
}
|
||||
@@ -221,39 +203,40 @@ SHAMapItem::pointer SHAMap::peekNextItem(const uint256& id)
|
||||
{ // Get a pointer to the next item in the tree after a given item - item must be in tree
|
||||
ScopedLock sl(mLock);
|
||||
|
||||
std::vector<SHAMapInnerNode::pointer> path;
|
||||
SHAMapLeafNode::pointer leaf=walkToLeaf(id, false, path);
|
||||
SHAMapLeafNode::pointer leaf=walkToLeaf(id, false);
|
||||
if(!leaf) return SHAMapItem::pointer();
|
||||
|
||||
// is there another item in this leaf? (there almost never will be)
|
||||
SHAMapItem::pointer next=leaf->nextItem(id);
|
||||
if(next) return next;
|
||||
|
||||
for(std::vector<SHAMapInnerNode::pointer>::reverse_iterator rit=path.rbegin(); rit<path.rend(); ++rit)
|
||||
for(int depth=SHAMapNode::leafDepth-1; depth>=0; depth++)
|
||||
{ // walk up the tree until we find a node with a subsequent child
|
||||
SHAMapInnerNode::pointer& node=*rit;
|
||||
for(int i=node->selectBranch(id)+1; i<32; i++)
|
||||
if(!!node->getChildHash(i))
|
||||
{ // node has a subsequent child
|
||||
SHAMapNode nextNode=node->getChildNodeID(i);
|
||||
uint256 nextHash=node->getChildHash(i);
|
||||
|
||||
if(nextNode.isLeaf())
|
||||
{ // this is a terminal inner node
|
||||
leaf=getLeaf(nextNode, nextHash);
|
||||
if(!leaf) throw SHAMapException(MissingNode);
|
||||
next=leaf->firstItem();
|
||||
if(!next) throw SHAMapException(InvalidNode);
|
||||
return next;
|
||||
}
|
||||
|
||||
// the next item is the first item below this node
|
||||
SHAMapInnerNode::pointer inner=getInner(nextNode, nextHash);
|
||||
if(!inner) throw SHAMapException(MissingNode);
|
||||
SHAMapInnerNode::pointer node=mInnerNodeByID[SHAMapNode(depth, id)];
|
||||
if(!node) throw SHAMapException(MissingNode);
|
||||
for(int i=node->selectBranch(id)+1; i<32; i++)
|
||||
if(!!node->getChildHash(i))
|
||||
{ // node has a subsequent child
|
||||
SHAMapNode nextNode(node->getChildNodeID(i));
|
||||
const uint256& nextHash(node->getChildHash(i));
|
||||
|
||||
if(nextNode.isLeaf())
|
||||
{ // this is a terminal inner node
|
||||
leaf=getLeaf(nextNode, nextHash);
|
||||
if(!leaf) throw SHAMapException(MissingNode);
|
||||
next=leaf->firstItem();
|
||||
if(!next) throw SHAMapException(InvalidNode);
|
||||
return next;
|
||||
}
|
||||
|
||||
// the next item is the first item below this node
|
||||
SHAMapInnerNode::pointer inner=getInner(nextNode, nextHash);
|
||||
if(!inner) throw SHAMapException(MissingNode);
|
||||
next=firstBelow(inner);
|
||||
if(!next) throw SHAMapException(InvalidNode);
|
||||
return next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// must be last item
|
||||
@@ -265,15 +248,13 @@ SHAMapItem::pointer SHAMap::peekPrevItem(const uint256& id)
|
||||
// WRITEME
|
||||
}
|
||||
|
||||
SHAMapLeafNode::pointer SHAMap::createLeaf(const SHAMapInnerNode& lowestParent, const uint256& id,
|
||||
std::vector<SHAMapInnerNode::pointer>& path)
|
||||
SHAMapLeafNode::pointer SHAMap::createLeaf(const SHAMapInnerNode& lowestParent, const uint256& id)
|
||||
{
|
||||
int depth=lowestParent.getDepth();
|
||||
for(int depth=lowestParent.getDepth(); depth<SHAMapNode::leafDepth; depth++)
|
||||
{
|
||||
SHAMapInnerNode::pointer newNode(new SHAMapInnerNode(SHAMapNode(++depth, id)));
|
||||
mInnerNodeByID[*newNode]=newNode;
|
||||
path.push_back(newNode);
|
||||
}
|
||||
|
||||
SHAMapLeafNode::pointer newLeaf(new SHAMapLeafNode(SHAMapNode(SHAMapNode::leafDepth, id)));
|
||||
@@ -284,8 +265,7 @@ SHAMapLeafNode::pointer SHAMap::createLeaf(const SHAMapInnerNode& lowestParent,
|
||||
SHAMapItem::pointer SHAMap::peekItem(const uint256& id)
|
||||
{
|
||||
ScopedLock sl(mLock);
|
||||
std::vector<SHAMapInnerNode::pointer> path;
|
||||
SHAMapLeafNode::pointer leaf=walkToLeaf(id, false, path);
|
||||
SHAMapLeafNode::pointer leaf=walkToLeaf(id, false);
|
||||
if(!leaf) return SHAMapItem::pointer();
|
||||
return leaf->findItem(id);
|
||||
}
|
||||
@@ -293,8 +273,7 @@ SHAMapItem::pointer SHAMap::peekItem(const uint256& id)
|
||||
bool SHAMap::hasItem(const uint256& id)
|
||||
{ // does the tree have an item with this ID
|
||||
ScopedLock sl(mLock);
|
||||
std::vector<SHAMapInnerNode::pointer> path;
|
||||
SHAMapLeafNode::pointer leaf=walkToLeaf(id, false, path);
|
||||
SHAMapLeafNode::pointer leaf=walkToLeaf(id, false);
|
||||
if(!leaf) return false;
|
||||
SHAMapItem::pointer item=leaf->findItem(id);
|
||||
return (bool) item;
|
||||
@@ -303,34 +282,31 @@ bool SHAMap::hasItem(const uint256& id)
|
||||
bool SHAMap::delItem(const uint256& id)
|
||||
{ // delete the item with this ID
|
||||
ScopedLock sl(mLock);
|
||||
std::vector<SHAMapInnerNode::pointer> path;
|
||||
SHAMapLeafNode::pointer leaf=walkToLeaf(id, false, path);
|
||||
SHAMapLeafNode::pointer leaf=walkToLeaf(id, false);
|
||||
if(!leaf) return false;
|
||||
if(!leaf->delItem(id)) return false;
|
||||
dirtyUp(id, path);
|
||||
dirtyUp(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SHAMap::addGiveItem(const SHAMapItem::pointer item)
|
||||
{ // add the specified item
|
||||
ScopedLock sl(mLock);
|
||||
std::vector<SHAMapInnerNode::pointer> path;
|
||||
SHAMapLeafNode::pointer leaf=walkToLeaf(item->getTag(), true, path);
|
||||
SHAMapLeafNode::pointer leaf=walkToLeaf(item->getTag(), true);
|
||||
if(!leaf) return false;
|
||||
if(leaf->hasItem(item->getTag())) return false;
|
||||
if(!leaf->addUpdateItem(item)) return false;
|
||||
dirtyUp(item->getTag(), path);
|
||||
dirtyUp(item->getTag());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SHAMap::updateGiveItem(SHAMapItem::pointer item)
|
||||
{
|
||||
ScopedLock sl(mLock);
|
||||
std::vector<SHAMapInnerNode::pointer> path;
|
||||
SHAMapLeafNode::pointer leaf=walkToLeaf(item->getTag(), true, path);
|
||||
SHAMapLeafNode::pointer leaf=walkToLeaf(item->getTag(), true);
|
||||
if(!leaf) return false;
|
||||
if(!leaf->addUpdateItem(item)) return false;
|
||||
dirtyUp(item->getTag(), path);
|
||||
dirtyUp(item->getTag());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -340,37 +316,48 @@ void SHAMapItem::dump()
|
||||
}
|
||||
|
||||
// overloads for backed maps
|
||||
bool SHAMap::fetchNode(const uint256 &, const SHAMapNode &, std::vector<unsigned char> &)
|
||||
bool SHAMap::fetchInnerNode(const uint256&, const SHAMapNode&, std::vector<unsigned char>&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SHAMap::writeNode(const uint256 &, const SHAMapNode &, const std::vector<unsigned char> &)
|
||||
bool SHAMap::fetchLeafNode(const uint256&, const SHAMapNode&, std::vector<SHAMapItem::pointer>&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SHAMap::writeInnerNode(const uint256&, const SHAMapNode&, const std::vector<unsigned char>&)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void SHAMap::badNode(const uint256 &, const SHAMapNode &)
|
||||
bool SHAMap::writeLeafNode(const uint256&, const SHAMapNode&, const std::vector<unsigned char>&)
|
||||
{
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void SHAMap::dump()
|
||||
{
|
||||
}
|
||||
|
||||
static std::vector<unsigned char>IntToVUC(int i)
|
||||
{
|
||||
std::vector<unsigned char> vuc;
|
||||
vuc.push_back((unsigned char) i);
|
||||
return vuc;
|
||||
}
|
||||
|
||||
bool SHAMap::TestSHAMap()
|
||||
{
|
||||
uint256 h1, h2, h3, h4, h5;
|
||||
h1.SetHex("436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
|
||||
h2.SetHex("b92891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
h3.SetHex("b92891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca8");
|
||||
h4.SetHex("a92891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
h5.SetHex("092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
h1.SetHex("092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
h2.SetHex("436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
|
||||
h3.SetHex("b92891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
h4.SetHex("b92891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca8");
|
||||
h5.SetHex("a92891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
|
||||
SHAMap sMap(32);
|
||||
// SHAMapItem i1(h1), i2(h2), i3(h3), i4(h4), i5(h5);
|
||||
SHAMap sMap;
|
||||
SHAMapItem i1(h1, IntToVUC(1)), i2(h2, IntToVUC(2)), i3(h3, IntToVUC(3)), i4(h4, IntToVUC(4)), i5(h5, IntToVUC(5));
|
||||
|
||||
sMap.dump();
|
||||
}
|
||||
|
||||
19
SHAMap.h
19
SHAMap.h
@@ -188,18 +188,16 @@ private:
|
||||
std::map<SHAMapNode, SHAMapLeafNode::pointer> mLeafByID;
|
||||
std::map<SHAMapNode, SHAMapInnerNode::pointer> mInnerNodeByID;
|
||||
boost::shared_ptr<std::map<SHAMapNode, SHAMapLeafNode::pointer> > mDirtyLeafNodes;
|
||||
boost::shared_ptr<std::map<SHAMapNode, SHAMapInnerNode::pointer> > mDirtyInnerNodes;
|
||||
boost::shared_ptr<std::map<SHAMapNode, SHAMapInnerNode::pointer> > mDirtyInnerNodes;
|
||||
|
||||
SHAMapInnerNode::pointer root;
|
||||
|
||||
protected:
|
||||
void dirtyUp(const uint256& id, const std::vector<SHAMapInnerNode::pointer>& path);
|
||||
void dirtyUp(const uint256 &id);
|
||||
|
||||
SHAMapLeafNode::pointer createLeaf(const SHAMapInnerNode& lowestParent, const uint256& id,
|
||||
std::vector<SHAMapInnerNode::pointer>& path);
|
||||
SHAMapLeafNode::pointer createLeaf(const SHAMapInnerNode& lowestParent, const uint256& id);
|
||||
SHAMapLeafNode::pointer checkCacheLeaf(const SHAMapNode &);
|
||||
SHAMapLeafNode::pointer walkToLeaf(const uint256& id, bool create,
|
||||
std::vector<SHAMapInnerNode::pointer>& path);
|
||||
SHAMapLeafNode::pointer walkToLeaf(const uint256& id, bool create);
|
||||
|
||||
SHAMapLeafNode::pointer getLeaf(const SHAMapNode& id, const uint256& hash);
|
||||
SHAMapInnerNode::pointer getInner(const SHAMapNode& id, const uint256& hash);
|
||||
@@ -208,7 +206,7 @@ protected:
|
||||
SHAMapItem::pointer lastBelow(SHAMapInnerNode::pointer);
|
||||
|
||||
public:
|
||||
SHAMap(int leafDataSize=32, int leafDataOffset=-1);
|
||||
SHAMap();
|
||||
|
||||
// hold the map stable across operations
|
||||
ScopedLock Lock() const { return ScopedLock(mLock); }
|
||||
@@ -260,9 +258,10 @@ public:
|
||||
int flushDirty(int maxNodes);
|
||||
|
||||
// overloads for backed maps
|
||||
virtual bool fetchNode(const uint256& hash, const SHAMapNode& id, std::vector<unsigned char>& rawNode);
|
||||
virtual bool writeNode(const uint256& hash, const SHAMapNode& id, const std::vector<unsigned char>& rawNode);
|
||||
virtual void badNode(const uint256& hash, const SHAMapNode& id);
|
||||
virtual bool fetchInnerNode(const uint256& hash, const SHAMapNode& id, std::vector<unsigned char>& rawNode);
|
||||
virtual bool fetchLeafNode(const uint256& hash, const SHAMapNode& id, std::vector<SHAMapItem::pointer>& nodeData);
|
||||
virtual bool writeInnerNode(const uint256& hash, const SHAMapNode& id, const std::vector<unsigned char>& rawNode);
|
||||
virtual bool writeLeafNode(const uint256& hash, const SHAMapNode& id, const std::vector<unsigned char>& rawNode);
|
||||
|
||||
static bool TestSHAMap();
|
||||
virtual void dump(void);
|
||||
|
||||
@@ -121,10 +121,10 @@ bool SHAMapLeafNode::addUpdateItem(SHAMapItem::pointer item)
|
||||
SHAMapItem &nodeItem=**it;
|
||||
if(nodeItem.getTag()==item->getTag())
|
||||
{
|
||||
if(nodeItem.peekData()==item->peekData())
|
||||
return false; // no change
|
||||
nodeItem.updateData(item->peekData());
|
||||
return updateHash();
|
||||
if(nodeItem.peekData()==item->peekData())
|
||||
return false; // no change
|
||||
nodeItem.updateData(item->peekData());
|
||||
return updateHash();
|
||||
}
|
||||
if(nodeItem.getTag()>item->getTag())
|
||||
{
|
||||
@@ -184,7 +184,7 @@ SHAMapItem::pointer SHAMapLeafNode::lastItem(void)
|
||||
bool SHAMapLeafNode::updateHash(void)
|
||||
{
|
||||
uint256 nh;
|
||||
if(mItems.size()!=0) nh=0;
|
||||
if(mItems.size()!=0)
|
||||
{
|
||||
Serializer s;
|
||||
BOOST_FOREACH(const SHAMapItem::pointer &mi, mItems)
|
||||
|
||||
Reference in New Issue
Block a user