From b357fe76e05992f70796312a844bd539d3ad7f1e Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Fri, 14 Aug 2026 14:53:15 -0700 Subject: [PATCH 1/3] automatically translate hard-coded oids in the regression tests to what doltgres records --- testing/go/regression/tool/compare.go | 130 ++++++++++++++++- .../go/regression/tool/dump_trackers_test.go | 88 +++++++++++ testing/go/regression/tool/oid_map.go | 137 ++++++++++++++++++ testing/go/regression/tool/oid_map_test.go | 106 ++++++++++++++ testing/go/regression/tool/replay.go | 35 ++++- 5 files changed, 484 insertions(+), 12 deletions(-) create mode 100644 testing/go/regression/tool/dump_trackers_test.go create mode 100644 testing/go/regression/tool/oid_map.go create mode 100644 testing/go/regression/tool/oid_map_test.go diff --git a/testing/go/regression/tool/compare.go b/testing/go/regression/tool/compare.go index 04204dd71c..84c94896a8 100644 --- a/testing/go/regression/tool/compare.go +++ b/testing/go/regression/tool/compare.go @@ -31,20 +31,25 @@ import ( "github.com/dolthub/doltgresql/utils" ) -// CompareRowsOrdered compares the two rows, enforcing that the order matches between the two rows. -func CompareRowsOrdered(aRowDesc, bRowDesc *pgproto3.RowDescription, aRows, bRows []*pgproto3.DataRow) error { +// CompareRowsOrdered compares the two rows, enforcing that the order matches between the two rows. The aRowDesc and +// aRows are always the recorded Postgres responses, while bRowDesc and bRows are the Doltgres responses. User-object +// OIDs in `oid`-typed columns are compared through the given OIDMap (see OIDMap for details), and any new mappings +// are learned when the comparison succeeds. +func CompareRowsOrdered(oidMap *OIDMap, aRowDesc, bRowDesc *pgproto3.RowDescription, aRows, bRows []*pgproto3.DataRow) error { if len(aRows) != len(bRows) { return errors.Errorf("expected a row count of %d but received %d", len(aRows), len(bRows)) } aReadRows := ReadRows(aRowDesc, aRows) bReadRows := ReadRows(bRowDesc, bRows) + oidCols := oidColumns(aRowDesc) + candidates := make(map[uint32]uint32) for rowIdx := range aReadRows { if len(aReadRows[rowIdx]) != len(bReadRows[rowIdx]) { return errors.Errorf("expected a row column count of %d but received %d", len(aReadRows[rowIdx]), len(bReadRows[rowIdx])) } for colIdx := range aReadRows[rowIdx] { - if aReadRows[rowIdx][colIdx] != bReadRows[rowIdx][colIdx] { + if !cellsMatch(aReadRows[rowIdx][colIdx], bReadRows[rowIdx][colIdx], oidCols[colIdx], oidMap, candidates) { if len(aReadRows)+len(bReadRows) < 8 { return errors.Errorf("row sets differ:\n%s", rowsToErrorString(aReadRows, bReadRows)) } else { @@ -54,21 +59,132 @@ func CompareRowsOrdered(aRowDesc, bRowDesc *pgproto3.RowDescription, aRows, bRow } } } + if oidMap != nil { + oidMap.LearnAll(candidates) + } return nil } +// oidColumns returns, for each column in the recorded row description, whether the column is of the `oid` type. +func oidColumns(rowDesc *pgproto3.RowDescription) []bool { + if rowDesc == nil { + return nil + } + cols := make([]bool, len(rowDesc.Fields)) + for i, field := range rowDesc.Fields { + cols[i] = field.DataTypeOID == pgtype.OIDOID + } + return cols +} + +// cellsMatch returns whether the recorded cell matches the replayed cell. Cells in `oid`-typed columns that hold +// user-object OIDs are matched through the OIDMap: a previously learned mapping must agree, while a brand new pair is +// tentatively accepted and added to candidates (the caller commits candidates to the map only if the entire +// comparison succeeds, which keeps OID relationships consistent within a result set). +func cellsMatch(aVal, bVal interface{}, isOIDCol bool, oidMap *OIDMap, candidates map[uint32]uint32) bool { + if aVal == bVal { + return true + } + if !isOIDCol || oidMap == nil { + return false + } + aOID, aOK := cellToOID(aVal) + bOID, bOK := cellToOID(bVal) + if !aOK || !bOK || aOID < minUserOID || bOID == 0 { + return false + } + if mapped, ok := candidates[aOID]; ok { + return mapped == bOID + } + // Unknown pairing (or the object was dropped and recreated on one side): tentatively accept it. OID values are + // implementation-specific, so requiring consistency within the result set is the strongest meaningful check. + candidates[aOID] = bOID + return true +} + // CompareRowsUnordered compares the two rows. Order is not enforced, however if there are any duplicate rows, then it -// is expected that the duplicate counts match. -func CompareRowsUnordered(aRowDesc, bRowDesc *pgproto3.RowDescription, aRows, bRows []*pgproto3.DataRow) error { +// is expected that the duplicate counts match. As with CompareRowsOrdered, the a-side is the recorded Postgres +// response and the b-side is the Doltgres response, with user-object OIDs matched through the given OIDMap. +func CompareRowsUnordered(oidMap *OIDMap, aRowDesc, bRowDesc *pgproto3.RowDescription, aRows, bRows []*pgproto3.DataRow) error { if len(aRows) != len(bRows) { return errors.Errorf("expected a row count of %d but received %d", len(aRows), len(bRows)) } + aReadRows := ReadRows(aRowDesc, aRows) + bReadRows := ReadRows(bRowDesc, bRows) + oidCols := oidColumns(aRowDesc) + hasOIDCols := false + for _, isOID := range oidCols { + hasOIDCols = hasOIDCols || isOID + } + // Translate already-learned OID mappings in the recorded rows so that the multiset comparison sees replay OIDs. + if oidMap != nil && hasOIDCols { + for _, row := range aReadRows { + for colIdx := range row { + if !oidCols[colIdx] { + continue + } + if oid, ok := cellToOID(row[colIdx]); ok && oid >= minUserOID { + if mapped, ok := oidMap.Get(oid); ok { + row[colIdx] = mapped + } + } + } + } + } + err := compareRowsMultiset(aReadRows, bReadRows) + if err != nil && oidMap != nil && hasOIDCols && len(aReadRows) <= 2000 { + // The multiset comparison may have failed only because the result contains OIDs we haven't learned yet, so + // attempt a matching that is allowed to learn new mappings. The original error is kept if that fails too. + if compareRowsUnorderedLearning(oidMap, oidCols, aReadRows, bReadRows) { + return nil + } + } + return err +} + +// compareRowsUnorderedLearning greedily matches each recorded row to a replayed row under OID-lenient equality, +// accumulating tentative OID mappings as it goes. Returns whether a complete matching was found, in which case the +// tentative mappings are committed to the map. +func compareRowsUnorderedLearning(oidMap *OIDMap, oidCols []bool, aReadRows, bReadRows []sql.Row) bool { + candidates := make(map[uint32]uint32) + used := make([]bool, len(bReadRows)) + for _, aRow := range aReadRows { + matched := false + BRows: + for bIdx, bRow := range bReadRows { + if used[bIdx] || len(aRow) != len(bRow) { + continue + } + // Trial-match against a copy so that a failed row match doesn't pollute the accumulated candidates + trial := make(map[uint32]uint32, len(candidates)) + for k, v := range candidates { + trial[k] = v + } + for colIdx := range aRow { + if !cellsMatch(aRow[colIdx], bRow[colIdx], oidCols[colIdx], oidMap, trial) { + continue BRows + } + } + candidates = trial + used[bIdx] = true + matched = true + break + } + if !matched { + return false + } + } + oidMap.LearnAll(candidates) + return true +} + +// compareRowsMultiset compares the two sets of decoded rows as multisets, using each row's string form as its +// identity. +func compareRowsMultiset(aReadRows, bReadRows []sql.Row) error { // It's possible that two different rows can hash to the same result, but we're not concerned with that. // The same row will always output the same hash, and that's the only property that we really care about. aMap := make(map[string]int) bMap := make(map[string]int) - aReadRows := ReadRows(aRowDesc, aRows) - bReadRows := ReadRows(bRowDesc, bRows) for rowIdx := range aReadRows { // Column counts should always match, so this is a sanity check if len(aReadRows[rowIdx]) != len(bReadRows[rowIdx]) { diff --git a/testing/go/regression/tool/dump_trackers_test.go b/testing/go/regression/tool/dump_trackers_test.go new file mode 100644 index 0000000000..c7bb09c5ff --- /dev/null +++ b/testing/go/regression/tool/dump_trackers_test.go @@ -0,0 +1,88 @@ +// Copyright 2026 Dolthub, Inc. +// +// 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 main + +import ( + "fmt" + "os" + "sort" + "strings" + "testing" +) + +func TestDumpTrackers(t *testing.T) { + outPath, ok := os.LookupEnv("DUMP_TRACKERS") + if !ok { + t.Skip() + } + inPath := os.Getenv("DUMP_TRACKERS_IN") + if inPath == "" { + inPath = "out/results.trackers" + } + trackers, err := regressionFolder.ReadReplayTrackers(inPath) + if err != nil { + t.Fatal(err) + } + sb := &strings.Builder{} + totalSuccess, totalFail, totalPartial := uint32(0), uint32(0), uint32(0) + type fileStat struct { + file string + success, fail uint32 + } + stats := make([]fileStat, 0, len(trackers)) + for _, tr := range trackers { + totalSuccess += tr.Success + totalFail += tr.Failed + totalPartial += tr.PartialSuccess + stats = append(stats, fileStat{tr.File, tr.Success, tr.Failed}) + } + total := totalSuccess + totalFail + fmt.Fprintf(sb, "TOTAL: %d SUCCESS: %d (%.2f%%) FAIL: %d (%.2f%%) PARTIAL: %d\n\n", + total, totalSuccess, float64(totalSuccess)/float64(total)*100, + totalFail, float64(totalFail)/float64(total)*100, totalPartial) + sort.Slice(stats, func(i, j int) bool { return stats[i].fail > stats[j].fail }) + fmt.Fprintf(sb, "PER-FILE (sorted by failures):\n") + for _, s := range stats { + ft := s.success + s.fail + if ft == 0 { + continue + } + fmt.Fprintf(sb, "%-40s total=%-5d success=%-5d fail=%-5d (%.1f%% pass)\n", + s.file, ft, s.success, s.fail, float64(s.success)/float64(ft)*100) + } + sb.WriteString("\n==================== FAILURE DETAILS ====================\n") + for _, tr := range trackers { + if len(tr.FailPartialItems) == 0 { + continue + } + fmt.Fprintf(sb, "\n########## FILE: %s (fail=%d partial=%d) ##########\n", tr.File, tr.Failed, tr.PartialSuccess) + for _, item := range tr.FailPartialItems { + fmt.Fprintf(sb, "---\nQUERY: %s\n", item.Query) + if item.ExpectedError != "" { + fmt.Fprintf(sb, "EXPECTED ERROR: %s\n", item.ExpectedError) + } + if item.UnexpectedError != "" { + fmt.Fprintf(sb, "RECEIVED ERROR: %s\n", item.UnexpectedError) + } + for _, p := range item.PartialSuccess { + fmt.Fprintf(sb, "PARTIAL: %s\n", p) + } + } + } + if err := os.WriteFile(outPath, []byte(sb.String()), 0644); err != nil { + t.Fatal(err) + } + fmt.Println("wrote", outPath) +} diff --git a/testing/go/regression/tool/oid_map.go b/testing/go/regression/tool/oid_map.go new file mode 100644 index 0000000000..006304f6cb --- /dev/null +++ b/testing/go/regression/tool/oid_map.go @@ -0,0 +1,137 @@ +// Copyright 2026 Dolthub, Inc. +// +// 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 main + +import ( + "strconv" + "strings" +) + +// minUserOID is the first OID that Postgres assigns to user-created objects. OIDs below this value belong to the +// system catalogs, which are expected to be stable, so we never map them. +const minUserOID = 16384 + +// OIDMap tracks the mapping from OIDs that were recorded in the original Postgres session to the OIDs that the +// Doltgres server assigned to the same objects during the replay. Clients (psql in particular) read OIDs from +// catalog queries and embed them verbatim in follow-up queries, so a replay against a server with different OID +// assignments must translate those embedded OIDs for the follow-ups to have any chance of succeeding. +// +// Mappings are learned during row comparison: whenever an `oid`-typed result column contains a user-object OID that +// differs between the recording and the replay (and the rest of the comparison succeeds), the pair is recorded. +type OIDMap struct { + oids map[uint32]uint32 +} + +// NewOIDMap returns a new *OIDMap. +func NewOIDMap() *OIDMap { + return &OIDMap{oids: make(map[uint32]uint32)} +} + +// Get returns the replay OID that the given recorded OID maps to. +func (om *OIDMap) Get(recorded uint32) (uint32, bool) { + mapped, ok := om.oids[recorded] + return mapped, ok +} + +// LearnAll records all of the given recorded-to-replay OID pairs. Later learnings overwrite earlier ones, since a +// dropped and recreated object may reuse an OID on one side only. +func (om *OIDMap) LearnAll(candidates map[uint32]uint32) { + for recorded, actual := range candidates { + om.oids[recorded] = actual + } +} + +// RewriteQuery replaces every standalone numeric token that matches a recorded OID with its replay OID. Tokens that +// are part of an identifier (adjacent to letters, digits, or underscores) are left untouched. Replacements are not +// rescanned, so a replacement value can never be mistaken for another recorded OID. +func (om *OIDMap) RewriteQuery(query string) string { + if len(om.oids) == 0 { + return query + } + var sb *strings.Builder + last := 0 + for i := 0; i < len(query); { + if !isDigit(query[i]) { + i++ + continue + } + start := i + for i < len(query) && isDigit(query[i]) { + i++ + } + // A user OID has at least 5 digits; also reject digit runs that are part of an identifier + if i-start < 5 || i-start > 10 || + (start > 0 && isWordChar(query[start-1])) || + (i < len(query) && isWordChar(query[i])) { + continue + } + parsed, err := strconv.ParseUint(query[start:i], 10, 32) + if err != nil { + continue + } + mapped, ok := om.oids[uint32(parsed)] + if !ok { + continue + } + if sb == nil { + sb = &strings.Builder{} + sb.Grow(len(query) + 16) + } + sb.WriteString(query[last:start]) + sb.WriteString(strconv.FormatUint(uint64(mapped), 10)) + last = i + } + if sb == nil { + return query + } + sb.WriteString(query[last:]) + return sb.String() +} + +func isDigit(c byte) bool { + return c >= '0' && c <= '9' +} + +func isWordChar(c byte) bool { + return isDigit(c) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' +} + +// cellToOID interprets a decoded result cell as an OID if possible. Cells arrive as whatever type the pgtype scan +// produced, which differs between the recorded Postgres response and the Doltgres response, so this accepts every +// integer representation along with numeric strings. +func cellToOID(cell interface{}) (uint32, bool) { + switch val := cell.(type) { + case uint32: + return val, true + case int64: + if val >= 0 && val <= 4294967295 { + return uint32(val), true + } + case uint64: + if val <= 4294967295 { + return uint32(val), true + } + case int32: + if val >= 0 { + return uint32(val), true + } + case string: + parsed, err := strconv.ParseUint(val, 10, 32) + if err == nil { + return uint32(parsed), true + } + } + return 0, false +} diff --git a/testing/go/regression/tool/oid_map_test.go b/testing/go/regression/tool/oid_map_test.go new file mode 100644 index 0000000000..d66717db13 --- /dev/null +++ b/testing/go/regression/tool/oid_map_test.go @@ -0,0 +1,106 @@ +// Copyright 2026 Dolthub, Inc. +// +// 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 main + +import ( + "testing" + + "github.com/jackc/pgx/v5/pgproto3" + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOIDMapRewriteQuery(t *testing.T) { + om := NewOIDMap() + om.LearnAll(map[uint32]uint32{159776: 21005, 159780: 21009}) + assert.Equal(t, `SELECT 1 WHERE c.oid = '21005'`, om.RewriteQuery(`SELECT 1 WHERE c.oid = '159776'`)) + assert.Equal(t, `SELECT 1 WHERE c.oid = 21005 AND i.indexrelid = 21009`, + om.RewriteQuery(`SELECT 1 WHERE c.oid = 159776 AND i.indexrelid = 159780`)) + // Unknown OIDs, short numbers, and numbers embedded in identifiers are untouched + assert.Equal(t, `SELECT 159777, 1597, col159776, tbl_159776x FROM t`, + om.RewriteQuery(`SELECT 159777, 1597, col159776, tbl_159776x FROM t`)) + // A replacement value is never rescanned as another recorded OID + om2 := NewOIDMap() + om2.LearnAll(map[uint32]uint32{100001: 100002, 100002: 100003}) + assert.Equal(t, `100002 100003`, om2.RewriteQuery(`100001 100002`)) + // The query is returned as-is when the map is empty + empty := NewOIDMap() + assert.Equal(t, `SELECT '159776'`, empty.RewriteQuery(`SELECT '159776'`)) +} + +func TestOIDMapCompareLearning(t *testing.T) { + oidDesc := &pgproto3.RowDescription{Fields: []pgproto3.FieldDescription{ + {Name: []byte("oid"), DataTypeOID: pgtype.OIDOID, Format: 0}, + {Name: []byte("relname"), DataTypeOID: pgtype.TextOID, Format: 0}, + }} + rows := func(vals ...[2]string) []*pgproto3.DataRow { + out := make([]*pgproto3.DataRow, len(vals)) + for i, v := range vals { + out[i] = &pgproto3.DataRow{Values: [][]byte{[]byte(v[0]), []byte(v[1])}} + } + return out + } + + // Ordered comparison learns a new user-OID pairing when everything else matches + om := NewOIDMap() + require.NoError(t, CompareRowsOrdered(om, + oidDesc, oidDesc, + rows([2]string{"159776", "attmp"}), + rows([2]string{"21005", "attmp"}))) + mapped, ok := om.Get(159776) + require.True(t, ok) + assert.Equal(t, uint32(21005), mapped) + + // A learned mapping must stay consistent within a result set + require.Error(t, CompareRowsOrdered(om, + oidDesc, oidDesc, + rows([2]string{"159776", "a"}, [2]string{"159776", "b"}), + rows([2]string{"21005", "a"}, [2]string{"31000", "b"}))) + + // Non-OID differences still fail, and nothing is learned from a failed comparison + om2 := NewOIDMap() + require.Error(t, CompareRowsOrdered(om2, + oidDesc, oidDesc, + rows([2]string{"159776", "attmp"}), + rows([2]string{"21005", "other"}))) + _, ok = om2.Get(159776) + assert.False(t, ok) + + // System OIDs (below 16384) are never treated as equal when they differ + require.Error(t, CompareRowsOrdered(NewOIDMap(), + oidDesc, oidDesc, + rows([2]string{"1259", "pg_class"}), + rows([2]string{"1260", "pg_class"}))) + + // Unordered comparison learns pairings too, keyed off the non-OID columns + om3 := NewOIDMap() + require.NoError(t, CompareRowsUnordered(om3, + oidDesc, oidDesc, + rows([2]string{"159776", "a"}, [2]string{"159780", "b"}), + rows([2]string{"21009", "b"}, [2]string{"21005", "a"}))) + mapped, ok = om3.Get(159776) + require.True(t, ok) + assert.Equal(t, uint32(21005), mapped) + mapped, ok = om3.Get(159780) + require.True(t, ok) + assert.Equal(t, uint32(21009), mapped) + + // Unordered comparison uses previously learned mappings for translation + require.NoError(t, CompareRowsUnordered(om3, + oidDesc, oidDesc, + rows([2]string{"159776", "a"}), + rows([2]string{"21005", "a"}))) +} diff --git a/testing/go/regression/tool/replay.go b/testing/go/regression/tool/replay.go index 456d99b9a6..ea98fc42be 100644 --- a/testing/go/regression/tool/replay.go +++ b/testing/go/regression/tool/replay.go @@ -40,6 +40,10 @@ type ReplayOptions struct { func Replay(options ReplayOptions) (*ReplayTracker, error) { tracker := NewReplayTracker(options.File) reader := NewMessageReader(FilterMessages(options.Messages)) + // Clients read OIDs from catalog queries and embed them in follow-up queries, so we track the mapping between + // the OIDs in the recorded session and the OIDs the Doltgres server actually assigned. The map persists across + // the file's connections, since the recorded session's objects do too. + oidMap := NewOIDMap() t := time.Now() fmt.Println("-------------------- ", tracker.File, " --------------------") @@ -223,7 +227,14 @@ ListenerLoop: } } case *pgproto3.FunctionCall: - if err = connection.Send(message); err != nil { + sendFunctionCall := message + if mapped, ok := oidMap.Get(message.Function); ok { + // The recorded function OID belongs to the original session; translate it to the replay's OID + dup := *message + dup.Function = mapped + sendFunctionCall = &dup + } + if err = connection.Send(sendFunctionCall); err != nil { tracker.Failed++ tracker.AddFailure(ReplayTrackerItem{ Query: fmt.Sprintf("Function OID: %d", message.Function), @@ -344,7 +355,15 @@ ListenerLoop: } } case *pgproto3.Parse: - connection.Queue(message) + sendParse := message + if rewritten := oidMap.RewriteQuery(message.Query); rewritten != message.Query { + // Send the OID-translated text, but keep reporting the recorded text in the tracker so that + // cross-run comparisons see stable query strings + dup := *message + dup.Query = rewritten + sendParse = &dup + } + connection.Queue(sendParse) if sync, ok := reader.Peek().(*pgproto3.Sync); ok { _ = reader.Next() connection.Queue(sync) @@ -467,7 +486,13 @@ ListenerLoop: continue MessageLoop } } - if err = connection.Send(message); err != nil { + sendQuery := message + if rewritten := oidMap.RewriteQuery(message.String); rewritten != message.String { + // Send the OID-translated text, but keep reporting the recorded text in the tracker so that + // cross-run comparisons see stable query strings + sendQuery = &pgproto3.Query{String: rewritten} + } + if err = connection.Send(sendQuery); err != nil { tracker.Failed++ tracker.AddFailure(ReplayTrackerItem{ Query: message.String, @@ -614,7 +639,7 @@ ListenerLoop: } if strings.Contains(strings.ToLower(message.String), "order by") { // There's an ORDER BY, so we need to check based on the order - if err = CompareRowsOrdered(expectedRowDesc, responseRowDesc, expectedDataRows, responseDataRows); err != nil { + if err = CompareRowsOrdered(oidMap, expectedRowDesc, responseRowDesc, expectedDataRows, responseDataRows); err != nil { tracker.Failed++ tracker.AddFailure(ReplayTrackerItem{ Query: message.String, @@ -624,7 +649,7 @@ ListenerLoop: } } else { // There's no ORDER BY, so our native row order may differ from Postgres. - if err = CompareRowsUnordered(expectedRowDesc, responseRowDesc, expectedDataRows, responseDataRows); err != nil { + if err = CompareRowsUnordered(oidMap, expectedRowDesc, responseRowDesc, expectedDataRows, responseDataRows); err != nil { tracker.Failed++ tracker.AddFailure(ReplayTrackerItem{ Query: message.String, From ec568dee77aaca887c691d48f36a15c4a596ef20 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Fri, 14 Aug 2026 15:26:11 -0700 Subject: [PATCH 2/3] some renamings --- testing/go/regression/tool/compare.go | 28 +++++++++++----------- testing/go/regression/tool/oid_map.go | 6 ++--- testing/go/regression/tool/oid_map_test.go | 4 ++-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/testing/go/regression/tool/compare.go b/testing/go/regression/tool/compare.go index 84c94896a8..70121d10a3 100644 --- a/testing/go/regression/tool/compare.go +++ b/testing/go/regression/tool/compare.go @@ -42,14 +42,14 @@ func CompareRowsOrdered(oidMap *OIDMap, aRowDesc, bRowDesc *pgproto3.RowDescript aReadRows := ReadRows(aRowDesc, aRows) bReadRows := ReadRows(bRowDesc, bRows) oidCols := oidColumns(aRowDesc) - candidates := make(map[uint32]uint32) + oidReplacements := make(map[uint32]uint32) for rowIdx := range aReadRows { if len(aReadRows[rowIdx]) != len(bReadRows[rowIdx]) { return errors.Errorf("expected a row column count of %d but received %d", len(aReadRows[rowIdx]), len(bReadRows[rowIdx])) } for colIdx := range aReadRows[rowIdx] { - if !cellsMatch(aReadRows[rowIdx][colIdx], bReadRows[rowIdx][colIdx], oidCols[colIdx], oidMap, candidates) { + if !cellsMatch(aReadRows[rowIdx][colIdx], bReadRows[rowIdx][colIdx], oidCols[colIdx], oidMap, oidReplacements) { if len(aReadRows)+len(bReadRows) < 8 { return errors.Errorf("row sets differ:\n%s", rowsToErrorString(aReadRows, bReadRows)) } else { @@ -60,7 +60,7 @@ func CompareRowsOrdered(oidMap *OIDMap, aRowDesc, bRowDesc *pgproto3.RowDescript } } if oidMap != nil { - oidMap.LearnAll(candidates) + oidMap.PutAll(oidReplacements) } return nil } @@ -81,7 +81,7 @@ func oidColumns(rowDesc *pgproto3.RowDescription) []bool { // user-object OIDs are matched through the OIDMap: a previously learned mapping must agree, while a brand new pair is // tentatively accepted and added to candidates (the caller commits candidates to the map only if the entire // comparison succeeds, which keeps OID relationships consistent within a result set). -func cellsMatch(aVal, bVal interface{}, isOIDCol bool, oidMap *OIDMap, candidates map[uint32]uint32) bool { +func cellsMatch(aVal, bVal interface{}, isOIDCol bool, oidMap *OIDMap, oidReplacements map[uint32]uint32) bool { if aVal == bVal { return true } @@ -93,12 +93,12 @@ func cellsMatch(aVal, bVal interface{}, isOIDCol bool, oidMap *OIDMap, candidate if !aOK || !bOK || aOID < minUserOID || bOID == 0 { return false } - if mapped, ok := candidates[aOID]; ok { + if mapped, ok := oidReplacements[aOID]; ok { return mapped == bOID } // Unknown pairing (or the object was dropped and recreated on one side): tentatively accept it. OID values are // implementation-specific, so requiring consistency within the result set is the strongest meaningful check. - candidates[aOID] = bOID + oidReplacements[aOID] = bOID return true } @@ -135,18 +135,18 @@ func CompareRowsUnordered(oidMap *OIDMap, aRowDesc, bRowDesc *pgproto3.RowDescri if err != nil && oidMap != nil && hasOIDCols && len(aReadRows) <= 2000 { // The multiset comparison may have failed only because the result contains OIDs we haven't learned yet, so // attempt a matching that is allowed to learn new mappings. The original error is kept if that fails too. - if compareRowsUnorderedLearning(oidMap, oidCols, aReadRows, bReadRows) { + if compareRowsUnorderedWithOidReplacement(oidMap, oidCols, aReadRows, bReadRows) { return nil } } return err } -// compareRowsUnorderedLearning greedily matches each recorded row to a replayed row under OID-lenient equality, +// compareRowsUnorderedWithOidReplacement greedily matches each recorded row to a replayed row under OID-lenient equality, // accumulating tentative OID mappings as it goes. Returns whether a complete matching was found, in which case the // tentative mappings are committed to the map. -func compareRowsUnorderedLearning(oidMap *OIDMap, oidCols []bool, aReadRows, bReadRows []sql.Row) bool { - candidates := make(map[uint32]uint32) +func compareRowsUnorderedWithOidReplacement(oidMap *OIDMap, oidCols []bool, aReadRows, bReadRows []sql.Row) bool { + oidReplacements := make(map[uint32]uint32) used := make([]bool, len(bReadRows)) for _, aRow := range aReadRows { matched := false @@ -156,8 +156,8 @@ func compareRowsUnorderedLearning(oidMap *OIDMap, oidCols []bool, aReadRows, bRe continue } // Trial-match against a copy so that a failed row match doesn't pollute the accumulated candidates - trial := make(map[uint32]uint32, len(candidates)) - for k, v := range candidates { + trial := make(map[uint32]uint32, len(oidReplacements)) + for k, v := range oidReplacements { trial[k] = v } for colIdx := range aRow { @@ -165,7 +165,7 @@ func compareRowsUnorderedLearning(oidMap *OIDMap, oidCols []bool, aReadRows, bRe continue BRows } } - candidates = trial + oidReplacements = trial used[bIdx] = true matched = true break @@ -174,7 +174,7 @@ func compareRowsUnorderedLearning(oidMap *OIDMap, oidCols []bool, aReadRows, bRe return false } } - oidMap.LearnAll(candidates) + oidMap.PutAll(oidReplacements) return true } diff --git a/testing/go/regression/tool/oid_map.go b/testing/go/regression/tool/oid_map.go index 006304f6cb..3489a2e592 100644 --- a/testing/go/regression/tool/oid_map.go +++ b/testing/go/regression/tool/oid_map.go @@ -45,10 +45,10 @@ func (om *OIDMap) Get(recorded uint32) (uint32, bool) { return mapped, ok } -// LearnAll records all of the given recorded-to-replay OID pairs. Later learnings overwrite earlier ones, since a +// PutAll records all of the given recorded-to-replay OID pairs. Later learnings overwrite earlier ones, since a // dropped and recreated object may reuse an OID on one side only. -func (om *OIDMap) LearnAll(candidates map[uint32]uint32) { - for recorded, actual := range candidates { +func (om *OIDMap) PutAll(replacements map[uint32]uint32) { + for recorded, actual := range replacements { om.oids[recorded] = actual } } diff --git a/testing/go/regression/tool/oid_map_test.go b/testing/go/regression/tool/oid_map_test.go index d66717db13..da81eb0fb6 100644 --- a/testing/go/regression/tool/oid_map_test.go +++ b/testing/go/regression/tool/oid_map_test.go @@ -25,7 +25,7 @@ import ( func TestOIDMapRewriteQuery(t *testing.T) { om := NewOIDMap() - om.LearnAll(map[uint32]uint32{159776: 21005, 159780: 21009}) + om.PutAll(map[uint32]uint32{159776: 21005, 159780: 21009}) assert.Equal(t, `SELECT 1 WHERE c.oid = '21005'`, om.RewriteQuery(`SELECT 1 WHERE c.oid = '159776'`)) assert.Equal(t, `SELECT 1 WHERE c.oid = 21005 AND i.indexrelid = 21009`, om.RewriteQuery(`SELECT 1 WHERE c.oid = 159776 AND i.indexrelid = 159780`)) @@ -34,7 +34,7 @@ func TestOIDMapRewriteQuery(t *testing.T) { om.RewriteQuery(`SELECT 159777, 1597, col159776, tbl_159776x FROM t`)) // A replacement value is never rescanned as another recorded OID om2 := NewOIDMap() - om2.LearnAll(map[uint32]uint32{100001: 100002, 100002: 100003}) + om2.PutAll(map[uint32]uint32{100001: 100002, 100002: 100003}) assert.Equal(t, `100002 100003`, om2.RewriteQuery(`100001 100002`)) // The query is returned as-is when the map is empty empty := NewOIDMap() From 3f181014af07c8b80425bc0ee86cfdc1dd2b74d3 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Fri, 14 Aug 2026 15:32:50 -0700 Subject: [PATCH 3/3] new comment --- testing/go/regression/tool/dump_trackers_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/testing/go/regression/tool/dump_trackers_test.go b/testing/go/regression/tool/dump_trackers_test.go index c7bb09c5ff..b4900815e1 100644 --- a/testing/go/regression/tool/dump_trackers_test.go +++ b/testing/go/regression/tool/dump_trackers_test.go @@ -22,6 +22,18 @@ import ( "testing" ) +// TestDumpTrackers is a debugging tool rather than a unit test: it converts a binary .trackers file (produced by +// TestRegressionTests) into a human-readable text report, containing overall pass/fail counts, per-file pass rates +// sorted by failure count, and every failed or partially-successful query along with its expected and received +// errors. The text form is convenient for grepping and for bulk failure analysis (e.g. categorizing failures by +// error message to find common root causes). +// +// It only runs when the DUMP_TRACKERS environment variable is set, and asserts nothing. Usage: +// +// DUMP_TRACKERS=/path/to/report.txt go test -run TestDumpTrackers +// +// By default it reads out/results.trackers (the file the regression run writes); set DUMP_TRACKERS_IN to dump a +// different trackers file, such as a saved baseline or one downloaded from a CI artifact. func TestDumpTrackers(t *testing.T) { outPath, ok := os.LookupEnv("DUMP_TRACKERS") if !ok {