-- ============================================================ -- 10-validate.sql — Template validation -- ============================================================ CREATE OR REPLACE FUNCTION fsql.validate() RETURNS SETOF fsql.validation_result LANGUAGE plpgsql STABLE AS $$ DECLARE _r record; BEGIN /* 1. Unknown cmd */ FOR _r IN SELECT path, cmd FROM fsql.templates WHERE cmd IS NOT NULL AND cmd NOT IN ( 'exec','ref','if','exec_tpl','map', -- new names 'exejson','exejsontp','templ','json', -- legacy aliases 'templ_key','array') -- deprecated (accepted) LOOP RETURN QUERY SELECT _r.path::text, 'ERROR'::text, format('Unknown cmd: %s', _r.cmd); END LOOP; /* 2. ref pointing to non-existent target */ FOR _r IN SELECT t.path, t.body FROM fsql.templates t WHERE t.cmd IN ('ref', 'templ', 'templ_key') AND NOT EXISTS ( SELECT 1 FROM fsql.templates t2 WHERE t2.path = t.body) LOOP RETURN QUERY SELECT _r.path::text, 'ERROR'::text, format('ref target not found: %s', _r.body); END LOOP; /* 3. if-nodes without any child branches */ FOR _r IN SELECT t.path FROM fsql.templates t WHERE t.cmd = 'if' AND NOT EXISTS ( SELECT 1 FROM fsql.templates c WHERE c.path LIKE t.path || '.%' AND SPLIT_PART(replace(c.path, t.path, ''), '.', 3) = '') LOOP RETURN QUERY SELECT _r.path::text, 'WARNING'::text, 'if-node has no child branches'; END LOOP; /* 4. Empty body on exec / exec_tpl */ FOR _r IN SELECT path FROM fsql.templates WHERE cmd IN ('exec', 'exec_tpl', 'exejson', 'exejsontp') AND (body IS NULL OR body = '') LOOP RETURN QUERY SELECT _r.path::text, 'ERROR'::text, 'exec template has empty body'; END LOOP; /* 5. defaults is not valid JSON */ FOR _r IN SELECT path, defaults FROM fsql.templates WHERE defaults IS NOT NULL AND defaults <> '' LOOP BEGIN PERFORM _r.defaults::jsonb; EXCEPTION WHEN OTHERS THEN RETURN QUERY SELECT _r.path::text, 'ERROR'::text, format('defaults is not valid JSON: %s', left(_r.defaults, 60)); END; END LOOP; /* 6. Orphan children (only fragments — cmd IS NULL) */ FOR _r IN SELECT c.path, regexp_replace(c.path, '\.[^.]+$', '') AS parent_path FROM fsql.templates c WHERE c.path LIKE '%.%' AND c.cmd IS NULL AND NOT EXISTS ( SELECT 1 FROM fsql.templates p WHERE p.path = regexp_replace(c.path, '\.[^.]+$', '')) LOOP RETURN QUERY SELECT _r.path::text, 'WARNING'::text, format('Parent not found: %s', _r.parent_path); END LOOP; /* All good */ IF NOT FOUND THEN RETURN QUERY SELECT ''::text, 'OK'::text, format('All %s templates are valid', (SELECT count(*) FROM fsql.templates)); END IF; END; $$; COMMENT ON FUNCTION fsql.validate IS 'Validate all templates: unknown cmd, broken refs, missing if-branches, ' 'invalid defaults, orphan children.';