diff --git a/csharp/.changeset/support-string-id-aliases.md b/csharp/.changeset/support-string-id-aliases.md new file mode 100644 index 0000000..202b5d9 --- /dev/null +++ b/csharp/.changeset/support-string-id-aliases.md @@ -0,0 +1,5 @@ +--- +'Foundation.Data.Doublets.Cli': minor +--- + +Added full string ID alias support for advanced LiNo queries through the named types decorator. diff --git a/csharp/Foundation.Data.Doublets.Cli.Tests/AdvancedMixedQueryProcessor.cs b/csharp/Foundation.Data.Doublets.Cli.Tests/AdvancedMixedQueryProcessor.cs index ef351b1..b81aa9d 100644 --- a/csharp/Foundation.Data.Doublets.Cli.Tests/AdvancedMixedQueryProcessor.cs +++ b/csharp/Foundation.Data.Doublets.Cli.Tests/AdvancedMixedQueryProcessor.cs @@ -1501,14 +1501,37 @@ public void DeduplicateNamedLinks_MultipleQueries_ShouldReuseSameIds() }); } + [Fact] + public void StringAliasesInVariableRestriction_ShouldConstrainMatchesToNamedLinks() + { + RunTestWithLinks(links => + { + ProcessQuery(links, "(() ((father: father father)))"); + ProcessQuery(links, "(() ((mother: mother mother)))"); + ProcessQuery(links, "(() ((child: father mother)))"); + + var fatherId = links.GetByName("father"); + var motherId = links.GetByName("mother"); + var childId = links.GetByName("child"); + + ProcessQuery(links, "((($id: father mother)) (($id: mother father)))"); + + var allLinks = GetAllLinks(links); + Assert.Equal(3, allLinks.Count); + AssertLinkExists(allLinks, fatherId, fatherId, fatherId); + AssertLinkExists(allLinks, motherId, motherId, motherId); + AssertLinkExists(allLinks, childId, motherId, fatherId); + }); + } + // Helper methods - private static void RunTestWithLinks(Action> testAction, bool enableTracing = false) + private static void RunTestWithLinks(Action> testAction, bool enableTracing = false) { string tempDbFile = Path.GetTempFileName(); - NamedLinksDecorator? decoratedLinks = null; + NamedTypesDecorator? decoratedLinks = null; try { - decoratedLinks = new NamedLinksDecorator(tempDbFile, tracingEnabled: enableTracing); + decoratedLinks = new NamedTypesDecorator(tempDbFile, tracingEnabled: enableTracing); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); var task = Task.Run(() => @@ -1536,7 +1559,7 @@ private static void RunTestWithLinks(Action> testActio } } - private static List GetAllLinks(NamedLinksDecorator links) + private static List GetAllLinks(NamedTypesDecorator links) { var any = links.Constants.Any; var query = new DoubletLink(index: any, source: any, target: any); @@ -1545,23 +1568,23 @@ private static List GetAllLinks(NamedLinksDecorator links) return allLinks; } - private static void ProcessQuery(NamedLinksDecorator links, string query) + private static void ProcessQuery(NamedTypesDecorator links, string query) { ProcessQuery(links, new Options { Query = query }); } - private static void ProcessQuery(NamedLinksDecorator links, Options options) + private static void ProcessQuery(NamedTypesDecorator links, Options options) { options.AutoCreateMissingReferences = true; Foundation.Data.Doublets.Cli.AdvancedMixedQueryProcessor.ProcessQuery(links, options); } - private static void ProcessQueryStrict(NamedLinksDecorator links, string query) + private static void ProcessQueryStrict(NamedTypesDecorator links, string query) { ProcessQueryStrict(links, new Options { Query = query }); } - private static void ProcessQueryStrict(NamedLinksDecorator links, Options options) + private static void ProcessQueryStrict(NamedTypesDecorator links, Options options) { options.AutoCreateMissingReferences = false; Foundation.Data.Doublets.Cli.AdvancedMixedQueryProcessor.ProcessQuery(links, options); diff --git a/csharp/Foundation.Data.Doublets.Cli.Tests/LinoDatabaseOutputTests.cs b/csharp/Foundation.Data.Doublets.Cli.Tests/LinoDatabaseOutputTests.cs index 4ea7996..75a84b5 100644 --- a/csharp/Foundation.Data.Doublets.Cli.Tests/LinoDatabaseOutputTests.cs +++ b/csharp/Foundation.Data.Doublets.Cli.Tests/LinoDatabaseOutputTests.cs @@ -113,14 +113,14 @@ public void WriteToFile_WritesCompleteDatabaseAsLinoLines() }); } - private static void WithNamedLinks(Action> test) + private static void WithNamedLinks(Action> test) { var dbPath = Path.GetTempFileName(); - var namesDbPath = NamedLinksDecorator.MakeNamesDatabaseFilename(dbPath); + var namesDbPath = NamedTypesDecorator.MakeNamesDatabaseFilename(dbPath); try { - var links = new NamedLinksDecorator(dbPath, false); + var links = new NamedTypesDecorator(dbPath, false); test(links); } finally diff --git a/csharp/Foundation.Data.Doublets.Cli/AdvancedMixedQueryProcessor.cs b/csharp/Foundation.Data.Doublets.Cli/AdvancedMixedQueryProcessor.cs index f8f1141..cc0c700 100644 --- a/csharp/Foundation.Data.Doublets.Cli/AdvancedMixedQueryProcessor.cs +++ b/csharp/Foundation.Data.Doublets.Cli/AdvancedMixedQueryProcessor.cs @@ -28,7 +28,7 @@ public class Options public static implicit operator Options(string query) => new Options { Query = query }; } - public static void ProcessQuery(NamedLinksDecorator links, Options options) + public static void ProcessQuery(INamedTypesLinks links, Options options) { var query = options.Query; TraceIfEnabled(options, $"[ProcessQuery] Query: \"{query}\""); @@ -288,7 +288,7 @@ public static void ProcessQuery(NamedLinksDecorator links, Options options /// Recursively ensures that a LinoLink (potentially nested) is created. /// Returns the numeric ID or ANY if leaf/unparseable. /// - private static uint EnsureNestedLinkCreatedRecursively(NamedLinksDecorator links, LinoLink pattern, Options options) + private static uint EnsureNestedLinkCreatedRecursively(INamedTypesLinks links, LinoLink pattern, Options options) { var nullConstant = links.Constants.Null; var anyConstant = links.Constants.Any; @@ -320,7 +320,7 @@ private static uint EnsureNestedLinkCreatedRecursively(NamedLinksDecorator } private static void RestoreUnexpectedLinkDeletions( - NamedLinksDecorator links, + INamedTypesLinks links, List unexpectedDeletions, Dictionary finalIntendedStates, Options options) @@ -354,7 +354,7 @@ private static void RestoreUnexpectedLinkDeletions( private static List<(DoubletLink before, DoubletLink after)> DetermineOperationsFromPatterns( List restrictions, List substitutions, - NamedLinksDecorator links) + INamedTypesLinks links) { var anyOrZero = new HashSet { 0, links.Constants.Any }; @@ -415,7 +415,7 @@ private static void RestoreUnexpectedLinkDeletions( } private static void ApplyAllPlannedOperations( - NamedLinksDecorator links, + INamedTypesLinks links, List<(DoubletLink before, DoubletLink after)> operations, Options options) { @@ -477,7 +477,7 @@ private static void ApplyAllPlannedOperations( } } - private static List> FindAllSolutions(NamedLinksDecorator links, List patterns) + private static List> FindAllSolutions(INamedTypesLinks links, List patterns) { var partialSolutions = new List> { new Dictionary() }; @@ -526,7 +526,7 @@ private static bool AreSolutionsCompatible( } private static IEnumerable> MatchPattern( - NamedLinksDecorator links, + INamedTypesLinks links, Pattern pattern, Dictionary currentSolution) { @@ -591,7 +591,7 @@ private static IEnumerable> MatchPattern( } private static IEnumerable> RecursiveMatchSubPattern( - NamedLinksDecorator links, + INamedTypesLinks links, Pattern? pattern, uint linkId, Dictionary currentSolution) @@ -635,7 +635,7 @@ private static IEnumerable> RecursiveMatchSubPattern( } private static bool CheckIdMatch( - NamedLinksDecorator links, + INamedTypesLinks links, string patternId, uint candidateId, Dictionary currentSolution) @@ -653,7 +653,7 @@ private static bool CheckIdMatch( } uint parsed = links.Constants.Any; - if (TryParseLinkId(patternId, links.Constants, ref parsed)) + if (TryParseLinkId(patternId, links, ref parsed)) { if (parsed == links.Constants.Any) return true; return parsed == candidateId; @@ -675,7 +675,7 @@ private static bool IsVariable(string identifier) } private static uint ResolveId( - NamedLinksDecorator links, + INamedTypesLinks links, string identifier, Dictionary currentSolution) { @@ -689,16 +689,10 @@ private static uint ResolveId( { return anyConstant; } - if (TryParseLinkId(identifier, links.Constants, ref anyConstant)) + if (TryParseLinkId(identifier, links, ref anyConstant)) { return anyConstant; } - // Add name resolution for deletion patterns - var namedId = links.GetByName(identifier); - if (namedId != links.Constants.Null) - { - return namedId; - } return anyConstant; } @@ -706,7 +700,7 @@ private static bool DetermineIfSolutionIsNoOperation( Dictionary solution, List restrictions, List substitutions, - NamedLinksDecorator links) + INamedTypesLinks links) { var substitutedRestrictions = restrictions .Select(r => ApplySolutionToPattern(links, solution, r)) @@ -735,7 +729,7 @@ private static bool DetermineIfSolutionIsNoOperation( } private static List ExtractMatchedLinks( - NamedLinksDecorator links, + INamedTypesLinks links, Dictionary solution, List patterns) { @@ -756,7 +750,7 @@ private static List ExtractMatchedLinks( } private static DoubletLink? ApplySolutionToPattern( - NamedLinksDecorator links, + INamedTypesLinks links, Dictionary solution, Pattern? pattern) { @@ -786,7 +780,7 @@ private static List ExtractMatchedLinks( } } - private static void CreateOrUpdateLink(NamedLinksDecorator links, DoubletLink linkDefinition, Options options) + private static void CreateOrUpdateLink(INamedTypesLinks links, DoubletLink linkDefinition, Options options) { var nullConstant = links.Constants.Null; var anyConstant = links.Constants.Any; @@ -882,7 +876,7 @@ private static void CreateOrUpdateLink(NamedLinksDecorator links, DoubletL } private static void RemoveLinks( - NamedLinksDecorator links, + INamedTypesLinks links, DoubletLink restriction, Options options) { @@ -907,28 +901,28 @@ private static void RemoveLinks( } } - private static DoubletLink ConvertToDoubletLink(NamedLinksDecorator links, LinoLink linoLink, uint defaultValue) + private static DoubletLink ConvertToDoubletLink(INamedTypesLinks links, LinoLink linoLink, uint defaultValue) { uint index = defaultValue; uint source = defaultValue; uint target = defaultValue; - TryParseLinkId(linoLink.Id, links.Constants, ref index); + TryParseLinkId(linoLink.Id, links, ref index); if (linoLink.Values?.Count == 2) { var sourceLink = linoLink.Values[0]; - TryParseLinkId(sourceLink.Id, links.Constants, ref source); + TryParseLinkId(sourceLink.Id, links, ref source); var targetLink = linoLink.Values[1]; - TryParseLinkId(targetLink.Id, links.Constants, ref target); + TryParseLinkId(targetLink.Id, links, ref target); } return new DoubletLink(index, source, target); } - private static bool TryParseLinkId(string? id, LinksConstants constants, ref uint parsedValue) + private static bool TryParseLinkId(string? id, INamedTypesLinks links, ref uint parsedValue) { if (string.IsNullOrEmpty(id)) return false; if (id == "*") { - parsedValue = constants.Any; + parsedValue = links.Constants.Any; return true; } else if (id.EndsWith(":")) @@ -939,12 +933,29 @@ private static bool TryParseLinkId(string? id, LinksConstants constants, r parsedValue = linkId; return true; } + // Try to resolve as string alias + var aliasId = links.GetByName(trimmed); + if (aliasId != links.Constants.Null) + { + parsedValue = aliasId; + return true; + } } else if (uint.TryParse(id, out uint linkVal)) { parsedValue = linkVal; return true; } + else + { + // Try to resolve as string alias + var aliasId = links.GetByName(id); + if (aliasId != links.Constants.Null) + { + parsedValue = aliasId; + return true; + } + } return false; } @@ -982,7 +993,7 @@ private static Pattern CreatePatternFromLino(LinoLink linkNode) return new Pattern(linkNode.Id ?? ""); } - private static uint EnsureLinkCreated(NamedLinksDecorator links, DoubletLink link, Options options) + private static uint EnsureLinkCreated(INamedTypesLinks links, DoubletLink link, Options options) { var nullConstant = links.Constants.Null; var anyConstant = links.Constants.Any; @@ -1066,7 +1077,7 @@ private static void TraceIfEnabled(Options options, string message) } // Consolidates getting or creating a named link (leaf) without setting its relationships - private static uint EnsureNamedLeafLink(NamedLinksDecorator links, string name, Options options) + private static uint EnsureNamedLeafLink(INamedTypesLinks links, string name, Options options) { var existing = links.GetByName(name); if (existing != links.Constants.Null) return existing; @@ -1077,7 +1088,7 @@ private static uint EnsureNamedLeafLink(NamedLinksDecorator links, string } // Applies a single structural update to an existing link: sets its source and target - private static void ApplyCompositeUpdate(NamedLinksDecorator links, uint id, uint source, uint target, Options options) + private static void ApplyCompositeUpdate(INamedTypesLinks links, uint id, uint source, uint target, Options options) { var restriction = new DoubletLink(id, links.Constants.Null, links.Constants.Null); var substitution = new DoubletLink(id, source, target); @@ -1126,7 +1137,7 @@ private static CompositeCase ClassifyCompositeCase(string name, LinoLink left, L throw new InvalidOperationException($"Invalid composite pattern for name '{name}'"); } - private static uint HandleStringComposite(string name, LinoLink left, LinoLink right, NamedLinksDecorator links, Options options) + private static uint HandleStringComposite(string name, LinoLink left, LinoLink right, INamedTypesLinks links, Options options) { var id = EnsureNamedLeafLink(links, name, options); var caseType = ClassifyCompositeCase(name, left, right); @@ -1155,7 +1166,7 @@ private static uint HandleStringComposite(string name, LinoLink left, LinoLink r /// /// Resolves a single leaf pattern into its numeric or named link ID. /// - private static uint ResolveLeaf(LinoLink pattern, NamedLinksDecorator links, Options options) + private static uint ResolveLeaf(LinoLink pattern, INamedTypesLinks links, Options options) { var nullConstant = links.Constants.Null; var anyConstant = links.Constants.Any; @@ -1208,7 +1219,7 @@ private static uint CreateCompositeLink( string? literalIdentifier, uint sourceLinkId, uint targetLinkId, - NamedLinksDecorator links, + INamedTypesLinks links, Options options) { // Determine the numeric index for the composite: default 0, wildcard, or parsed from identifier @@ -1242,7 +1253,7 @@ private static uint CreateCompositeLink( } private static void ValidateLinksExistOrWillBeCreated( - NamedLinksDecorator links, + INamedTypesLinks links, IList restrictionPatterns, IList substitutionPatterns, Options options) @@ -1299,7 +1310,7 @@ private sealed class MissingLinkReference public string Key => NumericId.HasValue ? $"id:{NumericId.Value}" : $"name:{Identifier}"; } - private static LinkReferencePlan BuildLinkReferencePlan(NamedLinksDecorator links, IList substitutionPatterns) + private static LinkReferencePlan BuildLinkReferencePlan(INamedTypesLinks links, IList substitutionPatterns) { var plan = new LinkReferencePlan(); var reservedNumericIds = new HashSet(); @@ -1343,7 +1354,7 @@ private static void CollectExplicitDefinitions(LinoLink pattern, LinkReferencePl private static void CollectImplicitDefinitions( LinoLink pattern, - NamedLinksDecorator links, + INamedTypesLinks links, LinkReferencePlan plan, HashSet reservedNumericIds) { @@ -1363,7 +1374,7 @@ private static void CollectImplicitDefinitions( } } - private static uint GetNextAvailableLinkId(NamedLinksDecorator links, HashSet reservedNumericIds) + private static uint GetNextAvailableLinkId(INamedTypesLinks links, HashSet reservedNumericIds) { uint nextId = 1; while (links.Exists(nextId) || reservedNumericIds.Contains(nextId)) @@ -1375,7 +1386,7 @@ private static uint GetNextAvailableLinkId(NamedLinksDecorator links, Hash private static void CollectMissingReferences( IList patterns, - NamedLinksDecorator links, + INamedTypesLinks links, LinkReferencePlan plan, bool isSubstitution, string patternType, @@ -1389,7 +1400,7 @@ private static void CollectMissingReferences( private static void CollectMissingReferences( LinoLink pattern, - NamedLinksDecorator links, + INamedTypesLinks links, LinkReferencePlan plan, bool isSubstitution, string patternType, @@ -1413,7 +1424,7 @@ private static void CollectMissingReferences( private static void ValidateReferenceIdentifier( string identifier, - NamedLinksDecorator links, + INamedTypesLinks links, LinkReferencePlan plan, string patternType, Options options) @@ -1449,7 +1460,7 @@ private static void ValidateReferenceIdentifier( } private static void AutoCreateMissingReferences( - NamedLinksDecorator links, + INamedTypesLinks links, IList missingReferences, Options options) { @@ -1483,7 +1494,7 @@ private static void AutoCreateMissingReferences( } } - private static void EnsureNamedPointLink(NamedLinksDecorator links, string name, Options options) + private static void EnsureNamedPointLink(INamedTypesLinks links, string name, Options options) { if (links.GetByName(name) != links.Constants.Null) { diff --git a/csharp/Foundation.Data.Doublets.Cli/INamedTypes.cs b/csharp/Foundation.Data.Doublets.Cli/INamedTypes.cs index 54b1b62..aa594cf 100644 --- a/csharp/Foundation.Data.Doublets.Cli/INamedTypes.cs +++ b/csharp/Foundation.Data.Doublets.Cli/INamedTypes.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Platform.Data.Doublets; namespace Foundation.Data.Doublets.Cli { @@ -10,4 +11,9 @@ public interface INamedTypes TLinkAddress GetByName(string name); void RemoveName(TLinkAddress link); } -} \ No newline at end of file + + public interface INamedTypesLinks : ILinks, INamedTypes + where TLinkAddress : IUnsignedNumber + { + } +} diff --git a/csharp/Foundation.Data.Doublets.Cli/LinoDatabaseOutput.cs b/csharp/Foundation.Data.Doublets.Cli/LinoDatabaseOutput.cs index b0e08a2..53c1a0a 100644 --- a/csharp/Foundation.Data.Doublets.Cli/LinoDatabaseOutput.cs +++ b/csharp/Foundation.Data.Doublets.Cli/LinoDatabaseOutput.cs @@ -10,7 +10,7 @@ public static class LinoDatabaseOutput { private static readonly Regex NumberTokenRegex = new(@"(? FormatDatabase(NamedLinksDecorator links) + public static IReadOnlyList FormatDatabase(INamedTypesLinks links) { var any = links.Constants.Any; var query = new DoubletLink(index: any, source: any, target: any); @@ -23,7 +23,7 @@ public static IReadOnlyList FormatDatabase(NamedLinksDecorator lin .ToList(); } - public static void WriteDatabase(NamedLinksDecorator links, TextWriter writer) + public static void WriteDatabase(INamedTypesLinks links, TextWriter writer) { foreach (var line in FormatDatabase(links)) { @@ -31,25 +31,25 @@ public static void WriteDatabase(NamedLinksDecorator links, TextWriter wri } } - public static void WriteToFile(NamedLinksDecorator links, string path) + public static void WriteToFile(INamedTypesLinks links, string path) { using var writer = new StreamWriter(path, append: false); WriteDatabase(links, writer); } - public static string FormatLink(NamedLinksDecorator links, DoubletLink link) + public static string FormatLink(INamedTypesLinks links, DoubletLink link) { return $"({FormatReference(links, link.Index)}: {FormatReference(links, link.Source)} {FormatReference(links, link.Target)})"; } - public static string FormatChange(NamedLinksDecorator links, DoubletLink linkBefore, DoubletLink linkAfter) + public static string FormatChange(INamedTypesLinks links, DoubletLink linkBefore, DoubletLink linkAfter) { var beforeText = linkBefore.IsNull() ? "" : FormatLink(links, linkBefore); var afterText = linkAfter.IsNull() ? "" : FormatLink(links, linkAfter); return $"({beforeText}) ({afterText})"; } - public static string Namify(NamedLinksDecorator namedLinks, string linksNotation) + public static string Namify(INamedTypesLinks namedLinks, string linksNotation) { return NumberTokenRegex.Replace(linksNotation, match => { @@ -59,7 +59,7 @@ public static string Namify(NamedLinksDecorator namedLinks, string linksNo }); } - private static string FormatReference(NamedLinksDecorator links, uint link) + private static string FormatReference(INamedTypesLinks links, uint link) { var name = links.GetName(link); return name is null ? link.ToString() : EscapeReference(name); diff --git a/csharp/Foundation.Data.Doublets.Cli/NamedLinksDecorator.cs b/csharp/Foundation.Data.Doublets.Cli/NamedLinksDecorator.cs index 4c2d3be..64abe8c 100644 --- a/csharp/Foundation.Data.Doublets.Cli/NamedLinksDecorator.cs +++ b/csharp/Foundation.Data.Doublets.Cli/NamedLinksDecorator.cs @@ -12,7 +12,7 @@ namespace Foundation.Data.Doublets.Cli { - public class NamedLinksDecorator : LinksDecoratorBase + public class NamedLinksDecorator : LinksDecoratorBase, INamedTypesLinks where TLinkAddress : struct, IUnsignedNumber, IComparisonOperators, diff --git a/csharp/Foundation.Data.Doublets.Cli/NamedTypesDecorator.cs b/csharp/Foundation.Data.Doublets.Cli/NamedTypesDecorator.cs index fa91e85..8588c93 100644 --- a/csharp/Foundation.Data.Doublets.Cli/NamedTypesDecorator.cs +++ b/csharp/Foundation.Data.Doublets.Cli/NamedTypesDecorator.cs @@ -12,7 +12,7 @@ namespace Foundation.Data.Doublets.Cli { - public class NamedTypesDecorator : LinksDecoratorBase, INamedTypes, IPinnedTypes + public class NamedTypesDecorator : LinksDecoratorBase, INamedTypesLinks, IPinnedTypes where TLinkAddress : struct, IUnsignedNumber, IComparisonOperators, diff --git a/csharp/Foundation.Data.Doublets.Cli/Program.cs b/csharp/Foundation.Data.Doublets.Cli/Program.cs index b34f9b2..0c3c0cd 100644 --- a/csharp/Foundation.Data.Doublets.Cli/Program.cs +++ b/csharp/Foundation.Data.Doublets.Cli/Program.cs @@ -109,7 +109,7 @@ var after = context.ParseResult.GetValueForOption(afterOption); var outputPath = context.ParseResult.GetValueForOption(outputOption); - var decoratedLinks = new NamedLinksDecorator(db, trace); + var decoratedLinks = new NamedTypesDecorator(db, trace); if (structure.HasValue) { @@ -193,17 +193,17 @@ await rootCommand.InvokeAsync(args); -static void PrintAllLinks(NamedLinksDecorator links) +static void PrintAllLinks(INamedTypesLinks links) { LinoDatabaseOutput.WriteDatabase(links, Console.Out); } -static void PrintChange(NamedLinksDecorator links, DoubletLink linkBefore, DoubletLink linkAfter) +static void PrintChange(INamedTypesLinks links, DoubletLink linkBefore, DoubletLink linkAfter) { Console.WriteLine(LinoDatabaseOutput.FormatChange(links, linkBefore, linkAfter)); } -static bool TryWriteLinoOutput(NamedLinksDecorator links, string? outputPath, InvocationContext context) +static bool TryWriteLinoOutput(INamedTypesLinks links, string? outputPath, InvocationContext context) { if (string.IsNullOrWhiteSpace(outputPath)) { diff --git a/rust/changelog.d/20260430_105500_named_type_query_cli.md b/rust/changelog.d/20260430_105500_named_type_query_cli.md new file mode 100644 index 0000000..f5f0ab5 --- /dev/null +++ b/rust/changelog.d/20260430_105500_named_type_query_cli.md @@ -0,0 +1,5 @@ +--- +bump: minor +--- + +Routed Rust query processing and CLI string ID aliases through the named types decorator so aliases resolve from the separate names database, matching C# full string ID behavior. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 5591ef5..05de5b9 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -23,11 +23,13 @@ mod link_reference_validator; mod link_storage; mod lino_link; mod named_links; +mod named_type_links; mod named_types; mod parser; mod pinned_types; mod query_options; mod query_processor; +mod query_types; pub mod sequences; mod unicode_string_storage; @@ -39,6 +41,7 @@ pub use link::{DoubletsLink, Link}; pub use link_storage::LinkStorage; pub use lino_link::LinoLink; pub use named_links::NamedLinks; +pub use named_type_links::NamedTypeLinks; pub use named_types::{NamedTypes, NamedTypesDecorator}; pub use parser::Parser; pub use pinned_types::{PinnedTypes, PinnedTypesAccess, PinnedTypesDecorator}; diff --git a/rust/src/link_reference_validator.rs b/rust/src/link_reference_validator.rs index 9fe13fc..bf00fd4 100644 --- a/rust/src/link_reference_validator.rs +++ b/rust/src/link_reference_validator.rs @@ -3,8 +3,8 @@ use std::collections::HashSet; use crate::error::LinkError; use crate::link::Link; -use crate::link_storage::LinkStorage; use crate::lino_link::LinoLink; +use crate::named_type_links::NamedTypeLinks; pub(crate) struct LinkReferenceValidator { trace: bool, @@ -53,7 +53,7 @@ impl LinkReferenceValidator { pub(crate) fn validate_links_exist_or_will_be_created( &self, - storage: &mut LinkStorage, + storage: &mut impl NamedTypeLinks, restriction_patterns: &[LinoLink], substitution_patterns: &[LinoLink], ) -> Result> { @@ -75,14 +75,14 @@ impl LinkReferenceValidator { restriction_patterns, false, "restriction", - ); + )?; self.collect_missing_references( storage, &mut plan, substitution_patterns, true, "substitution", - ); + )?; if plan.missing_references.is_empty() { self.trace_msg("[ValidateLinksExistOrWillBeCreated] Validation completed"); @@ -105,7 +105,7 @@ impl LinkReferenceValidator { fn build_link_reference_plan( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, substitution_patterns: &[LinoLink], ) -> LinkReferencePlan { let mut plan = LinkReferencePlan::default(); @@ -153,7 +153,7 @@ impl LinkReferenceValidator { fn collect_implicit_definitions( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, pattern: &LinoLink, plan: &mut LinkReferencePlan, reserved_numeric_ids: &mut HashSet, @@ -173,7 +173,10 @@ impl LinkReferenceValidator { } } - fn next_available_link_id(storage: &LinkStorage, reserved_numeric_ids: &HashSet) -> u32 { + fn next_available_link_id( + storage: &mut impl NamedTypeLinks, + reserved_numeric_ids: &HashSet, + ) -> u32 { let mut next_id = 1; while storage.exists(next_id) || reserved_numeric_ids.contains(&next_id) { next_id += 1; @@ -183,12 +186,12 @@ impl LinkReferenceValidator { fn collect_missing_references( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, plan: &mut LinkReferencePlan, patterns: &[LinoLink], is_substitution: bool, pattern_type: &'static str, - ) { + ) -> Result<()> { for pattern in patterns { self.collect_missing_references_in_pattern( storage, @@ -196,25 +199,26 @@ impl LinkReferenceValidator { pattern, is_substitution, pattern_type, - ); + )?; } + Ok(()) } fn collect_missing_references_in_pattern( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, plan: &mut LinkReferencePlan, pattern: &LinoLink, is_substitution: bool, pattern_type: &'static str, - ) { + ) -> Result<()> { let pattern_id_is_definition = is_substitution && Self::is_composite_lino(pattern) && Self::concrete_identifier(pattern.id.as_deref()).is_some(); if !pattern_id_is_definition { if let Some(identifier) = Self::concrete_identifier(pattern.id.as_deref()) { - self.validate_reference_identifier(storage, plan, &identifier, pattern_type); + self.validate_reference_identifier(storage, plan, &identifier, pattern_type)?; } } @@ -226,18 +230,19 @@ impl LinkReferenceValidator { sub_pattern, is_substitution, pattern_type, - ); + )?; } } + Ok(()) } fn validate_reference_identifier( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, plan: &mut LinkReferencePlan, identifier: &str, pattern_type: &'static str, - ) { + ) -> Result<()> { if let Ok(link_id) = identifier.parse::() { if !storage.exists(link_id) && !plan.numeric_ids_to_be_created.contains(&link_id) { plan.add_missing_reference(MissingLinkReference { @@ -245,15 +250,15 @@ impl LinkReferenceValidator { pattern_type, numeric_id: Some(link_id), }); - return; + return Ok(()); } self.trace_msg(&format!( "[ValidateReferencesInPattern] Link {link_id} reference validated in {pattern_type} pattern" )); - return; + return Ok(()); } - if storage.get_by_name(identifier).is_none() + if storage.get_by_name(identifier)?.is_none() && !plan.names_to_be_created.contains(identifier) { plan.add_missing_reference(MissingLinkReference { @@ -261,17 +266,18 @@ impl LinkReferenceValidator { pattern_type, numeric_id: None, }); - return; + return Ok(()); } self.trace_msg(&format!( "[ValidateReferencesInPattern] Named link '{identifier}' reference validated in {pattern_type} pattern" )); + Ok(()) } fn auto_create_missing_references( &self, - storage: &mut LinkStorage, + storage: &mut impl NamedTypeLinks, missing_references: &[MissingLinkReference], ) -> Result> { let mut created = Vec::new(); @@ -292,8 +298,8 @@ impl LinkReferenceValidator { )); storage.ensure_created(link_id); storage.update(link_id, link_id, link_id)?; - if let Some(link) = storage.get(link_id) { - created.push(*link); + if let Some(link) = storage.get_link(link_id) { + created.push(link); } } @@ -306,16 +312,16 @@ impl LinkReferenceValidator { named_references.dedup(); for name in named_references { - if storage.get_by_name(&name).is_some() { + if storage.get_by_name(&name)?.is_some() { continue; } self.trace_msg(&format!( "[ValidateLinksExistOrWillBeCreated] Auto-creating missing named reference '{name}' as point link." )); - let link_id = storage.get_or_create_named(&name); - if let Some(link) = storage.get(link_id) { - created.push(*link); + let link_id = storage.get_or_create_named(&name)?; + if let Some(link) = storage.get_link(link_id) { + created.push(link); } } diff --git a/rust/src/main.rs b/rust/src/main.rs index 39a59aa..8fa9726 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -5,7 +5,7 @@ use anyhow::Result; use link_cli::cli::{Cli, CliCommand}; -use link_cli::{LinkStorage, QueryProcessor}; +use link_cli::{NamedTypeLinks, NamedTypesDecorator, QueryProcessor}; fn main() -> Result<()> { let cli = match Cli::parse()? { @@ -20,8 +20,8 @@ fn main() -> Result<()> { } }; - // Create link storage - let mut storage = LinkStorage::new(&cli.db, cli.trace)?; + // Create link storage with separate named-type aliases. + let mut storage = NamedTypesDecorator::new(&cli.db, cli.trace)?; // If --structure is provided, handle it separately if let Some(link_id) = cli.structure { @@ -35,7 +35,7 @@ fn main() -> Result<()> { // Print before state if requested if cli.before { - storage.print_all_links(); + storage.print_all_lino()?; } // Get effective query (option takes precedence over positional argument) @@ -56,13 +56,13 @@ fn main() -> Result<()> { // Print changes if requested if cli.changes && !changes_list.is_empty() { for (before_link, after_link) in &changes_list { - storage.print_change(before_link, after_link); + storage.print_change_lino(before_link, after_link)?; } } // Print after state if requested if cli.after { - storage.print_all_links(); + storage.print_all_lino()?; } if let Some(output_path) = &cli.lino_output { diff --git a/rust/src/named_type_links.rs b/rust/src/named_type_links.rs new file mode 100644 index 0000000..b32a37a --- /dev/null +++ b/rust/src/named_type_links.rs @@ -0,0 +1,287 @@ +use anyhow::{Context, Result}; +use std::fs::OpenOptions; +use std::io::{BufWriter, Write}; +use std::path::Path; + +use crate::error::LinkError; +use crate::link::Link; +use crate::link_storage::LinkStorage; +use crate::named_types::{NamedTypes, NamedTypesDecorator}; + +pub trait NamedTypeLinks { + fn create(&mut self, source: u32, target: u32) -> u32; + fn ensure_created(&mut self, id: u32) -> u32; + fn get_link(&mut self, id: u32) -> Option; + fn exists(&mut self, id: u32) -> bool; + fn update(&mut self, id: u32, source: u32, target: u32) -> Result; + fn delete(&mut self, id: u32) -> Result; + fn all_links(&mut self) -> Vec; + fn search(&mut self, source: u32, target: u32) -> Option; + fn get_or_create(&mut self, source: u32, target: u32) -> u32; + fn get_name(&mut self, id: u32) -> Result>; + fn set_name(&mut self, id: u32, name: &str) -> Result; + fn get_by_name(&mut self, name: &str) -> Result>; + fn remove_name(&mut self, id: u32) -> Result<()>; + fn save(&mut self) -> Result<()>; + + fn get_or_create_named(&mut self, name: &str) -> Result { + if let Some(id) = self.get_by_name(name)? { + return Ok(id); + } + + let id = self.create(0, 0); + self.set_name(id, name)?; + self.update(id, id, id)?; + Ok(id) + } + + fn format_reference(&mut self, id: u32) -> Result { + Ok(self + .get_name(id)? + .map(|name| escape_lino_reference(&name)) + .unwrap_or_else(|| id.to_string())) + } + + fn format_lino(&mut self, link: &Link) -> Result { + Ok(format!( + "({}: {} {})", + self.format_reference(link.index)?, + self.format_reference(link.source)?, + self.format_reference(link.target)? + )) + } + + fn lino_lines(&mut self) -> Result> { + let mut links = self.all_links(); + links.sort_by_key(|link| link.index); + + links + .iter() + .map(|link| self.format_lino(link)) + .collect::>>() + } + + fn write_lino_output>(&mut self, path: P) -> Result<()> { + let path = path.as_ref(); + let file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path) + .with_context(|| format!("Failed to create LiNo output: {}", path.display()))?; + + let mut writer = BufWriter::new(file); + for line in self.lino_lines()? { + writeln!(writer, "{line}")?; + } + writer.flush()?; + Ok(()) + } + + fn print_all_lino(&mut self) -> Result<()> { + for line in self.lino_lines()? { + println!("{line}"); + } + Ok(()) + } + + fn print_change_lino(&mut self, before: &Option, after: &Option) -> Result<()> { + let before_text = before + .map(|link| self.format_lino(&link)) + .transpose()? + .unwrap_or_default(); + let after_text = after + .map(|link| self.format_lino(&link)) + .transpose()? + .unwrap_or_default(); + println!("({before_text}) ({after_text})"); + Ok(()) + } + + fn format_structure(&mut self, id: u32) -> Result { + let link = self.get_link(id).ok_or(LinkError::NotFound(id))?; + self.format_structure_recursive(&link, true) + } + + fn format_structure_recursive(&mut self, link: &Link, is_root: bool) -> Result { + if link.is_full_point() && !is_root { + return self.format_reference(link.index); + } + + let source = if link.source == link.index { + self.format_reference(link.index)? + } else if let Some(source_link) = self.get_link(link.source) { + self.format_structure_recursive(&source_link, false)? + } else { + link.source.to_string() + }; + + let target = if link.target == link.index { + self.format_reference(link.index)? + } else if let Some(target_link) = self.get_link(link.target) { + self.format_structure_recursive(&target_link, false)? + } else { + link.target.to_string() + }; + + Ok(format!("({source} {target})")) + } +} + +impl NamedTypeLinks for LinkStorage { + fn create(&mut self, source: u32, target: u32) -> u32 { + LinkStorage::create(self, source, target) + } + + fn ensure_created(&mut self, id: u32) -> u32 { + LinkStorage::ensure_created(self, id) + } + + fn get_link(&mut self, id: u32) -> Option { + self.get(id).copied() + } + + fn exists(&mut self, id: u32) -> bool { + LinkStorage::exists(self, id) + } + + fn update(&mut self, id: u32, source: u32, target: u32) -> Result { + LinkStorage::update(self, id, source, target) + } + + fn delete(&mut self, id: u32) -> Result { + LinkStorage::delete(self, id) + } + + fn all_links(&mut self) -> Vec { + self.all().into_iter().copied().collect() + } + + fn search(&mut self, source: u32, target: u32) -> Option { + LinkStorage::search(self, source, target) + } + + fn get_or_create(&mut self, source: u32, target: u32) -> u32 { + LinkStorage::get_or_create(self, source, target) + } + + fn get_name(&mut self, id: u32) -> Result> { + Ok(LinkStorage::get_name(self, id).cloned()) + } + + fn set_name(&mut self, id: u32, name: &str) -> Result { + LinkStorage::set_name(self, id, name); + Ok(id) + } + + fn get_by_name(&mut self, name: &str) -> Result> { + Ok(LinkStorage::get_by_name(self, name)) + } + + fn remove_name(&mut self, id: u32) -> Result<()> { + LinkStorage::remove_name(self, id); + Ok(()) + } + + fn save(&mut self) -> Result<()> { + LinkStorage::save(self) + } + + fn get_or_create_named(&mut self, name: &str) -> Result { + Ok(LinkStorage::get_or_create_named(self, name)) + } +} + +impl NamedTypeLinks for NamedTypesDecorator { + fn create(&mut self, source: u32, target: u32) -> u32 { + NamedTypesDecorator::create(self, source, target) + } + + fn ensure_created(&mut self, id: u32) -> u32 { + NamedTypesDecorator::ensure_created(self, id) + } + + fn get_link(&mut self, id: u32) -> Option { + self.get(id).copied() + } + + fn exists(&mut self, id: u32) -> bool { + NamedTypesDecorator::exists(self, id) + } + + fn update(&mut self, id: u32, source: u32, target: u32) -> Result { + NamedTypesDecorator::update(self, id, source, target) + } + + fn delete(&mut self, id: u32) -> Result { + NamedTypesDecorator::delete(self, id) + } + + fn all_links(&mut self) -> Vec { + self.all().into_iter().copied().collect() + } + + fn search(&mut self, source: u32, target: u32) -> Option { + NamedTypesDecorator::search(self, source, target) + } + + fn get_or_create(&mut self, source: u32, target: u32) -> u32 { + NamedTypesDecorator::get_or_create(self, source, target) + } + + fn get_name(&mut self, id: u32) -> Result> { + NamedTypes::get_name(self, id) + } + + fn set_name(&mut self, id: u32, name: &str) -> Result { + NamedTypes::set_name(self, id, name) + } + + fn get_by_name(&mut self, name: &str) -> Result> { + NamedTypes::get_by_name(self, name) + } + + fn remove_name(&mut self, id: u32) -> Result<()> { + NamedTypes::remove_name(self, id) + } + + fn save(&mut self) -> Result<()> { + NamedTypesDecorator::save(self) + } +} + +pub(crate) fn escape_lino_reference(reference: &str) -> String { + if reference.is_empty() || reference.trim().is_empty() { + return String::new(); + } + + let has_single_quote = reference.contains('\''); + let has_double_quote = reference.contains('"'); + let needs_quoting = reference.contains(':') + || reference.contains('(') + || reference.contains(')') + || reference.contains(' ') + || reference.contains('\t') + || reference.contains('\n') + || reference.contains('\r') + || has_single_quote + || has_double_quote; + + if has_single_quote && has_double_quote { + return format!("'{}'", reference.replace('\'', "\\'")); + } + + if has_double_quote { + return format!("'{reference}'"); + } + + if has_single_quote { + return format!("\"{reference}\""); + } + + if needs_quoting { + return format!("'{reference}'"); + } + + reference.to_string() +} diff --git a/rust/src/query_processor.rs b/rust/src/query_processor.rs index 4272bce..c24c3af 100644 --- a/rust/src/query_processor.rs +++ b/rust/src/query_processor.rs @@ -10,9 +10,10 @@ use crate::changes_simplifier::simplify_changes; use crate::error::LinkError; use crate::link::Link; use crate::link_reference_validator::LinkReferenceValidator; -use crate::link_storage::LinkStorage; use crate::lino_link::LinoLink; +use crate::named_type_links::NamedTypeLinks; use crate::parser::Parser; +use crate::query_types::{Pattern, ResolvedLink}; /// QueryProcessor handles LiNo query parsing and execution /// Corresponds to AdvancedMixedQueryProcessor in C# @@ -21,50 +22,6 @@ pub struct QueryProcessor { auto_create_missing_references: bool, } -#[derive(Clone, Debug, Eq, PartialEq)] -struct Pattern { - index: String, - source: Option>, - target: Option>, -} - -impl Pattern { - fn new(index: String, source: Option, target: Option) -> Self { - Self { - index, - source: source.map(Box::new), - target: target.map(Box::new), - } - } - - fn is_leaf(&self) -> bool { - self.source.is_none() && self.target.is_none() - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct ResolvedLink { - index: u32, - source: u32, - target: u32, - name: Option, -} - -impl ResolvedLink { - fn new(index: u32, source: u32, target: u32, name: Option) -> Self { - Self { - index, - source, - target, - name, - } - } - - fn to_link(&self) -> Link { - Link::new(self.index, self.source, self.target) - } -} - impl QueryProcessor { /// Creates a new QueryProcessor pub fn new(trace: bool) -> Self { @@ -85,7 +42,7 @@ impl QueryProcessor { /// Processes a LiNo query and returns the list of changes pub fn process_query( &self, - storage: &mut LinkStorage, + storage: &mut impl NamedTypeLinks, query: &str, ) -> Result, Option)>> { self.trace_msg(&format!("[ProcessQuery] Query: \"{}\"", query)); @@ -159,8 +116,8 @@ impl QueryProcessor { "[ProcessQuery] Created link ID #{} from substitution pattern.", created_id )); - if let Some(link) = storage.get(created_id) { - changes_list.push((None, Some(*link))); + if let Some(link) = storage.get_link(created_id) { + changes_list.push((None, Some(link))); } } } @@ -183,7 +140,7 @@ impl QueryProcessor { let restriction_patterns = self.patterns_from_lino(restriction_link); let mut links_to_delete = Vec::new(); for pattern in &restriction_patterns { - links_to_delete.extend(self.matched_links(storage, pattern, &HashMap::new())); + links_to_delete.extend(self.matched_links(storage, pattern, &HashMap::new())?); } links_to_delete.sort_by_key(|link| link.index); links_to_delete.dedup_by_key(|link| link.index); @@ -217,7 +174,7 @@ impl QueryProcessor { .into_iter() .map(|link| (None, Some(link))), ); - let solutions = self.find_all_solutions(storage, &restriction_patterns); + let solutions = self.find_all_solutions(storage, &restriction_patterns)?; if solutions.is_empty() { self.trace_msg("[ProcessQuery] No solutions found => returning."); @@ -227,19 +184,23 @@ impl QueryProcessor { return Ok(changes_list); } - let all_solutions_no_operation = solutions.iter().all(|solution| { - self.solution_is_no_operation( + let mut all_solutions_no_operation = true; + for solution in &solutions { + if !self.solution_is_no_operation( storage, solution, &restriction_patterns, &substitution_patterns, - ) - }); + )? { + all_solutions_no_operation = false; + break; + } + } if all_solutions_no_operation { for solution in &solutions { for pattern in &restriction_patterns { - for link in self.matched_links(storage, pattern, solution) { + for link in self.matched_links(storage, pattern, solution)? { if !changes_list.contains(&(Some(link), Some(link))) { changes_list.push((Some(link), Some(link))); } @@ -270,7 +231,7 @@ impl QueryProcessor { fn validate_links_exist_or_will_be_created( &self, - storage: &mut LinkStorage, + storage: &mut impl NamedTypeLinks, restriction_patterns: &[LinoLink], substitution_patterns: &[LinoLink], ) -> Result> { @@ -315,15 +276,15 @@ impl QueryProcessor { fn find_all_solutions( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, patterns: &[Pattern], - ) -> Vec> { + ) -> Result>> { let mut partial_solutions = vec![HashMap::new()]; for pattern in patterns { let mut new_solutions = Vec::new(); for solution in &partial_solutions { - for match_solution in self.match_pattern(storage, pattern, solution) { + for match_solution in self.match_pattern(storage, pattern, solution)? { if Self::solutions_are_compatible(solution, &match_solution) { let mut combined = solution.clone(); combined.extend(match_solution); @@ -337,7 +298,7 @@ impl QueryProcessor { } } - partial_solutions + Ok(partial_solutions) } fn solutions_are_compatible( @@ -351,14 +312,15 @@ impl QueryProcessor { fn match_pattern( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, pattern: &Pattern, current_solution: &HashMap, - ) -> Vec> { + ) -> Result>> { if pattern.is_leaf() { - let resolved_index = self.resolve_match_id(storage, &pattern.index, current_solution); - return storage - .all() + let resolved_index = + self.resolve_match_id(storage, &pattern.index, current_solution)?; + return Ok(storage + .all_links() .into_iter() .filter(|link| Self::is_any(resolved_index) || link.index == resolved_index) .map(|link| { @@ -366,39 +328,41 @@ impl QueryProcessor { Self::assign_variable(&pattern.index, link.index, &mut assignments); assignments }) - .collect(); + .collect()); } - let resolved_index = self.resolve_match_id(storage, &pattern.index, current_solution); + let resolved_index = self.resolve_match_id(storage, &pattern.index, current_solution)?; if !Self::is_variable(&pattern.index) && !Self::is_any(resolved_index) && resolved_index != 0 && storage.exists(resolved_index) { - let link = *storage.get(resolved_index).unwrap(); + let link = storage.get_link(resolved_index).unwrap(); return self.match_link_against_pattern(storage, pattern, link, current_solution); } - storage - .all() - .into_iter() - .copied() - .flat_map(|link| { - self.match_link_against_pattern(storage, pattern, link, current_solution) - }) - .collect() + let mut results = Vec::new(); + for link in storage.all_links() { + results.extend(self.match_link_against_pattern( + storage, + pattern, + link, + current_solution, + )?); + } + Ok(results) } fn match_link_against_pattern( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, pattern: &Pattern, link: Link, current_solution: &HashMap, - ) -> Vec> { - if !self.check_id_match(storage, &pattern.index, link.index, current_solution) { - return Vec::new(); + ) -> Result>> { + if !self.check_id_match(storage, &pattern.index, link.index, current_solution)? { + return Ok(Vec::new()); } let mut results = Vec::new(); @@ -407,7 +371,7 @@ impl QueryProcessor { pattern.source.as_deref(), link.source, current_solution, - ); + )?; for source_solution in source_matches { let target_matches = self.recursive_match_subpattern( @@ -415,38 +379,38 @@ impl QueryProcessor { pattern.target.as_deref(), link.target, &source_solution, - ); + )?; for mut target_solution in target_matches { Self::assign_variable(&pattern.index, link.index, &mut target_solution); results.push(target_solution); } } - results + Ok(results) } fn recursive_match_subpattern( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, pattern: Option<&Pattern>, link_id: u32, current_solution: &HashMap, - ) -> Vec> { + ) -> Result>> { let Some(pattern) = pattern else { - return vec![current_solution.clone()]; + return Ok(vec![current_solution.clone()]); }; if pattern.is_leaf() { - if self.check_id_match(storage, &pattern.index, link_id, current_solution) { + if self.check_id_match(storage, &pattern.index, link_id, current_solution)? { let mut solution = current_solution.clone(); Self::assign_variable(&pattern.index, link_id, &mut solution); - return vec![solution]; + return Ok(vec![solution]); } - return Vec::new(); + return Ok(Vec::new()); } - let Some(link) = storage.get(link_id).copied() else { - return Vec::new(); + let Some(link) = storage.get_link(link_id) else { + return Ok(Vec::new()); }; self.match_link_against_pattern(storage, pattern, link, current_solution) @@ -454,90 +418,91 @@ impl QueryProcessor { fn check_id_match( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, pattern_id: &str, candidate_id: u32, current_solution: &HashMap, - ) -> bool { + ) -> Result { if pattern_id.is_empty() || pattern_id == "*" { - return true; + return Ok(true); } if Self::is_variable(pattern_id) { - return current_solution + return Ok(current_solution .get(pattern_id) - .is_none_or(|existing| *existing == candidate_id); + .is_none_or(|existing| *existing == candidate_id)); } if let Ok(parsed) = pattern_id.parse::() { - return parsed == candidate_id; + return Ok(parsed == candidate_id); } - storage - .get_by_name(pattern_id) - .is_some_and(|named_id| named_id == candidate_id) + Ok(storage + .get_by_name(pattern_id)? + .is_some_and(|named_id| named_id == candidate_id)) } fn resolve_match_id( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, identifier: &str, current_solution: &HashMap, - ) -> u32 { + ) -> Result { if identifier.is_empty() || identifier == "*" { - return u32::MAX; + return Ok(u32::MAX); } if let Some(value) = current_solution.get(identifier) { - return *value; + return Ok(*value); } if Self::is_variable(identifier) { - return u32::MAX; + return Ok(u32::MAX); } if let Ok(parsed) = identifier.parse::() { - return parsed; + return Ok(parsed); } - storage.get_by_name(identifier).unwrap_or(0) + Ok(storage.get_by_name(identifier)?.unwrap_or(0)) } fn matched_links( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, pattern: &Pattern, solution: &HashMap, - ) -> Vec { + ) -> Result> { if pattern.is_leaf() { - let resolved_index = self.resolve_match_id(storage, &pattern.index, solution); - return storage - .all() + let resolved_index = self.resolve_match_id(storage, &pattern.index, solution)?; + return Ok(storage + .all_links() .into_iter() .filter(|link| Self::is_any(resolved_index) || link.index == resolved_index) - .copied() - .collect(); + .collect()); } - self.match_pattern(storage, pattern, solution) - .into_iter() - .filter_map(|matched_solution| { - self.resolve_pattern_readonly(storage, pattern, &matched_solution, false) - }) - .flat_map(|definition| self.links_matching_definition(storage, &definition)) - .collect() + let mut links = Vec::new(); + for matched_solution in self.match_pattern(storage, pattern, solution)? { + if let Some(definition) = + self.resolve_pattern_readonly(storage, pattern, &matched_solution, false)? + { + links.extend(self.links_matching_definition(storage, &definition)?); + } + } + Ok(links) } fn solution_is_no_operation( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, solution: &HashMap, restrictions: &[Pattern], substitutions: &[Pattern], - ) -> bool { + ) -> Result { let mut restriction_links = self - .resolve_patterns_readonly(storage, restrictions, solution, false) + .resolve_patterns_readonly(storage, restrictions, solution, false)? .into_iter() .map(|definition| definition.to_link()) .collect::>(); let mut substitution_links = self - .resolve_patterns_readonly(storage, substitutions, solution, true) + .resolve_patterns_readonly(storage, substitutions, solution, true)? .into_iter() .map(|definition| definition.to_link()) .collect::>(); @@ -545,92 +510,96 @@ impl QueryProcessor { restriction_links.sort_by_key(|link| link.index); substitution_links.sort_by_key(|link| link.index); - restriction_links == substitution_links + Ok(restriction_links == substitution_links) } fn resolve_patterns_readonly( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, patterns: &[Pattern], solution: &HashMap, is_substitution: bool, - ) -> Vec { - patterns - .iter() - .filter_map(|pattern| { - self.resolve_pattern_readonly(storage, pattern, solution, is_substitution) - }) - .collect() + ) -> Result> { + let mut resolved = Vec::new(); + for pattern in patterns { + if let Some(link) = + self.resolve_pattern_readonly(storage, pattern, solution, is_substitution)? + { + resolved.push(link); + } + } + Ok(resolved) } fn resolve_pattern_readonly( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, pattern: &Pattern, solution: &HashMap, is_substitution: bool, - ) -> Option { + ) -> Result> { if pattern.is_leaf() { let index = self.resolve_identifier_readonly( storage, &pattern.index, solution, if is_substitution { 0 } else { u32::MAX }, - ); - return Some(ResolvedLink::new(index, u32::MAX, u32::MAX, None)); + )?; + return Ok(Some(ResolvedLink::new(index, u32::MAX, u32::MAX, None))); } + let source_pattern = pattern + .source + .as_deref() + .ok_or_else(|| LinkError::InvalidFormat("Invalid source pattern".to_string()))?; + let target_pattern = pattern + .target + .as_deref() + .ok_or_else(|| LinkError::InvalidFormat("Invalid target pattern".to_string()))?; + let source = self - .resolve_pattern_readonly( - storage, - pattern.source.as_deref()?, - solution, - is_substitution, - )? + .resolve_pattern_readonly(storage, source_pattern, solution, is_substitution)? + .ok_or_else(|| LinkError::InvalidFormat("Invalid source pattern".to_string()))? .index; let target = self - .resolve_pattern_readonly( - storage, - pattern.target.as_deref()?, - solution, - is_substitution, - )? + .resolve_pattern_readonly(storage, target_pattern, solution, is_substitution)? + .ok_or_else(|| LinkError::InvalidFormat("Invalid target pattern".to_string()))? .index; let default_index = if is_substitution { 0 } else { u32::MAX }; let index = - self.resolve_identifier_readonly(storage, &pattern.index, solution, default_index); + self.resolve_identifier_readonly(storage, &pattern.index, solution, default_index)?; - Some(ResolvedLink::new(index, source, target, None)) + Ok(Some(ResolvedLink::new(index, source, target, None))) } fn resolve_identifier_readonly( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, identifier: &str, solution: &HashMap, default_value: u32, - ) -> u32 { + ) -> Result { if identifier.is_empty() { - return default_value; + return Ok(default_value); } if identifier == "*" { - return u32::MAX; + return Ok(u32::MAX); } if let Some(value) = solution.get(identifier) { - return *value; + return Ok(*value); } if Self::is_variable(identifier) { - return default_value; + return Ok(default_value); } if let Ok(parsed) = identifier.parse::() { - return parsed; + return Ok(parsed); } - storage.get_by_name(identifier).unwrap_or(default_value) + Ok(storage.get_by_name(identifier)?.unwrap_or(default_value)) } fn resolve_patterns( &self, - storage: &mut LinkStorage, + storage: &mut impl NamedTypeLinks, patterns: &[Pattern], solution: &HashMap, is_substitution: bool, @@ -643,7 +612,7 @@ impl QueryProcessor { fn resolve_pattern( &self, - storage: &mut LinkStorage, + storage: &mut impl NamedTypeLinks, pattern: &Pattern, solution: &HashMap, is_substitution: bool, @@ -697,7 +666,7 @@ impl QueryProcessor { fn resolve_identifier( &self, - storage: &mut LinkStorage, + storage: &mut impl NamedTypeLinks, identifier: &str, solution: &HashMap, default_value: u32, @@ -718,11 +687,11 @@ impl QueryProcessor { if let Ok(parsed) = identifier.parse::() { return Ok(parsed); } - if let Some(named_id) = storage.get_by_name(identifier) { + if let Some(named_id) = storage.get_by_name(identifier)? { return Ok(named_id); } if create_named_leaf { - return Ok(storage.get_or_create_named(identifier)); + return storage.get_or_create_named(identifier); } Ok(default_value) } @@ -792,14 +761,14 @@ impl QueryProcessor { fn apply_operation( &self, - storage: &mut LinkStorage, + storage: &mut impl NamedTypeLinks, before: Option, after: Option, changes: &mut Vec<(Option, Option)>, ) -> Result<()> { match (before, after) { (Some(before), None) => { - let mut links = self.links_matching_definition(storage, &before); + let mut links = self.links_matching_definition(storage, &before)?; links.sort_by_key(|link| link.index); links.dedup_by_key(|link| link.index); for link in links { @@ -815,14 +784,14 @@ impl QueryProcessor { } (Some(before), Some(after)) => { if before.index == after.index && storage.exists(before.index) { - let before_link = *storage.get(before.index).unwrap(); + let before_link = storage.get_link(before.index).unwrap(); if before_link.source != after.source || before_link.target != after.target { storage.update(before.index, after.source, after.target)?; } if let Some(name) = &after.name { - storage.set_name(before.index, name); + storage.set_name(before.index, name)?; } - let after_link = *storage.get(before.index).unwrap(); + let after_link = storage.get_link(before.index).unwrap(); changes.push((Some(before_link), Some(after_link))); } else { self.apply_operation(storage, Some(before), None, changes)?; @@ -837,7 +806,7 @@ impl QueryProcessor { fn create_or_update_resolved_link( &self, - storage: &mut LinkStorage, + storage: &mut impl NamedTypeLinks, definition: &ResolvedLink, ) -> Result { let id = if Self::is_normal_index(definition.index) { @@ -851,19 +820,19 @@ impl QueryProcessor { }; if let Some(name) = &definition.name { - storage.set_name(id, name); + storage.set_name(id, name)?; } - Ok(*storage.get(id).unwrap()) + Ok(storage.get_link(id).unwrap()) } fn links_matching_definition( &self, - storage: &LinkStorage, + storage: &mut impl NamedTypeLinks, definition: &ResolvedLink, - ) -> Vec { - storage - .all() + ) -> Result> { + Ok(storage + .all_links() .into_iter() .filter(|link| { (definition.index == 0 @@ -872,8 +841,7 @@ impl QueryProcessor { && (Self::is_any(definition.source) || link.source == definition.source) && (Self::is_any(definition.target) || link.target == definition.target) }) - .copied() - .collect() + .collect()) } fn assign_variable(id: &str, value: u32, assignments: &mut HashMap) { @@ -899,7 +867,11 @@ impl QueryProcessor { } /// Ensures a link is created from a LinoLink pattern - fn ensure_link_created(&self, storage: &mut LinkStorage, lino_link: &LinoLink) -> Result { + fn ensure_link_created( + &self, + storage: &mut impl NamedTypeLinks, + lino_link: &LinoLink, + ) -> Result { // Handle leaf nodes (names or numbers) if !lino_link.has_values() { if let Some(ref id) = lino_link.id { @@ -913,7 +885,7 @@ impl QueryProcessor { } // It's a name - get or create - return Ok(storage.get_or_create_named(id)); + return storage.get_or_create_named(id); } return Ok(0); } @@ -937,13 +909,13 @@ impl QueryProcessor { storage.get_or_create(source_id, target_id) } else { // Named link - let existing = storage.get_by_name(id); + let existing = storage.get_by_name(id)?; if let Some(id_num) = existing { storage.update(id_num, source_id, target_id)?; id_num } else { let new_id = storage.create(source_id, target_id); - storage.set_name(new_id, id); + storage.set_name(new_id, id)?; new_id } } diff --git a/rust/src/query_types.rs b/rust/src/query_types.rs new file mode 100644 index 0000000..8b3cb54 --- /dev/null +++ b/rust/src/query_types.rs @@ -0,0 +1,45 @@ +use crate::link::Link; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct Pattern { + pub(crate) index: String, + pub(crate) source: Option>, + pub(crate) target: Option>, +} + +impl Pattern { + pub(crate) fn new(index: String, source: Option, target: Option) -> Self { + Self { + index, + source: source.map(Box::new), + target: target.map(Box::new), + } + } + + pub(crate) fn is_leaf(&self) -> bool { + self.source.is_none() && self.target.is_none() + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct ResolvedLink { + pub(crate) index: u32, + pub(crate) source: u32, + pub(crate) target: u32, + pub(crate) name: Option, +} + +impl ResolvedLink { + pub(crate) fn new(index: u32, source: u32, target: u32, name: Option) -> Self { + Self { + index, + source, + target, + name, + } + } + + pub(crate) fn to_link(&self) -> Link { + Link::new(self.index, self.source, self.target) + } +} diff --git a/rust/tests/cli_named_types_tests.rs b/rust/tests/cli_named_types_tests.rs new file mode 100644 index 0000000..244cd7e --- /dev/null +++ b/rust/tests/cli_named_types_tests.rs @@ -0,0 +1,42 @@ +use anyhow::{ensure, Result}; +use link_cli::NamedTypesDecorator; +use std::process::Command; +use tempfile::tempdir; + +#[test] +fn cli_stores_string_aliases_in_separate_names_database() -> Result<()> { + let temp_dir = tempdir()?; + let db_path = temp_dir.path().join("string-ids.links"); + let names_path = NamedTypesDecorator::make_names_database_filename(&db_path); + + let output = Command::new(env!("CARGO_BIN_EXE_clink")) + .args([ + "--db", + db_path.to_str().unwrap(), + "--auto-create-missing-references", + "() ((child: father mother))", + "--after", + ]) + .output()?; + + ensure!( + output.status.success(), + "clink failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8(output.stdout)?; + assert!(stdout.contains("(father: father father)")); + assert!(stdout.contains("(mother: mother mother)")); + assert!(stdout.contains("(child: father mother)")); + + let main_database = std::fs::read_to_string(&db_path)?; + assert!(!main_database.contains("father")); + assert!(!main_database.contains("mother")); + assert!(!main_database.contains("child")); + + let names_database = std::fs::read_to_string(&names_path)?; + assert!(!names_database.trim().is_empty()); + + Ok(()) +} diff --git a/rust/tests/query_processor_csharp_parity_tests.rs b/rust/tests/query_processor_csharp_parity_tests.rs index eb31988..a955515 100644 --- a/rust/tests/query_processor_csharp_parity_tests.rs +++ b/rust/tests/query_processor_csharp_parity_tests.rs @@ -1,30 +1,40 @@ //! C# AdvancedMixedQueryProcessor parity tests. use anyhow::Result; -use link_cli::{Link, LinkStorage, QueryProcessor}; +use link_cli::{Link, NamedTypes, NamedTypesDecorator, QueryProcessor}; use tempfile::NamedTempFile; -fn with_storage(test: impl FnOnce(&mut LinkStorage, &QueryProcessor) -> Result<()>) -> Result<()> { +fn with_storage( + test: impl FnOnce(&mut NamedTypesDecorator, &QueryProcessor) -> Result<()>, +) -> Result<()> { let temp_file = NamedTempFile::new()?; + let names_file = NamedTempFile::new()?; let db_path = temp_file.path().to_str().unwrap(); - let mut storage = LinkStorage::new(db_path, false)?; + let names_path = names_file.path().to_str().unwrap(); + let mut storage = NamedTypesDecorator::with_names_database_path(db_path, names_path, false)?; let processor = QueryProcessor::new(false).with_auto_create_missing_references(true); test(&mut storage, &processor) } -fn sorted_links(storage: &LinkStorage) -> Vec { +fn sorted_links(storage: &NamedTypesDecorator) -> Vec { let mut links: Vec = storage.all().into_iter().copied().collect(); links.sort_by_key(|link| link.index); links } -fn assert_link_exists(storage: &LinkStorage, index: u32, source: u32, target: u32) { +fn assert_link_exists(storage: &NamedTypesDecorator, index: u32, source: u32, target: u32) { let link = storage .get(index) .unwrap_or_else(|| panic!("missing link {index}: {source} {target}")); assert_eq!(*link, Link::new(index, source, target)); } +fn name_id(storage: &mut NamedTypesDecorator, name: &str) -> Result { + Ok(storage + .get_by_name(name)? + .unwrap_or_else(|| panic!("{name} should exist"))) +} + #[test] fn test_unwrapped_create_query_matches_csharp() -> Result<()> { with_storage(|storage, processor| { @@ -151,10 +161,10 @@ fn test_named_link_rename_matches_csharp() -> Result<()> { processor.process_query(storage, "(((child: father mother)) ((son: father mother)))")?; - assert_eq!(storage.get_by_name("child"), None); - let son_id = storage.get_by_name("son").expect("son should exist"); - let father_id = storage.get_by_name("father").expect("father should exist"); - let mother_id = storage.get_by_name("mother").expect("mother should exist"); + assert_eq!(storage.get_by_name("child")?, None); + let son_id = name_id(storage, "son")?; + let father_id = name_id(storage, "father")?; + let mother_id = name_id(storage, "mother")?; assert_link_exists(storage, son_id, father_id, mother_id); assert_eq!(storage.all().len(), 3); Ok(()) @@ -168,9 +178,9 @@ fn test_delete_by_names_keeps_leaf_names_matches_csharp() -> Result<()> { processor.process_query(storage, "(((child: father mother)) ())")?; - assert_eq!(storage.get_by_name("child"), None); - assert!(storage.get_by_name("father").is_some()); - assert!(storage.get_by_name("mother").is_some()); + assert_eq!(storage.get_by_name("child")?, None); + assert!(storage.get_by_name("father")?.is_some()); + assert!(storage.get_by_name("mother")?.is_some()); assert_eq!(storage.all().len(), 2); Ok(()) }) @@ -190,8 +200,8 @@ fn test_unknown_named_restriction_fails_without_auto_create() -> Result<()> { assert!(error .to_string() .contains("--auto-create-missing-references")); - assert!(storage.get_by_name("known").is_some()); - assert!(storage.get_by_name("unknown").is_none()); + assert!(storage.get_by_name("known")?.is_some()); + assert!(storage.get_by_name("unknown")?.is_none()); Ok(()) }) } @@ -202,11 +212,33 @@ fn test_string_composite_left_child_does_not_create_extra_leaf() -> Result<()> { processor.process_query(storage, "(() ((type: type type)))")?; processor.process_query(storage, "(() ((link: link type)))")?; - let type_id = storage.get_by_name("type").expect("type should exist"); - let link_id = storage.get_by_name("link").expect("link should exist"); + let type_id = name_id(storage, "type")?; + let link_id = name_id(storage, "link")?; assert_eq!(storage.all().len(), 2); assert_link_exists(storage, type_id, type_id, type_id); assert_link_exists(storage, link_id, link_id, type_id); Ok(()) }) } + +#[test] +fn test_string_aliases_in_variable_restriction_constrain_matches_to_named_links_matches_csharp( +) -> Result<()> { + with_storage(|storage, processor| { + processor.process_query(storage, "(() ((father: father father)))")?; + processor.process_query(storage, "(() ((mother: mother mother)))")?; + processor.process_query(storage, "(() ((child: father mother)))")?; + + let father_id = name_id(storage, "father")?; + let mother_id = name_id(storage, "mother")?; + let child_id = name_id(storage, "child")?; + + processor.process_query(storage, "((($id: father mother)) (($id: mother father)))")?; + + assert_eq!(storage.all().len(), 3); + assert_link_exists(storage, father_id, father_id, father_id); + assert_link_exists(storage, mother_id, mother_id, mother_id); + assert_link_exists(storage, child_id, mother_id, father_id); + Ok(()) + }) +}