Index collection prefixes for path correspondence
This commit is contained in:
@@ -176,7 +176,15 @@ change was added to provide.
|
|||||||
|
|
||||||
## Finding 4 — Correspondence lookup repeatedly serializes growing path prefixes
|
## Finding 4 — Correspondence lookup repeatedly serializes growing path prefixes
|
||||||
|
|
||||||
**Priority:** P2. **Status:** Open.
|
**Priority:** P2. **Status:** Complete.
|
||||||
|
|
||||||
|
Completion: eligible collection prefixes now use the existing prefix index.
|
||||||
|
Translation formats the requested pointer once, retains concrete path-kind
|
||||||
|
checks, and copies the resulting path independently. No-eligible cases return
|
||||||
|
positionally without formatting prefixes; no-registration loads skip matching
|
||||||
|
construction entirely. Cloning rebuilds an independently owned index. Existing
|
||||||
|
matching/history/ownership coverage and a numeric-key/escaped-key regression
|
||||||
|
passed, along with `./scripts/check.sh`.
|
||||||
|
|
||||||
**Code:** [correspondence translations](../../internal/engine/resolution_correspondence.go),
|
**Code:** [correspondence translations](../../internal/engine/resolution_correspondence.go),
|
||||||
`entryPath` at lines 96–125 and `finalPath` at lines 129–156; consumers include
|
`entryPath` at lines 96–125 and `finalPath` at lines 129–156; consumers include
|
||||||
|
|||||||
@@ -13,28 +13,45 @@ type collectionCorrespondence struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type resolutionCorrespondence struct {
|
type resolutionCorrespondence struct {
|
||||||
collections map[string]collectionCorrespondence
|
collections map[string]collectionCorrespondence
|
||||||
|
prefix pathPrefixIndex
|
||||||
|
indexedCollections []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c resolutionCorrespondence) clone() resolutionCorrespondence {
|
func (c resolutionCorrespondence) clone() resolutionCorrespondence {
|
||||||
result := resolutionCorrespondence{collections: make(map[string]collectionCorrespondence, len(c.collections))}
|
var result resolutionCorrespondence
|
||||||
for key, collection := range c.collections {
|
for _, collection := range c.collections {
|
||||||
collection.path = collection.path.clone()
|
collection.path = collection.path.clone()
|
||||||
collection.entryToFinal = append([]int(nil), collection.entryToFinal...)
|
collection.entryToFinal = append([]int(nil), collection.entryToFinal...)
|
||||||
collection.finalToEntry = append([]int(nil), collection.finalToEntry...)
|
collection.finalToEntry = append([]int(nil), collection.finalToEntry...)
|
||||||
result.collections[key] = collection
|
result.indexCollection(collection)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *resolutionCorrespondence) indexCollection(collection collectionCorrespondence) {
|
||||||
|
if c.collections == nil {
|
||||||
|
c.collections = make(map[string]collectionCorrespondence)
|
||||||
|
}
|
||||||
|
pointer := collection.path.String()
|
||||||
|
c.collections[pointer] = collection
|
||||||
|
if collection.eligible {
|
||||||
|
c.prefix.insert(pointer, len(c.indexedCollections))
|
||||||
|
c.indexedCollections = append(c.indexedCollections, pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func buildResolutionCorrespondence(entry, final *document.Node, policy sequencePolicy) resolutionCorrespondence {
|
func buildResolutionCorrespondence(entry, final *document.Node, policy sequencePolicy) resolutionCorrespondence {
|
||||||
result := resolutionCorrespondence{collections: make(map[string]collectionCorrespondence)}
|
var result resolutionCorrespondence
|
||||||
|
if policy.selectorTrie == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
var indexes mappingIndex
|
var indexes mappingIndex
|
||||||
var visit func(*document.Node, *document.Node, Path)
|
var visit func(*document.Node, *document.Node, Path)
|
||||||
visit = func(before, after *document.Node, path Path) {
|
visit = func(before, after *document.Node, path Path) {
|
||||||
if fields, registered := policy.identityFieldsAt(path); registered &&
|
if fields, registered := policy.identityFieldsAt(path); registered &&
|
||||||
((before != nil && before.Kind == document.Sequence) || (after != nil && after.Kind == document.Sequence)) {
|
((before != nil && before.Kind == document.Sequence) || (after != nil && after.Kind == document.Sequence)) {
|
||||||
result.collections[path.String()] = matchResolutionCollection(before, after, path, fields)
|
result.indexCollection(matchResolutionCollection(before, after, path, fields))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if before != nil && before.Kind == document.Mapping {
|
if before != nil && before.Kind == document.Mapping {
|
||||||
@@ -94,64 +111,55 @@ func matchResolutionCollection(before, after *document.Node, path Path, fields [
|
|||||||
// entryPath translates a surviving final record prefix; additions have no entry.
|
// entryPath translates a surviving final record prefix; additions have no entry.
|
||||||
// Paths outside eligible collections and wholly absent final positions stay positional.
|
// Paths outside eligible collections and wholly absent final positions stay positional.
|
||||||
func (c resolutionCorrespondence) entryPath(path Path) (Path, bool) {
|
func (c resolutionCorrespondence) entryPath(path Path) (Path, bool) {
|
||||||
if !path.valid() {
|
return c.translate(path, false)
|
||||||
return Path{}, false
|
|
||||||
}
|
|
||||||
for depth := 0; depth < len(path.segments); depth++ {
|
|
||||||
prefix := Path{segments: path.segments[:depth]}
|
|
||||||
collection, found := c.collections[prefix.String()]
|
|
||||||
if !found || !collection.eligible {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
compatible := true
|
|
||||||
for i, segment := range collection.path.segments {
|
|
||||||
if segment.kind == pathSegmentKey && path.segments[i].kind == pathSegmentIndex {
|
|
||||||
compatible = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
index, ok := schemaIndex(path.segments[depth])
|
|
||||||
if !compatible || !ok || index >= len(collection.finalToEntry) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
previous := collection.finalToEntry[index]
|
|
||||||
if previous < 0 {
|
|
||||||
return Path{}, false
|
|
||||||
}
|
|
||||||
translated := path.clone()
|
|
||||||
translated.segments[depth] = pathSegment{kind: pathSegmentIndex, index: previous}
|
|
||||||
return translated, true
|
|
||||||
}
|
|
||||||
return path.clone(), true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// finalPath translates historical record descendants without matching removals.
|
// finalPath translates historical record descendants without matching removals.
|
||||||
func (c resolutionCorrespondence) finalPath(path Path) (Path, bool) {
|
func (c resolutionCorrespondence) finalPath(path Path) (Path, bool) {
|
||||||
|
return c.translate(path, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c resolutionCorrespondence) translate(path Path, toFinal bool) (Path, bool) {
|
||||||
if !path.valid() {
|
if !path.valid() {
|
||||||
return Path{}, false
|
return Path{}, false
|
||||||
}
|
}
|
||||||
for depth := 0; depth < len(path.segments); depth++ {
|
translated := path.clone()
|
||||||
prefix := Path{segments: path.segments[:depth]}
|
if len(c.indexedCollections) == 0 {
|
||||||
collection, found := c.collections[prefix.String()]
|
return translated, true
|
||||||
if !found || !collection.eligible {
|
}
|
||||||
continue
|
matched, handled := true, false
|
||||||
|
c.prefix.visitAncestors(path.String(), func(id int) {
|
||||||
|
if handled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
collection := c.collections[c.indexedCollections[id]]
|
||||||
|
depth := len(collection.path.segments)
|
||||||
|
if depth >= len(path.segments) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
for i, segment := range collection.path.segments {
|
for i, segment := range collection.path.segments {
|
||||||
if segment.kind == pathSegmentKey && path.segments[i].kind == pathSegmentIndex {
|
if segment.kind == pathSegmentKey && path.segments[i].kind == pathSegmentIndex {
|
||||||
return path.clone(), true
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
index, ok := schemaIndex(path.segments[depth])
|
index, ok := schemaIndex(path.segments[depth])
|
||||||
if !ok || index >= len(collection.entryToFinal) {
|
indices := collection.finalToEntry
|
||||||
continue
|
if toFinal {
|
||||||
|
indices = collection.entryToFinal
|
||||||
}
|
}
|
||||||
current := collection.entryToFinal[index]
|
if !ok || index >= len(indices) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handled = true
|
||||||
|
current := indices[index]
|
||||||
if current < 0 {
|
if current < 0 {
|
||||||
return Path{}, false
|
matched = false
|
||||||
|
return
|
||||||
}
|
}
|
||||||
translated := path.clone()
|
|
||||||
translated.segments[depth] = pathSegment{kind: pathSegmentIndex, index: current}
|
translated.segments[depth] = pathSegment{kind: pathSegmentIndex, index: current}
|
||||||
return translated, true
|
})
|
||||||
|
if !matched {
|
||||||
|
return Path{}, false
|
||||||
}
|
}
|
||||||
return path.clone(), true
|
return translated, true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,3 +143,45 @@ func TestCyclicMatchingProtectsRemovedAndDerivedValues(t *testing.T) {
|
|||||||
t.Fatal("effective rendering leaked")
|
t.Fatal("effective rendering leaked")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCorrespondenceIndexPreservesPathShapes(t *testing.T) {
|
||||||
|
destination := Keys("groups", "0", "a/b~")
|
||||||
|
policy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{{
|
||||||
|
selector: sequenceIdentitySelector{
|
||||||
|
literalSequenceSelector("groups"), literalSequenceSelector("0"), literalSequenceSelector("a/b~"),
|
||||||
|
},
|
||||||
|
identityFields: []string{"id"},
|
||||||
|
}})
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatal(diagnostics)
|
||||||
|
}
|
||||||
|
before := parseCompositionDocument(t, "before", "groups: {'0': {'a/b~': [{id: a}, {id: b}]}}")
|
||||||
|
after := parseCompositionDocument(t, "after", "groups: {'0': {'a/b~': [{id: b}, {id: a}, {id: new}]}}")
|
||||||
|
matching := buildResolutionCorrespondence(before, after, policy)
|
||||||
|
parsed, err := ParsePath("/groups/0/a~1b~0/0/field")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, path := range []Path{destination.Index(0).Key("field"), parsed} {
|
||||||
|
entry, found := matching.entryPath(path)
|
||||||
|
if !found || entry.String() != destination.Index(1).Key("field").String() {
|
||||||
|
t.Fatalf("entry = %s, %t", entry.String(), found)
|
||||||
|
}
|
||||||
|
final, found := matching.finalPath(entry)
|
||||||
|
if !found || final.String() != path.String() {
|
||||||
|
t.Fatalf("round trip = %s, %t", final.String(), found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, path := range []Path{
|
||||||
|
Keys("groups").Index(0).Key("a/b~").Index(0),
|
||||||
|
destination.Key("0"), destination.Index(8), destination,
|
||||||
|
} {
|
||||||
|
entry, found := matching.entryPath(path)
|
||||||
|
if !found || entry.String() != path.String() {
|
||||||
|
t.Fatalf("positional path %s became %s, %t", path.String(), entry.String(), found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, found := matching.entryPath(destination.Index(2)); found {
|
||||||
|
t.Fatal("new identity acquired entry evidence")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func closeSecretDependenciesWithCorrespondence(paths map[string]Path, sensitive
|
|||||||
}
|
}
|
||||||
|
|
||||||
var projected map[string]bool
|
var projected map[string]bool
|
||||||
if len(correspondence.collections) != 0 {
|
if len(correspondence.indexedCollections) != 0 {
|
||||||
projected = make(map[string]bool, len(paths))
|
projected = make(map[string]bool, len(paths))
|
||||||
}
|
}
|
||||||
project := func(path Path) {
|
project := func(path Path) {
|
||||||
|
|||||||
Reference in New Issue
Block a user