Repository: ClickHouse/ClickHouse
File: src/Functions/FunctionsConversion.h (C/C++)

Code before:
        /// From Array to QBit
        if (from_array_type)
        {
            switch (to_type.getElementSize())
            {
                case 16:
                    return createArrayToQBitWrapper<BFloat16>(*from_array_type, to_type);
                case 32:
                    return createArrayToQBitWrapper<Float32>(*from_array_type, to_type);
                case 64:
                    return createArrayToQBitWrapper<Float64>(*from_array_type, to_type);
                default:
                    UNREACHABLE();
            }
        }

        throw Exception(
            ErrorCodes::TYPE_MISMATCH,
            "CAST AS QBit can only be performed from String, Array or another QBit. Left type: {}, right type: {}",
            from_type_untyped->getName(),
            to_type.getName());
    }

    template <typename FloatType>
    static ColumnPtr convertArrayToQBit(
        ColumnsWithTypeAndName & arguments, const DataTypePtr &, const ColumnNullable * /* nullable_source */, size_t n, size_t size)
    {
        using Word = std::conditional_t<sizeof(FloatType) == 2, UInt16, std::conditional_t<sizeof(FloatType) == 4, UInt32, UInt64>>;

        ColumnPtr src_col = arguments.front().column;
        const auto * col_array = checkAndGetColumn<ColumnArray>(src_col.get());

        if (!col_array)
            throw Exception(
                ErrorCodes::LOGICAL_ERROR, "Unexpected column type {} for Array source when converting to QBit", src_col->getName());

        const auto & offsets = col_array->getOffsets();
        const auto & data = typeid_cast<const ColumnVector<FloatType> &>(col_array->getData()).getData();
        const size_t arrays_count = offsets.size();
        const size_t bytes_per_fixedstring = DataTypeQBit::bitsToBytes(n);
        const size_t padded_dimension = bytes_per_fixedstring * 8;

        /// Verify array size matches expected QBit size
        size_t prev_offset = 0;
        for (auto off : offsets)
        {
            size_t array_size = off - prev_offset;
            if (array_size != n)
                throw Exception(
                    ErrorCodes::SIZES_OF_ARRAYS_DONT_MATCH, "Array arguments must have size {} for QBit conversion, got {}", n, array_size);
            prev_offset = off;

Review comment (suggestion):
Yes, the fix is on the right path.

The remaining issue is that we need `nullable_source` to skip rows where we have `NULL`. For example,
```sql
SELECT CAST(
    [NULL::Nullable(Tuple(Array(Float64), String))],
    'Array(Nullable(Tuple(QBit(BFloat16, 3), String)))'
);
```
In PR, now throws with:

```sql
Code: 190. DB::Exception: Array arguments must have size 3 for QBit conversion, got 0: In scope SELECT CAST([CAST(NULL, 'Nullable(Tuple(Array(Float64), String))')], 'Array(Nullable(Tuple(QBit(BFloat16, 3), String)))'). (SIZES_OF_ARRAYS_DONT_MATCH)
```
But it should return `NULL`.

We have to use `nullable_source` to get the null map data and only try to convert array to QBit for non-null rows in `convertArrayToQBit`.

We can also add the following query for proper validation of the logic:

```sql
SELECT
    CAST(v, 'Array(Nullable(Tuple(QBit(BFloat16, 3), String)))') AS converted
FROM VALUES(
    'v Array(Nullable(Tuple(Array(Float64), String)))',
    [([1.0, 2.0, 3.0], 'hello')],
    [NULL]
);
```

It should return:
```response
[([1,2,3],'hello')]
[NULL]
```


Code after:
        /// From Array to QBit
        if (from_array_type)
        {
            switch (to_type.getElementSize())
            {
                case 16:
                    return createArrayToQBitWrapper<BFloat16>(*from_array_type, to_type);
                case 32:
                    return createArrayToQBitWrapper<Float32>(*from_array_type, to_type);
                case 64:
                    return createArrayToQBitWrapper<Float64>(*from_array_type, to_type);
                default:
                    UNREACHABLE();
            }
        }

        throw Exception(
            ErrorCodes::TYPE_MISMATCH,
            "CAST AS QBit can only be performed from String, Array or another QBit. Left type: {}, right type: {}",
            from_type_untyped->getName(),
            to_type.getName());
    }

    template <typename FloatType>
    static ColumnPtr convertArrayToQBit(
        ColumnsWithTypeAndName & arguments, const DataTypePtr &, const ColumnNullable * nullable_source, size_t n, size_t size)
    {
        using Word = std::conditional_t<sizeof(FloatType) == 2, UInt16, std::conditional_t<sizeof(FloatType) == 4, UInt32, UInt64>>;

        ColumnPtr src_col = arguments.front().column;
        const auto * col_array = checkAndGetColumn<ColumnArray>(src_col.get());

        if (!col_array)
            throw Exception(
                ErrorCodes::LOGICAL_ERROR, "Unexpected column type {} for Array source when converting to QBit", src_col->getName());

        const auto & offsets = col_array->getOffsets();
        const auto & data = typeid_cast<const ColumnVector<FloatType> &>(col_array->getData()).getData();
        const size_t arrays_count = offsets.size();
        const size_t bytes_per_fixedstring = DataTypeQBit::bitsToBytes(n);
        const size_t padded_dimension = bytes_per_fixedstring * 8;

        /// Use the null map to skip NULL rows — their nested arrays may have default (empty) values
        /// that don't match the expected dimension, but the result will be masked by NULL anyway.
        const NullMap * null_map = nullable_source ? &nullable_source->getNullMapData() : nullptr;

        /// Verify array size matches expected QBit size (skip NULL rows)
        size_t prev_offset = 0;
        for (size_t row = 0; row < arrays_count; ++row)
        {
            size_t array_size = offsets[row] - prev_offset;