Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions acceptance/bundle/validate/empty_tasks/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
bundle:
name: empty_tasks

resources:
jobs:
empty_tasks:
name: job with empty tasks
tasks:
valid:
name: valid job
tasks:
- task_key: t
sql_task:
warehouse_id: w123
query:
query_id: abc
3 changes: 3 additions & 0 deletions acceptance/bundle/validate/empty_tasks/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions acceptance/bundle/validate/empty_tasks/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

>>> [CLI] bundle validate -o json
{
"empty_tasks": {
"deployment": {
"kind": "BUNDLE",
"metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/empty_tasks/default/state/metadata.json"
},
"edit_mode": "UI_LOCKED",
"format": "MULTI_TASK",
"max_concurrent_runs": 1,
"name": "job with empty tasks",
"queue": {
"enabled": true
},
"tasks": null
},
"valid": {
"deployment": {
"kind": "BUNDLE",
"metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/empty_tasks/default/state/metadata.json"
},
"edit_mode": "UI_LOCKED",
"format": "MULTI_TASK",
"max_concurrent_runs": 1,
"name": "valid job",
"queue": {
"enabled": true
},
"tasks": [
{
"sql_task": {
"query": {
"query_id": "abc"
},
"warehouse_id": "w123"
},
"task_key": "t"
}
]
}
}
1 change: 1 addition & 0 deletions acceptance/bundle/validate/empty_tasks/script
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
trace $CLI bundle validate -o json | jq .resources.jobs
19 changes: 17 additions & 2 deletions libs/dyn/pattern.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dyn

import (
"errors"
"fmt"
"slices"
"strings"
Expand Down Expand Up @@ -116,6 +117,20 @@ func (e expectedSequenceError) Error() string {
return fmt.Sprintf("expected a sequence at %q, found %s", e.p, e.v.Kind())
}

// isNoMatchError reports whether err means the pattern suffix didn't match the
// visited value. Wildcard components skip such elements (e.g. a job with an
// empty "tasks:" block) instead of failing the visit for all valid siblings.
func isNoMatchError(err error) bool {
if IsNoSuchKeyError(err) || IsIndexOutOfBoundsError(err) || IsCannotTraverseNilError(err) {
return true
}
if _, ok := errors.AsType[expectedMapError](err); ok {
return true
}
_, ok := errors.AsType[expectedSequenceError](err)
return ok
}

// This function implements the patternComponent interface.
func (c anyKeyComponent) visit(v Value, prefix Path, suffix Pattern, opts visitOptions) (Value, error) {
m, ok := v.AsMap()
Expand All @@ -132,7 +147,7 @@ func (c anyKeyComponent) visit(v Value, prefix Path, suffix Pattern, opts visitO
nv, err := visit(pv, append(prefix, Key(pk.MustString())), suffix, opts)
if err != nil {
// Leave the value intact if the suffix pattern didn't match any value.
if IsNoSuchKeyError(err) || IsIndexOutOfBoundsError(err) {
if isNoMatchError(err) {
continue
}
return InvalidValue, err
Expand Down Expand Up @@ -164,7 +179,7 @@ func (c anyIndexComponent) visit(v Value, prefix Path, suffix Pattern, opts visi
nv, err := visit(value, append(prefix, Index(i)), suffix, opts)
if err != nil {
// Leave the value intact if the suffix pattern didn't match any value.
if IsNoSuchKeyError(err) || IsIndexOutOfBoundsError(err) {
if isNoMatchError(err) {
continue
}
return InvalidValue, err
Expand Down
88 changes: 88 additions & 0 deletions libs/dyn/visit_map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,94 @@ func TestMapByPatternOnSequence(t *testing.T) {
}, vout.AsAny())
}

func TestMapByPatternWildcardSkipsNonMatchingSiblings(t *testing.T) {
tests := []struct {
name string
vin dyn.Value
pattern dyn.Pattern
want any
visited []string
}{
{
name: "nil sibling under any key",
vin: dyn.V(map[string]dyn.Value{
"a": dyn.NilValue,
"b": dyn.V(map[string]dyn.Value{"x": dyn.V(1)}),
}),
pattern: dyn.NewPattern(dyn.AnyKey(), dyn.Key("x")),
want: map[string]any{
"a": nil,
"b": map[string]any{"x": 42},
},
visited: []string{"b.x"},
},
{
name: "nil sequence sibling under any key",
vin: dyn.V(map[string]dyn.Value{
"a": dyn.V(map[string]dyn.Value{"tasks": dyn.NilValue}),
"b": dyn.V(map[string]dyn.Value{"tasks": dyn.V([]dyn.Value{dyn.V(1)})}),
}),
pattern: dyn.NewPattern(dyn.AnyKey(), dyn.Key("tasks"), dyn.AnyIndex()),
want: map[string]any{
"a": map[string]any{"tasks": nil},
"b": map[string]any{"tasks": []any{42}},
},
visited: []string{"b.tasks[0]"},
},
{
name: "wrong kind sibling under any key",
vin: dyn.V(map[string]dyn.Value{
"a": dyn.V(map[string]dyn.Value{"tasks": dyn.V("oops")}),
"b": dyn.V(map[string]dyn.Value{"tasks": dyn.V([]dyn.Value{dyn.V(1)})}),
}),
pattern: dyn.NewPattern(dyn.AnyKey(), dyn.Key("tasks"), dyn.AnyIndex()),
want: map[string]any{
"a": map[string]any{"tasks": "oops"},
"b": map[string]any{"tasks": []any{42}},
},
visited: []string{"b.tasks[0]"},
},
{
name: "nil element under any index",
vin: dyn.V([]dyn.Value{
dyn.NilValue,
dyn.V(map[string]dyn.Value{"x": dyn.V(1)}),
}),
pattern: dyn.NewPattern(dyn.AnyIndex(), dyn.Key("x")),
want: []any{
nil,
map[string]any{"x": 42},
},
visited: []string{"[1].x"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var visited []string
vout, err := dyn.MapByPattern(tc.vin, tc.pattern, func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
visited = append(visited, p.String())
return dyn.V(42), nil
})
assert.NoError(t, err)
assert.Equal(t, tc.want, vout.AsAny())
assert.Equal(t, tc.visited, visited)
})
}
}

func TestMapByPatternWildcardPropagatesMapFuncError(t *testing.T) {
vin := dyn.V(map[string]dyn.Value{
"a": dyn.V(map[string]dyn.Value{"x": dyn.V(1)}),
})

ref := errors.New("error")
_, err := dyn.MapByPattern(vin, dyn.NewPattern(dyn.AnyKey(), dyn.Key("x")), func(_ dyn.Path, v dyn.Value) (dyn.Value, error) {
return dyn.InvalidValue, ref
})
assert.ErrorIs(t, err, ref)
}

func TestMapByPatternOnSequenceWithoutMatch(t *testing.T) {
vin := dyn.V([]dyn.Value{
dyn.V([]dyn.Value{
Expand Down
Loading