summaryrefslogtreecommitdiff
path: root/internal/mapr/groupset_ordering_test.go
blob: 18b845f9568ab0dedef3f6c52f5c35b09c78525b (plain)
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
package mapr

import (
	"reflect"
	"testing"
)

// TestGroupSetResultOrderIsDeterministicWithoutOrderBy is a negative test that
// reproduces the non-determinism bug in result(): when OrderBy is unset, the
// output row order depended on Go's map iteration order, which is intentionally
// randomised per runtime invocation. Two consecutive calls to result() on the
// same GroupSet could return rows in different orders.
//
// The fix collects group keys, sorts them lexicographically before building
// rows, and only then applies SortStable for the OrderBy pass. Ties on OrderBy
// (or no OrderBy) therefore resolve to lexicographic groupKey order rather than
// to random map iteration order.
func TestGroupSetResultOrderIsDeterministicWithoutOrderBy(t *testing.T) {
	t.Parallel()

	// Query with no ORDER BY clause — the bug case where map iteration order
	// was the sole determinant of row order.
	query, err := NewQuery("select count(line) from logs group by host")
	if err != nil {
		t.Fatalf("Unable to parse query: %v", err)
	}

	groupSet := NewGroupSet()

	// Insert keys in reverse lexicographic order to ensure the expected sorted
	// order cannot coincide with insertion order.
	for _, host := range []string{"host-z", "host-m", "host-a", "host-b"} {
		set := groupSet.GetSet(host)
		if err := set.Aggregate("count(line)", Count, "1", false); err != nil {
			t.Fatalf("Aggregate failed for %s: %v", host, err)
		}
	}

	// Run result() many times. Before the fix a handful of iterations was
	// enough to observe a different ordering; with the fix every call must
	// return exactly the same lexicographically sorted sequence of groupKeys.
	var firstKeys []string
	const iterations = 50
	for i := range iterations {
		rows, _, err := groupSet.result(query, false)
		if err != nil {
			t.Fatalf("result() iteration %d returned error: %v", i, err)
		}
		if len(rows) != 4 {
			t.Fatalf("Expected 4 rows, got %d on iteration %d", len(rows), i)
		}

		keys := make([]string, len(rows))
		for j, r := range rows {
			keys[j] = r.groupKey
		}

		if i == 0 {
			firstKeys = keys
			// Verify the order is lexicographic (the contract of the fix).
			expected := []string{"host-a", "host-b", "host-m", "host-z"}
			if !reflect.DeepEqual(keys, expected) {
				t.Fatalf("First result not in lexicographic order: got %v, want %v", keys, expected)
			}
			continue
		}

		// Every subsequent call must return the identical key sequence.
		if !reflect.DeepEqual(keys, firstKeys) {
			t.Fatalf("Non-deterministic ordering detected on iteration %d: got %v, want %v", i, keys, firstKeys)
		}
	}
}

// TestGroupSetResultOrderIsDeterministicWithOrderByTies verifies that when
// multiple rows share the same OrderBy value (a tie), the tie-break falls back
// to lexicographic groupKey order rather than to random map iteration order.
// SortStable preserves the relative order of equal elements, so the pre-sort of
// keys guarantees a deterministic tie-break.
func TestGroupSetResultOrderIsDeterministicWithOrderByTies(t *testing.T) {
	t.Parallel()

	// ORDER BY count(line) — all rows will have the same count (1), creating a
	// full tie that must resolve to lexicographic groupKey order.
	query, err := NewQuery("select count(line) from logs group by host order by count(line)")
	if err != nil {
		t.Fatalf("Unable to parse query: %v", err)
	}

	groupSet := NewGroupSet()

	// All hosts receive the same count value to force a tie.
	for _, host := range []string{"host-z", "host-m", "host-a", "host-b"} {
		set := groupSet.GetSet(host)
		if err := set.Aggregate("count(line)", Count, "1", false); err != nil {
			t.Fatalf("Aggregate failed for %s: %v", host, err)
		}
	}

	var firstKeys []string
	const iterations = 50
	for i := range iterations {
		rows, _, err := groupSet.result(query, false)
		if err != nil {
			t.Fatalf("result() iteration %d returned error: %v", i, err)
		}
		if len(rows) != 4 {
			t.Fatalf("Expected 4 rows, got %d on iteration %d", len(rows), i)
		}

		keys := make([]string, len(rows))
		for j, r := range rows {
			keys[j] = r.groupKey
		}

		if i == 0 {
			firstKeys = keys
			continue
		}

		if !reflect.DeepEqual(keys, firstKeys) {
			t.Fatalf("Non-deterministic tie-break detected on iteration %d: got %v, want %v", i, keys, firstKeys)
		}
	}
}