Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
82d361a
Initial commit with task details
konard May 20, 2026
0eeab48
docs(issue-94): add case study for transactions and version-control l…
konard May 20, 2026
2b95c78
Revert "Initial commit with task details"
konard May 20, 2026
ccc054f
feat(csharp): add TransactionsDecorator with reversible transitions log
konard May 20, 2026
08fcdec
test(csharp): add TransactionsDecorator tests; fix multi-callback han…
konard May 20, 2026
8cacf3b
feat(csharp): add VersionControlDecorator for time travel and branching
konard May 20, 2026
8846f8d
feat(csharp): add CLI flags for transactions and version-control layers
konard May 20, 2026
3ce75bb
feat(rust): add TransactionsDecorator with reversible transitions log
konard May 20, 2026
f65ef81
feat(rust): add VersionControlDecorator for time travel and branching
konard May 20, 2026
2847255
feat(rust): wire CLI flags through transactions and version-control d…
konard May 20, 2026
19ada23
test(rust): add CLI integration tests for transactions and version-co…
konard May 20, 2026
7c646a0
docs(examples): add runnable transactions and version-control demos
konard May 20, 2026
330a012
docs(issue-94): document optional transactions and version-control la…
konard May 20, 2026
b9ca937
style(rust): reformat cli_transactions_and_vc_tests with cargo fmt
konard May 20, 2026
7e9698b
chore(issue-94): add changeset and changelog fragment for transaction…
konard May 20, 2026
e55659d
refactor(rust): extract transactions value types into submodule
konard May 20, 2026
08a5ba7
fix(issue-94): verify full-stack transaction ACID behavior
konard May 20, 2026
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ package built from `doublets-rs`.
- [docs/HOW-IT-WORKS.md](docs/HOW-IT-WORKS.md): deeper explanation of query processing, references, import/export, triggers, and the WebAssembly workbench.
- [docs/case-studies/issue-71/README.md](docs/case-studies/issue-71/README.md): evidence and analysis behind the original documentation refresh.
- [docs/case-studies/issue-92/README.md](docs/case-studies/issue-92/README.md): evidence and analysis behind the dual CLI + library packaging and unified API documentation site.
- [docs/case-studies/issue-94/README.md](docs/case-studies/issue-94/README.md): evidence and analysis for the optional transactions and version-control layers.

### API references

Expand Down
16 changes: 16 additions & 0 deletions csharp/.changeset/issue-94-transactions-and-version-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'Foundation.Data.Doublets.Cli': minor
---

Added optional transactions and version-control layers (issue #94). The
new `TransactionsDecorator` records each Create/Update/Delete as a
reversible transition in a sidecar doublets store and exposes
`BeginTransaction()` / `Commit()` / `Rollback()` plus three retention
policies (`infinite`, `sized:<n>`, `chunked:<n>:<dir>`) and two commit
modes (`sync`, `async`). The new `VersionControlDecorator` adds
branching, tagging, and time-travel checkout over that log. The CLI
surfaces both layers through `--transactions`, `--transactions-file`,
`--commit-mode`, `--retention`, `--log`, `--vc`, `--vc-file`,
`--branch`, `--branch-from`, `--checkout`, `--tag`, `--list-branches`,
and `--list-tags`. When no flag is passed, behaviour is byte-identical
to the existing CLI — no sidecar is written and no extra cost is paid.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using System.Reflection;
using Platform.Delegates;
using Platform.Memory;
using Platform.Data;
Expand All @@ -12,7 +13,7 @@

namespace Foundation.Data.Doublets.Cli
{
public class NamedTypesDecorator<TLinkAddress> : LinksDecoratorBase<TLinkAddress>, INamedTypesLinks<TLinkAddress>, IPinnedTypes<TLinkAddress>
public class NamedTypesDecorator<TLinkAddress> : LinksDecoratorBase<TLinkAddress>, INamedTypesLinks<TLinkAddress>, IPinnedTypes<TLinkAddress>, IDisposable
where TLinkAddress : struct,
IUnsignedNumber<TLinkAddress>,
IComparisonOperators<TLinkAddress, TLinkAddress, bool>,
Expand All @@ -24,6 +25,8 @@ public class NamedTypesDecorator<TLinkAddress> : LinksDecoratorBase<TLinkAddress
public readonly PinnedTypesDecorator<TLinkAddress> PinnedTypesDecorator;
public readonly NamedLinks<TLinkAddress> NamedLinks;
public readonly string NamedLinksDatabaseFileName;
private readonly ILinks<TLinkAddress> _namedLinksFacade;
private bool _disposed;

public static ILinks<TLinkAddress> MakeLinks(string databaseFilename)
{
Expand Down Expand Up @@ -53,6 +56,7 @@ public NamedTypesDecorator(PinnedTypesDecorator<TLinkAddress> pinnedTypesDecorat
var namesMemory = new FileMappedResizableDirectMemory(namesDatabaseFilename, UnitedMemoryLinks<TLinkAddress>.DefaultLinksSizeStep);
var namesLinks = new UnitedMemoryLinks<TLinkAddress>(namesMemory, UnitedMemoryLinks<TLinkAddress>.DefaultLinksSizeStep, namesConstants, IndexTreeType.Default);
var decoratedNamesLinks = namesLinks.DecorateWithAutomaticUniquenessAndUsagesResolution();
_namedLinksFacade = decoratedNamesLinks;
NamedLinks = new UnicodeStringStorage<TLinkAddress>(decoratedNamesLinks).NamedLinks;
NamedLinksDatabaseFileName = namesDatabaseFilename;
}
Expand All @@ -62,6 +66,52 @@ public NamedTypesDecorator(string databaseFilename, bool tracingEnabled = false)
{
}

public void Dispose()
{
if (_disposed) return;
_disposed = true;
DisposeLinksFacade(_namedLinksFacade);
DisposeLinksFacade(PinnedTypesDecorator);
}

private static void DisposeLinksFacade(object? facade)
{
var visited = new HashSet<object>(ReferenceEqualityComparer.Instance);
DisposeLinksFacade(facade, visited);
}

private static void DisposeLinksFacade(object? facade, HashSet<object> visited)
{
if (facade is null || !visited.Add(facade))
{
return;
}

foreach (var inner in EnumerateInnerLinks(facade))
{
DisposeLinksFacade(inner, visited);
}

if (facade is IDisposable disposable)
{
disposable.Dispose();
}
}

private static IEnumerable<object?> EnumerateInnerLinks(object facade)
{
for (var type = facade.GetType(); type is not null; type = type.BaseType)
{
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
{
if (typeof(ILinks<TLinkAddress>).IsAssignableFrom(field.FieldType))
{
yield return field.GetValue(facade);
}
}
}
}

public IEnumerator<TLinkAddress> GetEnumerator()
{
return PinnedTypesDecorator.GetEnumerator();
Expand Down
Loading
Loading