Skip to content

Commit dcee1b9

Browse files
committed
put: Assignment to array or set
Update the put operator to allow assignments to elements in an array or set. Closes #4798
1 parent f530788 commit dcee1b9

15 files changed

Lines changed: 580 additions & 319 deletions

File tree

compiler/semantic/op.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -619,16 +619,6 @@ func (a *analyzer) semOp(o ast.Op, seq dag.Seq) (dag.Seq, error) {
619619
if err != nil {
620620
return nil, err
621621
}
622-
// We can do collision checking on static paths, so check what we can.
623-
var fields field.List
624-
for _, a := range assignments {
625-
if this, ok := a.LHS.(*dag.This); ok {
626-
fields = append(fields, this.Path)
627-
}
628-
}
629-
if err := expr.CheckPutFields(fields); err != nil {
630-
return nil, fmt.Errorf("put: %w", err)
631-
}
632622
return append(seq, &dag.Put{
633623
Kind: "Put",
634624
Args: assignments,

docs/language/operators/put.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,5 @@ echo '{a:1} 1' | zq -z 'b:=2' -
8282
=>
8383
```mdtest-output
8484
{a:1,b:2}
85-
error({message:"put: not a record",on:1})
85+
error({message:"put: not a puttable element",on:1})
8686
```

runtime/expr/cutter.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,11 @@ func (c *Cutter) Eval(ectx Context, in *zed.Value) *zed.Value {
9494
func (c *Cutter) lookupBuilder(ectx Context, in *zed.Value) (*recordBuilderCachedTypes, field.List, error) {
9595
paths := c.fieldRefs[:0]
9696
for _, p := range c.lvals {
97-
path, err := p.Eval(ectx, in)
97+
path, err := p.EvalAsRecordPath(ectx, in)
9898
if err != nil {
9999
return nil, nil, err
100100
}
101-
if path.IsEmpty() {
101+
if len(path) == 0 {
102102
return nil, nil, errors.New("'this' not allowed (use record literal)")
103103
}
104104
paths = append(paths, path)

runtime/expr/dynfield/path.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package dynfield
2+
3+
import (
4+
"github.com/brimdata/zed"
5+
"github.com/brimdata/zed/zson"
6+
)
7+
8+
type Path []zed.Value
9+
10+
func (p Path) Append(b []byte) []byte {
11+
for i, v := range p {
12+
if i > 0 {
13+
b = append(b, 0)
14+
}
15+
b = append(b, v.Bytes()...)
16+
}
17+
return b
18+
}
19+
20+
func (p Path) String() string {
21+
var b []byte
22+
for i, v := range p {
23+
if i > 0 {
24+
b = append(b, '.')
25+
}
26+
b = append(b, zson.FormatValue(&v)...)
27+
}
28+
return string(b)
29+
}
30+
31+
type List []Path
32+
33+
func (l List) Append(b []byte) []byte {
34+
for i, path := range l {
35+
if i > 0 {
36+
b = append(b, ',')
37+
}
38+
b = path.Append(b)
39+
}
40+
return b
41+
}

runtime/expr/lval.go

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import (
55

66
"github.com/brimdata/zed"
77
"github.com/brimdata/zed/pkg/field"
8+
"github.com/brimdata/zed/runtime/expr/dynfield"
89
"github.com/brimdata/zed/zson"
910
)
1011

1112
type Lval struct {
12-
Elems []LvalElem
13-
cache field.Path
13+
Elems []LvalElem
14+
cache []zed.Value
15+
fieldCache field.Path
1416
}
1517

1618
func NewLval(evals []LvalElem) *Lval {
@@ -19,18 +21,36 @@ func NewLval(evals []LvalElem) *Lval {
1921

2022
// Eval returns the path of the lval. If there's an error the returned *zed.Value
2123
// will not be nill.
22-
func (l *Lval) Eval(ectx Context, this *zed.Value) (field.Path, error) {
24+
func (l *Lval) Eval(ectx Context, this *zed.Value) (dynfield.Path, error) {
2325
l.cache = l.cache[:0]
2426
for _, e := range l.Elems {
25-
name, err := e.Eval(ectx, this)
27+
val, err := e.Eval(ectx, this)
2628
if err != nil {
2729
return nil, err
2830
}
29-
l.cache = append(l.cache, name)
31+
l.cache = append(l.cache, *val)
3032
}
3133
return l.cache, nil
3234
}
3335

36+
func (l *Lval) EvalAsRecordPath(ectx Context, this *zed.Value) (field.Path, error) {
37+
l.fieldCache = l.fieldCache[:0]
38+
for _, e := range l.Elems {
39+
val, err := e.Eval(ectx, this)
40+
if err != nil {
41+
return nil, err
42+
}
43+
if !val.IsString() {
44+
// XXX Add context to error so we know what element is failing but
45+
// let's wait until we can test this so we have a feel for what we
46+
// want to see.
47+
return nil, errors.New("field reference is not a string")
48+
}
49+
l.fieldCache = append(l.fieldCache, val.AsString())
50+
}
51+
return l.fieldCache, nil
52+
}
53+
3454
// Path returns the receiver's path. Path returns false when the receiver
3555
// contains a dynamic element.
3656
func (l *Lval) Path() (field.Path, bool) {
@@ -46,15 +66,15 @@ func (l *Lval) Path() (field.Path, bool) {
4666
}
4767

4868
type LvalElem interface {
49-
Eval(ectx Context, this *zed.Value) (string, error)
69+
Eval(ectx Context, this *zed.Value) (*zed.Value, error)
5070
}
5171

5272
type StaticLvalElem struct {
5373
Name string
5474
}
5575

56-
func (l *StaticLvalElem) Eval(_ Context, _ *zed.Value) (string, error) {
57-
return l.Name, nil
76+
func (l *StaticLvalElem) Eval(_ Context, _ *zed.Value) (*zed.Value, error) {
77+
return zed.NewString(l.Name), nil
5878
}
5979

6080
type ExprLvalElem struct {
@@ -69,17 +89,12 @@ func NewExprLvalElem(zctx *zed.Context, e Evaluator) *ExprLvalElem {
6989
}
7090
}
7191

72-
func (l *ExprLvalElem) Eval(ectx Context, this *zed.Value) (string, error) {
92+
func (l *ExprLvalElem) Eval(ectx Context, this *zed.Value) (*zed.Value, error) {
7393
val := l.eval.Eval(ectx, this)
7494
if val.IsError() {
75-
return "", lvalErr(ectx, val)
76-
}
77-
if !val.IsString() {
78-
if val = l.caster.Eval(ectx, val); val.IsError() {
79-
return "", errors.New("field reference is not a string")
80-
}
95+
return nil, lvalErr(ectx, val)
8196
}
82-
return val.AsString(), nil
97+
return val, nil
8398
}
8499

85100
func lvalErr(ectx Context, errVal *zed.Value) error {
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package pathbuilder
2+
3+
import (
4+
"errors"
5+
6+
"github.com/brimdata/zed"
7+
"github.com/brimdata/zed/runtime/expr/dynfield"
8+
)
9+
10+
type builder struct {
11+
inputCount int
12+
base Step
13+
}
14+
15+
func New(base zed.Type, paths []dynfield.Path, leafs []zed.Value) (Step, error) {
16+
if len(paths) != len(leafs) {
17+
return nil, errors.New("paths and leafs must be the same length")
18+
}
19+
b := &builder{base: newLeafStep(base, -1)}
20+
for i, p := range paths {
21+
if err := b.Put(p, leafs[i].Type); err != nil {
22+
return nil, err
23+
}
24+
}
25+
return b.base, nil
26+
}
27+
28+
func (m *builder) Put(p dynfield.Path, leaf zed.Type) error {
29+
defer func() { m.inputCount++ }()
30+
return m.put(&m.base, p, leaf)
31+
}
32+
33+
func (m *builder) put(parent *Step, p dynfield.Path, typ zed.Type) error {
34+
// Actually let's do this differently. If current is a string then we are
35+
// putting to a record. When we support maps we'll need to check for that.
36+
if p[0].IsString() {
37+
return m.putRecord(parent, p, typ)
38+
}
39+
// This could be for a map or a set but keep it simple for now.
40+
if zed.IsInteger(p[0].Type.ID()) {
41+
return m.putVector(parent, p, typ)
42+
}
43+
// if zed.TypeUnder(parent.typeof())
44+
return errors.New("unsupported types")
45+
}
46+
47+
func (m *builder) putRecord(s *Step, p dynfield.Path, typ zed.Type) error {
48+
current, p := p[0], p[1:]
49+
rstep, ok := (*s).(*recordStep)
50+
if !ok {
51+
// If this is a leafStep with a type of record than we need to
52+
// initialize a recordStep with fields, otherwise just replace this will
53+
// a recordStep.
54+
var fields []zed.Field
55+
if lstep, ok := (*s).(*leafStep); ok && zed.TypeRecordOf(lstep.typ) != nil {
56+
fields = zed.TypeRecordOf(lstep.typ).Fields
57+
}
58+
rstep = newRecordStep(fields)
59+
if *s == m.base {
60+
rstep.isBase = true
61+
}
62+
*s = rstep
63+
}
64+
i := rstep.lookup(current.AsString())
65+
field := &rstep.fields[i]
66+
if len(p) == 0 {
67+
field.step = newLeafStep(typ, m.inputCount)
68+
return nil
69+
}
70+
return m.put(&field.step, p, typ)
71+
}
72+
73+
func (m *builder) putVector(s *Step, p dynfield.Path, typ zed.Type) error {
74+
current, p := p[0], p[1:]
75+
vstep, ok := (*s).(*vectorStep)
76+
if !ok {
77+
// If this is a leafStep with a type of array than we need to
78+
// initialize a arrayStep with fields, otherwise just replace this with
79+
// an arrayStep.
80+
vstep = &vectorStep{}
81+
if lstep, ok := (*s).(*leafStep); ok && zed.InnerType(lstep.typ) != nil {
82+
vstep.inner = zed.InnerType(lstep.typ)
83+
_, vstep.isSet = zed.TypeUnder(lstep.typ).(*zed.TypeSet)
84+
}
85+
if *s == m.base {
86+
vstep.isBase = true
87+
}
88+
*s = vstep
89+
}
90+
at := vstep.lookup(int(current.AsInt()))
91+
elem := &vstep.elems[at]
92+
if len(p) == 0 {
93+
elem.step = newLeafStep(typ, m.inputCount)
94+
return nil
95+
}
96+
return m.put(&elem.step, p, typ)
97+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package pathbuilder
2+
3+
import (
4+
"testing"
5+
6+
"github.com/brimdata/zed"
7+
"github.com/brimdata/zed/runtime/expr/dynfield"
8+
"github.com/brimdata/zed/zcode"
9+
"github.com/brimdata/zed/zson"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func parsePath(zctx *zed.Context, ss ...string) dynfield.Path {
14+
var path dynfield.Path
15+
for _, s := range ss {
16+
path = append(path, *zson.MustParseValue(zctx, s))
17+
}
18+
return path
19+
}
20+
21+
type testCase struct {
22+
describe string
23+
base string
24+
paths [][]string
25+
values []string
26+
expected string
27+
}
28+
29+
func runTestCase(t *testing.T, c testCase) {
30+
zctx := zed.NewContext()
31+
var baseTyp zed.Type
32+
var baseBytes []byte
33+
if c.base != "" {
34+
base := zson.MustParseValue(zctx, c.base)
35+
baseTyp, baseBytes = base.Type, base.Bytes()
36+
}
37+
var paths []dynfield.Path
38+
for _, ss := range c.paths {
39+
paths = append(paths, parsePath(zctx, ss...))
40+
}
41+
var values []zed.Value
42+
for _, s := range c.values {
43+
values = append(values, *zson.MustParseValue(zctx, s))
44+
}
45+
step, err := New(baseTyp, paths, values)
46+
require.NoError(t, err)
47+
var b zcode.Builder
48+
typ, err := step.Build(zctx, &b, baseBytes, values)
49+
require.NoError(t, err)
50+
val := zed.NewValue(typ, b.Bytes())
51+
require.Equal(t, c.expected, zson.FormatValue(val))
52+
}
53+
54+
func TestIt(t *testing.T) {
55+
runTestCase(t, testCase{
56+
base: `{"a": 1, "b": 2}`,
57+
paths: [][]string{
58+
{`"c"`, `"a"`, `"a"`},
59+
{`"c"`, `"b"`},
60+
{`"c"`, `"c"`},
61+
},
62+
values: []string{
63+
`45`,
64+
`"string"`,
65+
"127.0.0.1",
66+
},
67+
expected: `{a:1,b:2,c:{a:{a:45},b:"string",c:127.0.0.1}}`,
68+
})
69+
runTestCase(t, testCase{
70+
base: `{"a": [1,{foo:"bar"}]}`,
71+
paths: [][]string{
72+
{`"a"`, `0`},
73+
{`"a"`, `1`, `"foo"`},
74+
},
75+
values: []string{
76+
`"hi"`,
77+
`"baz"`,
78+
},
79+
expected: `{a:["hi",{foo:"baz"}]}`,
80+
})
81+
runTestCase(t, testCase{
82+
describe: "create from empty base",
83+
paths: [][]string{
84+
{`"a"`},
85+
{`"b"`},
86+
},
87+
values: []string{
88+
`"foo"`,
89+
`"bar"`,
90+
},
91+
expected: `{a:"foo",b:"bar"}`,
92+
})
93+
runTestCase(t, testCase{
94+
describe: "assign to base level array",
95+
base: `["a", "b", "c"]`,
96+
paths: [][]string{
97+
{`0`},
98+
{`1`},
99+
{`2`},
100+
},
101+
values: []string{
102+
`"foo"`,
103+
`"bar"`,
104+
`"baz"`,
105+
},
106+
expected: `["foo","bar","baz"]`,
107+
})
108+
}

0 commit comments

Comments
 (0)