Support for arrays in `startsWith` (and, maybe, `endsWith`)

**Use case**

Would be quite useful for handling tree-like structured data where the node paths are specified as arrays of names or IDs.

**Describe the solution you'd like**

The desired behavior would be like this:
```
startsWith([1, 2, 3, 4], [1, 2])  -- true (1)
startsWith(['qwe', 'rty', 'ui', 'op'], ['qwe', 'rty'])  -- true (1)
startsWith([1, 2, 3, 4], [2, 4])  -- false (0)
startsWith([1, 1, 2, 2], [1, 2])  -- false (0)
```

And similarly, for `endsWith`

**Describe alternatives you've considered**

Something like `arraySlice(first, 1, length(second)) = second`

But `startsWith` would be much more readable, harder to make a mistake in and, probably, can be better optimized (i.e. might not have to create new objects like `arraySlice` does)
{
    const bool is_offset_null = !offset_column || offset_column->onlyNull();
    const ColumnUInt8::Container * offset_null_map = nullptr;
    const IColumn * offset_nested_column = nullptr;
    if (!is_offset_null)
    {
        const auto * offset_nullable = typeid_cast<const ColumnNullable *>(offset_column);
        offset_null_map = offset_nullable ? &offset_nullable->getNullMapData() : nullptr;
        offset_nested_column = offset_nullable ? &offset_nullable->getNestedColumn() : offset_column;
    }
    const bool is_length_null = !length_column || length_column->onlyNull();
    const ColumnUInt8::Container * length_null_map = nullptr;
    const IColumn * length_nested_column = nullptr;
    if (!is_length_null)
    {
        const auto * length_nullable = typeid_cast<const ColumnNullable *>(length_column);
        length_null_map = length_nullable ? &length_nullable->getNullMapData() : nullptr;
        length_nested_column = length_nullable ? &length_nullable->getNestedColumn() : length_column;
    }
    while (!src.isEnd())
    {
        size_t row_num = src.rowNum();
        bool has_offset = !is_offset_null && !(offset_null_map && (*offset_null_map)[row_num]);