ERROR: must be owner of table patient (SQLSTATE 42501) means the role can read and write the table but does not own it, and ALTER TABLE needs the owner. Run the change as the owner, or make the role the owner with ALTER TABLE patient OWNER TO clinic_app.

In the test, clinic_app had SELECT, INSERT, UPDATE, DELETE on the table and its INSERT worked, yet ALTER TABLE ... ADD COLUMN failed until ownership moved. GRANT did not cover the schema change; ownership did.

The error

SET ROLE clinic_app;
SET
INSERT INTO patient VALUES (1, 'Maria Garcia');
INSERT 0 1
ALTER TABLE patient ADD COLUMN phone text;
psql:/tmp/f42501.sql:7: ERROR:  42501: must be owner of table patient
LOCATION:  aclcheck_error, aclchk.c:2931

Why it happens

The table was created by postgres, so postgres owned it, as \dt showed:

           List of tables
 Schema |  Name   | Type  |  Owner
--------+---------+-------+----------
 public | patient | table | postgres
(1 row)

The four table privileges granted to clinic_app covered the data, and the insert went through. The ALTER TABLE was refused with 42501 and the message names the missing thing directly: ownership of patient. A follow-up test with GRANT ALL ON invoice TO clinic_app on a second table ended the same way: must be owner of table invoice. The setup to look for is one role that created the tables and another that now runs the schema change.

The fix

ALTER TABLE patient OWNER TO clinic_app;
ALTER TABLE
SET ROLE clinic_app;
SET
ALTER TABLE patient ADD COLUMN phone text;
ALTER TABLE
SELECT * FROM patient;
 id |  full_name   | phone
----+--------------+-------
  1 | Maria Garcia |
(1 row)

RESET ROLE;
RESET
            List of tables
 Schema |  Name   | Type  |   Owner
--------+---------+-------+------------
 public | patient | table | clinic_app
(1 row)

The first statement ran as postgres, the current owner. After it, clinic_app added the column and \dt lists it as the owner. Pick the role that runs your migrations as the owner of the tables they change; the role that only reads and writes data can keep plain grants. The other way out, without moving ownership, is to run the ALTER TABLE itself as the owning role, as the first line here ran as postgres.

How it was reproduced

A postgres:18 container (PostgreSQL 18.4, Debian 18.4-1.pgdg13+1) on Docker Engine 29.9.2, database lab. The script f42501.sql, run as postgres with docker exec fix8-pg3 sh -c "psql -U postgres -d lab -e -f /tmp/f42501.sql 2>&1", created a login role clinic_app and a table patient, granted the four data privileges, switched with SET ROLE clinic_app, inserted a fictional row and tried the ALTER TABLE. \set VERBOSITY verbose at the top adds the SQLSTATE. The container and role names are the test's own.

Frequently asked

Why can my PostgreSQL user insert but not ALTER TABLE?
INSERT needs a privilege that GRANT gives; ALTER TABLE needed ownership in the test. clinic_app inserted a row and then got must be owner of table patient on ADD COLUMN.
How do I change the owner of a table in PostgreSQL?
Run ALTER TABLE patient OWNER TO clinic_app as the current owner; in the test that was postgres. Then \dt listed clinic_app as the owner and the ADD COLUMN worked.
Does GRANT ALL fix 'must be owner of table'?
No, not in the test. After GRANT ALL ON invoice TO clinic_app, ALTER TABLE invoice ADD COLUMN paid boolean still failed with must be owner of table invoice. Moving ownership fixed the patient table.

More decoded errors in the Fixes category. The same code with a different message, for creating tables, is in PostgreSQL: permission denied for schema public.