|
| 1 | +package is.hail.types.arrow |
| 2 | + |
| 3 | +import is.hail.types.physical._ |
| 4 | +import is.hail.utils.fatal |
| 5 | +import is.hail.variant.ReferenceGenomeEnv |
| 6 | + |
| 7 | +import scala.jdk.CollectionConverters._ |
| 8 | + |
| 9 | +import org.apache.arrow.vector.types.FloatingPointPrecision |
| 10 | +import org.apache.arrow.vector.types.pojo.{ArrowType, Field, FieldType, Schema} |
| 11 | + |
| 12 | +/** Conversion between Hail physical types and the Arrow schema of the columnar format. |
| 13 | + * |
| 14 | + * Arrow nullability carries Hail requiredness at every level below the root, with one exception: |
| 15 | + * dict keys are always non-nullable, so a dict whose key type is optional reads back with a |
| 16 | + * required key type. The root struct has no Arrow nullability, so it always reads back as |
| 17 | + * required. |
| 18 | + * |
| 19 | + * Every locus type reads back as a [[PPackedLocus]], whose representation is the Arrow storage. |
| 20 | + * |
| 21 | + * Arrow -> Hail accepts exactly the Arrow types that Hail -> Arrow produces. |
| 22 | + */ |
| 23 | +object ArrowSchemaConversion { |
| 24 | + ExtensionTypes.register() |
| 25 | + |
| 26 | + val ElementFieldName: String = "element" |
| 27 | + val EntriesFieldName: String = "entries" |
| 28 | + val KeyFieldName: String = "key" |
| 29 | + val ValueFieldName: String = "value" |
| 30 | + |
| 31 | + private val Int32 = new ArrowType.Int(32, true) |
| 32 | + private val Int64 = new ArrowType.Int(64, true) |
| 33 | + private val Float32 = new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE) |
| 34 | + private val Float64 = new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE) |
| 35 | + |
| 36 | + private val IntervalFieldNames = |
| 37 | + IndexedSeq("start", "end", "includes_start", "includes_end") |
| 38 | + |
| 39 | + private val TensorFieldNames = IndexedSeq("data", "shape") |
| 40 | + |
| 41 | + def toArrowSchema(t: PStruct): Schema = |
| 42 | + new Schema(t.fields.map(f => toArrowField(f.name, f.typ)).asJava) |
| 43 | + |
| 44 | + def toArrowField(name: String, t: PType): Field = { |
| 45 | + def field(arrowType: ArrowType, children: Field*): Field = |
| 46 | + mkField(name, !t.required, arrowType, children: _*) |
| 47 | + |
| 48 | + def child(name: String, t: PType): Field = |
| 49 | + toArrowField(name, t) |
| 50 | + |
| 51 | + t match { |
| 52 | + case _: PBoolean => field(ArrowType.Bool.INSTANCE) |
| 53 | + case _: PInt32 => field(Int32) |
| 54 | + case _: PInt64 => field(Int64) |
| 55 | + case _: PFloat32 => field(Float32) |
| 56 | + case _: PFloat64 => field(Float64) |
| 57 | + case _: PString => field(ArrowType.Utf8.INSTANCE) |
| 58 | + case _: PBinary => field(ArrowType.Binary.INSTANCE) |
| 59 | + case _: PCall => field(CallExtensionType.Current) |
| 60 | + case t: PLocus => field(new LocusExtensionType(t.rg.name, t.rg.contigs)) |
| 61 | + case t: PInterval => |
| 62 | + field( |
| 63 | + IntervalExtensionType, |
| 64 | + child(IntervalFieldNames(0), t.pointType), |
| 65 | + child(IntervalFieldNames(1), t.pointType), |
| 66 | + mkField(IntervalFieldNames(2), nullable = false, ArrowType.Bool.INSTANCE), |
| 67 | + mkField(IntervalFieldNames(3), nullable = false, ArrowType.Bool.INSTANCE), |
| 68 | + ) |
| 69 | + case t: PNDArray => |
| 70 | + field( |
| 71 | + VariableShapeTensorExtensionType, |
| 72 | + mkField( |
| 73 | + TensorFieldNames(0), |
| 74 | + nullable = false, |
| 75 | + ArrowType.List.INSTANCE, |
| 76 | + child(ElementFieldName, t.elementType), |
| 77 | + ), |
| 78 | + mkField( |
| 79 | + TensorFieldNames(1), |
| 80 | + nullable = false, |
| 81 | + new ArrowType.FixedSizeList(t.nDims), |
| 82 | + mkField(ElementFieldName, nullable = false, Int32), |
| 83 | + ), |
| 84 | + ) |
| 85 | + case t: PSet => field(SetExtensionType, child(ElementFieldName, t.elementType)) |
| 86 | + case t: PDict => |
| 87 | + field( |
| 88 | + new ArrowType.Map( /*keysSorted=*/ true), |
| 89 | + mkField( |
| 90 | + EntriesFieldName, |
| 91 | + nullable = false, |
| 92 | + ArrowType.Struct.INSTANCE, |
| 93 | + child(KeyFieldName, t.keyType.setRequired(true)), |
| 94 | + child(ValueFieldName, t.valueType), |
| 95 | + ), |
| 96 | + ) |
| 97 | + case t: PArray => field(ArrowType.List.INSTANCE, child(ElementFieldName, t.elementType)) |
| 98 | + case t: PTuple => field(TupleExtensionType, t.fields.map(f => child(f.name, f.typ)): _*) |
| 99 | + case t: PStruct => |
| 100 | + field(ArrowType.Struct.INSTANCE, t.fields.map(f => child(f.name, f.typ)): _*) |
| 101 | + case _ => |
| 102 | + fatal(s"field '$name': Hail type ${t.virtualType} cannot be stored in the columnar format") |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + def fromArrowSchema(schema: Schema, rgs: ReferenceGenomeEnv): PCanonicalStruct = |
| 107 | + PCanonicalStruct(structFields(schema.getFields.asScala.toIndexedSeq, rgs), required = true) |
| 108 | + |
| 109 | + def fromArrowField(field: Field, rgs: ReferenceGenomeEnv): PType = { |
| 110 | + val name = field.getName |
| 111 | + val required = !field.isNullable |
| 112 | + val children = field.getChildren.asScala.toIndexedSeq |
| 113 | + |
| 114 | + def unsupported(reason: String): Nothing = |
| 115 | + fatal( |
| 116 | + s"field '$name' of Arrow type ${field.getType} is not part of the columnar format: $reason" |
| 117 | + ) |
| 118 | + |
| 119 | + def checkChildren(expected: IndexedSeq[String]): Unit = |
| 120 | + if (children.map(_.getName) != expected) |
| 121 | + unsupported( |
| 122 | + s"expected child fields ${expected.mkString("[", ", ", "]")}, found ${children.map(_.getName).mkString("[", ", ", "]")}" |
| 123 | + ) |
| 124 | + |
| 125 | + def onlyChild(): Field = |
| 126 | + if (children.length == 1) children.head |
| 127 | + else unsupported(s"expected 1 child field, found ${children.length}") |
| 128 | + |
| 129 | + def convert(f: Field): PType = fromArrowField(f, rgs) |
| 130 | + |
| 131 | + def requiredChild(f: Field): Field = |
| 132 | + if (f.isNullable) unsupported(s"child field '${f.getName}' must be non-nullable") |
| 133 | + else f |
| 134 | + |
| 135 | + def leaf(t: PType): PType = |
| 136 | + if (children.isEmpty) t |
| 137 | + else unsupported(s"expected no child fields, found ${children.length}") |
| 138 | + |
| 139 | + // Rejects what the Arrow type of `f` does not show: dictionary encoding, and an extension type |
| 140 | + // that is not registered, which reads as its storage type. Child fields that are not |
| 141 | + // converted by `fromArrowField` need this check too. |
| 142 | + def checkEncoding(f: Field): Unit = { |
| 143 | + if (f.getDictionary != null) |
| 144 | + unsupported(s"field '${f.getName}' is dictionary encoded") |
| 145 | + if (!f.getType.isInstanceOf[ArrowType.ExtensionType]) |
| 146 | + Option(f.getMetadata.get(ArrowType.ExtensionType.EXTENSION_METADATA_KEY_NAME)).foreach { |
| 147 | + n => unsupported(s"field '${f.getName}' has unregistered extension type '$n'") |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + checkEncoding(field) |
| 152 | + |
| 153 | + field.getType match { |
| 154 | + case _: CallExtensionType if field.getType == CallExtensionType.Current => |
| 155 | + leaf(PCanonicalCall(required)) |
| 156 | + case t: CallExtensionType => |
| 157 | + unsupported(s"unknown call encoding ${t.encoding}") |
| 158 | + case t: LocusExtensionType => |
| 159 | + val rg = rgs.get(t.referenceGenome).getOrElse( |
| 160 | + fatal(s"field '$name': reference genome '${t.referenceGenome}' is not defined") |
| 161 | + ) |
| 162 | + if (rg.contigs != t.contigs) |
| 163 | + fatal( |
| 164 | + s"field '$name': the stored contigs of reference genome '${t.referenceGenome}' " + |
| 165 | + "differ from the contigs of the defined reference genome" |
| 166 | + ) |
| 167 | + leaf(PPackedLocus(rg, required)) |
| 168 | + case SetExtensionType => |
| 169 | + PCanonicalSet(convert(onlyChild()), required) |
| 170 | + case IntervalExtensionType => |
| 171 | + checkChildren(IntervalFieldNames) |
| 172 | + val IndexedSeq(start, end, includesStart, includesEnd) = children |
| 173 | + val pointType = convert(start) |
| 174 | + if (convert(end) != pointType) |
| 175 | + unsupported("interval 'start' and 'end' have different types") |
| 176 | + for (f <- Seq(includesStart, includesEnd)) { |
| 177 | + checkEncoding(f) |
| 178 | + if (f.isNullable || f.getType != ArrowType.Bool.INSTANCE || !f.getChildren.isEmpty) |
| 179 | + unsupported(s"interval '${f.getName}' must be a non-nullable bool") |
| 180 | + } |
| 181 | + PCanonicalInterval(pointType, required) |
| 182 | + case TupleExtensionType => |
| 183 | + val fields = children.map { c => |
| 184 | + val idx = c.getName.toIntOption.filter(i => i >= 0 && i.toString == c.getName).getOrElse( |
| 185 | + unsupported(s"tuple field name '${c.getName}' is not a non-negative integer") |
| 186 | + ) |
| 187 | + PTupleField(idx, convert(c)) |
| 188 | + } |
| 189 | + if (fields.map(_.index).distinct.length != fields.length) |
| 190 | + unsupported("tuple field names are not distinct") |
| 191 | + PCanonicalTuple(fields, required) |
| 192 | + case VariableShapeTensorExtensionType => |
| 193 | + checkChildren(TensorFieldNames) |
| 194 | + val IndexedSeq(data, shape) = children |
| 195 | + checkEncoding(data) |
| 196 | + checkEncoding(shape) |
| 197 | + val nDims = shape.getType match { |
| 198 | + case t: ArrowType.FixedSizeList => t.getListSize |
| 199 | + case t => unsupported(s"tensor 'shape' must be a fixed_size_list, found $t") |
| 200 | + } |
| 201 | + requiredChild(shape): Unit |
| 202 | + if (shape.getChildren.size != 1 || convert(shape.getChildren.get(0)) != PInt32(true)) |
| 203 | + unsupported("tensor 'shape' must contain non-nullable int32") |
| 204 | + if (requiredChild(data).getType != ArrowType.List.INSTANCE || data.getChildren.size != 1) |
| 205 | + unsupported("tensor 'data' must be a list") |
| 206 | + val elementType = convert(requiredChild(data.getChildren.get(0))) |
| 207 | + if (elementType.containsPointers) |
| 208 | + unsupported(s"tensor element type ${elementType.virtualType} is not supported") |
| 209 | + PCanonicalNDArray(elementType, nDims, required) |
| 210 | + case t: ArrowType.ExtensionType => |
| 211 | + unsupported(s"unknown extension type '${t.extensionName}'") |
| 212 | + case ArrowType.Bool.INSTANCE => leaf(PBoolean(required)) |
| 213 | + case Int32 => leaf(PInt32(required)) |
| 214 | + case Int64 => leaf(PInt64(required)) |
| 215 | + case Float32 => leaf(PFloat32(required)) |
| 216 | + case Float64 => leaf(PFloat64(required)) |
| 217 | + case ArrowType.Utf8.INSTANCE => leaf(PCanonicalString(required)) |
| 218 | + case ArrowType.Binary.INSTANCE => leaf(PCanonicalBinary(required)) |
| 219 | + case ArrowType.List.INSTANCE => PCanonicalArray(convert(onlyChild()), required) |
| 220 | + case t: ArrowType.Map => |
| 221 | + if (!t.getKeysSorted) unsupported("map keys must be sorted") |
| 222 | + val entries = requiredChild(onlyChild()) |
| 223 | + checkEncoding(entries) |
| 224 | + if (entries.getType != ArrowType.Struct.INSTANCE || entries.getChildren.size != 2) |
| 225 | + unsupported("map entries must be a struct with two fields") |
| 226 | + val key = requiredChild(entries.getChildren.get(0)) |
| 227 | + PCanonicalDict(convert(key), convert(entries.getChildren.get(1)), required) |
| 228 | + case ArrowType.Struct.INSTANCE => |
| 229 | + PCanonicalStruct(structFields(children, rgs), required) |
| 230 | + case _ => unsupported("unsupported type") |
| 231 | + } |
| 232 | + } |
| 233 | + |
| 234 | + private def structFields(fields: IndexedSeq[Field], rgs: ReferenceGenomeEnv) |
| 235 | + : IndexedSeq[PField] = { |
| 236 | + val duplicates = fields.map(_.getName).groupBy(identity).collect { |
| 237 | + case (name, names) if names.length > 1 => name |
| 238 | + } |
| 239 | + if (duplicates.nonEmpty) |
| 240 | + fatal( |
| 241 | + s"duplicate field names are not part of the columnar format: ${duplicates.mkString(", ")}" |
| 242 | + ) |
| 243 | + fields.zipWithIndex.map { case (f, i) => |
| 244 | + PField(f.getName, fromArrowField(f, rgs), i) |
| 245 | + } |
| 246 | + } |
| 247 | + |
| 248 | + private def mkField(name: String, nullable: Boolean, arrowType: ArrowType, children: Field*) |
| 249 | + : Field = |
| 250 | + new Field(name, new FieldType(nullable, arrowType, /*dictionary=*/ null), children.asJava) |
| 251 | +} |
0 commit comments