Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/flashduty/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func TestProjectionOverflowFailsHard(t *testing.T) {
if stdout.Len() != 0 {
t.Errorf("[#79] a failed projection must write nothing to stdout, got %d bytes:\n%s", stdout.Len(), stdout.String())
}
if !strings.Contains(stderr.String(), "Error: projected list is") || !strings.Contains(stderr.String(), "exceeds the 16384-byte limit") {
if !strings.Contains(stderr.String(), "Error: projected list is") || !strings.Contains(stderr.String(), "16384-byte structured-output limit") {
t.Errorf("[#79] stderr should report the byte-limit refusal, got:\n%s", stderr.String())
}
}
6 changes: 3 additions & 3 deletions internal/cli/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1721,13 +1721,13 @@ func TestCommandListProjectionOverflowFails(t *testing.T) {
stub.data = map[string]any{"items": []any{row}, "total": 1}

out, stderrText, err := execCommandSplit("incident", "list", "--fields", "incident_id,labels", "--output-format", "json")
if err == nil || !strings.Contains(err.Error(), "exceeds the 16384-byte limit") {
if err == nil || !strings.Contains(err.Error(), "16384-byte structured-output limit") {
t.Fatalf("irreducible projection error = %v, want the byte-limit refusal", err)
}
if out != "" {
t.Errorf("a failed projection must write nothing to stdout, got %d bytes", len(out))
}
if strings.Contains(stderrText, "exceeds the") {
if strings.Contains(stderrText, "structured-output limit") {
t.Errorf("the error is returned for the entrypoint to report, not printed mid-run, got:\n%s", stderrText)
}
})
Expand All @@ -1747,7 +1747,7 @@ func TestCommandListProjectionOverflowFails(t *testing.T) {
stub.data = map[string]any{"items": []any{row}, "total": 1}

out, _, err := execCommandSplit("alert-event", "list", "--fields", "event_id,labels", "--output-format", "json")
if err == nil || !strings.Contains(err.Error(), "exceeds the 16384-byte limit") {
if err == nil || !strings.Contains(err.Error(), "16384-byte structured-output limit") {
t.Fatalf("irreducible projection error = %v, want the byte-limit refusal", err)
}
if out != "" {
Expand Down
68 changes: 46 additions & 22 deletions internal/cli/fieldproject.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ type projectionBound struct {
shortened int
valuesTotal int
fields []string
// maxBytes is the budget the reduction was sized against.
// maxBytes is the published limit the payload had to fit under — not the
// rows' share of it, which is smaller when they ride inside an envelope.
maxBytes int
}

Expand Down Expand Up @@ -203,20 +204,29 @@ func noteProjectionBound(cmd *cobra.Command, bound projectionBound) {
// callers turn it into the command-appropriate failure with
// explainProjectionOverflow.
type projectionOverflow struct {
detail bool
bytes int
rows int
detail bool
bytes int
rows int
// maxBytes is the published limit, so the error names that cap rather than
// the rows' internal share of it.
maxBytes int
largest string
}

func (o *projectionOverflow) Error() string {
if o.detail {
return fmt.Sprintf("projected detail is %d bytes, exceeds the %d-byte limit; largest fields: %s",
return fmt.Sprintf("projected detail is %d bytes and does not fit under the %d-byte structured-output limit; largest fields: %s",
o.bytes, o.maxBytes, o.largest)
}
return fmt.Sprintf("projected list is %d bytes across %d rows, exceeds the %d-byte limit; largest fields: %s",
o.bytes, o.rows, o.maxBytes, o.largest)
// The count is the rows' own encoding, the limit the payload's: inside an
// envelope the two live in different spaces, so the sentence states the
// size and the refusal separately rather than comparing them.
rowNoun := "rows"
if o.rows == 1 {
rowNoun = "row"
}
return fmt.Sprintf("projected list is %d bytes across %d %s and cannot be reduced to fit under the %d-byte structured-output limit; largest fields: %s",
o.bytes, o.rows, rowNoun, o.maxBytes, o.largest)
}

// explainProjectionOverflow returns err with the remedy for cmd appended when
Expand Down Expand Up @@ -259,7 +269,7 @@ func boundProjectedOutput(data any, maxBytes int) (any, projectionBound, error)
case map[string]any:
return value, projectionBound{}, boundProjectedDetail(value, maxBytes)
case []map[string]any:
return boundProjectedList(value, maxBytes)
return boundProjectedList(value, maxBytes, 0)
default:
return nil, projectionBound{}, fmt.Errorf("internal error: unsupported projected output %T", data)
}
Expand Down Expand Up @@ -325,7 +335,9 @@ func boundProjectedDetail(row map[string]any, maxBytes int) error {
if err != nil {
return err
}
return &projectionOverflow{detail: true, bytes: len(encoded), maxBytes: maxBytes, largest: largest}
// +1 for the trailing newline the printer appends, so the reported size is
// the one the fit test just measured.
return &projectionOverflow{detail: true, bytes: len(encoded) + 1, maxBytes: maxBytes, largest: largest}
}

// isIdentifierField reports whether a projected field is an identifier:
Expand Down Expand Up @@ -356,12 +368,21 @@ func isIdentifierField(key string) bool {
// command fails with a small error instead of emitting values that look
// real but aren't. Whatever it reduces or clips, it reports back in the
// returned projectionBound.
func boundProjectedList(rows []map[string]any, maxBytes int) ([]map[string]any, projectionBound, error) {
//
// The rows are sized against limit minus framing, where framing is the bytes
// the payload costs around them besides the row array (0 when the rows are the
// payload), while the returned facts name limit: the caller's note quotes the
// published cap, not an internal remainder. The input rows are never modified,
// so a caller that re-bounds them against a smaller budget (the envelope
// re-fit) measures every pass from the original values and reports facts that
// match the payload it prints.
func boundProjectedList(rows []map[string]any, limit, framing int) ([]map[string]any, projectionBound, error) {
budget := limit - framing
encoded, err := marshalStructured(rows)
if err != nil {
return nil, projectionBound{}, err
}
if len(encoded)+1 < maxBytes {
if len(encoded)+1 < budget {
return rows, projectionBound{}, nil
}

Expand All @@ -372,15 +393,17 @@ func boundProjectedList(rows []map[string]any, maxBytes int) ([]map[string]any,
if err != nil {
return nil, projectionBound{}, err
}
return nil, projectionBound{}, &projectionOverflow{bytes: len(encoded), rows: len(rows), maxBytes: maxBytes, largest: largest}
// +1 for the trailing newline the printer appends, so the reported size
// is the one the fit test just measured.
return nil, projectionBound{}, &projectionOverflow{bytes: len(encoded) + 1, rows: len(rows), maxBytes: limit, largest: largest}
}

kept, err := largestFittingPrefix(rows, maxBytes)
kept, err := largestFittingPrefix(rows, budget)
if err != nil {
return nil, projectionBound{}, err
}
if kept > 0 {
return rows[:kept], projectionBound{rowsEmitted: kept, rowsTotal: len(rows), maxBytes: maxBytes}, nil
return rows[:kept], projectionBound{rowsEmitted: kept, rowsTotal: len(rows), maxBytes: limit}, nil
}

maxLen := 0
Expand Down Expand Up @@ -415,7 +438,7 @@ func boundProjectedList(rows []map[string]any, maxBytes int) ([]map[string]any,
if err != nil {
return false, err
}
return len(trialEncoded)+1 < maxBytes, nil
return len(trialEncoded)+1 < budget, nil
}

// minMarkedTruncationCap is the smallest cap for which truncateUTF8Bytes
Expand Down Expand Up @@ -453,13 +476,13 @@ func boundProjectedList(rows []map[string]any, maxBytes int) ([]map[string]any,

shortened, total := 0, 0
fields := map[string]bool{}
for _, row := range rows {
bounded := make([]map[string]any, len(rows))
for i, row := range rows {
boundedRow := make(map[string]any, len(row))
for key, value := range row {
if isIdentifierField(key) {
continue
}
text, ok := value.(string)
if !ok {
if !ok || isIdentifierField(key) {
boundedRow[key] = value
continue
}
total++
Expand All @@ -468,8 +491,9 @@ func boundProjectedList(rows []map[string]any, maxBytes int) ([]map[string]any,
shortened++
fields[key] = true
}
row[key] = clipped
boundedRow[key] = clipped
}
bounded[i] = boundedRow
}
if shortened == 0 {
return rows, projectionBound{}, nil
Expand All @@ -479,7 +503,7 @@ func boundProjectedList(rows []map[string]any, maxBytes int) ([]map[string]any,
names = append(names, name)
}
sort.Strings(names)
return rows, projectionBound{shortened: shortened, valuesTotal: total, fields: names, maxBytes: maxBytes}, nil
return bounded, projectionBound{shortened: shortened, valuesTotal: total, fields: names, maxBytes: limit}, nil
}

// largestFittingPrefix returns the largest n < len(rows) whose encoded prefix
Expand Down
52 changes: 47 additions & 5 deletions internal/cli/fieldproject_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,29 +38,71 @@ func TestBoundProjectedOutputCapsStructuredFormats(t *testing.T) {
t.Run(format, func(t *testing.T) {
saveAndResetGlobals(t)
flagOutputFormat = format
longTitle := strings.Repeat("数据库故障", 2000)
rows := []map[string]any{{
"incident_id": "inc-1",
"title": strings.Repeat("数据库故障", 2000),
"title": longTitle,
}}

if _, _, err := boundProjectedOutput(rows, 512); err != nil {
bounded, _, err := boundProjectedOutput(rows, 512)
if err != nil {
t.Fatalf("bound projected output: %v", err)
}
encoded, err := marshalStructured(rows)
boundedRows, ok := bounded.([]map[string]any)
if !ok {
t.Fatalf("bounded output = %T, want []map[string]any", bounded)
}
encoded, err := marshalStructured(boundedRows)
if err != nil {
t.Fatalf("marshal bounded output: %v", err)
}
if len(encoded)+1 >= 512 {
t.Fatalf("bounded %s output is %d bytes, want <512", format, len(encoded)+1)
}
title := rows[0]["title"].(string)
// The input stays untouched: the caller prints the returned value,
// and the envelope re-fit re-bounds the same rows against a smaller
// budget, so every pass must see the original data.
if original, _ := rows[0]["title"].(string); original != longTitle {
t.Fatalf("bounding modified the input row: title is %d bytes, want it untouched", len(original))
}
title := boundedRows[0]["title"].(string)
if !utf8.ValidString(title) || !strings.HasSuffix(title, "...") {
t.Fatalf("truncated title = %q, want valid UTF-8 with marker", title)
}
})
}
}

// TestBoundProjectedOutputRefusesOversizeDetail pins the detail half of the
// refusal: a single-object projection is never modified (a clipped id or status
// would pass for a real value), so one that overflows the detail limit fails
// instead, and its message reports the rendered size the fit test measured
// against the detail cap — the two numbers live in the same space here, so the
// sentence can state them together.
func TestBoundProjectedOutputRefusesOversizeDetail(t *testing.T) {
saveAndResetGlobals(t)
flagOutputFormat = "json"
row := map[string]any{
"incident_id": "inc-1",
"counts": make([]int, 5000),
}

_, _, err := boundProjectedOutput(row, compactDetailOutputLimit)
if err == nil {
t.Fatal("oversize detail = nil error, want the overflow refusal")
}
if !strings.Contains(err.Error(), "projected detail is ") {
t.Fatalf("detail refusal should name the detail payload, got: %v", err)
}
want := fmt.Sprintf("does not fit under the %d-byte structured-output limit", compactDetailOutputLimit)
if !strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), "counts") {
t.Fatalf("detail refusal = %v, want %q and the largest field", err, want)
}
if strings.Contains(err.Error(), "exceeds") {
t.Fatalf("the refusal must not claim a size exceeds a limit it was measured against in another space, got: %v", err)
}
}

func TestBoundProjectedOutputRejectsIrreducibleMetadata(t *testing.T) {
saveAndResetGlobals(t)
flagOutputFormat = "json"
Expand All @@ -73,7 +115,7 @@ func TestBoundProjectedOutputRejectsIrreducibleMetadata(t *testing.T) {
if err == nil {
t.Fatal("irreducible output = nil error, want the overflow refusal")
}
if !strings.Contains(err.Error(), "exceeds the 512-byte limit") || !strings.Contains(err.Error(), "counts") {
if !strings.Contains(err.Error(), "cannot be reduced to fit under the 512-byte structured-output limit") || !strings.Contains(err.Error(), "counts") {
t.Fatalf("irreducible output error = %v, want the byte budget and the largest field", err)
}
// The byte-bounder's own message is deliberately flag-neutral: it knows
Expand Down
44 changes: 25 additions & 19 deletions internal/cli/gen_support.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ func printBoundedGenericResult(ctx *RunContext, data any) error {
if !ok {
return ctx.Printer.Print(data, nil)
}
bounded, bound, err := boundProjectedList(rows, compactListOutputLimit)
bounded, bound, err := boundProjectedList(rows, compactListOutputLimit, 0)
if err != nil {
return explainProjectionOverflow(ctx.Cmd, err)
}
Expand All @@ -399,30 +399,36 @@ func printBoundedGenericResult(ctx *RunContext, data any) error {
if !ok {
return ctx.Printer.Print(data, nil)
}
// boundProjectedList sizes the rows standalone, but printed inside the
// envelope they share the budget with the pagination siblings (and, in
// indented JSON, sit one indent level deeper). Fit against the full
// limit, then re-fit with the observed envelope overhead subtracted
// until the whole payload is under it.
budget := compactListOutputLimit
// The rows are first sized against the whole limit, but printed inside
// the envelope they share the budget with the pagination siblings (and,
// in indented JSON, sit one indent level deeper). Each re-fit sizes
// them against the limit minus the framing seen so far, growing that
// framing by each observed overflow until the whole payload fits.
framing := 0
for {
bounded, bound, err := boundProjectedList(rows, budget)
bounded, bound, err := boundProjectedList(rows, compactListOutputLimit, framing)
if err != nil {
return explainProjectionOverflow(ctx.Cmd, err)
}
value[key] = bounded
// In-payload, not just on stderr: scripts discard stderr, and
// the pagination siblings keep describing the server page, so a
// reduced page would otherwise read as complete. emitted_rows
// is set only when rows were withheld (a prefix was dropped),
// which is the case paging can repair; when instead long values
// were clipped, truncated rides alone — re-requesting cannot
// restore them, narrowing --fields can. Stamp both keys from this
// pass alone, so a key left over from an earlier re-fit cannot
// describe a reduction this payload does not carry.
if bound.reduced() {
// In-payload, not just on stderr: scripts discard stderr, and
// the pagination siblings keep describing the server page, so a
// reduced page would otherwise read as complete. emitted_rows
// is set only when rows were WITHHELD (a prefix was dropped),
// which is the case paging can repair; when instead long values
// were clipped, truncated rides alone — re-requesting cannot
// restore them, narrowing --fields can.
value["truncated"] = true
if len(bounded) < len(rows) {
value["emitted_rows"] = len(bounded)
}
} else {
delete(value, "truncated")
}
if len(bounded) < len(rows) {
value["emitted_rows"] = len(bounded)
} else {
delete(value, "emitted_rows")
}
out, err := marshalStructured(value)
if err != nil {
Expand All @@ -432,7 +438,7 @@ func printBoundedGenericResult(ctx *RunContext, data any) error {
noteProjectionBound(ctx.Cmd, bound)
return ctx.Printer.Print(value, nil)
}
budget -= len(out) + 2 - compactListOutputLimit
framing += len(out) + 2 - compactListOutputLimit
}
default:
return ctx.Printer.Print(data, nil)
Expand Down
Loading