Skip to content

Procedural Language (PL)

Nano runs plain-SQL DO blocks and simple SQL/PL/pgSQL functions; PL/pgSQL control flow, trigger bodies, PL/SQL, T-SQL and DB2 SQL PL are planned. This page describes the procedural language surface, and marks what runs in the current release and what is planned.

Overview

The procedural language module enables:

  • Stored Procedures: Encapsulate SQL statements behind CALL
  • User-Defined Functions: Create reusable computations
  • Control Flow (planned): IF/ELSE, LOOP, WHILE, FOR statements
  • Variables: DECLARE and SELECT INTO today; := assignment and initial values are planned
  • Exception Handling (planned): EXCEPTION blocks for error handling
  • Cursors (planned): Iterate over query results

Supported Dialects

DialectDescriptionPrimary Use
PL/pgSQLPostgreSQL procedural languageDefault; the subset listed above runs today
T-SQLMicrosoft SQL ServerPlanned
PL/SQLOracle DatabasePlanned
DB2 SQL PLIBM DB2Planned
AutoAuto-detect dialectPlanned

Basic Syntax

Anonymous Blocks

DO $$
BEGIN
UPDATE users SET status = 'active' WHERE status IS NULL;
DELETE FROM sessions WHERE expires_at < now();
END;
$$;

DO blocks run plain SQL statements only; DECLARE, RAISE and other PL/pgSQL control flow inside a DO block are planned.

CREATE FUNCTION

CREATE OR REPLACE FUNCTION get_user_count()
RETURNS INTEGER AS $$
DECLARE
v_count INTEGER;
BEGIN
SELECT COUNT(*) INTO v_count FROM users;
RETURN v_count;
END;
$$ LANGUAGE plpgsql;

CREATE PROCEDURE

CREATE OR REPLACE PROCEDURE update_user_status(
p_user_id INTEGER,
p_status VARCHAR
)
AS $$
BEGIN
UPDATE users SET status = p_status WHERE id = p_user_id;
END;
$$ LANGUAGE plpgsql;

Variables

Declaration

DECLARE
v_name VARCHAR(100); -- Simple variable
v_count INTEGER := 0; -- With default value
v_active CONSTANT BOOLEAN := TRUE; -- Constant
v_user users%ROWTYPE; -- Row type
v_id users.id%TYPE; -- Column type

Assignment

-- Direct assignment
v_count := 42;
-- SELECT INTO
SELECT name INTO v_name FROM users WHERE id = 1;
-- Multiple values
SELECT name, email INTO v_name, v_email FROM users WHERE id = 1;

Control Flow

IF Statement

IF v_count > 100 THEN
RAISE NOTICE 'High count';
ELSIF v_count > 50 THEN
RAISE NOTICE 'Medium count';
ELSE
RAISE NOTICE 'Low count';
END IF;

CASE Statement

CASE v_status
WHEN 'active' THEN
RETURN 1;
WHEN 'inactive' THEN
RETURN 0;
ELSE
RETURN -1;
END CASE;

LOOP

-- Basic loop
LOOP
v_count := v_count + 1;
EXIT WHEN v_count >= 10;
END LOOP;
-- WHILE loop
WHILE v_count < 10 LOOP
v_count := v_count + 1;
END LOOP;
-- FOR loop (numeric range)
FOR i IN 1..10 LOOP
RAISE NOTICE 'Iteration: %', i;
END LOOP;
-- FOR loop (query result)
FOR rec IN SELECT * FROM users LOOP
RAISE NOTICE 'User: %', rec.name;
END LOOP;

Exception Handling

BEGIN
-- Code that might fail
INSERT INTO users (id, name) VALUES (1, 'Alice');
EXCEPTION
WHEN unique_violation THEN
RAISE NOTICE 'User already exists';
WHEN OTHERS THEN
RAISE NOTICE 'Unexpected error: %', SQLERRM;
END;

Common Exception Types

ExceptionDescription
unique_violationDuplicate key
foreign_key_violationFK constraint failed
null_value_not_allowedNULL in NOT NULL column
check_violationCHECK constraint failed
division_by_zeroDivision by zero
OTHERSCatch-all handler

Cursors

DECLARE
user_cursor CURSOR FOR SELECT * FROM users;
user_record users%ROWTYPE;
BEGIN
OPEN user_cursor;
LOOP
FETCH user_cursor INTO user_record;
EXIT WHEN NOT FOUND;
RAISE NOTICE 'User: %', user_record.name;
END LOOP;
CLOSE user_cursor;
END;

Dialect-Specific Examples

T-SQL Style

-- T-SQL uses @ for variables
DECLARE @v_count INT;
SET @v_count = 0;
WHILE @v_count < 10
BEGIN
SET @v_count = @v_count + 1;
PRINT @v_count;
END;

PL/SQL Style

-- PL/SQL uses := for assignment
DECLARE
v_count NUMBER := 0;
BEGIN
FOR i IN 1..10 LOOP
v_count := v_count + 1;
DBMS_OUTPUT.PUT_LINE(v_count);
END LOOP;
END;

Integration with Triggers

Procedural language functions are commonly used as trigger handlers:

-- Create trigger function
CREATE OR REPLACE FUNCTION audit_changes()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_log (table_name, action, changed_at)
VALUES (TG_TABLE_NAME, TG_OP, NOW());
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Attach to trigger
CREATE TRIGGER users_audit
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW
EXECUTE FUNCTION audit_changes();

Built-in Functions

Utility Functions

FunctionDescription
RAISE NOTICEOutput debug message
RAISE EXCEPTIONThrow error
FOUNDBoolean - last query returned rows
ROW_COUNTRows affected by last statement
NOW()Current timestamp

Trigger Context Variables

VariableDescription
TG_OPOperation (INSERT, UPDATE, DELETE)
TG_TABLE_NAMEName of trigger table
OLDRow before modification
NEWRow after modification
TG_WHENBEFORE or AFTER

Performance Considerations

  • PL functions are compiled and cached
  • Avoid excessive dynamic SQL
  • Use bulk operations where possible
  • Consider using IMMUTABLE/STABLE/VOLATILE appropriately

See Also