exception.PostgresErrorException.scala Maven / Gradle / Ivy
// Copyright (c) 2018-2021 by Rob Norris
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT
package skunk.exception
import cats.syntax.all._
import natchez.TraceValue
import skunk.SqlState
import skunk.data.Type
import skunk.util.Origin
// TODO: turn this into an ADT of structured error types
class PostgresErrorException (
sql: String,
sqlOrigin: Option[Origin],
info: Map[Char, String],
history: List[Either[Any, Any]],
arguments: List[(Type, Option[String])] = Nil,
argumentsOrigin: Option[Origin] = None
) extends SkunkException(
sql = Some(sql),
message = {
val m = info.getOrElse('M', sys.error("Invalid ErrorInfo: no message"))
m.take(1).toUpperCase + m.drop(1) + "."
},
position = info.get('P').map(_.toInt),
detail = info.get('D'),
hint = info.get('H'),
history = history,
arguments = arguments,
sqlOrigin = sqlOrigin,
argumentsOrigin = argumentsOrigin,
) {
override def fields: Map[String, TraceValue] = {
var map = super.fields
map += "error.postgres.message" -> message
map += "error.postgres.severity" -> severity
map += "error.postgres.code" -> code
internalPosition.foreach(a => map += "error.postgres.internalPosition" -> a)
internalQuery .foreach(a => map += "error.postgres.internalQuery" -> a)
where .foreach(a => map += "error.postgres.where" -> a)
schemaName .foreach(a => map += "error.postgres.schemaName" -> a)
tableName .foreach(a => map += "error.postgres.tableName" -> a)
columnName .foreach(a => map += "error.postgres.columnName" -> a)
dataTypeName .foreach(a => map += "error.postgres.dataTypeName" -> a)
constraintName .foreach(a => map += "error.postgres.constraintName" -> a)
fileName .foreach(a => map += "error.postgres.fileName" -> a)
line .foreach(a => map += "error.postgres.line" -> a)
routine .foreach(a => map += "error.postgres.routine" -> a)
map
}
/**
* The field contents are ERROR, FATAL, or PANIC (in an error message), or WARNING, NOTICE, DEBUG,
* INFO, or LOG (in a notice message), or a localized translation of one of these .Always present.
*/
def severity: String =
info.getOrElse('S', sys.error("Invalid ErrorInfo: no severity"))
/** The SQLSTATE code for the error (see Appendix A) .Not localizable .Always present .*/
def code: String =
info.getOrElse('C', sys.error("Invalid ErrorInfo: no code/sqlstate"))
/**
* Defined the same as the P field, but used when the cursor position refers to an internally
* generated command rather than the one submitted by the client .The `query` field will always
* appear when this field appears.
*/
def internalPosition: Option[Int] =
info.get('P').map(_.toInt)
/**
* The text of a failed internally-generated command .This could be, for example, a SQL query
* issued by a PL/pgSQL function.
*/
def internalQuery: Option[String] =
info.get('q')
/**
* An indication of the context in which the error occurred .Presently this includes a call stack
* traceback of active procedural language functions and internally-generated queries .The trace
* is one entry per line, most recent first.
*/
def where: Option[String] =
info.get('w')
/**
* If the error was associated with a specific database object, the name of the schema containing
* that object, if any.
*/
def schemaName: Option[String] =
info.get('s')
/**
* If the error was associated with a specific table, the name of the table .(Refer to the schema
* name field for the name of the table's schema.)
*/
def tableName: Option[String] =
info.get('t')
/**
* If the error was associated with a specific table column, the name of the column .(Refer to
* the schema and table name fields to identify the table.)
*/
def columnName: Option[String] =
info.get('c')
/**
* If the error was associated with a specific data type, the name of the data type .(Refer to
* the schema name field for the name of the data type's schema.)
*/
def dataTypeName: Option[String] =
info.get('d')
/**
* If the error was associated with a specific constraint, the name of the constraint .Refer to
* fields listed above for the associated table or domain .(For this purpose, indexes are treated
* as constraints, even if they weren't created with constraint syntax.)
*/
def constraintName: Option[String] =
info.get('n')
/** The file name of the source-code location where the error was reported .*/
def fileName: Option[String] =
info.get('F')
/** The line number of the source-code location where the error was reported .*/
def line: Option[Int] =
info.get('L').map(_.toInt)
/** The name of the source-code routine reporting the error .*/
def routine: Option[String] =
info.get('R')
// These will likely get abstracted up and out, but for now we'll do it here in a single
// error class.
override def title: String = {
val pgSource = (fileName, line, routine).mapN((f, l, r) => s"raised in $r ($f:$l)")
s"Postgres ${severity} $code ${pgSource.orEmpty}"
}
private def trap: String =
SqlState.values.find(_.code == code).foldMap { st =>
s"""|If this is an error you wish to trap and handle in your application, you can do
|so with a SqlState extractor. For example:
|
| ${Console.GREEN}doSomething.recoverWith { case SqlState.${st}(ex) => ...}${Console.RESET}
|
|""".stripMargin
}
// private def errorResponse: String =
// if (info.isEmpty) "" else
// s"""|ErrorResponse map:
// |
// | ${info.toList.map { case (k, v) => s"$k = $v" } .mkString("\n| ")}
// |
// |""".stripMargin
override def sections =
List(header, statement, args, trap) //, exchanges, errorResponse)
}
object PostgresErrorException {
def raiseError[F[_], A](
sql: String,
sqlOrigin: Option[Origin],
info: Map[Char, String],
history: List[Either[Any, Any]],
arguments: List[(Type, Option[String])] = Nil,
argumentsOrigin: Option[Origin] = None
)(
implicit ev: cats.MonadError[F, Throwable]
): F[A] =
new PostgresErrorException(sql, sqlOrigin, info, history, arguments, argumentsOrigin)
.raiseError[F, A]
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy