Skip to content

Commit 33a455e

Browse files
authored
Merge pull request #2679 from simonredfern/develop
DBUtil for MS SQL Server for NVARCHAR JDBC type -9 cols
2 parents a7c2278 + 5d04c20 commit 33a455e

File tree

5 files changed

+104
-22
lines changed

5 files changed

+104
-22
lines changed

obp-api/src/main/scala/code/api/util/DBUtil.scala

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,77 @@
11
package code.api.util
22

33
import code.api.Constant
4+
import net.liftweb.db.{DB, DefaultConnectionIdentifier}
5+
import net.liftweb.util.Helpers.tryo
6+
7+
import java.sql.{ResultSet, Types}
48

59
object DBUtil {
610
def dbUrl: String = APIUtil.getPropsValue("db.url") openOr Constant.h2DatabaseDefaultUrlValue
11+
12+
def isSqlServer: Boolean = dbUrl.contains("sqlserver")
13+
14+
/**
15+
* SQL Server-safe alternative to Lift's DB.runQuery.
16+
*
17+
* Lift's DB.runQuery uses DB.asString which doesn't handle SQL Server's NVARCHAR type
18+
* (JDBC type -9), causing MatchError. This function handles all JDBC types properly.
19+
*
20+
* @param query SQL query string
21+
* @param params Query parameters (for prepared statement)
22+
* @return Tuple of (column names, rows as List[List[String]])
23+
*/
24+
def runQuery(query: String, params: List[String] = Nil): (List[String], List[List[String]]) = {
25+
DB.use(DefaultConnectionIdentifier) { conn =>
26+
val stmt = conn.prepareStatement(query)
27+
try {
28+
// Set parameters
29+
params.zipWithIndex.foreach { case (param, idx) =>
30+
stmt.setString(idx + 1, param)
31+
}
32+
33+
val rs = stmt.executeQuery()
34+
val meta = rs.getMetaData
35+
val colCount = meta.getColumnCount
36+
37+
// Get column names
38+
val colNames = (1 to colCount).map(i => meta.getColumnName(i)).toList
39+
40+
// Get rows - convert all types to String safely
41+
var rows = List[List[String]]()
42+
while (rs.next()) {
43+
val row = (1 to colCount).map { i =>
44+
safeGetString(rs, i, meta.getColumnType(i))
45+
}.toList
46+
rows = rows :+ row
47+
}
48+
49+
(colNames, rows)
50+
} finally {
51+
stmt.close()
52+
}
53+
}
54+
}
55+
56+
/**
57+
* Safely convert any JDBC type to String, including SQL Server's NVARCHAR (-9).
58+
*/
59+
private def safeGetString(rs: ResultSet, columnIndex: Int, jdbcType: Int): String = {
60+
val value = jdbcType match {
61+
case Types.NVARCHAR | Types.NCHAR | Types.LONGNVARCHAR | Types.NCLOB =>
62+
// SQL Server NVARCHAR types that Lift doesn't handle
63+
rs.getNString(columnIndex)
64+
case Types.CLOB =>
65+
val clob = rs.getClob(columnIndex)
66+
if (clob != null) clob.getSubString(1, clob.length().toInt) else null
67+
case Types.BLOB =>
68+
val blob = rs.getBlob(columnIndex)
69+
if (blob != null) new String(blob.getBytes(1, blob.length().toInt)) else null
70+
case _ =>
71+
rs.getString(columnIndex)
72+
}
73+
if (rs.wasNull()) null else value
74+
}
775

876
def getDbConnectionParameters: (String, String, String) = {
977
dbUrl.contains("jdbc:h2") match {

obp-api/src/main/scala/code/metrics/MappedMetrics.scala

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,8 @@ object MappedMetrics extends APIMetrics with MdcLoggable{
436436
AND (${trueOrFalse(excludeAppNames.isEmpty) } or appname not in ($excludeAppNamesList))
437437
AND (${trueOrFalse(excludeImplementedByPartialFunctions.isEmpty) } or implementedbypartialfunction not in ($excludeImplementedByPartialFunctionsList))
438438
""".stripMargin
439-
val (_, rows) = DB.runQuery(sqlQuery, List())
439+
// Use DBUtil.runQuery which handles SQL Server NVARCHAR properly
440+
val (_, rows) = DBUtil.runQuery(sqlQuery)
440441
logger.debug("code.metrics.MappedMetrics.getAllAggregateMetricsBox.sqlQuery --: " + sqlQuery)
441442
logger.info(s"getAllAggregateMetricsBox - Query executed, returned ${rows.length} rows")
442443
val sqlResult = rows.map(
@@ -504,13 +505,13 @@ object MappedMetrics extends APIMetrics with MdcLoggable{
504505

505506
val (dbUrl, _, _) = DBUtil.getDbConnectionParameters
506507

507-
val result: List[TopApi] = {
508+
val result: Box[List[TopApi]] = tryo {
508509
// MS SQL server has the specific syntax for limiting number of rows
509510
val msSqlLimit = if (dbUrl.contains("sqlserver")) s"TOP ($limit)" else s""
510511
// TODO Make it work in case of Oracle database
511512
val otherDbLimit = if (dbUrl.contains("sqlserver")) s"" else s"LIMIT $limit"
512513
val sqlQuery: String =
513-
s"""SELECT ${msSqlLimit} count(*), metric.implementedbypartialfunction, metric.implementedinversion
514+
s"""SELECT ${msSqlLimit} count(*), metric.implementedbypartialfunction, metric.implementedinversion
514515
FROM metric
515516
WHERE
516517
date_c >= '${sqlTimestamp(fromDate.get)}' AND
@@ -522,29 +523,35 @@ object MappedMetrics extends APIMetrics with MdcLoggable{
522523
AND (${trueOrFalse(url.isEmpty)} or url = ${url.getOrElse("null")})
523524
AND (${trueOrFalse(appName.isEmpty)} or appname = ${appName.getOrElse("null")})
524525
AND (${trueOrFalse(verb.isEmpty)} or verb = ${verb.getOrElse("null")})
525-
AND (${falseOrTrue(anon.isDefined && anon.equals(Some(true)))} or userid = null)
526-
AND (${falseOrTrue(anon.isDefined && anon.equals(Some(false)))} or userid != null)
526+
AND (${falseOrTrue(anon.isDefined && anon.equals(Some(true)))} or userid = null)
527+
AND (${falseOrTrue(anon.isDefined && anon.equals(Some(false)))} or userid != null)
527528
AND (${trueOrFalse(httpStatusCode.isEmpty)} or httpcode = ${sqlFriendlyInt(httpStatusCode)})
528529
AND (${trueOrFalse(excludeUrlPatterns.isEmpty)} or (url NOT LIKE ($excludeUrlPatternsQueries)))
529530
AND (${trueOrFalse(excludeAppNames.isEmpty)} or appname not in ($excludeAppNamesNumberList))
530531
AND (${trueOrFalse(excludeImplementedByPartialFunctions.isEmpty)} or implementedbypartialfunction not in ($excludeImplementedByPartialFunctionsNumberList))
531-
GROUP BY metric.implementedbypartialfunction, metric.implementedinversion
532+
GROUP BY metric.implementedbypartialfunction, metric.implementedinversion
532533
ORDER BY count(*) DESC
533534
${otherDbLimit}
534535
""".stripMargin
535536

536-
val (_, rows) = DB.runQuery(sqlQuery, List())
537+
logger.debug(s"getTopApisFuture SQL query: $sqlQuery")
538+
// Use DBUtil.runQuery which handles SQL Server NVARCHAR properly
539+
val (_, rows) = DBUtil.runQuery(sqlQuery)
540+
logger.debug(s"getTopApisFuture returned ${rows.length} rows")
541+
if (rows.nonEmpty) {
542+
logger.debug(s"getTopApisFuture first row sample: ${rows.head}")
543+
}
537544
val sqlResult =
538545
rows.map { rs => // Map result to case class
539546
TopApi(
540-
rs(0).toInt,
547+
tryo(rs(0).toInt).getOrElse(0), // Safe conversion with fallback
541548
rs(1),
542549
rs(2)
543550
)
544551
}
545552
sqlResult
546553
}
547-
tryo(result)
554+
result
548555
}}
549556
}}
550557

@@ -591,11 +598,10 @@ object MappedMetrics extends APIMetrics with MdcLoggable{
591598
val msSqlLimit = if (dbUrl.contains("sqlserver")) s"TOP ($limit)" else s""
592599
// TODO Make it work in case of Oracle database
593600
val otherDbLimit: String = if (dbUrl.contains("sqlserver")) s"" else s"LIMIT $limit"
594-
595601
val result: List[TopConsumer] = {
596602
val sqlQuery =
597-
s"""SELECT ${msSqlLimit} count(*) as count, consumer.id as consumerprimaryid, metric.appname as appname,
598-
consumer.developeremail as email, consumer.consumerid as consumerid
603+
s"""SELECT ${msSqlLimit} count(*) as count, consumer.id as consumerprimaryid, metric.appname as appname,
604+
consumer.developeremail as email, consumer.consumerid as consumerid
599605
FROM metric, consumer
600606
WHERE metric.appname = consumer.name
601607
AND date_c >= '${sqlTimestamp(fromDate.get)}'
@@ -613,11 +619,12 @@ object MappedMetrics extends APIMetrics with MdcLoggable{
613619
AND (${trueOrFalse(excludeUrlPatterns.isEmpty) } or (url NOT LIKE ($excludeUrlPatternsQueries)))
614620
AND (${trueOrFalse(excludeAppNames.isEmpty) } or appname not in ($excludeAppNamesList))
615621
AND (${trueOrFalse(excludeImplementedByPartialFunctions.isEmpty) } or implementedbypartialfunction not in ($excludeImplementedByPartialFunctionsList))
616-
GROUP BY appname, consumer.developeremail, consumer.id, consumer.consumerid
622+
GROUP BY appname, consumer.developeremail, consumer.id, consumer.consumerid
617623
ORDER BY count DESC
618624
${otherDbLimit}
619625
""".stripMargin
620-
val (_, rows) = DB.runQuery(sqlQuery, List())
626+
// Use DBUtil.runQuery which handles SQL Server NVARCHAR properly
627+
val (_, rows) = DBUtil.runQuery(sqlQuery)
621628
val sqlResult =
622629
rows.map { rs => // Map result to case class
623630
TopConsumer(

obp-api/src/main/scala/code/model/dataAccess/ResourceUser.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import java.util.UUID.randomUUID
3131

3232
import code.api.Constant
3333
import code.api.cache.Caching
34-
import code.api.util.APIUtil
34+
import code.api.util.{APIUtil, DBUtil}
3535
import code.util.MappedUUID
3636
import com.openbankproject.commons.model.{User, UserPrimaryKey}
3737
import com.tesobe.CacheKeyFromArguments
@@ -141,7 +141,8 @@ object ResourceUser extends ResourceUser with LongKeyedMetaMapper[ResourceUser]{
141141
CacheKeyFromArguments.buildCacheKey {
142142
Caching.memoizeSyncWithProvider(Some(cacheKey.toString()))(cacheTTL.seconds) {
143143
val sql = "SELECT DISTINCT provider_ FROM resourceuser ORDER BY provider_"
144-
val (_, rows) = DB.runQuery(sql, List())
144+
// Use DBUtil.runQuery which handles SQL Server NVARCHAR properly
145+
val (_, rows) = DBUtil.runQuery(sql)
145146
rows.flatten
146147
}
147148
}

obp-api/src/main/scala/code/util/AttributeQueryTrait.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package code.util
22

3+
import code.api.util.DBUtil
34
import com.openbankproject.commons.model.BankId
4-
import net.liftweb.mapper.{BaseMappedField, BaseMetaMapper, DB}
5+
import net.liftweb.mapper.{BaseMappedField, BaseMetaMapper}
56

67
import scala.collection.immutable.List
78

@@ -39,7 +40,8 @@ trait AttributeQueryTrait { self: BaseMetaMapper =>
3940
def getParentIdByParams(bankId: BankId, params: Map[String, List[String]]): List[String] = {
4041
if (params.isEmpty) {
4142
val sql = s"SELECT DISTINCT attr.$parentIdColumn FROM $tableName attr where attr.$bankIdColumn = ? "
42-
val (_, list) = DB.runQuery(sql, List(bankId.value))
43+
// Use DBUtil.runQuery which handles SQL Server NVARCHAR properly
44+
val (_, list) = DBUtil.runQuery(sql, List(bankId.value))
4345
list.flatten
4446
} else {
4547
val paramList = params.toList
@@ -67,7 +69,8 @@ trait AttributeQueryTrait { self: BaseMetaMapper =>
6769
| AND ($sqlParametersFilter)
6870
|""".stripMargin
6971

70-
val (columnNames: List[String], list: List[List[String]]) = DB.runQuery(sql, bankId.value :: parameters)
72+
// Use DBUtil.runQuery which handles SQL Server NVARCHAR properly
73+
val (columnNames: List[String], list: List[List[String]]) = DBUtil.runQuery(sql, bankId.value :: parameters)
7174
val columnNamesLowerCase = columnNames.map(_.toLowerCase)
7275
val parentIdIndex = columnNamesLowerCase.indexOf(parentIdColumn.toLowerCase)
7376
val nameIndex = columnNamesLowerCase.indexOf(nameColumn.toLowerCase)

obp-api/src/main/scala/code/util/NewAttributeQueryTrait.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package code.util
22

3+
import code.api.util.DBUtil
34
import com.openbankproject.commons.model.BankId
4-
import net.liftweb.mapper.{BaseMappedField, BaseMetaMapper, DB}
5+
import net.liftweb.mapper.{BaseMappedField, BaseMetaMapper}
56

67
import scala.collection.immutable.List
78

@@ -36,7 +37,8 @@ trait NewAttributeQueryTrait {
3637
def getParentIdByParams(bankId: BankId, params: Map[String, List[String]]): List[String] = {
3738
if (params.isEmpty) {
3839
val sql = s"SELECT DISTINCT attr.$parentIdColumn FROM $tableName attr where attr.$bankIdColumn = ? "
39-
val (_, list) = DB.runQuery(sql, List(bankId.value))
40+
// Use DBUtil.runQuery which handles SQL Server NVARCHAR properly
41+
val (_, list) = DBUtil.runQuery(sql, List(bankId.value))
4042
list.flatten
4143
} else {
4244
val paramList = params.toList
@@ -64,7 +66,8 @@ trait NewAttributeQueryTrait {
6466
| AND ($sqlParametersFilter)
6567
|""".stripMargin
6668

67-
val (columnNames: List[String], list: List[List[String]]) = DB.runQuery(sql, bankId.value :: parameters)
69+
// Use DBUtil.runQuery which handles SQL Server NVARCHAR properly
70+
val (columnNames: List[String], list: List[List[String]]) = DBUtil.runQuery(sql, bankId.value :: parameters)
6871
val columnNamesLowerCase = columnNames.map(_.toLowerCase)
6972
val parentIdIndex = columnNamesLowerCase.indexOf(parentIdColumn.toLowerCase)
7073
val nameIndex = columnNamesLowerCase.indexOf(nameColumn.toLowerCase)

0 commit comments

Comments
 (0)