Repository navigation
[query] Add columnar table writer #15949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
chrisvittal
wants to merge
5
commits into
main
Choose a base branch
from
cv/query/columnar-table-for-primitives
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,300
−43
Draft
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
8271937
[query] Add Hail <-> Arrow schema conversion
chrisvittal 9069d5e
[query] Add a format-aware commit check for tables and matrix tables
chrisvittal 559702d
Add columnar format specification
chrisvittal ff9de34
[query] Add a columnar table writer
chrisvittal 7f0f970
Simplify the columnar writer's partition and table writers
chrisvittal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package is.hail.expr.ir | ||
|
|
||
| import is.hail.io.fs.FS | ||
|
|
||
| object StoredDataset { | ||
|
|
||
| /** Whether the table or matrix table at `path` was completely written. | ||
| * | ||
| * The native format commits by writing `_SUCCESS`. The columnar format writes no `_SUCCESS`; it | ||
| * commits by writing its manifest root last. | ||
| */ | ||
| def isCommitted(fs: FS, path: String): Boolean = | ||
| fs.isFile(s"$path/_SUCCESS") || fs.isFile(s"$path/manifest.json") | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
157 changes: 157 additions & 0 deletions
157
hail/hail/src/is/hail/io/columnar/ColumnarFileSink.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| package is.hail.io.columnar | ||
|
|
||
| import is.hail.io.fs.FS | ||
|
|
||
| import scala.jdk.CollectionConverters._ | ||
|
|
||
| import java.io.{BufferedOutputStream, ByteArrayInputStream, OutputStream} | ||
| import java.nio.channels.Channels | ||
| import java.util.Base64 | ||
|
|
||
| import org.apache.arrow.vector.ipc.ReadChannel | ||
| import org.apache.arrow.vector.ipc.message.MessageSerializer | ||
| import org.apache.arrow.vector.types.pojo.Schema | ||
| import org.apache.hadoop.conf.Configuration | ||
| import org.apache.parquet.conf.{ParquetConfiguration, PlainParquetConfiguration} | ||
| import org.apache.parquet.hadoop.{ParquetFileWriter, ParquetWriter} | ||
| import org.apache.parquet.hadoop.api.WriteSupport | ||
| import org.apache.parquet.hadoop.metadata.CompressionCodecName | ||
| import org.apache.parquet.io.{OutputFile, PositionOutputStream} | ||
| import org.apache.parquet.io.api.{Binary, RecordConsumer} | ||
| import org.apache.parquet.schema.MessageType | ||
|
|
||
| /** What a task needs to open a [[ColumnarFileSink]]: the file's Arrow schema, as the base64 | ||
| * encapsulated IPC message that also goes in the footer's `ARROW:schema`. | ||
| * | ||
| * Arrow schemas aren't serializable, so this carries the encoded form to workers. | ||
| */ | ||
| case class ColumnarFileSpec(arrowSchema: String) { | ||
| def open(fs: FS, path: String): ColumnarFileSink = new ColumnarFileSink(fs, path, arrowSchema) | ||
| } | ||
|
|
||
| object ColumnarFileSink { | ||
| val ArrowSchemaKey: String = "ARROW:schema" | ||
|
|
||
| def decodeSchema(arrowSchema: String): Schema = | ||
| MessageSerializer.deserializeSchema( | ||
| new ReadChannel(Channels.newChannel( | ||
| new ByteArrayInputStream(Base64.getDecoder.decode(arrowSchema)) | ||
| )) | ||
| ) | ||
|
|
||
| private class HailOutputFile(fs: FS, path: String) extends OutputFile { | ||
| override def create(blockSizeHint: Long): PositionOutputStream = | ||
| createOrOverwrite(blockSizeHint) | ||
|
|
||
| override def createOrOverwrite(blockSizeHint: Long): PositionOutputStream = | ||
| new PositionOutputStream { | ||
| private[this] val out: OutputStream = | ||
| new BufferedOutputStream(fs.createNoCompression(path), 1 << 20) | ||
|
|
||
| private[this] var pos: Long = 0 | ||
|
|
||
| override def getPos: Long = pos | ||
|
|
||
| override def write(b: Int): Unit = { out.write(b); pos += 1 } | ||
|
|
||
| override def write(b: Array[Byte], off: Int, len: Int): Unit = { | ||
| out.write(b, off, len); pos += len | ||
| } | ||
|
|
||
| override def flush(): Unit = out.flush() | ||
|
|
||
| override def close(): Unit = out.close() | ||
| } | ||
|
|
||
| override def supportsBlockSize(): Boolean = false | ||
|
|
||
| override def defaultBlockSize(): Long = 0 | ||
|
|
||
| override def getPath: String = path | ||
| } | ||
|
|
||
| /** Hands parquet-java's record consumer to [[ColumnarFileSink]], which emits each record itself. | ||
| * parquet-java replaces the consumer whenever it starts a new row group. | ||
| */ | ||
| private class ConsumerWriteSupport(schema: MessageType, arrowSchema: String) | ||
| extends WriteSupport[AnyRef] { | ||
| var consumer: RecordConsumer = _ | ||
|
|
||
| override def init(conf: Configuration): WriteSupport.WriteContext = | ||
| new WriteSupport.WriteContext(schema, Map(ArrowSchemaKey -> arrowSchema).asJava) | ||
|
|
||
| override def init(conf: ParquetConfiguration): WriteSupport.WriteContext = | ||
| new WriteSupport.WriteContext(schema, Map(ArrowSchemaKey -> arrowSchema).asJava) | ||
|
|
||
| override def prepareForWrite(recordConsumer: RecordConsumer): Unit = | ||
| consumer = recordConsumer | ||
|
|
||
| // The record was already emitted through `consumer`. | ||
| override def write(record: AnyRef): Unit = () | ||
| } | ||
|
|
||
| private class Builder(file: OutputFile, support: ConsumerWriteSupport) | ||
| extends ParquetWriter.Builder[AnyRef, Builder](file) { | ||
| override def self(): Builder = this | ||
|
|
||
| override def getWriteSupport(conf: Configuration): WriteSupport[AnyRef] = support | ||
|
|
||
| override def getWriteSupport(conf: ParquetConfiguration): WriteSupport[AnyRef] = support | ||
| } | ||
| } | ||
|
|
||
| /** Writes one Parquet data file of the columnar format, one record at a time. | ||
| * | ||
| * Emitted code drives this directly: `startRecord`, then a sequence of field, group and value | ||
| * calls following parquet-java's [[RecordConsumer]] protocol for the file's Parquet schema, then | ||
| * `endRecord`. The footer carries the Arrow schema under `ARROW:schema`. Statistics and the page | ||
| * index are written for every leaf, and pages are zstd-compressed. | ||
| */ | ||
| final class ColumnarFileSink(fs: FS, path: String, arrowSchema: String) { | ||
| import ColumnarFileSink._ | ||
|
|
||
| private[this] val support = new ConsumerWriteSupport( | ||
| ColumnarSchema.toParquet(decodeSchema(arrowSchema)), | ||
| arrowSchema, | ||
| ) | ||
|
|
||
| private[this] val writer: ParquetWriter[AnyRef] = | ||
| new Builder(new HailOutputFile(fs, path), support) | ||
| .withConf(new PlainParquetConfiguration()) | ||
| .withWriteMode(ParquetFileWriter.Mode.OVERWRITE) | ||
| .withCompressionCodec(CompressionCodecName.ZSTD) | ||
| .withStatisticsEnabled(true) | ||
| .build() | ||
|
|
||
| private[this] def consumer: RecordConsumer = support.consumer | ||
|
|
||
| def startRecord(): Unit = consumer.startMessage() | ||
|
|
||
| def endRecord(): Unit = { | ||
| consumer.endMessage() | ||
| // Counts the record emitted above, and flushes the row group when it is full. | ||
| writer.write(null) | ||
| } | ||
|
|
||
| def startField(name: String, index: Int): Unit = consumer.startField(name, index) | ||
|
|
||
| def endField(name: String, index: Int): Unit = consumer.endField(name, index) | ||
|
|
||
| def startGroup(): Unit = consumer.startGroup() | ||
|
|
||
| def endGroup(): Unit = consumer.endGroup() | ||
|
|
||
| def addBoolean(v: Boolean): Unit = consumer.addBoolean(v) | ||
|
|
||
| def addInt(v: Int): Unit = consumer.addInteger(v) | ||
|
|
||
| def addLong(v: Long): Unit = consumer.addLong(v) | ||
|
|
||
| def addFloat(v: Float): Unit = consumer.addFloat(v) | ||
|
|
||
| def addDouble(v: Double): Unit = consumer.addDouble(v) | ||
|
|
||
| def addBinary(v: Array[Byte]): Unit = consumer.addBinary(Binary.fromConstantByteArray(v)) | ||
|
|
||
| def close(): Unit = writer.close() | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: hail-is/hail
Length of output: 1054
Script executed:
Repository: hail-is/hail
Length of output: 21658
Script executed:
Repository: hail-is/hail
Length of output: 33930
Script executed:
Repository: hail-is/hail
Length of output: 3826
Script executed:
Repository: hail-is/hail
Length of output: 8697
Script executed:
Repository: hail-is/hail
Length of output: 3010
Reject unsupported columnar reads explicitly.
When a columnar table is passed to
TableIR.reador checkpoint reuse, the commitment check accepts it, but native metadata loading reports it as corrupted. Detect the columnar format and report that reading it is unsupported until a columnar reader is available.