Repository: iterative/datachain
Pull request #1107: Add 'slice' and 'join' array functions
Author: login: dreadatour
Created: 2025-05-19T09:22:38Z | Merged: 2025-05-21T04:30:40Z

See also related SaaS PR.


Diff:
diff --git a/src/datachain/func/array.py b/src/datachain/func/array.py
index 1467a96a4..afb2c5156 100644
--- a/src/datachain/func/array.py
+++ b/src/datachain/func/array.py
@@ -178,6 +178,126 @@ def inner(arg):
     return Func("contains", inner=inner, cols=cols, args=args, result_type=int)
 
 
+def slice(
+    arr: Union[str, Sequence, Func],
+    offset: int,
+    length: Optional[int] = None,
+) -> Func:
+    """
+    Returns a slice of the array.
+
+    Args:
+        arr (str | Sequence | Func): Array to check for the element.
+            If a string is provided, it is assumed to be the name of the array column.
+            If a sequence is provided, it is assumed to be an array of values.
+            If a Func is provided, it is assumed to be a function returning an array.
+        offset (int): Offset to start the slice from.
+        length (int, optional): Length of the slice. If not provided, the slice will
+            continue to the end of the array.
+
+    Returns:
+        Func: A Func object that represents the slice function. Result of the
+            function will be a slice of the array starting from the offset
+            and with the given length.
+
+    Example:
+        ```py
+        dc.mutate(
+            contains1=func.array.slice("signal.values", 3),
+            contains2=func.array.slice([1, 2, 3, 4, 5], 1, 3),
+        )
+        ```
+    """
+
+    def inner(arg):
+        if length is not None:
+            return array.slice(arg, offset, length)
+        return array.slice(arg, offset)
+
+    def element_type(el):
+        if isinstance(el, list):
+            try:
+                return list[element_type(el[0])]
+            except IndexError:
+                # if the array is empty, return list[str] as default type
+                return list[str]
+        return type(el)
+
+    def type_from_args(arr, *_):
+        if isinstance(arr, list):
+            try:
+                return list[element_type(arr[0])]
+            except IndexError:
+                # if the array is empty, return list[str] as default type
+                return list[str]
+        return None
+
+    if isinstance(arr, (str, Func)):
+        cols = [arr]
+        args = None
+    else:
+        cols = None
+        args = [arr]
+
+    return Func(
+        "slice",
+        inner=inner,
+        cols=cols,
+        args=args,
+        from_array=True,
+        is_array=True,
+        type_from_args=type_from_args,
+    )
+
+
+def join(
+    arr: Union[str, Sequence, Func],
+    sep: str = "",
+) -> Func:
+    """
+    Returns a string that is the concatenation of the elements of the array,
+
+    Args:
+        arr (str | Sequence | Func): Array to check for the element.
+            If a string is provided, it is assumed to be the name of the array column.
+            If a sequence is provided, it is assumed to be an array of values.
+            If a Func is provided, it is assumed to be a function returning an array.
+        sep (str): Separator to use for the concatenation. Default is an empty string.
+
+    Returns:
+        Func: A Func object that represents the join function. Result of the
+            function will be a string that is the concatenation of the elements
+            of the array, separated by the given separator.
+
+    Example:
+        ```py
+        dc.mutate(
+            contains1=func.array.join("signal.values", ":"),
+            contains2=func.array.join(["1", "2", "3", "4", "5"], "/"),
+        )
+        ```
+    """
+
+    def inner(arg):
+        return array.join(arg, sep)
+
+    if isinstance(arr, (str, Func)):
+        cols = [arr]
+        args = None
+    else:
+        cols = None
+        args = [arr]
+
+    return Func(
+        "join",
+        inner=inner,
+        cols=cols,
+        args=args,
+        from_array=True,
+        result_type=str,
+    )
+
+
 def get_element(arg: Union[str, Sequence, Func], index: int) -> Func:
     """
     Returns the element at the given index from the array.
diff --git a/src/datachain/func/func.py b/src/datachain/func/func.py
index 79968ff17..9f1398d6d 100644
--- a/src/datachain/func/func.py
+++ b/src/datachain/func/func.py
@@ -108,18 +108,20 @@ def _db_col_type(self, signals_schema: "SignalSchema") -> Optional["DataType"]:
                 )
 
         if self.from_array:
-            if get_origin(col_type) is list:
-                col_args = get_args(col_type)
-                if len(col_args) != 1:
-                    raise DataChainColumnError(
-                        str(self),
-                        "Array column must have a single type argument",
-                    )
-                return col_args[0]
-            raise DataChainColumnError(
-                str(self),
-                "Array column must be of type list",
-            )
+            if get_origin(col_type) is not list:
+                raise DataChainColumnError(
+                    str(self),
+                    "Array column must be of type list",
+                )
+            if self.is_array:
+                return col_type
+            col_args = get_args(col_type)
+            if len(col_args) != 1:
+                raise DataChainColumnError(
+                    str(self),
+                    "Array column must have a single type argument",
+                )
+            return col_args[0]
 
         return list[col_type] if self.is_array else col_type  # type: ignore[valid-type]
 
diff --git a/src/datachain/sql/functions/array.py b/src/datachain/sql/functions/array.py
index 321d033b8..43afe2610 100644
--- a/src/datachain/sql/functions/array.py
+++ b/src/datachain/sql/functions/array.py
@@ -1,6 +1,6 @@
 from sqlalchemy.sql.functions import GenericFunction
 
-from datachain.sql.types import Boolean, Float, Int64
+from datachain.sql.types import Boolean, Float, Int64, String
 from datachain.sql.utils import compiler_not_implemented
 
 
@@ -48,6 +48,27 @@ class contains(GenericFunction):  # noqa: N801
     inherit_cache = True
 
 
+class slice(GenericFunction):  # noqa: N801
+    """
+    Returns a slice of the array.
+    """
+
+    package = "array"
+    name = "slice"
+    inherit_cache = True
+
+
+class join(GenericFunction):  # noqa: N801
+    """
+    Returns the concatenation of the array elements.
+    """
+
+    type = String()
+    package = "array"
+    name = "join"
+    inherit_cache = True
+
+
 class get_element(GenericFunction):  # noqa: N801
     """
     Returns the element at the given index in the array.
diff --git a/src/datachain/sql/sqlite/base.py b/src/datachain/sql/sqlite/base.py
index bf01529d9..c978abeea 100644
--- a/src/datachain/sql/sqlite/base.py
+++ b/src/datachain/sql/sqlite/base.py
@@ -88,6 +88,8 @@ def setup():
     compiles(sql_path.file_ext, "sqlite")(compile_path_file_ext)
     compiles(array.length, "sqlite")(compile_array_length)
     compiles(array.contains, "sqlite")(compile_array_contains)
+    compiles(array.slice, "sqlite")(compile_array_slice)
+    compiles(array.join, "sqlite")(compile_array_join)
     compiles(array.get_element, "sqlite")(compile_array_get_element)
     compiles(string.length, "sqlite")(compile_string_length)
     compiles(string.split, "sqlite")(compile_string_split)
@@ -275,6 +277,15 @@ def create_array_functions(conn):
         conn.create_function(
             "json_array_get_element", 2, py_json_array_get_element, deterministic=True
         )
+        conn.create_function(
+            "json_array_slice", 2, py_json_array_slice, deterministic=True
+        )
+        conn.create_function(
+            "json_array_slice", 3, py_json_array_slice, deterministic=True
+        )
+        conn.create_function(
+            "json_array_join", 2, py_json_array_join, deterministic=True
+        )
 
     _registered_function_creators["array_functions"] = create_array_functions
 
@@ -454,6 +465,20 @@ def py_json_array_get_element(val, idx):
         return None
 
 
+def py_json_array_slice(val, offset: int, length: Optional[int] = None):
+    arr = orjson.loads(val)
+    try:
+        return orjson.dumps(
+            list(arr[offset : offset + length] if length is not None else arr[offset:])
+        ).decode("utf-8")
+    except IndexError:
+        return None
+
+
+def py_json_array_join(val, sep: str):
+    return sep.join(orjson.loads(val))
+
+
 def compile_array_get_element(element, compiler, **kwargs):
     return compiler.process(
         func.json_array_get_element(*element.clauses.clauses), **kwargs
@@ -470,6 +495,14 @@ def compile_array_contains(element, compiler, **kwargs):
     )
 
 
+def compile_array_slice(element, compiler, **kwargs):
+    return compiler.process(func.json_array_slice(*element.clauses.clauses), **kwargs)
+
+
+def compile_array_join(element, compiler, **kwargs):
+    return compiler.process(func.json_array_join(*element.clauses.clauses), **kwargs)
+
+
 def compile_string_length(element, compiler, **kwargs):
     return compiler.process(func.length(*element.clauses.clauses), **kwargs)
 
diff --git a/tests/func/test_func.py b/tests/func/test_func.py
index 2c64804dc..86e4d9089 100644
--- a/tests/func/test_func.py
+++ b/tests/func/test_func.py
@@ -17,26 +17,134 @@ def values_almost_equal(a, b):
     return a == b
 
 
-def tuples_almost_equal(t1, t2):
+def tuples_almost_equal(t1, t2, path=""):
     """Compare two tuples, treating NaN floats as equal."""
     if len(t1) != len(t2):
-        return False
-    return all(values_almost_equal(x, y) for x, y in zip(t1, t2))
+        raise AssertionError(
+            f"Tuple length mismatch at {path or 'root'}: {len(t1)} != {len(t2)}\n"
+            f"  Left ({type(t1)}): {t1}\n"
+            f"  Right ({type(t2)}): {t2}"
+        )
+
+    for i, (x, y) in enumerate(zip(t1, t2)):
+        subpath = f"{path}[{i}]"
+        if isinstance(x, tuple) and isinstance(y, tuple):
+            tuples_almost_equal(x, y, path=subpath)
+        elif not values_almost_equal(x, y):
+            raise AssertionError(
+                f"Mismatch at {subpath}:\n"
+                f"  Left ({type(x)}): {x}\n"
+                f"  Right ({type(y)}): {y}"
+            )
+
+
+def test_array_slice(test_session):
+    class Arr(dc.DataModel):
+        i: list[int]
+        f: list[float]
+        s: list[str]
 
+    ds = list(
+        dc.read_values(
+            id=[1, 2, 3],
+            arr=(
+                Arr(i=[10, 20, 30], f=[1.0, 2.0, 3.0], s=["a", "b", "c"]),
+                Arr(i=[40, 50, 60], f=[4.0, 5.0, 6.0], s=["d", "e", "f"]),
+                Arr(i=[50], f=[5.0], s=["g"]),
+            ),
+            session=test_session,
+        )
+        .mutate(
+            t1=func.array.slice("arr.i", 1),
+            t2=func.array.slice("arr.i", 100),
+            t3=func.array.slice("arr.f", 0),
+            t4=func.array.slice("arr.f", 1, 1),
+            t5=func.array.slice("arr.s", 2),
+            t6=func.array.slice("arr.s", 1, 10),
+            t7=func.array.slice([9.0], 0),
+            t8=func.array.slice([17], 5),
+            t9=func.array.slice(["a", "b", "c", "d"], 1, 5),
+            t10=func.array.slice(["a", "b", "c", "d"], 100),
+            t11=func.array.slice([], 0),
+        )
+        .order_by("id")
+        .collect("t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8", "t9", "t10", "t11")
+    )
 
-def sets_of_tuples_almost_equal(s1, s2):
-    """Compare two sets of tuples, treating NaN floats as equal."""
-    if len(s1) != len(s2):
-        return False
-    unmatched = list(s2)
-    for item1 in s1:
-        for item2 in unmatched:
-            if tuples_almost_equal(item1, item2):
-                unmatched.remove(item2)
-                break
-        else:
-            return False
-    return True
+    assert tuple(ds) == (
+        (
+            [20, 30],
+            [],
+            [1.0, 2.0, 3.0],
+            [2.0],
+            ["c"],
+            ["b", "c"],
+            [9.0],
+            [],
+            ["b", "c", "d"],
+            [],
+            [],
+        ),
+        (
+            [50, 60],
+            [],
+            [4.0, 5.0, 6.0],
+            [5.0],
+            ["f"],
+            ["e", "f"],
+            [9.0],
+            [],
+            ["b", "c", "d"],
+            [],
+            [],
+        ),
+        (
+            [],
+            [],
+            [5.0],
+            [],
+            [],
+            [],
+            [9.0],
+            [],
+            ["b", "c", "d"],
+            [],
+            [],
+        ),
+    )
+
+
+def test_array_join(test_session):
+    class Arr(dc.DataModel):
+        s: list[str]
+
+    ds = list(
+        dc.read_values(
+            id=[1, 2, 3],
+            arr=(
+                Arr(s=["a", "b", "c"]),
+                Arr(s=["d"]),
+                Arr(s=[]),
+            ),
+            session=test_session,
+        )
+        .mutate(
+            t1=func.array.join("arr.s", "/"),
+            t2=func.array.join("arr.s", ","),
+            t3=func.array.join("arr.s"),
+            t4=func.array.join(["a", "b", "c", "d"], ":"),
+            t5=func.array.join(["1", "2"], ","),
+            t6=func.array.join([]),
+        )
+        .order_by("id")
+        .collect("t1", "t2", "t3", "t4", "t5", "t6")
+    )
+
+    assert tuple(ds) == (
+        ("a/b/c", "a,b,c", "abc", "a:b:c:d", "1,2", ""),
+        ("d", "d", "d", "a:b:c:d", "1,2", ""),
+        ("", "", "", "a:b:c:d", "1,2", ""),
+    )
 
 
 def test_array_get_element(test_session):
@@ -46,8 +154,9 @@ class Arr(dc.DataModel):
         i: list[int]
         f: list[float]
 
-    ds = (
+    ds = list(
         dc.read_values(
+            id=[1, 2, 3],
             arr=(
                 Arr(i=[10, 20, 30], f=[1.0, 2.0, 3.0]),
                 Arr(i=[40, 50, 60], f=[4.0, 5.0, 6.0]),
@@ -56,34 +165,24 @@ class Arr(dc.DataModel):
             session=test_session,
         )
         .mutate(
-            first_i=func.array.get_element("arr.i", 0),
-            second_i=func.array.get_element("arr.i", 1),
-            unknown_i=func.array.get_element("arr.i", 100),
-            first_f=func.array.get_element("arr.f", 0),
-            second_f=func.array.get_element("arr.f", 1),
-            first_f2=func.array.get_element([9.0], 0),
-            first_s=func.array.get_element(["a", "b", "c", "d"], 0),
-            second_s=func.array.get_element(["a", "b", "c", "d"], 1),
-            unknown_s=func.array.get_element(["a", "b", "c", "d"], 100),
-            unknown=func.array.get_element([], 0),
-        )
-        .collect(
-            "first_i",
-            "second_i",
-            "unknown_i",
-            "first_f",
-            "second_f",
-            "first_f2",
-            "first_s",
-            "second_s",
-            "unknown_s",
-            "unknown",
+            t1=func.array.get_element("arr.i", 0),
+            t2=func.array.get_element("arr.i", 1),
+            t3=func.array.get_element("arr.i", 100),
+            t4=func.array.get_element("arr.f", 0),
+            t5=func.array.get_element("arr.f", 1),
+            t6=func.array.get_element([9.0], 0),
+            t7=func.array.get_element(["a", "b", "c", "d"], 0),
+            t8=func.array.get_element(["a", "b", "c", "d"], 1),
+            t9=func.array.get_element(["a", "b", "c", "d"], 100),
+            t10=func.array.get_element([], 0),
         )
+        .order_by("id")
+        .collect("t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8", "t9", "t10")
     )
 
-    assert sets_of_tuples_almost_equal(
-        set(ds),
-        {
+    tuples_almost_equal(
+        tuple(ds),
+        (
             (
                 10,
                 20,
@@ -120,5 +219,5 @@ class Arr(dc.DataModel):
                 String.default_value(db_dialect),
                 String.default_value(db_dialect),
             ),
-        },
+        ),
     )
diff --git a/tests/unit/sql/test_array.py b/tests/unit/sql/test_array.py
index a81c1f5c9..a47b7c90e 100644
--- a/tests/unit/sql/test_array.py
+++ b/tests/unit/sql/test_array.py
@@ -123,6 +123,48 @@ def test_contains(warehouse):
     assert result == ((1, 0, 1, 1, 1, 1),)
 
 
+def test_slice(warehouse):
+    query = select(
+        func.array.slice(["abc", "def", "g", "hi"], 1).label("slice1"),
+        func.array.slice(["abc", "def", "g", "hi"], 0, 10).label("slice2"),
+        func.array.slice(["abc", "def", "g", "hi"], 0, 2).label("slice3"),
+        func.array.slice([3.0, 5.0, 1.0, 6.0, 1.0], 4).label("slice4"),
+        func.array.slice([[1, None, 3], [4, 5, 6]], 0).label("slice5"),
+        func.array.slice([1, None, 3], 1, 10).label("slice6"),
+        func.array.slice([1, True, 3], 0).label("slice7"),
+    )
+    result = tuple(warehouse.dataset_rows_select(query))
+    assert result == (
+        (
+            ["def", "g", "hi"],
+            ["abc", "def", "g", "hi"],
+            ["abc", "def"],
+            [1.0],
+            [[1, None, 3], [4, 5, 6]],
+            [None, 3],
+            [1, True, 3],
+        ),
+    )
+
+
+def test_join(warehouse):
+    query = select(
+        func.array.join(["abc", "def", "g", "hi"], "/").label("join1"),
+        func.array.join(["abc", "def", "g", "hi"], ",").label("join2"),
+        func.array.join(["abc", "def", "g", "hi"]).label("join3"),
+        func.array.join(["abc"], ":").label("join4"),
+    )
+    result = tuple(warehouse.dataset_rows_select(query))
+    assert result == (
+        (
+            "abc/def/g/hi",
+            "abc,def,g,hi",
+            "abcdefghi",
+            "abc",
+        ),
+    )
+
+
 def test_length_on_split(warehouse):
     query = select(
         func.array.length(func.string.split(func.literal("abc/def/g/hi"), "/")),


Reviews:
user: login: shcheklein
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T02:25:54Z
user: login: shcheklein
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T02:27:34Z
user: login: shcheklein
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T02:28:11Z
user: login: amritghimire
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:39:17Z
user: login: dreadatour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T04:03:58Z
user: login: dreadatour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T04:04:49Z

Review comments:
diff_hunk: @@ -178,6 +178,124 @@ def inner(arg):
     return Func("contains", inner=inner, cols=cols, args=args, result_type=int)
 
 
+def slice(
+    arr: Union[str, Sequence, Func],
+    offset: int,
+    length: Optional[int] = None,
+) -> Func:
+    """
+    Returns a slice of the array.
+
+    Args:
+        arr (str | Sequence | Func): Array to check for the element.
+            If a string is provided, it is assumed to be the name of the array column.
+            If a sequence is provided, it is assumed to be an array of values.
+            If a Func is provided, it is assumed to be a function returning an array.
+        offset (int): Offset to start the slice from.
+        length (int, optional): Length of the slice. If not provided, the slice will
+            continue to the end of the array.
+
+    Returns:
+        Func: A Func object that represents the slice function. Result of the
+            function will be a slice of the array starting from the offset
+            and with the given length.
+
+    Example:
+        ```py
+        dc.mutate(
+            contains1=func.array.slice("signal.values", 3),
path: src/datachain/func/array.py
user: login: shcheklein
body: does it accept `C` syntax (just to double check)
created_at: 2025-05-20T02:25:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -178,6 +178,124 @@ def inner(arg):
     return Func("contains", inner=inner, cols=cols, args=args, result_type=int)
 
 
+def slice(
+    arr: Union[str, Sequence, Func],
+    offset: int,
+    length: Optional[int] = None,
+) -> Func:
+    """
+    Returns a slice of the array.
+
+    Args:
+        arr (str | Sequence | Func): Array to check for the element.
+            If a string is provided, it is assumed to be the name of the array column.
+            If a sequence is provided, it is assumed to be an array of values.
+            If a Func is provided, it is assumed to be a function returning an array.
+        offset (int): Offset to start the slice from.
+        length (int, optional): Length of the slice. If not provided, the slice will
+            continue to the end of the array.
+
+    Returns:
+        Func: A Func object that represents the slice function. Result of the
+            function will be a slice of the array starting from the offset
+            and with the given length.
+
+    Example:
+        ```py
+        dc.mutate(
+            contains1=func.array.slice("signal.values", 3),
+            contains2=func.array.slice([1, 2, 3, 4, 5], 1, 3),
+        )
+        ```
+    """
+
+    def inner(arg):
+        if length is not None:
+            return array.slice(arg, offset, length)
+        return array.slice(arg, offset)
+
+    def element_type(el):
+        if isinstance(el, list):
+            try:
+                return list[element_type(el[0])]
+            except IndexError:
+                return str
+        return type(el)
+
+    def type_from_args(arr, *_):
+        if isinstance(arr, list):
+            try:
+                return list[element_type(arr[0])]
+            except IndexError:
+                return str  # if the array is empty, return str as default type
+        return None
+
+    if isinstance(arr, (str, Func)):
+        cols = [arr]
+        args = None
+    else:
+        cols = None
+        args = [arr]
+
+    return Func(
+        "slice",
+        inner=inner,
+        cols=cols,
+        args=args,
+        from_array=True,
+        is_array=True,
+        type_from_args=type_from_args,
+    )
+
+
+def join(
+    arr: Union[str, Sequence, Func],
+    sep: str,
+) -> Func:
+    """
+    Returns a string that is the concatenation of the elements of the array,
+
+    Args:
+        arr (str | Sequence | Func): Array to check for the element.
+            If a string is provided, it is assumed to be the name of the array column.
+            If a sequence is provided, it is assumed to be an array of values.
+            If a Func is provided, it is assumed to be a function returning an array.
+        sep (str): Separator to use for the concatenation.
+
+    Returns:
+        Func: A Func object that represents the join function. Result of the
+            function will be a string that is the concatenation of the elements
+            of the array, separated by the given separator.
+
+    Example:
+        ```py
+        dc.mutate(
+            contains1=func.array.join("signal.values", ":"),
+            contains2=func.array.join([1, 2, 3, 4, 5], "/"),
path: src/datachain/func/array.py
user: login: shcheklein
body: do we cast elements to str? will it always work fine?
created_at: 2025-05-20T02:28:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -178,6 +178,124 @@ def inner(arg):
     return Func("contains", inner=inner, cols=cols, args=args, result_type=int)
 
 
+def slice(
+    arr: Union[str, Sequence, Func],
+    offset: int,
+    length: Optional[int] = None,
+) -> Func:
+    """
+    Returns a slice of the array.
+
+    Args:
+        arr (str | Sequence | Func): Array to check for the element.
+            If a string is provided, it is assumed to be the name of the array column.
+            If a sequence is provided, it is assumed to be an array of values.
+            If a Func is provided, it is assumed to be a function returning an array.
+        offset (int): Offset to start the slice from.
+        length (int, optional): Length of the slice. If not provided, the slice will
+            continue to the end of the array.
+
+    Returns:
+        Func: A Func object that represents the slice function. Result of the
+            function will be a slice of the array starting from the offset
+            and with the given length.
+
+    Example:
+        ```py
+        dc.mutate(
+            contains1=func.array.slice("signal.values", 3),
+            contains2=func.array.slice([1, 2, 3, 4, 5], 1, 3),
+        )
+        ```
+    """
+
+    def inner(arg):
+        if length is not None:
+            return array.slice(arg, offset, length)
+        return array.slice(arg, offset)
+
+    def element_type(el):
+        if isinstance(el, list):
+            try:
+                return list[element_type(el[0])]
+            except IndexError:
+                return str
+        return type(el)
+
+    def type_from_args(arr, *_):
+        if isinstance(arr, list):
+            try:
+                return list[element_type(arr[0])]
+            except IndexError:
+                return str  # if the array is empty, return str as default type
+        return None
+
+    if isinstance(arr, (str, Func)):
+        cols = [arr]
+        args = None
+    else:
+        cols = None
+        args = [arr]
+
+    return Func(
+        "slice",
+        inner=inner,
+        cols=cols,
+        args=args,
+        from_array=True,
+        is_array=True,
+        type_from_args=type_from_args,
+    )
+
+
+def join(
+    arr: Union[str, Sequence, Func],
+    sep: str,
+) -> Func:
+    """
+    Returns a string that is the concatenation of the elements of the array,
+
+    Args:
+        arr (str | Sequence | Func): Array to check for the element.
+            If a string is provided, it is assumed to be the name of the array column.
+            If a sequence is provided, it is assumed to be an array of values.
+            If a Func is provided, it is assumed to be a function returning an array.
+        sep (str): Separator to use for the concatenation.
+
+    Returns:
+        Func: A Func object that represents the join function. Result of the
+            function will be a string that is the concatenation of the elements
+            of the array, separated by the given separator.
+
+    Example:
+        ```py
+        dc.mutate(
+            contains1=func.array.join("signal.values", ":"),
+            contains2=func.array.join([1, 2, 3, 4, 5], "/"),
path: src/datachain/func/array.py
user: login: dreadatour
body: Fixed the docstrings. Only strings are allowed to be joined.
created_at: 2025-05-21T04:03:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -178,6 +178,124 @@ def inner(arg):
     return Func("contains", inner=inner, cols=cols, args=args, result_type=int)
 
 
+def slice(
+    arr: Union[str, Sequence, Func],
+    offset: int,
+    length: Optional[int] = None,
+) -> Func:
+    """
+    Returns a slice of the array.
+
+    Args:
+        arr (str | Sequence | Func): Array to check for the element.
+            If a string is provided, it is assumed to be the name of the array column.
+            If a sequence is provided, it is assumed to be an array of values.
+            If a Func is provided, it is assumed to be a function returning an array.
+        offset (int): Offset to start the slice from.
+        length (int, optional): Length of the slice. If not provided, the slice will
+            continue to the end of the array.
+
+    Returns:
+        Func: A Func object that represents the slice function. Result of the
+            function will be a slice of the array starting from the offset
+            and with the given length.
+
+    Example:
+        ```py
+        dc.mutate(
+            contains1=func.array.slice("signal.values", 3),
path: src/datachain/func/array.py
user: login: dreadatour
body: Unfortunately no, no `C` syntax allowed in any `dc.func` functions as it can be quite complicated. We can try to add this later if needed.
created_at: 2025-05-21T04:04:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-05-19T09:22:40Z
body: ## Deploying datachain-documentation with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>885bec9</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://5e548cb7.datachain-documentation.pages.dev'>https://5e548cb7.datachain-documentation.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://add-more-array-functions.datachain-documentation.pages.dev'>https://add-more-array-functions.datachain-documentation.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/e471c5ec97d957624fbe991da0a1c119/pages/view/datachain-documentation/5e548cb7-a4c8-4aea-af3a-76d3244f25e9)

author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-19T09:27:33Z
body: ## [Codecov](https://app.codecov.io/gh/iterative/datachain/pull/1107?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative) Report
Attention: Patch coverage is `85.50725%` with `10 lines` in your changes missing coverage. Please review.
> Project coverage is 87.95%. Comparing base [(`8500235`)](https://app.codecov.io/gh/iterative/datachain/commit/8500235648c28804770ef132574e684df82b253c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative) to head [(`885bec9`)](https://app.codecov.io/gh/iterative/datachain/commit/885bec9940ceae54e369f4eb343b35e86f9d3704?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/iterative/datachain/pull/1107?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative) | Patch % | Lines |
|---|---|---|
| [src/datachain/func/array.py](https://app.codecov.io/gh/iterative/datachain/pull/1107?src=pr&el=tree&filepath=src%2Fdatachain%2Ffunc%2Farray.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative#diff-c3JjL2RhdGFjaGFpbi9mdW5jL2FycmF5LnB5) | 88.23% | [3 Missing and 1 partial :warning: ](https://app.codecov.io/gh/iterative/datachain/pull/1107?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative) |
| [src/datachain/func/func.py](https://app.codecov.io/gh/iterative/datachain/pull/1107?src=pr&el=tree&filepath=src%2Fdatachain%2Ffunc%2Ffunc.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative#diff-c3JjL2RhdGFjaGFpbi9mdW5jL2Z1bmMucHk=) | 50.00% | [2 Missing and 2 partials :warning: ](https://app.codecov.io/gh/iterative/datachain/pull/1107?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative) |
| [src/datachain/sql/sqlite/base.py](https://app.codecov.io/gh/iterative/datachain/pull/1107?src=pr&el=tree&filepath=src%2Fdatachain%2Fsql%2Fsqlite%2Fbase.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative#diff-c3JjL2RhdGFjaGFpbi9zcWwvc3FsaXRlL2Jhc2UucHk=) | 88.23% | [2 Missing :warning: ](https://app.codecov.io/gh/iterative/datachain/pull/1107?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1107      +/-   ##
==========================================
+ Coverage   87.94%   87.95%   +0.01%     
==========================================
  Files         148      148              
  Lines       12752    12814      +62     
  Branches     1783     1789       +6     
==========================================
+ Hits        11215    11271      +56     
- Misses       1098     1103       +5     
- Partials      439      440       +1     
```

| [Flag](https://app.codecov.io/gh/iterative/datachain/pull/1107/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative) | Coverage Δ | |
|---|---|---|
| [datachain](https://app.codecov.io/gh/iterative/datachain/pull/1107/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative) | `87.88% <85.50%> (+0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/iterative/datachain/pull/1107?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iterative).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE