From 32319f4cfc7e5db7f3408478005aab7053161369 Mon Sep 17 00:00:00 2001 From: Ben Bellick Date: Mon, 23 Mar 2026 17:37:58 -0400 Subject: [PATCH 1/6] feat(extensions): add element_at list function --- extensions/functions_list.yaml | 45 +++++++++++++++++++++++++++ tests/cases/list/element_at.test | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 tests/cases/list/element_at.test diff --git a/extensions/functions_list.yaml b/extensions/functions_list.yaml index f7776350f..7aa855984 100644 --- a/extensions/functions_list.yaml +++ b/extensions/functions_list.yaml @@ -130,3 +130,48 @@ scalar_functions: value: func boolean?> nullability: DECLARED_OUTPUT return: boolean? + + - name: "element_at" + description: >- + Returns the element at the specified index from the list. + + Index is 1-based (i.e., the first element is at index 1). Zero is not + a valid index. Handling of negative indices depends on the + negative_index_semantics argument. + + If the input list is null, the result is null. If the index is null, + the result is null. If the element at the specified index is null, + the result is null. + impls: + - args: + - name: input + value: list + - name: index + value: i64 + - name: negative_index_semantics + description: >- + How to interpret negative index values. + + * WRAP_AROUND: -1 refers to the last element, -2 to second-to-last, etc. + Negative indices whose absolute value exceeds the list length are still + out of bounds (e.g., -6 on a 5-element list is out of bounds, not wrapped). + + * OUT_OF_BOUNDS: All negative indices are treated as out of bounds. + options: [WRAP_AROUND, OUT_OF_BOUNDS] + options: + on_out_of_bounds: + description: >- + Behavior when the index is out of bounds (index is 0, positive index > list length, + or for WRAP_AROUND negative index whose absolute value > list length, + or for OUT_OF_BOUNDS any negative index). + + * RETURN_NULL: Return null for out-of-bounds indices. + + * ERROR: Raise an error for out-of-bounds indices. + + * ERROR_ON_ZERO_ONLY: Raise an error if the index is 0, but return null for + other out-of-bounds indices. This matches Trino's behavior which treats index 0 + as a programmer error (wrong indexing convention) rather than a simple out-of-bounds. + values: [RETURN_NULL, ERROR, ERROR_ON_ZERO_ONLY] + nullability: DECLARED_OUTPUT + return: any1? diff --git a/tests/cases/list/element_at.test b/tests/cases/list/element_at.test new file mode 100644 index 000000000..b6d944bf9 --- /dev/null +++ b/tests/cases/list/element_at.test @@ -0,0 +1,52 @@ +### SUBSTRAIT_SCALAR_TEST: v1.0 +### SUBSTRAIT_INCLUDE: '/extensions/functions_list.yaml' + +# basic: Basic positive index (1-based) +element_at([10, 20, 30]::list, 1::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 10::i32? +element_at([10, 20, 30]::list, 2::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 20::i32? +element_at([10, 20, 30]::list, 3::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 30::i32? +element_at(['a', 'b', 'c']::list, 2::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 'b'::string? + +# wrap_around: Negative indices count from end with WRAP_AROUND +element_at([10, 20, 30]::list, -1::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 30::i32? +element_at([10, 20, 30]::list, -2::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 20::i32? +element_at([10, 20, 30]::list, -3::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 10::i32? + +# out_of_bounds_negative: Negative indices are out of bounds with OUT_OF_BOUNDS +element_at([10, 20, 30]::list, -1::i64, OUT_OF_BOUNDS::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? +element_at([10, 20, 30]::list, -3::i64, OUT_OF_BOUNDS::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# out_of_bounds_positive: Positive index exceeds list length +element_at([10, 20, 30]::list, 5::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? +element_at([10, 20, 30]::list, 100::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# out_of_bounds_wrap_around: Negative index exceeds list length with WRAP_AROUND +element_at([10, 20, 30]::list, -5::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# zero_out_of_bounds: Zero index is out of bounds +element_at([10, 20, 30]::list, 0::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? +element_at([10, 20, 30]::list, 0::i64, OUT_OF_BOUNDS::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# null_list: Null list returns null +element_at(null::list?, 1::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# null_index: Null index returns null +element_at([10, 20, 30]::list, null::i64?, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# null_element: Retrieving a null element returns null +element_at([1, null, 3]::list, 2::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# empty_list: Out of bounds on empty list +element_at([]::list, 1::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# error_on_out_of_bounds: ERROR option raises error for out-of-bounds indices +element_at([10, 20, 30]::list, 0::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR] = +element_at([10, 20, 30]::list, 5::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR] = +element_at([10, 20, 30]::list, -1::i64, OUT_OF_BOUNDS::enum) [on_out_of_bounds:ERROR] = +element_at([]::list, 1::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR] = + +# error_on_zero_only: ERROR_ON_ZERO_ONLY errors on index 0, returns null otherwise (Trino behavior) +element_at([10, 20, 30]::list, 0::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR_ON_ZERO_ONLY] = +element_at([10, 20, 30]::list, 5::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR_ON_ZERO_ONLY] = null::i32? +element_at([10, 20, 30]::list, -5::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR_ON_ZERO_ONLY] = null::i32? +element_at([10, 20, 30]::list, 2::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR_ON_ZERO_ONLY] = 20::i32? From 455827fb53f4445f57c21b9e4935c40d1d5cda71 Mon Sep 17 00:00:00 2001 From: Ben Bellick Date: Tue, 24 Mar 2026 12:04:00 -0400 Subject: [PATCH 2/6] feat(extensions): rename element_at to list_subscript, simplify to SQL standard semantics --- extensions/functions_list.yaml | 30 ++++------------ tests/cases/list/element_at.test | 52 ---------------------------- tests/cases/list/list_subscript.test | 37 ++++++++++++++++++++ 3 files changed, 44 insertions(+), 75 deletions(-) delete mode 100644 tests/cases/list/element_at.test create mode 100644 tests/cases/list/list_subscript.test diff --git a/extensions/functions_list.yaml b/extensions/functions_list.yaml index 7aa855984..28cf5ccb1 100644 --- a/extensions/functions_list.yaml +++ b/extensions/functions_list.yaml @@ -131,13 +131,13 @@ scalar_functions: nullability: DECLARED_OUTPUT return: boolean? - - name: "element_at" + - name: "list_subscript" description: >- - Returns the element at the specified index from the list. + Returns the element at the specified index from the list, corresponding + to the SQL standard array subscript operator []. - Index is 1-based (i.e., the first element is at index 1). Zero is not - a valid index. Handling of negative indices depends on the - negative_index_semantics argument. + Index is 1-based (i.e., the first element is at index 1). Zero and + negative indices are out of bounds. If the input list is null, the result is null. If the index is null, the result is null. If the element at the specified index is null, @@ -148,30 +148,14 @@ scalar_functions: value: list - name: index value: i64 - - name: negative_index_semantics - description: >- - How to interpret negative index values. - - * WRAP_AROUND: -1 refers to the last element, -2 to second-to-last, etc. - Negative indices whose absolute value exceeds the list length are still - out of bounds (e.g., -6 on a 5-element list is out of bounds, not wrapped). - - * OUT_OF_BOUNDS: All negative indices are treated as out of bounds. - options: [WRAP_AROUND, OUT_OF_BOUNDS] options: on_out_of_bounds: description: >- - Behavior when the index is out of bounds (index is 0, positive index > list length, - or for WRAP_AROUND negative index whose absolute value > list length, - or for OUT_OF_BOUNDS any negative index). + Behavior when the index is out of bounds (index <= 0 or index > list length). * RETURN_NULL: Return null for out-of-bounds indices. * ERROR: Raise an error for out-of-bounds indices. - - * ERROR_ON_ZERO_ONLY: Raise an error if the index is 0, but return null for - other out-of-bounds indices. This matches Trino's behavior which treats index 0 - as a programmer error (wrong indexing convention) rather than a simple out-of-bounds. - values: [RETURN_NULL, ERROR, ERROR_ON_ZERO_ONLY] + values: [RETURN_NULL, ERROR] nullability: DECLARED_OUTPUT return: any1? diff --git a/tests/cases/list/element_at.test b/tests/cases/list/element_at.test deleted file mode 100644 index b6d944bf9..000000000 --- a/tests/cases/list/element_at.test +++ /dev/null @@ -1,52 +0,0 @@ -### SUBSTRAIT_SCALAR_TEST: v1.0 -### SUBSTRAIT_INCLUDE: '/extensions/functions_list.yaml' - -# basic: Basic positive index (1-based) -element_at([10, 20, 30]::list, 1::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 10::i32? -element_at([10, 20, 30]::list, 2::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 20::i32? -element_at([10, 20, 30]::list, 3::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 30::i32? -element_at(['a', 'b', 'c']::list, 2::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 'b'::string? - -# wrap_around: Negative indices count from end with WRAP_AROUND -element_at([10, 20, 30]::list, -1::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 30::i32? -element_at([10, 20, 30]::list, -2::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 20::i32? -element_at([10, 20, 30]::list, -3::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = 10::i32? - -# out_of_bounds_negative: Negative indices are out of bounds with OUT_OF_BOUNDS -element_at([10, 20, 30]::list, -1::i64, OUT_OF_BOUNDS::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? -element_at([10, 20, 30]::list, -3::i64, OUT_OF_BOUNDS::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? - -# out_of_bounds_positive: Positive index exceeds list length -element_at([10, 20, 30]::list, 5::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? -element_at([10, 20, 30]::list, 100::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? - -# out_of_bounds_wrap_around: Negative index exceeds list length with WRAP_AROUND -element_at([10, 20, 30]::list, -5::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? - -# zero_out_of_bounds: Zero index is out of bounds -element_at([10, 20, 30]::list, 0::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? -element_at([10, 20, 30]::list, 0::i64, OUT_OF_BOUNDS::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? - -# null_list: Null list returns null -element_at(null::list?, 1::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? - -# null_index: Null index returns null -element_at([10, 20, 30]::list, null::i64?, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? - -# null_element: Retrieving a null element returns null -element_at([1, null, 3]::list, 2::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? - -# empty_list: Out of bounds on empty list -element_at([]::list, 1::i64, WRAP_AROUND::enum) [on_out_of_bounds:RETURN_NULL] = null::i32? - -# error_on_out_of_bounds: ERROR option raises error for out-of-bounds indices -element_at([10, 20, 30]::list, 0::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR] = -element_at([10, 20, 30]::list, 5::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR] = -element_at([10, 20, 30]::list, -1::i64, OUT_OF_BOUNDS::enum) [on_out_of_bounds:ERROR] = -element_at([]::list, 1::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR] = - -# error_on_zero_only: ERROR_ON_ZERO_ONLY errors on index 0, returns null otherwise (Trino behavior) -element_at([10, 20, 30]::list, 0::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR_ON_ZERO_ONLY] = -element_at([10, 20, 30]::list, 5::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR_ON_ZERO_ONLY] = null::i32? -element_at([10, 20, 30]::list, -5::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR_ON_ZERO_ONLY] = null::i32? -element_at([10, 20, 30]::list, 2::i64, WRAP_AROUND::enum) [on_out_of_bounds:ERROR_ON_ZERO_ONLY] = 20::i32? diff --git a/tests/cases/list/list_subscript.test b/tests/cases/list/list_subscript.test new file mode 100644 index 000000000..5aac0d9fd --- /dev/null +++ b/tests/cases/list/list_subscript.test @@ -0,0 +1,37 @@ +### SUBSTRAIT_SCALAR_TEST: v1.0 +### SUBSTRAIT_INCLUDE: '/extensions/functions_list.yaml' + +# basic: Basic positive index (1-based) +list_subscript([10, 20, 30]::list, 1::i64) [on_out_of_bounds:RETURN_NULL] = 10::i32? +list_subscript([10, 20, 30]::list, 2::i64) [on_out_of_bounds:RETURN_NULL] = 20::i32? +list_subscript([10, 20, 30]::list, 3::i64) [on_out_of_bounds:RETURN_NULL] = 30::i32? +list_subscript(['a', 'b', 'c']::list, 2::i64) [on_out_of_bounds:RETURN_NULL] = 'b'::string? + +# out_of_bounds_positive: Positive index exceeds list length +list_subscript([10, 20, 30]::list, 5::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? +list_subscript([10, 20, 30]::list, 100::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# zero_out_of_bounds: Zero index is out of bounds +list_subscript([10, 20, 30]::list, 0::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# negative_out_of_bounds: Negative indices are out of bounds +list_subscript([10, 20, 30]::list, -1::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? +list_subscript([10, 20, 30]::list, -3::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# null_list: Null list returns null +list_subscript(null::list?, 1::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# null_index: Null index returns null +list_subscript([10, 20, 30]::list, null::i64?) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# null_element: Retrieving a null element returns null +list_subscript([1, null, 3]::list, 2::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# empty_list: Out of bounds on empty list +list_subscript([]::list, 1::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? + +# error_on_out_of_bounds: ERROR option raises error for out-of-bounds indices +list_subscript([10, 20, 30]::list, 0::i64) [on_out_of_bounds:ERROR] = +list_subscript([10, 20, 30]::list, 5::i64) [on_out_of_bounds:ERROR] = +list_subscript([10, 20, 30]::list, -1::i64) [on_out_of_bounds:ERROR] = +list_subscript([]::list, 1::i64) [on_out_of_bounds:ERROR] = From ae447d8f42c55dcd50adad52a165e02887301297 Mon Sep 17 00:00:00 2001 From: Ben Bellick Date: Tue, 24 Mar 2026 12:20:22 -0400 Subject: [PATCH 3/6] fix: remove SQL standard claim from list_subscript description --- extensions/functions_list.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/functions_list.yaml b/extensions/functions_list.yaml index 28cf5ccb1..7311da9bc 100644 --- a/extensions/functions_list.yaml +++ b/extensions/functions_list.yaml @@ -134,7 +134,7 @@ scalar_functions: - name: "list_subscript" description: >- Returns the element at the specified index from the list, corresponding - to the SQL standard array subscript operator []. + to the common array subscript operator []. Index is 1-based (i.e., the first element is at index 1). Zero and negative indices are out of bounds. From abeae6b41ade3ea180947b3d76ecc4dc828ee362 Mon Sep 17 00:00:00 2001 From: Ben Bellick Date: Tue, 24 Mar 2026 12:21:52 -0400 Subject: [PATCH 4/6] fix: simplify list_subscript to always return null on out-of-bounds --- .history | 7 ++++++ extensions/functions_list.yaml | 17 ++------------- tests/cases/list/list_subscript.test | 32 +++++++++++----------------- 3 files changed, 22 insertions(+), 34 deletions(-) create mode 100644 .history diff --git a/.history b/.history new file mode 100644 index 000000000..35db52747 --- /dev/null +++ b/.history @@ -0,0 +1,7 @@ +#V2 +SELECT any_value(t) FROM VALUES((1)) as t; +SELECT last_value(t) FROM VALUES((1)) as t; +SELECT last_value(t) \nFROM (VALUES (1)) as v(t); +SELECT any_value(t) \nFROM (VALUES (1)) as v(t); +SELECT arbitrary(t) \nFROM (VALUES (1)) as v(t); +SELECT last_value(v.t) \nFROM (VALUES (1)) as v(t); diff --git a/extensions/functions_list.yaml b/extensions/functions_list.yaml index 7311da9bc..190e55499 100644 --- a/extensions/functions_list.yaml +++ b/extensions/functions_list.yaml @@ -136,26 +136,13 @@ scalar_functions: Returns the element at the specified index from the list, corresponding to the common array subscript operator []. - Index is 1-based (i.e., the first element is at index 1). Zero and - negative indices are out of bounds. - - If the input list is null, the result is null. If the index is null, - the result is null. If the element at the specified index is null, - the result is null. + Index is 1-based (i.e., the first element is at index 1). Out-of-bounds + access (index <= 0 or index > list length) returns null. impls: - args: - name: input value: list - name: index value: i64 - options: - on_out_of_bounds: - description: >- - Behavior when the index is out of bounds (index <= 0 or index > list length). - - * RETURN_NULL: Return null for out-of-bounds indices. - - * ERROR: Raise an error for out-of-bounds indices. - values: [RETURN_NULL, ERROR] nullability: DECLARED_OUTPUT return: any1? diff --git a/tests/cases/list/list_subscript.test b/tests/cases/list/list_subscript.test index 5aac0d9fd..69444c76b 100644 --- a/tests/cases/list/list_subscript.test +++ b/tests/cases/list/list_subscript.test @@ -2,36 +2,30 @@ ### SUBSTRAIT_INCLUDE: '/extensions/functions_list.yaml' # basic: Basic positive index (1-based) -list_subscript([10, 20, 30]::list, 1::i64) [on_out_of_bounds:RETURN_NULL] = 10::i32? -list_subscript([10, 20, 30]::list, 2::i64) [on_out_of_bounds:RETURN_NULL] = 20::i32? -list_subscript([10, 20, 30]::list, 3::i64) [on_out_of_bounds:RETURN_NULL] = 30::i32? -list_subscript(['a', 'b', 'c']::list, 2::i64) [on_out_of_bounds:RETURN_NULL] = 'b'::string? +list_subscript([10, 20, 30]::list, 1::i64) = 10::i32? +list_subscript([10, 20, 30]::list, 2::i64) = 20::i32? +list_subscript([10, 20, 30]::list, 3::i64) = 30::i32? +list_subscript(['a', 'b', 'c']::list, 2::i64) = 'b'::string? # out_of_bounds_positive: Positive index exceeds list length -list_subscript([10, 20, 30]::list, 5::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? -list_subscript([10, 20, 30]::list, 100::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? +list_subscript([10, 20, 30]::list, 5::i64) = null::i32? +list_subscript([10, 20, 30]::list, 100::i64) = null::i32? # zero_out_of_bounds: Zero index is out of bounds -list_subscript([10, 20, 30]::list, 0::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? +list_subscript([10, 20, 30]::list, 0::i64) = null::i32? # negative_out_of_bounds: Negative indices are out of bounds -list_subscript([10, 20, 30]::list, -1::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? -list_subscript([10, 20, 30]::list, -3::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? +list_subscript([10, 20, 30]::list, -1::i64) = null::i32? +list_subscript([10, 20, 30]::list, -3::i64) = null::i32? # null_list: Null list returns null -list_subscript(null::list?, 1::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? +list_subscript(null::list?, 1::i64) = null::i32? # null_index: Null index returns null -list_subscript([10, 20, 30]::list, null::i64?) [on_out_of_bounds:RETURN_NULL] = null::i32? +list_subscript([10, 20, 30]::list, null::i64?) = null::i32? # null_element: Retrieving a null element returns null -list_subscript([1, null, 3]::list, 2::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? +list_subscript([1, null, 3]::list, 2::i64) = null::i32? # empty_list: Out of bounds on empty list -list_subscript([]::list, 1::i64) [on_out_of_bounds:RETURN_NULL] = null::i32? - -# error_on_out_of_bounds: ERROR option raises error for out-of-bounds indices -list_subscript([10, 20, 30]::list, 0::i64) [on_out_of_bounds:ERROR] = -list_subscript([10, 20, 30]::list, 5::i64) [on_out_of_bounds:ERROR] = -list_subscript([10, 20, 30]::list, -1::i64) [on_out_of_bounds:ERROR] = -list_subscript([]::list, 1::i64) [on_out_of_bounds:ERROR] = +list_subscript([]::list, 1::i64) = null::i32? From 2d840b57ecc4d67e3118872b08120e98effeb89b Mon Sep 17 00:00:00 2001 From: Ben Bellick Date: Tue, 24 Mar 2026 12:24:58 -0400 Subject: [PATCH 5/6] refactor: rename list_subscript to subscript_operator --- .history | 7 ------ extensions/functions_list.yaml | 2 +- tests/cases/list/list_subscript.test | 31 ------------------------ tests/cases/list/subscript_operator.test | 31 ++++++++++++++++++++++++ 4 files changed, 32 insertions(+), 39 deletions(-) delete mode 100644 .history delete mode 100644 tests/cases/list/list_subscript.test create mode 100644 tests/cases/list/subscript_operator.test diff --git a/.history b/.history deleted file mode 100644 index 35db52747..000000000 --- a/.history +++ /dev/null @@ -1,7 +0,0 @@ -#V2 -SELECT any_value(t) FROM VALUES((1)) as t; -SELECT last_value(t) FROM VALUES((1)) as t; -SELECT last_value(t) \nFROM (VALUES (1)) as v(t); -SELECT any_value(t) \nFROM (VALUES (1)) as v(t); -SELECT arbitrary(t) \nFROM (VALUES (1)) as v(t); -SELECT last_value(v.t) \nFROM (VALUES (1)) as v(t); diff --git a/extensions/functions_list.yaml b/extensions/functions_list.yaml index 190e55499..8318f2603 100644 --- a/extensions/functions_list.yaml +++ b/extensions/functions_list.yaml @@ -131,7 +131,7 @@ scalar_functions: nullability: DECLARED_OUTPUT return: boolean? - - name: "list_subscript" + - name: "subscript_operator" description: >- Returns the element at the specified index from the list, corresponding to the common array subscript operator []. diff --git a/tests/cases/list/list_subscript.test b/tests/cases/list/list_subscript.test deleted file mode 100644 index 69444c76b..000000000 --- a/tests/cases/list/list_subscript.test +++ /dev/null @@ -1,31 +0,0 @@ -### SUBSTRAIT_SCALAR_TEST: v1.0 -### SUBSTRAIT_INCLUDE: '/extensions/functions_list.yaml' - -# basic: Basic positive index (1-based) -list_subscript([10, 20, 30]::list, 1::i64) = 10::i32? -list_subscript([10, 20, 30]::list, 2::i64) = 20::i32? -list_subscript([10, 20, 30]::list, 3::i64) = 30::i32? -list_subscript(['a', 'b', 'c']::list, 2::i64) = 'b'::string? - -# out_of_bounds_positive: Positive index exceeds list length -list_subscript([10, 20, 30]::list, 5::i64) = null::i32? -list_subscript([10, 20, 30]::list, 100::i64) = null::i32? - -# zero_out_of_bounds: Zero index is out of bounds -list_subscript([10, 20, 30]::list, 0::i64) = null::i32? - -# negative_out_of_bounds: Negative indices are out of bounds -list_subscript([10, 20, 30]::list, -1::i64) = null::i32? -list_subscript([10, 20, 30]::list, -3::i64) = null::i32? - -# null_list: Null list returns null -list_subscript(null::list?, 1::i64) = null::i32? - -# null_index: Null index returns null -list_subscript([10, 20, 30]::list, null::i64?) = null::i32? - -# null_element: Retrieving a null element returns null -list_subscript([1, null, 3]::list, 2::i64) = null::i32? - -# empty_list: Out of bounds on empty list -list_subscript([]::list, 1::i64) = null::i32? diff --git a/tests/cases/list/subscript_operator.test b/tests/cases/list/subscript_operator.test new file mode 100644 index 000000000..ad41f11d4 --- /dev/null +++ b/tests/cases/list/subscript_operator.test @@ -0,0 +1,31 @@ +### SUBSTRAIT_SCALAR_TEST: v1.0 +### SUBSTRAIT_INCLUDE: '/extensions/functions_list.yaml' + +# basic: Basic positive index (1-based) +subscript_operator([10, 20, 30]::list, 1::i64) = 10::i32? +subscript_operator([10, 20, 30]::list, 2::i64) = 20::i32? +subscript_operator([10, 20, 30]::list, 3::i64) = 30::i32? +subscript_operator(['a', 'b', 'c']::list, 2::i64) = 'b'::string? + +# out_of_bounds_positive: Positive index exceeds list length +subscript_operator([10, 20, 30]::list, 5::i64) = null::i32? +subscript_operator([10, 20, 30]::list, 100::i64) = null::i32? + +# zero_out_of_bounds: Zero index is out of bounds +subscript_operator([10, 20, 30]::list, 0::i64) = null::i32? + +# negative_out_of_bounds: Negative indices are out of bounds +subscript_operator([10, 20, 30]::list, -1::i64) = null::i32? +subscript_operator([10, 20, 30]::list, -3::i64) = null::i32? + +# null_list: Null list returns null +subscript_operator(null::list?, 1::i64) = null::i32? + +# null_index: Null index returns null +subscript_operator([10, 20, 30]::list, null::i64?) = null::i32? + +# null_element: Retrieving a null element returns null +subscript_operator([1, null, 3]::list, 2::i64) = null::i32? + +# empty_list: Out of bounds on empty list +subscript_operator([]::list, 1::i64) = null::i32? From f09db46f4fb681a1940f52f83fb4f8746286cab6 Mon Sep 17 00:00:00 2001 From: Ben Bellick Date: Tue, 24 Mar 2026 15:16:29 -0400 Subject: [PATCH 6/6] feat(extensions): add index_of function --- extensions/functions_list.yaml | 13 +++++++++++++ tests/cases/list/index_of.test | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 tests/cases/list/index_of.test diff --git a/extensions/functions_list.yaml b/extensions/functions_list.yaml index 8318f2603..9dcf5ed12 100644 --- a/extensions/functions_list.yaml +++ b/extensions/functions_list.yaml @@ -146,3 +146,16 @@ scalar_functions: value: i64 nullability: DECLARED_OUTPUT return: any1? + + - name: "index_of" + description: >- + Returns the 1-based index of the first occurrence of the specified value + in the list, or null if the value is not found. + impls: + - args: + - name: input + value: list + - name: value + value: any1 + nullability: DECLARED_OUTPUT + return: i64? diff --git a/tests/cases/list/index_of.test b/tests/cases/list/index_of.test new file mode 100644 index 000000000..3adbf60e2 --- /dev/null +++ b/tests/cases/list/index_of.test @@ -0,0 +1,27 @@ +### SUBSTRAIT_SCALAR_TEST: v1.0 +### SUBSTRAIT_INCLUDE: '/extensions/functions_list.yaml' + +# basic: Find element in list (1-based index) +index_of([10, 20, 30]::list, 10::i32) = 1::i64? +index_of([10, 20, 30]::list, 20::i32) = 2::i64? +index_of([10, 20, 30]::list, 30::i32) = 3::i64? +index_of(['a', 'b', 'c']::list, 'b'::string) = 2::i64? + +# not_found: Element not in list returns null +index_of([10, 20, 30]::list, 5::i32) = null::i64? +index_of([10, 20, 30]::list, 100::i32) = null::i64? + +# first_match: Returns index of first occurrence +index_of([10, 20, 20, 30]::list, 20::i32) = 2::i64? + +# empty_list: Empty list returns null +index_of([]::list, 1::i32) = null::i64? + +# null_list: Null list returns null +index_of(null::list?, 1::i32) = null::i64? + +# null_value: Searching for null returns null +index_of([10, 20, 30]::list, null::i32?) = null::i64? + +# find_null_element: Finding null in list with nulls +index_of([1, null, 3]::list, null::i32?) = 2::i64?