diff --git a/opengin/core-api/pkg/schema/schema.go b/opengin/core-api/pkg/schema/schema.go index 284e77f8..d427fee5 100644 --- a/opengin/core-api/pkg/schema/schema.go +++ b/opengin/core-api/pkg/schema/schema.go @@ -271,6 +271,11 @@ func (sg *SchemaGenerator) GenerateSchema(anyValue *anypb.Any) (*SchemaInfo, err StorageType: storageinference.ScalarData, TypeInfo: &typeinference.TypeInfo{}, } + if m == nil || m.GetKind() == nil { + schema.TypeInfo.Type = typeinference.NullType + schema.TypeInfo.IsNullable = true + return schema, nil + } switch m.GetKind().(type) { case *structpb.Value_StringValue: schema.TypeInfo.Type = typeinference.StringType @@ -340,6 +345,11 @@ func (sg *SchemaGenerator) GenerateSchema(anyValue *anypb.Any) (*SchemaInfo, err StorageType: storageinference.ScalarData, TypeInfo: &typeinference.TypeInfo{}, } + if value == nil || value.GetKind() == nil { + schema.TypeInfo.Type = typeinference.NullType + schema.TypeInfo.IsNullable = true + return schema, nil + } switch value.GetKind().(type) { case *structpb.Value_StringValue: schema.TypeInfo.Type = typeinference.StringType @@ -513,62 +523,62 @@ func inferColumnTypes(columnsList, rowsList *structpb.ListValue) (map[string]typ // Scan rows until each column has a type from its first non-null value (or all-null → NullType below). nPending := len(columnTypes) - rowsLoop: - for rowIndex, row := range rowsList.Values { - rowData := row.GetListValue() - if len(rowData.Values) != len(columnsList.Values) { - return nil, fmt.Errorf("row %d length (%d) does not match number of columns (%d)", rowIndex, len(rowData.Values), len(columnsList.Values)) +rowsLoop: + for rowIndex, row := range rowsList.Values { + rowData := row.GetListValue() + if len(rowData.Values) != len(columnsList.Values) { + return nil, fmt.Errorf("row %d length (%d) does not match number of columns (%d)", rowIndex, len(rowData.Values), len(columnsList.Values)) + } + for i, value := range rowData.Values { + colName := columnsList.Values[i].GetStringValue() + if columnTypes[colName].Type != typeinference.DataType(unknown) { + continue } - for i, value := range rowData.Values { - colName := columnsList.Values[i].GetStringValue() - if columnTypes[colName].Type != typeinference.DataType(unknown) { - continue - } - if structpbScalarIsNull(value) { - continue - } + if structpbScalarIsNull(value) { + continue + } - matched := false - switch value.GetKind().(type) { - case *structpb.Value_NumberValue: - columnTypes[colName] = typeinference.TypeInfo{ - Type: typeinference.NumericType, - IsNullable: true, - } - matched = true - case *structpb.Value_BoolValue: - columnTypes[colName] = typeinference.TypeInfo{ - Type: typeinference.BoolType, - IsNullable: true, - } - matched = true - case *structpb.Value_StringValue: - str := value.GetStringValue() - var inferredType typeinference.DataType - if isDate, isDateTime := isDateOrDateTime(str); isDate { - if isDateTime { - inferredType = typeinference.DateTimeType - } else { - inferredType = typeinference.DateType - } + matched := false + switch value.GetKind().(type) { + case *structpb.Value_NumberValue: + columnTypes[colName] = typeinference.TypeInfo{ + Type: typeinference.NumericType, + IsNullable: true, + } + matched = true + case *structpb.Value_BoolValue: + columnTypes[colName] = typeinference.TypeInfo{ + Type: typeinference.BoolType, + IsNullable: true, + } + matched = true + case *structpb.Value_StringValue: + str := value.GetStringValue() + var inferredType typeinference.DataType + if isDate, isDateTime := isDateOrDateTime(str); isDate { + if isDateTime { + inferredType = typeinference.DateTimeType } else { - inferredType = typeinference.StringType + inferredType = typeinference.DateType } - columnTypes[colName] = typeinference.TypeInfo{ - Type: inferredType, - IsNullable: true, - } - matched = true + } else { + inferredType = typeinference.StringType } - if matched { - nPending-- - if nPending == 0 { - break rowsLoop - } + columnTypes[colName] = typeinference.TypeInfo{ + Type: inferredType, + IsNullable: true, + } + matched = true + } + if matched { + nPending-- + if nPending == 0 { + break rowsLoop } } } + } // Any column still unknown = all nulls for colName, info := range columnTypes { @@ -2015,6 +2025,11 @@ func (sg *SchemaGenerator) handleScalarData(structValue *structpb.Struct, schema // Find the first scalar field var scalarField *structpb.Value for _, field := range structValue.Fields { + if field == nil || field.GetKind() == nil || structpbScalarIsNull(field) { + scalarField = field + break + } + switch field.GetKind().(type) { case *structpb.Value_StringValue, *structpb.Value_NumberValue, *structpb.Value_BoolValue, *structpb.Value_NullValue: scalarField = field diff --git a/opengin/core-api/pkg/schema/schema_test.go b/opengin/core-api/pkg/schema/schema_test.go index e8bb2ba1..c2f306d2 100644 --- a/opengin/core-api/pkg/schema/schema_test.go +++ b/opengin/core-api/pkg/schema/schema_test.go @@ -9,8 +9,10 @@ import ( "testing" "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/structpb" + "lk/datafoundation/core-api/pkg/storageinference" "lk/datafoundation/core-api/pkg/typeinference" ) @@ -773,3 +775,20 @@ func TestInferColumnTypesUnsetKindActsAsNull(t *testing.T) { assert.Equal(t, typeinference.NumericType, colTypes["x"].Type) assert.True(t, colTypes["x"].IsNullable) } + +func TestGenerateSchemaScalarNullWithUnsetKind(t *testing.T) { + structValue := &structpb.Struct{Fields: map[string]*structpb.Value{ + "value": { + Kind: nil, + }, + }} + anyValue, err := anypb.New(structValue) + assert.NoError(t, err) + + generator := NewSchemaGenerator() + schema, err := generator.GenerateSchema(anyValue) + assert.NoError(t, err) + assert.Equal(t, storageinference.ScalarData, schema.StorageType) + assert.Equal(t, typeinference.NullType, schema.TypeInfo.Type) + assert.True(t, schema.TypeInfo.IsNullable) +} diff --git a/opengin/core-api/pkg/storageinference/inference.go b/opengin/core-api/pkg/storageinference/inference.go index 71e54007..8ba88639 100644 --- a/opengin/core-api/pkg/storageinference/inference.go +++ b/opengin/core-api/pkg/storageinference/inference.go @@ -197,6 +197,9 @@ func isScalar(structValue *structpb.Struct) bool { // Check if the struct has a single field with a scalar value if len(structValue.Fields) == 1 { for _, value := range structValue.Fields { + if value == nil || value.GetKind() == nil { + return true + } switch value.GetKind().(type) { case *structpb.Value_NumberValue, *structpb.Value_StringValue, *structpb.Value_BoolValue, *structpb.Value_NullValue: return true diff --git a/opengin/core-api/pkg/storageinference/inference_test.go b/opengin/core-api/pkg/storageinference/inference_test.go index 9705b486..bd8127c7 100644 --- a/opengin/core-api/pkg/storageinference/inference_test.go +++ b/opengin/core-api/pkg/storageinference/inference_test.go @@ -172,6 +172,7 @@ func TestDirectScalarEntity(t *testing.T) { "float": `3.14159`, "string": `"test"`, "boolean": `true`, + "null": `null`, } inferrer := &StorageInferrer{}