From ddebbdca1f420747c464af2c75067383c2b3e2dc Mon Sep 17 00:00:00 2001 From: ShryukGrandhi <85529990+ShryukGrandhi@users.noreply.github.com> Date: Tue, 20 Oct 2026 02:50:48 -0700 Subject: [PATCH] Split buffers by target type capacity when cloning primitive columns The CloneAs*Container methods mapped each source buffer to one target buffer, which fails when the target element type is larger than the source (e.g. a full int buffer cannot fit in a single double buffer) and leaves non-full buffers when it is smaller, breaking row index lookups. Fixes #7381 --- .../PrimitiveColumnContainer.cs | 248 +++++------------- .../PrimitiveDataFrameColumnTests.cs | 35 +++ 2 files changed, 101 insertions(+), 182 deletions(-) diff --git a/src/Microsoft.Data.Analysis/PrimitiveColumnContainer.cs b/src/Microsoft.Data.Analysis/PrimitiveColumnContainer.cs index 7c52032785..6e83420205 100644 --- a/src/Microsoft.Data.Analysis/PrimitiveColumnContainer.cs +++ b/src/Microsoft.Data.Analysis/PrimitiveColumnContainer.cs @@ -543,239 +543,123 @@ public PrimitiveColumnContainer Clone() return ret; } - internal PrimitiveColumnContainer CloneAsBoolContainer() + // The values of the source buffers are redistributed so that every buffer of the result (except the last one) + // holds exactly ReadOnlyDataFrameBuffer.MaxCapacity values, as sizeof(U) may differ from sizeof(T) + private PrimitiveColumnContainer CloneAs(Func convert) + where U : unmanaged { - var ret = new PrimitiveColumnContainer(); - foreach (ReadOnlyDataFrameBuffer buffer in Buffers) - { - DataFrameBuffer newBuffer = new DataFrameBuffer(buffer.Length); - ret.Buffers.Add(newBuffer); - newBuffer.IncreaseSize(buffer.Length); + var ret = new PrimitiveColumnContainer(); + int maxCapacity = ReadOnlyDataFrameBuffer.MaxCapacity; + long remaining = Length; + DataFrameBuffer newBuffer = null; + DataFrameBuffer newNullBitMapBuffer = null; - if (typeof(T) == typeof(bool)) - { - var localBuffer = buffer; - ReadOnlyDataFrameBuffer boolLocalBuffer = Unsafe.As, ReadOnlyDataFrameBuffer>(ref localBuffer); - boolLocalBuffer.ReadOnlySpan.TryCopyTo(newBuffer.RawSpan); - } - else + for (int b = 0; b < Buffers.Count; b++) + { + ReadOnlySpan span = Buffers[b].ReadOnlySpan; + ReadOnlySpan nullBitMapSpan = NullBitMapBuffers[b].ReadOnlySpan; + int index = 0; + while (index < span.Length) { - newBuffer.Span.Fill(false); + if (newBuffer == null || newBuffer.Length == maxCapacity) + { + int capacity = (int)Math.Min(remaining, maxCapacity); + newBuffer = new DataFrameBuffer(capacity); + newNullBitMapBuffer = new DataFrameBuffer((capacity + 7) / 8); + newNullBitMapBuffer.IncreaseSize((capacity + 7) / 8); + ret.Buffers.Add(newBuffer); + ret.NullBitMapBuffers.Add(newNullBitMapBuffer); + } + + int newIndex = newBuffer.Length; + int count = Math.Min(span.Length - index, maxCapacity - newIndex); + newBuffer.IncreaseSize(count); + Span newSpan = newBuffer.Span; + Span newNullBitMapSpan = newNullBitMapBuffer.Span; + for (int i = 0; i < count; i++) + { + newSpan[newIndex + i] = convert(span[index + i]); + } + + if (NullCount == 0) + { + BitUtility.SetBits(newNullBitMapSpan, newIndex, count, true); + } + else + { + for (int i = 0; i < count; i++) + { + BitUtility.SetBit(newNullBitMapSpan, newIndex + i, BitUtility.IsValid(nullBitMapSpan, index + i)); + } + } + + index += count; + remaining -= count; } - ret.Length += buffer.Length; } - ret.NullBitMapBuffers = CloneNullBitMapBuffers(); + ret.Length = Length; ret.NullCount = NullCount; return ret; } + internal PrimitiveColumnContainer CloneAsBoolContainer() + { + return CloneAs(value => typeof(T) == typeof(bool) && Unsafe.As(ref value)); + } + internal PrimitiveColumnContainer CloneAsByteContainer() { - var ret = new PrimitiveColumnContainer(); - foreach (ReadOnlyDataFrameBuffer buffer in Buffers) - { - ret.Length += buffer.Length; - DataFrameBuffer newBuffer = new DataFrameBuffer(buffer.Length); - ret.Buffers.Add(newBuffer); - ReadOnlySpan span = buffer.ReadOnlySpan; - for (int i = 0; i < span.Length; i++) - { - newBuffer.Append(ByteConverter.Instance.GetByte(span[i])); - } - } - ret.NullBitMapBuffers = CloneNullBitMapBuffers(); - ret.NullCount = NullCount; - return ret; + return CloneAs(value => ByteConverter.Instance.GetByte(value)); } internal PrimitiveColumnContainer CloneAsSByteContainer() { - var ret = new PrimitiveColumnContainer(); - foreach (ReadOnlyDataFrameBuffer buffer in Buffers) - { - ret.Length += buffer.Length; - DataFrameBuffer newBuffer = new DataFrameBuffer(buffer.Length); - ret.Buffers.Add(newBuffer); - ReadOnlySpan span = buffer.ReadOnlySpan; - for (int i = 0; i < span.Length; i++) - { - newBuffer.Append(SByteConverter.Instance.GetSByte(span[i])); - } - } - ret.NullBitMapBuffers = CloneNullBitMapBuffers(); - ret.NullCount = NullCount; - return ret; + return CloneAs(value => SByteConverter.Instance.GetSByte(value)); } internal PrimitiveColumnContainer CloneAsDoubleContainer() { - var ret = new PrimitiveColumnContainer(); - foreach (ReadOnlyDataFrameBuffer buffer in Buffers) - { - ret.Length += buffer.Length; - DataFrameBuffer newBuffer = new DataFrameBuffer(buffer.Length); - ret.Buffers.Add(newBuffer); - ReadOnlySpan span = buffer.ReadOnlySpan; - for (int i = 0; i < span.Length; i++) - { - newBuffer.Append(DoubleConverter.Instance.GetDouble(span[i])); - } - } - ret.NullBitMapBuffers = CloneNullBitMapBuffers(); - ret.NullCount = NullCount; - return ret; + return CloneAs(value => DoubleConverter.Instance.GetDouble(value)); } internal PrimitiveColumnContainer CloneAsDecimalContainer() { - var ret = new PrimitiveColumnContainer(); - foreach (ReadOnlyDataFrameBuffer buffer in Buffers) - { - ret.Length += buffer.Length; - DataFrameBuffer newBuffer = new DataFrameBuffer(buffer.Length); - ret.Buffers.Add(newBuffer); - ReadOnlySpan span = buffer.ReadOnlySpan; - for (int i = 0; i < span.Length; i++) - { - newBuffer.Append(DecimalConverter.Instance.GetDecimal(span[i])); - } - } - ret.NullBitMapBuffers = CloneNullBitMapBuffers(); - ret.NullCount = NullCount; - return ret; + return CloneAs(value => DecimalConverter.Instance.GetDecimal(value)); } internal PrimitiveColumnContainer CloneAsShortContainer() { - var ret = new PrimitiveColumnContainer(); - foreach (ReadOnlyDataFrameBuffer buffer in Buffers) - { - ret.Length += buffer.Length; - DataFrameBuffer newBuffer = new DataFrameBuffer(buffer.Length); - ret.Buffers.Add(newBuffer); - ReadOnlySpan span = buffer.ReadOnlySpan; - for (int i = 0; i < span.Length; i++) - { - newBuffer.Append(Int16Converter.Instance.GetInt16(span[i])); - } - } - ret.NullBitMapBuffers = CloneNullBitMapBuffers(); - ret.NullCount = NullCount; - return ret; + return CloneAs(value => Int16Converter.Instance.GetInt16(value)); } internal PrimitiveColumnContainer CloneAsUShortContainer() { - var ret = new PrimitiveColumnContainer(); - foreach (ReadOnlyDataFrameBuffer buffer in Buffers) - { - ret.Length += buffer.Length; - DataFrameBuffer newBuffer = new DataFrameBuffer(buffer.Length); - ret.Buffers.Add(newBuffer); - ReadOnlySpan span = buffer.ReadOnlySpan; - for (int i = 0; i < span.Length; i++) - { - newBuffer.Append(UInt16Converter.Instance.GetUInt16(span[i])); - } - } - ret.NullBitMapBuffers = CloneNullBitMapBuffers(); - ret.NullCount = NullCount; - return ret; + return CloneAs(value => UInt16Converter.Instance.GetUInt16(value)); } internal PrimitiveColumnContainer CloneAsIntContainer() { - var ret = new PrimitiveColumnContainer(); - foreach (ReadOnlyDataFrameBuffer buffer in Buffers) - { - ret.Length += buffer.Length; - DataFrameBuffer newBuffer = new DataFrameBuffer(buffer.Length); - ret.Buffers.Add(newBuffer); - ReadOnlySpan span = buffer.ReadOnlySpan; - for (int i = 0; i < span.Length; i++) - { - newBuffer.Append(Int32Converter.Instance.GetInt32(span[i])); - } - } - ret.NullBitMapBuffers = CloneNullBitMapBuffers(); - ret.NullCount = NullCount; - return ret; + return CloneAs(value => Int32Converter.Instance.GetInt32(value)); } internal PrimitiveColumnContainer CloneAsUIntContainer() { - var ret = new PrimitiveColumnContainer(); - foreach (ReadOnlyDataFrameBuffer buffer in Buffers) - { - ret.Length += buffer.Length; - DataFrameBuffer newBuffer = new DataFrameBuffer(buffer.Length); - ret.Buffers.Add(newBuffer); - ReadOnlySpan span = buffer.ReadOnlySpan; - for (int i = 0; i < span.Length; i++) - { - newBuffer.Append(UInt32Converter.Instance.GetUInt32(span[i])); - } - } - ret.NullBitMapBuffers = CloneNullBitMapBuffers(); - ret.NullCount = NullCount; - return ret; + return CloneAs(value => UInt32Converter.Instance.GetUInt32(value)); } internal PrimitiveColumnContainer CloneAsLongContainer() { - var ret = new PrimitiveColumnContainer(); - foreach (ReadOnlyDataFrameBuffer buffer in Buffers) - { - ret.Length += buffer.Length; - DataFrameBuffer newBuffer = new DataFrameBuffer(buffer.Length); - ret.Buffers.Add(newBuffer); - ReadOnlySpan span = buffer.ReadOnlySpan; - for (int i = 0; i < span.Length; i++) - { - newBuffer.Append(Int64Converter.Instance.GetInt64(span[i])); - } - } - ret.NullBitMapBuffers = CloneNullBitMapBuffers(); - ret.NullCount = NullCount; - return ret; + return CloneAs(value => Int64Converter.Instance.GetInt64(value)); } internal PrimitiveColumnContainer CloneAsULongContainer() { - var ret = new PrimitiveColumnContainer(); - foreach (ReadOnlyDataFrameBuffer buffer in Buffers) - { - ret.Length += buffer.Length; - DataFrameBuffer newBuffer = new DataFrameBuffer(buffer.Length); - ret.Buffers.Add(newBuffer); - ReadOnlySpan span = buffer.ReadOnlySpan; - for (int i = 0; i < span.Length; i++) - { - newBuffer.Append(UInt64Converter.Instance.GetUInt64(span[i])); - } - } - ret.NullBitMapBuffers = CloneNullBitMapBuffers(); - ret.NullCount = NullCount; - return ret; + return CloneAs(value => UInt64Converter.Instance.GetUInt64(value)); } internal PrimitiveColumnContainer CloneAsFloatContainer() { - var ret = new PrimitiveColumnContainer(); - foreach (ReadOnlyDataFrameBuffer buffer in Buffers) - { - ret.Length += buffer.Length; - DataFrameBuffer newBuffer = new DataFrameBuffer(buffer.Length); - ret.Buffers.Add(newBuffer); - ReadOnlySpan span = buffer.ReadOnlySpan; - for (int i = 0; i < span.Length; i++) - { - newBuffer.Append(SingleConverter.Instance.GetSingle(span[i])); - } - } - ret.NullBitMapBuffers = CloneNullBitMapBuffers(); - ret.NullCount = NullCount; - return ret; + return CloneAs(value => SingleConverter.Instance.GetSingle(value)); } } } diff --git a/test/Microsoft.Data.Analysis.Tests/PrimitiveDataFrameColumnTests.cs b/test/Microsoft.Data.Analysis.Tests/PrimitiveDataFrameColumnTests.cs index 0b58d3d3e8..7a106c767b 100644 --- a/test/Microsoft.Data.Analysis.Tests/PrimitiveDataFrameColumnTests.cs +++ b/test/Microsoft.Data.Analysis.Tests/PrimitiveDataFrameColumnTests.cs @@ -54,6 +54,41 @@ public void PrimitiveDataFrameColumn_Int32_ElementwiseEquals_AcrossMultipleBuffe Assert.False(results[count - 1]); // null != 0 } + [X64Fact("32-bit doesn't allow to allocate more than 2 Gb")] + public void PrimitiveDataFrameColumn_Int32_CloneAsDouble_AcrossMultipleBuffers() + { + // Mirrors ReadOnlyDataFrameBuffer.MaxCapacity, the number of values held by a single buffer. + // A full int buffer holds twice as many values as a double buffer can. + const int MaxCapacity = 0X7FEFFFFF / sizeof(int); + const int DoubleMaxCapacity = 0X7FEFFFFF / sizeof(double); + + const long count = MaxCapacity + 16L; + var column = new Int32DataFrameColumn("Int", count); + Assert.Equal(2, column.GetReadOnlyDataBuffers().Count()); + + column[0] = 1; + column[DoubleMaxCapacity - 1] = 2; + column[DoubleMaxCapacity] = 3; + column[MaxCapacity - 1] = 4; + column[MaxCapacity] = 5; + column[count - 1] = 6; + + // Adding a double value clones the int column as a double column + DoubleDataFrameColumn result = column.Add(0.5); + + Assert.Equal(count, result.Length); + Assert.Equal(count - 6, result.NullCount); + Assert.Equal(1.5, result[0]); + Assert.Null(result[1]); + Assert.Equal(2.5, result[DoubleMaxCapacity - 1]); + Assert.Equal(3.5, result[DoubleMaxCapacity]); + Assert.Null(result[DoubleMaxCapacity + 1]); + Assert.Equal(4.5, result[MaxCapacity - 1]); + Assert.Equal(5.5, result[MaxCapacity]); + Assert.Null(result[MaxCapacity + 1]); + Assert.Equal(6.5, result[count - 1]); + } + [X64Fact("32-bit doesn't allow to allocate more than 2 Gb")] public void TestGetterAndSetterForColumnsGreaterThanMaxCapacity() {