DROP FUNCTION IF EXISTS relation_sql(text);

CREATE FUNCTION relation_sql(mode text DEFAULT 'status') RETURNS TABLE (
  status text
  , command text
  , schema_name text
  , table_name text
  , fk_name text
  , fk_columns text[]
  , ref_schema_name text
  , ref_table_name text
  , ref_columns text[]
  , direction text
  , calc_name text
  , calc_args text
  , calc_return text
  , calc_body text
  , db_name text
  , db_args text
  , db_return text
  , db_body text
  , drop_db_sql text
  , create_replace_body_sql text
  , status_drop_sql text
) LANGUAGE plpgsql SET search_path FROM CURRENT
AS $fn$
#variable_conflict use_column
DECLARE
  r record;
BEGIN
  IF mode = 'show' THEN
    RETURN QUERY
    WITH fk AS (
      SELECT c.oid fk_oid
        , c.conname fk_name
        , cn.nspname schema_name
        , cn.oid schema_oid
        , cr.relname table_name
        , cr.oid table_oid
        , cr.reltype table_type_oid
        , rn.nspname ref_schema_name
        , rn.oid ref_schema_oid
        , rr.relname ref_table_name
        , rr.oid ref_table_oid
        , rr.reltype ref_table_type_oid
        , c.conkey
        , c.confkey
        , pk.ref_pk_attnum
        , pk.ref_pk_name
      FROM pg_constraint c
      JOIN pg_class cr ON cr.oid = c.conrelid
      JOIN pg_namespace cn ON cn.oid = cr.relnamespace
      JOIN pg_class rr ON rr.oid = c.confrelid
      JOIN pg_namespace rn ON rn.oid = rr.relnamespace
      LEFT JOIN LATERAL (
        SELECT p.conkey[1] ref_pk_attnum, a.attname ref_pk_name
        FROM pg_constraint p
        JOIN pg_attribute a ON (a.attrelid, a.attnum) = (p.conrelid, p.conkey[1])
        WHERE (p.conrelid, p.contype) = (c.confrelid, 'p')
          AND cardinality(p.conkey) = 1
      ) pk ON true
      WHERE c.contype = 'f'
        AND c.conparentid = 0
        AND cn.nspname NOT LIKE 'pg\_%'
        AND cn.nspname <> 'information_schema'
    )
    , fk_col_pos AS (
      SELECT f.fk_oid
        , f.table_oid
        , f.ref_table_oid
        , u.ord
        , a.attname col
        , ra.attname ref_col
        , u.ref_attnum
        , f.ref_pk_attnum
      FROM fk f
      JOIN LATERAL unnest(f.conkey, f.confkey) WITH ORDINALITY u(attnum, ref_attnum, ord) ON true
      JOIN pg_attribute a ON (a.attrelid, a.attnum) = (f.table_oid, u.attnum)
      JOIN pg_attribute ra ON (ra.attrelid, ra.attnum) = (f.ref_table_oid, u.ref_attnum)
    )
    , fk_cols AS (
      SELECT fk_oid
        , array_agg(col ORDER BY ord) fk_columns
        , array_agg(ref_col ORDER BY ord) ref_columns
        , min(col) FILTER (WHERE ref_attnum = ref_pk_attnum) pk_ref_column
      FROM fk_col_pos
      GROUP BY fk_oid
    )
    , pos_variant AS (
      SELECT table_oid, ref_table_oid, ord, count(DISTINCT col) variants
      FROM fk_col_pos
      GROUP BY table_oid, ref_table_oid, ord
    )
    , fk_diff AS (
      SELECT p.fk_oid
        , min(p.col) FILTER (WHERE v.variants > 1) diff_column
        , min(p.ref_col) FILTER (WHERE v.variants > 1) diff_ref_column
        , count(*) FILTER (WHERE v.variants > 1) diff_count
      FROM fk_col_pos p
      JOIN pos_variant v ON (v.table_oid, v.ref_table_oid, v.ord) = (p.table_oid, p.ref_table_oid, p.ord)
      GROUP BY p.fk_oid
    )
    , uniq AS (
      SELECT fk_oid
        , EXISTS (
          SELECT FROM pg_index
          WHERE indrelid = fk.table_oid
            AND indisunique
            AND indpred IS NULL
            AND indexprs IS NULL
            AND (
              SELECT array_agg(t.k) FROM unnest(indkey::smallint[]) WITH ORDINALITY t(k, ord)
              WHERE t.ord <= indnkeyatts
            ) <@ fk.conkey
        ) one_to_one
      FROM fk
    )
    , named AS (
      SELECT f.*
        , fc.fk_columns
        , fc.ref_columns
        , u.one_to_one
        , coalesce(
          CASE WHEN f.fk_name LIKE '%\_rel' AND length(f.fk_name) > 4 THEN left(f.fk_name, -4) END
          , nullif(
            CASE
              WHEN s.sem IS NULL OR w.cut IS NULL THEN NULL
              WHEN s.sem = w.cut THEN ''
              WHEN right(s.sem, length(w.cut) + 1) = '_' || w.cut THEN left(s.sem, -(length(w.cut) + 1))
              WHEN right(s.sem, length(initcap(w.cut))) = initcap(w.cut) THEN left(s.sem, -length(initcap(w.cut)))
              ELSE s.sem
            END
            , ''
          )
          , f.ref_table_name
        ) role_name
        , count(*) OVER (PARTITION BY f.table_oid, f.ref_table_oid) > 1 ambiguous
      FROM fk f
      JOIN fk_cols fc ON fc.fk_oid = f.fk_oid
      JOIN fk_diff fd ON fd.fk_oid = f.fk_oid
      JOIN uniq u ON u.fk_oid = f.fk_oid
      CROSS JOIN LATERAL (
        SELECT CASE
            WHEN cardinality(fc.fk_columns) = 1 THEN fc.fk_columns[1]
            WHEN fc.pk_ref_column IS NOT NULL THEN fc.pk_ref_column
            WHEN fd.diff_count = 1 THEN fd.diff_column
          END sem
          , CASE
            WHEN cardinality(fc.fk_columns) = 1 THEN fc.ref_columns[1]
            WHEN fc.pk_ref_column IS NOT NULL THEN f.ref_pk_name
            WHEN fd.diff_count = 1 THEN fd.diff_ref_column
          END sem_ref
      ) s
      CROSS JOIN LATERAL (
        SELECT CASE WHEN s.sem = s.sem_ref THEN f.ref_pk_name ELSE s.sem_ref END cut
      ) w
    )
    , calc AS (
      SELECT n.schema_name
        , n.table_name
        , n.fk_name
        , n.fk_columns
        , n.ref_schema_name
        , n.ref_table_name
        , n.ref_columns
        , d.direction
        , d.arg_schema
        , d.arg_table
        , d.arg_nsp_oid
        , d.arg_type_oid
        , d.ret_schema
        , d.ret_table
        , d.ret_type_oid
        , (CASE WHEN EXISTS (
            SELECT FROM pg_attribute
            WHERE (attrelid, attname) = (d.arg_table_oid, d.calc_name)
              AND attnum > 0
              AND NOT attisdropped
          ) THEN d.calc_name || '_ref' ELSE d.calc_name END)::name::text calc_name
        , format(
          'SELECT * FROM %I.%I WHERE (%s) = (%s)'
          , d.ret_schema, d.ret_table
          , (
            SELECT string_agg(format('%I', m.col), ', ' ORDER BY m.ord)
            FROM unnest(d.ret_match_columns) WITH ORDINALITY m(col, ord)
          )
          , (
            SELECT string_agg(format('($1).%I', m.col), ', ' ORDER BY m.ord)
            FROM unnest(d.arg_match_columns) WITH ORDINALITY m(col, ord)
          )
        ) calc_body
        , format('%I.%I', d.arg_schema, d.arg_table) calc_args
        , format('SETOF %I.%I', d.ret_schema, d.ret_table) calc_return
      FROM named n
      CROSS JOIN LATERAL (
        VALUES (
          'lookup'
          , n.schema_name, n.table_name, n.schema_oid, n.table_oid, n.table_type_oid
          , n.ref_schema_name, n.ref_table_name, n.ref_table_type_oid
          , n.role_name
          , n.ref_columns, n.fk_columns
        ), (
          'list'
          , n.ref_schema_name, n.ref_table_name, n.ref_schema_oid, n.ref_table_oid, n.ref_table_type_oid
          , n.schema_name, n.table_name, n.table_type_oid
          , CASE WHEN n.ambiguous THEN n.role_name || '_' ELSE '' END
            || n.table_name
            || CASE WHEN n.one_to_one THEN '' ELSE '_list' END
          , n.fk_columns, n.ref_columns
        )
      ) d(direction, arg_schema, arg_table, arg_nsp_oid, arg_table_oid, arg_type_oid, ret_schema, ret_table, ret_type_oid, calc_name, ret_match_columns, arg_match_columns)
    )
    , db AS (
      SELECT c.*
        , format(
          'CREATE OR REPLACE FUNCTION %I.%I(%I.%I) RETURNS SETOF %I.%I LANGUAGE sql STABLE PARALLEL SAFE AS %L;'
          , c.arg_schema, c.calc_name, c.arg_schema, c.arg_table, c.ret_schema, c.ret_table, c.calc_body
        ) create_replace_body_sql
        , format('COMMENT ON FUNCTION %I.%I(%I.%I) IS %L;', c.arg_schema, c.calc_name, c.arg_schema, c.arg_table, 'pg_relation_sql') comment_sql
        , p.oid db_oid
        , p.proname db_name
        , pg_get_function_arguments(p.oid) db_args
        , pg_get_function_result(p.oid) db_return
        , p.prosrc db_body
        , p.proretset db_retset
        , p.prorettype db_rettype_oid
        , p.proparallel db_parallel
        , coalesce(obj_description(p.oid, 'pg_proc') = 'pg_relation_sql', false) db_marked
        , CASE WHEN p.oid IS NOT NULL THEN format('DROP FUNCTION %s;', p.oid::regprocedure) END drop_db_sql
        , count(*) OVER (PARTITION BY c.arg_nsp_oid, c.calc_name, c.arg_type_oid) name_claims
      FROM calc c
      LEFT JOIN pg_proc p ON (p.pronamespace, p.proname) = (c.arg_nsp_oid, c.calc_name)
        AND p.pronargs = 1
        AND p.proargtypes[0] = c.arg_type_oid
    )
    , verdict AS (
      SELECT db.*
        , CASE
          WHEN name_claims > 1 THEN 'DUPLICATE_NAME'
          WHEN db_oid IS NULL THEN 'NEED_CREATE_OR_REPLACE'
          WHEN NOT db_marked THEN 'FOREIGN_FUNCTION'
          WHEN NOT (db_retset AND db_rettype_oid = ret_type_oid) THEN 'NEED_DROP_CREATE'
          WHEN db_body <> calc_body OR db_parallel <> 's' THEN 'NEED_CREATE_OR_REPLACE'
          ELSE 'OK'
        END status
      FROM db
    )
    , orphan AS (
      SELECT pn.nspname schema_name
        , p.proname db_name
        , pg_get_function_arguments(p.oid) db_args
        , pg_get_function_result(p.oid) db_return
        , p.prosrc db_body
        , format('DROP FUNCTION %s;', p.oid::regprocedure) drop_db_sql
      FROM pg_proc p
      JOIN pg_namespace pn ON pn.oid = p.pronamespace
      WHERE obj_description(p.oid, 'pg_proc') = 'pg_relation_sql'
        AND NOT EXISTS (
          SELECT FROM calc c
          WHERE (c.arg_nsp_oid, c.calc_name, c.arg_type_oid) = (p.pronamespace, p.proname, p.proargtypes[0])
        )
    )
    , result AS (
      SELECT status
        , CASE status
          WHEN 'NEED_DROP_CREATE' THEN drop_db_sql || ' ' || create_replace_body_sql || ' ' || comment_sql
          WHEN 'NEED_CREATE_OR_REPLACE' THEN create_replace_body_sql || ' ' || comment_sql
        END command
        , schema_name::text
        , table_name::text
        , fk_name::text
        , fk_columns::text[]
        , ref_schema_name::text
        , ref_table_name::text
        , ref_columns::text[]
        , direction
        , calc_name::text
        , calc_args
        , calc_return
        , calc_body
        , db_name::text
        , db_args
        , db_return
        , db_body
        , drop_db_sql
        , create_replace_body_sql
        , CASE WHEN status NOT IN ('FOREIGN_FUNCTION', 'DUPLICATE_NAME') THEN drop_db_sql END status_drop_sql
      FROM verdict
      UNION ALL
      SELECT 'ORPHANED'
        , drop_db_sql
        , schema_name::text
        , NULL, NULL, NULL::text[], NULL, NULL, NULL::text[], NULL, NULL, NULL, NULL, NULL
        , db_name::text
        , db_args
        , db_return
        , db_body
        , drop_db_sql
        , NULL
        , drop_db_sql
      FROM orphan
    )
    SELECT * FROM result
    ORDER BY 3, 4, 5, 10;
    RETURN;
  ELSIF mode IN ('sync', 'drop') THEN
    FOR r IN
      SELECT c.q
      FROM relation_sql('show') s
      , LATERAL (SELECT CASE mode WHEN 'sync' THEN s.command ELSE s.status_drop_sql END q) c
      WHERE c.q IS NOT NULL
    LOOP
      EXECUTE r.q;
    END LOOP;
  ELSIF mode IN ('install', 'uninstall') THEN
    BEGIN
      EXECUTE 'DROP EVENT TRIGGER IF EXISTS relation_sql_ddl';
      EXECUTE 'DROP FUNCTION IF EXISTS relation_sql_event()';
      IF mode = 'install' THEN
        EXECUTE $e$
          CREATE FUNCTION relation_sql_event() RETURNS event_trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT
          AS $b$ BEGIN PERFORM count(*) FROM relation_sql('sync'); EXCEPTION WHEN OTHERS THEN RAISE WARNING 'pg_relation_sql sync failed: %', SQLERRM; END $b$
        $e$;
        EXECUTE 'CREATE EVENT TRIGGER relation_sql_ddl ON ddl_command_end WHEN TAG IN (''CREATE TABLE'', ''ALTER TABLE'', ''DROP TABLE'', ''CREATE SCHEMA'', ''DROP SCHEMA'') EXECUTE FUNCTION relation_sql_event()';
      END IF;
    EXCEPTION WHEN insufficient_privilege THEN
      RAISE WARNING 'pg_relation_sql: event trigger needs superuser rights, skipped — %', SQLERRM;
    END;
    IF mode = 'install' THEN
      PERFORM count(*) FROM relation_sql('sync');
    END IF;
  ELSIF mode <> 'status' THEN
    RAISE EXCEPTION 'unknown mode: %', mode USING HINT = 'status, show, sync, install, uninstall, drop';
  END IF;
  RETURN QUERY
  WITH trg AS (
    SELECT EXISTS (SELECT FROM pg_event_trigger WHERE evtname = 'relation_sql_ddl') installed
      , 'SELECT status, command FROM relation_sql(%L)' cmd
      , '0.2.2' version
  )
  , agg AS (
    SELECT count(*) FILTER (WHERE status = 'OK') n_ok
      , count(*) FILTER (WHERE status IN ('NEED_CREATE_OR_REPLACE', 'NEED_DROP_CREATE', 'ORPHANED')) n_pending
      , count(*) FILTER (WHERE status = 'FOREIGN_FUNCTION') n_foreign
      , count(*) FILTER (WHERE status = 'DUPLICATE_NAME') n_duplicate
    FROM relation_sql('show')
  )
  , board(status, command) AS (
    SELECT 'pg_relation_sql ' || version || ' — relation functions generated from foreign keys'
      , 'SELECT status, command FROM relation_sql()'
    FROM trg
    UNION ALL
    SELECT 'event trigger: ' || CASE WHEN installed THEN 'installed' ELSE 'not installed' END
      , format(cmd, CASE WHEN installed THEN 'uninstall' ELSE 'install' END)
    FROM trg
    UNION ALL
    SELECT format('relation functions: %s ok, %s to sync, %s foreign, %s duplicate', a.n_ok, a.n_pending, a.n_foreign, a.n_duplicate)
      , format(t.cmd, CASE WHEN a.n_pending > 0 THEN 'sync' ELSE 'drop' END)
    FROM agg a, trg t
    UNION ALL
    SELECT 'details', 'SELECT * FROM relation_sql(''show'')'
  )
  SELECT status, command
    , NULL, NULL, NULL, NULL::text[], NULL, NULL, NULL::text[], NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL
  FROM board;
END $fn$;

SELECT status, command FROM relation_sql('install');
