class ViewModel::DeserializationError::UniqueViolation

def self.from_postgres_error(err, nodes)

def self.from_postgres_error(err, nodes)
  result         = err.result
  constraint     = result.error_field(PG::PG_DIAG_CONSTRAINT_NAME)
  message_detail = result.error_field(PG::PG_DIAG_MESSAGE_DETAIL)
  columns, values, conflicts = 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, conflicts, nodes)
end

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

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

def meta

def meta
  super.merge(constraint: @constraint, columns: @columns, values: @values, conflicts: @conflicts)
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_identifiers(stream)

def parse_identifiers(stream)
  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
  identifiers
end

def parse_message_detail(detail)

def parse_message_detail(detail)
  stream = detail.dup
  return nil unless stream.delete_prefix!(DETAIL_PREFIX)
  # The message should start with an identifier list: pop off identifier
  # tokens while we can.
  identifiers = parse_identifiers(stream)
  return nil if identifiers.nil?
  # The message should now contain ")=(" followed by the value list and
  # the suffix, potentially including a conflict list. We consider the
  # value and conflict lists to be essentially unparseable because they
  # are free to contain commas and no escaping is used. We make a best
  # effort to extract them anyway.
  values, conflicts =
    if (m = UNIQUE_SUFFIX_TEMPLATE.match(stream))
      m.values_at(:values)
    elsif (m = EXCLUSION_SUFFIX_TEMPLATE.match(stream))
      m.values_at(:values, :conflicts)
    else
      return nil
    end
  return identifiers, values, conflicts
end