Skip to content

Commit 2f30bfd

Browse files
committed
making sure counts clear cache properly
1 parent db5791f commit 2f30bfd

File tree

5 files changed

+690
-0
lines changed

5 files changed

+690
-0
lines changed

pkg/cypher/cache.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,8 @@ func (sc *SmartQueryCache) Put(cypher string, params map[string]interface{}, res
587587

588588
// InvalidateLabels removes only cache entries that depend on the given labels.
589589
// This is much more efficient than full invalidation for multi-label workloads.
590+
// Also invalidates queries with NO labels (like "MATCH (n) RETURN count(n)")
591+
// since they match all nodes and are affected by any label change.
590592
func (sc *SmartQueryCache) InvalidateLabels(labels []string) {
591593
sc.mu.Lock()
592594
defer sc.mu.Unlock()
@@ -602,6 +604,14 @@ func (sc *SmartQueryCache) InvalidateLabels(labels []string) {
602604
}
603605
}
604606

607+
// Also invalidate queries with NO labels (they match all nodes)
608+
// These queries are affected by ANY node creation/deletion
609+
for key, entry := range sc.cache {
610+
if len(entry.labels) == 0 {
611+
keysToRemove[key] = struct{}{}
612+
}
613+
}
614+
605615
// Remove collected entries
606616
for key := range keysToRemove {
607617
sc.removeEntry(key)

pkg/cypher/count_bug_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package cypher
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/orneryd/nornicdb/pkg/storage"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestBug_CountReturnsZeroWhenNodesExist reproduces the bug where:
13+
// - MATCH (n) RETURN n returns actual nodes
14+
// - MATCH (n) RETURN count(n) returns 0
15+
// - Storage NodeCount() returns 0
16+
// But nodes clearly exist in the database.
17+
func TestBug_CountReturnsZeroWhenNodesExist(t *testing.T) {
18+
store := storage.NewMemoryEngine()
19+
defer store.Close()
20+
exec := NewStorageExecutor(store)
21+
ctx := context.Background()
22+
23+
// Create 5 nodes
24+
for i := 0; i < 5; i++ {
25+
_, err := exec.Execute(ctx, "CREATE (n:TestNode {idx: $idx})", map[string]any{"idx": i})
26+
require.NoError(t, err)
27+
}
28+
29+
// Verify nodes exist by returning them
30+
result, err := exec.Execute(ctx, "MATCH (n:TestNode) RETURN n", nil)
31+
require.NoError(t, err)
32+
actualNodeCount := len(result.Rows)
33+
t.Logf("MATCH (n) RETURN n returned %d rows", actualNodeCount)
34+
assert.Equal(t, 5, actualNodeCount, "Should return 5 nodes")
35+
36+
// Now test count(n)
37+
result, err = exec.Execute(ctx, "MATCH (n:TestNode) RETURN count(n) as cnt", nil)
38+
require.NoError(t, err)
39+
require.Len(t, result.Rows, 1)
40+
countN := result.Rows[0][0]
41+
t.Logf("MATCH (n) RETURN count(n) returned: %v (type: %T)", countN, countN)
42+
43+
// Convert to int64 for comparison
44+
var countNInt int64
45+
switch v := countN.(type) {
46+
case int64:
47+
countNInt = v
48+
case int:
49+
countNInt = int64(v)
50+
case float64:
51+
countNInt = int64(v)
52+
}
53+
assert.Equal(t, int64(5), countNInt, "count(n) should return 5")
54+
55+
// Test count(*)
56+
result, err = exec.Execute(ctx, "MATCH (n:TestNode) RETURN count(*) as cnt", nil)
57+
require.NoError(t, err)
58+
require.Len(t, result.Rows, 1)
59+
countStar := result.Rows[0][0]
60+
t.Logf("MATCH (n) RETURN count(*) returned: %v (type: %T)", countStar, countStar)
61+
62+
var countStarInt int64
63+
switch v := countStar.(type) {
64+
case int64:
65+
countStarInt = v
66+
case int:
67+
countStarInt = int64(v)
68+
case float64:
69+
countStarInt = int64(v)
70+
}
71+
assert.Equal(t, int64(5), countStarInt, "count(*) should return 5")
72+
73+
// Test storage layer NodeCount
74+
nodeCount, err := store.NodeCount()
75+
require.NoError(t, err)
76+
t.Logf("Storage NodeCount() returned: %d", nodeCount)
77+
assert.Equal(t, int64(5), nodeCount, "Storage NodeCount should return 5")
78+
}
79+
80+
// TestBug_CountAfterDeleteAndRecreate tests count behavior after delete/recreate cycle
81+
func TestBug_CountAfterDeleteAndRecreate(t *testing.T) {
82+
store := storage.NewMemoryEngine()
83+
defer store.Close()
84+
exec := NewStorageExecutor(store)
85+
ctx := context.Background()
86+
87+
// Create initial nodes
88+
for i := 0; i < 3; i++ {
89+
_, err := exec.Execute(ctx, "CREATE (n:CycleTest {idx: $idx})", map[string]any{"idx": i})
90+
require.NoError(t, err)
91+
}
92+
93+
// Verify count
94+
nodeCount, _ := store.NodeCount()
95+
t.Logf("After initial create: NodeCount = %d", nodeCount)
96+
assert.Equal(t, int64(3), nodeCount)
97+
98+
// Delete all nodes
99+
_, err := exec.Execute(ctx, "MATCH (n:CycleTest) DELETE n", nil)
100+
require.NoError(t, err)
101+
102+
nodeCount, _ = store.NodeCount()
103+
t.Logf("After delete: NodeCount = %d", nodeCount)
104+
assert.Equal(t, int64(0), nodeCount)
105+
106+
// Recreate nodes
107+
for i := 0; i < 5; i++ {
108+
_, err := exec.Execute(ctx, "CREATE (n:CycleTest {idx: $idx})", map[string]any{"idx": i})
109+
require.NoError(t, err)
110+
}
111+
112+
nodeCount, _ = store.NodeCount()
113+
t.Logf("After recreate: NodeCount = %d", nodeCount)
114+
assert.Equal(t, int64(5), nodeCount, "NodeCount should be 5 after recreating nodes")
115+
116+
// Verify with Cypher count
117+
result, err := exec.Execute(ctx, "MATCH (n:CycleTest) RETURN count(n) as cnt", nil)
118+
require.NoError(t, err)
119+
require.Len(t, result.Rows, 1)
120+
cypherCount := result.Rows[0][0]
121+
t.Logf("Cypher count(n) after recreate: %v", cypherCount)
122+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package cypher
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/orneryd/nornicdb/pkg/storage"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestBug_CountDirectReturnVsWithReturn reproduces the bug where:
13+
// - MATCH (n) RETURN count(n) returns 0 (WRONG)
14+
// - MATCH (n) WITH n RETURN count(n) returns correct count (CORRECT)
15+
func TestBug_CountDirectReturnVsWithReturn(t *testing.T) {
16+
store := storage.NewMemoryEngine()
17+
defer store.Close()
18+
exec := NewStorageExecutor(store)
19+
ctx := context.Background()
20+
21+
// Create 10 nodes
22+
for i := 0; i < 10; i++ {
23+
_, err := exec.Execute(ctx, "CREATE (n:CountTest {idx: $idx})", map[string]any{"idx": i})
24+
require.NoError(t, err)
25+
}
26+
27+
// Verify nodes exist
28+
result, err := exec.Execute(ctx, "MATCH (n:CountTest) RETURN n", nil)
29+
require.NoError(t, err)
30+
t.Logf("MATCH (n) RETURN n: %d rows", len(result.Rows))
31+
require.Len(t, result.Rows, 10, "Should have 10 nodes")
32+
33+
// Test 1: Direct count in RETURN (this is the broken query)
34+
t.Run("direct_count_in_return", func(t *testing.T) {
35+
result, err := exec.Execute(ctx, "MATCH (n:CountTest) RETURN count(n) as cnt", nil)
36+
require.NoError(t, err)
37+
require.Len(t, result.Rows, 1)
38+
39+
cnt := result.Rows[0][0]
40+
t.Logf("MATCH (n) RETURN count(n): %v (type: %T)", cnt, cnt)
41+
42+
var countVal int64
43+
switch v := cnt.(type) {
44+
case int64:
45+
countVal = v
46+
case int:
47+
countVal = int64(v)
48+
case float64:
49+
countVal = int64(v)
50+
}
51+
assert.Equal(t, int64(10), countVal, "MATCH (n) RETURN count(n) should return 10")
52+
})
53+
54+
// Test 2: Count with WITH clause (this works correctly)
55+
t.Run("count_with_with_clause", func(t *testing.T) {
56+
result, err := exec.Execute(ctx, "MATCH (n:CountTest) WITH n RETURN count(n) as cnt", nil)
57+
require.NoError(t, err)
58+
require.Len(t, result.Rows, 1)
59+
60+
cnt := result.Rows[0][0]
61+
t.Logf("MATCH (n) WITH n RETURN count(n): %v (type: %T)", cnt, cnt)
62+
63+
var countVal int64
64+
switch v := cnt.(type) {
65+
case int64:
66+
countVal = v
67+
case int:
68+
countVal = int64(v)
69+
case float64:
70+
countVal = int64(v)
71+
}
72+
assert.Equal(t, int64(10), countVal, "MATCH (n) WITH n RETURN count(n) should return 10")
73+
})
74+
75+
// Test 3: count(*) direct
76+
t.Run("count_star_direct", func(t *testing.T) {
77+
result, err := exec.Execute(ctx, "MATCH (n:CountTest) RETURN count(*) as cnt", nil)
78+
require.NoError(t, err)
79+
require.Len(t, result.Rows, 1)
80+
81+
cnt := result.Rows[0][0]
82+
t.Logf("MATCH (n) RETURN count(*): %v (type: %T)", cnt, cnt)
83+
84+
var countVal int64
85+
switch v := cnt.(type) {
86+
case int64:
87+
countVal = v
88+
case int:
89+
countVal = int64(v)
90+
case float64:
91+
countVal = int64(v)
92+
}
93+
assert.Equal(t, int64(10), countVal, "MATCH (n) RETURN count(*) should return 10")
94+
})
95+
96+
// Test 4: count without label filter
97+
t.Run("count_all_nodes_direct", func(t *testing.T) {
98+
result, err := exec.Execute(ctx, "MATCH (n) RETURN count(n) as cnt", nil)
99+
require.NoError(t, err)
100+
require.Len(t, result.Rows, 1)
101+
102+
cnt := result.Rows[0][0]
103+
t.Logf("MATCH (n) RETURN count(n) [all nodes]: %v (type: %T)", cnt, cnt)
104+
105+
var countVal int64
106+
switch v := cnt.(type) {
107+
case int64:
108+
countVal = v
109+
case int:
110+
countVal = int64(v)
111+
case float64:
112+
countVal = int64(v)
113+
}
114+
assert.Equal(t, int64(10), countVal, "MATCH (n) RETURN count(n) [all nodes] should return 10")
115+
})
116+
}
117+
118+
// TestBug_CountAfterDeleteRecreate tests count behavior after delete/recreate
119+
func TestBug_CountAfterDeleteRecreate(t *testing.T) {
120+
store := storage.NewMemoryEngine()
121+
defer store.Close()
122+
exec := NewStorageExecutor(store)
123+
ctx := context.Background()
124+
125+
// Create initial nodes
126+
for i := 0; i < 5; i++ {
127+
_, err := exec.Execute(ctx, "CREATE (n:Cycle {idx: $idx})", map[string]any{"idx": i})
128+
require.NoError(t, err)
129+
}
130+
131+
// Verify initial count
132+
result, err := exec.Execute(ctx, "MATCH (n:Cycle) RETURN count(n) as cnt", nil)
133+
require.NoError(t, err)
134+
t.Logf("Initial count: %v", result.Rows[0][0])
135+
136+
// Delete all
137+
_, err = exec.Execute(ctx, "MATCH (n:Cycle) DELETE n", nil)
138+
require.NoError(t, err)
139+
140+
// Verify delete count
141+
result, err = exec.Execute(ctx, "MATCH (n:Cycle) RETURN count(n) as cnt", nil)
142+
require.NoError(t, err)
143+
t.Logf("After delete count: %v", result.Rows[0][0])
144+
145+
// Recreate
146+
for i := 0; i < 3; i++ {
147+
_, err := exec.Execute(ctx, "CREATE (n:Cycle {idx: $idx, recreated: true})", map[string]any{"idx": i + 100})
148+
require.NoError(t, err)
149+
}
150+
151+
// Verify recreate count
152+
result, err = exec.Execute(ctx, "MATCH (n:Cycle) RETURN count(n) as cnt", nil)
153+
require.NoError(t, err)
154+
cnt := result.Rows[0][0]
155+
t.Logf("After recreate count: %v", cnt)
156+
157+
var countVal int64
158+
switch v := cnt.(type) {
159+
case int64:
160+
countVal = v
161+
case int:
162+
countVal = int64(v)
163+
case float64:
164+
countVal = int64(v)
165+
}
166+
assert.Equal(t, int64(3), countVal, "Count after recreate should be 3")
167+
}

0 commit comments

Comments
 (0)