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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ clink '((1: 2 1) (2: 1 2)) ()' --changes --after
| `--query` | string | _None_ | `--apply`, `--do`, `-q` | LiNo query for CRUD operation |
| `query` (positional) | string | _None_ | _N/A_ | LiNo query for CRUD operation (provided as the first positional argument) |
| `--trace` | bool | `false` | `-t` | Enable trace (verbose output) |
| `--auto-create-missing-references` | bool | `false` | _None_ | Create missing numeric and named references as self-referential point links |
| `--structure` | uint? | _None_ | `-s` | ID of the link to format its structure |
| `--before` | bool | `false` | `-b` | Print the state of the database before applying changes |
| `--changes` | bool | `false` | `-c` | Print the changes applied by the query |
Expand Down
5 changes: 5 additions & 0 deletions csharp/.changeset/validate-missing-references.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'Foundation.Data.Doublets.Cli': minor
---

Added strict validation for missing numeric and named link references, plus `--auto-create-missing-references` to create missing references as self-referential point links.
Original file line number Diff line number Diff line change
Expand Up @@ -374,15 +374,16 @@ public void NoExactMatch2LevelNestedLinksTest()
RunTestWithLinks(links =>
{
// Arrange
ProcessQuery(links, "() ((1: 1 1))");
ProcessQueryStrict(links, "() ((1: 1 1) (2: 2 2))");

// Act
ProcessQuery(links, "((1: (1: 1 1) (1: 2 1))) ()");
ProcessQueryStrict(links, "((1: (1: 1 1) (1: 2 1))) ()");

// Assert
var allLinks = GetAllLinks(links);
Assert.Single(allLinks);
Assert.Equal(2, allLinks.Count);
AssertLinkExists(allLinks, 1, 1, 1);
AssertLinkExists(allLinks, 2, 2, 2);
});
}

Expand Down Expand Up @@ -1544,6 +1545,28 @@ private static List<DoubletLink> GetAllLinks(NamedLinksDecorator<uint> links)
return allLinks;
}

private static void ProcessQuery(NamedLinksDecorator<uint> links, string query)
{
ProcessQuery(links, new Options { Query = query });
}

private static void ProcessQuery(NamedLinksDecorator<uint> links, Options options)
{
options.AutoCreateMissingReferences = true;
Foundation.Data.Doublets.Cli.AdvancedMixedQueryProcessor.ProcessQuery(links, options);
}

private static void ProcessQueryStrict(NamedLinksDecorator<uint> links, string query)
{
ProcessQueryStrict(links, new Options { Query = query });
}

private static void ProcessQueryStrict(NamedLinksDecorator<uint> links, Options options)
{
options.AutoCreateMissingReferences = false;
Foundation.Data.Doublets.Cli.AdvancedMixedQueryProcessor.ProcessQuery(links, options);
}

private static void AssertLinkExists(List<DoubletLink> allLinks, uint index, uint source, uint target)
{
var link = new DoubletLink(index, source, target);
Expand All @@ -1554,5 +1577,167 @@ private static void AssertChangeExists(List<(DoubletLink, DoubletLink)> changes,
{
Assert.Contains(changes, change => change.Item1 == linkBefore && change.Item2 == linkAfter);
}

// New tests for link reference validation

[Fact]
public void CreateLinkWithNonExistentReference_ShouldThrowException()
{
RunTestWithLinks(links =>
{
// Act & Assert - should throw exception for referencing non-existent link 10
var exception = Assert.Throws<InvalidOperationException>(() =>
{
ProcessQueryStrict(links, "(() ((1: 10 20)))");
});

Assert.Contains("Invalid reference to non-existent link '10'", exception.Message);
Assert.Contains("--auto-create-missing-references", exception.Message);
});
}

[Fact]
public void CreateLinkWithValidSelfReference_ShouldSucceed()
{
RunTestWithLinks(links =>
{
// Act - should succeed because link 1 references itself
ProcessQueryStrict(links, "(() ((1: 1 1)))");

// Assert
var allLinks = GetAllLinks(links);
Assert.Single(allLinks);
AssertLinkExists(allLinks, 1, 1, 1);
});
}

[Fact]
public void CreateMultipleLinksWithCrossReferences_ShouldSucceed()
{
RunTestWithLinks(links =>
{
// Act - should succeed because both links are created in the same operation
ProcessQueryStrict(links, "(() ((1: 1 2) (2: 2 1)))");

// Assert
var allLinks = GetAllLinks(links);
Assert.Equal(2, allLinks.Count);
AssertLinkExists(allLinks, 1, 1, 2);
AssertLinkExists(allLinks, 2, 2, 1);
});
}

[Fact]
public void CreateLinkReferencingExistingLink_ShouldSucceed()
{
RunTestWithLinks(links =>
{
// Arrange - create first link
ProcessQueryStrict(links, "(() ((1: 1 1)))");

// Act - should succeed because link 1 exists
ProcessQueryStrict(links, "(() ((2: 2 1)))");

// Assert
var allLinks = GetAllLinks(links);
Assert.Equal(2, allLinks.Count);
AssertLinkExists(allLinks, 1, 1, 1);
AssertLinkExists(allLinks, 2, 2, 1);
});
}

[Fact]
public void UpdateWithNonExistentReference_ShouldThrowException()
{
RunTestWithLinks(links =>
{
// Arrange - create initial link
ProcessQueryStrict(links, "(() ((1: 1 1)))");

// Act & Assert - should throw exception for referencing non-existent link 99
var exception = Assert.Throws<InvalidOperationException>(() =>
{
ProcessQueryStrict(links, "(((1: 1 1)) ((1: 1 99)))");
});

Assert.Contains("Invalid reference to non-existent link '99'", exception.Message);
});
}

[Fact]
public void CreateNamedLinkWithMissingNamedReferences_ShouldThrowException()
{
RunTestWithLinks(links =>
{
var exception = Assert.Throws<InvalidOperationException>(() =>
{
ProcessQueryStrict(links, "(() ((child: father mother)))");
});

Assert.Contains("Invalid reference to non-existent link 'father'", exception.Message);
Assert.Contains("--auto-create-missing-references", exception.Message);
});
}

[Fact]
public void CreateLinkWithAutoCreateMissingNumericReferences_ShouldCreatePointLinks()
{
RunTestWithLinks(links =>
{
ProcessQuery(links, "(() ((20: 10 20)))");

var allLinks = GetAllLinks(links);
Assert.Equal(2, allLinks.Count);
AssertLinkExists(allLinks, 10, 10, 10);
AssertLinkExists(allLinks, 20, 10, 20);
});
}

[Fact]
public void CreateNamedLinkWithAutoCreateMissingNamedReferences_ShouldCreatePointLinks()
{
RunTestWithLinks(links =>
{
ProcessQuery(links, "(() ((child: father mother)))");

var fatherId = links.GetByName("father");
var motherId = links.GetByName("mother");
var childId = links.GetByName("child");

var allLinks = GetAllLinks(links);
Assert.Equal(3, allLinks.Count);
AssertLinkExists(allLinks, fatherId, fatherId, fatherId);
AssertLinkExists(allLinks, motherId, motherId, motherId);
AssertLinkExists(allLinks, childId, fatherId, motherId);
});
}

[Fact]
public void CreateLinkWithVariableReferences_ShouldSucceed()
{
RunTestWithLinks(links =>
{
// Act - should succeed because variables are not validated
ProcessQueryStrict(links, "(() (($link: $source $target)))");

// Assert - one link should be created with variables resolved
var allLinks = GetAllLinks(links);
Assert.Single(allLinks);
});
}

[Fact]
public void CreateLinkWithWildcardReferences_ShouldSucceed()
{
RunTestWithLinks(links =>
{
// Act - should succeed because wildcards are not validated
ProcessQueryStrict(links, "(() ((1: * *)))");

// Assert
var allLinks = GetAllLinks(links);
Assert.Single(allLinks);
});
}
}
}
}
Loading
Loading