Skip to content
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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,24 @@ When links do not have names, exported references are plain link numbers:
(2: 1 2)
```

## Persistent transformation triggers

Store a query as a trigger with `--always` to apply it after later write operations:

```bash
clink --db graph.links --always '(((1: 1 1)) ((1: 1 2)))'
clink --db graph.links --auto-create-missing-references '() ((1: 1 1))' --after
```

Use `--once` for a trigger that deletes itself after the first successful application, and `--never` to remove matching stored triggers:

```bash
clink --db graph.links --once '(((1: 1 1)) ((1: 1 2)))'
clink --db graph.links --never '(((1: 1 1)) ((1: 1 2)))'
```

Triggers are stored as binary links using the structure `(Always ((Condition ...) (Substitution ...)))` or `(Once ((Condition ...) (Substitution ...)))`. By default they are kept in a companion `<database-name>.triggers.links` file, such as `graph.triggers.links` for `graph.links`. Use `--triggers-file path/to/triggers.links` to choose a different companion file, `--triggers` to enable trigger evaluation explicitly, or `--embed-triggers` to store trigger links in the main database file.

## Update single link

Update link with index 1 and source 1 and target 1, changing target to 2.
Expand Down Expand Up @@ -320,6 +338,12 @@ clink '((1: 2 1) (2: 1 2)) ()' --changes --after
| `--changes` | bool | `false` | `-c` | Print the changes applied by the query |
| `--after` | bool | `false` | `--links`, `-a` | Print the state of the database after applying changes |
| `--out` | string | _None_ | `--export`, `--lino-output` | Write the complete database as a LiNo file |
| `--always` | bool | `false` | _None_ | Store the query as an always-on persistent transformation trigger |
| `--once` | bool | `false` | _None_ | Store the query as a one-shot persistent transformation trigger |
| `--never` | bool | `false` | _None_ | Remove stored persistent transformation triggers matching the query |
| `--triggers` | bool | `false` | _None_ | Enable persistent transformation triggers for the command |
| `--triggers-file` | string | `<db>.triggers.links` | _None_ | Path to the persistent transformation trigger links database |
| `--embed-triggers` | bool | `false` | _None_ | Store persistent transformation triggers in the main links database |

## For developers and debugging

Expand Down
5 changes: 5 additions & 0 deletions csharp/.changeset/add-persistent-transformation-triggers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'Foundation.Data.Doublets.Cli': minor
---

Added binary links-backed persistent transformation triggers with `--always`, `--once`, `--never`, `--triggers-file`, and `--embed-triggers`.
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,44 @@ await File.WriteAllLinesAsync(inputPath, new[]
}
}

[Fact]
public async Task AlwaysTriggerOption_StoresTriggerAndAppliesItOnLaterChange()
{
var tempDirectory = CreateTempDirectory();

try
{
var dbPath = Path.Combine(tempDirectory, "triggered.links");
var triggersPath = Path.Combine(tempDirectory, "triggers.links");
var outputPath = Path.Combine(tempDirectory, "triggered.lino");

AssertClinkSucceeded(await RunClinkAsync(
"--db",
dbPath,
"--triggers-file",
triggersPath,
"--always",
"(((1: 1 1)) ((1: 1 2)))"));

var result = await RunClinkAsync(
"--db",
dbPath,
"--triggers-file",
triggersPath,
"--auto-create-missing-references",
"() ((1: 1 1))",
"--export",
outputPath);

AssertClinkSucceeded(result);
Assert.Equal(new[] { "(1: 1 2)", "(2: 2 2)" }, File.ReadAllLines(outputPath));
}
finally
{
Directory.Delete(tempDirectory, recursive: true);
}
}

private static async Task<CommandResult> RunClinkAsync(params string[] clinkArguments)
{
var csharpDirectory = FindCsharpDirectory();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using Platform.Data;
using Platform.Data.Doublets;

using DoubletLink = Platform.Data.Doublets.Link<uint>;

namespace Foundation.Data.Doublets.Cli.Tests.Tests
{
public class PersistentTransformationDecoratorTests
{
[Fact]
public void AlwaysTriggerIsStoredInLinksAndAppliedAfterWrite()
{
RunWithPersistentLinks((links, triggerLinks) =>
{
links.StoreTrigger(PersistentTransformationKind.Always, "(((1: 1 1)) ((1: 1 2)))");

var allTriggerLinks = AllLinks(triggerLinks);
var alwaysId = triggerLinks.GetByName("Always");
Assert.NotEqual(triggerLinks.Constants.Null, alwaysId);
Assert.Contains(allTriggerLinks, link => link.Source == alwaysId && link.Target != alwaysId);

Foundation.Data.Doublets.Cli.AdvancedMixedQueryProcessor.ProcessQuery(links, new Foundation.Data.Doublets.Cli.AdvancedMixedQueryProcessor.Options
{
Query = "(() ((1: 1 1)))",
AutoCreateMissingReferences = true
});

Assert.Contains(AllLinks(links), link => link.Index == 1 && link.Source == 1 && link.Target == 2);
});
}

[Fact]
public void OnceTriggerDeletesItselfAfterFirstMatch()
{
RunWithPersistentLinks((links, triggerLinks) =>
{
links.StoreTrigger(PersistentTransformationKind.Once, "(((1: 1 1)) ((1: 1 2)))");

Foundation.Data.Doublets.Cli.AdvancedMixedQueryProcessor.ProcessQuery(links, new Foundation.Data.Doublets.Cli.AdvancedMixedQueryProcessor.Options
{
Query = "(() ((1: 1 1)))",
AutoCreateMissingReferences = true
});

Assert.DoesNotContain(links.GetTriggers(), trigger => trigger.Kind == PersistentTransformationKind.Once);

Foundation.Data.Doublets.Cli.AdvancedMixedQueryProcessor.ProcessQuery(links, new Foundation.Data.Doublets.Cli.AdvancedMixedQueryProcessor.Options
{
Query = "(((1: 1 2)) ((1: 1 1)))",
AutoCreateMissingReferences = true
});

Assert.Contains(AllLinks(links), link => link.Index == 1 && link.Source == 1 && link.Target == 1);
});
}

[Fact]
public void NeverRemovesMatchingStoredTrigger()
{
RunWithPersistentLinks((links, triggerLinks) =>
{
links.StoreTrigger(PersistentTransformationKind.Always, "(((1: 1 1)) ((1: 1 2)))");

var removed = links.RemoveTriggers("(((1: 1 1)) ((1: 1 2)))");

Assert.Equal(1, removed);
Assert.Empty(links.GetTriggers());
});
}

private static void RunWithPersistentLinks(Action<PersistentTransformationDecorator, NamedTypesDecorator<uint>> action)
{
var dataFile = Path.GetTempFileName();
var triggerFile = Path.GetTempFileName();
NamedTypesDecorator<uint>? dataLinks = null;
NamedTypesDecorator<uint>? triggerLinks = null;
try
{
dataLinks = new NamedTypesDecorator<uint>(dataFile);
triggerLinks = new NamedTypesDecorator<uint>(triggerFile);
var links = new PersistentTransformationDecorator(dataLinks, triggerLinks)
{
AutoCreateMissingReferences = true
};

action(links, triggerLinks);
}
finally
{
DeleteIfExists(dataFile);
DeleteIfExists(triggerFile);
if (dataLinks is not null)
{
DeleteIfExists(dataLinks.NamedLinksDatabaseFileName);
}
if (triggerLinks is not null)
{
DeleteIfExists(triggerLinks.NamedLinksDatabaseFileName);
}
}
}

private static List<DoubletLink> AllLinks(INamedTypesLinks<uint> links)
{
var any = links.Constants.Any;
return links.All(new DoubletLink(any, any, any)).Select(link => new DoubletLink(link)).ToList();
}

private static void DeleteIfExists(string path)
{
if (File.Exists(path))
{
File.Delete(path);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,20 @@ public void NameIsRemovedWhenExternalReferenceIsDeletedTest()
});
}

[Fact]
public void ExternalReferenceCanUseNameThatAlreadyExistsInternallyTest()
{
RunTestWithLinks(links =>
{
var storage = new UnicodeStringStorage<uint>(links);
var externalRef = links.Create(new uint[] { 0, 0 });

storage.NamedLinks.SetNameForExternalReference(externalRef, "Type");

Assert.Equal(externalRef, storage.NamedLinks.GetExternalReferenceByName("Type"));
});
}

// Helper method to create a test environment with a temporary file
private static void RunTestWithLinks(Action<ILinks<uint>> testAction)
{
Expand All @@ -242,4 +256,4 @@ private static void RunTestWithLinks(Action<ILinks<uint>> testAction)
}
}
}
}
}
20 changes: 15 additions & 5 deletions csharp/Foundation.Data.Doublets.Cli/NamedLinks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,25 @@ public TLinkAddress GetByName(string name)

public TLinkAddress GetExternalReferenceByName(string name)
{
var reference = (Hybrid<TLinkAddress>)GetByName(name);
if (reference.IsExternal)
var nameSequence = _createString(name);
var nameLink = _links.SearchOrDefault(_nameType, nameSequence);
if (nameLink.Equals(_links.Constants.Null))
{
return TLinkAddress.CreateTruncating(reference.AbsoluteValue);
return _links.Constants.Null;
}
else

var any = _links.Constants.Any;
var query = new Link<TLinkAddress>(any, any, nameLink);
foreach (var link in _links.All(query))
{
return _links.Constants.Null;
var reference = (Hybrid<TLinkAddress>)_links.GetSource(link);
if (reference.IsExternal)
{
return TLinkAddress.CreateTruncating(reference.AbsoluteValue);
}
}

return _links.Constants.Null;
}

public void RemoveName(TLinkAddress link)
Expand Down
Loading
Loading