/
semcompilation.go
316 lines (287 loc) · 8.57 KB
/
semcompilation.go
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
// Copyright 2021 Cloud Privacy Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ls
import (
"fmt"
"strings"
"github.com/cloudprivacylabs/lpg/v2"
"github.com/cloudprivacylabs/opencypher"
)
// An instance of CompileContext is passed to all compile
// functions. It contains a statement cache to prevent recompiling
// same statemement multiple times
type CompileContext struct {
statementCache map[any]any
}
// GetCompiledStatement returns the compiled statement if any
func (c *CompileContext) GetCompiledStatement(statement any) (any, bool) {
if c.statementCache == nil {
return nil, false
}
stmt, exists := c.statementCache[statement]
return stmt, exists
}
// SetCompiledStatement sets a compiled statement
func (c *CompileContext) SetCompiledStatement(statement, compiled any) {
if c.statementCache == nil {
c.statementCache = make(map[any]any)
}
c.statementCache[statement] = compiled
}
func (c *CompileContext) CompileOpencypher(statement string) (opencypher.Evaluatable, error) {
compiled, exists := c.GetCompiledStatement(statement)
if exists {
return compiled.(opencypher.Evaluatable), nil
}
compiled, err := opencypher.Parse(statement)
if err != nil {
return nil, err
}
c.SetCompiledStatement(statement, compiled)
return compiled.(opencypher.Evaluatable), nil
}
// NodeCompiler interface represents term compilation algorithm when
// the term is a node.
//
// During schema compilation, if a node is found to be a semantic
// annotation node (i.e. not an attribute node), and if the term
// metadata for the node label implements NodeCompiler, this function
// is called to compile the node.
type NodeCompiler interface {
// CompileNode gets a node and compiles the associated term on that
// node. It should store the compiled state into node.Compiled with
// the an opaque key
CompileNode(*CompileContext, *Layer, *lpg.Node) error
}
// EdgeCompiler interface represents term compilation algorithm when
// the term is an edge
//
// During schema compilation, if the term metadata for the edge label
// implements EdgeCompiler, this method is called.
type EdgeCompiler interface {
// CompileEdge gets an edge and compiles the associated term on that
// edge. It should store the compiled state into edge.Compiled with
// an opaque key
CompileEdge(*CompileContext, *Layer, *lpg.Edge) error
}
// CompilablePropertyContainer contains properties and a compiled data map
type CompilablePropertyContainer interface {
GetProperty(string) (interface{}, bool)
SetProperty(string, interface{})
}
// TermCompiler interface represents term compilation algorithm. This
// is used to compile terms stored as node/edge properties. If the
// term metadata implements TermCompiler, this method is called, and
// the result is stored in the Compiled map of the node/edge with the
// term as the key.
type TermCompiler interface {
// CompileTerm gets a node or edge, the term and its value, and
// compiles it. It can store compilation data in the compiled data
// map.
CompileTerm(*CompileContext, CompilablePropertyContainer, string, *PropertyValue) error
}
type emptyCompiler struct{}
// CompileNode returns the value unmodified
func (emptyCompiler) CompileNode(*CompileContext, *Layer, *lpg.Node) error { return nil }
func (emptyCompiler) CompileEdge(*CompileContext, *Layer, *lpg.Edge) error { return nil }
func (emptyCompiler) CompileTerm(*CompileContext, CompilablePropertyContainer, string, *PropertyValue) error {
return nil
}
// GetNodeCompiler return a compiler that will compile the term when
// the term is a node label
func GetNodeCompiler(term string) NodeCompiler {
md := GetTermMetadata(term)
if md == nil {
return emptyCompiler{}
}
c, ok := md.(NodeCompiler)
if ok {
return c
}
return emptyCompiler{}
}
// GetEdgeCompiler return a compiler that will compile the term when
// the term is an edge label
func GetEdgeCompiler(term string) EdgeCompiler {
md := GetTermMetadata(term)
if md == nil {
return emptyCompiler{}
}
c, ok := md.(EdgeCompiler)
if ok {
return c
}
return emptyCompiler{}
}
// GetTermCompiler return a compiler that will compile the term when
// the term is a node/edge property
func GetTermCompiler(term string) TermCompiler {
md := GetTermMetadata(term)
if md == nil {
return emptyCompiler{}
}
c, ok := md.(TermCompiler)
if ok {
return c
}
return emptyCompiler{}
}
// CompiledProperties is a lazy-initialized map
type CompiledProperties struct {
m map[interface{}]interface{}
}
func (p *CompiledProperties) GetCompiledProperty(key interface{}) (interface{}, bool) {
if p.m == nil {
return nil, false
}
property, exists := p.m[key]
return property, exists
}
func (p *CompiledProperties) SetCompiledProperty(key, value interface{}) {
if p.m == nil {
p.m = make(map[interface{}]interface{})
}
p.m[key] = value
}
func (p *CompiledProperties) CopyCompiledToMap(target map[interface{}]interface{}) {
if p.m == nil {
return
}
for k, v := range p.m {
target[k] = v
}
}
func (p *CompiledProperties) CopyTo(target *CompiledProperties) {
if p.m == nil {
return
}
if target.m == nil {
target.m = make(map[interface{}]interface{})
}
for k, v := range p.m {
target.m[k] = v
}
}
// CompileOCSemantics is a compilation implementation for terms
// containing a slice of opencypher expressions. It compiles one or
// more expressions of the term, and places them im $compiled_term
// property.
type CompileOCSemantics struct{}
func (CompileOCSemantics) CompileTerm(ctx *CompileContext, target CompilablePropertyContainer, term string, value *PropertyValue) error {
if value == nil {
return nil
}
expr := make([]opencypher.Evaluatable, 0)
for _, str := range value.MustStringSlice() {
e, err := ctx.CompileOpencypher(str)
if err != nil {
return err
}
expr = append(expr, e)
}
target.SetProperty("$compiled_"+term, expr)
return nil
}
func (CompileOCSemantics) Compiled(target CompilablePropertyContainer, term string) []opencypher.Evaluatable {
x, _ := target.GetProperty("$compiled_" + term)
if x == nil {
return nil
}
ret, _ := x.([]opencypher.Evaluatable)
return ret
}
// Evaluate all compiled expressions and return nonempty resultsets.
func (c CompileOCSemantics) Evaluate(target CompilablePropertyContainer, term string, evalCtx *opencypher.EvalContext) ([]opencypher.ResultSet, error) {
ret := make([]opencypher.ResultSet, 0)
for _, expr := range c.Compiled(target, term) {
v, err := expr.Evaluate(evalCtx)
if err != nil {
return nil, err
}
if v.Get() == nil {
continue
}
rs, ok := v.Get().(opencypher.ResultSet)
if !ok {
continue
}
if len(rs.Rows) == 0 {
continue
}
ret = append(ret, rs)
}
return ret, nil
}
// DialectValueSemantics is a compilation implementation for terms
// containing an expression, or value. The property is expected to be:
//
// dialect: value
//
// For example:
//
// opencypher: expr
//
// is an expression
//
// literal: value
//
// is a literal
//
// Anything else is error
type DialectValueSemantics struct{}
type DialectValue struct {
Dialect string
Value any
}
func (DialectValueSemantics) CompileTerm(ctx *CompileContext, target CompilablePropertyContainer, term string, value *PropertyValue) error {
if value == nil {
return nil
}
result := make([]DialectValue, 0)
for _, str := range value.MustStringSlice() {
parts := strings.SplitN(str, ":", 2)
if len(parts) != 2 {
return fmt.Errorf("Invalid dialect:value expression: %s", str)
}
parts[0] = strings.TrimSpace(parts[0])
switch parts[0] {
case "opencypher":
e, err := ctx.CompileOpencypher(parts[1])
if err != nil {
return err
}
result = append(result, DialectValue{
Dialect: "opencypher",
Value: e,
})
case "literal":
result = append(result, DialectValue{
Dialect: "literal",
Value: parts[1],
})
default:
return fmt.Errorf("Unknown dialect in expression: %s", parts[0])
}
}
target.SetProperty("$compiled_"+term, result)
return nil
}
func (DialectValueSemantics) Compiled(target CompilablePropertyContainer, term string) []DialectValue {
x, _ := target.GetProperty("$compiled_" + term)
if x == nil {
return nil
}
ret, _ := x.([]DialectValue)
return ret
}