--
-- Error model: how Ruby exceptions and PostgreSQL errors interact, what is
-- preserved when a database error is caught, and what is not.
--

-- A specific exception class's message is preserved when it propagates out.
CREATE FUNCTION es_custom() RETURNS int LANGUAGE plruby AS $$
    raise ArgumentError, 'custom failure'
$$;
SELECT es_custom();

-- A caught database error: it is a StandardError, its message names the
-- offending object, its SQLSTATE is exposed, and the session keeps working
-- afterwards.
CREATE FUNCTION es_caught() RETURNS text LANGUAGE plruby AS $$
    begin
        spi_exec('select * from missing_relation_xyz')
        'no error'
    rescue => e
        "std=#{e.is_a?(StandardError)} names_it=#{e.message.include?('missing_relation_xyz')} sqlstate=#{e.sqlstate}"
    end
$$;
SELECT es_caught();

-- The SQLSTATE reflects the error class (division_by_zero here).  A
-- PLRuby::Error not backed by a database error (elog) has a nil sqlstate,
-- and an ordinary Ruby exception has no sqlstate method at all.
CREATE FUNCTION es_sqlstate() RETURNS text LANGUAGE plruby AS $$
    db = begin
             spi_exec('select 1 / 0')
             'no error'
         rescue => e
             e.sqlstate
         end
    pl = begin
             elog('ERROR', 'synthetic')
         rescue PLRuby::Error => e
             e.sqlstate.inspect
         end
    rb = begin
             raise 'plain ruby error'
         rescue => e
             e.respond_to?(:sqlstate) ? 'has method' : 'no method'
         end
    "db=#{db} elog=#{pl} ruby=#{rb}"
$$;
SELECT es_sqlstate();

-- An ensure block runs on both the success and the rescue path.
CREATE FUNCTION es_ensure(boolean) RETURNS text LANGUAGE plruby AS $$
    log = []
    r = begin
            raise 'boom' if args[0]
            'ok'
        rescue
            'rescued'
        ensure
            log << 'ensured'
        end
    "#{r}:#{log.join}"
$$;
SELECT es_ensure(false);
SELECT es_ensure(true);

-- pg_raise can attach DETAIL, HINT, and a specific SQLSTATE...
CREATE FUNCTION es_structured() RETURNS void LANGUAGE plruby AS $$
    pg_raise('ERROR', 'order rejected',
             detail: 'order 42 exceeds the credit limit',
             hint: 'raise the limit or split the order',
             sqlstate: 'P0001')
$$;
SELECT es_structured();

-- ...and a caller that rescues the resulting database error sees all three.
CREATE FUNCTION es_catch_structured() RETURNS text LANGUAGE plruby AS $$
    begin
        spi_exec('select es_structured()')
        'no error'
    rescue PLRuby::Error => e
        "state=#{e.sqlstate} detail=#{e.detail} hint=#{e.hint}"
    end
$$;
SELECT es_catch_structured();

-- Non-error levels emit the fields directly.
CREATE FUNCTION es_notice_fields() RETURNS void LANGUAGE plruby AS $$
    pg_raise('NOTICE', 'heads up', detail: 'the details', hint: 'the hint')
$$;
SELECT es_notice_fields();

-- An invalid sqlstate is rejected at the call.
CREATE FUNCTION es_badstate() RETURNS void LANGUAGE plruby AS $$
    pg_raise('ERROR', 'nope', sqlstate: 'abc')
$$;
SELECT es_badstate();

-- pg_raise and elog both reject an unknown level with a PL/Ruby error.
CREATE FUNCTION es_badraise() RETURNS void LANGUAGE plruby AS $$
    pg_raise('BOGUS', 'nope')
$$;
SELECT es_badraise();
CREATE FUNCTION es_badelog() RETURNS void LANGUAGE plruby AS $$
    elog('BOGUS', 'nope')
$$;
SELECT es_badelog();
