class ViewModel::DeserializationError::UniqueViolation

def self.from_postgres_error(err, nodes)

Not exposed in pg gem
def self.from_postgres_error(err, nodes)
  result         = err.result
  constraint     = result.error_field(PG_ERROR_FIELD_CONSTRAINT_NAME)
  message_detail = result.error_field(PG::Result::PG_DIAG_MESSAGE_DETAIL)
  columns, values = parse_message_detail(message_detail)
  unless columns
    # Couldn't parse the detail message, fall back on an unparsed error
    return DatabaseConstraint.new(err.message, nodes)
  end
  self.new(err.message, constraint, columns, values, nodes)
end

def initialize(detail, constraint, columns, values, nodes = [])

def initialize(detail, constraint, columns, values, nodes = [])
  @detail     = detail
  @constraint = constraint
  @columns    = columns
  @values     = values
  super(nodes)
end

def meta

def meta
  super.merge(constraint: @constraint, columns: @columns, values: @values)
end

def parse_identifier(stream)

def parse_identifier(stream)
  if (identifier = stream.slice!(UNQUOTED_IDENTIFIER))
    identifier
  elsif (quoted_identifier = stream.slice!(QUOTED_IDENTIFIER))
    quoted_identifier[1..-2].gsub('""', '"')
  else
    nil
  end
end

def parse_message_detail(detail)

def parse_message_detail(detail)
  stream = detail.dup
  return nil unless stream.delete_prefix!(DETAIL_PREFIX)
  return nil unless stream.delete_suffix!(DETAIL_SUFFIX)
  # The message should start with an identifier list: pop off identifier
  # tokens while we can.
  identifiers = []
  identifier = parse_identifier(stream)
  return nil unless identifier
  identifiers << identifier
  while stream.delete_prefix!(', ')
    identifier = parse_identifier(stream)
    return nil unless identifier
    identifiers << identifier
  end
  # The message should now contain ")=(" followed by the (unparseable)
  # value list.
  return nil unless stream.delete_prefix!(DETAIL_INFIX)
  [identifiers, stream]
end