-- plx cross-cutting feature tests: things not tied to one dialect's syntax
-- (OUT params, triggers, record-field assignment, idempotency, string escaping,
-- semantic edge cases, and DO blocks in every dialect).
CREATE EXTENSION IF NOT EXISTS plx;
SET client_min_messages = notice;

-- OUT parameters
CREATE FUNCTION f_out(a int, b int, OUT sum int, OUT diff int) LANGUAGE plxruby AS $$
sum = a + b
diff = a - b
$$;
SELECT * FROM f_out(10, 3);

-- trigger function assigning to NEW.field (plxruby)
CREATE TABLE f_trg(id int, tag text);
CREATE FUNCTION f_trg_fn() RETURNS trigger LANGUAGE plxruby AS $$
NEW.tag = "set-#{NEW.id}"
return NEW
$$;
CREATE TRIGGER f_tr BEFORE INSERT ON f_trg FOR EACH ROW EXECUTE FUNCTION f_trg_fn();
INSERT INTO f_trg(id) VALUES (7);
SELECT tag FROM f_trg;

-- trigger function in plxpython3
CREATE TABLE f_trg2(id int, tag text);
CREATE FUNCTION f_trg_py() RETURNS trigger LANGUAGE plxpython3 AS $$
NEW.tag = f"py-{NEW.id}"
return NEW
$$;
CREATE TRIGGER f_tr2 BEFORE INSERT ON f_trg2 FOR EACH ROW EXECUTE FUNCTION f_trg_py();
INSERT INTO f_trg2(id) VALUES (9);
SELECT tag FROM f_trg2;

-- CREATE OR REPLACE is idempotent and re-transpiles the new body
CREATE FUNCTION f_idem() RETURNS int LANGUAGE plxruby AS $$ return 1 $$;
CREATE OR REPLACE FUNCTION f_idem() RETURNS int LANGUAGE plxruby AS $$ return 2 $$;
SELECT f_idem();
SELECT prosrc ~ '^/[*]plx:v1:' AS has_sentinel FROM pg_proc WHERE proname = 'f_idem';

-- string escaping: apostrophe inside interpolation
CREATE FUNCTION f_apos(n int) RETURNS text LANGUAGE plxruby AS $$
return "it's #{n} o'clock"
$$;
SELECT f_apos(9);

-- escape sequences lower to an E-string
CREATE FUNCTION f_esc() RETURNS boolean LANGUAGE plxruby AS $$
return "a\tb\nc" = E'a\tb\nc'
$$;
SELECT f_esc();

-- literal percent in a RAISE message is preserved
CREATE FUNCTION f_pct() RETURNS void LANGUAGE plxruby AS $$
raise notice: "50% done for #{1 + 1} items"
$$;
SELECT f_pct();

-- exclusive range yields the exact element set (built via interpolation, since
-- + stays numeric)
CREATE FUNCTION f_excl() RETURNS text LANGUAGE plxruby AS $$
out = "" #:: text
for i in 1...4
  out = "#{out}#{i}"
end
return out
$$;
SELECT f_excl();

-- SQL three-valued equality: == maps to plain = (not null-aware), so a
-- comparison involving NULL is unknown and the condition is not taken.
CREATE FUNCTION f_null(a int, b int) RETURNS text LANGUAGE plxruby AS $$
if a == b
  return "eq"
end
return "ne"
$$;
SELECT f_null(NULL, NULL) AS null_vs_null_is_ne,
       f_null(1, NULL) AS val_vs_null_is_ne,
       f_null(2, 2) AS eq;
-- the literal-nil form is the only null special case
CREATE FUNCTION f_isnil(a int) RETURNS text LANGUAGE plxruby AS $$
return a == nil ? "nil" : "notnil"
$$;
SELECT f_isnil(NULL) AS nil, f_isnil(5) AS notnil;

-- DO blocks in every dialect
DO LANGUAGE plxruby $$ raise notice: "ruby do #{2 * 3}" $$;
DO LANGUAGE plxphp $$ raise('notice', 'php do ' . (2 * 3)); $$;
DO LANGUAGE plxjs $$ raise("notice", `js do ${2 * 3}`); $$;
DO LANGUAGE plxpython3 $$
raise('notice', f'py do {2 * 3}')
$$;

-- interpolating a NULL propagates it, so the whole value is NULL in every
-- dialect, the way SQL || behaves
CREATE FUNCTION f_interp_rb(x int) RETURNS text LANGUAGE plxruby AS $$ return "[#{x}]" $$;
CREATE FUNCTION f_interp_php(x int) RETURNS text LANGUAGE plxphp AS $$ return "[{$x}]"; $$;
CREATE FUNCTION f_interp_js(x int) RETURNS text LANGUAGE plxjs AS $$ return `[${x}]`; $$;
CREATE FUNCTION f_interp_py(x int) RETURNS text LANGUAGE plxpython3 AS $$ return f"[{x}]" $$;
SELECT f_interp_rb(NULL) IS NULL AS rb, f_interp_php(NULL) IS NULL AS php,
       f_interp_js(NULL) IS NULL AS js, f_interp_py(NULL) IS NULL AS py;
SELECT f_interp_rb(7) AS rb7;

-- but a message built for RAISE keeps each value as an empty string, so one
-- NULL cannot swallow the text the message was written to carry
CREATE FUNCTION f_msg_rb(x int) RETURNS void LANGUAGE plxruby AS $$
raise notice: "value is [#{x}] here"
$$;
CREATE FUNCTION f_msg_py(x int) RETURNS void LANGUAGE plxpython3 AS $$
raise('notice', f'value is [{x}] here')
$$;
SELECT f_msg_rb(7), f_msg_rb(NULL);
SELECT f_msg_py(7), f_msg_py(NULL);

-- OUT / INOUT parameters (audit): supported across dialects
CREATE FUNCTION f_out(a int, OUT b int, OUT c int) LANGUAGE plxruby AS $$
b = a * 2
c = a + 1
$$;
SELECT b, c FROM f_out(5);
CREATE FUNCTION f_inout(INOUT x int) LANGUAGE plxphp AS $$ $x = $x + 100; $$;
SELECT f_inout(1);

-- RETURNS TABLE: the table columns are OUT parameters, so emit a row with a bare
-- emit / return_next (an argument form is rejected by plpgsql here)
CREATE FUNCTION f_table(n int) RETURNS TABLE(k int, sq int) LANGUAGE plxruby AS $$
for i in 1..n
  k = i
  sq = i * i
  emit
end
return
$$;
SELECT string_agg(k || ':' || sq, ',') FROM f_table(3);

-- SELECT INTO with fetch_one! is STRICT (raises when no row is returned)
CREATE FUNCTION f_strict() RETURNS int LANGUAGE plxruby AS $$
begin
  r = fetch_one!("SELECT 1 AS a WHERE false")
  return r.a
rescue => e
  return -1
end
$$;
SELECT f_strict() AS strict_no_row;
