Summary
In v1.10.0, pgschema apply succeeds for a schema with RLS policies and CHECK constraints that reference same-schema objects, but a repeat pgschema plan immediately reports changes again.
This is not the old hard planning failure from #399. The apply succeeds. The problem is that pgschema cannot converge after apply because its desired expression form and PostgreSQL's stored expression form are compared differently.
Version
Version: 1.10.0@592c19c9 linux/amd64 2026-05-30 05:52:01
Tested against:
PostgreSQL 17.6 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 13.2.0, 64-bit
The target database was a disposable local Supabase/Postgres scratch database.
Minimal reproduction
Bootstrap
The target database has a pre-created auth.uid() stub because the policy references it:
CREATE SCHEMA auth;
CREATE SCHEMA profiles;
CREATE FUNCTION auth.uid() RETURNS uuid
LANGUAGE sql
STABLE
AS $$
SELECT '00000000-0000-0000-0000-000000000001'::uuid;
$$;
Desired state
Run pgschema with --schema profiles --file repeat-plan-drift-repro.sql:
CREATE TYPE profiles.profile_status AS ENUM ('active', 'disabled');
CREATE TABLE profiles.profiles (
id uuid PRIMARY KEY,
user_id uuid NOT NULL,
status profiles.profile_status NOT NULL DEFAULT 'active'::profiles.profile_status,
CONSTRAINT profiles_status_check CHECK (status <> 'disabled'::profiles.profile_status)
);
CREATE TABLE profiles.profile_media (
id uuid PRIMARY KEY,
profile_id uuid NOT NULL REFERENCES profiles.profiles (id)
);
ALTER TABLE profiles.profile_media ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can read their own media" ON profiles.profile_media
FOR SELECT
USING (profile_id IN (
SELECT id
FROM profiles.profiles
WHERE user_id = auth.uid()
));
Commands
pgschema apply \
--host <host> --port <port> --db pgschema_trial_drift_repro --user postgres \
--password postgres --sslmode disable \
--schema profiles \
--file repeat-plan-drift-repro.sql \
--plan-host <host> --plan-port <port> --plan-db pgschema_trial_drift_repro \
--plan-user postgres --plan-password postgres --plan-sslmode disable \
--auto-approve
pgschema plan \
--host <host> --port <port> --db pgschema_trial_drift_repro --user postgres \
--password postgres --sslmode disable \
--schema profiles \
--file repeat-plan-drift-repro.sql \
--plan-host <host> --plan-port <port> --plan-db pgschema_trial_drift_repro \
--plan-user postgres --plan-password postgres --plan-sslmode disable
Expected
The first apply succeeds. The repeat plan immediately after apply should report:
Actual
The first apply succeeds.
The repeat plan reports:
Plan: 2 to modify.
Summary by type:
tables: 2 to modify
Tables:
~ profile_media
~ Users can read their own media (policy)
~ profiles
- profiles_status_check (constraint)
+ profiles_status_check (constraint)
The repeat plan DDL is:
ALTER POLICY "Users can read their own media" ON profile_media USING (profile_id IN ( SELECT profiles.id FROM profiles WHERE (profiles.user_id = auth.uid())));
ALTER TABLE profiles DROP CONSTRAINT profiles_status_check;
ALTER TABLE profiles.profiles
ADD CONSTRAINT profiles_status_check CHECK (status <> 'disabled'::profile_status) NOT VALID;
ALTER TABLE profiles.profiles VALIDATE CONSTRAINT profiles_status_check;
PostgreSQL stores the policy expression as:
(profile_id IN ( SELECT profiles.id
FROM profiles.profiles
WHERE (profiles.user_id = auth.uid())))
The desired source used:
SELECT id
FROM profiles.profiles
WHERE user_id = auth.uid()
So after apply, pgschema still wants to rewrite a policy expression that PostgreSQL already accepted and stored in an equivalent normalized form.
The same repeat plan also wants to drop and recreate the CHECK constraint, changing the cast from schema-qualified:
'disabled'::profiles.profile_status
to unqualified:
'disabled'::profile_status
Impact
This blocks using pgschema as a clean migration planner in CI/CD:
- A clean database cannot reach
No changes detected after apply.
- Every deployment plan would include repeat policy changes and constraint drop/recreate statements.
- The constraint drift is especially risky because repeated apply uses
DROP CONSTRAINT, ADD ... NOT VALID, and VALIDATE CONSTRAINT.
- In a real schema graph, this creates enough noise that legitimate changes are hard to review.
Related issues
Suggested direction
The comparison step likely needs to treat target-schema-qualified and unqualified references as equivalent inside policy and constraint expressions when they resolve to the same object in the managed schema.
Two possible approaches:
- Normalize both desired and current expressions by stripping the managed schema qualifier where it points to the target schema.
- Or make PostgreSQL catalog rendering use a search path that includes the managed schema before comparing expressions, so the current side is rendered closer to the desired side.
Summary
In v1.10.0,
pgschema applysucceeds for a schema with RLS policies and CHECK constraints that reference same-schema objects, but a repeatpgschema planimmediately reports changes again.This is not the old hard planning failure from #399. The apply succeeds. The problem is that pgschema cannot converge after apply because its desired expression form and PostgreSQL's stored expression form are compared differently.
Version
Version: 1.10.0@592c19c9 linux/amd64 2026-05-30 05:52:01Tested against:
PostgreSQL 17.6 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 13.2.0, 64-bitThe target database was a disposable local Supabase/Postgres scratch database.
Minimal reproduction
Bootstrap
The target database has a pre-created
auth.uid()stub because the policy references it:Desired state
Run pgschema with
--schema profiles --file repeat-plan-drift-repro.sql:Commands
Expected
The first
applysucceeds. The repeatplanimmediately after apply should report:Actual
The first
applysucceeds.The repeat
planreports:The repeat plan DDL is:
PostgreSQL stores the policy expression as:
The desired source used:
So after apply, pgschema still wants to rewrite a policy expression that PostgreSQL already accepted and stored in an equivalent normalized form.
The same repeat plan also wants to drop and recreate the CHECK constraint, changing the cast from schema-qualified:
to unqualified:
'disabled'::profile_statusImpact
This blocks using pgschema as a clean migration planner in CI/CD:
No changes detectedafter apply.DROP CONSTRAINT,ADD ... NOT VALID, andVALIDATE CONSTRAINT.Related issues
Suggested direction
The comparison step likely needs to treat target-schema-qualified and unqualified references as equivalent inside policy and constraint expressions when they resolve to the same object in the managed schema.
Two possible approaches: