Skip to content
This repository was archived by the owner on Dec 20, 2018. It is now read-only.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ To save DataFrame as avro you should use the `save` method in `AvroSaver`. For e
```scala
scala> AvroSaver.save(myRDD, "my/output/dir")
```

To include aliases column in scheme invoke the method `addAvroAliasColumns()` of DataFrame.
With alias while saving `saveAsAvroFile`, alias columns in DataFrame schema will not be include in Avro file.
Alias will be available in `aliases` of avro schema.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it stored as "aliases" or "_aliases"?

```scala
scala>val dfWithAlias = df.addAvroAliasColumns()
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also include examples of how to set alias and docs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marmbrus user can set alias and docs to a avro using avro tools. Not using spark-avro

You can also specifiy the the record name and namespace with optional parameters:
```scala
scala> AvroSaver.save(myRDD, "my/output/dir", Map("recordName" -> "MyRecord", "recordNamespace" -> "com.mycompany.mystuff"))
Expand Down
7 changes: 6 additions & 1 deletion src/main/scala/com/databricks/spark/avro/AvroSaver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,12 @@ object AvroSaver {

while (convertersIterator.hasNext) {
val converter = convertersIterator.next()
record.put(fieldNamesIterator.next(), converter(rowIterator.next()))
val fieldName = fieldNamesIterator.next()
if (schema.getField(fieldName) != null) {
record.put(fieldName, converter(rowIterator.next()))
} else {
rowIterator.next()
}
}
record
}
Expand Down
58 changes: 47 additions & 11 deletions src/main/scala/com/databricks/spark/avro/SchemaConverters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,22 @@
package com.databricks.spark.avro

import scala.collection.JavaConversions._

import org.apache.avro.{Schema, SchemaBuilder}
import org.apache.avro.SchemaBuilder._

import org.apache.spark.sql.types._
import org.apache.avro.Schema.Type._
import org.apache.spark.sql.DataFrame
import util.control.Breaks._

/**
* This object contains method that are used to convert sparkSQL schemas to avro schemas and vice
* versa.
*/
private object SchemaConverters {

val METADATA_KEY_DOC = "doc";
val METADATA_KEY_ALIASES = "aliases";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is good, you can put even the "_parent" here as a constant


case class SchemaType(dataType: DataType, nullable: Boolean)

/**
Expand All @@ -49,7 +52,15 @@ private object SchemaConverters {
case RECORD =>
val fields = avroSchema.getFields.map { f =>
val schemaType = toSqlType(f.schema())
StructField(f.name, schemaType.dataType, schemaType.nullable)
var meta = new MetadataBuilder()
if (f.doc != null) meta.putString(METADATA_KEY_DOC, f.doc)
if (f.aliases() != null && f.aliases().size() > 0) {
val aliasArray = new Array[String](f.aliases().size())
meta.putString("_parent", f.name)
f.aliases copyToArray(aliasArray)
meta.putStringArray(METADATA_KEY_ALIASES, aliasArray);
}
StructField(f.name, schemaType.dataType, schemaType.nullable, meta.build())
}

SchemaType(StructType(fields), nullable = false)
Expand Down Expand Up @@ -88,6 +99,19 @@ private object SchemaConverters {
}
}

def dataFrameWithAliasColumn(df : DataFrame) : DataFrame = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function still needed? or is this redundant with the above listing of field names ++ aliases?

var newDf = df
for (field <- df.schema.fields) {
if (field.metadata.contains(METADATA_KEY_ALIASES)) {
val aliasArray = field.metadata.getStringArray(METADATA_KEY_ALIASES)
for (alias <- aliasArray) {
newDf = newDf.withColumn(alias, df.col(field.name))
}
}
}
newDf
}

/**
* This function converts sparkSQL StructType into avro schema. This method uses two other
* converter methods in order to do the conversion.
Expand All @@ -98,14 +122,26 @@ private object SchemaConverters {
recordNamespace: String): T = {
val fieldsAssembler: FieldAssembler[T] = schemaBuilder.fields()
structType.fields.foreach { field =>
val newField = fieldsAssembler.name(field.name).`type`()

if (field.nullable) {
convertFieldTypeToAvro(field.dataType, newField.nullable(), field.name, recordNamespace)
.noDefault
} else {
convertFieldTypeToAvro(field.dataType, newField, field.name, recordNamespace)
.noDefault
breakable {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be more readable to just make it "if ... else" statement instead of "breakable" and "break"?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used breakable for moving next element. primary checks before creating the fields. I feel this is simple than writing complex if condition check. I can modify if required.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is nice to check them before. But as far as simplicity and readability goes...

breakable {
  if (predicate) break
  body
}

is equivalent to

if (!predicate) {
  body
}

Hence is is shorter, cleaner and other folks do not need to know what breakable is. Right?

BTW: the code here can be written as...

val nonAliasStructFields = structType.fields.filterNot(field =>
  field.metadata.contains(METADATA_KEY_PARENT) && !field.metadata.getString(METADATA_KEY_PARENT).equals(field.name))
nonAliasStructFields.foreach { field =>
  var newFieldBuilder = fieldsAssembler.name(field.name)
...

if (field.metadata.contains(METADATA_KEY_ALIASES) && field.metadata.contains("_parent")
&& !field.metadata.getString("_parent").equals(field.name)) {
break
}
var newFieldBuilder = fieldsAssembler.name(field.name)
if (field.metadata contains (METADATA_KEY_DOC)) {
newFieldBuilder = newFieldBuilder.doc(field.metadata.getString(METADATA_KEY_DOC))
}
if (field.metadata.contains(METADATA_KEY_ALIASES)){
newFieldBuilder = newFieldBuilder.aliases(field.metadata.getStringArray(METADATA_KEY_ALIASES): _*)
}
val newField = newFieldBuilder.`type`()
if (field.nullable) {
convertFieldTypeToAvro(field.dataType, newField.nullable(), field.name, recordNamespace)
.noDefault
} else {
convertFieldTypeToAvro(field.dataType, newField, field.name, recordNamespace)
.noDefault
}
}
}
fieldsAssembler.endRecord()
Expand Down
4 changes: 4 additions & 0 deletions src/main/scala/com/databricks/spark/avro/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ package object avro {
implicit class AvroContext(sqlContext: SQLContext) {
def avroFile(filePath: String, minPartitions: Int = 0) =
sqlContext.baseRelationToDataFrame(AvroRelation(filePath, None, minPartitions)(sqlContext))

}

/**
Expand All @@ -35,5 +36,8 @@ package object avro {
path: String,
parameters: Map[String, String] = AvroSaver.defaultParameters): Unit =
AvroSaver.save(dataFrame, path, parameters)

def addAvroAliasColumns() : DataFrame =
SchemaConverters.dataFrameWithAliasColumn(dataFrame)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get rid of this entirely? I'd prefer to only have to support a single unified way to do this.

}
Binary file modified src/test/resources/test.avro
Binary file not shown.
33 changes: 22 additions & 11 deletions src/test/resources/test.avsc
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,32 @@
"fields" : [{
"name" : "string",
"type" : "string",
"doc" : "Meaningless string of characters"
"doc" : "Meaningless string of characters",
"aliases" : ["string_alias1", "string_alias2"]
}, {
"name" : "simple_map",
"type" : {"type": "map", "values": "int"}
"type" : {"type": "map", "values": "int"},
"aliases" : ["map_alias"]
}, {
"name" : "complex_map",
"type" : {"type": "map", "values": {"type": "map", "values": "string"}}
"type" : {"type": "map", "values": {"type": "map", "values": "string"}},
"aliases" : ["complex_map_alias"]
}, {
"name" : "union_string_null",
"type" : ["null", "string"]
"type" : ["null", "string"],
"aliases" : ["union_string_alias"]
}, {
"name" : "union_int_long_null",
"type" : ["int", "long", "null"]
"type" : ["int", "long", "null"],
"aliases" : ["union_int_alias"]
}, {
"name" : "union_float_double",
"type" : ["float", "double"]
"type" : ["float", "double"],
"aliases" : ["union_float_alias1", "union_float_alias2"]
}, {
"name": "fixed3",
"type": {"type": "fixed", "size": 3, "name": "fixed3"}
"type": {"type": "fixed", "size": 3, "name": "fixed3"},
"aliases" : ["fixed3_alias"]
}, {
"name": "fixed2",
"type": {"type": "fixed", "size": 2, "name": "fixed2"}
Expand All @@ -31,7 +38,8 @@
"type": { "type": "enum",
"name": "Suit",
"symbols" : ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"]
}
},
"aliases" : ["enum_alias"]
}, {
"name": "record",
"type": {
Expand All @@ -40,14 +48,17 @@
"aliases": ["RecordAlias"],
"fields" : [{
"name": "value_field",
"type": "string"
"type": "string",
"aliases" : ["value_field_alias"]
}]
}
}, {
"name": "array_of_boolean",
"type": {"type": "array", "items": "boolean"}
"type": {"type": "array", "items": "boolean"},
"aliases" : ["array_of_boolean_alias"]
}, {
"name": "bytes",
"type": "bytes"
"type": "bytes",
"aliases" : ["bytes_alias"]
}]
}
89 changes: 89 additions & 0 deletions src/test/scala/com/databricks/spark/avro/AvroSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -394,4 +394,93 @@ class AvroSuite extends FunSuite {
assert(newDf.count == 8)
}

test("test doc in meta") {
val df = TestSQLContext.load(episodesFile, "com.databricks.spark.avro")
df.schema.fields(0).metadata.getString(SchemaConverters.METADATA_KEY_DOC)

for (x <- df.schema.fields) {
if (x.name == "title") {
assert("episode title" == x.metadata.getString(SchemaConverters.METADATA_KEY_DOC))
} else if (x.name == "doctor") {
assert("main actor playing the Doctor in episode" ==
x.metadata.getString(SchemaConverters.METADATA_KEY_DOC))
} else if (x.name == "air_date") {
assert("initial date" == x.metadata.getString(SchemaConverters.METADATA_KEY_DOC))
}
}
}

test("test aliases in meta") {
val df = TestSQLContext.load(testFile, "com.databricks.spark.avro")

for (x <- df.schema.fields) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess people will believe the metadata are not type dependent. Hence you can put them only on one field in test file - let's say "string" and then this whole test can become

val df = TestSQLContext.load(testFile, "com.databricks.spark.avro")
assert(df.schema("string").getStringArray(SchemaConverters.METADATA_KEY_ALIASES) === Array("string_alias1", "string_alias1")

Would that make it easier?

if (x.name == "string") {
assert(x.metadata.contains(SchemaConverters.METADATA_KEY_ALIASES))
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES).size == 2)
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(0) == "string_alias1")
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(1) == "string_alias2")
} else if (x.name == "simple_map") {
assert(x.metadata.contains(SchemaConverters.METADATA_KEY_ALIASES))
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES).size == 1)
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(0) == "map_alias")
} else if (x.name == "complex_map") {
assert(x.metadata.contains(SchemaConverters.METADATA_KEY_ALIASES))
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES).size == 1)
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(0) == "complex_map_alias")
} else if (x.name == "union_string_null") {
assert(x.metadata.contains(SchemaConverters.METADATA_KEY_ALIASES))
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES).size == 1)
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(0) == "union_string_alias")
} else if (x.name == "union_int_long_null") {
assert(x.metadata.contains(SchemaConverters.METADATA_KEY_ALIASES))
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES).size == 1)
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(0) == "union_int_alias")
} else if (x.name == "union_float_double") {
assert(x.metadata.contains(SchemaConverters.METADATA_KEY_ALIASES))
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES).size == 2)
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(0) == "union_float_alias1")
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(1) == "union_float_alias2")
} else if (x.name == "fixed3") {
assert(x.metadata.contains(SchemaConverters.METADATA_KEY_ALIASES))
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES).size == 1)
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(0) == "fixed3_alias")
} else if (x.name == "enum") {
assert(x.metadata.contains(SchemaConverters.METADATA_KEY_ALIASES))
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES).size == 1)
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(0) == "enum_alias")
} else if (x.name == "value_field") {
assert(x.metadata.contains(SchemaConverters.METADATA_KEY_ALIASES))
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES).size == 1)
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(0) == "value_field_alias")
} else if (x.name == "array_of_boolean") {
assert(x.metadata.contains(SchemaConverters.METADATA_KEY_ALIASES))
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES).size == 1)
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(0) == "array_of_boolean_alias")
} else if (x.name == "bytes") {
assert(x.metadata.contains(SchemaConverters.METADATA_KEY_ALIASES))
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES).size == 1)
assert(x.metadata.getStringArray(SchemaConverters.METADATA_KEY_ALIASES)(0) == "bytes_alias")
}
}
}

test("test aliases columns in data frame") {
var df = TestSQLContext.load(testFile, "com.databricks.spark.avro")
var fieldArray = df.schema.fieldNames;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

break these into two tests instead of using a var.

assert(fieldArray contains("string"))
assert(!(fieldArray contains("string_alias1")))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either use infix notation or leave the . in.

assert(!(fieldArray contains("string_alias2")))
assert(!(fieldArray contains("map_alias")))
assert(!(fieldArray contains("enum_alias")))
assert(!(fieldArray contains("union_int_alias")))

fieldArray = SchemaConverters.dataFrameWithAliasColumn(df).schema.fieldNames

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test should probably use the user visible option instead of diving into internals.

assert(fieldArray contains("string"))
assert(fieldArray contains("string_alias1"))
assert(fieldArray contains("string_alias2"))
assert(fieldArray contains("map_alias"))
assert(fieldArray contains("enum_alias"))
assert(fieldArray contains("union_int_alias"))
}

}