Skip to main content

Routine Complexity Policy: Limits for Functions, Procedures, and Triggers

A complexity policy sets limits for the functions, procedures, and triggers in your schema, and reports the migrations that go past them.

To get started, install Atlas:

To download and install the latest release of the Atlas CLI, simply run the following in your terminal:

curl -sSf https://atlasgo.sh | sh

Enable the Policy​

The policy is the complexity analyzer of migration linting. It is off by default. To enable it, add its block to the lint configuration:

atlas.hcl
lint {
complexity {
cyclomatic = 10 # Defaults to 10.
nesting = 3 # Defaults to 3.
statements = 40 # Disabled by default.
parameters = 8 # Disabled by default.
error = true # Defaults to false.
}
}

Each attribute other than error sets a limit:

  • cyclomatic: the number of independent paths through a routine.
  • nesting: how deep IF and loop statements are nested.
  • statements: how many statements the body has.
  • parameters: how many parameters the routine takes.

A limit set to 0 is disabled. With error = true, a routine above a limit fails the lint. Without it, the routine is reported as a warning.

Run the Policy​

atlas migrate lint checks each function, procedure, and trigger that a migration file creates or changes, and reports the ones above a limit. The policy measures routines on PostgreSQL, CockroachDB, YugabyteDB, Aurora DSQL, HorizonDB, Redshift, MySQL, MariaDB, SQL Server, Oracle, Snowflake, and more.

Run it before you open a pull request, with a dev database of your engine:

atlas migrate lint -c file://atlas.hcl --dev-url "docker://postgres/18/dev" --latest 1

-c loads the lint block from atlas.hcl, and --latest 1 lints the newest migration file. On Redshift and Snowflake, --dev-url points to a database of your own, as they have no Docker image. On SQL Server, the statements limit does not apply.

In CI, the policy runs with the rest of migration linting on every pull request.

Examples​

The examples below run atlas migrate lint on PostgreSQL. Each one shows a migration and the report Atlas prints for it.

A New Routine Above the Limits​

With these limits:

atlas.hcl
lint {
complexity {
cyclomatic = 10
nesting = 2
statements = 30
parameters = 1
}
}

Version 2 creates fulfill_orders, which goes over every limit:

migrations/2.sql
CREATE FUNCTION fulfill_orders(p_warehouse int, p_limit int DEFAULT 100)
RETURNS TABLE (order_id bigint, status text)
LANGUAGE plpgsql
AS $$
DECLARE
o orders%ROWTYPE;
item record;
stock int;
missing int := 0;
attempts int;
tags text[];
tag text;
c_items CURSOR (oid bigint) FOR SELECT * FROM order_items WHERE order_items.order_id = oid;
BEGIN
<<orders_loop>>
FOR o IN SELECT * FROM orders WHERE warehouse_id = p_warehouse AND state = 'pending' ORDER BY created_at LIMIT p_limit FOR UPDATE SKIP LOCKED LOOP
missing := 0;
FOR item IN c_items(o.id) LOOP
SELECT quantity INTO stock FROM inventory WHERE sku = item.sku AND warehouse_id = p_warehouse;
IF NOT FOUND THEN
missing := missing + 1;
CONTINUE;
ELSIF stock < item.quantity THEN
missing := missing + 1;
ELSE
UPDATE inventory SET quantity = quantity - item.quantity WHERE sku = item.sku AND warehouse_id = p_warehouse;
END IF;
END LOOP;
CASE
WHEN missing = 0 THEN
UPDATE orders SET state = 'fulfilled' WHERE id = o.id;
order_id := o.id; status := 'fulfilled';
WHEN missing < 3 THEN
UPDATE orders SET state = 'partial' WHERE id = o.id;
order_id := o.id; status := 'partial';
ELSE
order_id := o.id; status := 'backorder';
END CASE;
RETURN NEXT;
attempts := 0;
WHILE attempts < 3 LOOP
BEGIN
INSERT INTO shipments (order_id, created_at) VALUES (o.id, now());
EXIT;
EXCEPTION
WHEN unique_violation THEN
EXIT;
WHEN serialization_failure OR deadlock_detected THEN
attempts := attempts + 1;
PERFORM pg_sleep(0.1 * attempts);
END;
END LOOP;
tags := string_to_array(o.tags, ',');
IF tags IS NOT NULL THEN
FOREACH tag IN ARRAY tags LOOP
CONTINUE WHEN tag = '';
EXECUTE format('INSERT INTO order_tags (order_id, tag) VALUES (%L, %L)', o.id, tag);
END LOOP;
END IF;
END LOOP orders_loop;
GET DIAGNOSTICS stock = ROW_COUNT;
RAISE NOTICE 'processed % orders', stock;
END
$$;
  -- analyzing version 2
-- complex routines detected:
-- L1: Function "fulfill_orders" has a cyclomatic complexity of 12, above the limit of 10
https://atlasgo.io/lint/analyzers#CX101
-- L1: Function "fulfill_orders" nests control statements 3 levels deep, above the limit
of 2 https://atlasgo.io/lint/analyzers#CX102
-- L1: Function "fulfill_orders" has 34 statements, above the limit of 30
https://atlasgo.io/lint/analyzers#CX103
-- L1: Function "fulfill_orders" has 2 parameters, above the limit of 1
https://atlasgo.io/lint/analyzers#CX104
-- suggested fix:
-> Split complex routines into smaller functions or procedures that each handle a single
task

The highlighted lines are the two parameters, and the IF inside the item loop, 3 levels deep.

A Change That Makes a Routine More Complex​

Version 3 replaces the function and adds a guard at its top:

migrations/3.sql
-CREATE FUNCTION fulfill_orders(p_warehouse int, p_limit int DEFAULT 100)
+CREATE OR REPLACE FUNCTION fulfill_orders(p_warehouse int, p_limit int DEFAULT 100)
...
BEGIN
+ IF p_limit > 1000 THEN
+ RAISE EXCEPTION 'limit % is too large', p_limit;
+ END IF;
<<orders_loop>>
FOR o IN SELECT * FROM orders ...

Only the limits it grew on are reported again, with their previous values:

  -- analyzing version 3
-- complex routines detected:
-- L1: Function "fulfill_orders" has a cyclomatic complexity of 13 (was 12), above the
limit of 10 https://atlasgo.io/lint/analyzers#CX101
-- L1: Function "fulfill_orders" has 36 statements (was 34), above the limit of 30
https://atlasgo.io/lint/analyzers#CX103
-- suggested fix:
-> Split complex routines into smaller functions or procedures that each handle a single
task

Adopt the Policy on an Existing Schema​

Only the routines that a migration file creates or changes are checked, and a changed routine is reported only if its complexity grew. Start with warnings everywhere, and fail the lint in CI once the limits fit your routines:

atlas.hcl
lint {
complexity {}
}

env "ci" {
lint {
complexity {
error = true
}
}
}

complexity {} enables the policy with the default limits.

Adjust Single Checks​

Each limit is a separate check, and the lint report prints the check's code with every finding. To change one check without touching the others, use its code in a check block. For example, to fail the lint on cyclomatic complexity while the other limits stay warnings:

atlas.hcl
lint {
complexity {}
check "CX101" {
error = true
}
}

To stop reporting a check, set skip = true in its block instead.

Get Started with a Coding Agent​

With a coding agent, install the Atlas agent skills. One command installs them:

Install the skills from a terminal. It installs the Atlas plugin, with all its skills, for every project on this machine.

claude plugin marketplace add ariga/atlas
claude plugin install atlas@ariga

Then start the onboarding skill in Claude Code. See Claude Code with Atlas for the other ways to use the skills.

/atlas:atlas-onboard

The atlas skill runs atlas migrate lint on each migration the agent generates, so the policy applies to the routines it writes. To set up Atlas in a new project, start the atlas-onboard skill.

Next Steps​