Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions internal/difftest/normalize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package difftest

import (
"fmt"
"sort"
"strings"

"github.com/aybavs/sql-query-engine/internal/value"
)

// nullCell is a sentinel no literal in the generated grammar can produce, so a
// text value never masquerades as NULL.
const nullCell = "∅"

// normalizeEngineRow renders one engine row as comparable cells. Cells carry a
// type tag so the text "35" cannot compare equal to the number 35.
func normalizeEngineRow(r value.Row) []string {
out := make([]string, len(r))
for i, v := range r {
switch {
case v.IsNull():
out[i] = nullCell
case v.Type == value.TInt:
out[i] = num(float64(v.I))
case v.Type == value.TFloat:
out[i] = num(v.F)
case v.Type == value.TBool:
out[i] = fmt.Sprintf("b:%t", v.B)
default:
out[i] = "s:" + v.S
}
}
return out
}

// normalizeOracleRow renders one SQLite driver row the same way.
func normalizeOracleRow(r Row) []string {
out := make([]string, len(r))
for i, cell := range r {
switch v := cell.(type) {
case nil:
out[i] = nullCell
case int64:
out[i] = num(float64(v))
case float64:
out[i] = num(v)
case bool:
out[i] = fmt.Sprintf("b:%t", v)
case []byte:
out[i] = "s:" + string(v)
case string:
out[i] = "s:" + v
default:
out[i] = fmt.Sprintf("?:%v", v)
}
}
return out
}

// num renders a number so an integer and an equal float compare the same.
// SQLite returns an average as a float where this engine may return an int, and
// the two can disagree in the last bits; six significant digits absorb that
// noise while still separating genuinely different values.
func num(f float64) string { return fmt.Sprintf("n:%.6g", f) }

// canonical sorts rows so two result sets can be compared as multisets. Row
// order is unspecified in SQL without ORDER BY, so the comparison must not
// depend on it — but duplicates still have to match.
func canonical(rows [][]string) []string {
out := make([]string, len(rows))
for i, r := range rows {
out[i] = strings.Join(r, "\x1f")
}
sort.Strings(out)
return out
}

// Compare reports whether two result sets hold the same rows, ignoring order
// but respecting duplicates.
func Compare(engine, oracle [][]string) error {
a, b := canonical(engine), canonical(oracle)
if len(a) != len(b) {
return fmt.Errorf("row count: engine %d, sqlite %d\n engine: %v\n sqlite: %v", len(a), len(b), a, b)
}
for i := range a {
if a[i] != b[i] {
return fmt.Errorf("row %d differs:\n engine: %s\n sqlite: %s", i, a[i], b[i])
}
}
return nil
}
86 changes: 86 additions & 0 deletions internal/difftest/normalize_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package difftest

import (
"testing"

"github.com/aybavs/sql-query-engine/internal/value"
)

func TestNormalizeMatchesIntAndFloat(t *testing.T) {
eng := normalizeEngineRow(value.Row{value.Int64(35)})
orc := normalizeOracleRow(Row{float64(35)})
if eng[0] != orc[0] {
t.Fatalf("int 35 = %q but float 35.0 = %q; they must normalize alike", eng[0], orc[0])
}
}

func TestNormalizeNull(t *testing.T) {
eng := normalizeEngineRow(value.Row{value.NullOf(value.TInt)})
orc := normalizeOracleRow(Row{nil})
if eng[0] != orc[0] {
t.Fatalf("NULL normalized differently: %q vs %q", eng[0], orc[0])
}
}

func TestNormalizeTextIsNotConfusedWithNull(t *testing.T) {
eng := normalizeEngineRow(value.Row{value.Text("NULL")})
orc := normalizeOracleRow(Row{nil})
if eng[0] == orc[0] {
t.Fatal("the text 'NULL' must not normalize to the NULL sentinel")
}
}

func TestNormalizeTextIsNotConfusedWithNumber(t *testing.T) {
eng := normalizeEngineRow(value.Row{value.Text("35")})
orc := normalizeOracleRow(Row{int64(35)})
if eng[0] == orc[0] {
t.Fatal("the text '35' must not normalize to the number 35")
}
}

func TestNormalizeAbsorbsFloatNoise(t *testing.T) {
// An average computed two different ways can differ in the last bits.
a := normalizeEngineRow(value.Row{value.Float64(35.0)})
b := normalizeOracleRow(Row{35.000000000000004})
if a[0] != b[0] {
t.Fatalf("float noise must not read as a difference: %q vs %q", a[0], b[0])
}
}

func TestNormalizeKeepsRealNumericDifference(t *testing.T) {
a := normalizeEngineRow(value.Row{value.Float64(35.0)})
b := normalizeOracleRow(Row{35.1})
if a[0] == b[0] {
t.Fatal("35.0 and 35.1 are genuinely different and must not normalize alike")
}
}

func TestCompareIgnoresRowOrder(t *testing.T) {
a := [][]string{{"1"}, {"2"}}
b := [][]string{{"2"}, {"1"}}
if err := Compare(a, b); err != nil {
t.Fatalf("row order must not matter: %v", err)
}
}

func TestCompareDetectsDifference(t *testing.T) {
a := [][]string{{"1"}, {"2"}}
b := [][]string{{"1"}, {"3"}}
if err := Compare(a, b); err == nil {
t.Fatal("differing multisets must be reported")
}
}

func TestCompareDetectsDuplicateCount(t *testing.T) {
a := [][]string{{"1"}, {"1"}}
b := [][]string{{"1"}}
if err := Compare(a, b); err == nil {
t.Fatal("duplicate counts must matter")
}
}

func TestCompareEmptyResultsMatch(t *testing.T) {
if err := Compare(nil, nil); err != nil {
t.Fatalf("two empty result sets must match: %v", err)
}
}
Loading