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 f0c2686
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 52 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
27 changes: 16 additions & 11 deletions compiler/semantic/op.go
Original file line number Diff line number Diff line change
Expand Up @@ -640,22 +640,27 @@ 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 {
if err := expr.CheckRenameField(srcThis.Path, dstThis.Path); err != nil {
return nil, fmt.Errorf("rename: %w", err)
}
}
assignments = append(assignments, dag.Assignment{Kind: "Assignment", LHS: dst, RHS: src})
Expand Down
2 changes: 1 addition & 1 deletion docs/language/operators/rename.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ echo '{a:1,r:{b:2,c:3}}' | zq -z 'rename w:=r.b' -
```
=>
```mdtest-output
rename: cannot rename r.b to w
rename: left-hand side and right-hand side must have the same depth (r.b vs w)
```
_Record literals can be used instead of rename for mutation_
```mdtest-command
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/schools.md
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ zq -Z 'rename toplevel:=outer.inner' nested.zson
```
produces this compile-time error message and the query is not run:
```mdtest-output
rename: cannot rename outer.inner to toplevel
rename: left-hand side and right-hand side must have the same depth (outer.inner vs toplevel)
```
This goal could instead be achieved by combining [`put`](#44-put) and [`drop`](#42-drop),
e.g.,
Expand Down
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
113 changes: 79 additions & 34 deletions runtime/expr/renamer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,87 @@ 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 CheckRenameField(src, dst field.Path) error {
if len(src) != len(dst) {
return fmt.Errorf("left-hand side and right-hand side must have the same depth (%s vs %s)", src, dst)
}
for i := 0; i <= len(src)-2; i++ {
if src[i] != dst[i] {
return fmt.Errorf("cannot rename %s to %s (differ in %s vs %s)", src, dst, src[i], dst[i])
}
}
return nil
}

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 err := CheckRenameField(src, dst); err != nil {
return nil, nil, err
}
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 +131,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())
}
2 changes: 1 addition & 1 deletion runtime/expr/ztests/rename-error-move.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ zed: rename dst:=id.resp_h
input: |
{id:{orig_h:10.164.94.120,orig_p:39681(port=uint16),resp_h:10.47.3.155,resp_p:3389(port)}}
errorRE: "rename: cannot rename id.resp_h to dst"
errorRE: "rename: left-hand side and right-hand side must have the same depth \\(id.resp_h vs dst\\)"
22 changes: 22 additions & 0 deletions runtime/op/ztests/rename-dynamic-field.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: left-hand side and right-hand side must have the same depth (a.a.b vs a.c)",on:{foo:"a"}})

0 comments on commit f0c2686

Please sign in to comment.