diff --git a/paimon-common/src/main/java/org/apache/paimon/casting/ArrayToStringCastRule.java b/paimon-common/src/main/java/org/apache/paimon/casting/ArrayToStringCastRule.java new file mode 100644 index 000000000000..ea7056678bb9 --- /dev/null +++ b/paimon-common/src/main/java/org/apache/paimon/casting/ArrayToStringCastRule.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.paimon.casting; + +import org.apache.paimon.data.BinaryString; +import org.apache.paimon.data.InternalArray; +import org.apache.paimon.types.ArrayType; +import org.apache.paimon.types.DataType; +import org.apache.paimon.types.DataTypeFamily; +import org.apache.paimon.types.DataTypeRoot; +import org.apache.paimon.types.VarCharType; + +/** {@link DataTypeRoot#ARRAY} to {@link DataTypeFamily#CHARACTER_STRING} cast rule. */ +public class ArrayToStringCastRule extends AbstractCastRule { + + static final ArrayToStringCastRule INSTANCE = new ArrayToStringCastRule(); + + private ArrayToStringCastRule() { + super( + CastRulePredicate.builder() + .input(DataTypeRoot.ARRAY) + .target(DataTypeFamily.CHARACTER_STRING) + .build()); + } + + @Override + public CastExecutor create( + DataType inputType, DataType targetType) { + ArrayType arrayType = (ArrayType) inputType; + InternalArray.ElementGetter elementGetter = + InternalArray.createElementGetter(arrayType.getElementType()); + CastExecutor castExecutor = + CastExecutors.resolve(arrayType.getElementType(), VarCharType.STRING_TYPE); + + return arrayData -> { + int size = arrayData.size(); + StringBuilder sb = new StringBuilder(); + sb.append("["); + for (int i = 0; i < size; i++) { + Object o = elementGetter.getElementOrNull(arrayData, i); + if (o == null) { + sb.append("null"); + } else { + sb.append(castExecutor.cast(o)); + } + if (i != size - 1) { + sb.append(", "); + } + } + sb.append("]"); + return BinaryString.fromString(sb.toString()); + }; + } +} diff --git a/paimon-common/src/main/java/org/apache/paimon/casting/CastExecutors.java b/paimon-common/src/main/java/org/apache/paimon/casting/CastExecutors.java index 546066d10aa3..8b95f00210b8 100644 --- a/paimon-common/src/main/java/org/apache/paimon/casting/CastExecutors.java +++ b/paimon-common/src/main/java/org/apache/paimon/casting/CastExecutors.java @@ -58,6 +58,9 @@ public class CastExecutors { .addRule(TimeToStringCastRule.INSTANCE) .addRule(DateToStringCastRule.INSTANCE) .addRule(StringToStringCastRule.INSTANCE) + .addRule(ArrayToStringCastRule.INSTANCE) + .addRule(MapToStringCastRule.INSTANCE) + .addRule(RowToStringCastRule.INSTANCE) // From string rules .addRule(StringToBooleanCastRule.INSTANCE) .addRule(StringToDecimalCastRule.INSTANCE) diff --git a/paimon-common/src/main/java/org/apache/paimon/casting/MapToStringCastRule.java b/paimon-common/src/main/java/org/apache/paimon/casting/MapToStringCastRule.java new file mode 100644 index 000000000000..c0231b7186ef --- /dev/null +++ b/paimon-common/src/main/java/org/apache/paimon/casting/MapToStringCastRule.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.paimon.casting; + +import org.apache.paimon.data.BinaryString; +import org.apache.paimon.data.InternalArray; +import org.apache.paimon.data.InternalMap; +import org.apache.paimon.types.DataType; +import org.apache.paimon.types.DataTypeFamily; +import org.apache.paimon.types.DataTypeRoot; +import org.apache.paimon.types.MapType; +import org.apache.paimon.types.VarCharType; + +/** {@link DataTypeRoot#MAP} to {@link DataTypeFamily#CHARACTER_STRING} cast rule. */ +public class MapToStringCastRule extends AbstractCastRule { + + static final MapToStringCastRule INSTANCE = new MapToStringCastRule(); + + private MapToStringCastRule() { + super( + CastRulePredicate.builder() + .input(DataTypeRoot.MAP) + .target(DataTypeFamily.CHARACTER_STRING) + .build()); + } + + @Override + public CastExecutor create(DataType inputType, DataType targetType) { + MapType mapType = (MapType) inputType; + InternalArray.ElementGetter keyGetter = + InternalArray.createElementGetter(mapType.getKeyType()); + InternalArray.ElementGetter valueGetter = + InternalArray.createElementGetter(mapType.getValueType()); + CastExecutor keyCastExecutor = + CastExecutors.resolve(mapType.getKeyType(), VarCharType.STRING_TYPE); + CastExecutor valueCastExecutor = + CastExecutors.resolve(mapType.getValueType(), VarCharType.STRING_TYPE); + + return mapData -> { + InternalArray keyArray = mapData.keyArray(); + InternalArray valueArray = mapData.valueArray(); + int size = mapData.size(); + StringBuilder sb = new StringBuilder(); + sb.append("{"); + for (int i = 0; i < size; i++) { + Object k = keyGetter.getElementOrNull(keyArray, i); + if (k == null) { + sb.append("null"); + } else { + sb.append(keyCastExecutor.cast(k)); + } + sb.append(" -> "); + Object v = valueGetter.getElementOrNull(valueArray, i); + if (v == null) { + sb.append("null"); + } else { + sb.append(valueCastExecutor.cast(v)); + } + if (i != size - 1) { + sb.append(", "); + } + } + sb.append("}"); + return BinaryString.fromString(sb.toString()); + }; + } +} diff --git a/paimon-common/src/main/java/org/apache/paimon/casting/RowToStringCastRule.java b/paimon-common/src/main/java/org/apache/paimon/casting/RowToStringCastRule.java new file mode 100644 index 000000000000..a6d56327bfe2 --- /dev/null +++ b/paimon-common/src/main/java/org/apache/paimon/casting/RowToStringCastRule.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.paimon.casting; + +import org.apache.paimon.data.BinaryString; +import org.apache.paimon.data.InternalRow; +import org.apache.paimon.types.DataType; +import org.apache.paimon.types.DataTypeFamily; +import org.apache.paimon.types.DataTypeRoot; +import org.apache.paimon.types.RowType; +import org.apache.paimon.types.VarCharType; +import org.apache.paimon.utils.InternalRowUtils; + +/** {@link DataTypeRoot#ROW} to {@link DataTypeFamily#CHARACTER_STRING} cast rule. */ +public class RowToStringCastRule extends AbstractCastRule { + + static final RowToStringCastRule INSTANCE = new RowToStringCastRule(); + + private RowToStringCastRule() { + super( + CastRulePredicate.builder() + .input(DataTypeRoot.ROW) + .target(DataTypeFamily.CHARACTER_STRING) + .build()); + } + + @Override + public CastExecutor create(DataType inputType, DataType targetType) { + RowType rowType = (RowType) inputType; + int fieldCount = rowType.getFieldCount(); + InternalRow.FieldGetter[] fieldGetters = new InternalRow.FieldGetter[fieldCount]; + CastExecutor[] castExecutors = new CastExecutor[fieldCount]; + for (int i = 0; i < rowType.getFieldCount(); i++) { + DataType fieldType = rowType.getTypeAt(i); + fieldGetters[i] = InternalRowUtils.createNullCheckingFieldGetter(fieldType, i); + castExecutors[i] = CastExecutors.resolve(fieldType, VarCharType.STRING_TYPE); + } + + return rowDate -> { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + for (int i = 0; i < fieldCount; i++) { + Object field = fieldGetters[i].getFieldOrNull(rowDate); + if (field == null) { + sb.append("null"); + } else { + sb.append(castExecutors[i].cast(field).toString()); + } + if (i != fieldCount - 1) { + sb.append(", "); + } + } + sb.append("}"); + return BinaryString.fromString(sb.toString()); + }; + } +} diff --git a/paimon-common/src/test/java/org/apache/paimon/casting/CastExecutorTest.java b/paimon-common/src/test/java/org/apache/paimon/casting/CastExecutorTest.java index e8c3e98ffd27..cb3e14c536a3 100644 --- a/paimon-common/src/test/java/org/apache/paimon/casting/CastExecutorTest.java +++ b/paimon-common/src/test/java/org/apache/paimon/casting/CastExecutorTest.java @@ -19,17 +19,24 @@ package org.apache.paimon.casting; import org.apache.paimon.data.BinaryString; +import org.apache.paimon.data.GenericArray; +import org.apache.paimon.data.GenericMap; +import org.apache.paimon.data.GenericRow; import org.apache.paimon.data.Timestamp; +import org.apache.paimon.types.ArrayType; import org.apache.paimon.types.BigIntType; import org.apache.paimon.types.BinaryType; import org.apache.paimon.types.BooleanType; import org.apache.paimon.types.CharType; +import org.apache.paimon.types.DataTypes; import org.apache.paimon.types.DateType; import org.apache.paimon.types.DecimalType; import org.apache.paimon.types.DoubleType; import org.apache.paimon.types.FloatType; import org.apache.paimon.types.IntType; import org.apache.paimon.types.LocalZonedTimestampType; +import org.apache.paimon.types.MapType; +import org.apache.paimon.types.RowType; import org.apache.paimon.types.SmallIntType; import org.apache.paimon.types.TimeType; import org.apache.paimon.types.TimestampType; @@ -41,6 +48,8 @@ import org.junit.jupiter.api.Test; +import java.util.HashMap; +import java.util.Map; import java.util.TimeZone; import static org.assertj.core.api.Assertions.assertThat; @@ -681,6 +690,65 @@ public void testTimeToTimestamp() { DateTimeUtils.parseTimestampData("1970-01-01 " + time, 3)); } + @Test + public void testArrayToString() { + ArrayType arrayType = new ArrayType(DataTypes.INT()); + GenericArray genericArray = new GenericArray(new Integer[] {1, null, 2}); + compareCastResult( + CastExecutors.resolve(arrayType, DataTypes.STRING()), + genericArray, + BinaryString.fromString("[1, null, 2]")); + } + + @Test + public void testMapToString() { + MapType mapType = new MapType(DataTypes.INT(), DataTypes.STRING()); + Map javaMap = new HashMap<>(); + javaMap.put(1, BinaryString.fromString("i")); + javaMap.put(2, BinaryString.fromString("miss")); + javaMap.put(3, BinaryString.fromString("you")); + javaMap.put(4, null); + GenericMap genericMap = new GenericMap(javaMap); + compareCastResult( + CastExecutors.resolve(mapType, DataTypes.STRING()), + genericMap, + BinaryString.fromString("{1 -> i, 2 -> miss, 3 -> you, 4 -> null}")); + } + + @Test + public void testRowToString() { + RowType rowType = + DataTypes.ROW( + DataTypes.FIELD(0, "f0", DataTypes.INT()), + DataTypes.FIELD( + 1, + "f1", + DataTypes.ROW( + DataTypes.FIELD(2, "f0", DataTypes.DATE()), + DataTypes.FIELD( + 3, + "f1", + new MapType( + DataTypes.INT(), + new ArrayType(DataTypes.INT()))), + DataTypes.FIELD(4, "f2", DataTypes.INT())))); + + HashMap javaMap = new HashMap<>(); + javaMap.put(1, new GenericArray(new Integer[] {1, null, 2})); + GenericRow row = + GenericRow.of( + 1, + GenericRow.of( + DateTimeUtils.parseDate("2025-01-06"), + new GenericMap(javaMap), + null)); + + compareCastResult( + CastExecutors.resolve(rowType, DataTypes.STRING()), + row, + BinaryString.fromString("{1, {2025-01-06, {1 -> [1, null, 2]}, null}}")); + } + @SuppressWarnings("rawtypes") private void compareCastResult(CastExecutor cast, Object input, Object output) { assertThat(((CastExecutor) cast).cast(input)).isEqualTo(output);