Artifact 89c284d50953799e33d5001fe5c0ea557f8dce20ec80ab3f323c93f695bbc332:
- File
internal/ops/codec.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: 5366) [annotate] [blame] [check-ins using]
// codec.go — the form/URL-values half of the two-codec contract (0001 §9: // In structs must be expressible as both JSON and form values). Field names // come from the `json` tag so both codecs share one vocabulary; there is no // separate `form` tag to drift. package ops import ( "fmt" "net/url" "reflect" "strconv" "strings" ) // DecodeValues fills dst (pointer to struct) from form/query values. Missing // keys leave the zero value; unknown keys are ignored (HTML forms carry CSRF // tokens and submit buttons). Repeated keys populate slice fields. func DecodeValues(values url.Values, dst any) error { rv := reflect.ValueOf(dst) if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Struct { return fmt.Errorf("ops: DecodeValues dst must be *struct, got %T", dst) } return decodeValues(rv.Elem(), values, true) } // OverlayValues sets only the fields these values name, leaving every // other field exactly as it was. // // This exists because the router decodes twice: once from the query or // body, then again from the routed path so the path is authoritative. // The second pass used DecodeValues, whose contract is to apply a // field's declared default when its key is ABSENT — and every field is // absent from a path that names one wildcard. So a decoded ?limit=200 // was silently reset to its default of 50 on the way through, on every // op that has both a wildcard route and a defaulted field. Only fields // carrying a default were affected, which is why ?y=all worked and // ?limit did not, and why it survived this long. func OverlayValues(values url.Values, dst any) error { rv := reflect.ValueOf(dst) if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Struct { return fmt.Errorf("ops: OverlayValues dst must be *struct, got %T", dst) } return decodeValues(rv.Elem(), values, false) } func decodeValues(sv reflect.Value, values url.Values, defaults bool) error { st := sv.Type() for i := 0; i < st.NumField(); i++ { f := st.Field(i) name := fieldName(f) if name == "" || !f.IsExported() { continue } vals, ok := values[name] if !ok || len(vals) == 0 { if defaults { // Absent keys take the field's declared default (if any) — // which is how an explicit 0 stays distinguishable from // "not provided" (ticket 080). applyDefault(sv.Field(i), f) } continue } if err := setField(sv.Field(i), vals); err != nil { return fmt.Errorf("field %q: %w", name, err) } } return nil } func applyDefault(fv reflect.Value, f reflect.StructField) { r, err := parseRules(f.Tag.Get("validate")) if err != nil || !r.hasDef { return } switch fv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: fv.SetInt(int64(r.def)) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: fv.SetUint(uint64(r.def)) case reflect.Float32, reflect.Float64: fv.SetFloat(r.def) } } // fieldName returns the form/JSON key for a struct field: the json tag's // name segment, the field name if untagged, "" if the field is json:"-". func fieldName(f reflect.StructField) string { tag, ok := f.Tag.Lookup("json") if !ok { return f.Name } name, _, _ := strings.Cut(tag, ",") if name == "-" { return "" } if name == "" { return f.Name } return name } func setField(fv reflect.Value, vals []string) error { if fv.Kind() == reflect.Slice { elem := fv.Type().Elem() out := reflect.MakeSlice(fv.Type(), len(vals), len(vals)) for i, s := range vals { if err := setScalar(out.Index(i), elem.Kind(), s); err != nil { return err } } fv.Set(out) return nil } return setScalar(fv, fv.Kind(), vals[0]) } // setScalar parses one value. Parse failures return client-oriented // messages — these cross the HTTP boundary in 400 bodies, so no stdlib // error text leaks through. func setScalar(fv reflect.Value, kind reflect.Kind, s string) error { switch kind { case reflect.String: fv.SetString(s) case reflect.Bool: b, err := strconv.ParseBool(s) if err != nil { return fmt.Errorf("must be a boolean, got %q", s) } fv.SetBool(b) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: n, err := strconv.ParseInt(s, 10, fv.Type().Bits()) if err != nil { return fmt.Errorf("must be an integer, got %q", s) } fv.SetInt(n) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: n, err := strconv.ParseUint(s, 10, fv.Type().Bits()) if err != nil { return fmt.Errorf("must be a non-negative integer, got %q", s) } fv.SetUint(n) case reflect.Float32, reflect.Float64: n, err := strconv.ParseFloat(s, fv.Type().Bits()) if err != nil { return fmt.Errorf("must be a number, got %q", s) } fv.SetFloat(n) default: return fmt.Errorf("unsupported kind %s (In/Out structs must stay flat)", kind) } return nil } // Wildcards returns the {name} segments of a ServeMux pattern in order, // stripping a trailing "..." — the keys the router copies from // Request.PathValue into the url.Values handed to DecodeValues. func Wildcards(pattern string) []string { var names []string for _, seg := range strings.Split(pattern, "/") { if len(seg) > 1 && seg[0] == '{' && seg[len(seg)-1] == '}' { names = append(names, strings.TrimSuffix(seg[1:len(seg)-1], "...")) } } return names }