Skip to content

Commit

Permalink
Dynamic fields in rename
Browse files Browse the repository at this point in the history
Add the ability to rename fields dynamic for the rename op.
  • Loading branch information
mattnibs committed Oct 25, 2023
1 parent ef9695f commit ea83146
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 49 deletions.
16 changes: 12 additions & 4 deletions compiler/kernel/op.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,18 @@ func (b *Builder) compileLeaf(o dag.Op, parent zbuf.Puller) (zbuf.Puller, error)
putter := expr.NewPutter(b.octx.Zctx, clauses)
return op.NewApplier(b.octx, parent, putter), nil
case *dag.Rename:
var srcs, dsts field.List
for _, a := range v.Args {
dsts = append(dsts, a.LHS.(*dag.This).Path)
srcs = append(srcs, a.RHS.(*dag.This).Path)
var srcs, dsts []*expr.Lval
for k := range v.Args {
src, err := b.compileLval(v.Args[k].RHS)
if err != nil {
return nil, err
}
dst, err := b.compileLval(v.Args[k].LHS)
if err != nil {
return nil, err
}
srcs = append(srcs, src)
dsts = append(dsts, dst)
}
renamer := expr.NewRenamer(b.octx.Zctx, srcs, dsts)
return op.NewApplier(b.octx, parent, renamer), nil
Expand Down
35 changes: 24 additions & 11 deletions compiler/semantic/op.go
Original file line number Diff line number Diff line change
Expand Up @@ -640,22 +640,35 @@ func (a *analyzer) semOp(o ast.Op, seq dag.Seq) (dag.Seq, error) {
case *ast.Rename:
var assignments []dag.Assignment
for _, fa := range o.Args {
dst, err := a.semField(fa.LHS)
dst, err := a.semExpr(fa.LHS)
if err != nil {
return nil, errors.New("rename: requires explicit field references")
return nil, fmt.Errorf("rename: %w", err)
}
src, err := a.semField(fa.RHS)
if !isLval(dst) {
return nil, fmt.Errorf("rename: illegal left-hand side of assignment")
}
src, err := a.semExpr(fa.RHS)
if err != nil {
return nil, errors.New("rename: requires explicit field references")
return nil, fmt.Errorf("rename: %w", err)
}
if len(dst.Path) != len(src.Path) {
return nil, fmt.Errorf("rename: cannot rename %s to %s", src, dst)
if !isLval(src) {
return nil, fmt.Errorf("rename: illegal right-hand side of assignment")
}
// Check that the prefixes match and, if not, report first place
// that they don't.
for i := 0; i <= len(src.Path)-2; i++ {
if src.Path[i] != dst.Path[i] {
return nil, fmt.Errorf("rename: cannot rename %s to %s (differ in %s vs %s)", src, dst, src.Path[i], dst.Path[i])
// If both paths are static validate rename paths, otherwise this
// will be done at runtime.
srcThis, srcOk := src.(*dag.This)
dstThis, dstOk := dst.(*dag.This)
if dstOk && srcOk {
s, d := field.Path(srcThis.Path), field.Path(dstThis.Path)
if len(d) != len(s) {
return nil, fmt.Errorf("rename: cannot rename %s to %s", s, d)
}
// Check that the prefixes match and, if not, report first place
// that they don't.
for i := 0; i <= len(s)-2; i++ {
if s[i] != d[i] {
return nil, fmt.Errorf("rename: cannot rename %s to %s (differ in %s vs %s)", s, d, s[i], d[i])
}
}
}
assignments = append(assignments, dag.Assignment{Kind: "Assignment", LHS: dst, RHS: src})
Expand Down
22 changes: 22 additions & 0 deletions compiler/ztests/dynamic-field-rename.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
script: |
echo '{target:"foo",src:"bar"} {target:"fool",src:"baz"}' | zq -z 'rename this[target] := src' -
echo '// ==='
echo '{target:"a",a:"bar"} {target:"b",b:"baz"}' | zq -z 'rename dst := this[target]' -
# runtime error cases
echo '// ==='
echo '{foo:"a",bar:"b"}' | zq -z 'rename this[foo]["c"] := this[bar]["d"]' -
echo '// ==='
echo '{foo:"a"}' | zq -z 'rename this[foo]["c"] := this[foo]["a"]["b"]' -
outputs:
- name: stdout
data: |
{target:"foo",foo:"bar"}
{target:"fool",fool:"baz"}
// ===
{target:"a",dst:"bar"}
{target:"b",dst:"baz"}
// ===
error({message:"rename: cannot rename b.d to a.c (differ in b vs a)",on:{foo:"a",bar:"b"}})
// ===
error({message:"rename: cannot rename a.a.b to a.c",on:{foo:"a"}})
25 changes: 25 additions & 0 deletions pkg/field/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ func (p Path) String() string {
return strings.Join(p, ".")
}

// AppendTo appends the string representation of the path to byte slice b.
func (p Path) AppendTo(b []byte) []byte {
if len(p) == 0 {
return append(b, "this"...)
}
for i, s := range p {
if i > 0 {
b = append(b, '.')
}
b = append(b, s...)
}
return b
}

func (p Path) Leaf() string {
return p[len(p)-1]
}
Expand Down Expand Up @@ -65,6 +79,17 @@ func (l List) String() string {
return strings.Join(paths, ",")
}

// AppendTo appends the string representation of the list to byte slice b.
func (l List) AppendTo(b []byte) []byte {
for i, p := range l {
if i > 0 {
b = append(b, ',')
}
b = p.AppendTo(b)
}
return b
}

func (l List) Has(in Path) bool {
return slices.ContainsFunc(l, in.Equal)
}
Expand Down
106 changes: 72 additions & 34 deletions runtime/expr/renamer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,80 @@ type Renamer struct {
zctx *zed.Context
// For the dst field name, we just store the leaf name since the
// src path and the dst path are the same and only differ in the leaf name.
srcs field.List
dsts field.List
typeMap map[int]*zed.TypeRecord
srcs []*Lval
dsts []*Lval
typeMap map[int]map[string]*zed.TypeRecord
// fieldsStr is used to reduce allocations when computing the fields id.
fieldsStr []byte
}

func NewRenamer(zctx *zed.Context, srcs, dsts field.List) *Renamer {
return &Renamer{zctx, srcs, dsts, make(map[int]*zed.TypeRecord)}
func NewRenamer(zctx *zed.Context, srcs, dsts []*Lval) *Renamer {
return &Renamer{zctx, srcs, dsts, make(map[int]map[string]*zed.TypeRecord), nil}
}

func (r *Renamer) Eval(ectx Context, this *zed.Value) *zed.Value {
if !zed.IsRecordType(this.Type) {
return this
}
srcs, dsts, err := r.evalFields(ectx, this)
if err != nil {
return ectx.CopyValue(*r.zctx.WrapError(fmt.Sprintf("rename: %s", err), this))
}
id := this.Type.ID()
m, ok := r.typeMap[id]
if !ok {
m = make(map[string]*zed.TypeRecord)
r.typeMap[id] = m
}
r.fieldsStr = srcs.AppendTo(r.fieldsStr[:0])
r.fieldsStr = dsts.AppendTo(r.fieldsStr)
typ, ok := m[string(r.fieldsStr)]
if !ok {
var err error
typ, err = r.computeType(zed.TypeRecordOf(this.Type), srcs, dsts)
if err != nil {
return ectx.CopyValue(*r.zctx.WrapError(fmt.Sprintf("rename: %s", err), this))
}
m[string(r.fieldsStr)] = typ
}
out := this.Copy()
return ectx.NewValue(typ, out.Bytes())
}

func (r *Renamer) evalFields(ectx Context, this *zed.Value) (field.List, field.List, error) {
var srcs, dsts field.List
for i := range r.srcs {
src, err := r.srcs[i].Eval(ectx, this)
if err != nil {
return nil, nil, err
}
dst, err := r.dsts[i].Eval(ectx, this)
if err != nil {
return nil, nil, err
}
if len(src) != len(dst) {
return nil, nil, fmt.Errorf("cannot rename %s to %s", src, dst)
}
for i := 0; i <= len(src)-2; i++ {
if src[i] != dst[i] {
return nil, nil, fmt.Errorf("cannot rename %s to %s (differ in %s vs %s)", src, dst, src[i], dst[i])
}
}
srcs = append(srcs, src)
dsts = append(dsts, dst)
}
return srcs, dsts, nil
}

func (r *Renamer) computeType(typ *zed.TypeRecord, srcs, dsts field.List) (*zed.TypeRecord, error) {
for k, dst := range dsts {
var err error
typ, err = r.dstType(typ, srcs[k], dst)
if err != nil {
return nil, err
}
}
return typ, nil
}

func (r *Renamer) dstType(typ *zed.TypeRecord, src, dst field.Path) (*zed.TypeRecord, error) {
Expand Down Expand Up @@ -57,32 +124,3 @@ func (r *Renamer) dstType(typ *zed.TypeRecord, src, dst field.Path) (*zed.TypeRe
}
return typ, nil
}

func (r *Renamer) computeType(typ *zed.TypeRecord) (*zed.TypeRecord, error) {
for k, dst := range r.dsts {
var err error
typ, err = r.dstType(typ, r.srcs[k], dst)
if err != nil {
return nil, err
}
}
return typ, nil
}

func (r *Renamer) Eval(ectx Context, this *zed.Value) *zed.Value {
if !zed.IsRecordType(this.Type) {
return this
}
id := this.Type.ID()
typ, ok := r.typeMap[id]
if !ok {
var err error
typ, err = r.computeType(zed.TypeRecordOf(this.Type))
if err != nil {
return r.zctx.WrapError(fmt.Sprintf("rename: %s", err), this)
}
r.typeMap[id] = typ
}
out := this.Copy()
return ectx.NewValue(typ, out.Bytes())
}

0 comments on commit ea83146

Please sign in to comment.