|
| 1 | +/* |
| 2 | + Copyright 2025 GitHub Inc. |
| 3 | + See https://github.com/github/gh-ost/blob/master/LICENSE |
| 4 | +*/ |
| 5 | + |
| 6 | +package logic |
| 7 | + |
| 8 | +import ( |
| 9 | + "testing" |
| 10 | + |
| 11 | + "github.com/stretchr/testify/require" |
| 12 | + |
| 13 | + "github.com/github/gh-ost/go/sql" |
| 14 | +) |
| 15 | + |
| 16 | +// TestSerializeRangeValues covers the table-agnostic, hex-per-value encoding used |
| 17 | +// to store a move-table's unique-key range in the single checkpoint table. |
| 18 | +func TestSerializeRangeValues(t *testing.T) { |
| 19 | + // nil ColumnValues serializes to the empty string. |
| 20 | + require.Equal(t, "", serializeRangeValues(nil)) |
| 21 | + |
| 22 | + // A single integer key: hex of its decimal text ("172" -> 31 37 32). |
| 23 | + require.Equal(t, "313732", serializeRangeValues(sql.ToColumnValues([]interface{}{172}))) |
| 24 | + |
| 25 | + // A varchar key: hex of the UTF-8 bytes ("code_8" -> 63 6f 64 65 5f 38). |
| 26 | + require.Equal(t, "636f64655f38", serializeRangeValues(sql.ToColumnValues([]interface{}{"code_8"}))) |
| 27 | + |
| 28 | + // A compound key of heterogeneous types is comma-joined. |
| 29 | + require.Equal(t, "3235,636f64655f38", |
| 30 | + serializeRangeValues(sql.ToColumnValues([]interface{}{25, "code_8"}))) |
| 31 | + |
| 32 | + // Raw bytes are hex-encoded as-is. |
| 33 | + require.Equal(t, "e590", |
| 34 | + serializeRangeValues(sql.ToColumnValues([]interface{}{[]byte{0xe5, 0x90}}))) |
| 35 | + |
| 36 | + // A nil column value is encoded with the unambiguous NULL token. |
| 37 | + require.Equal(t, moveTableCheckpointNullToken, |
| 38 | + serializeRangeValues(sql.ToColumnValues([]interface{}{nil}))) |
| 39 | +} |
| 40 | + |
| 41 | +// TestDeserializeRangeValuesRoundTrip verifies the encode->store->decode cycle. |
| 42 | +// Values come back as []byte (accepted directly as prepared-statement args), so |
| 43 | +// the round trip is checked on the serialized (canonical) form, which is what a |
| 44 | +// resumed run actually compares. |
| 45 | +func TestDeserializeRangeValuesRoundTrip(t *testing.T) { |
| 46 | + orig := sql.ToColumnValues([]interface{}{172, "code_8"}) |
| 47 | + s := serializeRangeValues(orig) |
| 48 | + |
| 49 | + got := deserializeRangeValues(s, 2) |
| 50 | + require.Equal(t, s, serializeRangeValues(got), "re-serializing the decoded range must reproduce the stored text") |
| 51 | + |
| 52 | + vals := got.AbstractValues() |
| 53 | + require.Len(t, vals, 2) |
| 54 | + require.Equal(t, []byte("172"), vals[0]) |
| 55 | + require.Equal(t, []byte("code_8"), vals[1]) |
| 56 | +} |
| 57 | + |
| 58 | +// TestDeserializeRangeValuesNullToken verifies the NULL marker decodes back to a |
| 59 | +// nil column value while other columns decode normally. |
| 60 | +func TestDeserializeRangeValuesNullToken(t *testing.T) { |
| 61 | + got := deserializeRangeValues(moveTableCheckpointNullToken+",3235", 2) |
| 62 | + vals := got.AbstractValues() |
| 63 | + require.Len(t, vals, 2) |
| 64 | + require.Nil(t, vals[0]) |
| 65 | + require.Equal(t, []byte("25"), vals[1]) |
| 66 | +} |
| 67 | + |
| 68 | +// TestIsEmptyRange verifies the predicate that tells a resumed run a table had no |
| 69 | +// completed chunk yet (so it must restart from the table minimum). |
| 70 | +func TestIsEmptyRange(t *testing.T) { |
| 71 | + require.True(t, isEmptyRange(nil), "nil range is empty") |
| 72 | + require.True(t, isEmptyRange(sql.NewColumnValues(0)), "zero-column range is empty") |
| 73 | + require.True(t, isEmptyRange(deserializeRangeValues(moveTableCheckpointNullToken, 1)), "all-nil range is empty") |
| 74 | + require.False(t, isEmptyRange(sql.ToColumnValues([]interface{}{1})), "a range with a value is not empty") |
| 75 | +} |
0 commit comments