blob: 5ef3575986e5d3ae6d1cd7b2fb9ce7700a433fa7 [file]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Data.SqlTypes;
using Apache.Arrow.Arrays;
using Apache.Arrow.Scalars.Variant;
namespace Apache.Arrow.Operations.Shredding
{
/// <summary>
/// Zero-copy reader for a single row of a (possibly shredded) variant column.
/// Composes with the <see cref="ShredSchema"/> for this position to expose the
/// typed columns and residual bytes side-by-side, or to materialize the logical
/// value on demand.
/// <para>
/// A <see cref="ShreddedVariant"/> does not own any Arrow buffers; it is only
/// valid while the underlying Arrow arrays are alive.
/// </para>
/// </summary>
public ref struct ShreddedVariant
{
private readonly ShredSchema _schema;
private readonly ReadOnlySpan<byte> _metadata;
// _valueArray is the residual binary column at this level (may be null).
private readonly IArrowArray _valueArray;
// _typedValueArray is the typed column at this level (may be null if no shredding here).
private readonly IArrowArray _typedValueArray;
private readonly int _index;
internal ShreddedVariant(
ShredSchema schema,
ReadOnlySpan<byte> metadata,
IArrowArray valueArray,
IArrowArray typedValueArray,
int index)
{
_schema = schema ?? throw new ArgumentNullException(nameof(schema));
_metadata = metadata;
_valueArray = valueArray;
_typedValueArray = typedValueArray;
_index = index;
}
/// <summary>The schema describing how this slot is shredded.</summary>
public ShredSchema Schema => _schema;
/// <summary>The column-level variant metadata.</summary>
public ReadOnlySpan<byte> Metadata => _metadata;
/// <summary>True when the residual <c>value</c> column has a value at this index.</summary>
public bool HasResidual => _valueArray != null && !_valueArray.IsNull(_index);
/// <summary>True when the <c>typed_value</c> column has a value at this index.</summary>
public bool HasTypedValue => _typedValueArray != null && !_typedValueArray.IsNull(_index);
/// <summary>
/// True when neither the residual nor the typed column is populated at this index
/// — valid only for sub-fields of shredded objects.
/// </summary>
public bool IsMissing => !HasResidual && !HasTypedValue;
/// <summary>
/// Materializes this slot into a logical <see cref="VariantValue"/>, merging
/// typed-column values with residual bytes per the shredding spec.
/// </summary>
/// <exception cref="InvalidOperationException">If the slot is missing.</exception>
public VariantValue ToVariantValue()
{
// Both-null at this slot means the logical value is variant null. (The
// "missing" encoding — omitting the field entirely from the output — is
// a choice made by the container: see ShreddedObject, which uses
// IsMissing to decide whether to drop a sub-field.)
if (IsMissing)
{
return VariantValue.Null;
}
switch (_schema.TypedValueType)
{
case ShredType.None:
return ReadResidual();
case ShredType.Object:
return GetObject().ToVariantValue();
case ShredType.Array:
return GetArray().ToVariantValue();
default:
// Primitive shredding. Per the Parquet variant shredding spec, a
// primitive slot may have at most one of value / typed_value set.
// If both are populated at the same row, the shredded data is
// invalid and implementations should reject it.
if (HasTypedValue)
{
if (HasResidual)
{
throw new InvalidOperationException(
"Invalid shredded variant: primitive slot has both 'value' and 'typed_value' populated.");
}
return ReadTypedPrimitive();
}
return ReadResidual();
}
}
/// <summary>
/// If the residual column has a value at this index, returns a
/// <see cref="VariantReader"/> over its bytes.
/// </summary>
public bool TryGetResidualReader(out VariantReader reader)
{
if (HasResidual)
{
BinaryArray binary = (BinaryArray)_valueArray;
ReadOnlySpan<byte> bytes = binary.GetBytes(_index, out _);
reader = new VariantReader(_metadata, bytes);
return true;
}
reader = default;
return false;
}
/// <summary>
/// Reader for a shredded object at this slot. Valid only when the schema's
/// <see cref="ShredSchema.TypedValueType"/> is <see cref="ShredType.Object"/>.
/// </summary>
public ShreddedObject GetObject()
{
if (_schema.TypedValueType != ShredType.Object)
{
throw new InvalidOperationException(
$"Slot is not shredded as an object (schema type {_schema.TypedValueType}).");
}
return new ShreddedObject(_schema, _metadata, _typedValueArray as StructArray, _valueArray, _index);
}
/// <summary>
/// Reader for a shredded array at this slot. Valid only when the schema's
/// <see cref="ShredSchema.TypedValueType"/> is <see cref="ShredType.Array"/>.
/// </summary>
public ShreddedArray GetArray()
{
if (_schema.TypedValueType != ShredType.Array)
{
throw new InvalidOperationException(
$"Slot is not shredded as an array (schema type {_schema.TypedValueType}).");
}
return new ShreddedArray(_schema, _metadata, _typedValueArray as ListArray, _valueArray, _index);
}
private VariantValue ReadResidual()
{
if (!HasResidual)
{
throw new InvalidOperationException("No residual value to read.");
}
BinaryArray binary = (BinaryArray)_valueArray;
ReadOnlySpan<byte> bytes = binary.GetBytes(_index, out _);
return new VariantReader(_metadata, bytes).ToVariantValue();
}
// ---------------------------------------------------------------
// Typed-column accessors — zero-copy access to the shredded value
// without materializing a VariantValue.
//
// Each getter requires:
// (a) the slot's schema to match the requested type, and
// (b) the typed_value column to be populated at this index.
// Otherwise it throws. Callers that want automatic residual fallback
// should use ToVariantValue instead.
// ---------------------------------------------------------------
/// <summary>Reads the shredded boolean value at this slot.</summary>
public bool GetBoolean() => ((BooleanArray)RequireTyped(ShredType.Boolean)).GetValue(_index).Value;
/// <summary>Reads the shredded 8-bit signed integer at this slot.</summary>
public sbyte GetInt8() => ((Int8Array)RequireTyped(ShredType.Int8)).GetValue(_index).Value;
/// <summary>Reads the shredded 16-bit signed integer at this slot.</summary>
public short GetInt16() => ((Int16Array)RequireTyped(ShredType.Int16)).GetValue(_index).Value;
/// <summary>Reads the shredded 32-bit signed integer at this slot.</summary>
public int GetInt32() => ((Int32Array)RequireTyped(ShredType.Int32)).GetValue(_index).Value;
/// <summary>Reads the shredded 64-bit signed integer at this slot.</summary>
public long GetInt64() => ((Int64Array)RequireTyped(ShredType.Int64)).GetValue(_index).Value;
/// <summary>Reads the shredded 32-bit float at this slot.</summary>
public float GetFloat() => ((FloatArray)RequireTyped(ShredType.Float)).GetValue(_index).Value;
/// <summary>Reads the shredded 64-bit double at this slot.</summary>
public double GetDouble() => ((DoubleArray)RequireTyped(ShredType.Double)).GetValue(_index).Value;
/// <summary>
/// Reads the shredded decimal value at this slot. Works for Decimal4, Decimal8,
/// and Decimal16 shred types, regardless of whether the Arrow column is backed
/// by Decimal32Array, Decimal64Array, or Decimal128Array.
/// </summary>
public decimal GetDecimal()
{
RequireDecimalSchema();
if (!HasTypedValue) ThrowNoTyped();
IArrowArray arr = UnwrapExtension(_typedValueArray);
switch (arr)
{
case Decimal32Array d32: return d32.GetValue(_index).Value;
case Decimal64Array d64: return d64.GetValue(_index).Value;
case Decimal128Array d128: return d128.GetValue(_index).Value;
default:
throw new InvalidOperationException(
$"Shredded decimal column is backed by {arr.GetType().Name}, which is not a supported decimal array type.");
}
}
/// <summary>
/// Reads the shredded decimal value at this slot. Works for Decimal4, Decimal8,
/// and Decimal16 shred types, regardless of whether the Arrow column is backed
/// by Decimal32Array, Decimal64Array, or Decimal128Array.
/// </summary>
public SqlDecimal GetSqlDecimal()
{
RequireDecimalSchema();
if (!HasTypedValue) ThrowNoTyped();
IArrowArray arr = UnwrapExtension(_typedValueArray);
switch (arr)
{
case Decimal32Array d32: return d32.GetValue(_index).Value;
case Decimal64Array d64: return d64.GetValue(_index).Value;
case Decimal128Array d128: return d128.GetSqlDecimal(_index).Value;
default:
throw new InvalidOperationException(
$"Shredded decimal column is backed by {arr.GetType().Name}, which is not a supported decimal array type.");
}
}
/// <summary>Reads the shredded date (days since epoch) at this slot.</summary>
public int GetDateDays() => ((Date32Array)RequireTyped(ShredType.Date)).GetValue(_index).Value;
/// <summary>Reads the shredded timestamp (microseconds since epoch, UTC) at this slot.</summary>
public long GetTimestampMicros() => ((TimestampArray)RequireTyped(ShredType.Timestamp)).GetValue(_index).Value;
/// <summary>Reads the shredded timestamp-without-tz (microseconds since epoch) at this slot.</summary>
public long GetTimestampNtzMicros() => ((TimestampArray)RequireTyped(ShredType.TimestampNtz)).GetValue(_index).Value;
/// <summary>Reads the shredded time-without-tz (microseconds since midnight) at this slot.</summary>
public long GetTimeNtzMicros() => ((Time64Array)RequireTyped(ShredType.TimeNtz)).GetValue(_index).Value;
/// <summary>Reads the shredded timestamp-with-tz (nanoseconds since epoch) at this slot.</summary>
public long GetTimestampTzNanos() => ((TimestampArray)RequireTyped(ShredType.TimestampTzNanos)).GetValue(_index).Value;
/// <summary>Reads the shredded timestamp-without-tz (nanoseconds since epoch) at this slot.</summary>
public long GetTimestampNtzNanos() => ((TimestampArray)RequireTyped(ShredType.TimestampNtzNanos)).GetValue(_index).Value;
/// <summary>Reads the shredded string value at this slot.</summary>
public string GetString() => ((StringArray)RequireTyped(ShredType.String)).GetString(_index);
/// <summary>Reads the shredded binary value at this slot as a byte span.</summary>
public ReadOnlySpan<byte> GetBinaryBytes() => ((BinaryArray)RequireTyped(ShredType.Binary)).GetBytes(_index);
/// <summary>Reads the shredded UUID at this slot.</summary>
public Guid GetUuid()
{
FixedSizeBinaryArray arr = (FixedSizeBinaryArray)RequireTyped(ShredType.Uuid);
ReadOnlySpan<byte> raw = arr.GetBytes(_index);
#if NET8_0_OR_GREATER
return new Guid(raw, bigEndian: true);
#else
byte[] bytes = new byte[16];
bytes[0] = raw[3]; bytes[1] = raw[2]; bytes[2] = raw[1]; bytes[3] = raw[0];
bytes[4] = raw[5]; bytes[5] = raw[4];
bytes[6] = raw[7]; bytes[7] = raw[6];
raw.Slice(8, 8).CopyTo(bytes.AsSpan(8));
return new Guid(bytes);
#endif
}
/// <summary>Reads the shredded UUID at this slot as raw big-endian (RFC 4122) bytes.</summary>
public ReadOnlySpan<byte> GetUuidBytes()
=> ((FixedSizeBinaryArray)RequireTyped(ShredType.Uuid)).GetBytes(_index);
// ---------------------------------------------------------------
// Primitive dispatch for internal materialization. Delegates to the
// typed getters so the two paths stay in sync.
// ---------------------------------------------------------------
private VariantValue ReadTypedPrimitive()
{
switch (_schema.TypedValueType)
{
case ShredType.Boolean: return VariantValue.FromBoolean(GetBoolean());
case ShredType.Int8: return VariantValue.FromInt8(GetInt8());
case ShredType.Int16: return VariantValue.FromInt16(GetInt16());
case ShredType.Int32: return VariantValue.FromInt32(GetInt32());
case ShredType.Int64: return VariantValue.FromInt64(GetInt64());
case ShredType.Float: return VariantValue.FromFloat(GetFloat());
case ShredType.Double: return VariantValue.FromDouble(GetDouble());
case ShredType.Decimal4: return VariantValue.FromDecimal4(GetDecimal());
case ShredType.Decimal8: return VariantValue.FromDecimal8(GetDecimal());
case ShredType.Decimal16: return VariantValue.FromDecimal16(GetSqlDecimal());
case ShredType.Date: return VariantValue.FromDate(GetDateDays());
case ShredType.Timestamp: return VariantValue.FromTimestamp(GetTimestampMicros());
case ShredType.TimestampNtz: return VariantValue.FromTimestampNtz(GetTimestampNtzMicros());
case ShredType.TimeNtz: return VariantValue.FromTimeNtz(GetTimeNtzMicros());
case ShredType.TimestampTzNanos: return VariantValue.FromTimestampTzNanos(GetTimestampTzNanos());
case ShredType.TimestampNtzNanos: return VariantValue.FromTimestampNtzNanos(GetTimestampNtzNanos());
case ShredType.String: return VariantValue.FromString(GetString());
case ShredType.Binary: return VariantValue.FromBinary(GetBinaryBytes().ToArray());
case ShredType.Uuid: return VariantValue.FromUuid(GetUuid());
default:
throw new InvalidOperationException(
$"Unexpected primitive shred type {_schema.TypedValueType}.");
}
}
private IArrowArray RequireTyped(ShredType expected)
{
if (_schema.TypedValueType != expected)
{
throw new InvalidOperationException(
$"Slot schema is {_schema.TypedValueType}, not {expected}.");
}
if (!HasTypedValue) ThrowNoTyped();
return UnwrapExtension(_typedValueArray);
}
private void RequireDecimalSchema()
{
if (_schema.TypedValueType != ShredType.Decimal4 &&
_schema.TypedValueType != ShredType.Decimal8 &&
_schema.TypedValueType != ShredType.Decimal16)
{
throw new InvalidOperationException(
$"Slot schema is {_schema.TypedValueType}, not a decimal type.");
}
}
private void ThrowNoTyped() =>
throw new InvalidOperationException(
"No typed_value at this index (check HasTypedValue first, or use ToVariantValue for residual fallback).");
private static IArrowArray UnwrapExtension(IArrowArray arr) =>
arr is ExtensionArray ext ? ext.Storage : arr;
}
}