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

[Improve]: scan operations for IReadOnlyStoreView and fix of Find with all 0xff keyPrefix #3688

Closed
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3b7cd98
improve: scan operations for ReadOnlyStoreView
nan01ab Jan 21, 2025
6ccb8ca
improve: scan operations for ReadOnlyStoreView
nan01ab Jan 22, 2025
5628012
Improve: add more comments
nan01ab Jan 22, 2025
a81b8f6
Update src/Neo/Extensions/Persistence/ReadOnlyViewExtensions.cs
shargon Jan 22, 2025
16c9ef2
Merge branch 'master' into feat.scan-readonly-storeview
shargon Jan 22, 2025
86a8d75
Improve: use ArrayExtensions.Repeat instead
nan01ab Jan 22, 2025
77fb843
Improve: use ArrayExtensions.Repeat instead
nan01ab Jan 22, 2025
7477f87
Fix code format and add more comments
nan01ab Jan 22, 2025
1bb8715
Merge branch 'master' into feat.scan-readonly-storeview
Jim8y Jan 23, 2025
0c7767a
Merge branch 'master' into feat.scan-readonly-storeview
cschuchardt88 Jan 23, 2025
3dde742
Update src/Neo/Persistence/ReadOnlyStoreView.cs
nan01ab Jan 23, 2025
746b2ad
Update src/Neo/Extensions/Persistence/ReadOnlyViewExtensions.cs
nan01ab Jan 23, 2025
c06a38d
Update src/Neo/Extensions/Persistence/ReadOnlyViewExtensions.cs
nan01ab Jan 23, 2025
1eb7e1e
Update src/Neo/Extensions/Persistence/ReadOnlyViewExtensions.cs
nan01ab Jan 24, 2025
eb26415
Merge branch 'master' into feat.scan-readonly-storeview
shargon Jan 24, 2025
bf905fe
Merge branch 'master' into feat.scan-readonly-storeview
shargon Jan 26, 2025
b5bc0e7
Merge branch 'master' into feat.scan-readonly-storeview
shargon Jan 27, 2025
976dd32
Merge branch 'master' into feat.scan-readonly-storeview
cschuchardt88 Jan 31, 2025
5beabbc
Update src/Neo/Extensions/Persistence/ReadOnlyViewExtensions.cs
nan01ab Jan 31, 2025
4d7f58a
Fix: unit test issue
nan01ab Jan 31, 2025
46c28a8
Merge branch 'master' into feat.scan-readonly-storeview
Jim8y Feb 1, 2025
a417172
fix: merge conflict
nan01ab Feb 1, 2025
f959ca4
Merge branch 'master' into feat.scan-readonly-storeview
nan01ab Feb 1, 2025
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
34 changes: 34 additions & 0 deletions src/Neo.Extensions/ArrayExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// ArrayExtensions.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using System;
using System.Runtime.CompilerServices;

namespace Neo.Extensions
{
public static class ArrayExtensions
{
/// <summary>
/// Creates an array of the specified length, filled with the specified value.
/// </summary>
/// <typeparam name="T">The type of the array elements.</typeparam>
/// <param name="value">The value to fill the array with.</param>
/// <param name="count">The number of elements in the array.</param>
/// <returns>An array of the specified length, filled with the specified value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T[] Repeat<T>(this T value, int count)
{
T[] array = new T[count];
Array.Fill(array, value);
return array;
}
}
}
157 changes: 157 additions & 0 deletions src/Neo/Extensions/Persistence/ReadOnlyViewExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// ReadOnlyViewExtensions.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

#nullable enable

using Neo.Persistence;
using Neo.SmartContract;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace Neo.Extensions
{
public static class ReadOnlyViewExtensions
{
/// <summary>
/// Scans the entries starting with the specified prefix.
/// <para>
/// If <paramref name="direction"/> is <see cref="SeekDirection.Forward"/>,
/// it seeks to the first entry if <paramref name="keyPrefix"/> is null or empty.
/// </para>
/// <para>
/// If <paramref name="direction"/> is <see cref="SeekDirection.Backward"/>,
/// the <paramref name="keyPrefix"/> cannot be null or empty.
/// </para>
/// <para>
/// If want to scan all entries with <see cref="SeekDirection.Backward"/>,
/// set <paramref name="keyPrefix"/> to be N * 0xff, where N is the max length of the key.
/// See <see cref="ArrayExtensions.Repeat"/>.
/// </para>
/// </summary>
/// <param name="view">The view to scan.</param>
/// <param name="keyPrefix">The prefix of the key.</param>
/// <param name="direction">The search direction.</param>
/// <returns>The entries found with the desired prefix.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEnumerable<(StorageKey Key, StorageItem Value)> ScanPrefix(
this IReadOnlyStoreView view,
byte[]? keyPrefix,
SeekDirection direction = SeekDirection.Forward)
{
cschuchardt88 marked this conversation as resolved.
Show resolved Hide resolved
var seekPrefix = direction == SeekDirection.Forward ? keyPrefix : keyPrefix.GetSeekPrefix();
return view.ScanPrefix(keyPrefix, seekPrefix, direction);
}

internal static IEnumerable<(StorageKey Key, StorageItem Value)> ScanPrefix(
this IReadOnlyStoreView view,
byte[]? keyPrefix,
byte[]? seekPrefix,
SeekDirection direction = SeekDirection.Forward)
Copy link
Member

Choose a reason for hiding this comment

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

Just use comparer. Just forget about these direction things for a min. dotnet uses comparer why can't we? There are good reasons to use comparer instead of enums.

{
foreach (var (key, value) in view.Seek(seekPrefix, direction))
{
if (keyPrefix == null || key.ToArray().AsSpan().StartsWith(keyPrefix))
yield return new(key, value);
else if (direction == SeekDirection.Forward || (seekPrefix == null || !key.ToArray().SequenceEqual(seekPrefix)))
yield break;
Comment on lines +63 to +66
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't have to do this if you use comparer. Just look at LevelDbStore and MemoryStore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't have to do this if you use comparer. Just look at LevelDbStore and MemoryStore.

These lines aren't new.
I moved these from DataCache.cs to here.
https://github.com/neo-project/neo/blob/master/src/Neo/Persistence/DataCache.cs#L242

Copy link
Member

Choose a reason for hiding this comment

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

Copying/Moving bugs doesn't make it ok.

Copy link
Contributor Author

@nan01ab nan01ab Feb 1, 2025

Choose a reason for hiding this comment

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

Copying/Moving bugs doesn't make it ok.

This PR didn't change this.
You can submit a new PR to fix it.

}
}

/// <summary>
/// Scans the entries in the specified range.
/// <para>
/// If <paramref name="direction"/> is <see cref="SeekDirection.Forward"/>,
/// it seeks to the first entry if <paramref name="inclusiveStartKey"/> is null or empty.
/// </para>
/// <para>
/// If <paramref name="direction"/> is <see cref="SeekDirection.Backward"/>,
/// the <paramref name="inclusiveStartKey"/> cannot be null or empty.
/// </para>
/// <para>
/// If want to scan all entries with <see cref="SeekDirection.Backward"/>,
/// set <paramref name="inclusiveStartKey"/> to be N * 0xff and <paramref name="exclusiveEndKey"/> to be empty,
/// where N is the max length of the key.
/// </para>
/// </summary>
/// <param name="view">The view to scan.</param>
/// <param name="inclusiveStartKey">The inclusive start key.</param>
/// <param name="exclusiveEndKey">The exclusive end key.</param>
/// <param name="direction">The search direction.</param>
/// <returns>The entries found in the specified range.</returns>
public static IEnumerable<(StorageKey Key, StorageItem Value)> ScanRange(
this IReadOnlyStoreView view,
byte[]? inclusiveStartKey,
byte[] exclusiveEndKey,
SeekDirection direction = SeekDirection.Forward)
{
ByteArrayComparer comparer = direction == SeekDirection.Forward
? ByteArrayComparer.Default
: ByteArrayComparer.Reverse;
foreach (var (key, value) in view.Seek(inclusiveStartKey, direction))
{
if (comparer.Compare(key.ToArray(), exclusiveEndKey) < 0)
Copy link
Member

Choose a reason for hiding this comment

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

You want the the key. If the prefix is a key?

Suggested change
if (comparer.Compare(key.ToArray(), exclusiveEndKey) < 0)
if (comparer.Compare(key.ToArray(), exclusiveEndKey) <= 0)

Copy link
Contributor Author

@nan01ab nan01ab Jan 31, 2025

Choose a reason for hiding this comment

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

You want the the key. If the prefix is a key?

exclusiveEndKey

because of exclusive.

Copy link
Member

@cschuchardt88 cschuchardt88 Jan 31, 2025

Choose a reason for hiding this comment

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

Why go to all the trouble to edit or change this. If your not going to fix the issues with the class?

Copy link
Contributor Author

@nan01ab nan01ab Jan 31, 2025

Choose a reason for hiding this comment

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

Why go to all the trouble to edit or change this. If your not going to fix the issues with the class?

Because this is ScanRange/FindRange, and range is [StartKey, EndKey)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You want the the key. If the prefix is a key?

start key and end key are not prefix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where are you getting this information? What is not to understand? IStore doesn't work this way. Find has bugs in DataCache.

https://github.com/neo-project/neo/blob/master/src/Neo/Persistence/DataCache.cs#L264

Copy link
Member

Choose a reason for hiding this comment

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

Like I said, DataCache has bugs.

LevelDBStore

public static IEnumerable<(byte[], byte[])> Seek(this DB db, ReadOptions options, byte[]? keyOrPrefix, SeekDirection direction)
{
keyOrPrefix ??= [];
using var it = db.CreateIterator(options);
if (direction == SeekDirection.Forward)
{
for (it.Seek(keyOrPrefix); it.Valid(); it.Next())
yield return new(it.Key()!, it.Value()!);
}
else
{
// SeekForPrev
it.Seek(keyOrPrefix);
if (!it.Valid())
it.SeekToLast();
else if (it.Key().AsSpan().SequenceCompareTo(keyOrPrefix) > 0)
it.Prev();
for (; it.Valid(); it.Prev())
yield return new(it.Key()!, it.Value()!);
}
}

RocksDbStore

public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[]? keyOrPrefix, SeekDirection direction = SeekDirection.Forward)
{
keyOrPrefix ??= [];
using var it = _db.NewIterator();
if (direction == SeekDirection.Forward)
for (it.Seek(keyOrPrefix); it.Valid(); it.Next())
yield return (it.Key(), it.Value());
else
for (it.SeekForPrev(keyOrPrefix); it.Valid(); it.Prev())
yield return (it.Key(), it.Value());
}

MemoryStore

public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[]? keyOrPrefix, SeekDirection direction = SeekDirection.Forward)
{
keyOrPrefix ??= [];
if (direction == SeekDirection.Backward && keyOrPrefix.Length == 0) yield break;
var comparer = direction == SeekDirection.Forward ? ByteArrayComparer.Default : ByteArrayComparer.Reverse;
IEnumerable<KeyValuePair<byte[], byte[]>> records = _innerData;
if (keyOrPrefix.Length > 0)
records = records.Where(p => comparer.Compare(p.Key, keyOrPrefix) >= 0);
records = records.OrderBy(p => p.Key, comparer);
foreach (var pair in records)
yield return (pair.Key[..], pair.Value[..]);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Like I said, DataCache has bugs.

LevelDBStore

public static IEnumerable<(byte[], byte[])> Seek(this DB db, ReadOptions options, byte[]? keyOrPrefix, SeekDirection direction)
{
keyOrPrefix ??= [];
using var it = db.CreateIterator(options);
if (direction == SeekDirection.Forward)
{
for (it.Seek(keyOrPrefix); it.Valid(); it.Next())
yield return new(it.Key()!, it.Value()!);
}
else
{
// SeekForPrev
it.Seek(keyOrPrefix);
if (!it.Valid())
it.SeekToLast();
else if (it.Key().AsSpan().SequenceCompareTo(keyOrPrefix) > 0)
it.Prev();
for (; it.Valid(); it.Prev())
yield return new(it.Key()!, it.Value()!);
}
}

RocksDbStore

public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[]? keyOrPrefix, SeekDirection direction = SeekDirection.Forward)
{
keyOrPrefix ??= [];
using var it = _db.NewIterator();
if (direction == SeekDirection.Forward)
for (it.Seek(keyOrPrefix); it.Valid(); it.Next())
yield return (it.Key(), it.Value());
else
for (it.SeekForPrev(keyOrPrefix); it.Valid(); it.Prev())
yield return (it.Key(), it.Value());
}

MemoryStore

public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[]? keyOrPrefix, SeekDirection direction = SeekDirection.Forward)
{
keyOrPrefix ??= [];
if (direction == SeekDirection.Backward && keyOrPrefix.Length == 0) yield break;
var comparer = direction == SeekDirection.Forward ? ByteArrayComparer.Default : ByteArrayComparer.Reverse;
IEnumerable<KeyValuePair<byte[], byte[]>> records = _innerData;
if (keyOrPrefix.Length > 0)
records = records.Where(p => comparer.Compare(p.Key, keyOrPrefix) >= 0);
records = records.OrderBy(p => p.Key, comparer);
foreach (var pair in records)
yield return (pair.Key[..], pair.Value[..]);
}

If DataCache has bugs, you can submit a PR to fix it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not **Find**.

ScanRange / FindRange return items within range [StartKey(inclusive), EndKey(exclusive) )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

prefix is misleading word.

it doesnt matter. In the stores the key are in order. If you convert all keys to an integer they start from zero through Infinity (or biggest key in the store). so if last key is [0xff, 0xff] and i use prefix 0xff the seeker goto 0xff key.

Example with prefix 0xff for keys ranging from [0x01] to [0xff, 0xff]

Records returned in order:

Key: [0xff]
Key: [0xfe]
Key: [0xfd]
Key: [0xfc]
Key: [0xfb]
Key: [0xfa]
.... etc ...
Key: [0xdf]
Key: [0xde]
Key: [0xdd]
Key: [0xdc]
Key: [0xdb]
Key: [0xda]
.... etc ...
Key: [0x01]

You can read this:

/// <summary>
/// Finds the entries that between [start, end).
/// </summary>
/// <param name="start">The start key (inclusive).</param>
/// <param name="end">The end key (exclusive).</param>
/// <param name="direction">The search direction.</param>
/// <returns>The entries found with the desired range.</returns>
public IEnumerable<(StorageKey Key, StorageItem Value)> FindRange(byte[] start, byte[] end, SeekDirection direction = SeekDirection.Forward)

yield return new(key, value);
else
yield break;
}
}

/// <summary>
/// Gets the seek prefix for the specified key prefix.
/// <para>
/// If the <paramref name="keyPrefix"/> is all 0xff, and <paramref name="maxSizeWhenAll0xff"/> > 0,
/// the seek prefix will be set to be byte[maxSizeWhenAll0xff] and filled with 0xff.
/// </para>
/// <para>
/// If the <paramref name="keyPrefix"/> is all 0xff and <paramref name="maxSizeWhenAll0xff"/> is less than or equal to 0,
/// an ArgumentException will be thrown.
/// </para>
/// </summary>
/// <param name="keyPrefix">The key prefix.</param>
/// <param name="maxSizeWhenAll0xff">The maximum size when all bytes are 0xff.</param>
/// <returns>The seek prefix.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="keyPrefix"/> is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="keyPrefix"/> is empty.</exception>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="keyPrefix"/> is all 0xff and <paramref name="maxSizeWhenAll0xff"/> is less than or equal to 0.
/// </exception>
internal static byte[] GetSeekPrefix(this byte[]? keyPrefix, int maxSizeWhenAll0xff = 4096 /* make it long enough */)
nan01ab marked this conversation as resolved.
Show resolved Hide resolved
{
if (keyPrefix == null) // Backwards seek for null prefix is not supported for now.
Copy link
Member

Choose a reason for hiding this comment

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

This is inconsistent with MemoryStore, LevelDbStore and RocksDbStore. Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to fix it. #3680

Copy link
Member

Choose a reason for hiding this comment

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

Fixed in #3701, but that PR isn't going to get merge. So, if you want you can create new PR and use my work.

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 in #3701, but that PR isn't going to get merge. So, if you want you can create new PR and use my work.

Yes, I tried to fix it.
But a requirement I received is to maintain consistent interface/api behavior.

Copy link
Member

Choose a reason for hiding this comment

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

who required this? Well don't listen them. As long as two developer approve a PR. It gets merged (with no issues and change requests).

throw new ArgumentNullException(nameof(keyPrefix));

if (keyPrefix.Length == 0) // Backwards seek for zero prefix is not supported for now.
throw new ArgumentOutOfRangeException(nameof(keyPrefix));

byte[]? seekPrefix = null;
for (var i = keyPrefix.Length - 1; i >= 0; i--)
{
if (keyPrefix[i] < 0xff)
{
seekPrefix = keyPrefix.Take(i + 1).ToArray();
seekPrefix[i]++; // The next key after the key_prefix.
break;
}
}

if (seekPrefix == null)
{
if (maxSizeWhenAll0xff > 0)
seekPrefix = ((byte)0xff).Repeat(maxSizeWhenAll0xff);
else
throw new ArgumentException($"{nameof(keyPrefix)} with all bytes being 0xff is not supported now");
nan01ab marked this conversation as resolved.
Show resolved Hide resolved
}
return seekPrefix;
}
}
}
71 changes: 10 additions & 61 deletions src/Neo/Persistence/DataCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,50 +202,14 @@ public void Delete(StorageKey key)
/// <summary>
/// Finds the entries starting with the specified prefix.
/// </summary>
/// <param name="key_prefix">The prefix of the key.</param>
/// <param name="keyPrefix">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, SeekDirection direction = SeekDirection.Forward)
public IEnumerable<(StorageKey Key, StorageItem Value)> Find(byte[]? keyPrefix = null, SeekDirection direction = SeekDirection.Forward)
{
var seek_prefix = key_prefix;
if (direction == SeekDirection.Backward)
{
if (key_prefix == null)
{
// Backwards seek for null prefix is not supported for now.
throw new ArgumentNullException(nameof(key_prefix));
}
if (key_prefix.Length == 0)
{
// Backwards seek for zero prefix is not supported for now.
throw new ArgumentOutOfRangeException(nameof(key_prefix));
}
seek_prefix = null;
for (var 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;
}
}
if (seek_prefix == null)
{
throw new ArgumentException($"{nameof(key_prefix)} with all bytes being 0xff is not supported now");
}
}
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_prefix == null || key.ToArray().AsSpan().StartsWith(key_prefix))
yield return (key, value);
else if (direction == SeekDirection.Forward || (seek_prefix == null || !key.ToArray().SequenceEqual(seek_prefix)))
yield break;
// GetSeekPrefix with 0 for compatibility with old code
var seekPrefix = direction == SeekDirection.Forward ? keyPrefix : keyPrefix.GetSeekPrefix(0);
return this.ScanPrefix(keyPrefix, seekPrefix, direction);
shargon marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
Expand All @@ -257,14 +221,7 @@ public void Delete(StorageKey key)
/// <returns>The entries found with the desired range.</returns>
public IEnumerable<(StorageKey Key, StorageItem Value)> FindRange(byte[] start, byte[] end, SeekDirection direction = SeekDirection.Forward)
{
ByteArrayComparer comparer = direction == SeekDirection.Forward
? ByteArrayComparer.Default
: ByteArrayComparer.Reverse;
foreach (var (key, value) in Seek(start, direction))
if (comparer.Compare(key.ToArray(), end) < 0)
yield return (key, value);
else
yield break;
return this.ScanRange(start, end, direction);
}

/// <summary>
Expand Down Expand Up @@ -420,24 +377,16 @@ public StorageItem GetOrAdd(StorageKey key, Func<StorageItem> factory)
{
cached = _dictionary
.Where(p => p.Value.State != TrackState.Deleted && p.Value.State != TrackState.NotFound && (keyOrPrefix == null || comparer.Compare(p.Key.ToArray(), keyOrPrefix) >= 0))
.Select(p =>
(
KeyBytes: p.Key.ToArray(),
p.Key,
p.Value.Item
))
.Select(p => (KeyBytes: p.Key.ToArray(), p.Key, p.Value.Item))
.OrderBy(p => p.KeyBytes, comparer)
.ToArray();
cachedKeySet = new HashSet<StorageKey>(_dictionary.Keys);
}

var uncached = SeekInternal(keyOrPrefix ?? Array.Empty<byte>(), direction)
.Where(p => !cachedKeySet.Contains(p.Key))
.Select(p =>
(
KeyBytes: p.Key.ToArray(),
p.Key,
p.Value
));
.Select(p => (KeyBytes: p.Key.ToArray(), p.Key, p.Value));

using var e1 = cached.GetEnumerator();
using var e2 = uncached.GetEnumerator();
(byte[] KeyBytes, StorageKey Key, StorageItem Item) i1, i2;
Expand Down
20 changes: 19 additions & 1 deletion src/Neo/Persistence/IReadOnlyStoreView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

#nullable enable

using Neo.SmartContract;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace Neo.Persistence
{
Expand Down Expand Up @@ -41,6 +44,21 @@ public interface IReadOnlyStoreView
/// <param name="key">The key to get.</param>
/// <param name="item">The entry if found, null otherwise.</param>
/// <returns>True if the entry exists, false otherwise.</returns>
bool TryGet(StorageKey key, out StorageItem item);
bool TryGet(StorageKey key, [NotNullWhen(true)] out StorageItem? item);

/// <summary>
/// Seeks to the entry with the specified key.
/// <para>
/// If keyPrefix is null or empty, it will seek to the first key(even if the direction is backward).
/// So if seek to the last, keyPrefix should be N * 0xff, and N is the max length of the key.
/// </para>
/// </summary>
/// <param name="keyPrefix">The keyPrefix to be sought.</param>
/// <param name="direction">The direction of seek.</param>
/// <returns>
/// An enumerator containing all the entries after keyPrefix(Forward) or before keyPrefix(Backward).
/// </returns>
IEnumerable<(StorageKey Key, StorageItem Value)> Seek(
byte[]? keyPrefix = null, SeekDirection direction = SeekDirection.Forward);
}
}
28 changes: 23 additions & 5 deletions src/Neo/Persistence/ReadOnlyStoreView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,32 @@
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

#nullable enable

using Neo.SmartContract;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

namespace Neo.Persistence
{
/// <summary>
/// A read-only view of a store.
/// No cache and lock in this implementation,
/// so it is faster in some cases(For example, no repeated reads of the same key).
/// </summary>
public class ReadOnlyStoreView : IReadOnlyStoreView
{
private readonly IReadOnlyStore store;
private readonly IReadOnlyStore _store;

public ReadOnlyStoreView(IReadOnlyStore store)
{
this.store = store;
_store = store;
}

/// <inheritdoc/>
public bool Contains(StorageKey key) => store.Contains(key.ToArray());
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Contains(StorageKey key) => _store.Contains(key.ToArray());

/// <inheritdoc/>
public StorageItem this[StorageKey key]
Expand All @@ -38,11 +48,19 @@ public StorageItem this[StorageKey key]
}

/// <inheritdoc/>
public bool TryGet(StorageKey key, out StorageItem item)
public bool TryGet(StorageKey key, [NotNullWhen(true)] out StorageItem? item)
{
var ok = store.TryGet(key.ToArray(), out byte[] value);
var ok = _store.TryGet(key.ToArray(), out var value);
item = ok ? new StorageItem(value) : null;
return ok;
}

/// <inheritdoc/>
public IEnumerable<(StorageKey Key, StorageItem Value)> Seek(
byte[]? keyOrPrefix = null, SeekDirection direction = SeekDirection.Forward)
{
foreach (var (key, value) in _store.Seek(keyOrPrefix, direction))
yield return new(new(key), new(value));
}
}
}
Loading
Loading