All Downloads are FREE. Search and download functionalities are using the official Maven repository.

busymachines.pureharm.db.psql.PSQLExceptionInterpreters.scala Maven / Gradle / Ivy

The newest version!
/** Copyright (c) 2019 BusyMachines
  *
  * See company homepage at: https://www.busymachines.com/
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  * http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
package busymachines.pureharm.db.psql

import busymachines.pureharm.db._
import busymachines.pureharm.effects._
import busymachines.pureharm.effects.implicits._
import org.postgresql.util._

/** @author Lorand Szakacs, https://github.com/lorandszakacs
  * @since 02 Jul 2020
  */
object PSQLExceptionInterpreters {

  object PSQLStates {
    val NotNullViolation:    String = PSQLState.NOT_NULL_VIOLATION.getState
    val ForeignKeyViolation: String = PSQLState.FOREIGN_KEY_VIOLATION.getState
    val UniqueViolation:     String = PSQLState.UNIQUE_VIOLATION.getState
  }

  object PSQLErrorParsers {
    import atto._, Atto._
    private val underscore  = char('_')
    private val closeParens = char(')')
    private val doubleQuote = char('"')
    private val column: Parser[String] = many1(letterOrDigit | underscore).map(_.mkString_(""))

    private val untilNextDoubleQuote: Parser[String] =
      manyUntil(anyChar, doubleQuote).map(_.mkString(""))

    /** Consumes the last parenthesis as well
      */
    def untilLastClosedParens: Parser[String] = for {
      head      <- manyUntil(anyChar, closeParens).map(_.mkString(""))
      remaining <- get
      tail      <- if (remaining.contains(')')) untilLastClosedParens else "".pure[Parser]
    } yield s"$head$tail"

    /** usually has the value of format:
      * {{{
      *   Key (id)=(row1_id) already exists.
      * }}}
      */
    object unique {

      val parser: Parser[(String, String)] = for {
        _          <- string("Key (")
        columnName <- column
        _          <- string(")=(")
        value      <- untilLastClosedParens
        _          <- string(" already exists.")
      } yield (columnName, value)

      def apply[F[_]: MonadAttempt](s: String): F[(String, String)] =
        parser.parse(s).done.either.leftMap(s => new RuntimeException(s): Throwable).liftTo[F]

    }

    /** example:
      * {{{
      *   Key (row_id)=(120-3921-039213) is not present in table "pureharm_rows".
      * }}}
      */
    object foreignKey {

      private val parser: Parser[(String, String, String)] = for {
        _          <- string("Key (")
        columnName <- column
        _          <- string(")=(")
        value      <- untilLastClosedParens
        _          <- untilNextDoubleQuote
        table      <- untilNextDoubleQuote
      } yield (columnName, value, table)

      def apply[F[_]: MonadAttempt](s: String): F[(String, String, String)] =
        parser.parse(s).done.either.leftMap(s => new RuntimeException(s): Throwable).liftTo[F]
    }

  }

  /** Only call when [[PSQLException#getSQLState]] == [[PSQLStates.UniqueViolation]]
    *
    * Will attempt to extract the values of the state by doing regex over the
    * error message... yey, java?
    */
  def uniqueKey(e: PSQLException): Attempt[DBUniqueConstraintViolationAnomaly] =
    PSQLExceptionInterpreters.PSQLErrorParsers
      .unique[Attempt](e.getServerErrorMessage.getDetail)
      .map(t => DBUniqueConstraintViolationAnomaly(t._1, t._2))

  /** Only call when [[PSQLException#getSQLState]] == [[PSQLStates.ForeignKeyViolation]]
    *
    * Will attempt to extract the values of the state by doing regex over the
    * error message... yey, java?
    */
  def foreignKey(e: PSQLException): Attempt[DBForeignKeyConstraintViolationAnomaly] = {
    val msg = e.getServerErrorMessage
    for {
      table                   <- Option(msg.getTable).liftTo[Attempt](e)
      constraint              <- Option(msg.getConstraint).liftTo[Attempt](e)
      (column, value, fTable) <- PSQLErrorParsers.foreignKey[Attempt](msg.getDetail)
    } yield DBForeignKeyConstraintViolationAnomaly(
      table        = table,
      constraint   = constraint,
      column       = column,
      value        = value,
      foreignTable = fTable,
    )
  }

  lazy val adapt: PartialFunction[Throwable, Throwable] = {
    case e: PSQLException =>
      e.getSQLState match {
        case PSQLStates.UniqueViolation     => uniqueKey(e).getOrElse(e)
        case PSQLStates.ForeignKeyViolation => foreignKey(e).getOrElse(e)
        case _                              => e
      }
    case e => e
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy