Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cross schema reference with foreign key not working in 4.7.0+ #16585

Closed
afcode123 opened this issue Dec 1, 2022 · 22 comments · Fixed by prisma/prisma-engines#3537
Closed

Cross schema reference with foreign key not working in 4.7.0+ #16585

afcode123 opened this issue Dec 1, 2022 · 22 comments · Fixed by prisma/prisma-engines#3537
Assignees
Labels
bug/2-confirmed Bug has been reproduced and confirmed. kind/bug A reported bug. team/schema Issue for team Schema. tech/engines/migration engine Issue in the Migration Engine topic: multiSchema multiple schemas topic: prisma migrate dev CLI: prisma migrate dev
Milestone

Comments

@afcode123
Copy link

afcode123 commented Dec 1, 2022

Bug description

Running prisma migrate dev gives me following error:

Error: P4002

The schema of the introspected database was inconsistent: Illegal cross schema reference from `public.Users` to `auth.users` in constraint `Users_authId_fkey`. Foreign keys between database schemas are not supported in Prisma. Please follow the GitHub ticket: https://github.com/prisma/prisma/issues/1175

I am using postgres + supabase auth and cross reference is used from public schema to auth schema.

How to reproduce

  1. Use two separate postgres schemas
  2. Reference with foreign key from one table to another
  3. Run prisma migrate dev
  4. See error
Error: P4002

The schema of the introspected database was inconsistent: Illegal cross schema reference from `public.Users` to `auth.users` in constraint `Users_authId_fkey`. Foreign keys between database schemas are not supported in Prisma. Please follow the GitHub ticket: https://github.com/prisma/prisma/issues/1175

Expected behavior

No response

Prisma information

Prisma & prisma-client 4.7.0

Environment & setup

  • OS: MacOS
  • Database: Postgres 14.1 (supabase)
  • Node.js version:

Prisma Version

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["multiSchema"]
  binaryTargets   = ["native", "rhel-openssl-1.0.x"]
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  schemas  = ["auth", "public"]
}

model User {
  id         Int         @id @default(autoincrement())
  authId     String      @unique @db.Uuid
  createdAt  DateTime    @default(now())
  users      users       @relation(fields: [authId], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@map("Users")
  @@schema("public")
}

model users {
  instance_id                 String?       @db.Uuid
  id                          String        @id @db.Uuid
  aud                         String?       @db.VarChar(255)
  role                        String?       @db.VarChar(255)
  email                       String?       @unique @db.VarChar(255)
  encrypted_password          String?       @db.VarChar(255)
  email_confirmed_at          DateTime?     @db.Timestamptz(6)
  invited_at                  DateTime?     @db.Timestamptz(6)
  confirmation_token          String?       @db.VarChar(255)
  confirmation_sent_at        DateTime?     @db.Timestamptz(6)
  recovery_token              String?       @db.VarChar(255)
  recovery_sent_at            DateTime?     @db.Timestamptz(6)
  email_change_token_new      String?       @db.VarChar(255)
  email_change                String?       @db.VarChar(255)
  email_change_sent_at        DateTime?     @db.Timestamptz(6)
  last_sign_in_at             DateTime?     @db.Timestamptz(6)
  raw_app_meta_data           Json?
  raw_user_meta_data          Json?
  is_super_admin              Boolean?
  created_at                  DateTime?     @db.Timestamptz(6)
  updated_at                  DateTime?     @db.Timestamptz(6)
  phone                       String?       @unique @db.VarChar(15)
  phone_confirmed_at          DateTime?     @db.Timestamptz(6)
  phone_change                String?       @default("") @db.VarChar(15)
  phone_change_token          String?       @default("") @db.VarChar(255)
  phone_change_sent_at        DateTime?     @db.Timestamptz(6)
  confirmed_at                DateTime?     @default(dbgenerated("LEAST(email_confirmed_at, phone_confirmed_at)")) @db.Timestamptz(6)
  email_change_token_current  String?       @default("") @db.VarChar(255)
  email_change_confirm_status Int?          @default(0) @db.SmallInt
  banned_until                DateTime?     @db.Timestamptz(6)
  reauthentication_token      String?       @default("") @db.VarChar(255)
  reauthentication_sent_at    DateTime?     @db.Timestamptz(6)
  identities                  identities[]
  mfa_factors                 mfa_factors[]
  sessions                    sessions[]
  User                        User?

  @@index([instance_id])
  @@schema("auth")
}
@afcode123 afcode123 added the kind/bug A reported bug. label Dec 1, 2022
@janpio janpio added the topic: multiSchema multiple schemas label Dec 2, 2022
@janpio
Copy link
Member

janpio commented Dec 2, 2022

How did you create your schema - hand written or using Introspection?
Can you maybe share the SQL of your table(s)?

@eviefp eviefp added team/schema Issue for team Schema. tech/engines/migration engine Issue in the Migration Engine labels Dec 2, 2022
@mjlatty
Copy link

mjlatty commented Dec 2, 2022

I'm having this same problem after doing a db pull from the default database for this Next.JS + Supabase starter project.

After doing some name mapping to get rid of the model user vs model user clash, I'm getting this complaint when I run prisma migrate dev:

Illegal cross schema reference from `public.customers` to `auth.users` in constraint `customers_id_fkey`. Foreign keys between database schemas are not supported in Prisma.

It seems clear to me that my customers foreign key is pointing to the public schema's user model and not the auth schema's AuthUser (mapped to user) model, so this seems like a bug.

I'm very new to Prisma so definitely possible I'm just screwing something up, but I figured this might be helpful for you to know since it should be easily reproducible using that starter project.

Here is my schema.prisma file (Profile is the only custom table I've created that wasn't already in the starter project db.)

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["multiSchema"]
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  schemas  = ["auth", "public"]
}

model audit_log_entries {
  instance_id String?   @db.Uuid
  id          String    @id @db.Uuid
  payload     Json?     @db.Json
  created_at  DateTime? @db.Timestamptz(6)
  ip_address  String    @default("") @db.VarChar(64)

  @@index([instance_id], map: "audit_logs_instance_id_idx")
  @@schema("auth")
}

model identities {
  id              String
  user_id         String    @db.Uuid
  identity_data   Json
  provider        String
  last_sign_in_at DateTime? @db.Timestamptz(6)
  created_at      DateTime? @db.Timestamptz(6)
  updated_at      DateTime? @db.Timestamptz(6)
  users           AuthUser  @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@id([provider, id])
  @@index([user_id])
  @@schema("auth")
}

model instances {
  id              String    @id @db.Uuid
  uuid            String?   @db.Uuid
  raw_base_config String?
  created_at      DateTime? @db.Timestamptz(6)
  updated_at      DateTime? @db.Timestamptz(6)

  @@schema("auth")
}

model mfa_amr_claims {
  session_id            String   @db.Uuid
  created_at            DateTime @db.Timestamptz(6)
  updated_at            DateTime @db.Timestamptz(6)
  authentication_method String
  id                    String   @id(map: "amr_id_pk") @db.Uuid
  sessions              sessions @relation(fields: [session_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@unique([session_id, authentication_method], map: "mfa_amr_claims_session_id_authentication_method_pkey")
  @@schema("auth")
}

model mfa_challenges {
  id          String      @id @db.Uuid
  factor_id   String      @db.Uuid
  created_at  DateTime    @db.Timestamptz(6)
  verified_at DateTime?   @db.Timestamptz(6)
  ip_address  String      @db.Inet
  mfa_factors mfa_factors @relation(fields: [factor_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "mfa_challenges_auth_factor_id_fkey")

  @@schema("auth")
}

model mfa_factors {
  id             String           @id @db.Uuid
  user_id        String           @db.Uuid
  friendly_name  String?
  factor_type    factor_type
  status         factor_status
  created_at     DateTime         @db.Timestamptz(6)
  updated_at     DateTime         @db.Timestamptz(6)
  secret         String?
  mfa_challenges mfa_challenges[]
  users          AuthUser         @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@index([user_id, created_at], map: "factor_id_created_at_idx")
  @@schema("auth")
}

model refresh_tokens {
  instance_id String?   @db.Uuid
  id          BigInt    @id @default(autoincrement())
  token       String?   @unique(map: "refresh_tokens_token_unique") @db.VarChar(255)
  user_id     String?   @db.VarChar(255)
  revoked     Boolean?
  created_at  DateTime? @db.Timestamptz(6)
  updated_at  DateTime? @db.Timestamptz(6)
  parent      String?   @db.VarChar(255)
  session_id  String?   @db.Uuid
  sessions    sessions? @relation(fields: [session_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@index([session_id], map: "refresh_token_session_id")
  @@index([instance_id])
  @@index([instance_id, user_id])
  @@index([parent])
  @@index([session_id, revoked])
  @@index([token])
  @@schema("auth")
}

model saml_providers {
  id                String        @id @db.Uuid
  sso_provider_id   String        @db.Uuid
  entity_id         String        @unique
  metadata_xml      String
  metadata_url      String?
  attribute_mapping Json?
  created_at        DateTime?     @db.Timestamptz(6)
  updated_at        DateTime?     @db.Timestamptz(6)
  sso_providers     sso_providers @relation(fields: [sso_provider_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@index([sso_provider_id])
  @@schema("auth")
}

model saml_relay_states {
  id              String        @id @db.Uuid
  sso_provider_id String        @db.Uuid
  request_id      String
  for_email       String?
  redirect_to     String?
  from_ip_address String?       @db.Inet
  created_at      DateTime?     @db.Timestamptz(6)
  updated_at      DateTime?     @db.Timestamptz(6)
  sso_providers   sso_providers @relation(fields: [sso_provider_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@index([for_email])
  @@index([sso_provider_id])
  @@schema("auth")
}

model schema_migrations {
  version String @id @db.VarChar(255)

  @@schema("auth")
}

model sessions {
  id             String           @id @db.Uuid
  user_id        String           @db.Uuid
  created_at     DateTime?        @db.Timestamptz(6)
  updated_at     DateTime?        @db.Timestamptz(6)
  factor_id      String?          @db.Uuid
  aal            aal_level?
  not_after      DateTime?        @db.Timestamptz(6)
  mfa_amr_claims mfa_amr_claims[]
  refresh_tokens refresh_tokens[]
  users          AuthUser         @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
  sso_sessions   sso_sessions[]

  @@index([user_id])
  @@index([user_id, created_at], map: "user_id_created_at_idx")
  @@schema("auth")
}

model sso_domains {
  id              String        @id @db.Uuid
  sso_provider_id String        @db.Uuid
  domain          String
  created_at      DateTime?     @db.Timestamptz(6)
  updated_at      DateTime?     @db.Timestamptz(6)
  sso_providers   sso_providers @relation(fields: [sso_provider_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@index([sso_provider_id])
  @@schema("auth")
}

model sso_providers {
  id                String              @id @db.Uuid
  resource_id       String?
  created_at        DateTime?           @db.Timestamptz(6)
  updated_at        DateTime?           @db.Timestamptz(6)
  saml_providers    saml_providers[]
  saml_relay_states saml_relay_states[]
  sso_domains       sso_domains[]
  sso_sessions      sso_sessions[]

  @@schema("auth")
}

model sso_sessions {
  id              String         @id @db.Uuid
  session_id      String         @db.Uuid
  sso_provider_id String?        @db.Uuid
  not_before      DateTime?      @db.Timestamptz(6)
  not_after       DateTime?      @db.Timestamptz(6)
  idp_initiated   Boolean?       @default(false)
  created_at      DateTime?      @db.Timestamptz(6)
  updated_at      DateTime?      @db.Timestamptz(6)
  sessions        sessions       @relation(fields: [session_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
  sso_providers   sso_providers? @relation(fields: [sso_provider_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@index([session_id])
  @@index([sso_provider_id])
  @@schema("auth")
}

model AuthUser {
  instance_id                 String?       @db.Uuid
  id                          String        @id @db.Uuid
  aud                         String?       @db.VarChar(255)
  role                        String?       @db.VarChar(255)
  email                       String?       @unique @db.VarChar(255)
  encrypted_password          String?       @db.VarChar(255)
  email_confirmed_at          DateTime?     @db.Timestamptz(6)
  invited_at                  DateTime?     @db.Timestamptz(6)
  confirmation_token          String?       @db.VarChar(255)
  confirmation_sent_at        DateTime?     @db.Timestamptz(6)
  recovery_token              String?       @db.VarChar(255)
  recovery_sent_at            DateTime?     @db.Timestamptz(6)
  email_change_token_new      String?       @db.VarChar(255)
  email_change                String?       @db.VarChar(255)
  email_change_sent_at        DateTime?     @db.Timestamptz(6)
  last_sign_in_at             DateTime?     @db.Timestamptz(6)
  raw_app_meta_data           Json?
  raw_user_meta_data          Json?
  is_super_admin              Boolean?
  created_at                  DateTime?     @db.Timestamptz(6)
  updated_at                  DateTime?     @db.Timestamptz(6)
  phone                       String?       @unique @db.VarChar(15)
  phone_confirmed_at          DateTime?     @db.Timestamptz(6)
  phone_change                String?       @default("") @db.VarChar(15)
  phone_change_token          String?       @default("") @db.VarChar(255)
  phone_change_sent_at        DateTime?     @db.Timestamptz(6)
  confirmed_at                DateTime?     @default(dbgenerated("LEAST(email_confirmed_at, phone_confirmed_at)")) @db.Timestamptz(6)
  email_change_token_current  String?       @default("") @db.VarChar(255)
  email_change_confirm_status Int?          @default(0) @db.SmallInt
  banned_until                DateTime?     @db.Timestamptz(6)
  reauthentication_token      String?       @default("") @db.VarChar(255)
  reauthentication_sent_at    DateTime?     @db.Timestamptz(6)
  identities                  identities[]
  mfa_factors                 mfa_factors[]
  sessions                    sessions[]

  users users?

  @@index([instance_id])
  @@schema("auth")
}

model customers {
  id                 String  @id @db.Uuid
  stripe_customer_id String?
  users              users   @relation(fields: [id], references: [id], onDelete: NoAction, onUpdate: NoAction)

  @@schema("public")
}

model prices {
  id                String                 @id
  product_id        String?
  active            Boolean?
  description       String?
  unit_amount       BigInt?
  currency          String?
  type              pricing_type?
  interval          pricing_plan_interval?
  interval_count    Int?
  trial_period_days Int?
  metadata          Json?
  products          products?              @relation(fields: [product_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
  subscriptions     subscriptions[]

  @@schema("public")
}

model products {
  id          String   @id
  active      Boolean?
  name        String?
  description String?
  image       String?
  metadata    Json?
  prices      prices[]

  @@schema("public")
}

model Profile {
  id          BigInt    @id(map: "profile_pkey") @default(autoincrement())
  createdAt   DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
  displayName String?   @map("display_name")
  bio         String?

  @@map("profiles")
  @@schema("public")
}

model subscriptions {
  id                   String               @id
  user_id              String               @db.Uuid
  status               subscription_status?
  metadata             Json?
  price_id             String?
  quantity             Int?
  cancel_at_period_end Boolean?
  created              DateTime             @default(dbgenerated("timezone('utc'::text, now())")) @db.Timestamptz(6)
  current_period_start DateTime             @default(dbgenerated("timezone('utc'::text, now())")) @db.Timestamptz(6)
  current_period_end   DateTime             @default(dbgenerated("timezone('utc'::text, now())")) @db.Timestamptz(6)
  ended_at             DateTime?            @default(dbgenerated("timezone('utc'::text, now())")) @db.Timestamptz(6)
  cancel_at            DateTime?            @default(dbgenerated("timezone('utc'::text, now())")) @db.Timestamptz(6)
  canceled_at          DateTime?            @default(dbgenerated("timezone('utc'::text, now())")) @db.Timestamptz(6)
  trial_start          DateTime?            @default(dbgenerated("timezone('utc'::text, now())")) @db.Timestamptz(6)
  trial_end            DateTime?            @default(dbgenerated("timezone('utc'::text, now())")) @db.Timestamptz(6)
  prices               prices?              @relation(fields: [price_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
  users                users                @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction)

  @@schema("public")
}

model users {
  id             String          @id @db.Uuid
  fullName       String?         @map("full_name")
  avatarUrl      String?         @map("avatar_url")
  billingAddress Json?           @map("billing_address")
  paymentMethod  Json?           @map("payment_method")
  users          AuthUser        @relation(fields: [id], references: [id], onDelete: NoAction, onUpdate: NoAction)
  subscriptions  subscriptions[]
  customers      customers?

  @@schema("public")
}

enum aal_level {
  aal1
  aal2
  aal3

  @@schema("auth")
}

enum factor_status {
  unverified
  verified

  @@schema("auth")
}

enum factor_type {
  totp
  webauthn

  @@schema("auth")
}

enum pricing_plan_interval {
  day
  week
  month
  year

  @@schema("public")
}

enum pricing_type {
  one_time
  recurring

  @@schema("public")
}

enum subscription_status {
  trialing
  active
  canceled
  incomplete
  incomplete_expired
  past_due
  unpaid

  @@schema("public")
}

@firzanarmani
Copy link

I'd like to offer another reproduction of this bug on Supabase. (Hope it somehow helps)

How to reproduce

  1. Create a new Supabase database
  2. Run the "User Management Starter" SQL snippet on the new database
SQL snippet here for reference
-- Create a table for public profiles
create table profiles (
  id uuid references auth.users not null primary key,
  updated_at timestamp with time zone,
  username text unique,
  full_name text,
  avatar_url text,
  website text,

  constraint username_length check (char_length(username) >= 3)
);
-- Set up Row Level Security (RLS)
-- See https://supabase.com/docs/guides/auth/row-level-security for more details.
alter table profiles
  enable row level security;

create policy "Public profiles are viewable by everyone." on profiles
  for select using (true);

create policy "Users can insert their own profile." on profiles
  for insert with check (auth.uid() = id);

create policy "Users can update own profile." on profiles
  for update using (auth.uid() = id);

-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
create function public.handle_new_user()
returns trigger as $$
begin
  insert into public.profiles (id, full_name, avatar_url)
  values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
  return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();

-- Set up Storage!
insert into storage.buckets (id, name)
  values ('avatars', 'avatars');

-- Set up access controls for storage.
-- See https://supabase.com/docs/guides/storage#policy-examples for more details.
create policy "Avatar images are publicly accessible." on storage.objects
  for select using (bucket_id = 'avatars');

create policy "Anyone can upload an avatar." on storage.objects
  for insert with check (bucket_id = 'avatars');
  1. Set the schema.prisma to the following
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["multiSchema"]
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  schemas  = ["auth", "public"]
}
  1. Run prisma db pull to introspect
New schema.prisma looks like this
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["multiSchema"]
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  schemas  = ["auth", "public"]
}

model audit_log_entries {
  instance_id String?   @db.Uuid
  id          String    @id @db.Uuid
  payload     Json?     @db.Json
  created_at  DateTime? @db.Timestamptz(6)
  ip_address  String    @default("") @db.VarChar(64)

  @@index([instance_id], map: "audit_logs_instance_id_idx")
  @@schema("auth")
}

model identities {
  id              String
  user_id         String    @db.Uuid
  identity_data   Json
  provider        String
  last_sign_in_at DateTime? @db.Timestamptz(6)
  created_at      DateTime? @db.Timestamptz(6)
  updated_at      DateTime? @db.Timestamptz(6)
  users           users     @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@id([provider, id])
  @@index([user_id])
  @@schema("auth")
}

model instances {
  id              String    @id @db.Uuid
  uuid            String?   @db.Uuid
  raw_base_config String?
  created_at      DateTime? @db.Timestamptz(6)
  updated_at      DateTime? @db.Timestamptz(6)

  @@schema("auth")
}

model mfa_amr_claims {
  session_id            String   @db.Uuid
  created_at            DateTime @db.Timestamptz(6)
  updated_at            DateTime @db.Timestamptz(6)
  authentication_method String
  id                    String   @id(map: "amr_id_pk") @db.Uuid
  sessions              sessions @relation(fields: [session_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@unique([session_id, authentication_method], map: "mfa_amr_claims_session_id_authentication_method_pkey")
  @@schema("auth")
}

model mfa_challenges {
  id          String      @id @db.Uuid
  factor_id   String      @db.Uuid
  created_at  DateTime    @db.Timestamptz(6)
  verified_at DateTime?   @db.Timestamptz(6)
  ip_address  String      @db.Inet
  mfa_factors mfa_factors @relation(fields: [factor_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "mfa_challenges_auth_factor_id_fkey")

  @@schema("auth")
}

model mfa_factors {
  id             String           @id @db.Uuid
  user_id        String           @db.Uuid
  friendly_name  String?
  factor_type    factor_type
  status         factor_status
  created_at     DateTime         @db.Timestamptz(6)
  updated_at     DateTime         @db.Timestamptz(6)
  secret         String?
  mfa_challenges mfa_challenges[]
  users          users            @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@index([user_id, created_at], map: "factor_id_created_at_idx")
  @@schema("auth")
}

model refresh_tokens {
  instance_id String?   @db.Uuid
  id          BigInt    @id @default(autoincrement())
  token       String?   @unique(map: "refresh_tokens_token_unique") @db.VarChar(255)
  user_id     String?   @db.VarChar(255)
  revoked     Boolean?
  created_at  DateTime? @db.Timestamptz(6)
  updated_at  DateTime? @db.Timestamptz(6)
  parent      String?   @db.VarChar(255)
  session_id  String?   @db.Uuid
  sessions    sessions? @relation(fields: [session_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@index([session_id], map: "refresh_token_session_id")
  @@index([instance_id])
  @@index([instance_id, user_id])
  @@index([parent])
  @@index([session_id, revoked])
  @@index([token])
  @@schema("auth")
}

model saml_providers {
  id                String        @id @db.Uuid
  sso_provider_id   String        @db.Uuid
  entity_id         String        @unique
  metadata_xml      String
  metadata_url      String?
  attribute_mapping Json?
  created_at        DateTime?     @db.Timestamptz(6)
  updated_at        DateTime?     @db.Timestamptz(6)
  sso_providers     sso_providers @relation(fields: [sso_provider_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@index([sso_provider_id])
  @@schema("auth")
}

model saml_relay_states {
  id              String        @id @db.Uuid
  sso_provider_id String        @db.Uuid
  request_id      String
  for_email       String?
  redirect_to     String?
  from_ip_address String?       @db.Inet
  created_at      DateTime?     @db.Timestamptz(6)
  updated_at      DateTime?     @db.Timestamptz(6)
  sso_providers   sso_providers @relation(fields: [sso_provider_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@index([for_email])
  @@index([sso_provider_id])
  @@schema("auth")
}

model schema_migrations {
  version String @id @db.VarChar(255)

  @@schema("auth")
}

model sessions {
  id             String           @id @db.Uuid
  user_id        String           @db.Uuid
  created_at     DateTime?        @db.Timestamptz(6)
  updated_at     DateTime?        @db.Timestamptz(6)
  factor_id      String?          @db.Uuid
  aal            aal_level?
  not_after      DateTime?        @db.Timestamptz(6)
  mfa_amr_claims mfa_amr_claims[]
  refresh_tokens refresh_tokens[]
  users          users            @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
  sso_sessions   sso_sessions[]

  @@index([user_id])
  @@index([user_id, created_at], map: "user_id_created_at_idx")
  @@schema("auth")
}

model sso_domains {
  id              String        @id @db.Uuid
  sso_provider_id String        @db.Uuid
  domain          String
  created_at      DateTime?     @db.Timestamptz(6)
  updated_at      DateTime?     @db.Timestamptz(6)
  sso_providers   sso_providers @relation(fields: [sso_provider_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@index([sso_provider_id])
  @@schema("auth")
}

model sso_providers {
  id                String              @id @db.Uuid
  resource_id       String?
  created_at        DateTime?           @db.Timestamptz(6)
  updated_at        DateTime?           @db.Timestamptz(6)
  saml_providers    saml_providers[]
  saml_relay_states saml_relay_states[]
  sso_domains       sso_domains[]
  sso_sessions      sso_sessions[]

  @@schema("auth")
}

model sso_sessions {
  id              String         @id @db.Uuid
  session_id      String         @db.Uuid
  sso_provider_id String?        @db.Uuid
  not_before      DateTime?      @db.Timestamptz(6)
  not_after       DateTime?      @db.Timestamptz(6)
  idp_initiated   Boolean?       @default(false)
  created_at      DateTime?      @db.Timestamptz(6)
  updated_at      DateTime?      @db.Timestamptz(6)
  sessions        sessions       @relation(fields: [session_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
  sso_providers   sso_providers? @relation(fields: [sso_provider_id], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@index([session_id])
  @@index([sso_provider_id])
  @@schema("auth")
}

model users {
  instance_id                 String?       @db.Uuid
  id                          String        @id @db.Uuid
  aud                         String?       @db.VarChar(255)
  role                        String?       @db.VarChar(255)
  email                       String?       @unique @db.VarChar(255)
  encrypted_password          String?       @db.VarChar(255)
  email_confirmed_at          DateTime?     @db.Timestamptz(6)
  invited_at                  DateTime?     @db.Timestamptz(6)
  confirmation_token          String?       @db.VarChar(255)
  confirmation_sent_at        DateTime?     @db.Timestamptz(6)
  recovery_token              String?       @db.VarChar(255)
  recovery_sent_at            DateTime?     @db.Timestamptz(6)
  email_change_token_new      String?       @db.VarChar(255)
  email_change                String?       @db.VarChar(255)
  email_change_sent_at        DateTime?     @db.Timestamptz(6)
  last_sign_in_at             DateTime?     @db.Timestamptz(6)
  raw_app_meta_data           Json?
  raw_user_meta_data          Json?
  is_super_admin              Boolean?
  created_at                  DateTime?     @db.Timestamptz(6)
  updated_at                  DateTime?     @db.Timestamptz(6)
  phone                       String?       @unique @db.VarChar(15)
  phone_confirmed_at          DateTime?     @db.Timestamptz(6)
  phone_change                String?       @default("") @db.VarChar(15)
  phone_change_token          String?       @default("") @db.VarChar(255)
  phone_change_sent_at        DateTime?     @db.Timestamptz(6)
  confirmed_at                DateTime?     @default(dbgenerated("LEAST(email_confirmed_at, phone_confirmed_at)")) @db.Timestamptz(6)
  email_change_token_current  String?       @default("") @db.VarChar(255)
  email_change_confirm_status Int?          @default(0) @db.SmallInt
  banned_until                DateTime?     @db.Timestamptz(6)
  reauthentication_token      String?       @default("") @db.VarChar(255)
  reauthentication_sent_at    DateTime?     @db.Timestamptz(6)
  identities                  identities[]
  mfa_factors                 mfa_factors[]
  sessions                    sessions[]
  profiles                    profiles?

  @@index([instance_id])
  @@schema("auth")
}

model profiles {
  id         String    @id @db.Uuid
  updated_at DateTime? @db.Timestamptz(6)
  username   String?   @unique
  full_name  String?
  avatar_url String?
  website    String?
  users      users     @relation(fields: [id], references: [id], onDelete: NoAction, onUpdate: NoAction)

  @@schema("public")
}

enum aal_level {
  aal1
  aal2
  aal3

  @@schema("auth")
}

enum factor_status {
  unverified
  verified

  @@schema("auth")
}

enum factor_type {
  totp
  webauthn

  @@schema("auth")
}
  1. Run prisma migrate dev --name init
  2. Error encountered
Error: P4002

The schema of the introspected database was inconsistent: Illegal cross schema reference from `public.profiles` to `auth.users` in constraint `profiles_id_fkey`. Foreign keys between database schemas are not supported in Prisma. Please follow the GitHub ticket: https://github.com/prisma/prisma/issues/1175

Prisma Version

prisma                  : 4.7.1
@prisma/client          : 4.7.1
Current platform        : darwin
Query Engine (Node-API) : libquery-engine 272861e07ab64f234d3ffc4094e32bd61775599c (at node_modules/@prisma/engines/libquery_engine-darwin.dylib.node)
Migration Engine        : migration-engine-cli 272861e07ab64f234d3ffc4094e32bd61775599c (at node_modules/@prisma/engines/migration-engine-darwin)
Introspection Engine    : introspection-core 272861e07ab64f234d3ffc4094e32bd61775599c (at node_modules/@prisma/engines/introspection-engine-darwin)
Format Binary           : prisma-fmt 272861e07ab64f234d3ffc4094e32bd61775599c (at node_modules/@prisma/engines/prisma-fmt-darwin)
Format Wasm             : @prisma/prisma-fmt-wasm 4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c
Default Engines Hash    : 272861e07ab64f234d3ffc4094e32bd61775599c
Studio                  : 0.477.0
Preview Features        : multiSchema

@afcode123
Copy link
Author

How did you create your schema - hand written or using Introspection? Can you maybe share the SQL of your table(s)?

Hi! Sorry for late reply but in my case I used both approaches. I first wrote the "User" - model into the schema and ran prisma migrate. After this I added the "auth" schema into the prisma schema file, so that it recognizes the supabase auth tables. Then I executed prisma db pull command which imported the supabase auth tables into my prisma schema file.
Finally I added the relation from public.User table to auth.users table. This is when the error occurred as I tried to migrate the changes.

I can also verify both @firzanarmani and @mjlatty cases, as they are basically same setups as I have.

@janpio janpio added the bug/1-unconfirmed Bug should have enough information for reproduction, but confirmation has not happened yet. label Dec 4, 2022
@janpio janpio added this to the 4.8.0 milestone Dec 4, 2022
@janpio
Copy link
Member

janpio commented Dec 4, 2022

Thanks for providing the information here, I took a look to reproduce:

  1. Introspecting just the Supabase connection string with nothing created as expected returns that there are not tables/models
  2. After running the SQL that @firzanarmani provided, db pull correctly complains that cross schema references are illegal - the preview feature is not enabled.
  3. When then adding the preview feature multiSchema, I still get the error message. This is already a bit confusing, as I of course only forgot to supply the list of schemas as a property.
  4. When I do supply the lists of schemas, as also suggested by @firzanarmani, I get a correctly introspected schema that looks as expected.
  5. When I then do a migrate dev as you all described, I again get this error message. It looks like if there is an Introspection excecution behind the scenes that does not get the multiSchema preview feature or the list of schemas.

Full output with debugging enabled:

> npx prisma migrate dev
  prisma:engines  binaries to download libquery-engine, migration-engine, introspection-engine, prisma-fmt +0ms
  prisma:loadEnv  project root found at C:\Users\Jan\Documents\throwaway\crossSchemaIntrospection\package.json +0ms
  prisma:tryLoadEnv  Environment variables loaded from C:\Users\Jan\Documents\throwaway\crossSchemaIntrospection\.env +0ms
  prisma:getConfig  Using getConfig Wasm +0ms
  prisma:getConfig  config data retrieved without errors in getConfig Wasm +9ms
  prisma:loadEnv  project root found at C:\Users\Jan\Documents\throwaway\crossSchemaIntrospection\package.json +16ms
  prisma:tryLoadEnv  Environment variables loaded from C:\Users\Jan\Documents\throwaway\crossSchemaIntrospection\.env +15ms
Environment variables loaded from .env
Prisma schema loaded from prisma\schema.prisma
  prisma:getConfig  Using getConfig Wasm +10ms
  prisma:getConfig  config data retrieved without errors in getConfig Wasm +9ms
Datasource "db": PostgreSQL database "cross", schema "public" at "pg-db-provision.cm0mkpwj8arx.eu-central-1.rds.amazonaws.com:5432"

  prisma:getDMMF  Using CLI Query Engine (Node-API Library) at: C:\Users\Jan\Documents\throwaway\crossSchemaIntrospection\node_modules\@prisma\engines\query_engine-windows.dll.node +0ms
  prisma:getDMMF  Loaded Node-API Library +1ms
  prisma:getDMMF  unserialized dmmf result ready +32ms
  prisma:getDMMF  dmmf retrieved without errors in getDmmfNodeAPI +11ms
  prisma:getConfig  Using getConfig Wasm +55ms
  prisma:getConfig  config data retrieved without errors in getConfig Wasm +6ms
  prisma:getConfig  Using getConfig Wasm +2ms
  prisma:getConfig  config data retrieved without errors in getConfig Wasm +5ms
  prisma:migrateEngine:rpc  starting migration engine with binary: C:\Users\Jan\Documents\throwaway\crossSchemaIntrospection\node_modules\@prisma\engines\migration-engine-windows.exe +0ms
  prisma:migrateEngine:rpc  SENDING RPC CALL {"id":1,"jsonrpc":"2.0","method":"devDiagnostic","params":{"migrationsDirectoryPath":"C:\\Users\\Jan\\Documents\\throwaway\\crossSchemaIntrospection\\prisma\\migrations"}} +10ms
  prisma:migrateEngine:stderr  {"timestamp":"2022-12-04T19:38:49.9383737Z","level":"INFO","fields":{"message":"Starting migration engine RPC server","git_hash":"272861e07ab64f234d3ffc4094e32bd61775599c"},"target":"migration_engine"} +0ms
  prisma:migrateEngine:rpc  {
  jsonrpc: '2.0',
  error: {
    code: 4466,
    message: 'An error happened. Check the data field for details.',
    data: {
      is_panic: false,
      message: 'The schema of the introspected database was inconsistent: Illegal cross schema reference from `public.profiles` to `auth.users` in constraint `profiles_id_fkey`. Foreign keys between database schemas are not supported in Prisma. Please follow the GitHub ticket: https://github.com/prisma/prisma/issues/1175',
      meta: [Object],
      error_code: 'P4002'
    }
  },
  id: 1
} +2s
Error: Error: P4002

The schema of the introspected database was inconsistent: Illegal cross schema reference from `public.profiles` to `auth.users` in constraint `profiles_id_fkey`. Foreign keys between database schemas are not supported in Prisma. Please follow the GitHub ticket: https://github.com/prisma/prisma/issues/1175

    at Object.<anonymous> (C:\Users\Jan\Documents\throwaway\crossSchemaIntrospection\node_modules\prisma\build\index.js:93592:25)
    at MigrateEngine.handleResponse (C:\Users\Jan\Documents\throwaway\crossSchemaIntrospection\node_modules\prisma\build\index.js:93445:36)
    at LineStream3.<anonymous> (C:\Users\Jan\Documents\throwaway\crossSchemaIntrospection\node_modules\prisma\build\index.js:93544:16)
    at LineStream3.emit (node:events:513:28)
    at addChunk (node:internal/streams/readable:324:12)
    at readableAddChunk (node:internal/streams/readable:297:9)
    at Readable.push (node:internal/streams/readable:234:10)
    at LineStream3._pushBuffer (C:\Users\Jan\Documents\throwaway\crossSchemaIntrospection\node_modules\prisma\build\index.js:93325:17)
    at LineStream3._transform (C:\Users\Jan\Documents\throwaway\crossSchemaIntrospection\node_modules\prisma\build\index.js:93319:8)
    at Transform._write (node:internal/streams/transform:175:8)

For easier reproduction (without having to create a Supabase database) here is a full SQL dump for the auth and the public schemas:

-- DROP SCHEMA auth;

CREATE SCHEMA auth AUTHORIZATION supabase_admin;

-- DROP TYPE auth."aal_level";

CREATE TYPE auth."aal_level" AS ENUM (
	'aal1',
	'aal2',
	'aal3');

-- DROP TYPE auth."factor_status";

CREATE TYPE auth."factor_status" AS ENUM (
	'unverified',
	'verified');

-- DROP TYPE auth."factor_type";

CREATE TYPE auth."factor_type" AS ENUM (
	'totp',
	'webauthn');

-- DROP SEQUENCE auth.refresh_tokens_id_seq;

CREATE SEQUENCE auth.refresh_tokens_id_seq
	INCREMENT BY 1
	MINVALUE 1
	MAXVALUE 9223372036854775807
	START 1
	CACHE 1
	NO CYCLE;-- auth.audit_log_entries definition

-- Drop table

-- DROP TABLE auth.audit_log_entries;

CREATE TABLE auth.audit_log_entries (
	instance_id uuid NULL,
	id uuid NOT NULL,
	payload json NULL,
	created_at timestamptz NULL,
	ip_address varchar(64) NOT NULL DEFAULT ''::character varying,
	CONSTRAINT audit_log_entries_pkey PRIMARY KEY (id)
);
CREATE INDEX audit_logs_instance_id_idx ON auth.audit_log_entries USING btree (instance_id);


-- auth.instances definition

-- Drop table

-- DROP TABLE auth.instances;

CREATE TABLE auth.instances (
	id uuid NOT NULL,
	uuid uuid NULL,
	raw_base_config text NULL,
	created_at timestamptz NULL,
	updated_at timestamptz NULL,
	CONSTRAINT instances_pkey PRIMARY KEY (id)
);


-- auth.schema_migrations definition

-- Drop table

-- DROP TABLE auth.schema_migrations;

CREATE TABLE auth.schema_migrations (
	"version" varchar(255) NOT NULL,
	CONSTRAINT schema_migrations_pkey PRIMARY KEY (version)
);


-- auth.sso_providers definition

-- Drop table

-- DROP TABLE auth.sso_providers;

CREATE TABLE auth.sso_providers (
	id uuid NOT NULL,
	resource_id text NULL,
	created_at timestamptz NULL,
	updated_at timestamptz NULL,
	CONSTRAINT "resource_id not empty" CHECK (((resource_id = NULL::text) OR (char_length(resource_id) > 0))),
	CONSTRAINT sso_providers_pkey PRIMARY KEY (id)
);
CREATE UNIQUE INDEX sso_providers_resource_id_idx ON auth.sso_providers USING btree (lower(resource_id));


-- auth.users definition

-- Drop table

-- DROP TABLE auth.users;

CREATE TABLE auth.users (
	instance_id uuid NULL,
	id uuid NOT NULL,
	aud varchar(255) NULL,
	"role" varchar(255) NULL,
	email varchar(255) NULL,
	encrypted_password varchar(255) NULL,
	email_confirmed_at timestamptz NULL,
	invited_at timestamptz NULL,
	confirmation_token varchar(255) NULL,
	confirmation_sent_at timestamptz NULL,
	recovery_token varchar(255) NULL,
	recovery_sent_at timestamptz NULL,
	email_change_token_new varchar(255) NULL,
	email_change varchar(255) NULL,
	email_change_sent_at timestamptz NULL,
	last_sign_in_at timestamptz NULL,
	raw_app_meta_data jsonb NULL,
	raw_user_meta_data jsonb NULL,
	is_super_admin bool NULL,
	created_at timestamptz NULL,
	updated_at timestamptz NULL,
	phone varchar(15) NULL DEFAULT NULL::character varying,
	phone_confirmed_at timestamptz NULL,
	phone_change varchar(15) NULL DEFAULT ''::character varying,
	phone_change_token varchar(255) NULL DEFAULT ''::character varying,
	phone_change_sent_at timestamptz NULL,
	confirmed_at timestamptz NULL GENERATED ALWAYS AS (LEAST(email_confirmed_at, phone_confirmed_at)) STORED,
	email_change_token_current varchar(255) NULL DEFAULT ''::character varying,
	email_change_confirm_status int2 NULL DEFAULT 0,
	banned_until timestamptz NULL,
	reauthentication_token varchar(255) NULL DEFAULT ''::character varying,
	reauthentication_sent_at timestamptz NULL,
	CONSTRAINT users_email_change_confirm_status_check CHECK (((email_change_confirm_status >= 0) AND (email_change_confirm_status <= 2))),
	CONSTRAINT users_email_key UNIQUE (email),
	CONSTRAINT users_phone_key UNIQUE (phone),
	CONSTRAINT users_pkey PRIMARY KEY (id)
);
CREATE UNIQUE INDEX confirmation_token_idx ON auth.users USING btree (confirmation_token) WHERE ((confirmation_token)::text !~ '^[0-9 ]*$'::text);
CREATE UNIQUE INDEX email_change_token_current_idx ON auth.users USING btree (email_change_token_current) WHERE ((email_change_token_current)::text !~ '^[0-9 ]*$'::text);
CREATE UNIQUE INDEX email_change_token_new_idx ON auth.users USING btree (email_change_token_new) WHERE ((email_change_token_new)::text !~ '^[0-9 ]*$'::text);
CREATE UNIQUE INDEX reauthentication_token_idx ON auth.users USING btree (reauthentication_token) WHERE ((reauthentication_token)::text !~ '^[0-9 ]*$'::text);
CREATE UNIQUE INDEX recovery_token_idx ON auth.users USING btree (recovery_token) WHERE ((recovery_token)::text !~ '^[0-9 ]*$'::text);
CREATE INDEX users_instance_id_email_idx ON auth.users USING btree (instance_id, lower((email)::text));
CREATE INDEX users_instance_id_idx ON auth.users USING btree (instance_id);

-- Table Triggers

create trigger on_auth_user_created after
insert
    on
    auth.users for each row execute function handle_new_user();


-- auth.identities definition

-- Drop table

-- DROP TABLE auth.identities;

CREATE TABLE auth.identities (
	id text NOT NULL,
	user_id uuid NOT NULL,
	identity_data jsonb NOT NULL,
	provider text NOT NULL,
	last_sign_in_at timestamptz NULL,
	created_at timestamptz NULL,
	updated_at timestamptz NULL,
	CONSTRAINT identities_pkey PRIMARY KEY (provider, id),
	CONSTRAINT identities_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE
);
CREATE INDEX identities_user_id_idx ON auth.identities USING btree (user_id);


-- auth.mfa_factors definition

-- Drop table

-- DROP TABLE auth.mfa_factors;

CREATE TABLE auth.mfa_factors (
	id uuid NOT NULL,
	user_id uuid NOT NULL,
	friendly_name text NULL,
	"factor_type" auth."factor_type" NOT NULL,
	status auth."factor_status" NOT NULL,
	created_at timestamptz NOT NULL,
	updated_at timestamptz NOT NULL,
	secret text NULL,
	CONSTRAINT mfa_factors_pkey PRIMARY KEY (id),
	CONSTRAINT mfa_factors_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE
);
CREATE INDEX factor_id_created_at_idx ON auth.mfa_factors USING btree (user_id, created_at);
CREATE UNIQUE INDEX mfa_factors_user_friendly_name_unique ON auth.mfa_factors USING btree (friendly_name, user_id) WHERE (TRIM(BOTH FROM friendly_name) <> ''::text);


-- auth.saml_providers definition

-- Drop table

-- DROP TABLE auth.saml_providers;

CREATE TABLE auth.saml_providers (
	id uuid NOT NULL,
	sso_provider_id uuid NOT NULL,
	entity_id text NOT NULL,
	metadata_xml text NOT NULL,
	metadata_url text NULL,
	attribute_mapping jsonb NULL,
	created_at timestamptz NULL,
	updated_at timestamptz NULL,
	CONSTRAINT "entity_id not empty" CHECK ((char_length(entity_id) > 0)),
	CONSTRAINT "metadata_url not empty" CHECK (((metadata_url = NULL::text) OR (char_length(metadata_url) > 0))),
	CONSTRAINT "metadata_xml not empty" CHECK ((char_length(metadata_xml) > 0)),
	CONSTRAINT saml_providers_entity_id_key UNIQUE (entity_id),
	CONSTRAINT saml_providers_pkey PRIMARY KEY (id),
	CONSTRAINT saml_providers_sso_provider_id_fkey FOREIGN KEY (sso_provider_id) REFERENCES auth.sso_providers(id) ON DELETE CASCADE
);
CREATE INDEX saml_providers_sso_provider_id_idx ON auth.saml_providers USING btree (sso_provider_id);


-- auth.saml_relay_states definition

-- Drop table

-- DROP TABLE auth.saml_relay_states;

CREATE TABLE auth.saml_relay_states (
	id uuid NOT NULL,
	sso_provider_id uuid NOT NULL,
	request_id text NOT NULL,
	for_email text NULL,
	redirect_to text NULL,
	from_ip_address inet NULL,
	created_at timestamptz NULL,
	updated_at timestamptz NULL,
	CONSTRAINT "request_id not empty" CHECK ((char_length(request_id) > 0)),
	CONSTRAINT saml_relay_states_pkey PRIMARY KEY (id),
	CONSTRAINT saml_relay_states_sso_provider_id_fkey FOREIGN KEY (sso_provider_id) REFERENCES auth.sso_providers(id) ON DELETE CASCADE
);
CREATE INDEX saml_relay_states_for_email_idx ON auth.saml_relay_states USING btree (for_email);
CREATE INDEX saml_relay_states_sso_provider_id_idx ON auth.saml_relay_states USING btree (sso_provider_id);


-- auth.sessions definition

-- Drop table

-- DROP TABLE auth.sessions;

CREATE TABLE auth.sessions (
	id uuid NOT NULL,
	user_id uuid NOT NULL,
	created_at timestamptz NULL,
	updated_at timestamptz NULL,
	factor_id uuid NULL,
	aal auth."aal_level" NULL,
	not_after timestamptz NULL,
	CONSTRAINT sessions_pkey PRIMARY KEY (id),
	CONSTRAINT sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE
);
CREATE INDEX sessions_user_id_idx ON auth.sessions USING btree (user_id);
CREATE INDEX user_id_created_at_idx ON auth.sessions USING btree (user_id, created_at);


-- auth.sso_domains definition

-- Drop table

-- DROP TABLE auth.sso_domains;

CREATE TABLE auth.sso_domains (
	id uuid NOT NULL,
	sso_provider_id uuid NOT NULL,
	"domain" text NOT NULL,
	created_at timestamptz NULL,
	updated_at timestamptz NULL,
	CONSTRAINT "domain not empty" CHECK ((char_length(domain) > 0)),
	CONSTRAINT sso_domains_pkey PRIMARY KEY (id),
	CONSTRAINT sso_domains_sso_provider_id_fkey FOREIGN KEY (sso_provider_id) REFERENCES auth.sso_providers(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX sso_domains_domain_idx ON auth.sso_domains USING btree (lower(domain));
CREATE INDEX sso_domains_sso_provider_id_idx ON auth.sso_domains USING btree (sso_provider_id);


-- auth.sso_sessions definition

-- Drop table

-- DROP TABLE auth.sso_sessions;

CREATE TABLE auth.sso_sessions (
	id uuid NOT NULL,
	session_id uuid NOT NULL,
	sso_provider_id uuid NULL,
	not_before timestamptz NULL,
	not_after timestamptz NULL,
	idp_initiated bool NULL DEFAULT false,
	created_at timestamptz NULL,
	updated_at timestamptz NULL,
	CONSTRAINT sso_sessions_pkey PRIMARY KEY (id),
	CONSTRAINT sso_sessions_session_id_fkey FOREIGN KEY (session_id) REFERENCES auth.sessions(id) ON DELETE CASCADE,
	CONSTRAINT sso_sessions_sso_provider_id_fkey FOREIGN KEY (sso_provider_id) REFERENCES auth.sso_providers(id) ON DELETE CASCADE
);
CREATE INDEX sso_sessions_session_id_idx ON auth.sso_sessions USING btree (session_id);
CREATE INDEX sso_sessions_sso_provider_id_idx ON auth.sso_sessions USING btree (sso_provider_id);


-- auth.mfa_amr_claims definition

-- Drop table

-- DROP TABLE auth.mfa_amr_claims;

CREATE TABLE auth.mfa_amr_claims (
	session_id uuid NOT NULL,
	created_at timestamptz NOT NULL,
	updated_at timestamptz NOT NULL,
	authentication_method text NOT NULL,
	id uuid NOT NULL,
	CONSTRAINT amr_id_pk PRIMARY KEY (id),
	CONSTRAINT mfa_amr_claims_session_id_authentication_method_pkey UNIQUE (session_id, authentication_method),
	CONSTRAINT mfa_amr_claims_session_id_fkey FOREIGN KEY (session_id) REFERENCES auth.sessions(id) ON DELETE CASCADE
);


-- auth.mfa_challenges definition

-- Drop table

-- DROP TABLE auth.mfa_challenges;

CREATE TABLE auth.mfa_challenges (
	id uuid NOT NULL,
	factor_id uuid NOT NULL,
	created_at timestamptz NOT NULL,
	verified_at timestamptz NULL,
	ip_address inet NOT NULL,
	CONSTRAINT mfa_challenges_pkey PRIMARY KEY (id),
	CONSTRAINT mfa_challenges_auth_factor_id_fkey FOREIGN KEY (factor_id) REFERENCES auth.mfa_factors(id) ON DELETE CASCADE
);


-- auth.refresh_tokens definition

-- Drop table

-- DROP TABLE auth.refresh_tokens;

CREATE TABLE auth.refresh_tokens (
	instance_id uuid NULL,
	id bigserial NOT NULL,
	"token" varchar(255) NULL,
	user_id varchar(255) NULL,
	revoked bool NULL,
	created_at timestamptz NULL,
	updated_at timestamptz NULL,
	parent varchar(255) NULL,
	session_id uuid NULL,
	CONSTRAINT refresh_tokens_pkey PRIMARY KEY (id),
	CONSTRAINT refresh_tokens_token_unique UNIQUE (token),
	CONSTRAINT refresh_tokens_session_id_fkey FOREIGN KEY (session_id) REFERENCES auth.sessions(id) ON DELETE CASCADE
);
CREATE INDEX refresh_token_session_id ON auth.refresh_tokens USING btree (session_id);
CREATE INDEX refresh_tokens_instance_id_idx ON auth.refresh_tokens USING btree (instance_id);
CREATE INDEX refresh_tokens_instance_id_user_id_idx ON auth.refresh_tokens USING btree (instance_id, user_id);
CREATE INDEX refresh_tokens_parent_idx ON auth.refresh_tokens USING btree (parent);
CREATE INDEX refresh_tokens_session_id_revoked_idx ON auth.refresh_tokens USING btree (session_id, revoked);
CREATE INDEX refresh_tokens_token_idx ON auth.refresh_tokens USING btree (token);



CREATE OR REPLACE FUNCTION auth.email()
 RETURNS text
 LANGUAGE sql
 STABLE
AS $function$
  select 
  	coalesce(
		nullif(current_setting('request.jwt.claim.email', true), ''),
		(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'email')
	)::text
$function$
;

CREATE OR REPLACE FUNCTION auth.jwt()
 RETURNS jsonb
 LANGUAGE sql
 STABLE
AS $function$
  select 
    coalesce(
        nullif(current_setting('request.jwt.claim', true), ''),
        nullif(current_setting('request.jwt.claims', true), '')
    )::jsonb
$function$
;

CREATE OR REPLACE FUNCTION auth.role()
 RETURNS text
 LANGUAGE sql
 STABLE
AS $function$
  select 
  	coalesce(
		nullif(current_setting('request.jwt.claim.role', true), ''),
		(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role')
	)::text
$function$
;

CREATE OR REPLACE FUNCTION auth.uid()
 RETURNS uuid
 LANGUAGE sql
 STABLE
AS $function$
  select 
  	coalesce(
		nullif(current_setting('request.jwt.claim.sub', true), ''),
		(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')
	)::uuid
$function$
;

-- DROP SCHEMA public;

CREATE SCHEMA public AUTHORIZATION postgres;

COMMENT ON SCHEMA public IS 'standard public schema';
-- public.profiles definition

-- Drop table

-- DROP TABLE public.profiles;

CREATE TABLE public.profiles (
	id uuid NOT NULL,
	updated_at timestamptz NULL,
	username text NULL,
	full_name text NULL,
	avatar_url text NULL,
	website text NULL,
	CONSTRAINT profiles_pkey PRIMARY KEY (id),
	CONSTRAINT profiles_username_key UNIQUE (username),
	CONSTRAINT username_length CHECK ((char_length(username) >= 3))
);


-- public.profiles foreign keys

ALTER TABLE public.profiles ADD CONSTRAINT profiles_id_fkey FOREIGN KEY (id) REFERENCES auth.users(id);



CREATE OR REPLACE FUNCTION public.handle_new_user()
 RETURNS trigger
 LANGUAGE plpgsql
 SECURITY DEFINER
AS $function$
begin
  insert into public.profiles (id, full_name, avatar_url)
  values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
  return new;
end;
$function$
;

CREATE OR REPLACE FUNCTION public.install_available_extensions_and_test()
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE extension_name TEXT;
allowed_extentions TEXT[] := string_to_array(current_setting('supautils.privileged_extensions'), ',');
BEGIN 
  FOREACH extension_name IN ARRAY allowed_extentions 
  LOOP
    SELECT trim(extension_name) INTO extension_name;
    /* skip below extensions check for now */
    CONTINUE WHEN extension_name = 'pgroonga' OR  extension_name = 'pgroonga_database' OR extension_name = 'pgsodium';
    CONTINUE WHEN extension_name = 'plpgsql' OR  extension_name = 'plpgsql_check' OR extension_name = 'pgtap';
    CONTINUE WHEN extension_name = 'supabase_vault' OR extension_name = 'wrappers';
    RAISE notice 'START TEST FOR: %', extension_name;
    EXECUTE format('DROP EXTENSION IF EXISTS %s CASCADE', quote_ident(extension_name));
    EXECUTE format('CREATE EXTENSION %s CASCADE', quote_ident(extension_name));
    RAISE notice 'END TEST FOR: %', extension_name;
  END LOOP;
    RAISE notice 'EXTENSION TESTS COMPLETED..';
    return true;
END;
$function$
;

(Executing this will have a few errors, but you can just ignore them as it does not have influence on the result)

@janpio janpio added bug/2-confirmed Bug has been reproduced and confirmed. and removed bug/1-unconfirmed Bug should have enough information for reproduction, but confirmation has not happened yet. labels Dec 4, 2022
@janpio
Copy link
Member

janpio commented Dec 4, 2022

(The observation along the way lead me to create an improvement issue as well: #16615)

@tomhoule tomhoule assigned tomhoule and unassigned tomhoule Dec 5, 2022
@janpio
Copy link
Member

janpio commented Dec 5, 2022

Here is a simplified reproduction.

  • Given a minimal Prisma schema

    // ./prisma/schema.prisma
    generator client {
      provider        = "prisma-client-js"
      previewFeatures = ["multiSchema"]
    }
    
    datasource db {
      provider = "postgresql"
      url      = env("DATABASE_URL")
      schemas  = ["auth", "public"]
    }

    and the following docker-compose.yml file with Postgres 15

    # ./docker-compose.yml
    
    version: '3.7'
    
    services:
      postgres:
        image: postgres:15
        restart: always
        environment:
          - POSTGRES_DB=postgres
          - POSTGRES_USER=prisma
          - POSTGRES_PASSWORD=prisma
        volumes:
          - ./sql:/docker-entrypoint-initdb.d
        ports:
          - '5432:5432'
    

    create a ddl.sql file into a new ./sql folder:

    -- ./sql/ddl.sql
    
    CREATE SCHEMA auth AUTHORIZATION prisma;
    
    -- auth.users definition
    CREATE TABLE auth.users (
        id uuid NOT NULL,
        CONSTRAINT users_pkey PRIMARY KEY (id)
    );
    
    -- public.profiles definition
    CREATE TABLE public.profiles (
        id uuid NOT NULL,
        CONSTRAINT profiles_pkey PRIMARY KEY (id)
    );
    
    -- public.profiles foreign keys
    ALTER TABLE public.profiles ADD CONSTRAINT profiles_id_fkey FOREIGN KEY (id) REFERENCES auth.users(id);
  • Define the $DATABASE_URL variable as "postgres://prisma:prisma@localhost:5432/postgres", e.g. export DATABASE_URL="postgres://prisma:prisma@localhost:5432/postgres"

  • Initialize and run the database with docker-compose up

  • Introspect the database via npx prisma@4.7.1 db pull. ./prisma/schema.prisma should now look like the following:

    generator client {
      provider        = "prisma-client-js"
      previewFeatures = ["multiSchema"]
    }
    
    datasource db {
      provider = "postgresql"
      url      = env("DATABASE_URL")
      schemas  = ["auth", "public"]
    }
    
    model users {
      id       String    @id @db.Uuid
      profiles profiles?
    
      @@schema("auth")
    }
    
    model profiles {
      id    String @id @db.Uuid
      users users  @relation(fields: [id], references: [id], onDelete: NoAction, onUpdate: NoAction)
    
      @@schema("public")
    }
  • Run npx prisma@4.7.1 migrate dev to see the error:

    Prisma schema loaded from schema.prisma
    Datasource "db": PostgreSQL database "postgres", schema "public" at "localhost:5432"
    
    Error: P4002
    
    The schema of the introspected database was inconsistent: Illegal cross schema reference from `public.profiles` to `auth.users` in constraint `profiles_id_fkey`. Foreign keys between database schemas are not supported in Prisma. Please follow the GitHub ticket: https://github.com/prisma/prisma/issues/1175
    

@janpio
Copy link
Member

janpio commented Dec 5, 2022

Hey @firzanarmani @mjlatty and @afcode123, what exactly does your connection string look like?
What is the full output when you run migrate dev?

We have a theory that somehow, sometimes migrate dev does not get all the information it requires and hence thinks that it should not allow cross schema foreign keys. With that information we might be able to pinpoint or develop a workaround.

@firzanarmani
Copy link

firzanarmani commented Dec 5, 2022

I'm using the connection string as given in the 'Database Settings' in Supabase, i.e.
postgresql://[db-user]:[db-password]@db.[db-host].supabase.co:5432/[db-name]

The full output I get from running migrate dev is simply:

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "db.[db-host].supabase.co:5432"

Error: P4002

The schema of the introspected database was inconsistent: Illegal cross schema reference from `public.profiles` to `auth.users` in constraint `profiles_id_fkey`. Foreign keys between database schemas are not supported in Prisma. Please follow the GitHub ticket: https://github.com/prisma/prisma/issues/1175

Not too sure if there is a way to increase the verbosity of the output from the command.

Hope this helps. Feel free to correct me if I'm doing anything wrong.

@Jolg42
Copy link
Member

Jolg42 commented Dec 5, 2022

Using 4.8.0-dev.22 internal version or 4.7.1 (PostgreSQL 10.21)
Note I had to create manually the auth schema, the provided SQL was not enough.

With the following schema

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

npx prisma db pull also errors

Prisma schema loaded from prisma/schema.prisma
Environment variables loaded from prisma/.env
Datasource "db": PostgreSQL database "16585", schema "public" at "localhost:5432"

✖ Introspecting based on datasource defined in prisma/schema.prisma

Error: P4002

The schema of the introspected database was inconsistent: Illegal cross schema reference from `public.profiles` to `auth.users` in constraint `profiles_id_fkey`. Foreign keys between database schemas are not supported in Prisma. Please follow the GitHub ticket: https://github.com/prisma/prisma/issues/1175

With

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["multiSchema"]
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  schemas  = ["auth", "public"]
}

npx prisma db pull does not error, only migrate dev

@janpio
Copy link
Member

janpio commented Dec 5, 2022

No, that is great @firzanarmani. What does you Prisma schema look like before you run npx prisma db pull?

@firzanarmani
Copy link

Before running prisma db pull, my schema looks like this

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["multiSchema"]
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  schemas  = ["auth", "public"]
}

Worth noting that, similar to your reproduction attempt:

  1. Introspecting just the Supabase connection string with nothing created as expected returns that there are not tables/models

  2. After running the SQL that @firzanarmani provided, db pull correctly complains that cross schema references are illegal - the preview feature is not enabled.

  3. When then adding the preview feature multiSchema, I still get the error message. This is already a bit confusing, as I of course only forgot to supply the list of schemas as a property.

I find these to be true as well, which led me to using the schema as above.

@mjlatty
Copy link

mjlatty commented Dec 6, 2022

@janpio here's the full output of migrate dev for me:

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "db.qgpmxkwdgvsaeijbauxr.supabase.co:5432"

Error: P4002

The schema of the introspected database was inconsistent: Illegal cross schema reference from `public.customers` to `auth.users` in constraint `customers_id_fkey`. Foreign keys between database schemas are not supported in Prisma. Please follow the GitHub ticket: https://github.com/prisma/prisma/issues/1175

@Jolg42 Jolg42 added the topic: prisma migrate dev CLI: prisma migrate dev label Dec 6, 2022
tomhoule added a commit to prisma/prisma-engines that referenced this issue Dec 8, 2022
The namespaces are from the schema the engine was started with

This should address prisma/prisma#16634 and prisma/prisma#16585
tomhoule added a commit to prisma/prisma-engines that referenced this issue Dec 8, 2022
The namespaces are from the schema the engine was started with

This should address prisma/prisma#16634 and prisma/prisma#16585
tomhoule added a commit to prisma/prisma-engines that referenced this issue Dec 9, 2022
#3481)

The namespaces are from the schema the engine was started with

This should address prisma/prisma#16634 and prisma/prisma#16585
@pimeys
Copy link
Contributor

pimeys commented Dec 19, 2022

Should be fixed in prisma/prisma-engines#3481

@Jolg42
Copy link
Member

Jolg42 commented Dec 19, 2022

Note: it's going in tomorrow's, 4.8.0 release

@chrskerr
Copy link

unfortunately I am still getting the error in 4.8.0 (version below)

prisma                  : 4.8.0
@prisma/client          : 4.8.0
Current platform        : darwin-arm64
Query Engine (Node-API) : libquery-engine d6e67a83f971b175a593ccc12e15c4a757f93ffe (at ../../node_modules/@prisma/engines/libquery_engine-darwin-arm64.dylib.node)
Migration Engine        : migration-engine-cli d6e67a83f971b175a593ccc12e15c4a757f93ffe (at ../../node_modules/@prisma/engines/migration-engine-darwin-arm64)
Introspection Engine    : introspection-core d6e67a83f971b175a593ccc12e15c4a757f93ffe (at ../../node_modules/@prisma/engines/introspection-engine-darwin-arm64)
Format Binary           : prisma-fmt d6e67a83f971b175a593ccc12e15c4a757f93ffe (at ../../node_modules/@prisma/engines/prisma-fmt-darwin-arm64)
Format Wasm             : @prisma/prisma-fmt-wasm 4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe
Default Engines Hash    : d6e67a83f971b175a593ccc12e15c4a757f93ffe
Studio                  : 0.479.0
Preview Features        : multiSchema, postgresqlExtensions, clientExtensions

@janpio
Copy link
Member

janpio commented Dec 20, 2022

I can unfortunately reproduce the problem with the schema you sent over @chrskerr, when creating and applying a migration with it npx prisma migrate dev 😞

@janpio janpio reopened this Dec 20, 2022
@janpio janpio modified the milestones: 4.8.0, 4.9.0 Dec 20, 2022
@chrskerr
Copy link

Sorry @janpio I got mixed up between dev and deploy 🤦‍♂️ re-running now with yarn prisma migrate deploy returns the error P4002, I can send over my init migration file if that helpers too?

@pimeys
Copy link
Contributor

pimeys commented Dec 21, 2022

I localized this issue finally. What's going on here is that first, if you define the schemas:

datasource db {
  schemas = ["foo", "bar"]
}

And you either migrate dev, db push or db pull that needs to introspect the SQL Schema. If it finds a foreign key from a table that's in either foo or bar schema, but pointing to a third schema baz not included in the schemas list, an error is thrown.

A quick workaround for now is to include all the schemas visible in the error message to the schemas list in the datasource. We're currently looking into making this to work a bit better, some options I have in my mind:

  • The error should tell the user to add the schema to the list
  • We should only warn, and skip the @relation attribute

@janpio janpio changed the title Cross schema reference with foreign key not working in 4.7.0. Cross schema reference with foreign key not working in 4.7.0+ Dec 21, 2022
@janpio
Copy link
Member

janpio commented Dec 21, 2022

(I am not sure if @pimey's information above is correct - but please take a look if you can make it work via that somehow)

@albertilagan
Copy link

I can't seem to figure out why but here are some findings on our end.

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  schemas  = ["app", "identity", "reference"]
}

failed!: DATABASE_URL="postgresql://postgres:postgres@localhost:5437/dev?schema=app"
works!: DATABASE_URL="postgresql://postgres:postgres@localhost:5437/dev?schema=reference"
works!: DATABASE_URL="postgresql://postgres:postgres@localhost:5437/dev" (prisma migrations defaults public)

changing reference back to public

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  schemas  = ["app", "identity", "public"]
}

failed!: DATABASE_URL="postgresql://postgres:postgres@localhost:5437/dev?schema=app"
works!: DATABASE_URL="postgresql://postgres:postgres@localhost:5437/dev?schema=public"
works!: DATABASE_URL="postgresql://postgres:postgres@localhost:5437/dev" (prisma migrations defaults public)
works!: DATABASE_URL="postgresql://postgres:postgres@localhost:5437/dev?schema=prisma"

With all that, changing the default schema via DATABASE_URL seems to work for us.
Not sure if this is a solution but it is one that works for us.

@janpio
Copy link
Member

janpio commented Dec 21, 2022

Yes, I think that matches what Julius found out later as well. We'll have a fix for this hopefully soon to test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug/2-confirmed Bug has been reproduced and confirmed. kind/bug A reported bug. team/schema Issue for team Schema. tech/engines/migration engine Issue in the Migration Engine topic: multiSchema multiple schemas topic: prisma migrate dev CLI: prisma migrate dev
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants