Skip to content

Commit

Permalink
#18: Added unified error codes (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
morazow committed Oct 12, 2021
1 parent 740f7c6 commit 30e4ca0
Show file tree
Hide file tree
Showing 15 changed files with 159 additions and 56 deletions.
2 changes: 2 additions & 0 deletions doc/changes/changes_0.3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ In this release, we added custom user defined property separators. We also migra
## Refactoring

* #15: Migrated to Github actions
* #18: Added unified error codes

## Dependency Updates

### Runtime Dependency Updates

* Added `com.exasol:error-reporting-java:0.4.0`
* Updated `com.fasterxml.jackson.core:jackson-databind:2.11.3` to `2.12.5`
* Updated `com.fasterxml.jackson.module:jackson-module-scala:2.11.3` to `2.12.5`
* Updated `com.typesafe.scala-logging:scala-logging:3.9.2` to `3.9.4`
Expand Down
5 changes: 5 additions & 0 deletions error_code_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error-tags:
IEUCS:
packages:
- com.exasol.common
highest-index: 11
1 change: 1 addition & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ object Dependencies {
lazy val RuntimeDependencies: Seq[ModuleID] = Seq(
"com.exasol" % "exasol-script-api" % ExasolVersion,
"org.slf4j" % "slf4j-simple" % SLF4JSimpleVersion,
"com.exasol" % "error-reporting-java" % "0.4.0",
"com.typesafe.scala-logging" %% "scala-logging" % TypesafeLoggingVersion
exclude ("org.slf4j", "slf4j-api")
exclude ("org.scala-lang", "scala-library")
Expand Down
44 changes: 34 additions & 10 deletions src/main/scala/com/exasol/avro/AvroConverter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import java.util.{Map => JMap}
import java.util.Collection

import com.exasol.common.json.JsonMapper
import com.exasol.errorreporting.ExaError

import org.apache.avro.Conversions.DecimalConversion
import org.apache.avro.LogicalTypes
Expand Down Expand Up @@ -121,8 +122,13 @@ final class AvroConverter {
val precision = logicalType.getPrecision()
if (precision > EXASOL_DECIMAL_PRECISION) {
throw new IllegalArgumentException(
s"Decimal precision ${precision.toString()} is larger than " +
s"maximum allowed precision ${EXASOL_DECIMAL_PRECISION.toString()}."
ExaError
.messageBuilder("E-IEUCS-5")
.message("Decimal precision {{PRECISION}} is larger than maximal allowed {{ALLOWED}} precision.")
.parameter("PRECISION", precision.toString())
.parameter("ALLOWED", EXASOL_DECIMAL_PRECISION.toString())
.mitigation("Please ensure that Avro decimal value precision fits into Exasol decimal precision.")
.toString()
)
}
}
Expand All @@ -136,7 +142,11 @@ final class AvroConverter {
case fixed: GenericFixed => new String(fixed.bytes(), "UTF8")
case _ =>
throw new IllegalArgumentException(
s"Avro ${field.getName} type cannot be converted to string!"
ExaError
.messageBuilder("E-IEUCS-6")
.message("Avro field {{FIELD}} type cannot be converted to string.", field.getName())
.mitigation("Please ensure that Exasol table column and Avro field types match.")
.toString()
)
}

Expand All @@ -151,15 +161,19 @@ final class AvroConverter {
} else if (types.get(1).getType() == Schema.Type.NULL) {
getAvroValue(value, types.get(0))
} else {
throw new IllegalArgumentException(
"Avro Union type should contain a primitive and null!"
)
throw new IllegalArgumentException(getAvroUnionErrorMessage())
}
case _ =>
throw new IllegalArgumentException("Avro Union type should contain a primitive and null!")
case _ => throw new IllegalArgumentException(getAvroUnionErrorMessage())
}
}

private[this] def getAvroUnionErrorMessage(): String =
ExaError
.messageBuilder("E-IEUCS-7")
.message("Avro union type does not contain a primitive type and null.")
.mitigation("Please make sure that Avro union type contains a primitive type and a null.")
.toString()

private[this] def getArrayValue(value: Any, field: Schema): Array[Any] = value match {
case array: Array[_] => array.map(getAvroValue(_, field.getElementType()))
case list: Collection[_] =>
Expand All @@ -172,7 +186,12 @@ final class AvroConverter {
result
case other =>
throw new IllegalArgumentException(
s"Unsupported Avro Array type '${other.getClass.getName()}'."
ExaError
.messageBuilder("E-IEUCS-8")
.message("Unsupported Avro array type {{TYPE}}.", other.getClass.getName())
.mitigation("Please make sure Avro array type is of Array or Collection types.")
.ticketMitigation()
.toString()
)
}

Expand Down Expand Up @@ -201,7 +220,12 @@ final class AvroConverter {
result
case other =>
throw new IllegalArgumentException(
s"Unsupported Avro Record type '${other.getClass.getName()}'."
ExaError
.messageBuilder("E-IEUCS-9")
.message("Unsupported Avro record type {{TYPE}}.", other.getClass.getName())
.mitigation("Please make sure that Avro record type is of IndexedRecord type.")
.ticketMitigation()
.toString()
)
}

Expand Down
9 changes: 8 additions & 1 deletion src/main/scala/com/exasol/avro/AvroRowIterator.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.exasol.common.avro

import com.exasol.common.data.Row
import com.exasol.errorreporting.ExaError

import org.apache.avro.file.DataFileReader
import org.apache.avro.generic.GenericRecord
Expand Down Expand Up @@ -32,7 +33,13 @@ object AvroRowIterator {

override def next(): Row = {
if (!hasNext) {
throw new NoSuchElementException("Avro reader called next on an empty iterator!")
throw new NoSuchElementException(
ExaError
.messageBuilder("E-IEUCS-3")
.message("Avro reader next call on an empty iterator.")
.mitigation("Please check that Avro iterator has elements before calling next.")
.toString()
)
}
val record = reader.next()
AvroRow(record)
Expand Down
17 changes: 15 additions & 2 deletions src/main/scala/com/exasol/common/AbstractProperties.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.exasol.common
import com.exasol.ExaConnectionInformation
import com.exasol.ExaMetadata
import com.exasol.common.CommonConstants._
import com.exasol.errorreporting.ExaError

/**
* An abstract class that holds the user provided key-value parameters when using the user-defined-functions (UDFs).
Expand Down Expand Up @@ -85,7 +86,13 @@ abstract class AbstractProperties(private val properties: Map[String, String]) {
*/
private[common] final def getConnectionInformation(exaMetadata: Option[ExaMetadata]): ExaConnectionInformation =
exaMetadata.fold {
throw new IllegalArgumentException("Exasol metadata is None!")
throw new IllegalArgumentException(
ExaError
.messageBuilder("E-IEUCS-1")
.message("Provided Exasol metadata object is None.")
.mitigation("Please make sure it is valid metadata object.")
.toString()
)
}(_.getConnection(getString(CONNECTION_NAME)))

/**
Expand All @@ -96,7 +103,13 @@ abstract class AbstractProperties(private val properties: Map[String, String]) {
@throws[IllegalArgumentException]("If key does not exist.")
final def getString(key: String): String =
get(key).fold {
throw new IllegalArgumentException(s"Please provide a value for the $key property!")
throw new IllegalArgumentException(
ExaError
.messageBuilder("E-IEUCS-2")
.message("Failed to get value for {{KEY}} property.", key)
.mitigation("Please provide key-value pairs for {{KEY}} property.", key)
.toString()
)
}(identity)

/**
Expand Down
8 changes: 7 additions & 1 deletion src/main/scala/com/exasol/common/PropertiesParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.exasol.common

import scala.collection.SortedMap

import com.exasol.errorreporting.ExaError

/**
* A class that reads, serializes and deserializes key value properties to a string.
*
Expand Down Expand Up @@ -39,7 +41,11 @@ final case class PropertiesParser(private val propertySeparator: String, private
val idx = string.indexOf(keyValueAssignment)
if (idx < 0) {
throw new IllegalArgumentException(
s"Properties input string does not contain key-value assignment '$keyValueAssignment'."
ExaError
.messageBuilder("E-IEUCS-4")
.message("Properties input string does not contain key-value assignment {{KVA}}.", keyValueAssignment)
.mitigation("Please make sure that key-value pairs encoded correctly.")
.toString()
)
}
stripAndReplace(string.substring(0, idx)) -> stripAndReplace(string.substring(idx + keyValueAssignment.length()))
Expand Down
59 changes: 54 additions & 5 deletions src/main/scala/com/exasol/common/Row.scala
Original file line number Diff line number Diff line change
@@ -1,24 +1,73 @@
package com.exasol.common.data

import scala.reflect.ClassTag

import com.exasol.errorreporting.ExaError

/**
* The internal class that holds column data in an array.
*/
final case class Row(protected[data] val values: Seq[Any]) {

/** Checks whether the value at position {@code index} is null. */
/**
* Checks whether the value at position {@code index} is null.
*
* @param index an index into value array
* @return {@code true} if value at index is {@code null}, otherwise {@code false}
*/
def isNullAt(index: Int): Boolean = get(index) == null

/**
* Returns the value at position {@code index}.
*
* If the value is null, null is returned.
*
* @param index an index into values array
* @return value at the provided index
*/
@throws[IndexOutOfBoundsException]("When index is out of bounds")
def get(index: Int): Any = values(index)
def get(index: Int): Any =
if (index >= values.length) {
throw new IndexOutOfBoundsException(
ExaError
.messageBuilder("E-IEUCS-10")
.message("Given index {{INDEX}} is out of bounds.", String.valueOf(index))
.mitigation("Please use correct index to obtain field value.")
.toString()
)
} else {
values(index)
}

/** Returns the value at position {@code index} casted to the type. */
@throws[ClassCastException]("When data type does not match")
def getAs[T](index: Int): T = get(index).asInstanceOf[T]
/**
* Returns the value at position {@code index} casted to the type.
*
* For catching {@link java.lang.ClassCastException}, we use reflection based casting.
*
* @param index an index into values array
* @return value at the provided index casted to type
*/
@throws[IllegalArgumentException]("When data type does not match")
def getAs[T](index: Int)(implicit m: ClassTag[T]): T = {
val runtimeClass = m.runtimeClass
val value = get(index)
try {
runtimeClass.cast(value).asInstanceOf[T]
} catch {
case exception: ClassCastException =>
throw new IllegalArgumentException(
ExaError
.messageBuilder("E-IEUCS-11")
.message("Failed to cast {{VALUE}} at index {{INDEX}} to instance of {{INSTANCE_TYPE}}.")
.parameter("VALUE", value)
.parameter("INDEX", String.valueOf(index))
.parameter("INSTANCE_TYPE", runtimeClass.toString())
.mitigation("Please use valid type parameter for type casting.")
.toString(),
exception
)
}
}

/** Returns the value array. */
def getValues(): Seq[Any] =
Expand Down
11 changes: 7 additions & 4 deletions src/test/scala/com/exasol/avro/AvroComplexTypesTest.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.exasol.common.avro

import java.lang.Integer
import java.util.{List => JList}
import java.util.{Map => JMap}

Expand Down Expand Up @@ -105,7 +106,9 @@ class AvroComplexTypesTest extends AnyFunSuite {
val thrown = intercept[IllegalArgumentException] {
AvroRow(record)
}
assert(thrown.getMessage().contains("Unsupported Avro Array type"))
assert(
thrown.getMessage().startsWith("E-IEUCS-8: Unsupported Avro array type 'java.util.ImmutableCollections$MapN'.")
)
}

test("parse avro map type") {
Expand Down Expand Up @@ -142,7 +145,7 @@ class AvroComplexTypesTest extends AnyFunSuite {
record.put("id", 1)
record.put("address", address)
val row = AvroRow(record)
assert(row.getAs[Int](0) === 1)
assert(row.getAs[Integer](0) === 1)
assert(row.getAs[String](1) === """{"zipCode":40902,"street":"Street 9"}""")
}

Expand Down Expand Up @@ -183,7 +186,7 @@ class AvroComplexTypesTest extends AnyFunSuite {
| "age":42
|}""".stripMargin.replaceAll("\\s+", "")
val row = AvroRow(record)
assert(row.getAs[Int](0) === 1)
assert(row.getAs[Integer](0) === 1)
assert(row.getAs[String](1) === expected)
}

Expand All @@ -195,7 +198,7 @@ class AvroComplexTypesTest extends AnyFunSuite {
val thrown = intercept[IllegalArgumentException] {
AvroRow(record)
}
assert(thrown.getMessage().contains("Unsupported Avro Record type"))
assert(thrown.getMessage().startsWith("E-IEUCS-9: Unsupported Avro record type 'java.lang.String'."))
}

}
4 changes: 2 additions & 2 deletions src/test/scala/com/exasol/avro/AvroLogicalTypesTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ class AvroLogicalTypesTest extends AnyFunSuite {
val thrown = intercept[IllegalArgumentException] {
AvroRow(record)
}
val expected = "Decimal precision 40 is larger than maximum allowed precision 36."
assert(thrown.getMessage() === expected)
val expectedPrefix = "E-IEUCS-5: Decimal precision '40' is larger than maximal allowed '36' precision."
assert(thrown.getMessage().startsWith(expectedPrefix))
}

}
14 changes: 7 additions & 7 deletions src/test/scala/com/exasol/avro/AvroPrimitiveReaderTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,19 @@ class AvroPrimitiveReaderTest extends AnyFunSuite {
}

test("parse avro union with many types") {
val schema = getSchema(s"""["string", "int", "null"]""")
val thrown = intercept[IllegalArgumentException] {
unionTest(schema)
}
assert(thrown.getMessage() === "Avro Union type should contain a primitive and null!")
assertThrowsUnionError(getSchema(s"""["string", "int", "null"]"""))
}

test("parse avro union without a null type") {
val schema = getSchema(s"""["string", "int"]""")
assertThrowsUnionError(getSchema(s"""["string", "int"]"""))
}

private[this] def assertThrowsUnionError(schema: Schema): Unit = {
val thrown = intercept[IllegalArgumentException] {
unionTest(schema)
}
assert(thrown.getMessage() === "Avro Union type should contain a primitive and null!")
assert(thrown.getMessage().startsWith("E-IEUCS-7: Avro union type does not contain a primitive type and null."))
()
}

private[this] def unionTest(schema: Schema): Unit = {
Expand Down
2 changes: 1 addition & 1 deletion src/test/scala/com/exasol/avro/AvroRowIteratorTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class AvroRowIteratorTest extends AnyFunSuite with BeforeAndAfterEach {
val thrown = intercept[NoSuchElementException] {
iterator.next()
}
assert(thrown.getMessage() === "Avro reader called next on an empty iterator!")
assert(thrown.getMessage().startsWith("E-IEUCS-3: Avro reader next call on an empty iterator."))
}

private[this] def write[T <: GenericRecord](file: File, record: T): Unit = {
Expand Down
Loading

0 comments on commit 30e4ca0

Please sign in to comment.