Skip to content
Open
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
113 changes: 113 additions & 0 deletions Foundation.Data.Doublets.Cli.Tests/AdvancedMixedQueryProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1308,5 +1308,118 @@ 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>(() =>
{
ProcessQuery(links, "(() ((1: 10 20)))");
});

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

[Fact]
public void CreateLinkWithValidSelfReference_ShouldSucceed()
{
RunTestWithLinks(links =>
{
// Act - should succeed because link 1 references itself
ProcessQuery(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
ProcessQuery(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
ProcessQuery(links, "(() ((1: 1 1)))");

// Act - should succeed because link 1 exists
ProcessQuery(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
ProcessQuery(links, "(() ((1: 1 1)))");

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

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

[Fact]
public void CreateLinkWithVariableReferences_ShouldSucceed()
{
RunTestWithLinks(links =>
{
// Act - should succeed because variables are not validated
ProcessQuery(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
ProcessQuery(links, "(() ((1: * *)))");

// Assert
var allLinks = GetAllLinks(links);
Assert.Single(allLinks);
});
}
}
}
152 changes: 152 additions & 0 deletions Foundation.Data.Doublets.Cli/AdvancedMixedQueryProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Platform.Delegates;
using Platform.Data;
using Platform.Data.Doublets;
Expand Down Expand Up @@ -69,6 +70,19 @@ public static void ProcessQuery(NamedLinksDecorator<uint> links, Options options
if (restrictionLink.Values?.Count == 0 && (substitutionLink.Values?.Count ?? 0) > 0)
{
TraceIfEnabled(options, "[ProcessQuery] No restriction, but substitution is non-empty => creation scenario.");

// VALIDATION: Validate that all references in creation scenario are valid
try
{
var emptyRestrictionPatterns = new List<LinoLink>();
ValidateLinksExistOrWillBeCreated(links, emptyRestrictionPatterns, substitutionLink.Values ?? new List<LinoLink>(), options);
}
catch (Exception ex)
{
TraceIfEnabled(options, $"[ProcessQuery] Creation validation failed: {ex.Message}");
throw;
}

foreach (var linkToCreate in substitutionLink.Values ?? new List<LinoLink>())
{
var createdId = EnsureNestedLinkCreatedRecursively(links, linkToCreate, options);
Expand All @@ -84,6 +98,17 @@ public static void ProcessQuery(NamedLinksDecorator<uint> links, Options options
TraceIfEnabled(options, $"[ProcessQuery] Restriction patterns to parse: {restrictionPatterns.Count}");
TraceIfEnabled(options, $"[ProcessQuery] Substitution patterns to parse: {substitutionPatterns.Count}");

// VALIDATION: Check that all referenced links exist or will be created
try
{
ValidateLinksExistOrWillBeCreated(links, restrictionPatterns, substitutionPatterns, options);
}
catch (Exception ex)
{
TraceIfEnabled(options, $"[ProcessQuery] Validation failed: {ex.Message}");
throw;
}

var restrictionInternalPatterns = restrictionPatterns
.Select(l => CreatePatternFromLino(l))
.ToList();
Expand Down Expand Up @@ -1205,5 +1230,132 @@ private static uint CreateCompositeLink(
}
return compositeLinkId;
}

/// <summary>
/// Validates that all link references in the patterns either exist in the database
/// or will be created as part of the current operation.
/// </summary>
private static void ValidateLinksExistOrWillBeCreated(
NamedLinksDecorator<uint> links,
IList<LinoLink> restrictionPatterns,
IList<LinoLink> substitutionPatterns,
Options options)
{
TraceIfEnabled(options, "[ValidateLinksExistOrWillBeCreated] Starting validation");

// Collect all link IDs that will be created in this operation
var linkIdsToBeCreated = new HashSet<uint>();
CollectLinkIdsFromPatterns(substitutionPatterns, linkIdsToBeCreated, links);

TraceIfEnabled(options, $"[ValidateLinksExistOrWillBeCreated] Links to be created: {string.Join(", ", linkIdsToBeCreated)}");

// Validate all references in restriction patterns
ValidateReferencesInPatterns(restrictionPatterns, links, linkIdsToBeCreated, "restriction", options);

// Validate all references in substitution patterns
ValidateReferencesInPatterns(substitutionPatterns, links, linkIdsToBeCreated, "substitution", options);

TraceIfEnabled(options, "[ValidateLinksExistOrWillBeCreated] Validation completed");
}

/// <summary>
/// Collects all specific link IDs that will be created from substitution patterns.
/// </summary>
private static void CollectLinkIdsFromPatterns(IList<LinoLink> patterns, HashSet<uint> linkIds, NamedLinksDecorator<uint> links)
{
foreach (var pattern in patterns)
{
CollectLinkIdsFromPattern(pattern, linkIds, links);
}
}

/// <summary>
/// Recursively collects link IDs from a single pattern.
/// Only collects IDs that define links (end with ':')
/// </summary>
private static void CollectLinkIdsFromPattern(LinoLink pattern, HashSet<uint> linkIds, NamedLinksDecorator<uint> links, int depth = 0)
{
// Prevent infinite recursion
if (depth > 10) return;

// Check if this pattern defines a specific link ID (ends with ':')
if (!string.IsNullOrEmpty(pattern.Id) && pattern.Id.EndsWith(":") && !pattern.Id.StartsWith("$") && pattern.Id != "*:")
{
var cleanId = pattern.Id.Replace(":", "");
if (uint.TryParse(cleanId, out var linkId))
{
linkIds.Add(linkId);
}
}

// Recursively check sub-patterns
if (pattern.Values != null)
{
foreach (var subPattern in pattern.Values)
{
CollectLinkIdsFromPattern(subPattern, linkIds, links, depth + 1);
}
}
}

/// <summary>
/// Validates that all references in the given patterns are valid.
/// </summary>
private static void ValidateReferencesInPatterns(
IList<LinoLink> patterns,
NamedLinksDecorator<uint> links,
HashSet<uint> linkIdsToBeCreated,
string patternType,
Options options)
{
foreach (var pattern in patterns)
{
ValidateReferencesInPattern(pattern, links, linkIdsToBeCreated, patternType, options);
}
}

/// <summary>
/// Recursively validates references in a single pattern.
/// Only validates references (numeric IDs without ':' that are not variables or wildcards).
/// </summary>
private static void ValidateReferencesInPattern(
LinoLink pattern,
NamedLinksDecorator<uint> links,
HashSet<uint> linkIdsToBeCreated,
string patternType,
Options options,
int depth = 0)
{
// Prevent infinite recursion
if (depth > 10) return;

// Validate the pattern's own ID if it's a reference (not a definition, variable, or wildcard)
if (!string.IsNullOrEmpty(pattern.Id) &&
!pattern.Id.StartsWith("$") &&
pattern.Id != "*" &&
!pattern.Id.EndsWith(":"))
{
if (uint.TryParse(pattern.Id, out var linkId))
{
if (!links.Exists(linkId) && !linkIdsToBeCreated.Contains(linkId))
{
throw new InvalidOperationException(
$"Invalid reference to non-existent link {linkId} in {patternType} pattern. " +
$"Link {linkId} does not exist and will not be created by this operation."
);
}
TraceIfEnabled(options, $"[ValidateReferencesInPattern] Link {linkId} reference validated in {patternType} pattern");
}
}

// Recursively validate sub-patterns
if (pattern.Values != null)
{
foreach (var subPattern in pattern.Values)
{
ValidateReferencesInPattern(subPattern, links, linkIdsToBeCreated, patternType, options, depth + 1);
}
}
}
}
}