diff --git a/src/ZoneTree.UnitTests/InMemoryFileStreamProviderTests.cs b/src/ZoneTree.UnitTests/InMemoryFileStreamProviderTests.cs new file mode 100644 index 0000000..cc76522 --- /dev/null +++ b/src/ZoneTree.UnitTests/InMemoryFileStreamProviderTests.cs @@ -0,0 +1,28 @@ +using Tenray.ZoneTree.AbstractFileStream; +using Tenray.ZoneTree.Logger; +using Tenray.ZoneTree.Serializers; +using Tenray.ZoneTree.WAL; + +namespace Tenray.ZoneTree.UnitTests; + +public sealed class InMemoryFileStreamProviderTests +{ + [Test] + public void WalWithInMemoryProvider() + { + var serializer = new UnicodeStringSerializer(); + var provider = new InMemoryFileStreamProvider(); + var wal = new SyncFileSystemWriteAheadLog( + new ConsoleLogger(), + provider, + serializer, + serializer, + "test.wal"); + wal.Append("hello", "world", 0); + var result = wal.ReadLogEntries(false, false, true); + Assert.That(result.Success, Is.True); + Assert.That(result.Keys[0], Is.EqualTo("hello")); + Assert.That(result.Values[0], Is.EqualTo("world")); + wal.Drop(); + } +} diff --git a/src/ZoneTree/AbstractFileStream/InMemoryFileStream.cs b/src/ZoneTree/AbstractFileStream/InMemoryFileStream.cs new file mode 100644 index 0000000..23bee65 --- /dev/null +++ b/src/ZoneTree/AbstractFileStream/InMemoryFileStream.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; + +namespace Tenray.ZoneTree.AbstractFileStream; + +public sealed class InMemoryFileStream : MemoryStream, IFileStream +{ + readonly InMemoryFileStreamProvider Provider; + + public string FilePath { get; } + + public InMemoryFileStream( + InMemoryFileStreamProvider provider, + string path, + byte[] buffer) + { + Provider = provider; + FilePath = path; + if (buffer.Length > 0) + Write(buffer, 0, buffer.Length); + Position = 0; + } + + public void Flush(bool flushToDisk) + { + Flush(); + } + + public int ReadFaster(byte[] buffer, int offset, int count) + { + int totalRead = 0; + while (totalRead < count) + { + int read = Read(buffer, offset + totalRead, count - totalRead); + if (read == 0) + throw new EndOfStreamException(); + totalRead += read; + } + return totalRead; + } + + public Stream ToStream() => this; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + Provider.UpdateFile(FilePath, ToArray()); + } + base.Dispose(disposing); + } +} diff --git a/src/ZoneTree/AbstractFileStream/InMemoryFileStreamProvider.cs b/src/ZoneTree/AbstractFileStream/InMemoryFileStreamProvider.cs new file mode 100644 index 0000000..6a604fd --- /dev/null +++ b/src/ZoneTree/AbstractFileStream/InMemoryFileStreamProvider.cs @@ -0,0 +1,137 @@ +using System.Text; + +namespace Tenray.ZoneTree.AbstractFileStream; + +public sealed class InMemoryFileStreamProvider : IFileStreamProvider +{ + readonly Dictionary Files = new(); + readonly HashSet Directories = new(); + + public IFileStream CreateFileStream( + string path, + FileMode mode, + FileAccess access, + FileShare share, + int bufferSize = 4096, + FileOptions options = FileOptions.None) + { + lock (this) + { + if (!Files.ContainsKey(path)) + { + if (mode == FileMode.Open) + throw new FileNotFoundException(path); + Files[path] = Array.Empty(); + } + else if (mode == FileMode.CreateNew) + { + throw new IOException($"File {path} already exists."); + } + else if (mode == FileMode.Create) + { + Files[path] = Array.Empty(); + } + else if (mode == FileMode.Truncate) + { + Files[path] = Array.Empty(); + } + var bytes = Files[path]; + var stream = new InMemoryFileStream(this, path, bytes); + if (mode == FileMode.Append) + stream.Seek(0, SeekOrigin.End); + return stream; + } + } + + public bool FileExists(string path) + { + lock (this) return Files.ContainsKey(path); + } + + public bool DirectoryExists(string path) + { + lock (this) return Directories.Contains(path); + } + + public void CreateDirectory(string path) + { + lock (this) Directories.Add(path); + } + + public void DeleteFile(string path) + { + lock (this) Files.Remove(path); + } + + public void DeleteDirectory(string path, bool recursive) + { + lock (this) + { + Directories.Remove(path); + if (recursive) + { + var toRemove = Files.Keys.Where(x => x.StartsWith(path)).ToList(); + foreach (var f in toRemove) + Files.Remove(f); + } + } + } + + public string ReadAllText(string path) + { + lock (this) return Encoding.UTF8.GetString(Files[path]); + } + + public byte[] ReadAllBytes(string path) + { + lock (this) + { + var b = Files[path]; + var copy = new byte[b.Length]; + Buffer.BlockCopy(b, 0, copy, 0, b.Length); + return copy; + } + } + + public void Replace( + string sourceFileName, + string destinationFileName, + string destinationBackupFileName) + { + lock (this) + { + if (destinationBackupFileName != null && Files.ContainsKey(destinationFileName)) + { + Files[destinationBackupFileName] = Files[destinationFileName]; + } + Files[destinationFileName] = Files.ContainsKey(sourceFileName) ? Files[sourceFileName] : Array.Empty(); + Files.Remove(sourceFileName); + } + } + + public DurableFileWriter GetDurableFileWriter() + { + return new DurableFileWriter(this); + } + + public IReadOnlyList GetDirectories(string path) + { + lock (this) + { + return Directories.Where(x => x.StartsWith(path)).ToArray(); + } + } + + public string CombinePaths(string path1, string path2) + { + return Path.Combine(path1, path2); + } + + internal void UpdateFile(string path, byte[] data) + { + lock (this) + { + Files[path] = data; + } + } +} diff --git a/src/ZoneTree/docs/ZoneTree/guide/quick-start.md b/src/ZoneTree/docs/ZoneTree/guide/quick-start.md index e424c9c..c3569e0 100644 --- a/src/ZoneTree/docs/ZoneTree/guide/quick-start.md +++ b/src/ZoneTree/docs/ZoneTree/guide/quick-start.md @@ -162,4 +162,14 @@ while(iterator.Next()) { var key = iterator.CurrentKey; var value = iterator.CurrentValue; } -``` \ No newline at end of file +``` +## Using the InMemoryFileStreamProvider + +The `InMemoryFileStreamProvider` keeps all files entirely in memory. It is useful for unit testing or scenarios that require fast, temporary storage without touching the disk. + +```csharp +var provider = new InMemoryFileStreamProvider(); +using var zoneTree = new ZoneTreeFactory(provider) + .OpenOrCreate(); +zoneTree.Upsert(1, "value"); +```