Skip to content

migrate does not ADD a foreign key to a pre-existing table (only creates FKs at table-creation time) #79

Description

@MelbourneDeveloper

Summary

DataProviderMigrate migrate does not emit ALTER TABLE ... ADD CONSTRAINT for a foreign key added to a table that already exists in the target DB. It creates the new column but silently skips the FK constraint. New tables get their FKs correctly at creation time; only the "add a FK to a pre-existing table" path is missing.

The bug is masked because the apply phase reports success — it is the post-apply integrity check that then fails, leaving the operator with a half-applied schema and no automated way to converge it.

Reproduction

  1. Have a table conversations already present in the DB (created by an earlier migrate).
  2. In schema.yaml, add a new nullable column to it with a foreign key:
- name: end_user_id
  type: Uuid          # nullable
foreignKeys:
  - columns: [end_user_id]
    referencedTable: tenant_end_users     # a NEW table created in the same run
    referencedColumns: [id]
    onDelete: Cascade
  1. Run DataProviderMigrate migrate --schema schema.yaml --provider postgres.

Actual

  • Apply phase succeeds (creates tenant_end_users, adds conversations.end_user_id, applies grants/RLS — 53 ops here).
  • Post-apply integrity check FAILS:
    SCHEMA INTEGRITY CHECK FAILED
    public.conversations: missing foreign key FK_conversations_end_user_id on (end_user_id)
    
  • Re-running migrate reports "Schema is up to date — no operations needed" and still fails the same integrity check. So the apply planner never diffs/produces the missing FK on the existing table — it is not a transient ordering issue.

Expected

The planner emits ALTER TABLE public.conversations ADD CONSTRAINT "FK_conversations_end_user_id" FOREIGN KEY (end_user_id) REFERENCES public.tenant_end_users(id) ON DELETE CASCADE; so a re-run converges to zero drift.

Impact

Any FK added to an existing table never lands via the declarative pipeline. This breaks the "schema.yaml is the single source of truth, re-running is idempotent" contract for the common case of evolving an existing table. The only remediation is a manual ALTER TABLE ... ADD CONSTRAINT out of band, which defeats the declarative model.

Contrast with the FK-at-creation path, which works: tenant_end_users(tenant_id) -> tenants(id) (created in the same run) was applied correctly. The gap is specifically ADD FK on an ALTER of a pre-existing table.

Related secondary finding (integrity-check normalization, lower severity)

The same integrity check also reports a false positive on string-literal column defaults — it does not normalize Postgres's implicit ::text cast:

public.topup.status: default expected 'pending' but found 'pending'::text

'pending' and 'pending'::text are the same default; the check should normalize the cast before comparing (same class of round-trip mismatch as the filtered/expression-index issue in #65). Happy to split this into its own issue if preferred.

Environment

  • DataProvider 0.9.12-beta line (same as the gaps tracked in Nimblesite/NimblesiteAgenticPlatform docs/specs/deployment.md / docs/plans/authorization.md).
  • Postgres (Supabase), --provider postgres.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions