Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SmartContract: add FindOptions.Backwards to iterate in reverse order #2819

Merged
merged 7 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions src/Neo/Persistence/DataCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,42 @@ public void Delete(StorageKey key)
/// Finds the entries starting with the specified prefix.
/// </summary>
/// <param name="key_prefix">The prefix of the key.</param>
/// <param name="direction">The search direction.</param>
/// <returns>The entries found with the desired prefix.</returns>
public IEnumerable<(StorageKey Key, StorageItem Value)> Find(byte[] key_prefix = null)
public IEnumerable<(StorageKey Key, StorageItem Value)> Find(byte[] key_prefix = null, SeekDirection direction = SeekDirection.Forward)
{
foreach (var (key, value) in Seek(key_prefix, SeekDirection.Forward))
var seek_prefix = key_prefix;
if (direction == SeekDirection.Backward)
{
if (key_prefix == null || key_prefix.Length == 0)
{ // Backwards seek for zero prefix is not supported for now.
throw new ArgumentException();
}
seek_prefix = null;
for (int i = key_prefix.Length - 1; i >= 0; i--)
{
if (key_prefix[i] < 0xff)
{
seek_prefix = key_prefix.Take(i + 1).ToArray();
// The next key after the key_prefix.
seek_prefix[i]++;
break;
}
}
shargon marked this conversation as resolved.
Show resolved Hide resolved
if (seek_prefix == null)
{
throw new ArgumentException();
}
}
return FindInternal(key_prefix, seek_prefix, direction);
}

private IEnumerable<(StorageKey Key, StorageItem Value)> FindInternal(byte[] key_prefix, byte[] seek_prefix, SeekDirection direction)
{
foreach (var (key, value) in Seek(seek_prefix, direction))
if (key.ToArray().AsSpan().StartsWith(key_prefix))
yield return (key, value);
else
else if (direction == SeekDirection.Forward || !key.ToArray().SequenceEqual(seek_prefix))
yield break;
}

Expand Down
4 changes: 3 additions & 1 deletion src/Neo/SmartContract/ApplicationEngine.Storage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Neo.Persistence;
using Neo.SmartContract.Iterators;
using Neo.SmartContract.Native;
using System;
Expand Down Expand Up @@ -152,7 +153,8 @@ protected internal IIterator Find(StorageContext context, byte[] prefix, FindOpt
if ((options.HasFlag(FindOptions.PickField0) || options.HasFlag(FindOptions.PickField1)) && !options.HasFlag(FindOptions.DeserializeValues))
throw new ArgumentException(null, nameof(options));
byte[] prefix_key = StorageKey.CreateSearchPrefix(context.Id, prefix);
return new StorageIterator(Snapshot.Find(prefix_key).GetEnumerator(), prefix.Length, options);
SeekDirection direction = options.HasFlag(FindOptions.Backwards) ? SeekDirection.Backward : SeekDirection.Forward;
return new StorageIterator(Snapshot.Find(prefix_key, direction).GetEnumerator(), prefix.Length, options);
}

/// <summary>
Expand Down
7 changes: 6 additions & 1 deletion src/Neo/SmartContract/FindOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,14 @@ public enum FindOptions : byte
/// </summary>
PickField1 = 1 << 5,

/// <summary>
/// Indicates that results should be returned in backwards (descending) order.
/// </summary>
Backwards = 1 << 7,

/// <summary>
/// This value is only for internal use, and shouldn't be used in smart contracts.
/// </summary>
All = KeysOnly | RemovePrefix | ValuesOnly | DeserializeValues | PickField0 | PickField1
All = KeysOnly | RemovePrefix | ValuesOnly | DeserializeValues | PickField0 | PickField1 | Backwards
}
}
43 changes: 42 additions & 1 deletion tests/Neo.UnitTests/IO/Caching/UT_DataCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,52 @@ public void TestFind()
store.Put(key3.ToArray(), value3.ToArray());
store.Put(key4.ToArray(), value4.ToArray());

var items = myDataCache.Find(key1.ToArray());
var k1 = key1.ToArray();
var items = myDataCache.Find(k1);
key1.Should().Be(items.ElementAt(0).Key);
value1.Should().Be(items.ElementAt(0).Value);
items.Count().Should().Be(1);

// null and empty with the forward direction -> finds everything.
items = myDataCache.Find(null);
items.Count().Should().Be(4);
items = myDataCache.Find(new byte[] { });
items.Count().Should().Be(4);

// null and empty with the backwards direction -> miserably fails.
Action action = () => myDataCache.Find(null, SeekDirection.Backward);
action.Should().Throw<ArgumentException>();
action = () => myDataCache.Find(new byte[] { }, SeekDirection.Backward);
action.Should().Throw<ArgumentException>();

items = myDataCache.Find(k1, SeekDirection.Backward);
key1.Should().Be(items.ElementAt(0).Key);
value1.Should().Be(items.ElementAt(0).Value);
items.Count().Should().Be(1);

var prefix = k1.Take(k1.Count() - 1).ToArray(); // Just the "key" part to match everything.
items = myDataCache.Find(prefix);
items.Count().Should().Be(4);
key1.Should().Be(items.ElementAt(0).Key);
value1.Should().Be(items.ElementAt(0).Value);
key2.Should().Be(items.ElementAt(1).Key);
value2.Should().Be(items.ElementAt(1).Value);
key3.Should().Be(items.ElementAt(2).Key);
value3.EqualsTo(items.ElementAt(2).Value).Should().BeTrue();
key4.Should().Be(items.ElementAt(3).Key);
value4.EqualsTo(items.ElementAt(3).Value).Should().BeTrue();

items = myDataCache.Find(prefix, SeekDirection.Backward);
items.Count().Should().Be(4);
key4.Should().Be(items.ElementAt(0).Key);
value4.EqualsTo(items.ElementAt(0).Value).Should().BeTrue();
key3.Should().Be(items.ElementAt(1).Key);
value3.EqualsTo(items.ElementAt(1).Value).Should().BeTrue();
key2.Should().Be(items.ElementAt(2).Key);
value2.Should().Be(items.ElementAt(2).Value);
key1.Should().Be(items.ElementAt(3).Key);
value1.Should().Be(items.ElementAt(3).Value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests for the null cases length 0?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed and improved impl. a bit.


items = myDataCache.Find(key5.ToArray());
items.Count().Should().Be(0);
}
Expand Down