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:
- macOS + Linux
- Homebrew
- Docker
- Windows
- CI
- Manual Installation
To download and install the latest release of the Atlas CLI, simply run the following in your terminal:
curl -sSf https://atlasgo.sh | sh
Get the latest release with Homebrew:
brew install ariga/tap/atlas
To pull the Atlas image and run it as a Docker container:
docker pull arigaio/atlas
docker run --rm arigaio/atlas --help
If the container needs access to the host network or a local directory, use the --net=host flag and mount the desired
directory:
docker run --rm --net=host \
-v $(pwd)/migrations:/migrations \
arigaio/atlas migrate apply \
--url "mysql://root:pass@:3306/test"
Download the latest release and move the atlas binary to a file location on your system PATH.
GitHub Actions
Use the setup-atlas action to install Atlas in your GitHub Actions workflow:
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
Other CI Platforms
For other CI/CD platforms, use the installation script. See the CI/CD integrations for more details.
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:
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 deepIFand 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:
- PostgreSQL
- MySQL
- MariaDB
- SQL Server
- Oracle
atlas migrate lint -c file://atlas.hcl --dev-url "docker://postgres/18/dev" --latest 1
atlas migrate lint -c file://atlas.hcl --dev-url "docker://mysql/8/dev" --latest 1
atlas migrate lint -c file://atlas.hcl --dev-url "docker://mariadb/latest/dev" --latest 1
atlas migrate lint -c file://atlas.hcl --dev-url "docker://sqlserver/2022-latest/dev?mode=schema" --latest 1
atlas migrate lint -c file://atlas.hcl --dev-url "docker://oracle/free:latest-lite?t=10m&mode=database" --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:
lint {
complexity {
cyclomatic = 10
nesting = 2
statements = 30
parameters = 1
}
}
Version 2 creates fulfill_orders, which goes over every limit:
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:
-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:
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:
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:
- Claude Code
- OpenAI Codex
- Cursor
- GitHub Copilot
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
Install the skills from a terminal. Run it from the repository root. It installs every Atlas skill in .agents/skills/.
npx skills add ariga/atlas -a codex -y
Then start the onboarding skill in OpenAI Codex. See OpenAI Codex with Atlas for the other ways to use the skills.
$atlas-onboard
Install the skills from a terminal. Run it from the repository root. It installs every Atlas skill in .agents/skills/.
npx skills add ariga/atlas -a cursor -y
Then start the onboarding skill in Cursor. See Cursor with Atlas for the other ways to use the skills.
/atlas-onboard
Install the skills from a terminal. Run it from the repository root. It installs every Atlas skill in .agents/skills/.
npx skills add ariga/atlas -a github-copilot -y
Then start the onboarding skill in GitHub Copilot. See GitHub Copilot with Atlas for the other ways to use the skills.
/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.