Skip to content

Commit 30e4ca0

Browse files
authored
#18: Added unified error codes (#21)
1 parent 740f7c6 commit 30e4ca0

15 files changed

+159
-56
lines changed

doc/changes/changes_0.3.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ In this release, we added custom user defined property separators. We also migra
1313
## Refactoring
1414

1515
* #15: Migrated to Github actions
16+
* #18: Added unified error codes
1617

1718
## Dependency Updates
1819

1920
### Runtime Dependency Updates
2021

22+
* Added `com.exasol:error-reporting-java:0.4.0`
2123
* Updated `com.fasterxml.jackson.core:jackson-databind:2.11.3` to `2.12.5`
2224
* Updated `com.fasterxml.jackson.module:jackson-module-scala:2.11.3` to `2.12.5`
2325
* Updated `com.typesafe.scala-logging:scala-logging:3.9.2` to `3.9.4`

error_code_config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
error-tags:
2+
IEUCS:
3+
packages:
4+
- com.exasol.common
5+
highest-index: 11

project/Dependencies.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ object Dependencies {
2424
lazy val RuntimeDependencies: Seq[ModuleID] = Seq(
2525
"com.exasol" % "exasol-script-api" % ExasolVersion,
2626
"org.slf4j" % "slf4j-simple" % SLF4JSimpleVersion,
27+
"com.exasol" % "error-reporting-java" % "0.4.0",
2728
"com.typesafe.scala-logging" %% "scala-logging" % TypesafeLoggingVersion
2829
exclude ("org.slf4j", "slf4j-api")
2930
exclude ("org.scala-lang", "scala-library")

src/main/scala/com/exasol/avro/AvroConverter.scala

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import java.util.{Map => JMap}
88
import java.util.Collection
99

1010
import com.exasol.common.json.JsonMapper
11+
import com.exasol.errorreporting.ExaError
1112

1213
import org.apache.avro.Conversions.DecimalConversion
1314
import org.apache.avro.LogicalTypes
@@ -121,8 +122,13 @@ final class AvroConverter {
121122
val precision = logicalType.getPrecision()
122123
if (precision > EXASOL_DECIMAL_PRECISION) {
123124
throw new IllegalArgumentException(
124-
s"Decimal precision ${precision.toString()} is larger than " +
125-
s"maximum allowed precision ${EXASOL_DECIMAL_PRECISION.toString()}."
125+
ExaError
126+
.messageBuilder("E-IEUCS-5")
127+
.message("Decimal precision {{PRECISION}} is larger than maximal allowed {{ALLOWED}} precision.")
128+
.parameter("PRECISION", precision.toString())
129+
.parameter("ALLOWED", EXASOL_DECIMAL_PRECISION.toString())
130+
.mitigation("Please ensure that Avro decimal value precision fits into Exasol decimal precision.")
131+
.toString()
126132
)
127133
}
128134
}
@@ -136,7 +142,11 @@ final class AvroConverter {
136142
case fixed: GenericFixed => new String(fixed.bytes(), "UTF8")
137143
case _ =>
138144
throw new IllegalArgumentException(
139-
s"Avro ${field.getName} type cannot be converted to string!"
145+
ExaError
146+
.messageBuilder("E-IEUCS-6")
147+
.message("Avro field {{FIELD}} type cannot be converted to string.", field.getName())
148+
.mitigation("Please ensure that Exasol table column and Avro field types match.")
149+
.toString()
140150
)
141151
}
142152

@@ -151,15 +161,19 @@ final class AvroConverter {
151161
} else if (types.get(1).getType() == Schema.Type.NULL) {
152162
getAvroValue(value, types.get(0))
153163
} else {
154-
throw new IllegalArgumentException(
155-
"Avro Union type should contain a primitive and null!"
156-
)
164+
throw new IllegalArgumentException(getAvroUnionErrorMessage())
157165
}
158-
case _ =>
159-
throw new IllegalArgumentException("Avro Union type should contain a primitive and null!")
166+
case _ => throw new IllegalArgumentException(getAvroUnionErrorMessage())
160167
}
161168
}
162169

170+
private[this] def getAvroUnionErrorMessage(): String =
171+
ExaError
172+
.messageBuilder("E-IEUCS-7")
173+
.message("Avro union type does not contain a primitive type and null.")
174+
.mitigation("Please make sure that Avro union type contains a primitive type and a null.")
175+
.toString()
176+
163177
private[this] def getArrayValue(value: Any, field: Schema): Array[Any] = value match {
164178
case array: Array[_] => array.map(getAvroValue(_, field.getElementType()))
165179
case list: Collection[_] =>
@@ -172,7 +186,12 @@ final class AvroConverter {
172186
result
173187
case other =>
174188
throw new IllegalArgumentException(
175-
s"Unsupported Avro Array type '${other.getClass.getName()}'."
189+
ExaError
190+
.messageBuilder("E-IEUCS-8")
191+
.message("Unsupported Avro array type {{TYPE}}.", other.getClass.getName())
192+
.mitigation("Please make sure Avro array type is of Array or Collection types.")
193+
.ticketMitigation()
194+
.toString()
176195
)
177196
}
178197

@@ -201,7 +220,12 @@ final class AvroConverter {
201220
result
202221
case other =>
203222
throw new IllegalArgumentException(
204-
s"Unsupported Avro Record type '${other.getClass.getName()}'."
223+
ExaError
224+
.messageBuilder("E-IEUCS-9")
225+
.message("Unsupported Avro record type {{TYPE}}.", other.getClass.getName())
226+
.mitigation("Please make sure that Avro record type is of IndexedRecord type.")
227+
.ticketMitigation()
228+
.toString()
205229
)
206230
}
207231

src/main/scala/com/exasol/avro/AvroRowIterator.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.exasol.common.avro
22

33
import com.exasol.common.data.Row
4+
import com.exasol.errorreporting.ExaError
45

56
import org.apache.avro.file.DataFileReader
67
import org.apache.avro.generic.GenericRecord
@@ -32,7 +33,13 @@ object AvroRowIterator {
3233

3334
override def next(): Row = {
3435
if (!hasNext) {
35-
throw new NoSuchElementException("Avro reader called next on an empty iterator!")
36+
throw new NoSuchElementException(
37+
ExaError
38+
.messageBuilder("E-IEUCS-3")
39+
.message("Avro reader next call on an empty iterator.")
40+
.mitigation("Please check that Avro iterator has elements before calling next.")
41+
.toString()
42+
)
3643
}
3744
val record = reader.next()
3845
AvroRow(record)

src/main/scala/com/exasol/common/AbstractProperties.scala

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.exasol.common
33
import com.exasol.ExaConnectionInformation
44
import com.exasol.ExaMetadata
55
import com.exasol.common.CommonConstants._
6+
import com.exasol.errorreporting.ExaError
67

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

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

102115
/**

src/main/scala/com/exasol/common/PropertiesParser.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.exasol.common
22

33
import scala.collection.SortedMap
44

5+
import com.exasol.errorreporting.ExaError
6+
57
/**
68
* A class that reads, serializes and deserializes key value properties to a string.
79
*
@@ -39,7 +41,11 @@ final case class PropertiesParser(private val propertySeparator: String, private
3941
val idx = string.indexOf(keyValueAssignment)
4042
if (idx < 0) {
4143
throw new IllegalArgumentException(
42-
s"Properties input string does not contain key-value assignment '$keyValueAssignment'."
44+
ExaError
45+
.messageBuilder("E-IEUCS-4")
46+
.message("Properties input string does not contain key-value assignment {{KVA}}.", keyValueAssignment)
47+
.mitigation("Please make sure that key-value pairs encoded correctly.")
48+
.toString()
4349
)
4450
}
4551
stripAndReplace(string.substring(0, idx)) -> stripAndReplace(string.substring(idx + keyValueAssignment.length()))

src/main/scala/com/exasol/common/Row.scala

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,73 @@
11
package com.exasol.common.data
22

3+
import scala.reflect.ClassTag
4+
5+
import com.exasol.errorreporting.ExaError
6+
37
/**
48
* The internal class that holds column data in an array.
59
*/
610
final case class Row(protected[data] val values: Seq[Any]) {
711

8-
/** Checks whether the value at position {@code index} is null. */
12+
/**
13+
* Checks whether the value at position {@code index} is null.
14+
*
15+
* @param index an index into value array
16+
* @return {@code true} if value at index is {@code null}, otherwise {@code false}
17+
*/
918
def isNullAt(index: Int): Boolean = get(index) == null
1019

1120
/**
1221
* Returns the value at position {@code index}.
1322
*
1423
* If the value is null, null is returned.
24+
*
25+
* @param index an index into values array
26+
* @return value at the provided index
1527
*/
1628
@throws[IndexOutOfBoundsException]("When index is out of bounds")
17-
def get(index: Int): Any = values(index)
29+
def get(index: Int): Any =
30+
if (index >= values.length) {
31+
throw new IndexOutOfBoundsException(
32+
ExaError
33+
.messageBuilder("E-IEUCS-10")
34+
.message("Given index {{INDEX}} is out of bounds.", String.valueOf(index))
35+
.mitigation("Please use correct index to obtain field value.")
36+
.toString()
37+
)
38+
} else {
39+
values(index)
40+
}
1841

19-
/** Returns the value at position {@code index} casted to the type. */
20-
@throws[ClassCastException]("When data type does not match")
21-
def getAs[T](index: Int): T = get(index).asInstanceOf[T]
42+
/**
43+
* Returns the value at position {@code index} casted to the type.
44+
*
45+
* For catching {@link java.lang.ClassCastException}, we use reflection based casting.
46+
*
47+
* @param index an index into values array
48+
* @return value at the provided index casted to type
49+
*/
50+
@throws[IllegalArgumentException]("When data type does not match")
51+
def getAs[T](index: Int)(implicit m: ClassTag[T]): T = {
52+
val runtimeClass = m.runtimeClass
53+
val value = get(index)
54+
try {
55+
runtimeClass.cast(value).asInstanceOf[T]
56+
} catch {
57+
case exception: ClassCastException =>
58+
throw new IllegalArgumentException(
59+
ExaError
60+
.messageBuilder("E-IEUCS-11")
61+
.message("Failed to cast {{VALUE}} at index {{INDEX}} to instance of {{INSTANCE_TYPE}}.")
62+
.parameter("VALUE", value)
63+
.parameter("INDEX", String.valueOf(index))
64+
.parameter("INSTANCE_TYPE", runtimeClass.toString())
65+
.mitigation("Please use valid type parameter for type casting.")
66+
.toString(),
67+
exception
68+
)
69+
}
70+
}
2271

2372
/** Returns the value array. */
2473
def getValues(): Seq[Any] =

src/test/scala/com/exasol/avro/AvroComplexTypesTest.scala

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.exasol.common.avro
22

3+
import java.lang.Integer
34
import java.util.{List => JList}
45
import java.util.{Map => JMap}
56

@@ -105,7 +106,9 @@ class AvroComplexTypesTest extends AnyFunSuite {
105106
val thrown = intercept[IllegalArgumentException] {
106107
AvroRow(record)
107108
}
108-
assert(thrown.getMessage().contains("Unsupported Avro Array type"))
109+
assert(
110+
thrown.getMessage().startsWith("E-IEUCS-8: Unsupported Avro array type 'java.util.ImmutableCollections$MapN'.")
111+
)
109112
}
110113

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

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

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

201204
}

src/test/scala/com/exasol/avro/AvroLogicalTypesTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ class AvroLogicalTypesTest extends AnyFunSuite {
142142
val thrown = intercept[IllegalArgumentException] {
143143
AvroRow(record)
144144
}
145-
val expected = "Decimal precision 40 is larger than maximum allowed precision 36."
146-
assert(thrown.getMessage() === expected)
145+
val expectedPrefix = "E-IEUCS-5: Decimal precision '40' is larger than maximal allowed '36' precision."
146+
assert(thrown.getMessage().startsWith(expectedPrefix))
147147
}
148148

149149
}

0 commit comments

Comments
 (0)