From bf0b8ea058133634279b9b71e75cbfab25385f92 Mon Sep 17 00:00:00 2001 From: Delaksan Sritharan Date: Sat, 25 Jul 2026 21:09:37 +0530 Subject: [PATCH] fix: enhance type inference for zero float literals and improve JSON handling --- .../core-api/pkg/typeinference/inference.go | 42 +++--- .../pkg/typeinference/inference_test.go | 138 +++++++++++------- 2 files changed, 111 insertions(+), 69 deletions(-) diff --git a/opengin/core-api/pkg/typeinference/inference.go b/opengin/core-api/pkg/typeinference/inference.go index 232d3385..6d0521b7 100644 --- a/opengin/core-api/pkg/typeinference/inference.go +++ b/opengin/core-api/pkg/typeinference/inference.go @@ -4,8 +4,8 @@ package typeinference import ( - "fmt" "reflect" + "strconv" "strings" "time" @@ -29,10 +29,10 @@ const ( DateType DataType = "date" // Date values (e.g., "2024-03-20") TimeType DataType = "time" // Time values (e.g., "14:30:00") DateTimeType DataType = "datetime" // Date and time values (e.g., "2024-03-20T14:30:00Z") - + // Schema/DB-level Types — not produced by value inference; // used when column-level precision is unknown (e.g., tabular data). - NumericType DataType = "numeric" // Any numeric value, int or float (maps to Postgres NUMERIC) + NumericType DataType = "numeric" // Any numeric value, int or float (maps to Postgres NUMERIC) ) // TypeInfo contains both the data type and additional metadata about the type. @@ -108,27 +108,18 @@ func (ti *TypeInferrer) inferTypeFromValue(value *structpb.Value) (*TypeInfo, er case *structpb.Value_NumberValue: num := v.NumberValue - // Check if the number is a float by comparing it with its integer conversion - // This works because float64(int64(num)) truncates any decimal part - // If the original number has a decimal part, the comparison will be false + // Check if the number is a float by comparing it with its integer conversion. + // This works because float64(int64(num)) truncates any decimal part. if num != float64(int64(num)) { return &TypeInfo{Type: FloatType}, nil } - - // For zero, check if it was originally a float by looking at the string representation - // This is necessary because 0.0 and 0 have the same numeric value but different types - // We convert to string to check if the original format had a decimal point - if num == 0 { - // Use %g format to preserve decimal point for zero values - str := fmt.Sprintf("%g", num) - if strings.Contains(str, ".") { - return &TypeInfo{Type: FloatType}, nil - } - } return &TypeInfo{Type: IntType}, nil case *structpb.Value_StringValue: str := v.StringValue + if inferredType, ok := inferNumericTypeFromString(str); ok { + return &TypeInfo{Type: inferredType}, nil + } // Check for special string types if isDate(str) { return &TypeInfo{Type: DateType}, nil @@ -196,6 +187,23 @@ func (ti *TypeInferrer) inferTypeFromValue(value *structpb.Value) (*TypeInfo, er // Returns: // - *TypeInfo: The inferred type information // - error: Any error that occurred during inference +func inferNumericTypeFromString(str string) (DataType, bool) { + trimmed := strings.TrimSpace(str) + if trimmed == "" { + return "", false + } + + if _, err := strconv.ParseInt(trimmed, 10, 64); err == nil { + return IntType, true + } + + if _, err := strconv.ParseFloat(trimmed, 64); err == nil { + return FloatType, true + } + + return "", false +} + func (ti *TypeInferrer) inferTypeFromReflection(rv reflect.Value) (*TypeInfo, error) { switch rv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: diff --git a/opengin/core-api/pkg/typeinference/inference_test.go b/opengin/core-api/pkg/typeinference/inference_test.go index 4d3c5918..2c355e34 100644 --- a/opengin/core-api/pkg/typeinference/inference_test.go +++ b/opengin/core-api/pkg/typeinference/inference_test.go @@ -6,6 +6,7 @@ package typeinference import ( "encoding/json" "fmt" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -13,77 +14,106 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) -// JSONToAny converts a JSON string to a protobuf Any value +// JSONToAny converts a JSON string to a protobuf Any value. func JSONToAny(jsonStr string) (*anypb.Any, error) { var data interface{} - if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + decoder := json.NewDecoder(strings.NewReader(jsonStr)) + decoder.UseNumber() + if err := decoder.Decode(&data); err != nil { return nil, fmt.Errorf("failed to unmarshal JSON: %v", err) } - // Handle scalar values switch v := data.(type) { - case float64: - // Check if it's an integer - if v == float64(int64(v)) { - structValue, err := structpb.NewStruct(map[string]interface{}{ - "value": int64(v), - }) - if err != nil { - return nil, fmt.Errorf("failed to create struct: %v", err) - } - return anypb.New(structValue) - } - structValue, err := structpb.NewStruct(map[string]interface{}{ - "value": v, - }) - if err != nil { - return nil, fmt.Errorf("failed to create struct: %v", err) - } - return anypb.New(structValue) - case string: - structValue, err := structpb.NewStruct(map[string]interface{}{ - "value": v, - }) + case map[string]interface{}: + structValue, err := buildStruct(v) if err != nil { - return nil, fmt.Errorf("failed to create struct: %v", err) + return nil, err } return anypb.New(structValue) - case bool: - structValue, err := structpb.NewStruct(map[string]interface{}{ - "value": v, - }) + case []interface{}: + value, err := convertJSONValue(v) if err != nil { - return nil, fmt.Errorf("failed to create struct: %v", err) + return nil, err } - return anypb.New(structValue) - case nil: - structValue, err := structpb.NewStruct(map[string]interface{}{ - "value": nil, - }) + return anypb.New(&structpb.Struct{Fields: map[string]*structpb.Value{"value": value}}) + default: + value, err := convertJSONValue(v) if err != nil { - return nil, fmt.Errorf("failed to create struct: %v", err) + return nil, err } - return anypb.New(structValue) - case []interface{}: - structValue, err := structpb.NewStruct(map[string]interface{}{ - "value": v, - }) - if err != nil { - return nil, fmt.Errorf("failed to create struct: %v", err) - } - return anypb.New(structValue) + return anypb.New(&structpb.Struct{Fields: map[string]*structpb.Value{"value": value}}) } +} - // Handle objects - if obj, ok := data.(map[string]interface{}); ok { - structValue, err := structpb.NewStruct(obj) +func buildStruct(data map[string]interface{}) (*structpb.Struct, error) { + fields := make(map[string]*structpb.Value, len(data)) + for key, value := range data { + converted, err := convertJSONValue(value) if err != nil { - return nil, fmt.Errorf("failed to create struct: %v", err) + return nil, err } - return anypb.New(structValue) + fields[key] = converted } + return &structpb.Struct{Fields: fields}, nil +} - return nil, fmt.Errorf("unsupported data type: %T", data) +func convertJSONValue(value interface{}) (*structpb.Value, error) { + switch v := value.(type) { + case nil: + return &structpb.Value{Kind: &structpb.Value_NullValue{}}, nil + case string: + return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: v}}, nil + case bool: + return &structpb.Value{Kind: &structpb.Value_BoolValue{BoolValue: v}}, nil + case json.Number: + return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: v.String()}}, nil + case float64: + return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: v}}, nil + case float32: + return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v)}}, nil + case int: + return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v)}}, nil + case int8: + return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v)}}, nil + case int16: + return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v)}}, nil + case int32: + return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v)}}, nil + case int64: + return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v)}}, nil + case uint: + return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v)}}, nil + case uint8: + return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v)}}, nil + case uint16: + return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v)}}, nil + case uint32: + return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v)}}, nil + case uint64: + return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v)}}, nil + case []interface{}: + listValues := make([]*structpb.Value, len(v)) + for i, item := range v { + converted, err := convertJSONValue(item) + if err != nil { + return nil, err + } + listValues[i] = converted + } + return &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: listValues}}}, nil + case map[string]interface{}: + fields := make(map[string]*structpb.Value, len(v)) + for key, item := range v { + converted, err := convertJSONValue(item) + if err != nil { + return nil, err + } + fields[key] = converted + } + return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{Fields: fields}}}, nil + default: + return nil, fmt.Errorf("unsupported data type: %T", value) + } } // TestScalarTypes tests type inference for scalar data types @@ -105,6 +135,10 @@ func TestScalarTypes(t *testing.T) { json: `0.1`, expected: FloatType, }, + "zero_float_literal": { + json: `0.0`, + expected: FloatType, + }, "integer": { json: `42`, expected: IntType,