diff --git a/CHANGELOG.md b/CHANGELOG.md index bf560d96f6..c3c20913a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,8 @@ - Fixed SQL Server query input failure due to incorrect select query generation. - Fixed UDTF ingestion not preserving column nullability in the output schema. - Fixed an issue that caused the program to hang during multithreaded Parquet based ingestion when a data fetching error occurred. + - Fixed a bug in schema parsing when custom schema strings used upper-cased data type names (NUMERIC, NUMBER, DECIMAL, VARCHAR, STRING, TEXT). +- Fixed a bug in `Session.create_dataframe` where schema string parsing failed when using upper-cased data type names (e.g., NUMERIC, NUMBER, DECIMAL, VARCHAR, STRING, TEXT). #### Improvements diff --git a/src/snowflake/snowpark/_internal/type_utils.py b/src/snowflake/snowpark/_internal/type_utils.py index 3b61320f06..926a0d97b0 100644 --- a/src/snowflake/snowpark/_internal/type_utils.py +++ b/src/snowflake/snowpark/_internal/type_utils.py @@ -1089,11 +1089,11 @@ def get_data_type_string_object_mappings( DECIMAL_RE = re.compile( - r"^\s*(numeric|number|decimal)\s*\(\s*(\s*)(\d*)\s*,\s*(\d*)\s*\)\s*$" + r"(?i)^\s*(numeric|number|decimal)\s*\(\s*(\s*)(\d*)\s*,\s*(\d*)\s*\)\s*$" ) # support type string format like " decimal ( 2 , 1 ) " -STRING_RE = re.compile(r"^\s*(varchar|string|text)\s*\(\s*(\d*)\s*\)\s*$") +STRING_RE = re.compile(r"(?i)^\s*(varchar|string|text)\s*\(\s*(\d*)\s*\)\s*$") # support type string format like " string ( 23 ) " ARRAY_RE = re.compile(r"(?i)^\s*array\s*<") diff --git a/tests/integ/test_dataframe.py b/tests/integ/test_dataframe.py index 1230158204..616b90b2a9 100644 --- a/tests/integ/test_dataframe.py +++ b/tests/integ/test_dataframe.py @@ -6844,7 +6844,8 @@ def test_create_dataframe_implicit_struct_not_null_single(session): assert result == expected_rows -def test_create_dataframe_implicit_struct_not_null_multiple(session): +@pytest.mark.parametrize("upper_case", [False, True]) +def test_create_dataframe_implicit_struct_not_null_multiple(session, upper_case): """ Test a schema with multiple fields, one of which is NOT NULL. """ @@ -6852,7 +6853,11 @@ def test_create_dataframe_implicit_struct_not_null_multiple(session): [10, "foo"], [20, "bar"], ] - schema_str = "col1: int not null, col2: string" + # Only uppercase the types, not field names + if upper_case: + schema_str = "col1: INT NOT NULL, col2: STRING(100)" + else: + schema_str = "col1: int not null, col2: string(100)" df = session.create_dataframe(data, schema=schema_str) # Verify schema @@ -6860,7 +6865,7 @@ def test_create_dataframe_implicit_struct_not_null_multiple(session): expected_fields = [ StructField("COL1", LongType(), nullable=False), - StructField("COL2", StringType(2**24), nullable=True), + StructField("COL2", StringType(100), nullable=True), ] assert df.schema.fields == expected_fields @@ -6873,7 +6878,8 @@ def test_create_dataframe_implicit_struct_not_null_multiple(session): assert result == expected_rows -def test_create_dataframe_implicit_struct_not_null_nested(session): +@pytest.mark.parametrize("upper_case", [False, True]) +def test_create_dataframe_implicit_struct_not_null_nested(session, upper_case): """ Test a schema with nested array and a NOT NULL decimal field. """ @@ -6881,7 +6887,11 @@ def test_create_dataframe_implicit_struct_not_null_nested(session): [["1", "2"], Decimal("3.14")], [["5", "6"], Decimal("2.72")], ] - schema_str = "arr: array, val: decimal(10,2) NOT NULL" + # Only uppercase the types, not field names + if upper_case: + schema_str = "arr: ARRAY, val: DECIMAL(10,2) NOT NULL" + else: + schema_str = "arr: array, val: decimal(10,2) NOT NULL" df = session.create_dataframe(data, schema=schema_str) # Verify schema diff --git a/tests/resources/test_data_source_dir/test_data_source_data.py b/tests/resources/test_data_source_dir/test_data_source_data.py index 6d42ad08ee..1f76c5b04d 100644 --- a/tests/resources/test_data_source_dir/test_data_source_data.py +++ b/tests/resources/test_data_source_dir/test_data_source_data.py @@ -961,13 +961,13 @@ def unknown_dbms_create_connection(): ) -SQLITE3_DB_CUSTOM_SCHEMA_STRING = "id INTEGER, int_col INTEGER, real_col FLOAT, text_col STRING, blob_col BINARY, null_col STRING, ts_col TIMESTAMP, date_col DATE, time_col TIME, short_col SHORT, long_col LONG, double_col DOUBLE, decimal_col DECIMAL, map_col MAP, array_col ARRAY, var_col VARIANT" +SQLITE3_DB_CUSTOM_SCHEMA_STRING = "id INTEGER, int_col INTEGER, real_col FLOAT, text_col STRING, blob_col BINARY, null_col TEXT(200), ts_col TIMESTAMP, date_col DATE, time_col TIME, short_col SHORT, long_col LONG, double_col DOUBLE, decimal_col DECIMAL(25,8), map_col MAP, array_col ARRAY, var_col VARIANT" SQLITE3_DB_CUSTOM_SCHEMA_STRUCT_TYPE = StructType( [ StructField("id", IntegerType()), StructField("int_col", IntegerType()), StructField("real_col", FloatType()), - StructField("text_col", StringType()), + StructField("text_col", StringType(200)), StructField("blob_col", BinaryType()), StructField("null_col", NullType()), StructField("ts_col", TimestampType()), @@ -976,7 +976,7 @@ def unknown_dbms_create_connection(): StructField("short_col", ShortType()), StructField("long_col", LongType()), StructField("double_col", DoubleType()), - StructField("decimal_col", DecimalType()), + StructField("decimal_col", DecimalType(25, 8)), StructField("map_col", MapType()), StructField("array_col", ArrayType()), StructField("var_col", VariantType()), diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index 85b603401a..8f3483ff0d 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -1685,55 +1685,85 @@ def test_maptype_alias(): assert tpe.keyType == tpe.key_type -def test_type_string_to_type_object_basic_int(): - dt = type_string_to_type_object("int") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_basic_int(upper_case): + base_string = "int" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, IntegerType), f"Expected IntegerType, got {dt}" -def test_type_string_to_type_object_smallint(): - dt = type_string_to_type_object("smallint") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_smallint(upper_case): + base_string = "smallint" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, ShortType), f"Expected ShortType, got {dt}" -def test_type_string_to_type_object_byteint(): - dt = type_string_to_type_object("byteint") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_byteint(upper_case): + base_string = "byteint" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, ByteType), f"Expected ByteType, got {dt}" -def test_type_string_to_type_object_bigint(): - dt = type_string_to_type_object("bigint") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_bigint(upper_case): + base_string = "bigint" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, LongType), f"Expected LongType, got {dt}" -def test_type_string_to_type_object_number_decimal(): +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_number_decimal(upper_case): # For number(precision, scale) => DecimalType - dt = type_string_to_type_object("number(10,2)") + base_string = "number(10,2)" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, DecimalType), f"Expected DecimalType, got {dt}" assert dt.precision == 10, f"Expected precision=10, got {dt.precision}" assert dt.scale == 2, f"Expected scale=2, got {dt.scale}" - dt = type_string_to_type_object("decimal") + + +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_decimal(upper_case): + base_string = "decimal" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, DecimalType), f"Expected DecimalType, got {dt}" assert dt.precision == 38, f"Expected precision=38, got {dt.precision}" assert dt.scale == 0, f"Expected scale=0, got {dt.scale}" -def test_type_string_to_type_object_numeric_decimal(): - dt = type_string_to_type_object("numeric(20, 5)") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_numeric_decimal(upper_case): + base_string = "numeric(20, 5)" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, DecimalType), f"Expected DecimalType, got {dt}" assert dt.precision == 20, f"Expected precision=20, got {dt.precision}" assert dt.scale == 5, f"Expected scale=5, got {dt.scale}" -def test_type_string_to_type_object_decimal_spaces(): +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_decimal_spaces(upper_case): # Check spaces inside parentheses - dt = type_string_to_type_object(" decimal ( 2 , 1 ) ") + base_string = " decimal ( 2 , 1 ) " + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, DecimalType), f"Expected DecimalType, got {dt}" assert dt.precision == 2, f"Expected precision=2, got {dt.precision}" assert dt.scale == 1, f"Expected scale=1, got {dt.scale}" -def test_type_string_to_type_object_string_with_length(): - dt = type_string_to_type_object("string(50)") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_string_with_length(upper_case): + base_string = "string(50)" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, StringType), f"Expected StringType, got {dt}" # Snowpark's StringType typically doesn't store length internally, # but here, you're returning StringType(50) in your code, so let's check @@ -1741,48 +1771,95 @@ def test_type_string_to_type_object_string_with_length(): assert dt.length == 50, f"Expected length=50, got {dt.length}" -def test_type_string_to_type_object_text_with_length(): - dt = type_string_to_type_object("text(100)") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_text_with_length(upper_case): + base_string = "text(100)" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, StringType), f"Expected StringType, got {dt}" if hasattr(dt, "length"): assert dt.length == 100, f"Expected length=100, got {dt.length}" -def test_type_string_to_type_object_timestamp(): - dt = type_string_to_type_object("timestamp") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_varchar_with_length(upper_case): + base_string = "varchar(150)" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) + assert isinstance(dt, StringType), f"Expected StringType, got {dt}" + if hasattr(dt, "length"): + assert dt.length == 150, f"Expected length=150, got {dt.length}" + + +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_varchar_no_length(upper_case): + base_string = "varchar" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) + assert isinstance(dt, StringType), f"Expected StringType, got {dt}" + if hasattr(dt, "length"): + assert not dt.length, f"Expected length is None, got {dt.length}" + + +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_timestamp(upper_case): + base_string = "timestamp" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, TimestampType) assert dt.tz == TimestampTimeZone.DEFAULT - dt = type_string_to_type_object("timestamp_ntz") + + base_string = "timestamp_ntz" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, TimestampType) assert dt.tz == TimestampTimeZone.NTZ - dt = type_string_to_type_object("timestamp_tz") + + base_string = "timestamp_tz" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, TimestampType) assert dt.tz == TimestampTimeZone.TZ - dt = type_string_to_type_object("timestamp_ltz") + + base_string = "timestamp_ltz" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, TimestampType) assert dt.tz == TimestampTimeZone.LTZ -def test_type_string_to_type_object_year_month_interval(): - dt = type_string_to_type_object("yearmonthinterval") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_year_month_interval(upper_case): + base_string = "yearmonthinterval" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, YearMonthIntervalType) -def test_type_string_to_type_object_daytimeinterval(): - dt = type_string_to_type_object("daytimeinterval") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_daytimeinterval(upper_case): + base_string = "daytimeinterval" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, DayTimeIntervalType) -def test_type_string_to_type_object_array_of_int(): - dt = type_string_to_type_object("array") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_array_of_int(upper_case): + base_string = "array" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, ArrayType), f"Expected ArrayType, got {dt}" assert isinstance( dt.element_type, IntegerType ), f"Expected element_type=IntegerType, got {dt.element_type}" -def test_type_string_to_type_object_array_of_decimal(): - dt = type_string_to_type_object("array") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_array_of_decimal(upper_case): + base_string = "array" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, ArrayType), f"Expected ArrayType, got {dt}" assert isinstance( dt.element_type, DecimalType @@ -1799,8 +1876,11 @@ def test_type_string_to_type_object_array_of_decimal(): ), f"Expected not a supported type, got: {ex}" -def test_type_string_to_type_object_map_of_int_string(): - dt = type_string_to_type_object("map") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_map_of_int_string(upper_case): + base_string = "map" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, MapType), f"Expected MapType, got {dt}" assert isinstance( dt.key_type, IntegerType @@ -1810,8 +1890,11 @@ def test_type_string_to_type_object_map_of_int_string(): ), f"Expected value_type=StringType, got {dt.value_type}" -def test_type_string_to_type_object_map_of_array_decimal(): - dt = type_string_to_type_object("map< array, decimal(12,5)>") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_map_of_array_decimal(upper_case): + base_string = "map< array, decimal(12,5)>" + type_string = base_string.upper() if upper_case else base_string + dt = type_string_to_type_object(type_string) assert isinstance(dt, MapType), f"Expected MapType, got {dt}" assert isinstance( dt.key_type, ArrayType @@ -1826,8 +1909,14 @@ def test_type_string_to_type_object_map_of_array_decimal(): assert dt.value_type.scale == 5 -def test_type_string_to_type_object_explicit_struct_simple(): - dt = type_string_to_type_object("struct") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_explicit_struct_simple(upper_case): + # Only uppercase the types, not the field names + if upper_case: + type_string = "STRUCT" + else: + type_string = "struct" + dt = type_string_to_type_object(type_string) assert isinstance(dt, StructType), f"Expected StructType, got {dt}" assert len(dt.fields) == 2, f"Expected 2 fields, got {len(dt.fields)}" @@ -1842,10 +1931,14 @@ def test_type_string_to_type_object_explicit_struct_simple(): ), f"Expected {expected_field_b}, got {dt.fields[1]}" -def test_type_string_to_type_object_explicit_struct_nested(): - dt = type_string_to_type_object( - "struct, y: map>" - ) +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_explicit_struct_nested(upper_case): + # Only uppercase the types, not the field names + if upper_case: + type_string = "STRUCT, y: MAP>" + else: + type_string = "struct, y: map>" + dt = type_string_to_type_object(type_string) assert isinstance(dt, StructType), f"Expected StructType, got {dt}" assert len(dt.fields) == 2, f"Expected 2 fields, got {len(dt.fields)}" @@ -1863,19 +1956,25 @@ def test_type_string_to_type_object_explicit_struct_nested(): ), f"Expected {expected_field_y}, got {dt.fields[1]}" -def test_type_string_to_type_object_unknown_type(): +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_unknown_type(upper_case): + base_string = "unknown_type" + type_string = base_string.upper() if upper_case else base_string try: - type_string_to_type_object("unknown_type") + type_string_to_type_object(type_string) raise AssertionError("Expected ValueError for unknown type") except ValueError as ex: - assert "unknown_type" in str( + assert base_string in str( ex - ), f"Error message doesn't mention 'unknown_type': {ex}" + ), f"Error message doesn't mention '{type_string}': {ex}" -def test_type_string_to_type_object_mismatched_bracket_array(): +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_mismatched_bracket_array(upper_case): + base_string = "array>")) + print(type_string_to_type_object(type_string)) raise AssertionError("Expected ValueError for mismatched bracket") except ValueError as ex: assert "Unexpected characters after closing '>' in" in str( @@ -1893,18 +1995,27 @@ def test_type_string_to_type_object_mismatched_bracket_map(): ), f"Expected Unexpected characters after closing '>' error, got: {ex}" -def test_type_string_to_type_object_bad_decimal(): +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_bad_decimal(upper_case): + base_string = "decimal(10,2,5)" + type_string = base_string.upper() if upper_case else base_string try: - type_string_to_type_object("decimal(10,2,5)") + type_string_to_type_object(type_string) raise AssertionError("Expected ValueError for a malformed decimal argument") except ValueError: # "decimal(10,2,5)" doesn't match the DECIMAL_RE regex => unknown type => ValueError pass -def test_type_string_to_type_object_bad_struct_syntax(): +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_bad_struct_syntax(upper_case): + # Only uppercase the types, not field names + if upper_case: + type_string = "struct'. """ - dt = type_string_to_type_object("a: int, b: string") + # Only uppercase the types, not the field names + if upper_case: + type_string = "a: INT, b: STRING" + else: + type_string = "a: int, b: string" + dt = type_string_to_type_object(type_string) assert isinstance(dt, StructType), f"Expected StructType, got {dt}" assert len(dt.fields) == 2, f"Expected 2 fields, got {len(dt.fields)}" @@ -1936,12 +2053,18 @@ def test_type_string_to_type_object_implicit_struct_simple(): ), f"Expected {expected_field_b}, got {dt.fields[1]}" -def test_type_string_to_type_object_implicit_struct_single_field(): +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_implicit_struct_single_field(upper_case): """ Even a single 'name: type' with no commas should parse to StructType if your parser logic treats it as an implicit struct. """ - dt = type_string_to_type_object("c: decimal(10,2)") + # Only uppercase the type, not the field name + if upper_case: + type_string = "c: DECIMAL(10,2)" + else: + type_string = "c: decimal(10,2)" + dt = type_string_to_type_object(type_string) assert isinstance(dt, StructType), f"Expected StructType, got {dt}" assert len(dt.fields) == 1, f"Expected 1 field, got {len(dt.fields)}" @@ -1951,12 +2074,18 @@ def test_type_string_to_type_object_implicit_struct_single_field(): ), f"Expected {expected_field_c}, got {dt.fields[0]}" -def test_type_string_to_type_object_implicit_struct_nested(): +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_implicit_struct_nested(upper_case): """ Test an implicit struct with multiple fields, including nested array/map types. """ - dt = type_string_to_type_object("arr: array, kv: map") + # Only uppercase the types, not the field names + if upper_case: + type_string = "arr: ARRAY, kv: MAP" + else: + type_string = "arr: array, kv: map" + dt = type_string_to_type_object(type_string) assert isinstance(dt, StructType), f"Expected StructType, got {dt}" assert len(dt.fields) == 2, f"Expected 2 fields, got {len(dt.fields)}" @@ -1973,13 +2102,17 @@ def test_type_string_to_type_object_implicit_struct_nested(): ), f"Expected {expected_field_kv}, got {dt.fields[1]}" -def test_type_string_to_type_object_implicit_struct_with_spaces(): +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_implicit_struct_with_spaces(upper_case): """ Test spacing variations. E.g. " col1 : int , col2 : map< string , decimal(5,2) > ". """ - dt = type_string_to_type_object( - " col1 : int , col2 : map< string , decimal( 5 , 2 ) > " - ) + # Only uppercase the types, not the field names + if upper_case: + type_string = " col1 : INT , col2 : MAP< STRING , DECIMAL( 5 , 2 ) > " + else: + type_string = " col1 : int , col2 : map< string , decimal( 5 , 2 ) > " + dt = type_string_to_type_object(type_string) assert isinstance(dt, StructType), f"Expected StructType, got {dt}" assert len(dt.fields) == 2, f"Expected 2 fields, got {len(dt.fields)}" @@ -1996,8 +2129,14 @@ def test_type_string_to_type_object_implicit_struct_with_spaces(): ), f"Expected {expected_field_col2}, got {dt.fields[1]}" -def test_type_string_to_type_object_implicit_struct_inner_colon(): - dt = type_string_to_type_object("struct struct") +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_implicit_struct_inner_colon(upper_case): + # Only uppercase the types, not the field names + if upper_case: + type_string = "struct STRUCT" + else: + type_string = "struct struct" + dt = type_string_to_type_object(type_string) assert isinstance(dt, StructType), f"Expected StructType, got {dt}" assert len(dt.fields) == 1, f"Expected 1 field, got {len(dt.fields)}" expected_field_i = StructField( @@ -2010,13 +2149,22 @@ def test_type_string_to_type_object_implicit_struct_inner_colon(): ), f"Expected {expected_field_i}, got {dt.fields[0]}" -def test_type_string_to_type_object_implicit_struct_error(): +@pytest.mark.parametrize("upper_case", [False, True]) +def test_type_string_to_type_object_implicit_struct_error(upper_case): """ Check a malformed implicit struct that should raise ValueError (e.g. trailing comma or missing bracket for nested). """ + # Only uppercase the types, not field names + if upper_case: + type_string1 = "a: INT, b:" + type_string2 = "arr: ARRAY'