Skip to content

Commit 85bc994

Browse files
authored
Add TXID type (#36)
1 parent 0332387 commit 85bc994

File tree

8 files changed

+148
-68
lines changed

8 files changed

+148
-68
lines changed

cmd/ltx/checksum.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Usage:
4545
if err != nil {
4646
return err
4747
}
48-
defer f.Close()
48+
defer func() { _ = f.Close() }()
4949

5050
// Read database header to determine page size.
5151
buf := make([]byte, 100)

cmd/ltx/dump.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Arguments:
4747
if err != nil {
4848
return err
4949
}
50-
defer f.Close()
50+
defer func() { _ = f.Close() }()
5151

5252
dec := ltx.NewDecoder(f)
5353

@@ -59,8 +59,8 @@ Arguments:
5959
fmt.Printf("Flags: 0x%08x\n", hdr.Flags)
6060
fmt.Printf("Page size: %d\n", hdr.PageSize)
6161
fmt.Printf("Commit: %d\n", hdr.Commit)
62-
fmt.Printf("Min TXID: %s (%d)\n", ltx.FormatTXID(hdr.MinTXID), hdr.MinTXID)
63-
fmt.Printf("Max TXID: %s (%d)\n", ltx.FormatTXID(hdr.MaxTXID), hdr.MaxTXID)
62+
fmt.Printf("Min TXID: %s (%d)\n", hdr.MinTXID.String(), hdr.MinTXID)
63+
fmt.Printf("Max TXID: %s (%d)\n", hdr.MaxTXID.String(), hdr.MaxTXID)
6464
fmt.Printf("Timestamp: %s (%d)\n", time.UnixMilli(int64(hdr.Timestamp)).UTC().Format(time.RFC3339Nano), hdr.Timestamp)
6565
fmt.Printf("Pre-apply: %016x\n", hdr.PreApplyChecksum)
6666
fmt.Printf("WAL offset: %d\n", hdr.WALOffset)

cmd/ltx/list.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ Arguments:
4747
var w io.Writer = os.Stdout
4848
if !*tsv {
4949
tw := tabwriter.NewWriter(os.Stdout, 0, 8, 2, ' ', 0)
50-
defer tw.Flush()
50+
defer func() { _ = tw.Flush() }()
5151
w = tw
5252
}
5353

54-
fmt.Fprintln(w, "min_txid\tmax_txid\tcommit\tpages\tpreapply\tpostapply\ttimestamp\twal_offset\twal_size\twal_salt")
54+
_, _ = fmt.Fprintln(w, "min_txid\tmax_txid\tcommit\tpages\tpreapply\tpostapply\ttimestamp\twal_offset\twal_size\twal_salt")
5555
for _, arg := range fs.Args() {
5656
if err := c.printFile(w, arg); err != nil {
57-
fmt.Fprintf(os.Stderr, "%s: %s\n", arg, err)
57+
_, _ = fmt.Fprintf(os.Stderr, "%s: %s\n", arg, err)
5858
}
5959
}
6060

@@ -66,7 +66,7 @@ func (c *ListCommand) printFile(w io.Writer, filename string) error {
6666
if err != nil {
6767
return err
6868
}
69-
defer f.Close()
69+
defer func() { _ = f.Close() }()
7070

7171
dec := ltx.NewDecoder(f)
7272
if err := dec.Verify(); err != nil {
@@ -79,9 +79,9 @@ func (c *ListCommand) printFile(w io.Writer, filename string) error {
7979
timestamp = ""
8080
}
8181

82-
fmt.Fprintf(w, "%s\t%s\t%d\t%d\t%016x\t%016x\t%s\t%d\t%d\t%08x %08x\n",
83-
ltx.FormatTXID(dec.Header().MinTXID),
84-
ltx.FormatTXID(dec.Header().MaxTXID),
82+
_, _ = fmt.Fprintf(w, "%s\t%s\t%d\t%d\t%016x\t%016x\t%s\t%d\t%d\t%08x %08x\n",
83+
dec.Header().MinTXID.String(),
84+
dec.Header().MaxTXID.String(),
8585
dec.Header().Commit,
8686
dec.PageN(),
8787
dec.Header().PreApplyChecksum,

cmd/ltx/verify.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func (c *VerifyCommand) verifyFile(ctx context.Context, filename string) error {
6161
if err != nil {
6262
return err
6363
}
64-
defer f.Close()
64+
defer func() { _ = f.Close() }()
6565

6666
return ltx.NewDecoder(f).Verify()
6767
}

compactor.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ func (c *Compactor) Compact(ctx context.Context) (retErr error) {
5151
}
5252
if prevHdr.MaxTXID+1 != hdr.MinTXID {
5353
return fmt.Errorf("non-contiguous transaction ids in input files: (%s,%s) -> (%s,%s)",
54-
FormatTXID(prevHdr.MinTXID), FormatTXID(prevHdr.MaxTXID),
55-
FormatTXID(hdr.MinTXID), FormatTXID(hdr.MaxTXID),
54+
prevHdr.MinTXID.String(), prevHdr.MaxTXID.String(),
55+
hdr.MinTXID.String(), hdr.MaxTXID.String(),
5656
)
5757
}
5858
}

encoder.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,12 @@ func (enc *Encoder) EncodeHeader(hdr Header) error {
123123
// Use a compressed writer for the body if LZ4 is enabled.
124124
if enc.header.Flags&HeaderFlagCompressLZ4 != 0 {
125125
zw := lz4.NewWriter(enc.underlying)
126-
zw.Apply(lz4.BlockSizeOption(lz4.Block64Kb)) // minimize memory allocation
127-
zw.Apply(lz4.CompressionLevelOption(lz4.Fast))
126+
if err := zw.Apply(lz4.BlockSizeOption(lz4.Block64Kb)); err != nil { // minimize memory allocation
127+
return fmt.Errorf("cannot set lz4 block size: %w", err)
128+
}
129+
if err := zw.Apply(lz4.CompressionLevelOption(lz4.Fast)); err != nil {
130+
return fmt.Errorf("cannot set lz4 compression level: %w", err)
131+
}
128132
enc.w = zw
129133
}
130134

ltx.go

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const (
5858

5959
// Pos represents the transactional position of a database.
6060
type Pos struct {
61-
TXID uint64
61+
TXID TXID
6262
PostApplyChecksum uint64
6363
}
6464

@@ -75,7 +75,7 @@ func (p Pos) IsZero() bool {
7575
// Marshal serializes the position into JSON.
7676
func (p Pos) MarshalJSON() ([]byte, error) {
7777
var v posJSON
78-
v.TXID = FormatTXID(p.TXID)
78+
v.TXID = p.TXID.String()
7979
v.PostApplyChecksum = fmt.Sprintf("%016x", p.PostApplyChecksum)
8080
return json.Marshal(v)
8181
}
@@ -116,6 +116,51 @@ func (e *PosMismatchError) Error() string {
116116
return fmt.Sprintf("ltx position mismatch (%s)", e.Pos)
117117
}
118118

119+
// TXID represents a transaction ID.
120+
type TXID uint64
121+
122+
// ParseTXID parses a 16-character hex string into a transaction ID.
123+
func ParseTXID(s string) (TXID, error) {
124+
if len(s) != 16 {
125+
return 0, fmt.Errorf("invalid formatted transaction id length: %q", s)
126+
}
127+
v, err := strconv.ParseUint(s, 16, 64)
128+
if err != nil {
129+
return 0, fmt.Errorf("invalid transaction id format: %q", s)
130+
}
131+
return TXID(v), nil
132+
}
133+
134+
// String returns id formatted as a fixed-width hex number.
135+
func (t TXID) String() string {
136+
return fmt.Sprintf("%016x", uint64(t))
137+
}
138+
139+
func (t TXID) MarshalJSON() ([]byte, error) {
140+
return []byte(`"` + t.String() + `"`), nil
141+
}
142+
143+
func (t *TXID) UnmarshalJSON(data []byte) (err error) {
144+
var s *string
145+
if err := json.Unmarshal(data, &s); err != nil {
146+
return fmt.Errorf("cannot unmarshal TXID from JSON value")
147+
}
148+
149+
// Set to zero if value is nil.
150+
if s == nil {
151+
*t = 0
152+
return nil
153+
}
154+
155+
txID, err := ParseTXID(*s)
156+
if err != nil {
157+
return fmt.Errorf("cannot parse TXID from JSON string: %q", *s)
158+
}
159+
*t = TXID(txID)
160+
161+
return nil
162+
}
163+
119164
// Header flags.
120165
const (
121166
HeaderFlagMask = uint32(0x00000001)
@@ -129,8 +174,8 @@ type Header struct {
129174
Flags uint32 // reserved flags
130175
PageSize uint32 // page size, in bytes
131176
Commit uint32 // db size after transaction, in pages
132-
MinTXID uint64 // minimum transaction ID
133-
MaxTXID uint64 // maximum transaction ID
177+
MinTXID TXID // minimum transaction ID
178+
MaxTXID TXID // maximum transaction ID
134179
Timestamp int64 // milliseconds since unix epoch
135180
PreApplyChecksum uint64 // rolling checksum of database before applying this LTX file
136181
WALOffset int64 // file offset from original WAL; zero if journal
@@ -219,8 +264,8 @@ func (h *Header) MarshalBinary() ([]byte, error) {
219264
binary.BigEndian.PutUint32(b[4:], h.Flags)
220265
binary.BigEndian.PutUint32(b[8:], h.PageSize)
221266
binary.BigEndian.PutUint32(b[12:], h.Commit)
222-
binary.BigEndian.PutUint64(b[16:], h.MinTXID)
223-
binary.BigEndian.PutUint64(b[24:], h.MaxTXID)
267+
binary.BigEndian.PutUint64(b[16:], uint64(h.MinTXID))
268+
binary.BigEndian.PutUint64(b[24:], uint64(h.MaxTXID))
224269
binary.BigEndian.PutUint64(b[32:], uint64(h.Timestamp))
225270
binary.BigEndian.PutUint64(b[40:], h.PreApplyChecksum)
226271
binary.BigEndian.PutUint64(b[48:], uint64(h.WALOffset))
@@ -240,8 +285,8 @@ func (h *Header) UnmarshalBinary(b []byte) error {
240285
h.Flags = binary.BigEndian.Uint32(b[4:])
241286
h.PageSize = binary.BigEndian.Uint32(b[8:])
242287
h.Commit = binary.BigEndian.Uint32(b[12:])
243-
h.MinTXID = binary.BigEndian.Uint64(b[16:])
244-
h.MaxTXID = binary.BigEndian.Uint64(b[24:])
288+
h.MinTXID = TXID(binary.BigEndian.Uint64(b[16:]))
289+
h.MaxTXID = TXID(binary.BigEndian.Uint64(b[24:]))
245290
h.Timestamp = int64(binary.BigEndian.Uint64(b[32:]))
246291
h.PreApplyChecksum = binary.BigEndian.Uint64(b[40:])
247292
h.WALOffset = int64(binary.BigEndian.Uint64(b[48:]))
@@ -386,40 +431,23 @@ func ChecksumReader(r io.Reader, pageSize int) (uint64, error) {
386431
return chksum, nil
387432
}
388433

389-
// FormatTXID returns id formatted as a fixed-width hex number.
390-
func FormatTXID(id uint64) string {
391-
return fmt.Sprintf("%016x", id)
392-
}
393-
394-
// ParseTXID parses a 16-character hex string into a transaction ID.
395-
func ParseTXID(s string) (uint64, error) {
396-
if len(s) != 16 {
397-
return 0, fmt.Errorf("invalid formatted transaction id length: %q", s)
398-
}
399-
v, err := strconv.ParseUint(s, 16, 64)
400-
if err != nil {
401-
return 0, fmt.Errorf("invalid transaction id format: %q", s)
402-
}
403-
return uint64(v), nil
404-
}
405-
406434
// ParseFilename parses a transaction range from an LTX file.
407-
func ParseFilename(name string) (minTXID, maxTXID uint64, err error) {
435+
func ParseFilename(name string) (minTXID, maxTXID TXID, err error) {
408436
a := filenameRegex.FindStringSubmatch(name)
409437
if a == nil {
410438
return 0, 0, fmt.Errorf("invalid ltx filename: %s", name)
411439
}
412440

413-
minTXID, _ = strconv.ParseUint(a[1], 16, 64)
414-
maxTXID, _ = strconv.ParseUint(a[2], 16, 64)
415-
return minTXID, maxTXID, nil
441+
min, _ := strconv.ParseUint(a[1], 16, 64)
442+
max, _ := strconv.ParseUint(a[2], 16, 64)
443+
return TXID(min), TXID(max), nil
416444
}
417445

418446
var filenameRegex = regexp.MustCompile(`^([0-9a-f]{16})-([0-9a-f]{16})\.ltx$`)
419447

420448
// FormatFilename returns an LTX filename representing a range of transactions.
421-
func FormatFilename(minTXID, maxTXID uint64) string {
422-
return fmt.Sprintf("%016x-%016x.ltx", minTXID, maxTXID)
449+
func FormatFilename(minTXID, maxTXID TXID) string {
450+
return fmt.Sprintf("%s-%s.ltx", minTXID.String(), maxTXID.String())
423451
}
424452

425453
const PENDING_BYTE = 0x40000000

ltx_test.go

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ltx_test
33
import (
44
"bytes"
55
"context"
6+
"encoding/json"
67
"fmt"
78
"io"
89
"math"
@@ -245,9 +246,9 @@ func TestParseFilename(t *testing.T) {
245246
t.Run("OK", func(t *testing.T) {
246247
if min, max, err := ltx.ParseFilename("0000000000000001-00000000000003e8.ltx"); err != nil {
247248
t.Fatal(err)
248-
} else if got, want := min, uint64(1); got != want {
249+
} else if got, want := min, ltx.TXID(1); got != want {
249250
t.Fatalf("min=%d, want %d", got, want)
250-
} else if got, want := max, uint64(1000); got != want {
251+
} else if got, want := max, ltx.TXID(1000); got != want {
251252
t.Fatalf("max=%d, want %d", got, want)
252253
}
253254
})
@@ -287,14 +288,72 @@ func TestChecksumReader(t *testing.T) {
287288
})
288289
}
289290

290-
func TestFormatTXID(t *testing.T) {
291-
if got, want := ltx.FormatTXID(0), "0000000000000000"; got != want {
291+
func TestTXID_MarshalJSON(t *testing.T) {
292+
t.Run("OK", func(t *testing.T) {
293+
txID := ltx.TXID(1000)
294+
if buf, err := json.Marshal(txID); err != nil {
295+
t.Fatal(err)
296+
} else if got, want := string(buf), `"00000000000003e8"`; got != want {
297+
t.Fatalf("got=%q, want %q", got, want)
298+
}
299+
})
300+
t.Run("Map", func(t *testing.T) {
301+
m := map[string]ltx.TXID{"x": 1000, "y": 2000}
302+
if buf, err := json.Marshal(m); err != nil {
303+
t.Fatal(err)
304+
} else if got, want := string(buf), `{"x":"00000000000003e8","y":"00000000000007d0"}`; got != want {
305+
t.Fatalf("got=%q, want %q", got, want)
306+
}
307+
})
308+
}
309+
310+
func TestTXID_UnmarshalJSON(t *testing.T) {
311+
t.Run("OK", func(t *testing.T) {
312+
var txID ltx.TXID
313+
if err := json.Unmarshal([]byte(`"00000000000003e8"`), &txID); err != nil {
314+
t.Fatal(err)
315+
} else if got, want := txID, ltx.TXID(1000); got != want {
316+
t.Fatalf("got=%q, want %q", got, want)
317+
}
318+
})
319+
t.Run("Null", func(t *testing.T) {
320+
var txID ltx.TXID
321+
if err := json.Unmarshal([]byte(`null`), &txID); err != nil {
322+
t.Fatal(err)
323+
} else if got, want := txID, ltx.TXID(0); got != want {
324+
t.Fatalf("got=%q, want %q", got, want)
325+
}
326+
})
327+
t.Run("Map", func(t *testing.T) {
328+
var m map[string]ltx.TXID
329+
if err := json.Unmarshal([]byte(`{"x":"00000000000003e8","y":"00000000000007d0"}`), &m); err != nil {
330+
t.Fatal(err)
331+
} else if !reflect.DeepEqual(m, map[string]ltx.TXID{"x": 1000, "y": 2000}) {
332+
t.Fatalf("unexpected map: %#v", m)
333+
}
334+
})
335+
t.Run("ErrInvalidType", func(t *testing.T) {
336+
var txID ltx.TXID
337+
if err := json.Unmarshal([]byte(`123`), &txID); err == nil || err.Error() != `cannot unmarshal TXID from JSON value` {
338+
t.Fatalf("unexpected error: %s", err)
339+
}
340+
})
341+
t.Run("ErrStringFormat", func(t *testing.T) {
342+
var txID ltx.TXID
343+
if err := json.Unmarshal([]byte(`"xyz"`), &txID); err == nil || err.Error() != `cannot parse TXID from JSON string: "xyz"` {
344+
t.Fatalf("unexpected error: %s", err)
345+
}
346+
})
347+
}
348+
349+
func TestTXID_String(t *testing.T) {
350+
if got, want := ltx.TXID(0).String(), "0000000000000000"; got != want {
292351
t.Fatalf("got=%q, want %q", got, want)
293352
}
294-
if got, want := ltx.FormatTXID(1000), "00000000000003e8"; got != want {
353+
if got, want := ltx.TXID(1000).String(), "00000000000003e8"; got != want {
295354
t.Fatalf("got=%q, want %q", got, want)
296355
}
297-
if got, want := ltx.FormatTXID(math.MaxUint64), "ffffffffffffffff"; got != want {
356+
if got, want := ltx.TXID(math.MaxUint64).String(), "ffffffffffffffff"; got != want {
298357
t.Fatalf("got=%q, want %q", got, want)
299358
}
300359
}
@@ -303,19 +362,19 @@ func TestParseTXID(t *testing.T) {
303362
t.Run("OK", func(t *testing.T) {
304363
if v, err := ltx.ParseTXID("0000000000000000"); err != nil {
305364
t.Fatal(err)
306-
} else if got, want := v, uint64(0); got != want {
365+
} else if got, want := v, ltx.TXID(0); got != want {
307366
t.Fatalf("got=%d, want %d", got, want)
308367
}
309368

310369
if v, err := ltx.ParseTXID("00000000000003e8"); err != nil {
311370
t.Fatal(err)
312-
} else if got, want := v, uint64(1000); got != want {
371+
} else if got, want := v, ltx.TXID(1000); got != want {
313372
t.Fatalf("got=%d, want %d", got, want)
314373
}
315374

316375
if v, err := ltx.ParseTXID("ffffffffffffffff"); err != nil {
317376
t.Fatal(err)
318-
} else if got, want := v, uint64(math.MaxUint64); got != want {
377+
} else if got, want := v, ltx.TXID(math.MaxUint64); got != want {
319378
t.Fatalf("got=%d, want %d", got, want)
320379
}
321380
})
@@ -406,18 +465,7 @@ func createFile(tb testing.TB, name string) *os.File {
406465
if err != nil {
407466
tb.Fatal(err)
408467
}
409-
tb.Cleanup(func() { f.Close() })
410-
return f
411-
}
412-
413-
// openFile opens a file and returns the file handle. Fail on error.
414-
func openFile(tb testing.TB, name string) *os.File {
415-
tb.Helper()
416-
f, err := os.Open(name)
417-
if err != nil {
418-
tb.Fatal(err)
419-
}
420-
tb.Cleanup(func() { f.Close() })
468+
tb.Cleanup(func() { _ = f.Close() })
421469
return f
422470
}
423471

0 commit comments

Comments
 (0)