miller/pkg/output/record_writer_yaml.go
chaoliang yan 5e8175bc6b
fix: preserve key order in YAML output (#2034)
* fix: preserve key order in YAML output

MlrmapToYAMLNative converted the ordered Mlrmap to map[string]interface{},
which loses insertion order. yaml.v3.Marshal then sorts Go map keys
alphabetically, so YAML output always had sorted keys despite other formats
(JSON, pretty-print) correctly preserving order.

Switch to building yaml.Node trees that preserve the Mlrmap's linked-list
iteration order. Use yaml.Node.Encode for scalar values to correctly handle
edge cases (NaN/Inf, non-UTF-8 strings).

Fixes #2028.

* Update pkg/mlrval/mlrval_yaml_test.go with copilot code-review feedback

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: lawrence3699 <lawrence3699@users.noreply.github.com>
Co-authored-by: John Kerl <kerl.john.r@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-12 09:43:09 -04:00

93 lines
2.3 KiB
Go

package output
import (
"bufio"
"fmt"
"os"
"gopkg.in/yaml.v3"
"github.com/johnkerl/miller/v6/pkg/cli"
"github.com/johnkerl/miller/v6/pkg/mlrval"
"github.com/johnkerl/miller/v6/pkg/types"
)
type RecordWriterYAML struct {
writerOptions *cli.TWriterOptions
bufferedRecords []*yaml.Node // used when WrapYAMLOutputInOuterList is true
wroteAnyRecords bool // for multi-doc: emit "---\n" before 2nd and later docs
}
func NewRecordWriterYAML(writerOptions *cli.TWriterOptions) (*RecordWriterYAML, error) {
return &RecordWriterYAML{
writerOptions: writerOptions,
bufferedRecords: nil,
wroteAnyRecords: false,
}, nil
}
func (writer *RecordWriterYAML) Write(
outrec *mlrval.Mlrmap,
context *types.Context,
bufferedOutputStream *bufio.Writer,
outputIsStdout bool,
) error {
if writer.writerOptions.WrapYAMLOutputInOuterList {
writer.writeWithListWrap(outrec, bufferedOutputStream)
} else {
writer.writeWithoutListWrap(outrec, bufferedOutputStream)
}
return nil
}
func (writer *RecordWriterYAML) writeWithListWrap(
outrec *mlrval.Mlrmap,
bufferedOutputStream *bufio.Writer,
) {
if outrec != nil {
if writer.bufferedRecords == nil {
writer.bufferedRecords = []*yaml.Node{}
}
native, err := mlrval.MlrmapToYAMLNative(outrec)
if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1)
}
writer.bufferedRecords = append(writer.bufferedRecords, native)
} else {
// End of stream: emit single YAML document as array
seqNode := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
seqNode.Content = writer.bufferedRecords
out, err := yaml.Marshal(seqNode)
if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1)
}
bufferedOutputStream.Write(out)
writer.bufferedRecords = nil
}
}
func (writer *RecordWriterYAML) writeWithoutListWrap(
outrec *mlrval.Mlrmap,
bufferedOutputStream *bufio.Writer,
) {
if outrec == nil {
return
}
if writer.wroteAnyRecords {
bufferedOutputStream.WriteString("---\n")
}
native, err := mlrval.MlrmapToYAMLNative(outrec)
if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1)
}
out, err := yaml.Marshal(native)
if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1)
}
bufferedOutputStream.Write(out)
writer.wroteAnyRecords = true
}