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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
bundle:
name: test-bundle

resources:
jobs:
bar:
name: job bar
job_clusters:
- job_cluster_key: main
new_cluster:
azure_attributes:
spot_bid_max_price: 1.9
foo:
name: job foo
max_concurrent_runs: ${resources.jobs.bar.job_clusters[0].new_cluster.azure_attributes.spot_bid_max_price}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

>>> musterr [CLI] bundle plan
Error: cannot plan resources.jobs.foo: cannot update max_concurrent_runs with value of "${resources.jobs.bar.job_clusters[0].new_cluster.azure_attributes.spot_bid_max_price}": cannot set (*jobs.JobSettings).max_concurrent_runs to float64 (1.9): cannot set 1.9 to int: precision loss

Error: planning failed

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud = false
EnvMatrix.DMS = ["", "true"]
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
trace musterr $CLI bundle plan &> out.plan.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
RecordRequests = false

Env.DATABRICKS_CACHE_ENABLED = 'false'

[[Repls]]
Old = 'terraform.tfstate'
New = 'resources.json'
13 changes: 13 additions & 0 deletions acceptance/bundle/variables/float_to_int_field/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
bundle:
name: test-bundle

variables:
x:
default: 5.9

resources:
jobs:
foo:
name: job foo
# Records current behaviour: a warning, the value is dropped and the default (1) applies.
max_concurrent_runs: ${var.x}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud = false
EnvMatrix.DMS = ["", "true"]
28 changes: 28 additions & 0 deletions acceptance/bundle/variables/float_to_int_field/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

>>> [CLI] bundle validate
Warning: cannot accurately represent "5.9" as integer due to precision loss
at resources.jobs.foo.max_concurrent_runs
in databricks.yml:13:28

Name: test-bundle
Target: default
Workspace:
User: [USERNAME]
Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default

Found 1 warning

>>> [CLI] bundle validate -o json
Warning: cannot accurately represent "5.9" as integer due to precision loss
at resources.jobs.foo.max_concurrent_runs
in databricks.yml:13:28

{
"variables": {
"x": {
"default": 5.9,
"value": 5.9
}
},
"max_concurrent_runs": 1
}
2 changes: 2 additions & 0 deletions acceptance/bundle/variables/float_to_int_field/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
trace $CLI bundle validate
trace $CLI bundle validate -o json | jq '{variables, max_concurrent_runs: .resources.jobs.foo.max_concurrent_runs}'
87 changes: 87 additions & 0 deletions libs/structs/structaccess/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package structaccess
import (
"errors"
"fmt"
"math"
"reflect"
"slices"
"strconv"
Expand Down Expand Up @@ -280,6 +281,11 @@ func convertValue(valueVal reflect.Value, targetType reflect.Type) (reflect.Valu
return valueVal, nil
}

// Numeric conversions must not silently truncate, wrap or round.
if isNumericKind(valueType.Kind()) && isNumericKind(targetType.Kind()) {
return convertNumeric(valueVal, targetType)
}

// Convertibility check (handles typedefed types)
if valueType.ConvertibleTo(targetType) {
return valueVal.Convert(targetType), nil
Expand All @@ -294,6 +300,15 @@ func convertValue(valueVal reflect.Value, targetType reflect.Type) (reflect.Valu
ptr.Elem().Set(valueVal)
return ptr, nil
}
if isNumericKind(valueType.Kind()) && isNumericKind(elemType.Kind()) {
converted, err := convertNumeric(valueVal, elemType)
if err != nil {
return reflect.Value{}, err
}
ptr := reflect.New(elemType)
ptr.Elem().Set(converted)
return ptr, nil
}
if valueType.ConvertibleTo(elemType) {
ptr := reflect.New(elemType)
ptr.Elem().Set(valueVal.Convert(elemType))
Expand All @@ -310,6 +325,78 @@ func convertValue(valueVal reflect.Value, targetType reflect.Type) (reflect.Valu
return reflect.Value{}, fmt.Errorf("cannot convert %s to %s", valueType, targetType)
}

func isNumericKind(k reflect.Kind) bool {
switch k {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
return true
default:
return false
}
}

// convertNumeric converts between numeric kinds and fails instead of losing data,
// following the rules of libs/dyn/convert (normalizeInt). Unlike normalizeFloat,
// any conversion to a float kind is allowed, even when it rounds or overflows.
func convertNumeric(valueVal reflect.Value, targetType reflect.Type) (reflect.Value, error) {
dstKind := targetType.Kind()
zero := reflect.New(targetType).Elem()

switch {
case valueVal.CanInt():
v := valueVal.Int()
switch {
case zero.CanInt():
if zero.OverflowInt(v) {
return reflect.Value{}, fmt.Errorf("value %d overflows %s", v, dstKind)
}
case zero.CanUint():
if v < 0 || zero.OverflowUint(uint64(v)) {
return reflect.Value{}, fmt.Errorf("value %d overflows %s", v, dstKind)
}
default:
// Int to float: allowed even with a loss.
}

case valueVal.CanUint():
v := valueVal.Uint()
switch {
case zero.CanInt():
if v > math.MaxInt64 || zero.OverflowInt(int64(v)) {
return reflect.Value{}, fmt.Errorf("value %d overflows %s", v, dstKind)
}
case zero.CanUint():
if zero.OverflowUint(v) {
return reflect.Value{}, fmt.Errorf("value %d overflows %s", v, dstKind)
}
default:
// Uint to float: allowed even with a loss.
}

default:
f := valueVal.Float()
if !zero.CanInt() && !zero.CanUint() {
// Float to float: allowed even with a loss.
break
}
if math.IsNaN(f) || f != math.Trunc(f) {
return reflect.Value{}, fmt.Errorf("cannot set %v to %s: precision loss", f, dstKind)
}
// Bounds are powers of two, so they are exact in float64.
bits := float64(targetType.Bits())
lo, hi := 0.0, math.Exp2(bits)
if zero.CanInt() {
lo, hi = -math.Exp2(bits-1), math.Exp2(bits-1)
}
if f < lo || f >= hi {
return reflect.Value{}, fmt.Errorf("value %v overflows %s", f, dstKind)
}
}

return valueVal.Convert(targetType), nil
}

// updateForceSendFields handles ForceSendFields when setting values:
// - If setting nil: remove field from ForceSendFields
// - If setting empty value: add field to ForceSendFields (if not already present)
Expand Down
Loading
Loading