// FLATTEN/UNFLATTEN // // These are used by the flatten/unflatten verbs and DSL functions. They are // crucial to the operation of Miller 6 wherein records have full Mlrval // values, i.e. they can be arrays/maps as well as int/float/string. // // When we read JSON and write (say) CSV, we have two choices for handling the // fact that JSON handles multi-level data and CSV does not: // // (1) JSON-stringify values, using the json-stringify verb or json_stringify // DSL function. For example, the array of ints [1,2,3] becomes the string // "[1,2,3]" which works fine as a CSV field. // // (2) Flatten them by key-spreading. For example, the single field with key // "x" with value {"a":1,"b":2} flattens to the *pair* of fields x:a=1 and // x:b=2. // // The former are used implicitly (i.e. unless the user explicitly requests // otherwise) when we convert to/from JSON. package mlrval import ( "fmt" "os" "strings" "sync" "github.com/johnkerl/miller/v6/pkg/lib" ) // warnedUnflattenFieldNames tracks field names for which we've already emitted // an unflatten warning, to avoid flooding stderr on multi-record input. var warnedUnflattenFieldNames sync.Map // Flattens all field values in the record. This is a special case of // FlattenFields but it's worth its own special case (to avoid iffing on the // nullity of the fieldNameSet) since the flatten/unflatten check is done by // default on ALL Miller records whenever we convert to/from JSON. So, the // default path should be fast. // // Examples: // * The single field x = {"a": 7, "b": 8, "c": 9} becomes the three fields // x.a = 7, x.b = 8, x.c = 9. // * The single field x = [7,8,9] becomes the three fields // x.1 = 7, x.2 = 8, x.3 = 9. func (mlrmap *Mlrmap) Flatten(separator string) { if !mlrmap.isFlattenable() { // fast path: don't modify the record at all return } other := NewMlrmapAsRecord() for pe := mlrmap.Head; pe != nil; pe = pe.Next { if pe.Value.IsArrayOrMap() { pieces := pe.Value.FlattenToMap(pe.Key, separator) for pf := pieces.GetMap().Head; pf != nil; pf = pf.Next { other.PutReference(pf.Key, pf.Value) } } else { other.PutReference(pe.Key, pe.Value) } } *mlrmap = *other } // For mlr flatten -f. func (mlrmap *Mlrmap) FlattenFields( fieldNameSet map[string]bool, separator string, ) { if !mlrmap.isFlattenable() { // fast path return } other := NewMlrmapAsRecord() for pe := mlrmap.Head; pe != nil; pe = pe.Next { if pe.Value.IsArrayOrMap() && fieldNameSet[pe.Key] { pieces := pe.Value.FlattenToMap(pe.Key, separator) for pf := pieces.GetMap().Head; pf != nil; pf = pf.Next { other.PutReference(pf.Key, pf.Value) } } else { other.PutReference(pe.Key, pe.Value) } } *mlrmap = *other } // Optimization for Flatten, to avoid needless data motion in the case // where all field values are non-collections. func (mlrmap *Mlrmap) isFlattenable() bool { for pe := mlrmap.Head; pe != nil; pe = pe.Next { if pe.Value.IsArrayOrMap() { return true } } return false } // For mlr unflatten without -f. This undoes Unflatten. This is for conversion // from non-JSON to JSON. If there are fields x.a, x.b, x.c, etc. they're put // into a single field x with map-valued value keyed by "a", "b", "c". // // There is a heurtistic here though. Miller is (wildly) multi-format and needs // to accommodate all manner of data. In the JSON world, "." is the default // delimiter for nested data, and we're here to handle that. But in the R world, // "." is just like "_" in other languages: witness "data.frame" rather than // "data_frame". If the "." was intended as punctuation, in a say a field named // "a.b" with value 3, then unflatten-to-JSON will make `{"a": {"b": 3}}`. This // is just our default behavior; users can use --no-auto-unflatten. Weirder // are field names like ".", ".x", "x.", "x..y", etc. The heuristic here // is that when we split on "." and any of the pieces around/between the dots // are empty string, we don't try to unflatten that field. // // Special case: if the resulting string keys are string representations of 1, // 2, 3, etc -- without gaps -- then the map is converted to an array. // // Examples: // // - The three fields x.a = 7, x.b = 8, x.c = 9 become // the single field x = {"a": 7, "b": 8, "c": 9}. // // - The three fields x.1 = 7, x.2 = 8, x.3 = 9 become // the single field x = [7,8,9]. // // - The two fields x.1 = 7, x.3 = 9 become // the single field x = {"1": 7, "3": 9} func (mlrmap *Mlrmap) Unflatten( separator string, ) { *mlrmap = *(mlrmap.CopyUnflattened(separator)) } func (mlrmap *Mlrmap) CopyUnflattened( separator string, ) *Mlrmap { other := NewMlrmapAsRecord() affectedBaseIndices := make(map[string]bool) // We'll come through this loop once for x.a, another for x.b, etc. for pe := mlrmap.Head; pe != nil; pe = pe.Next { // If there are no dots in the field name, treat it as a terminal. if !strings.Contains(pe.Key, separator) { other.PutReference(pe.Key, unflattenTerminal(pe.Value)) continue } arrayOfIndices := SplitAXHelper(pe.Key, separator) arrayval := arrayOfIndices.intf.([]*Mlrval) lib.InternalCodingErrorIf(len(arrayval) < 1) // Check for "" in any of the split pieces; treat the field as terminal if so. legitDots := true for i := range arrayval { piece := arrayval[i].String() if piece == "" { legitDots = false break } } if !legitDots { if _, alreadyWarned := warnedUnflattenFieldNames.LoadOrStore(pe.Key, true); !alreadyWarned { fmt.Fprintf(os.Stderr, "mlr: field name %q contains separator %q but cannot be auto-unflattened; treating as a literal string. Use --no-auto-unflatten to suppress this warning.\n", pe.Key, separator) } other.PutReference(pe.Key, unflattenTerminal(pe.Value)) continue } // If the input field name was "x.a" then remember the "x". baseIndex := arrayval[0].String() affectedBaseIndices[baseIndex] = true // Use PutIndexed to assign $x["a"] = 7, or $x["b"] = 8, etc. // Unflattening is best-effort: this API has no error return. _ = other.PutIndexed( CopyMlrvalArray(arrayval), unflattenTerminal(pe.Value).Copy(), ) } // Go through all the field names which were turned into maps -- e.g. "x" // in the example above -- and see if the keys were like "1", "2", etc and // if so then convert to array. This undoes how Flatten flattens arrays. for baseIndex := range affectedBaseIndices { oldValue := other.Get(baseIndex) lib.InternalCodingErrorIf(oldValue == nil) newValue := oldValue.Arrayify() other.PutReference(baseIndex, newValue) } return other } // For mlr unflatten -f. See comments on Unflatten. Largely copypasta of // Unflatten, but split out separately since Flatten needn't check a // fieldNameSet. func (mlrmap *Mlrmap) UnflattenFields( fieldNameSet map[string]bool, separator string, ) { *mlrmap = *(mlrmap.CopyUnflattenFields(fieldNameSet, separator)) } func (mlrmap *Mlrmap) CopyUnflattenFields( fieldNameSet map[string]bool, separator string, ) *Mlrmap { other := NewMlrmapAsRecord() affectedBaseIndices := make(map[string]bool) // We'll come through this loop once for x.a, another for x.b, etc. for pe := mlrmap.Head; pe != nil; pe = pe.Next { // Is the field name something dot something? if strings.Contains(pe.Key, separator) { arrayOfIndices := SplitAXHelper(pe.Key, separator) arrayval := arrayOfIndices.intf.([]*Mlrval) lib.InternalCodingErrorIf(len(arrayval) < 1) // If the input field name was "x.a" then remember the "x". baseIndex := arrayval[0].String() if fieldNameSet[baseIndex] { // Use PutIndexed to assign $x["a"] = 7, or $x["b"] = 8, etc. // Unflattening is best-effort: this API has no error return. _ = other.PutIndexed( CopyMlrvalArray(arrayval), unflattenTerminal(pe.Value).Copy(), ) affectedBaseIndices[baseIndex] = true } else { other.PutReference(pe.Key, unflattenTerminal(pe.Value)) } } else { other.PutReference(pe.Key, unflattenTerminal(pe.Value)) } } // Go through all the field names which were turned into maps -- e.g. "x" // in the example above -- and see if the keys were like "1", "2", etc and // if so then convert to array. This undoes how Flatten flattens arrays. for baseIndex := range affectedBaseIndices { oldValue := other.Get(baseIndex) lib.InternalCodingErrorIf(oldValue == nil) newValue := oldValue.Arrayify() other.PutReference(baseIndex, newValue) } return other } // Flatten of empty map and empty array produce "{}" and "[]" as special cases. // (Without this, key-spreading would cause such fields to disappear entirely: // the field "x" -> {"a": 1, "b": 2} would spread to the pair of fields "x:a" // -> 1 and "x:b" -> 2, and the field "x" -> {"a": 1} would spread to the // single field "x:a" -> 1, so the field "x" -> {} would spread to zero // fields.) Here we reverse that special case of the flatten operation. func unflattenTerminal(input *Mlrval) *Mlrval { if !input.IsString() { return input } if input.printrep == "{}" { return FromMap(NewMlrmap()) } if input.printrep == "[]" { return FromArray([]*Mlrval{}) } return input } // SplitAXHelper is split out for the benefit of BIF_splitax and // BIF_unflatten. func SplitAXHelper(input string, separator string) *Mlrval { fields := lib.SplitString(input, separator) output := FromArray(make([]*Mlrval, len(fields))) for i, field := range fields { output.intf.([]*Mlrval)[i] = FromString(field) } return output }