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
3 changes: 3 additions & 0 deletions docs/data/sql_functions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,9 @@ collection:
- sql: ARRAY_CONCAT(array1, ...)
table: array1.arrayConcat(...)
description: Returns an array that is the result of concatenating at least one array. This array contains all the elements in the first array, followed by all the elements in the second array, and so forth, up to the Nth array. If any input array is NULL, the function returns NULL.
- sql: ARRAY_FLATTEN(array)
table: array.arrayFlatten()
description: Returns an array that is the result of flattening a nested array by one level. The function takes an ARRAY<ARRAY<T>> and returns an ARRAY<T> by concatenating all inner arrays. NULL inner arrays are skipped, but NULL elements within inner arrays are preserved. If the input array itself is NULL, the function returns NULL.
- sql: ARRAY_EXCEPT(array1, array2)
table: arrayOne.arrayExcept(arrayTwo)
description: Returns an ARRAY that contains the elements from array1 that are not in array2, without duplicates. If no elements remain after excluding the elements in array2 from array1, the function returns an empty ARRAY. If one or both arguments are NULL, the function returns NULL. The order of the elements from array1 is kept.
Expand Down
3 changes: 3 additions & 0 deletions docs/data/sql_functions_zh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,9 @@ collection:
- sql: ARRAY_CONCAT(array1, ...)
table: array1.arrayConcat(...)
description: 返回一个数组,该数组是连接至少一个数组的结果。该数组包含第一个数组中的所有元素,然后是第二个数组中的所有元素,依此类推,直到第 N 个数组。如果任何输入数组为 NULL,则函数返回 NULL。
- sql: ARRAY_FLATTEN(array)
table: array.arrayFlatten()
description: 返回一个数组,该数组是将嵌套数组展平一层的结果。该函数接受一个 ARRAY<ARRAY<T>> 类型的输入,通过连接所有内层数组返回一个 ARRAY<T> 类型的结果。NULL 内层数组会被跳过,但内层数组中的 NULL 元素会被保留。如果输入数组本身为 NULL,则函数返回 NULL。
- sql: ARRAY_EXCEPT(array1, array2)
table: arrayOne.arrayExcept(arrayTwo)
description: Returns an ARRAY that contains the elements from array1 that are not in array2, without duplicates. If no elements remain after excluding the elements in array2 from array1, the function returns an empty ARRAY. If one or both arguments are NULL, the function returns NULL. The order of the elements from array1 is kept.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,16 @@ ANY, and(logical(LogicalTypeRoot.BOOLEAN), LITERAL)
"org.apache.flink.table.runtime.functions.scalar.ArrayConcatFunction")
.build();

public static final BuiltInFunctionDefinition ARRAY_FLATTEN =
Comment thread
cytq-123 marked this conversation as resolved.
BuiltInFunctionDefinition.newBuilder()
.name("ARRAY_FLATTEN")
.kind(SCALAR)
.inputTypeStrategy(sequence(logical(LogicalTypeRoot.ARRAY)))
.outputTypeStrategy(nullableIfArgs(SpecificTypeStrategies.ARRAY_FLATTEN))
.runtimeClass(
"org.apache.flink.table.runtime.functions.scalar.ArrayFlattenFunction")
.build();

public static final BuiltInFunctionDefinition ARRAY_MAX =
BuiltInFunctionDefinition.newBuilder()
.name("ARRAY_MAX")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
import org.apache.flink.table.types.KeyValueDataType;
import org.apache.flink.table.types.inference.TypeStrategies;
import org.apache.flink.table.types.inference.TypeStrategy;
import org.apache.flink.table.types.logical.ArrayType;
import org.apache.flink.table.types.logical.LogicalType;
import org.apache.flink.table.types.logical.LogicalTypeRoot;
import org.apache.flink.table.types.utils.TypeConversions;

import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -67,6 +70,30 @@ public final class SpecificTypeStrategies {
/** Type strategy specific for array element. */
public static final TypeStrategy ARRAY_ELEMENT = new ArrayElementTypeStrategy();

/** Type strategy specific for {@link BuiltInFunctionDefinitions#ARRAY_FLATTEN}. */
public static final TypeStrategy ARRAY_FLATTEN =
callContext -> {
// Input type is ARRAY<ARRAY<T>>
final LogicalType inputType =
callContext.getArgumentDataTypes().get(0).getLogicalType();

if (!(inputType instanceof ArrayType)
|| !(((ArrayType) inputType).getElementType() instanceof ArrayType)) {
return callContext.fail(
true, "ARRAY_FLATTEN expects an argument of type ARRAY<ARRAY<T>>.");
}

final ArrayType outerArrayType = (ArrayType) inputType;
final ArrayType innerArrayType = (ArrayType) outerArrayType.getElementType();

// Output type is ARRAY<T> where T is the element type of the inner array
return Optional.of(
DataTypes.ARRAY(
TypeConversions.fromLogicalToDataType(
innerArrayType.getElementType()))
.nullable());
};

public static final TypeStrategy ITEM_AT = new ItemAtTypeStrategy();

/** See {@link ArrayAppendPrependTypeStrategy}. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Stream<TestSetSpec> getTestSetSpecs() {
arrayReverseTestCases(),
arrayUnionTestCases(),
arrayConcatTestCases(),
arrayFlattenTestCases(),
arrayMaxTestCases(),
arrayJoinTestCases(),
arraySliceTestCases(),
Expand Down Expand Up @@ -1900,4 +1901,68 @@ private Stream<TestSetSpec> arrayElementTestCases() {
.testResult($("f2").element(), "ELEMENT(f2)", 4.0F, DataTypes.FLOAT())
.testResult($("f3").element(), "ELEMENT(f3)", null, DataTypes.INT()));
}

private Stream<TestSetSpec> arrayFlattenTestCases() {
return Stream.of(
TestSetSpec.forFunction(BuiltInFunctionDefinitions.ARRAY_FLATTEN)
Comment thread
cytq-123 marked this conversation as resolved.
.onFieldsWithData(
new Integer[][] {new Integer[] {1, 2}, new Integer[] {3, 4}},
new String[][] {new String[] {"a", "b"}, new String[] {"c"}},
null,
new Integer[][] {new Integer[] {1, 2}, null, new Integer[] {3}},
new Integer[][] {new Integer[] {1, null, 2}, new Integer[] {3}},
new Integer[][] {new Integer[] {1}},
new Integer[] {1, 2, 3})
.andDataTypes(
DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.INT())),
DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.STRING())),
DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.INT())),
DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.INT()).nullable()),
DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.INT().nullable())),
DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.INT())),
DataTypes.ARRAY(DataTypes.INT()))
// Basic flattening
.testResult(
call("ARRAY_FLATTEN", $("f0")),
"ARRAY_FLATTEN(f0)",
new Integer[] {1, 2, 3, 4},
DataTypes.ARRAY(DataTypes.INT()))
// String arrays
.testResult(
call("ARRAY_FLATTEN", $("f1")),
"ARRAY_FLATTEN(f1)",
new String[] {"a", "b", "c"},
DataTypes.ARRAY(DataTypes.STRING()))
// NULL input
.testResult(
call("ARRAY_FLATTEN", $("f2")),
"ARRAY_FLATTEN(f2)",
null,
DataTypes.ARRAY(DataTypes.INT()).nullable())
// NULL inner arrays - should be skipped
.testResult(
call("ARRAY_FLATTEN", $("f3")),
"ARRAY_FLATTEN(f3)",
new Integer[] {1, 2, 3},
DataTypes.ARRAY(DataTypes.INT()))
// NULL elements - should be preserved
.testResult(
call("ARRAY_FLATTEN", $("f4")),
"ARRAY_FLATTEN(f4)",
new Integer[] {1, null, 2, 3},
DataTypes.ARRAY(DataTypes.INT().nullable()))
// Single element
.testResult(
call("ARRAY_FLATTEN", $("f5")),
"ARRAY_FLATTEN(f5)",
new Integer[] {1},
DataTypes.ARRAY(DataTypes.INT()))
// Error case: one-dimensional array (should reject non-nested array)
.testSqlValidationError(
"ARRAY_FLATTEN(f6)",
"ARRAY_FLATTEN expects an argument of type ARRAY<ARRAY<T>>.")
.testTableApiValidationError(
call("ARRAY_FLATTEN", $("f6")),
"ARRAY_FLATTEN expects an argument of type ARRAY<ARRAY<T>>."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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.flink.table.runtime.functions.scalar;

import org.apache.flink.annotation.Internal;
import org.apache.flink.table.data.ArrayData;
import org.apache.flink.table.data.GenericArrayData;
import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
import org.apache.flink.table.functions.SpecializedFunction;
import org.apache.flink.table.types.CollectionDataType;
import org.apache.flink.table.types.DataType;
import org.apache.flink.util.FlinkRuntimeException;

import javax.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;

/**
* Implementation of {@link BuiltInFunctionDefinitions#ARRAY_FLATTEN}.
*
* <p>Flattens a nested array by one level.
*
* <p>NULL handling:
*
* <ul>
* <li>If the input array is NULL, returns NULL
* <li>NULL inner arrays are skipped
* <li>NULL elements within arrays are preserved
* </ul>
*/
@Internal
public class ArrayFlattenFunction extends BuiltInScalarFunction {
private final ArrayData.ElementGetter outerElementGetter;
private final ArrayData.ElementGetter innerElementGetter;

public ArrayFlattenFunction(SpecializedFunction.SpecializedContext context) {
super(BuiltInFunctionDefinitions.ARRAY_FLATTEN, context);

// Get the input data type (ARRAY<ARRAY<T>>)
final DataType inputDataType = context.getCallContext().getArgumentDataTypes().get(0);
// Get the inner array type (ARRAY<T>)
final DataType innerArrayDataType =
((CollectionDataType) inputDataType).getElementDataType();
// Get the element type (T)
final DataType elementDataType =
((CollectionDataType) innerArrayDataType).getElementDataType();

// Create element getters
// Outer getter retrieves inner arrays from the outer array
outerElementGetter = ArrayData.createElementGetter(innerArrayDataType.getLogicalType());
// Inner getter retrieves elements from inner arrays
innerElementGetter = ArrayData.createElementGetter(elementDataType.getLogicalType());
}

/**
* Flattens a nested array by one level.
*
* @param array the input array of arrays
* @return the flattened array, or NULL if input is NULL
*/
public @Nullable ArrayData eval(ArrayData array) {
if (array == null) {
return null;
}

try {
List<Object> result = new ArrayList<>();

// Iterate through outer array
for (int i = 0; i < array.size(); i++) {
ArrayData innerArray = (ArrayData) outerElementGetter.getElementOrNull(array, i);

if (innerArray == null) {
// Skip NULL inner arrays
continue;
}

// Iterate through inner array and add all elements (including NULL)
for (int j = 0; j < innerArray.size(); j++) {
Object element = innerElementGetter.getElementOrNull(innerArray, j);
result.add(element); // Preserve NULL elements
}
}

return new GenericArrayData(result.toArray());
} catch (Throwable t) {
throw new FlinkRuntimeException(t);
}
}
}