Add private canonical path prefix index
This commit is contained in:
@@ -177,7 +177,7 @@ document comparison, and diagnostics.
|
|||||||
Validate: `go test ./internal/engine -run 'Resolution|Override|Overlay|Diagnostic|CompareDocument|Provenance'`
|
Validate: `go test ./internal/engine -run 'Resolution|Override|Overlay|Diagnostic|CompareDocument|Provenance'`
|
||||||
and the everyday check. Expected benefit is reduced formatting, not measured CPU.
|
and the everyday check. Expected benefit is reduced formatting, not measured CPU.
|
||||||
|
|
||||||
## Stage 6: Add a private canonical-pointer prefix index
|
## Stage 6: Add a private canonical-pointer prefix index — Complete
|
||||||
|
|
||||||
Scope: one helper plus tests, no caller migration yet.
|
Scope: one helper plus tests, no caller migration yet.
|
||||||
|
|
||||||
|
|||||||
138
internal/engine/path_prefix.go
Normal file
138
internal/engine/path_prefix.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
type pathPrefixIndex struct {
|
||||||
|
root pathPrefixNode
|
||||||
|
}
|
||||||
|
|
||||||
|
type pathPrefixNode struct {
|
||||||
|
children map[string]*pathPrefixNode
|
||||||
|
ids []int
|
||||||
|
minimumID int
|
||||||
|
hasMinimum bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *pathPrefixNode) includeID(id int) {
|
||||||
|
if !node.hasMinimum || id < node.minimumID {
|
||||||
|
node.minimumID = id
|
||||||
|
node.hasMinimum = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *pathPrefixIndex) insert(pointer string, id int) {
|
||||||
|
node := &index.root
|
||||||
|
node.includeID(id)
|
||||||
|
visitCanonicalPointerTokens(pointer, func(token string) bool {
|
||||||
|
if node.children == nil {
|
||||||
|
node.children = make(map[string]*pathPrefixNode)
|
||||||
|
}
|
||||||
|
child := node.children[token]
|
||||||
|
if child == nil {
|
||||||
|
child = &pathPrefixNode{}
|
||||||
|
node.children[token] = child
|
||||||
|
}
|
||||||
|
node = child
|
||||||
|
node.includeID(id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
node.ids = append(node.ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *pathPrefixIndex) visitAncestors(pointer string, visit func(int)) {
|
||||||
|
node := &index.root
|
||||||
|
for _, id := range node.ids {
|
||||||
|
visit(id)
|
||||||
|
}
|
||||||
|
visitCanonicalPointerTokens(pointer, func(token string) bool {
|
||||||
|
child := node.children[token]
|
||||||
|
if child == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
node = child
|
||||||
|
for _, id := range node.ids {
|
||||||
|
visit(id)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *pathPrefixIndex) visitDescendants(pointer string, visit func(int)) {
|
||||||
|
root := index.nodeAt(pointer)
|
||||||
|
if root == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var walk func(*pathPrefixNode)
|
||||||
|
walk = func(node *pathPrefixNode) {
|
||||||
|
for _, id := range node.ids {
|
||||||
|
visit(id)
|
||||||
|
}
|
||||||
|
for _, child := range node.children {
|
||||||
|
walk(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *pathPrefixIndex) earliestIntersectingID(pointer string) (int, bool) {
|
||||||
|
bestID, found := 0, false
|
||||||
|
consider := func(id int) {
|
||||||
|
if !found || id < bestID {
|
||||||
|
bestID = id
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node := &index.root
|
||||||
|
for _, id := range node.ids {
|
||||||
|
consider(id)
|
||||||
|
}
|
||||||
|
complete := visitCanonicalPointerTokens(pointer, func(token string) bool {
|
||||||
|
child := node.children[token]
|
||||||
|
if child == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
node = child
|
||||||
|
for _, id := range node.ids {
|
||||||
|
consider(id)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if complete && node.hasMinimum {
|
||||||
|
consider(node.minimumID)
|
||||||
|
}
|
||||||
|
return bestID, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *pathPrefixIndex) nodeAt(pointer string) *pathPrefixNode {
|
||||||
|
node := &index.root
|
||||||
|
complete := visitCanonicalPointerTokens(pointer, func(token string) bool {
|
||||||
|
child := node.children[token]
|
||||||
|
if child == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
node = child
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !complete {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func visitCanonicalPointerTokens(pointer string, visit func(string) bool) bool {
|
||||||
|
if pointer == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
start := 1
|
||||||
|
for {
|
||||||
|
end := start
|
||||||
|
for end < len(pointer) && pointer[end] != '/' {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
if !visit(pointer[start:end]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if end == len(pointer) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
start = end + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
81
internal/engine/path_prefix_test.go
Normal file
81
internal/engine/path_prefix_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPathPrefixIndexMatchesPointerScans(t *testing.T) {
|
||||||
|
entries := []struct {
|
||||||
|
pointer string
|
||||||
|
id int
|
||||||
|
}{
|
||||||
|
{pointer: "", id: 12},
|
||||||
|
{pointer: "/", id: 9},
|
||||||
|
{pointer: "/a", id: 6},
|
||||||
|
{pointer: "/a", id: 2},
|
||||||
|
{pointer: "/a/b", id: 4},
|
||||||
|
{pointer: "/a/b/c", id: 0},
|
||||||
|
{pointer: "/a/bc", id: 10},
|
||||||
|
{pointer: "/a~1b", id: 3},
|
||||||
|
{pointer: "/a~0b", id: 1},
|
||||||
|
{pointer: "/a~01b", id: 5},
|
||||||
|
{pointer: "/items/0", id: 8},
|
||||||
|
{pointer: "/items/01", id: 7},
|
||||||
|
{pointer: "/items/+1", id: 11},
|
||||||
|
}
|
||||||
|
var index pathPrefixIndex
|
||||||
|
for _, entry := range entries {
|
||||||
|
index.insert(entry.pointer, entry.id)
|
||||||
|
}
|
||||||
|
queries := []string{
|
||||||
|
"", "/", "/a", "/a/b", "/a/b/c", "/a/b/c/d", "/a/bc", "/a/bc/d",
|
||||||
|
"/a~1b", "/a~0b", "/a~01b", "/items", "/items/0", "/items/01", "/items/1", "/missing",
|
||||||
|
}
|
||||||
|
for _, query := range queries {
|
||||||
|
gotAncestors := make([]int, 0)
|
||||||
|
index.visitAncestors(query, func(id int) { gotAncestors = append(gotAncestors, id) })
|
||||||
|
wantAncestors := make([]int, 0)
|
||||||
|
gotDescendants := make([]int, 0)
|
||||||
|
wantDescendants := make([]int, 0)
|
||||||
|
wantEarliest, foundEarliest := 0, false
|
||||||
|
for _, entry := range entries {
|
||||||
|
if pointerContainsByString(entry.pointer, query) {
|
||||||
|
wantAncestors = append(wantAncestors, entry.id)
|
||||||
|
}
|
||||||
|
if pointerContainsByString(query, entry.pointer) {
|
||||||
|
wantDescendants = append(wantDescendants, entry.id)
|
||||||
|
}
|
||||||
|
if pointerContainsByString(entry.pointer, query) || pointerContainsByString(query, entry.pointer) {
|
||||||
|
if !foundEarliest || entry.id < wantEarliest {
|
||||||
|
wantEarliest, foundEarliest = entry.id, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index.visitDescendants(query, func(id int) { gotDescendants = append(gotDescendants, id) })
|
||||||
|
if !slices.Equal(sortedPrefixIDs(gotAncestors), sortedPrefixIDs(wantAncestors)) {
|
||||||
|
t.Errorf("ancestors(%q) = %v, want %v", query, sortedPrefixIDs(gotAncestors), sortedPrefixIDs(wantAncestors))
|
||||||
|
}
|
||||||
|
if !slices.Equal(sortedPrefixIDs(gotDescendants), sortedPrefixIDs(wantDescendants)) {
|
||||||
|
t.Errorf("descendants(%q) = %v, want %v", query, sortedPrefixIDs(gotDescendants), sortedPrefixIDs(wantDescendants))
|
||||||
|
}
|
||||||
|
gotEarliest, gotFound := index.earliestIntersectingID(query)
|
||||||
|
if gotEarliest != wantEarliest || gotFound != foundEarliest {
|
||||||
|
t.Errorf("earliestIntersectingID(%q) = %d, %v, want %d, %v", query, gotEarliest, gotFound, wantEarliest, foundEarliest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pointerContainsByString(container, descendant string) bool {
|
||||||
|
if container == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return descendant == container || strings.HasPrefix(descendant, container+"/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedPrefixIDs(ids []int) []int {
|
||||||
|
result := slices.Clone(ids)
|
||||||
|
slices.Sort(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user