view pkg/pgxutils/errors.go @ 5095:e21cbb9768a2

Prevent duplicate fairway areas In principal, there can be only one or no fairway area at each point on the map. Since polygons from real data will often be topologically inexact, just disallow equal geometries. This will also help to avoid importing duplicates with concurrent imports, once the history of fairway dimensions will be preserved.
author Tom Gottfried <tom@intevation.de>
date Wed, 25 Mar 2020 18:10:02 +0100
parents 8c590ef35280
children 73563c4bba5b
line wrap: on
line source

// This is Free Software under GNU Affero General Public License v >= 3.0
// without warranty, see README.md and license for details.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// License-Filename: LICENSES/AGPL-3.0.txt
//
// Copyright (C) 2019 by via donau
//   – Österreichische Wasserstraßen-Gesellschaft mbH
// Software engineering by Intevation GmbH
//
// Author(s):
//  * Tom Gottfried <tom.gottfried@intevation.de>
//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>

package pgxutils

import (
	"net/http"
	"strings"

	"github.com/jackc/pgx"
)

const (
	// camel cased condition name = error code
	// from appendix A of PostgreSQL documentation
	notNullViolation      = "23502"
	foreignKeyViolation   = "23503"
	uniqueViolation       = "23505"
	checkViolation        = "23514"
	exclusionViolation    = "23P01"
	insufficientPrivilege = "42501"
	duplicateObject       = "42710"
	noDataFound           = "P0002"
)

// ReadableError wraps a given error Err and
// permits extraction of more user-friendly
// error messages from it in case it is an error
// from the PostgreSQL backend.
type ReadableError struct {
	Err error
}

func (re ReadableError) Error() string {
	m, _ := re.MessageAndCode()
	return m
}

// MessageAndCode returns a user-readable message
// and a matching HTTP status code.
// If its not a pgx.PgError it defaults to
// calling the parent Error method and returns its
// result together with http.StatusInternalServerError.
func (re ReadableError) MessageAndCode() (string, int) {
	if e, ok := re.Err.(pgx.PgError); ok {
		return messageAndCode(e)
	}
	return re.Err.Error(), http.StatusInternalServerError
}

func messageAndCode(err pgx.PgError) (m string, c int) {

	c = http.StatusInternalServerError

	// Most recent line from stacktrace contains failed statement
	recent := strings.SplitN(err.Where, "\n", 1)[0]

	switch err.Code {
	case notNullViolation:
		switch err.SchemaName {
		case "waterway":
			switch err.TableName {
			case "gauges":
				switch err.ColumnName {
				case "objname":
					m = "Missing objname"
					return
				case "geom":
					m = "Missing lat/lon"
					return
				case "zero_point":
					m = "Missing zeropoint"
					return
				}
			}
		}
	case foreignKeyViolation:
		switch err.SchemaName {
		case "waterway":
			switch err.TableName {
			case "gauge_measurements", "gauge_predictions", "bottlenecks":
				switch err.ConstraintName {
				case "waterway_bottlenecks_reference_gauge",
					"waterway_gauge_measurements_reference_gauge",
					"waterway_gauge_predictions_reference_gauge":
					m = "Referenced gauge with matching temporal validity not available"
					return
				}
			}
			switch err.TableName {
			case "fairway_marks_bcnlat_dirimps",
				"fairway_marks_daymar_dirimps",
				"fairway_marks_notmrk_dirimps":
				switch err.ConstraintName {
				case "fairway_marks_bcnlat_dirimps_dirimp_fkey",
					"fairway_marks_daymar_dirimps_dirimp_fkey",
					"fairway_marks_notmrk_dirimps_dirimp_fkey":
					m = "Invalid value for dirimp"
					return
				}
			}
		}
	case uniqueViolation:
		switch err.SchemaName {
		case "users":
			switch err.TableName {
			case "stretches":
				switch err.ConstraintName {
				case "stretches_name_staging_done_key":
					m = "A stretch with that name already exists"
					c = http.StatusConflict
					return
				}
			}
		case "waterway":
			switch err.TableName {
			case "sections":
				switch err.ConstraintName {
				case "sections_name_staging_done_key":
					m = "A section with that name already exists"
					c = http.StatusConflict
					return
				}
			case "fairway_dimensions":
				switch err.ConstraintName {
				case "fairway_dimensions_area_unique":
					m = "Duplicate fairway area"
					c = http.StatusConflict
					return
				}
			}
		}
	case exclusionViolation:
		switch err.SchemaName {
		case "waterway":
			switch err.TableName {
			case "sections":
				switch err.ConstraintName {
				case "sections_name_country_excl":
					m = "A section with that name already exists for another country"
					c = http.StatusConflict
					return
				}
			}
		}
	case checkViolation:
		switch err.SchemaName {
		case "waterway":
			switch err.TableName {
			case "sounding_results":
				switch err.ConstraintName {
				case "b_sounding_results_in_bn_area":
					m = "Dataset does not intersect with given bottleneck"
					c = http.StatusConflict
					return
				}
			case "fairway_dimensions":
				switch err.ConstraintName {
				case "fairway_dimensions_area_check":
					m = "Geometry could not be stored as valid, non-empty polygon"
					return
				}
			}
		case "internal":
			switch err.TableName {
			case "user_profiles":
				switch err.ConstraintName {
				case "user_profiles_username_check":
					m = "User name too long"
					c = http.StatusBadRequest
					return
				}
			}
		}
	case duplicateObject:
		switch {
		case strings.Contains(recent, "CREATE ROLE"):
			m = "A user with that name already exists"
			c = http.StatusConflict
			return
		}
	case noDataFound:
		switch {
		case strings.Contains(recent, "isrsrange_points"):
			m = "No distance mark found for at least one given ISRS Location Code"
			return
		case strings.Contains(recent, "isrsrange_axis"):
			m = "No contiguous axis found between given ISRS Location Codes"
			return
		case strings.Contains(recent, "isrsrange_area"):
			m = "No area around axis between given ISRS Location Codes"
			return
		}
	case insufficientPrivilege:
		m = "Could not save: Data outside the area of responsibility."
		return
	}
	m = "Unexpected database error: " + err.Message
	return
}