• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orneryd / NornicDB / 26290860535

22 May 2026 01:36PM UTC coverage: 85.139% (-0.2%) from 85.37%
26290860535

push

github

orneryd
 fix(config,nornicdb,docker): honour search-flag precedence end-to-end

  Config values for the four search-index master switches were not flowing
  through the startup path the way the contract claimed. Symptoms (lab
  build 80719f25): operator sets NORNICDB_SEARCH_BM25_ENABLED=false via
  env or --search-bm25-enabled=false via CLI; the container's process
  shows the flag, the Go config layer reads it; default-DB warmup logs
  "Building BM25 + vector indexes for database nornic (bm25=true vector=true)"
  anyway and runs the build.

  Three independent gaps caused this:

  1. cmd/nornicdb/runServe built a fresh nornicdb.DefaultConfig() and
     hand-copied a subset of cfg fields into it. The four Search* fields
     weren't in the copy block, so env+CLI values landed in cfg but never
     reached dbConfig. Replaced the copy block: dbConfig is now an alias
     of cfg, so any field on Config flows automatically. The original
     silent-drop class of bug can't repeat.

  2. nornicdb.Open warms search indexes in a background goroutine that
     raced server.New's SetDbSearchFlagsResolver. When the resolver was
     nil at warmup time, default-DB warmup fell through to global
     defaults instead of per-DB overrides. Added Config.DeferSearchWarmup
     + db.MarkSearchWarmupReady; pkg/server opts in and releases the
     gate AFTER installing the resolver. Embedded callers (scripts,
     tests) keep today's behaviour with no extra wiring.

  3. docker/entrypoint.sh translated env vars to CLI flags but didn't
     forward the four NORNICDB_SEARCH_* vars. Added passthrough lines
     so the flags appear in `ps` / container inspect. Production binary
     path made overridable via NORNICDB_BIN for testability.

  Precedence ladder, lowest → highest, now consistent across every
  configuration source:

    1. Built-in defaults (config.LoadDefaults).
    2. Global config (YAML memory.search_*, NORNICDB_SEARCH_* env).
    3. Per-DB overrides (YAML databases: ... (continued)

21 of 22 new or added lines in 3 files covered. (95.45%)

3656 existing lines in 52 files now uncovered.

129606 of 152229 relevant lines covered (85.14%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.19
/pkg/cypher/query_patterns.go
1
// Package cypher provides query pattern detection for optimization routing.
2
//
3
// This file identifies query patterns that can be executed more efficiently
4
// than the generic traversal algorithm. Pattern detection happens BEFORE
5
// execution, allowing the executor to route to specialized implementations.
6
//
7
// Supported patterns:
8
//   - Mutual Relationship: (a)-[:TYPE]->(b)-[:TYPE]->(a) - cycle back to start
9
//   - Incoming Count Aggregation: MATCH (x)<-[:TYPE]-(y) RETURN x, count(y)
10
//   - Edge Property Aggregation: RETURN avg(r.prop), count(r) GROUP BY node
11
//   - Large Result Set: Any traversal with LIMIT > 100
12
package cypher
13

14
import (
15
        "context"
16
        "strings"
17
)
18

19
// QueryPattern identifies optimizable query structures
20
type QueryPattern int
21

22
const (
23
        // PatternGeneric is the default - use standard execution
24
        PatternGeneric QueryPattern = iota
25

26
        // PatternMutualRelationship detects (a)-[:T]->(b)-[:T]->(a) cycles
27
        // Optimized via single-pass edge set intersection
28
        PatternMutualRelationship
29

30
        // PatternIncomingCountAgg detects MATCH (x)<-[:T]-(y) RETURN x, count(y)
31
        // Optimized via single-pass edge counting
32
        PatternIncomingCountAgg
33

34
        // PatternOutgoingCountAgg detects MATCH (x)-[:T]->(y) RETURN x, count(y)
35
        // Optimized via single-pass edge counting
36
        PatternOutgoingCountAgg
37

38
        // PatternEdgePropertyAgg detects avg/sum/count on edge properties
39
        // Optimized via single-pass accumulation
40
        PatternEdgePropertyAgg
41

42
        // PatternLargeResultSet detects queries returning many rows (LIMIT > 100)
43
        // Optimized via batch node lookups and pre-allocation
44
        PatternLargeResultSet
45
)
46

47
// String returns a human-readable pattern name
48
func (p QueryPattern) String() string {
1✔
49
        switch p {
1✔
50
        case PatternMutualRelationship:
1✔
51
                return "MutualRelationship"
1✔
52
        case PatternIncomingCountAgg:
1✔
53
                return "IncomingCountAgg"
1✔
54
        case PatternOutgoingCountAgg:
1✔
55
                return "OutgoingCountAgg"
1✔
56
        case PatternEdgePropertyAgg:
1✔
57
                return "EdgePropertyAgg"
1✔
58
        case PatternLargeResultSet:
1✔
59
                return "LargeResultSet"
1✔
60
        default:
1✔
61
                return "Generic"
1✔
62
        }
63
}
64

65
// PatternInfo contains details about a detected pattern
66
type PatternInfo struct {
67
        Pattern      QueryPattern
68
        RelType      string   // Relationship type for the pattern (e.g., "FOLLOWS")
69
        StartVar     string   // Start node variable (e.g., "a")
70
        EndVar       string   // End node variable (e.g., "b")
71
        RelVar       string   // Relationship variable (e.g., "r")
72
        AggFunctions []string // Aggregation functions used (e.g., ["count", "avg"])
73
        AggProperty  string   // Property being aggregated (e.g., "rating")
74
        Limit        int      // LIMIT value if present
75
        GroupByVars  []string // Variables in implicit GROUP BY
76
}
77

78
// DetectQueryPattern analyzes a Cypher query and returns pattern info
79
func DetectQueryPattern(ctx context.Context, query string) PatternInfo {
1✔
80
        info := PatternInfo{
1✔
81
                Pattern: PatternGeneric,
1✔
82
        }
1✔
83

1✔
84
        upperQuery := strings.ToUpper(query)
1✔
85

1✔
86
        // Don't optimize queries with WITH clause - they have complex aggregation
1✔
87
        // semantics that the optimized executors don't handle (aliases, collect, etc.)
1✔
88
        // Use word boundary check to avoid matching "STARTS WITH" or "ENDS WITH"
1✔
89
        if containsKeywordOutsideStrings(query, "WITH") {
2✔
90
                return info
1✔
91
        }
1✔
92

93
        // Extract LIMIT first (affects multiple patterns)
94
        if limit, ok := ExtractLimit(query); ok {
2✔
95
                info.Limit = limit
1✔
96
        }
1✔
97

98
        // Check for mutual relationship pattern: (a)-[:T]->(b)-[:T]->(a)
99
        if info.Pattern == PatternGeneric && detectMutualRelationship(ctx, query, &info) {
2✔
100
                return info
1✔
101
        }
1✔
102

103
        // Check for incoming count aggregation
104
        if info.Pattern == PatternGeneric && strings.Contains(upperQuery, "COUNT(") {
2✔
105
                // Guardrails: the (in|out) count optimizers only support a very narrow query shape.
1✔
106
                // Do NOT route queries that:
1✔
107
                //   - have other aggregations (AVG/SUM/MIN/MAX/COLLECT)
1✔
108
                //   - have multiple relationship segments (chained traversals)
1✔
109
                //
1✔
110
                // Those queries should use the generic executor (and any traversal fast paths),
1✔
111
                // otherwise we can mis-route multi-hop queries and/or return incorrect columns.
1✔
112
                if !strings.Contains(upperQuery, "SUM(") &&
1✔
113
                        !strings.Contains(upperQuery, "AVG(") &&
1✔
114
                        !strings.Contains(upperQuery, "MIN(") &&
1✔
115
                        !strings.Contains(upperQuery, "MAX(") &&
1✔
116
                        !strings.Contains(upperQuery, "COLLECT(") {
2✔
117
                        matchClause := extractMatchClause(query)
1✔
118
                        if countRelationshipPatterns(matchClause) == 1 {
2✔
119
                                if detectIncomingCountAgg(ctx, query, &info) {
2✔
120
                                        return info
1✔
121
                                }
1✔
122
                                if detectOutgoingCountAgg(ctx, query, &info) {
2✔
123
                                        return info
1✔
124
                                }
1✔
125
                        }
126
                }
127
        }
128

129
        // Check for edge property aggregation
130
        if info.Pattern == PatternGeneric && detectEdgePropertyAgg(query, &info) {
2✔
131
                return info
1✔
132
        }
1✔
133

134
        // Check for large result set (LIMIT > 100)
135
        if info.Limit > 100 && strings.Contains(upperQuery, "MATCH") {
2✔
136
                info.Pattern = PatternLargeResultSet
1✔
137
                return info
1✔
138
        }
1✔
139

140
        return info
1✔
141
}
142

143
func extractMatchClause(query string) string {
1✔
144
        matchIdx := findKeywordIndex(query, "MATCH")
1✔
145
        if matchIdx < 0 {
1✔
UNCOV
146
                return ""
×
UNCOV
147
        }
×
148
        end := len(query)
1✔
149
        for _, keyword := range []string{"WHERE", "RETURN", "WITH", "ORDER", "LIMIT", "SKIP"} {
2✔
150
                if idx := findKeywordIndex(query[matchIdx:], keyword); idx > 0 {
2✔
151
                        if matchIdx+idx < end {
2✔
152
                                end = matchIdx + idx
1✔
153
                        }
1✔
154
                }
155
        }
156
        return query[matchIdx:end]
1✔
157
}
158

159
func countRelationshipPatterns(s string) int {
1✔
160
        inQuote := false
1✔
161
        quoteChar := byte(0)
1✔
162
        count := 0
1✔
163

1✔
164
        for i := 0; i < len(s); i++ {
2✔
165
                c := s[i]
1✔
166

1✔
167
                if (c == '\'' || c == '"') && (i == 0 || s[i-1] != '\\') {
2✔
168
                        if !inQuote {
2✔
169
                                inQuote = true
1✔
170
                                quoteChar = c
1✔
171
                        } else if c == quoteChar {
3✔
172
                                inQuote = false
1✔
173
                        }
1✔
174
                }
175
                if inQuote {
2✔
176
                        continue
1✔
177
                }
178

179
                if c != '[' {
2✔
180
                        continue
1✔
181
                }
182

183
                // Relationship patterns are introduced by a preceding '-' (possibly with whitespace).
184
                j := i - 1
1✔
185
                for j >= 0 && isWhitespace(s[j]) {
1✔
UNCOV
186
                        j--
×
UNCOV
187
                }
×
188
                if j >= 0 && s[j] == '-' {
2✔
189
                        count++
1✔
190
                }
1✔
191
        }
192

193
        return count
1✔
194
}
195

196
// detectMutualRelationship checks for (a)-[:T]->(b)-[:T]->(a) pattern
197
func detectMutualRelationship(ctx context.Context, query string, info *PatternInfo) bool {
1✔
198
        matchClause := extractMatchClause(query)
1✔
199
        matchClause = strings.TrimSpace(matchClause)
1✔
200
        if matchClause == "" {
1✔
UNCOV
201
                return false
×
UNCOV
202
        }
×
203
        if strings.HasPrefix(strings.ToUpper(matchClause), "MATCH") {
2✔
204
                matchClause = strings.TrimSpace(matchClause[len("MATCH"):])
1✔
205
        }
1✔
206

207
        exec := &StorageExecutor{}
1✔
208
        match := exec.parseTraversalPattern(ctx, matchClause)
1✔
209
        if match == nil || !match.IsChained || len(match.Segments) != 2 {
2✔
210
                return false
1✔
211
        }
1✔
212
        first := match.Segments[0]
1✔
213
        second := match.Segments[1]
1✔
214
        if first.Relationship.Direction != "outgoing" || second.Relationship.Direction != "outgoing" {
2✔
215
                return false
1✔
216
        }
1✔
217
        if first.FromNode.variable == "" || first.ToNode.variable == "" || second.ToNode.variable == "" {
1✔
UNCOV
218
                return false
×
UNCOV
219
        }
×
220
        if first.FromNode.variable != second.ToNode.variable {
2✔
221
                return false
1✔
222
        }
1✔
223
        if first.ToNode.variable != second.FromNode.variable {
1✔
224
                return false
×
225
        }
×
226
        if len(first.Relationship.Types) == 0 || len(second.Relationship.Types) == 0 {
1✔
227
                return false
×
228
        }
×
229
        if !strings.EqualFold(first.Relationship.Types[0], second.Relationship.Types[0]) {
1✔
UNCOV
230
                return false
×
UNCOV
231
        }
×
232

233
        info.Pattern = PatternMutualRelationship
1✔
234
        info.StartVar = first.FromNode.variable
1✔
235
        info.EndVar = first.ToNode.variable
1✔
236
        info.RelType = first.Relationship.Types[0]
1✔
237
        return true
1✔
238
}
239

240
// detectIncomingCountAgg checks for (x)<-[:T]-(y) ... count(y) pattern
241
func detectIncomingCountAgg(ctx context.Context, query string, info *PatternInfo) bool {
1✔
242
        startVar, relVar, relType, endVar, ok := parseDirectionalCountPattern(ctx, extractMatchClause(query), true)
1✔
243
        if !ok {
2✔
244
                return false
1✔
245
        }
1✔
246

247
        // Check if count() is on the end variable (the one doing the incoming), and
248
        // only optimize the narrow "RETURN x.name, count(y)" shape that the optimized executor implements.
249
        upperQuery := strings.ToUpper(query)
1✔
250
        countPattern := "COUNT(" + strings.ToUpper(endVar)
1✔
251
        countStarPattern := "COUNT(*)"
1✔
252

1✔
253
        if (strings.Contains(upperQuery, countPattern) || strings.Contains(upperQuery, countStarPattern)) &&
1✔
254
                isReturnNameCountShape(query, startVar, endVar) {
2✔
255
                info.Pattern = PatternIncomingCountAgg
1✔
256
                info.StartVar = startVar
1✔
257
                info.EndVar = endVar
1✔
258
                info.RelVar = relVar
1✔
259
                info.RelType = relType
1✔
260
                info.AggFunctions = []string{"count"}
1✔
261
                return true
1✔
262
        }
1✔
263

264
        return false
1✔
265
}
266

267
// detectOutgoingCountAgg checks for (x)-[:T]->(y) ... count(y) pattern
268
func detectOutgoingCountAgg(ctx context.Context, query string, info *PatternInfo) bool {
1✔
269
        startVar, relVar, relType, endVar, ok := parseDirectionalCountPattern(ctx, extractMatchClause(query), false)
1✔
270
        if !ok {
2✔
271
                return false
1✔
272
        }
1✔
273

274
        // Check if count() is on the end variable, and only optimize the narrow
275
        // "RETURN x.name, count(y)" shape that the optimized executor implements.
276
        upperQuery := strings.ToUpper(query)
1✔
277
        countPattern := "COUNT(" + strings.ToUpper(endVar)
1✔
278

1✔
279
        if strings.Contains(upperQuery, countPattern) && isReturnNameCountShape(query, startVar, endVar) {
2✔
280
                info.Pattern = PatternOutgoingCountAgg
1✔
281
                info.StartVar = startVar
1✔
282
                info.EndVar = endVar
1✔
283
                info.RelVar = relVar
1✔
284
                info.RelType = relType
1✔
285
                info.AggFunctions = []string{"count"}
1✔
286
                return true
1✔
287
        }
1✔
288

289
        return false
1✔
290
}
291

292
func isReturnNameCountShape(query string, startVar string, endVar string) bool {
1✔
293
        returnIdx := findKeywordIndex(query, "RETURN")
1✔
294
        if returnIdx < 0 {
1✔
UNCOV
295
                return false
×
UNCOV
296
        }
×
297

298
        returnPart := strings.TrimSpace(query[returnIdx+6:])
1✔
299

1✔
300
        // Strip ORDER BY / SKIP / LIMIT.
1✔
301
        end := len(returnPart)
1✔
302
        for _, keyword := range []string{"ORDER BY", "SKIP", "LIMIT"} {
2✔
303
                if idx := findKeywordIndex(returnPart, keyword); idx >= 0 && idx < end {
2✔
304
                        end = idx
1✔
305
                }
1✔
306
        }
307
        returnPart = strings.TrimSpace(returnPart[:end])
1✔
308
        if returnPart == "" {
1✔
UNCOV
309
                return false
×
UNCOV
310
        }
×
311

312
        parts := splitOutsideParens(returnPart, ',')
1✔
313
        if len(parts) != 2 {
2✔
314
                return false
1✔
315
        }
1✔
316

317
        left := strings.TrimSpace(parts[0])
1✔
318
        right := strings.TrimSpace(parts[1])
1✔
319

1✔
320
        // Handle "AS" aliases.
1✔
321
        if asIdx := strings.Index(strings.ToUpper(left), " AS "); asIdx > 0 {
2✔
322
                left = strings.TrimSpace(left[:asIdx])
1✔
323
        }
1✔
324
        if asIdx := strings.Index(strings.ToUpper(right), " AS "); asIdx > 0 {
2✔
325
                right = strings.TrimSpace(right[:asIdx])
1✔
326
        }
1✔
327

328
        // Require "startVar.name" (the optimized executor currently uses the "name" property).
329
        if !strings.EqualFold(left, startVar+".name") {
2✔
330
                return false
1✔
331
        }
1✔
332

333
        // Require COUNT(endVar) or COUNT(*).
334
        rightUpper := strings.ToUpper(strings.ReplaceAll(right, " ", ""))
1✔
335
        wantCountVar := "COUNT(" + strings.ToUpper(endVar) + ")"
1✔
336
        return rightUpper == wantCountVar || rightUpper == "COUNT(*)"
1✔
337
}
338

339
func parseDirectionalCountPattern(ctx context.Context, matchClause string, incoming bool) (string, string, string, string, bool) {
1✔
340
        matchClause = strings.TrimSpace(matchClause)
1✔
341
        if matchClause == "" {
1✔
UNCOV
342
                return "", "", "", "", false
×
UNCOV
343
        }
×
344
        if strings.HasPrefix(strings.ToUpper(matchClause), "MATCH") {
2✔
345
                matchClause = strings.TrimSpace(matchClause[len("MATCH"):])
1✔
346
        }
1✔
347
        if matchClause == "" {
1✔
UNCOV
348
                return "", "", "", "", false
×
UNCOV
349
        }
×
350

351
        exec := &StorageExecutor{}
1✔
352
        match := exec.parseTraversalPattern(ctx, matchClause)
1✔
353
        if match == nil || match.IsChained {
1✔
UNCOV
354
                return "", "", "", "", false
×
UNCOV
355
        }
×
356
        if incoming {
2✔
357
                if match.Relationship.Direction != "incoming" {
2✔
358
                        return "", "", "", "", false
1✔
359
                }
1✔
360
        } else if match.Relationship.Direction != "outgoing" {
2✔
361
                return "", "", "", "", false
1✔
362
        }
1✔
363
        if match.StartNode.variable == "" || match.EndNode.variable == "" {
2✔
364
                return "", "", "", "", false
1✔
365
        }
1✔
366
        relType := ""
1✔
367
        if len(match.Relationship.Types) > 0 {
2✔
368
                relType = match.Relationship.Types[0]
1✔
369
        }
1✔
370
        return match.StartNode.variable, match.Relationship.Variable, relType, match.EndNode.variable, true
1✔
371
}
372

373
// detectEdgePropertyAgg checks for avg(r.prop), sum(r.prop) patterns
374
func detectEdgePropertyAgg(query string, info *PatternInfo) bool {
1✔
375
        // First, find relationship variable in MATCH
1✔
376
        matchClause := extractMatchClause(query)
1✔
377
        relVar := extractRelationshipVariable(matchClause)
1✔
378
        if relVar == "" {
2✔
379
                return false
1✔
380
        }
1✔
381

382
        exec := &StorageExecutor{}
1✔
383
        returnIdx := findKeywordIndex(query, "RETURN")
1✔
384
        if returnIdx < 0 {
1✔
UNCOV
385
                return false
×
UNCOV
386
        }
×
387
        returnItems := exec.parseReturnItems(strings.TrimSpace(query[returnIdx+len("RETURN"):]))
1✔
388
        if len(returnItems) < 2 {
2✔
389
                return false
1✔
390
        }
1✔
391

392
        for _, item := range returnItems[1:] {
2✔
393
                expr, _ := parseProjectionExprAlias(item.expr)
1✔
394
                u := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(expr), " ", ""))
1✔
395
                open := strings.IndexByte(u, '(')
1✔
396
                close := strings.LastIndexByte(u, ')')
1✔
397
                if open <= 0 || close <= open {
2✔
398
                        continue
1✔
399
                }
400
                aggFunc := strings.ToLower(u[:open])
1✔
401
                inner := u[open+1 : close]
1✔
402

1✔
403
                if aggFunc == "count" {
2✔
404
                        if inner == "*" || strings.EqualFold(inner, strings.ToUpper(relVar)) {
2✔
405
                                continue
1✔
406
                        }
407
                        return false
1✔
408
                }
409
                if aggFunc != "sum" && aggFunc != "avg" && aggFunc != "min" && aggFunc != "max" {
2✔
410
                        return false
1✔
411
                }
1✔
412
                wantPrefix := strings.ToUpper(relVar) + "."
1✔
413
                if !strings.HasPrefix(inner, wantPrefix) {
1✔
UNCOV
414
                        return false
×
415
                }
×
416
                propName := inner[len(wantPrefix):]
1✔
417
                if propName == "" {
1✔
UNCOV
418
                        return false
×
UNCOV
419
                }
×
420
                if info.AggProperty == "" {
2✔
421
                        info.AggProperty = strings.ToLower(propName)
1✔
422
                } else if !strings.EqualFold(info.AggProperty, propName) {
1✔
UNCOV
423
                        return false
×
UNCOV
424
                }
×
425
                info.AggFunctions = append(info.AggFunctions, aggFunc)
1✔
426
        }
427

428
        if info.AggProperty == "" {
2✔
429
                return false
1✔
430
        }
1✔
431
        if !isReturnEdgePropertyAggNameShape(query, relVar, info.AggProperty) {
2✔
432
                return false
1✔
433
        }
1✔
434
        info.Pattern = PatternEdgePropertyAgg
1✔
435
        info.RelVar = relVar
1✔
436

1✔
437
        return true
1✔
438
}
439

440
func isReturnEdgePropertyAggNameShape(query string, relVar string, propName string) bool {
1✔
441
        returnIdx := findKeywordIndex(query, "RETURN")
1✔
442
        if returnIdx < 0 {
1✔
UNCOV
443
                return false
×
UNCOV
444
        }
×
445
        returnPart := strings.TrimSpace(query[returnIdx+6:])
1✔
446

1✔
447
        // Strip ORDER BY / SKIP / LIMIT.
1✔
448
        end := len(returnPart)
1✔
449
        for _, keyword := range []string{"ORDER BY", "SKIP", "LIMIT"} {
2✔
450
                if idx := findKeywordIndex(returnPart, keyword); idx >= 0 && idx < end {
2✔
451
                        end = idx
1✔
452
                }
1✔
453
        }
454
        returnPart = strings.TrimSpace(returnPart[:end])
1✔
455
        if returnPart == "" {
1✔
UNCOV
456
                return false
×
UNCOV
457
        }
×
458

459
        items := splitOutsideParens(returnPart, ',')
1✔
460
        if len(items) < 2 {
2✔
461
                return false
1✔
462
        }
1✔
463

464
        first := strings.TrimSpace(items[0])
1✔
465
        if asIdx := strings.Index(strings.ToUpper(first), " AS "); asIdx > 0 {
2✔
466
                first = strings.TrimSpace(first[:asIdx])
1✔
467
        }
1✔
468
        // Require "<var>.name" in first return position.
469
        dot := strings.LastIndex(first, ".")
1✔
470
        if dot < 0 || !strings.EqualFold(first[dot+1:], "name") {
2✔
471
                return false
1✔
472
        }
1✔
473

474
        wantAggPrefix := strings.ToUpper(relVar) + "."
1✔
475
        wantAggProp := strings.ToUpper(propName)
1✔
476

1✔
477
        // Remaining items must be aggregations over relVar.prop (optionally plus count(r)).
1✔
478
        for i := 1; i < len(items); i++ {
2✔
479
                item := strings.TrimSpace(items[i])
1✔
480
                if asIdx := strings.Index(strings.ToUpper(item), " AS "); asIdx > 0 {
2✔
481
                        item = strings.TrimSpace(item[:asIdx])
1✔
482
                }
1✔
483

484
                u := strings.ToUpper(strings.ReplaceAll(item, " ", ""))
1✔
485
                switch {
1✔
486
                case strings.HasPrefix(u, "COUNT(") && strings.HasSuffix(u, ")"):
1✔
487
                        // Allow COUNT(r) and COUNT(*).
1✔
488
                        inner := strings.TrimSuffix(strings.TrimPrefix(u, "COUNT("), ")")
1✔
489
                        if inner == "*" || strings.EqualFold(inner, relVar) {
2✔
490
                                continue
1✔
491
                        }
UNCOV
492
                        return false
×
493

494
                case strings.HasPrefix(u, "SUM(") || strings.HasPrefix(u, "AVG(") || strings.HasPrefix(u, "MIN(") || strings.HasPrefix(u, "MAX("):
1✔
495
                        open := strings.IndexByte(u, '(')
1✔
496
                        close := strings.LastIndexByte(u, ')')
1✔
497
                        if open < 0 || close < 0 || close <= open+1 {
1✔
UNCOV
498
                                return false
×
499
                        }
×
500
                        inner := u[open+1 : close]
1✔
501
                        if !strings.HasPrefix(inner, wantAggPrefix) {
1✔
UNCOV
502
                                return false
×
UNCOV
503
                        }
×
504
                        if !strings.EqualFold(inner[len(wantAggPrefix):], wantAggProp) {
2✔
505
                                return false
1✔
506
                        }
1✔
507
                        continue
1✔
UNCOV
508
                default:
×
UNCOV
509
                        return false
×
510
                }
511
        }
512

513
        return true
1✔
514
}
515

516
// extractNodeVariables extracts node variable names from a MATCH pattern
517
func extractNodeVariables(matchClause string) []string {
×
518
        var vars []string
×
UNCOV
519
        for i := 0; i < len(matchClause); i++ {
×
520
                if matchClause[i] != '(' {
×
521
                        continue
×
522
                }
523
                j := i + 1
×
524
                for j < len(matchClause) && isWhitespace(matchClause[j]) {
×
525
                        j++
×
526
                }
×
UNCOV
527
                name, next, ok := scanIdentifierToken(matchClause, j)
×
528
                if !ok {
×
529
                        continue
×
530
                }
531
                if next < len(matchClause) {
×
532
                        for next < len(matchClause) && isWhitespace(matchClause[next]) {
×
533
                                next++
×
UNCOV
534
                        }
×
UNCOV
535
                        if next < len(matchClause) && matchClause[next] != ':' && matchClause[next] != ')' && matchClause[next] != '{' {
×
536
                                continue
×
537
                        }
538
                }
UNCOV
539
                vars = append(vars, name)
×
540
        }
UNCOV
541
        return vars
×
542
}
543

544
// extractRelationshipType extracts the relationship type from a pattern
545
func extractRelationshipType(pattern string) string {
1✔
546
        for i := 0; i < len(pattern); i++ {
2✔
547
                if pattern[i] != '[' {
2✔
548
                        continue
1✔
549
                }
550
                inner, _, ok := extractBracketSectionQueryPattern(pattern[i:])
1✔
551
                if !ok {
1✔
UNCOV
552
                        continue
×
553
                }
554
                if colonIdx := strings.Index(inner, ":"); colonIdx >= 0 {
2✔
555
                        typePart := strings.TrimSpace(inner[colonIdx+1:])
1✔
556
                        if propIdx := strings.Index(typePart, "{"); propIdx >= 0 {
1✔
557
                                typePart = strings.TrimSpace(typePart[:propIdx])
×
558
                        }
×
559
                        if pipeIdx := strings.Index(typePart, "|"); pipeIdx >= 0 {
1✔
UNCOV
560
                                typePart = strings.TrimSpace(typePart[:pipeIdx])
×
UNCOV
561
                        }
×
562
                        if typePart != "" {
2✔
563
                                return typePart
1✔
564
                        }
1✔
565
                }
566
        }
567
        return ""
1✔
568
}
569

570
func extractRelationshipVariable(pattern string) string {
1✔
571
        for i := 0; i < len(pattern); i++ {
2✔
572
                if pattern[i] != '[' {
2✔
573
                        continue
1✔
574
                }
575
                inner, _, ok := extractBracketSectionQueryPattern(pattern[i:])
1✔
576
                if !ok {
1✔
UNCOV
577
                        continue
×
578
                }
579
                inner = strings.TrimSpace(inner)
1✔
580
                if inner == "" {
1✔
UNCOV
581
                        return ""
×
UNCOV
582
                }
×
583
                if colonIdx := strings.Index(inner, ":"); colonIdx >= 0 {
2✔
584
                        name := strings.TrimSpace(inner[:colonIdx])
1✔
585
                        if name != "" {
2✔
586
                                return name
1✔
587
                        }
1✔
588
                        return ""
1✔
589
                }
590
                if inner[0] == '*' {
2✔
591
                        return ""
1✔
592
                }
1✔
593
                name, _, ok := parseIdentifierTokenQueryPattern(inner)
1✔
594
                if ok {
2✔
595
                        return name
1✔
596
                }
1✔
597
                return ""
1✔
598
        }
599
        return ""
1✔
600
}
601

602
func scanIdentifierToken(s string, start int) (string, int, bool) {
×
603
        if start < 0 || start >= len(s) {
×
604
                return "", start, false
×
605
        }
×
606
        if !isIdentifierStart(s[start]) {
×
607
                return "", start, false
×
608
        }
×
609
        i := start + 1
×
610
        for i < len(s) && isIdentifierPart(s[i]) {
×
UNCOV
611
                i++
×
UNCOV
612
        }
×
UNCOV
613
        return s[start:i], i, true
×
614
}
615

616
func isIdentifierStart(c byte) bool {
1✔
617
        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
1✔
618
}
1✔
619

620
func isIdentifierPart(c byte) bool {
1✔
621
        return isIdentifierStart(c) || (c >= '0' && c <= '9')
1✔
622
}
1✔
623

624
func parseIdentifierTokenQueryPattern(s string) (string, string, bool) {
1✔
625
        s = strings.TrimSpace(s)
1✔
626
        if s == "" || !isIdentifierStart(s[0]) {
2✔
627
                return "", "", false
1✔
628
        }
1✔
629
        i := 1
1✔
630
        for i < len(s) && isIdentifierPart(s[i]) {
2✔
631
                i++
1✔
632
        }
1✔
633
        return s[:i], s[i:], true
1✔
634
}
635

636
func extractBracketSectionQueryPattern(s string) (inside string, rest string, ok bool) {
1✔
637
        if !strings.HasPrefix(s, "[") {
1✔
UNCOV
638
                return "", "", false
×
UNCOV
639
        }
×
640
        depth := 0
1✔
641
        for i := 0; i < len(s); i++ {
2✔
642
                switch s[i] {
1✔
643
                case '[':
1✔
644
                        depth++
1✔
645
                case ']':
1✔
646
                        depth--
1✔
647
                        if depth == 0 {
2✔
648
                                return s[1:i], s[i+1:], true
1✔
649
                        }
1✔
650
                }
651
        }
UNCOV
652
        return "", "", false
×
653
}
654

655
// IsOptimizable returns true if the pattern can be optimized
656
func (p PatternInfo) IsOptimizable() bool {
1✔
657
        return p.Pattern != PatternGeneric
1✔
658
}
1✔
659

660
// NeedsRelationshipTypeScan returns true if the optimization needs all edges of a type
661
func (p PatternInfo) NeedsRelationshipTypeScan() bool {
1✔
662
        switch p.Pattern {
1✔
663
        case PatternMutualRelationship, PatternIncomingCountAgg,
664
                PatternOutgoingCountAgg, PatternEdgePropertyAgg:
1✔
665
                return true
1✔
666
        default:
1✔
667
                return false
1✔
668
        }
669
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc