Skip to content

Commit fd6e147

Browse files
committed
* mixed case names are now lower cased unless they are quoted or contain other non-standard characters
* unified naming - columnName(s) is used all around
1 parent 5692a5a commit fd6e147

File tree

11 files changed

+171
-124
lines changed

11 files changed

+171
-124
lines changed

balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,18 @@ case class DBTable(tableName: String) extends DBQuerySupport{
5151
*
5252
* @param keyName - the name of the key column
5353
* @param keyValue - the value of the key column
54-
* @param fieldName - the name of the field to be returned
54+
* @param columnName - the name of the field to be returned
5555
* @param connection - a database connection used for the SELECT operation.
5656
* @tparam K - the type of the key value
5757
* @tparam T - the type of the returned field value
5858
* @return - the value of the field, if the value is NULL, then `Some(None)` is returned; if no row is found,
5959
* then `None` is returned.
6060
*/
61-
def fieldValue[K: QueryParamType, T](keyName: String, keyValue: K, fieldName: String)
61+
def fieldValue[K: QueryParamType, T](keyName: String, keyValue: K, columnName: String)
6262
(implicit connection: DBConnection): Option[Option[T]] = {
6363
where(Params.add(keyName, keyValue)){resultSet =>
6464
if (resultSet.hasNext) {
65-
Some(resultSet.next().getAs[T](fieldName))
65+
Some(resultSet.next().getAs[T](columnName))
6666
} else {
6767
None
6868
}

balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class QueryResult(resultSet: ResultSet) extends Iterator[QueryResultRow] {
3131

3232
private [this] var nextRow: Option[QueryResultRow] = None
3333

34-
private [this] implicit val fieldNames: ColumnNames = QueryResultRow.fieldNamesFromMetadata(resultSetMetaData)
34+
private [this] implicit val columnNames: ColumnNames = QueryResultRow.columnNamesFromMetadata(resultSetMetaData)
3535
private [this] implicit val extractors: Extractors = QueryResultRow.createExtractors(resultSetMetaData)
3636

3737
override def hasNext: Boolean = {

balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala

Lines changed: 27 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package za.co.absa.db.balta.classes
1818

1919
import QueryResultRow._
2020
import za.co.absa.db.balta.implicits.MapImplicits.MapEnhancements
21+
import za.co.absa.db.mag.core.ColumnName
2122

2223
import java.sql
2324
import java.sql.{Date, ResultSet, ResultSetMetaData, Time, Types}
@@ -29,26 +30,16 @@ import java.util.UUID
2930
*
3031
* @param rowNumber - the number of the row in the result set
3132
* @param fields - the values of the row
32-
* @param columnLabels - the names of the columns; class uses `columnLabel(s)` to refer to the column names in accordance
33-
* to `java.sql.ResultSet`, which is is build around and that despite that aliases are not expected
34-
* to appear here
33+
* @param columnNames - the names of the columns
3534
*/
3635
class QueryResultRow private[classes](val rowNumber: Int,
3736
private val fields: Vector[Option[Object]],
38-
private val columnLabels: ColumnNames) {
37+
private val columnNames: ColumnNames) {
3938

4039
def columnCount: Int = fields.length
41-
def columnNumber(columnLabel: String): Int = {
42-
val quotedRegex = """^"(.+)"$""".r
43-
columnLabel match {
44-
case quotedRegex(innerLabel) => // in case the column label is quoted, first try to find it as is, then without quotes
45-
columnLabels.getOrElse(
46-
columnLabel,
47-
columnLabels.getOrThrow(innerLabel, new NoSuchElementException(s"Column '$columnLabel' not found"))
48-
)
49-
case _ => columnLabels.getOrThrow(columnLabel, new NoSuchElementException(s"Column '$columnLabel' not found"))
50-
}
51-
40+
def columnNumber(columnName: String): Int = {
41+
val columnNameQuoteless = ColumnName(columnName).quoteLess
42+
columnNames.getOrThrow(columnNameQuoteless, new NoSuchElementException(s"Column '$columnNameQuoteless' not found"))
5243
}
5344

5445
/**
@@ -59,22 +50,22 @@ class QueryResultRow private[classes](val rowNumber: Int,
5950
def apply(column: Int): Option[Any] = getObject(column - 1)
6051
/**
6152
* Extracts a value from the row by column name.
62-
* @param columnLabel - the name of the column
53+
* @param columnName - the name of the column
6354
* @return - the value stored in the column, type `Any` is for warningless comparison with any type
6455
*/
65-
def apply(columnLabel: String): Option[Any] = getObject(columnNumber(columnLabel))
56+
def apply(columnName: String): Option[Any] = getObject(columnNumber(columnName))
6657

6758
def getAs[T](column: Int, transformer: TransformerFnc[T]): Option[T] = getObject(column).map(transformer)
6859
def getAs[T](column: Int): Option[T] = getObject(column)map(_.asInstanceOf[T])
6960

70-
def getAs[T](columnLabel: String, transformer: TransformerFnc[T]): Option[T] = getAs(columnNumber(columnLabel), transformer)
71-
def getAs[T](columnLabel: String): Option[T] = getObject(columnNumber(columnLabel)).map(_.asInstanceOf[T])
61+
def getAs[T](columnName: String, transformer: TransformerFnc[T]): Option[T] = getAs(columnNumber(columnName), transformer)
62+
def getAs[T](columnName: String): Option[T] = getObject(columnNumber(columnName)).map(_.asInstanceOf[T])
7263

7364
def getObject(column: Int): Option[Object] = fields(column - 1)
74-
def getObject(columnLabel: String): Option[Object] = getObject(columnNumber(columnLabel))
65+
def getObject(columnName: String): Option[Object] = getObject(columnNumber(columnName))
7566

7667
def getBoolean(column: Int): Option[Boolean] = getAs(column: Int, {item: Object => item.asInstanceOf[Boolean]})
77-
def getBoolean(columnLabel: String): Option[Boolean] = getBoolean(columnNumber(columnLabel))
68+
def getBoolean(columnName: String): Option[Boolean] = getBoolean(columnNumber(columnName))
7869

7970
def getChar(column: Int): Option[Char] = {
8071
getString(column) match {
@@ -84,45 +75,45 @@ class QueryResultRow private[classes](val rowNumber: Int,
8475
None
8576
}
8677
}
87-
def getChar(columnLabel: String): Option[Char] = getChar(columnNumber(columnLabel))
78+
def getChar(columnName: String): Option[Char] = getChar(columnNumber(columnName))
8879

8980
def getString(column: Int): Option[String] = getAs(column: Int, {item: Object => item.asInstanceOf[String]})
90-
def getString(columnLabel: String): Option[String] = getString(columnNumber(columnLabel))
81+
def getString(columnName: String): Option[String] = getString(columnNumber(columnName))
9182

9283
def getInt(column: Int): Option[Int] = getAs(column: Int, {item: Object => item.asInstanceOf[Int]})
93-
def getInt(columnLabel: String): Option[Int] = getInt(columnNumber(columnLabel))
84+
def getInt(columnName: String): Option[Int] = getInt(columnNumber(columnName))
9485

9586
def getLong(column: Int): Option[Long] = getAs(column: Int, {item: Object => item.asInstanceOf[Long]})
96-
def getLong(columnLabel: String): Option[Long] = getLong(columnNumber(columnLabel))
87+
def getLong(columnName: String): Option[Long] = getLong(columnNumber(columnName))
9788

9889
def getDouble(column: Int): Option[Double] = getAs(column: Int, {item: Object => item.asInstanceOf[Double]})
99-
def getDouble(columnLabel: String): Option[Double] = getDouble(columnNumber(columnLabel))
90+
def getDouble(columnName: String): Option[Double] = getDouble(columnNumber(columnName))
10091

10192
def getFloat(column: Int): Option[Float] = getAs(column: Int, {item: Object => item.asInstanceOf[Float]})
102-
def getFloat(columnLabel: String): Option[Float] = getFloat(columnNumber(columnLabel))
93+
def getFloat(columnName: String): Option[Float] = getFloat(columnNumber(columnName))
10394

10495
def getBigDecimal(column: Int): Option[BigDecimal] =
10596
getAs(column: Int, {item: Object => item.asInstanceOf[java.math.BigDecimal]})
10697
.map(scala.math.BigDecimal(_))
107-
def getBigDecimal(columnLabel: String): Option[BigDecimal] = getBigDecimal(columnNumber(columnLabel))
98+
def getBigDecimal(columnName: String): Option[BigDecimal] = getBigDecimal(columnNumber(columnName))
10899

109100
def getTime(column: Int): Option[Time] = getAs(column: Int, {item: Object => item.asInstanceOf[Time]})
110-
def getTime(columnLabel: String): Option[Time] = getTime(columnNumber(columnLabel))
101+
def getTime(columnName: String): Option[Time] = getTime(columnNumber(columnName))
111102

112103
def getDate(column: Int): Option[Date] = getAs(column: Int, {item: Object => item.asInstanceOf[Date]})
113-
def getDate(columnLabel: String): Option[Date] = getDate(columnNumber(columnLabel))
104+
def getDate(columnName: String): Option[Date] = getDate(columnNumber(columnName))
114105

115106
def getLocalDateTime(column: Int): Option[LocalDateTime] = getAs(column: Int, {item: Object => item.asInstanceOf[LocalDateTime]})
116-
def getLocalDateTime(columnLabel: String): Option[LocalDateTime] = getLocalDateTime(columnNumber(columnLabel))
107+
def getLocalDateTime(columnName: String): Option[LocalDateTime] = getLocalDateTime(columnNumber(columnName))
117108

118109
def getOffsetDateTime(column: Int): Option[OffsetDateTime] = getAs(column: Int, {item: Object => item.asInstanceOf[OffsetDateTime]})
119-
def getOffsetDateTime(columnLabel: String): Option[OffsetDateTime] = getOffsetDateTime(columnNumber(columnLabel))
110+
def getOffsetDateTime(columnName: String): Option[OffsetDateTime] = getOffsetDateTime(columnNumber(columnName))
120111

121112
def getInstant(column: Int): Option[Instant] = getOffsetDateTime(column).map(_.toInstant)
122-
def getInstant(columnLabel: String): Option[Instant] = getOffsetDateTime(columnLabel).map(_.toInstant)
113+
def getInstant(columnName: String): Option[Instant] = getOffsetDateTime(columnName).map(_.toInstant)
123114

124115
def getUUID(column: Int): Option[UUID] = getAs(column: Int, {item: Object => item.asInstanceOf[UUID]})
125-
def getUUID(columnLabel: String): Option[UUID] = getUUID(columnNumber(columnLabel))
116+
def getUUID(columnName: String): Option[UUID] = getUUID(columnNumber(columnName))
126117

127118
def getArray[T](column: Int): Option[Vector[T]] = {
128119
def transformerFnc(obj: Object): Vector[T] = {
@@ -131,7 +122,7 @@ class QueryResultRow private[classes](val rowNumber: Int,
131122
getAs(column: Int, transformerFnc _)
132123
}
133124

134-
def getArray[T](columnLabel: String): Option[Vector[T]] = getArray[T](columnNumber(columnLabel))
125+
def getArray[T](columnName: String): Option[Vector[T]] = getArray[T](columnNumber(columnName))
135126

136127
def getArray[T](column: Int, itemTransformerFnc: TransformerFnc[T]): Option[Vector[T]] = {
137128
def transformerFnc(obj: Object): Vector[T] = {
@@ -159,7 +150,7 @@ object QueryResultRow {
159150
new QueryResultRow(resultSet.getRow, fields, columnNames)
160151
}
161152

162-
def fieldNamesFromMetadata(metaData: ResultSetMetaData): ColumnNames = {
153+
def columnNamesFromMetadata(metaData: ResultSetMetaData): ColumnNames = {
163154
Range.inclusive(1, metaData.getColumnCount).map(i => metaData.getColumnName(i) -> i).toMap
164155
}
165156

balta/src/main/scala/za/co/absa/db/balta/classes/inner/Params.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ object Params {
115115
* @param paramName - the name of the parameter
116116
* @return - a list parameters to be used in an SQL prepared statement
117117
*/
118-
@deprecated("Use add(NULL)", "balta 0.3.0")
118+
@deprecated("Use add(paramName, NULL)", "balta 0.3.0")
119119
def addNull(paramName: String): NamedParams = {
120120
add(paramName, QueryParamType.NULL)
121121
}

balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,19 @@ object QueryResultRowImplicits {
7272
constructor.paramLists.flatten.map { param =>
7373
val name = param.name.decodedName.toString
7474
val paramType = param.typeSignature
75-
val columnLabel = namingConvention.stringPerConvention(name)
76-
getParamValue(columnLabel, paramType)
75+
val columnName = namingConvention.stringPerConvention(name)
76+
getParamValue(columnName, paramType)
7777
}
7878

7979
}
8080

81-
private def getParamValue[T: TypeTag](columnLabel: String, expectedType: Type): Any = {
82-
val value = row(columnLabel)
81+
private def getParamValue[T: TypeTag](columnName: String, expectedType: Type): Any = {
82+
//TODO TypeTag is not used, consider removing it https://github.com/AbsaOSS/balta/issues/64
83+
val value = row(columnName)
8384
if (isOptionType(expectedType)) {
8485
value
8586
} else {
86-
value.getOrThrow(new NullPointerException(s"Column '$columnLabel' is null"))
87+
value.getOrThrow(new NullPointerException(s"Column '$columnName' is null"))
8788
}
8889
}
8990
}

balta/src/main/scala/za/co/absa/db/balta/postgres/implicits/Postgres.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ object Postgres {
2525
private def simpleJsonStringTransformer(obj: Object): SimpleJsonString = SimpleJsonString(obj.asInstanceOf[PGobject].toString)
2626

2727
def getSimpleJsonString(column: Int): Option[SimpleJsonString] = row.getAs[SimpleJsonString](column: Int, simpleJsonStringTransformer _)
28-
def getSimpleJsonString(columnLabel: String): Option[SimpleJsonString] = getSimpleJsonString(row.columnNumber(columnLabel))
28+
def getSimpleJsonString(columnName: String): Option[SimpleJsonString] = getSimpleJsonString(row.columnNumber(columnName))
2929

3030
def getSJSArray(column: Int): Option[Vector[SimpleJsonString]] =
3131
row.getArray(column: Int, item => SimpleJsonString(item.asInstanceOf[String]))
32-
def getSJSArray(columnLabel: String): Option[Vector[SimpleJsonString]] = getSJSArray(row.columnNumber(columnLabel))
32+
def getSJSArray(columnName: String): Option[Vector[SimpleJsonString]] = getSJSArray(row.columnNumber(columnName))
3333

3434
}
3535
}

balta/src/main/scala/za/co/absa/db/mag/core/ColumnReference.scala

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ package za.co.absa.db.mag.core
1818

1919
trait ColumnReference extends SqlItem
2020

21-
abstract class ColumnName extends ColumnReference{
22-
def enteredName: String
23-
def sqlEntry: String
21+
case class ColumnName private(enteredName: String,
22+
sqlEntry: String,
23+
quoteLess: String
24+
) extends ColumnReference{
2425
override def equals(obj: Any): Boolean = {
2526
obj match {
2627
case that: ColumnName => this.sqlEntry == that.sqlEntry
@@ -32,40 +33,48 @@ abstract class ColumnName extends ColumnReference{
3233

3334
object ColumnReference {
3435
private val regularColumnNamePattern = "^([a-z_][a-z0-9_]*)$".r
36+
private val mixedCaseColumnNamePattern = "^[a-zA-Z_][a-zA-Z0-9_]*$".r
3537
private val quotedRegularColumnNamePattern = "^\"([a-z_][a-z0-9_]*)\"$".r
3638
private val quotedColumnNamePattern = "^\"(.+)\"$".r
3739

38-
private def quote(stringToQuote: String): String = s""""$stringToQuote""""
39-
private def escapeQuote(stringToEscape: String): String = stringToEscape.replace("\"", "\"\"")
40+
private[core] def quote(stringToQuote: String): String = s""""$stringToQuote""""
41+
private[core] def escapeQuote(stringToEscape: String): String = stringToEscape.replace("\"", "\"\"")
42+
private[core] def hasUnescapedQuotes(name: String): Boolean = {
43+
val reduced = name.replace("\"\"", "")
44+
reduced.contains('"')
45+
}
4046

4147
def apply(name: String): ColumnName = {
4248
val trimmedName = name.trim
4349
trimmedName match {
44-
case regularColumnNamePattern(columnName) => ColumnNameSimple(columnName) // column name per SQL standard, no quoting needed
45-
case quotedRegularColumnNamePattern(columnName) => ColumnNameExact(trimmedName, columnName) // quoted but regular name, remove quotes
46-
case quotedColumnNamePattern(_) => ColumnNameSimple(trimmedName) // quoted name, use as is
47-
case _ => ColumnNameExact(trimmedName, quote(escapeQuote(trimmedName))) // needs quoting and perhaps escaping
50+
case regularColumnNamePattern(columnName) =>
51+
ColumnName(columnName, columnName, columnName) // column name per SQL standard, no quoting needed
52+
case mixedCaseColumnNamePattern() =>
53+
val loweredColumnName = trimmedName.toLowerCase
54+
ColumnName(trimmedName, loweredColumnName, loweredColumnName) // mixed case name, turn to lower case for sql entry (per standard)
55+
case quotedRegularColumnNamePattern(columnName) =>
56+
ColumnName(trimmedName, columnName, columnName) // quoted but regular name, remove quotes
57+
case quotedColumnNamePattern(actualColumnName) =>
58+
if (hasUnescapedQuotes(actualColumnName)) {
59+
throw new IllegalArgumentException(s"Column name '$actualColumnName' has unescaped quotes. Use double quotes as escape sequence.")
60+
}
61+
val unescapedColumnName = actualColumnName.replace("\"\"", "\"")
62+
ColumnName(trimmedName, trimmedName, unescapedColumnName) // quoted name, use as is
63+
case _ =>
64+
ColumnName(trimmedName, quote(escapeQuote(trimmedName)), trimmedName) // needs quoting and perhaps escaping
4865
}
4966
}
5067

5168
def apply(index: Int): ColumnReference = {
5269
ColumnIndex(index)
5370
}
5471

55-
def unapply(columnName: ColumnName): String = columnName.enteredName
56-
57-
final case class ColumnNameSimple private(enteredName: String) extends ColumnName {
58-
override def sqlEntry: String = enteredName
59-
}
60-
61-
final case class ColumnNameExact private(enteredName: String, sqlEntry: String) extends ColumnName
62-
6372
final case class ColumnIndex private(index: Int) extends ColumnReference {
6473
val sqlEntry: String = index.toString
6574
}
6675
}
6776

6877
object ColumnName {
6978
def apply(name: String): ColumnName = ColumnReference(name)
70-
def unapply(columnName: ColumnName): String = columnName.enteredName
79+
def unapply(columnName: ColumnName): Option[String] = Option(columnName.enteredName)
7180
}

0 commit comments

Comments
 (0)