From 730797cec1f5ef1a194327347af3a94fbbcfef65 Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Sat, 17 Aug 2024 09:18:54 -0500 Subject: [PATCH] spoken: get message traces For #44. --- spoken/spoken.go | 40 ++++++++++++++++++ spoken/spoken_test.go | 96 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/spoken/spoken.go b/spoken/spoken.go index cd8cf90..58e2914 100644 --- a/spoken/spoken.go +++ b/spoken/spoken.go @@ -66,6 +66,46 @@ func Record[DB *sqlitex.Pool | *sqlite.Conn](ctx context.Context, db DB, tag, me return nil } +// Trace obtains the trace and time of the most recent instance of a message. +// If the message has not been recorded, the results are empty with a nil error. +func Trace[DB *sqlitex.Pool | *sqlite.Conn](ctx context.Context, db DB, tag, msg string) ([]string, time.Time, error) { + var conn *sqlite.Conn + switch db := any(db).(type) { + case *sqlite.Conn: + conn = db + case *sqlitex.Pool: + var err error + conn, err = db.Take(ctx) + defer db.Put(conn) + if err != nil { + return nil, time.Time{}, fmt.Errorf("couldn't get conn to find trace: %w", err) + } + } + const sel = `SELECT JSON(trace), time FROM spoken WHERE tag=:tag AND msg=:msg ORDER BY time DESC LIMIT 1` + st, err := conn.Prepare(sel) + if err != nil { + return nil, time.Time{}, fmt.Errorf("couldn't prepare statement to find trace: %w", err) + } + st.SetText(":tag", tag) + st.SetText(":msg", msg) + ok, err := st.Step() + if err != nil { + return nil, time.Time{}, fmt.Errorf("couldn't find trace: %w", err) + } + if !ok { + return nil, time.Time{}, nil + } + tr := st.ColumnText(0) + tm := st.ColumnInt64(1) + var trace []string + if err := json.Unmarshal([]byte(tr), &trace); err != nil { + return nil, time.Time{}, fmt.Errorf("couldn't decode trace: %w", err) + } + // Clean up the statement. + st.Step() + return trace, time.Unix(0, tm), nil +} + //go:embed schema.sql var schemaSQL string diff --git a/spoken/spoken_test.go b/spoken/spoken_test.go index d8bd878..de82c04 100644 --- a/spoken/spoken_test.go +++ b/spoken/spoken_test.go @@ -86,3 +86,99 @@ func TestRecord(t *testing.T) { t.Errorf("failed to scan: %v", err) } } + +func TestTrace(t *testing.T) { + // Create test fixture first. + ctx := context.Background() + db := testDB(ctx) + insert := []struct { + tag string + msg string + trace string + time int64 + }{ + {"kessoku", "bocchi", `["1"]`, 1}, + {"kessoku", "ryo", `["2"]`, 2}, + {"sickhack", "bocchi", `["3"]`, 3}, + {"kessoku", "ryo", `["4"]`, 4}, + } + { + conn, err := db.Take(ctx) + if err != nil { + t.Fatalf("couldn't get conn: %v", err) + } + st, err := conn.Prepare("INSERT INTO spoken (tag, msg, trace, time, meta) VALUES (:tag, :msg, JSONB(:trace), :time, JSONB('{}'))") + if err != nil { + t.Fatalf("couldn't prep insert: %v", err) + } + for _, r := range insert { + st.SetText(":tag", r.tag) + st.SetText(":msg", r.msg) + st.SetText(":trace", r.trace) + st.SetInt64(":time", r.time) + _, err := st.Step() + if err != nil { + t.Errorf("failed to insert %v: %v", r, err) + } + if err := st.Reset(); err != nil { + t.Errorf("couldn't reset: %v", err) + } + } + if err := st.Finalize(); err != nil { + t.Fatalf("couldn't finalize insert: %v", err) + } + db.Put(conn) + } + + cases := []struct { + name string + tag string + msg string + want []string + time time.Time + }{ + { + name: "none", + tag: "kessoku", + msg: "nijika", + want: nil, + time: time.Time{}, + }, + { + name: "single", + tag: "kessoku", + msg: "bocchi", + want: []string{"1"}, + time: time.Unix(0, 1), + }, + { + name: "latest", + tag: "kessoku", + msg: "ryo", + want: []string{"4"}, + time: time.Unix(0, 4), + }, + { + name: "tagged", + tag: "sickhack", + msg: "bocchi", + want: []string{"3"}, + time: time.Unix(0, 3), + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + trace, tm, err := spoken.Trace(context.Background(), db, c.tag, c.msg) + if err != nil { + t.Errorf("couldn't get trace: %v", err) + } + if !slices.Equal(trace, c.want) { + t.Errorf("wrong trace: want %q, got %q", c.want, trace) + } + if !tm.Equal(c.time) { + t.Errorf("wrong time: want %v, got %v", c.time, tm.UnixNano()) + } + }) + } +}