PL/pgSQL compatibility
HeliosDB Nano accepts DO $$ … $$ / DO LANGUAGE plpgsql $tag$ … $tag$ blocks and executes plain SQL statement bodies. Nano runs plain-SQL DO blocks and simple SQL/PL/pgSQL functions; PL/pgSQL control flow (variables, FOR … IN SELECT … LOOP, IF/ELSE, RAISE) and trigger bodies are planned. See Functions, procedures and triggers for the scope outside DO blocks.
When a DO block contains PL/pgSQL syntax, the server returns a clear error — it does not silently no-op, which would corrupt migrations that rely on the block running.
Supported
DO $$ BEGIN <plain SQL>; <plain SQL>; END $$;DO $tag$ BEGIN <plain SQL>; END $tag$;DO LANGUAGE plpgsql $$ <plain SQL> $$;
Bodies may contain CREATE, ALTER, DROP, INSERT, UPDATE, DELETE, and SELECT statements separated by ;. Each runs as its own implicit transaction. An EXCEPTION WHEN <condition> THEN NULL handler that swallows an error is honoured.
DO blocks are handled on the PostgreSQL simple-query path (for example psql or a driver’s simple query() call). The extended query protocol and the embedded execute() API do not run DO blocks.
Not supported
DECLARE <name> <type>;— variablesFOR <var> IN SELECT … LOOP/FOR <i> IN 1..n LOOP— loopsIF <cond> THEN … ELSIF … END IF;— conditionalsWHILE <cond> LOOP … END LOOP;RAISE NOTICE | EXCEPTION | WARNING …;RETURN,PERFORM,EXIT,CONTINUEEXCEPTION WHEN … THEN <statements> END;— error handlers other thanTHEN NULL- Variable assignment (
<var>:= <expr>) - Cursors (
DECLARE cur CURSOR FOR …; FETCH …)
Error message
When HeliosDB Nano detects PL/pgSQL control-flow tokens in a DO block body, it returns:
ERROR: PL/pgSQL control flow (`<KEYWORD>`) inside DO blocks is not yet supported in HeliosDB Nano. Rewrite the block as plain SQL, or execute each statement separately. See: docs/compatibility/plpgsql.md<KEYWORD> is the first control-flow token we spotted, chosen to help you locate the offending line.
Migration patterns
Backfill loop → plain SQL UPDATE … FROM
Common Drizzle / Prisma migration:
DO $$DECLARE u RECORD;BEGIN FOR u IN SELECT id, email FROM users LOOP INSERT INTO user_profile (user_id, display_name) VALUES (u.id, u.email); END LOOP;END $$;Rewrite as a single INSERT … SELECT:
INSERT INTO user_profile (user_id, display_name)SELECT id, email FROM users;Conditional index creation → CREATE INDEX IF NOT EXISTS
-- Not supportedDO $$BEGIN IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'users_email_idx') THEN CREATE INDEX users_email_idx ON users(email); END IF;END $$;
-- Use insteadCREATE INDEX IF NOT EXISTS users_email_idx ON users(email);Conditional data load → INSERT … ON CONFLICT DO NOTHING
-- Not supportedDO $$BEGIN IF NOT EXISTS (SELECT 1 FROM tenants WHERE id = 'default') THEN INSERT INTO tenants (id, name) VALUES ('default', 'Default Tenant'); END IF;END $$;
-- Use insteadINSERT INTO tenants (id, name) VALUES ('default', 'Default Tenant')ON CONFLICT (id) DO NOTHING;Functions, procedures and triggers
CREATE FUNCTION … LANGUAGE plpgsql. Function bodies may contain SQL statements,SELECT INTO,RETURN,NULL, nestedBEGIN … ENDblocks andDECLAREwithout initialisers.IF,CASE,LOOP,WHILE,FOR,:=,RAISE,EXIT,CONTINUE, cursors, dynamicEXECUTE,RETURN NEXT/RETURN QUERY,EXCEPTIONhandlers andCALLare refused when the function is created.LANGUAGE sqlfunctions are supported. Reference parameters with the$sigil ($1or$name); a bare parameter name is read as a column reference.CREATE PROCEDURE. Procedure bodies are not checked for control flow when they are created, and control flow inside them does not run correctly (anIFtakes theELSEbranch, aWHILEloop does not iterate). Keep procedure bodies to plain SQL statements.- Triggers.
CREATE TRIGGERis accepted, but trigger function bodies are not executed. The one exception is aBEFORE … FOR EACH ROWfunction whose body only assignsNEW.<col> = <expr>and returnsNEWorNULL.AFTERtriggers and statement-level triggers are accepted and do nothing. Do not rely on triggers for audit rows or derived data.
PL/pgSQL control flow in functions, procedures and DO blocks, and trigger function bodies, are planned.
Roadmap
A minimal PL/pgSQL interpreter is tracked as a follow-up. Priority depends on customer demand — if you hit a real migration that doesn’t fit the patterns above, open an issue with the block and we’ll either adjust the rewrite recipes or fast-track the interpreter.