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

Wrong type for joins with one-to-one relationships #471

Closed
2 tasks done
probablykasper opened this issue Aug 4, 2023 · 21 comments
Closed
2 tasks done

Wrong type for joins with one-to-one relationships #471

probablykasper opened this issue Aug 4, 2023 · 21 comments
Labels
bug Something isn't working

Comments

@probablykasper
Copy link

probablykasper commented Aug 4, 2023

Bug report

  • I confirm this is a bug with Supabase, not with my own application.
  • I confirm I have searched the Docs, GitHub Discussions, and Discord.

Describe the bug

I have a profiles and a customers table, with a one-to-one relationship:

profiles: {
  id: text, primary key
  ...
}
customers: {
  id: references profiles.id, primary key
  stripe_customer_id: text, unique
  ...
}

When doing a join between these, TypeScript thinks the type of customers is this:

{
    stripe_customer_id: string;
}[]

But when I run the code, this is the actual type I get:

{
    stripe_customer_id: string;
}

So when accessing the stripe_customer_id property, I get this type error:

Property 'stripe_customer_id' does not exist on type '{ stripe_customer_id: string; }[]'.

To Reproduce

const profile_result = await supabase
	.from('profiles')
	.select('id, customers ( stripe_customer_id )')
	.eq('id', params.id)
	.single()
if (profile_result.data) {
	console.log(profile_result.data.customers.stripe_customer_id)
    //                                        ^
    // Property 'stripe_customer_id' does not exist on
    // type '{ stripe_customer_id: string; }[]'.ts(2339)
}

Expected behavior

The TypeScript type should not be an array for one-to-one relationships. It should match the actual type

System information

  • OS: macOS
  • Version of supabase-js: 2.31.0
  • Version of Node.js: 18.14.2

Additional context

Related:

@probablykasper probablykasper added the bug Something isn't working label Aug 4, 2023
@aslakhellesoy
Copy link

aslakhellesoy commented Aug 7, 2023

I'm using this as a workaround for now:

export function cast<T>(notAnArray: T[]): T {
  return notAnArray as T;
}

And then:

console.log(cast(profile_result.data.customers).stripe_customer_id)

@aslakhellesoy
Copy link

Update: I've changed my workaround to this:

export function fixOneToOne<T>(objectOrNull: T[]): T | null {
  return (objectOrNull as T) || null;
}

I've changed the return type to T | null because the relationship may indeed be null. In the example above, this would happen when there is no customers record for the given profile.

In the example above, the correct type should be:

{
    stripe_customer_id: string;
} | null

@steve-chavez steve-chavez transferred this issue from supabase/supabase-js Aug 28, 2023
@FocusCookie
Copy link

I encountered the same issue today and I found another workaround.

const profile_result = await supabase
	.from('profiles')
	.select('id, customers ( stripe_customer_id )')
	.eq('id', params.id)
	.single()
        .returns<YourType[]>();

# type definition
type YourType {
    the type that you expect without the array
}

@timlgl
Copy link

timlgl commented Sep 12, 2023

I am experiencing the exact same issue. As my query is rather nested, workarounds are very inconvenient...

Does anyone know, whether an older version does not have this bug included?

@orels1
Copy link

orels1 commented Dec 17, 2023

Seems to still be an issue

@albertonii
Copy link

Same problem over here, is there a fix for this issue??

@Silventino
Copy link

Same problem here.

@DhenPadilla
Copy link

Yep, same here! Please fix when possible

@chrissy0
Copy link

Still a problem, please fix!

@fvermaut
Copy link

fvermaut commented Jun 7, 2024

same problem for me

@sanagurcia
Copy link

Please fix!

@avallete
Copy link
Member

Hi there,

I believe this bug might have been fixed. I tried to reproduce the issue with the following setup:

SQL Setup:

-- Create profiles table
CREATE TABLE profiles (
  id TEXT PRIMARY KEY
);

-- Create customers table with a foreign key to profiles
CREATE TABLE customers (
  id TEXT PRIMARY KEY REFERENCES profiles(id),
  stripe_customer_id TEXT UNIQUE
);

INSERT INTO profiles (id) VALUES ('1');
INSERT INTO customers (id, stripe_customer_id) VALUES ('1', '1');

TypeScript Test:

const profile_result = await supabase
  .from('profiles')
  .select('id, customers (stripe_customer_id)')
  .single();

console.log(profile_result.data);

With this setup, I got the expected results both from a types and runtime perspective:

TypeScript Types:

const profile_result: PostgrestSingleResponse<{
  id: string;
  customers: {
    stripe_customer_id: string | null;
  } | null;
}>

Runtime Output:

{
  "id": "1",
  "customers": {
    "stripe_customer_id": "1"
  }
}

Could you please check on your end if it's fixed for you as well?

@probablykasper
Copy link
Author

Looks like it's fixed yeah

@lorraineS
Copy link

lorraineS commented Sep 29, 2024

Absolutely not fixed.
A one-to-one relationship should not expect an array in this case.

Tables

CREATE TABLE companies (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL,
    scraped_site_id UUID,
    description TEXT,
);

CREATE TABLE surveys (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    company_id UUID REFERENCES companies(id),
);

CREATE TABLE scraped_sites (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    raw_data JSONB
);

Typescript

const { data, error } = await supabase
      .from("surveys")
      .select(
        `
        company:companies ( name, description, scraped_site:scraped_sites ( raw_data ) )
      `
      )
      .eq("id", surveyId)
      .single();

Capture d’écran 2024-09-29 à 09 57 31

Last version used (2.45.4)

@hawkcookie
Copy link

I think it will be difficult to investigate without also giving the related SQL schema.

Absolutely not fixed.
A one-to-one relationship should not expect an array in this case.

@lorraineS
Copy link

lorraineS commented Sep 29, 2024

I think it will be difficult to investigate without also giving the related SQL schema.

Absolutely not fixed.
A one-to-one relationship should not expect an array in this case.

You are right, I updated my previous message to include the tables to reproduce the issue.

@hawkcookie
Copy link

hawkcookie commented Sep 29, 2024

I checked the schema in my environment, and the type relationships generated by Supabase with the same schema you provided were correctly inferred.

スクリーンショット 2024-09-29 19 46 12

Based on your screenshot, I noticed that not only the one-to-one relationships, but also the text type columns are inferred as any type, which is concerning. Are the types being generated correctly by the Supabase CLI, and is the output file properly generated?

Here’s what I did

  • npx create-next-app -e with-supabase
  • add .env.local to supabase envrioment variables.
  • execute your schema sql in dashboard.
  • npx supabase gen types --lang=typescript --project-id XXXXXX --schema public > database.types.ts
  • add createServerClient to Database type generated by supabase cli.
 export const createClient = () => {
-  const cookieStore = cookies();
+       const cookieStore = cookies();

-  return createServerClient(
-    process.env.NEXT_PUBLIC_SUPABASE_URL!,
-    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
-    {
-      cookies: {
-        getAll() {
-          return cookieStore.getAll();
-        },
-        setAll(cookiesToSet) {
-          try {
-            cookiesToSet.forEach(({ name, value, options }) => {
-              cookieStore.set(name, value, options);
-            });
-          } catch (error) {
-            // The `set` method was called from a Server Component.
-            // This can be ignored if you have middleware refreshing
-            // user sessions.
-          }
-        },
-      },
-    },
-  );
+       return createServerClient<Database>(
+               process.env.NEXT_PUBLIC_SUPABASE_URL!,
+               process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+               {
+                       cookies: {
+                               getAll() {
+                                       return cookieStore.getAll();
+                               },
+                               setAll(cookiesToSet) {
+                                       try {
+                                               cookiesToSet.forEach(({ name, value, options }) => {
+                                                       cookieStore.set(name, value, options);
+                                               });
+                                       } catch (error) {
+                                               // The `set` method was called from a Server Component.
+                                               // This can be ignored if you have middleware refreshing
+                                               // user sessions.
+                                       }
+                               },
+                       },
+               },
+       );
 };

I think it will be difficult to investigate without also giving the related SQL schema.

Absolutely not fixed.
A one-to-one relationship should not expect an array in this case.

You are right, I updated my previous message to include the tables to reproduce the issue.

@lorraineS
Copy link

J'ai vérifié le schéma dans mon environnement et les relations de type générées par Supabase avec le même schéma que vous avez fourni ont été correctement déduites.

2024-09-29 19 46 12 D'après votre capture d'écran, j'ai remarqué que non seulement les relations un à un, mais aussi les colonnes de type texte sont déduites de n'importe quel type, ce qui est inquiétant. Les types sont-ils générés correctement par la CLI Supabase et le fichier de sortie est-il correctement généré ?

Voilà ce que j'ai fait

  • npx create-next-app -e with-supabase
  • ajoutez .env.local aux variables d'environnement supabase.
  • exécutez votre schéma SQL dans le tableau de bord.
  • npx supabase gen types --lang=typescript --project-id XXXXXX --schema public > database.types.ts
  • ajoutez createServerClient au type de base de données généré par supabase cli.
 export const createClient = () => {
-  const cookieStore = cookies();
+       const cookieStore = cookies();

-  return createServerClient(
-    process.env.NEXT_PUBLIC_SUPABASE_URL!,
-    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
-    {
-      cookies: {
-        getAll() {
-          return cookieStore.getAll();
-        },
-        setAll(cookiesToSet) {
-          try {
-            cookiesToSet.forEach(({ name, value, options }) => {
-              cookieStore.set(name, value, options);
-            });
-          } catch (error) {
-            // The `set` method was called from a Server Component.
-            // This can be ignored if you have middleware refreshing
-            // user sessions.
-          }
-        },
-      },
-    },
-  );
+       return createServerClient<Database>(
+               process.env.NEXT_PUBLIC_SUPABASE_URL!,
+               process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+               {
+                       cookies: {
+                               getAll() {
+                                       return cookieStore.getAll();
+                               },
+                               setAll(cookiesToSet) {
+                                       try {
+                                               cookiesToSet.forEach(({ name, value, options }) => {
+                                                       cookieStore.set(name, value, options);
+                                               });
+                                       } catch (error) {
+                                               // The `set` method was called from a Server Component.
+                                               // This can be ignored if you have middleware refreshing
+                                               // user sessions.
+                                       }
+                               },
+                       },
+               },
+       );
 };

Je pense qu'il sera difficile d'enquêter sans donner également le schéma SQL associé.

Absolument pas fixe.
Une relation un à un ne devrait pas s'attendre à un tableau dans ce cas.

Vous avez raison, j'ai mis à jour mon message précédent pour inclure les tableaux pour reproduire le problème.

Thank you, that fixed the problem.

@elyobo
Copy link

elyobo commented Feb 4, 2025

Seems fixed with .single() but not without it - the relationship is the same whether .single() or not though, it's surprising that you'd get an object with .single() and an array normally. Using a foreign key to a primary key on another table is very common and it's very weird that you end up getting an array of results for that.

Using !inner also resolves it, which is also weird, because the inner join doesn't change the cardinality.

@avallete
Copy link
Member

avallete commented Feb 4, 2025

Seems fixed with .single() but not without it - the relationship is the same whether .single() or not though, it's surprising that you'd get an object with .single() and an array normally. Using a foreign key to a primary key on another table is very common and it's very weird that you end up getting an array of results for that.

Using !inner also resolves it, which is also weird, because the inner join doesn't change the cardinality.

Hi there, would you mind providing a minimal reproduction example of your case with both the SQL to generate the relation and the select query you're running ?

.single should only affect the "root result" not the underlying relationship type. If I can reproduce I could investigate into this, please feel free to open a new issue and ping me on it.

I'm locking this issue as the original problem is fixed and similar ones will likely be under a different scope. Please open a new issue following the typing bug reporting infos:

Bug Reporting:
You can open an issue on GitHub. Please include:

The generated Database type used to instantiate the client (e.g., createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY)). If possible a minimal SQL declaration resulting in such Database type.
The query where type inference failed (e.g., .from('which-table').select('which-query')).
Your TypeScript version (npx tsc -v).

@elyobo
Copy link

elyobo commented Feb 4, 2025

For the .single() vs non-single behaviour, I confirmed with the example SQL (after removing some trailing commas!) above to ensure that it wasn't anything of mine.

I'll check now to see whether !inner also resolves it.

@supabase supabase locked as resolved and limited conversation to collaborators Feb 4, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests