File size: 3,826 Bytes
b110593
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
//                           _       _
// __      _____  __ ___   ___  __ _| |_ ___
// \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
//  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
//   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
//
//  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
//
//  CONTACT: [email protected]
//

package schema

import (
	"sort"

	"github.com/weaviate/weaviate/entities/filters"
	"github.com/weaviate/weaviate/entities/models"
	libschema "github.com/weaviate/weaviate/entities/schema"
)

// RefFinder is a helper that lists classes and their possible paths to to a
// desired target class.
//
// For example if the target class is "car". It might list:
// - Person, drives, Car
// - Person, owns, Car
// - Person, friendsWith, Person, drives, Car
// etc.
//
// It will stop at a preconfigured depth limit, to avoid infinite results, such
// as:
// - Person, friendsWith, Person, friendsWith, Person, ..., drives Car
type RefFinder struct {
	schemaGetter schemaGetterForRefFinder
	depthLimit   int
}

// NewRefFinder with SchemaGetter and depth limit
func NewRefFinder(getter schemaGetterForRefFinder, depthLimit int) *RefFinder {
	return &RefFinder{
		schemaGetter: getter,
		depthLimit:   depthLimit,
	}
}

type schemaGetterForRefFinder interface {
	GetSchemaSkipAuth() libschema.Schema
}

func (r *RefFinder) Find(className libschema.ClassName) []filters.Path {
	schema := r.schemaGetter.GetSchemaSkipAuth()

	var classes []*models.Class
	if schema.Objects != nil {
		classes = append(classes, schema.Objects.Classes...)
	}

	return r.findInClassList(className, classes, schema)
}

func (r *RefFinder) findInClassList(needle libschema.ClassName, classes []*models.Class,
	schema libschema.Schema,
) []filters.Path {
	var out []filters.Path

	for _, class := range classes {
		path, ok := r.hasRefTo(needle, class, schema, 1)
		if !ok {
			continue
		}

		out = append(out, path...)
	}

	return r.sortByPathLen(out)
}

func (r *RefFinder) hasRefTo(needle libschema.ClassName, class *models.Class,
	schema libschema.Schema, depth int,
) ([]filters.Path, bool) {
	var out []filters.Path

	if depth > r.depthLimit {
		return nil, false
	}

	for _, prop := range class.Properties {
		dt, err := schema.FindPropertyDataType(prop.DataType)
		if err != nil {
			// silently ignore, maybe the property was deleted in the meantime
			continue
		}

		if !dt.IsReference() {
			continue
		}

		for _, haystack := range dt.Classes() {
			refs := r.refsPerClass(needle, class, prop.Name, haystack, schema, depth)
			out = append(out, refs...)
		}
	}

	return out, len(out) > 0
}

func (r *RefFinder) refsPerClass(needle libschema.ClassName, class *models.Class,
	propName string, haystack libschema.ClassName, schema libschema.Schema,
	depth int,
) []filters.Path {
	if haystack == needle {
		// direct match
		return []filters.Path{
			{
				Class:    libschema.ClassName(class.Class),
				Property: libschema.PropertyName(propName),
				Child: &filters.Path{
					Class:    needle,
					Property: "id",
				},
			},
		}
	}

	// could still be an indirect (recursive) match
	innerClass := schema.FindClassByName(haystack)
	if innerClass == nil {
		return nil
	}
	paths, ok := r.hasRefTo(needle, innerClass, schema, depth+1)
	if !ok {
		return nil
	}

	var out []filters.Path
	for _, path := range paths {
		out = append(out, filters.Path{
			Class:    libschema.ClassName(class.Class),
			Property: libschema.PropertyName(propName),
			Child:    &path,
		})
	}

	return out
}

func (r *RefFinder) sortByPathLen(in []filters.Path) []filters.Path {
	sort.Slice(in, func(i, j int) bool {
		return len(in[i].Slice()) < len(in[j].Slice())
	})

	return in
}