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

Rework attributes to be instance based #38

Merged
merged 5 commits into from
Apr 10, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.MnemonicDB.TestModel.ComplexModel.ReadModels;
using NexusMods.Hashing.xxHash64;
using NexusMods.MnemonicDB.TestModel;
using NexusMods.Paths;

// ReSharper disable MemberCanBePrivate.Global
Expand All @@ -19,7 +19,7 @@ public class ReadTests : ABenchmark
private IDb _db = null!;
private EntityId[] _entityIds = null!;
private EntityId _readId;
private File[] _preLoaded = null!;
private File.Model[] _preLoaded = null!;
private EntityId _modId;

[Params(1, 1000, MaxCount)] public int Count { get; set; } = MaxCount;
Expand All @@ -31,7 +31,7 @@ public async Task Setup()
var tx = Connection.BeginTransaction();
var entityIds = new List<EntityId>();

var tmpMod = new Mod(tx)
var tmpMod = new Mod.Model(tx)
{
Name = "TestMod",
Source = new Uri("https://www.nexusmods.com"),
Expand All @@ -40,7 +40,7 @@ public async Task Setup()

for (var i = 0; i < Count; i++)
{
var file = new File(tx)
var file = new File.Model(tx)
{
Hash = Hash.From((ulong)i),
Path = $"C:\\test_{i}.txt",
Expand All @@ -60,14 +60,14 @@ public async Task Setup()

_db = Connection.Db;

_preLoaded = _db.Get<File>(_entityIds).ToArray();
_preLoaded = _db.Get<File.Model>(_entityIds).ToArray();
}

[Benchmark]
public ulong ReadFiles()
{
ulong sum = 0;
sum += _db.Get<File>(_readId).Size.Value;
sum += _db.Get<File.Model>(_readId).Size.Value;
return sum;
}

Expand All @@ -80,7 +80,7 @@ public ulong ReadProperty()
[Benchmark]
public long ReadAll()
{
return _db.Get<File>(_entityIds)
return _db.Get<File.Model>(_entityIds)
.Sum(e => (long)e.Size.Value);
}

Expand All @@ -94,7 +94,7 @@ public long ReadAllPreloaded()
[Benchmark]
public ulong ReadAllFromMod()
{
var mod = _db.Get<Mod>(_modId);
var mod = _db.Get<Mod.Model>(_modId);
ulong sum = 0;
for (var i = 0; i < mod.Files.Count; i++)
{
Expand Down
13 changes: 2 additions & 11 deletions benchmarks/OneBillionDatomsTest/Program.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NexusMods.MnemonicDB;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.MnemonicDB.Abstractions.DatomIterators;
using NexusMods.MnemonicDB.Storage;
using NexusMods.MnemonicDB.TestModel;
using NexusMods.MnemonicDB.TestModel.ComplexModel.Attributes;
using NexusMods.MnemonicDB.TestModel.ComplexModel.ReadModels;
using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;

Expand All @@ -38,9 +32,6 @@

var services = host.Services;

var store = services.GetRequiredService<IDatomStore>();
await store.Sync();

var connection = services.GetRequiredService<IConnection>();

ulong batchSize = 1024;
Expand All @@ -59,12 +50,12 @@

for (ulong i = 0; i < batches; i++)
{
var tx = connection.BeginTransaction();
using var tx = connection.BeginTransaction();

for (var j = 0; j < (int)batchSize; j++)
{
fileNumber += 1;
var _ = new File(tx)
var _ = new File.Model(tx)
{
Path = $"c:\\test_{i}_{j}.txt",
Hash = Hash.From(fileNumber % 0xFFFF),
Expand Down
108 changes: 41 additions & 67 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,92 +6,69 @@ hide:
## Getting Started

Good examples are often worth a thousand descriptions so let's start with a simple example. First of all we need to define a set
of attributes that we want to collect into a model. These attributes have a type and are defined in code as a class. These classes
inherit all their logic from predefined abstract classes so their definitions are simple. Each attribute is backed by a "symbol"
which is a value type that contains a unique string that is the name of the attribute. This symbol is used to identify the attribute
uniquely in the database. By default the symbol is detected by the name and namespace of the class. So let's define a set
attributes for files and mods that will contain these files:
of attributes that we want to collect into a model. These attributes will be instances of the `Attribute<T>` type, and can be
annotated with various parameters to describe the attribute. Each attribute must have a unique name in the format of
`name.space/name`. This name is used inside the database to link the `AttributeId` to a unique name that persists across
database restarts. These attributes are commonly stored in a static class (being static is recommended so that the class isn't
accidentally instantiated).


```csharp
public static class FileAttributes
public static class File
{
public class Hash : ScalarAttribute<Hash, ulong>;
public class Size : ScalarAttribute<Size, ulong>;
public class Name : ScalarAttribute<Name, string>;
public class ModId : ScalarAttribute<ModId, EntityId>;
}

public static class ModAttributes
{
public class Name : ScalarAttribute<Name, string>;
public class Enabled : ScalarAttribute<Enabled, bool>;
public static readonly Attribute<ulong> Hash = new("Test.Model.File/Hash", isIndexed: true);
public static readonly Attribute<ulong> Size = new("Test.Model.File/Size");
public static readonly Attribute<string> Name = new("Test.Model.File/Name", noHistory: true);
public static readonly Attribute<EntityId> ModId = new"Test.Model.File/ModId", cardinality: Cardinality.Many);

public class Model(ITransaction tx) : AEntity(tx)
{
public ulong Hash {
get => File.Hash.Get(this);
set => File.Hash.Set(this, value);

}

public ModId ModId {
get => File.ModId.Get(this);
set => File.ModId.Set(this, value);
}

public Mod.Model Mod {
get => Db.Get<Mod.Model>(ModId);
set => ModId = value.Id;
}
}
}
```

!!!info
Putting all attributes as child classes inside a static class is a convention, not a requirement. They are put this way
in this example so that it's clear that `Name` on a file is different from `Name` on a mod. Although, it's possible to put
the same attribute on multiple entities, this is not recommended as it removes the ability to quickly query for all files
based purely on a single attribute. Don't over generalize your attributes, it's better to have a few more attributes than
to make the model too complex.
The attributes are defined in the outer class level as they are commonly used throughout the system. The `Model` class is a
loose collection of attributes grouped into a common projection (or entity). There's no requirement that the attributes
must be grouped together with themselves, and it's recommended to mix and match attributes from different definitions where
appropriate. Attribute definition requires a bit of design thought, as additional performance can be gained by having more
attributes to partition the data being queried, but at the same time, too many attributes can result in queries needing
to concatenate results from multiple attributes.

So now that we have attributes we have to register them in the DI container. Currently we have to register these one by one,
but in the future we could easily register all attributes on a given static class.
Attributes must also be registered in the DI container. This is done by calling `.AddAttributeCollection(typeof(AttrCollection))`.
This method will scan the class for all static attribute definitions and register them with the DI container.

```csharp
public static IServiceCollection AddAttributes(this IServiceCollection services)
{
services.AddAttribute<FileAttributes.Hash>();
services.AddAttribute<FileAttributes.Size>();
services.AddAttribute<FileAttributes.Name>();
services.AddAttribute<FileAttributes.ModId>();
services.AddAttribute<ModAttributes.Name>();
services.AddAttribute<ModAttributes.Enabled>();
services.AddAttributeCollection(typeof(File));
return services;
}
```

While we could go and insert datoms now, the interface is verbose and not very user friendly, instead we will now group
these attributes together into a "read model". Here is a simple example of a read model:

```csharp
public class File(ITransaction? tx) : AReadModel<File>(tx)
{
[From<FileAttributes.Hash>]
public required ulong Hash { get; init; }

[From<FileAttributes.Size>]
public required ulong Size { get; init; }

[From<FileAttributes.Name>]
public required string Name { get; init; }

[From<FileAttributes.ModId>]
public required EntityId ModId { get; init; }
}

public class Mod(ITransaction? tx) : AReadModel<Mod>(tx)
{
[From<ModAttributes.Name>]
public required string Name { get; set; }

[From<ModAttributes.Enabled>]
public required bool Enabled { get; set; }
}
```

From here we can create a connection and insert the datoms. The connection is the central mutation point for the database,
and it's most often injected via the DI framework:

```
IConnection connection = serviceProvider.GetRequiredService<IConnection>();

var tx = connection.BeginTransaction();
using var tx = connection.BeginTransaction();

var mod = new Mod(tx) { Name = "My Mod", Enabled = true };

var file = new File(tx) { Hash = 123, Size = 456, Name = "My File", ModId = mod.Id };
var file = new File(tx) { Hash = 123, Size = 456, Name = "My File", Mod = mod };

var result = await tx.Commit();
```
Expand All @@ -109,10 +86,7 @@ file we just created:

var db = connection.Db;

mod = db.Get<Mod>(result[mod.Id]);
mod.Name.Should().Be("My Mod");

file = db.Get<File>(result[file.Id]);
file = db.Get<File.Model>(result[file.Id]);
file.Name.Should().Be("My File");
file.ModId.Should().Be(mod.Id);
```
Expand Down
Loading
Loading