Artifact 1b9f6bd8bd06698b8f96c828f76e36918a9a69253b812a185d033c472fa7cc62:
- File
internal/ops/codec_test.go
— part of check-in
[450a8ad0f3]
at
2026-08-17 20:42:49
on branch trunk
— index: denormalize the timeline into an event table
The timeline assembled itself at read time from a five-way UNION ALL, ending in USE TEMP B-TREE FOR ORDER BY — every request sorted every event in the repository to return fifty. tk cost 536ms; fossil serves the same page in 3-4ms whatever the repository size.
This is fossil's shape: one event table with a kind column and an index on date, so the read is an index walk that stops at the limit. Triggers maintain it, like search_fts, so it cannot drift from its sources. isleaf and the ticket label are resolved for the returned page only.
tk 536ms -> 4ms sqlite 210ms -> 5ms fossil 163ms -> 3ms pikchr 22ms -> 1ms home 399ms -> 3ms (it asked each repo for its newest event)
plan_test asserts the plan, not the types: nothing about the old query was ill-typed, and the defect was only ever visible in EXPLAIN.
Also fixes limit being ignored on every wildcard route: the router's second decode pass re-applied defaults to fields the path did not name, so ?limit=200 became 50. OverlayValues sets only what it names. (user: dmestas, size: 5521) [annotate] [blame] [check-ins using]
package ops import ( "net/url" "reflect" "strings" "testing" ) type formIn struct { Repo string `json:"repo"` Limit int `json:"limit"` After uint64 `json:"after"` Ratio float64 `json:"ratio"` Deleted bool `json:"deleted"` Tags []string `json:"tags"` Skip string `json:"-"` NoTag string } func TestDecodeValues(t *testing.T) { v := url.Values{ "repo": {"sqlite"}, "limit": {"50"}, "after": {"12345"}, "ratio": {"0.5"}, "deleted": {"true"}, "tags": {"a", "b"}, "NoTag": {"plain"}, } var in formIn if err := DecodeValues(v, &in); err != nil { t.Fatal(err) } want := formIn{ Repo: "sqlite", Limit: 50, After: 12345, Ratio: 0.5, Deleted: true, Tags: []string{"a", "b"}, NoTag: "plain", } if !reflect.DeepEqual(in, want) { t.Fatalf("got %+v, want %+v", in, want) } } func TestDecodeValuesMissingKeysLeaveZero(t *testing.T) { var in formIn if err := DecodeValues(url.Values{"repo": {"x"}}, &in); err != nil { t.Fatal(err) } if in.Repo != "x" || in.Limit != 0 || in.Tags != nil { t.Fatalf("got %+v", in) } } func TestDecodeValuesUnknownKeysIgnored(t *testing.T) { // HTML forms carry extras (CSRF tokens, submit buttons); never reject. var in formIn if err := DecodeValues(url.Values{"csrf": {"tok"}}, &in); err != nil { t.Fatal(err) } } func TestDecodeValuesBadNumber(t *testing.T) { var in formIn err := DecodeValues(url.Values{"limit": {"abc"}}, &in) if err == nil || !strings.Contains(err.Error(), "limit") { t.Fatalf("want error naming field limit, got %v", err) } } func TestDecodeValuesBadBool(t *testing.T) { var in formIn if err := DecodeValues(url.Values{"deleted": {"maybe"}}, &in); err == nil { t.Fatal("want bool parse error, got nil") } } func TestDecodeValuesSkipsDashTag(t *testing.T) { var in formIn if err := DecodeValues(url.Values{"Skip": {"x"}, "-": {"y"}}, &in); err != nil { t.Fatal(err) } if in.Skip != "" { t.Fatalf("json:\"-\" field decoded: %+v", in) } } func TestDecodeValuesRejectsNonPointerStruct(t *testing.T) { if err := DecodeValues(url.Values{}, formIn{}); err == nil { t.Fatal("want error for non-pointer dst, got nil") } x := 5 if err := DecodeValues(url.Values{}, &x); err == nil { t.Fatal("want error for non-struct dst, got nil") } } func TestDecodeValuesRejectsUnsupportedFieldType(t *testing.T) { type bad struct { Nested struct{ X int } `json:"nested"` } var b bad if err := DecodeValues(url.Values{"nested": {"{}"}}, &b); err == nil { t.Fatal("want unsupported-type error, got nil") } } func TestWildcards(t *testing.T) { for _, tc := range []struct { pattern string want []string }{ {"/api/v1/r/{repo}/timeline", []string{"repo"}}, {"/api/v1/r/{repo}/checkin/{hash}", []string{"repo", "hash"}}, {"/api/v1/version", nil}, {"/files/{path...}", []string{"path"}}, } { got := Wildcards(tc.pattern) if !reflect.DeepEqual(got, tc.want) { t.Errorf("Wildcards(%q) = %v, want %v", tc.pattern, got, tc.want) } } } func TestDecodeValuesAppliesDefaults(t *testing.T) { type withDef struct { Limit int `json:"limit" validate:"min=0,max=200,default=50"` } var in withDef if err := DecodeValues(url.Values{}, &in); err != nil { t.Fatal(err) } if in.Limit != 50 { t.Fatalf("absent key: limit = %d, want default 50", in.Limit) } // An explicit 0 is NOT the default (ticket 080). in = withDef{} if err := DecodeValues(url.Values{"limit": {"0"}}, &in); err != nil { t.Fatal(err) } if in.Limit != 0 { t.Fatalf("explicit 0: limit = %d", in.Limit) } } func TestDecodeValuesCleanParseErrors(t *testing.T) { var in formIn err := DecodeValues(url.Values{"limit": {"abc"}}, &in) if err == nil || strings.Contains(err.Error(), "strconv") || !strings.Contains(err.Error(), "must be an integer") { t.Fatalf("err = %v", err) } } // The router decodes twice — query/body first, then the routed path so // the path wins. DecodeValues applies a field's default when its key is // absent, and EVERY field is absent from a path that names one wildcard, // so the second pass used to reset defaulted fields that the first pass // had set. ?limit=200 became 50 on every op with a wildcard route. func TestOverlayValuesKeepsWhatItDoesNotName(t *testing.T) { type in struct { Repo string `json:"repo"` Limit int `json:"limit" validate:"min=0,max=200,default=50"` Branch string `json:"branch"` } var got in if err := DecodeValues(url.Values{"limit": {"200"}, "branch": {"trunk"}}, &got); err != nil { t.Fatal(err) } if got.Limit != 200 { t.Fatalf("first pass: Limit = %d, want 200", got.Limit) } // The path names only repo; everything else must survive untouched. if err := OverlayValues(url.Values{"repo": {"alpha"}}, &got); err != nil { t.Fatal(err) } if got.Repo != "alpha" { t.Errorf("Repo = %q, want the path value", got.Repo) } if got.Limit != 200 { t.Errorf("Limit = %d — the path pass reset it to its default", got.Limit) } if got.Branch != "trunk" { t.Errorf("Branch = %q, want it preserved", got.Branch) } } // DecodeValues must keep applying defaults; the fix is scoped to the // overlay pass, not a removal of default handling. func TestDecodeValuesStillAppliesDefaults(t *testing.T) { type in struct { Limit int `json:"limit" validate:"min=0,max=200,default=50"` } var got in if err := DecodeValues(url.Values{}, &got); err != nil { t.Fatal(err) } if got.Limit != 50 { t.Errorf("Limit = %d, want the declared default 50", got.Limit) } }