diff --git a/extensions/functions_list.yaml b/extensions/functions_list.yaml index f7776350f..d44b9d2c3 100644 --- a/extensions/functions_list.yaml +++ b/extensions/functions_list.yaml @@ -130,3 +130,40 @@ scalar_functions: value: func boolean?> nullability: DECLARED_OUTPUT return: boolean? + + - name: "has_overlap" + description: >- + Determines whether two lists share any common elements. + + Returns `true` if the two lists have at least one non-null element in common, + `false` if they definitely do not, and the behavior when null elements are + present but no non-null overlap exists is controlled by the `null_handling` + option. + + If either input list is `NULL`, returns `NULL`. + + If either input list is empty, returns `false`. + impls: + - args: + - name: left + value: list + - name: right + value: list + options: + null_handling: + description: >- + Controls how null elements affect the result when no non-null + elements overlap. + + - THREE_VALUED: Returns NULL when null elements are present in + both lists but no non-null overlap is found. + + - IGNORE_NULLS: Skips null elements entirely so the result is + always true or false. + + - NULL_EQUALS_NULL: Treats null elements as equal to each + other, so if both lists contain null that counts as an + overlap. + values: [THREE_VALUED, IGNORE_NULLS, NULL_EQUALS_NULL] + nullability: DECLARED_OUTPUT + return: boolean? diff --git a/extensions/functions_set.yaml b/extensions/functions_set.yaml index 3331b29f0..08495415c 100644 --- a/extensions/functions_set.yaml +++ b/extensions/functions_set.yaml @@ -26,3 +26,4 @@ scalar_functions: values: [ NAN_IS_NAN, NAN_IS_NOT_NAN ] nullability: DECLARED_OUTPUT return: i64? + diff --git a/tests/cases/list/has_overlap.test b/tests/cases/list/has_overlap.test new file mode 100644 index 000000000..6b5964cac --- /dev/null +++ b/tests/cases/list/has_overlap.test @@ -0,0 +1,58 @@ +### SUBSTRAIT_SCALAR_TEST: v1.0 +### SUBSTRAIT_INCLUDE: '/extensions/functions_list.yaml' + +# basic_overlap: Non-null overlap exists +has_overlap([1, 2, 3]::list, [3, 4, 5]::list) [null_handling:THREE_VALUED] = true::bool +has_overlap([1, 2, 3]::list, [3, 4, 5]::list) [null_handling:IGNORE_NULLS] = true::bool +has_overlap([1, 2, 3]::list, [3, 4, 5]::list) [null_handling:NULL_EQUALS_NULL] = true::bool + +# basic_no_overlap: No overlap exists +has_overlap([1, 2, 3]::list, [4, 5, 6]::list) [null_handling:THREE_VALUED] = false::bool +has_overlap([1, 2, 3]::list, [4, 5, 6]::list) [null_handling:IGNORE_NULLS] = false::bool +has_overlap([1, 2, 3]::list, [4, 5, 6]::list) [null_handling:NULL_EQUALS_NULL] = false::bool + +# duplicates: Duplicate elements +has_overlap([1, 1, 2]::list, [1, 3]::list) [null_handling:THREE_VALUED] = true::bool +has_overlap([1, 1, 2]::list, [1, 3]::list) [null_handling:IGNORE_NULLS] = true::bool +has_overlap([1, 1, 2]::list, [1, 3]::list) [null_handling:NULL_EQUALS_NULL] = true::bool + +# empty_right: One list is empty +has_overlap([1, 2, 3]::list, []::list) [null_handling:THREE_VALUED] = false::bool +has_overlap([1, 2, 3]::list, []::list) [null_handling:IGNORE_NULLS] = false::bool +has_overlap([1, 2, 3]::list, []::list) [null_handling:NULL_EQUALS_NULL] = false::bool + +# both_empty: Both lists are empty +has_overlap([]::list, []::list) [null_handling:THREE_VALUED] = false::bool +has_overlap([]::list, []::list) [null_handling:IGNORE_NULLS] = false::bool +has_overlap([]::list, []::list) [null_handling:NULL_EQUALS_NULL] = false::bool + +# null_left_list: Null left list returns null +has_overlap(null::list, [1, 2, 3]::list) [null_handling:THREE_VALUED] = null::bool +has_overlap(null::list, [1, 2, 3]::list) [null_handling:IGNORE_NULLS] = null::bool +has_overlap(null::list, [1, 2, 3]::list) [null_handling:NULL_EQUALS_NULL] = null::bool + +# null_right_list: Null right list returns null +has_overlap([1, 2, 3]::list, null::list) [null_handling:THREE_VALUED] = null::bool +has_overlap([1, 2, 3]::list, null::list) [null_handling:IGNORE_NULLS] = null::bool +has_overlap([1, 2, 3]::list, null::list) [null_handling:NULL_EQUALS_NULL] = null::bool + +# both_null_lists: Both lists null returns null +has_overlap(null::list, null::list) [null_handling:THREE_VALUED] = null::bool +has_overlap(null::list, null::list) [null_handling:IGNORE_NULLS] = null::bool +has_overlap(null::list, null::list) [null_handling:NULL_EQUALS_NULL] = null::bool + +# null_elements_with_non_null_overlap: Null elements present with a non-null overlap +has_overlap([1, null, 3]::list, [3, 4]::list) [null_handling:THREE_VALUED] = true::bool +has_overlap([1, null, 3]::list, [3, 4]::list) [null_handling:IGNORE_NULLS] = true::bool +has_overlap([1, null, 3]::list, [3, 4]::list) [null_handling:NULL_EQUALS_NULL] = true::bool + +# null_elements_no_non_null_overlap: Null elements in one list, no non-null overlap +has_overlap([1, null, 3]::list, [4, 5]::list) [null_handling:THREE_VALUED] = null::bool +has_overlap([1, null, 3]::list, [4, 5]::list) [null_handling:IGNORE_NULLS] = false::bool +has_overlap([1, null, 3]::list, [4, 5]::list) [null_handling:NULL_EQUALS_NULL] = false::bool + +# null_elements_in_both: Null elements in both lists, no non-null overlap +has_overlap([1, null]::list, [null, 4]::list) [null_handling:THREE_VALUED] = null::bool +has_overlap([1, null]::list, [null, 4]::list) [null_handling:IGNORE_NULLS] = false::bool +has_overlap([1, null]::list, [null, 4]::list) [null_handling:NULL_EQUALS_NULL] = true::bool +