Перейти до вмісту

Основи безпеки

Supabase "relation does not exist", хоча таблиця на місці

Supabase "relation does not exist" або PGRST205: жодна таблиця не має такої назви в цій схемі. Чотири перевірки, і на що зважити, відкриваючи схему.

Vlad Tkachenko10 хв читання
Будинок у розрізі поруч із шахтою ліфта. Ліфт стоїть на першому поверсі, а його зупинка поверхом вище намальована пунктирним бурштиновим отвором.

Коротко

  • Supabase "relation does not exist" (код Postgres 42P01) означає, що в схемах, які переглянув запит, немає жодної таблиці чи view саме з такою назвою. У вашому застосунку на сучасному проєкті та сама проблема читається як "Could not find the table in the schema cache" (PGRST205).
  • Спершу перевірте назву літера за літерою, разом із великими літерами, потім схему, потім проєкт. Усередині функції з порожнім search_path кожна таблиця потребує схеми попереду: public.invoices.
  • Відкрита схема прибирає помилку й дає API доступ до кожної таблиці в ній, тож перевірте Row Level Security на кожній із цих таблиць, перш ніж її відкривати.

Ваш білдер додав таблицю invoices. Ви бачите її в Table Editor, разом із рядками, а сторінка, яка мала б їх показати, лишається порожньою. Консоль браузера показує ось це:

{
  "code": "PGRST205",
  "details": null,
  "hint": "Perhaps you meant the table 'public.Invoices'",
  "message": "Could not find the table 'public.invoices' in the schema cache"
}

Supabase "relation does not exist" означає, що Postgres не знайшов жодної таблиці чи view саме з такою назвою в схемах, які переглянув запит. PGRST205 дає ту саму відповідь, але від PostgREST, частини Supabase, яка перетворює виклики supabase.from(...) вашого застосунку на запити до бази, ще до того, як запит дійде до Postgres.

Порада, яку для цієї помилки передають з рук у руки, звучить так: додати схему в Exposed schemas у налаштуваннях проєкту. Вона виправляє одну з чотирьох причин нижче. А ще вона відкриває кожну таблицю цієї схеми будь-кому, хто має публічний ключ вашого застосунку, тож прочитайте, що саме вона відкриває, перш ніж її вмикати.

Що означає "relation does not exist" у Supabase?

База шукала таблицю саме з такою назвою в схемах, які їй сказали переглянути, і не знайшла жодної. "Relation" у Postgres позначає все, з чого виходять рядки, тобто таблицю або view. Код помилки 42P01, і Postgres називає його undefined_table.

Уявіть свою базу як будинок. У ньому кожна схема займає окремий поверх: public на першому поверсі, а auth, де Supabase тримає ваших користувачів, на іншому. Кожна таблиця схожа на кімнату з назвою на дверях. Запит, який називає лише кімнату, шукає її на поверхах зі свого шляху пошуку. Запити вашого застосунку їдуть ліфтом, а ліфт зупиняється лише на поверхах, які відкриває ваш проєкт. Помилка означає, що на перевірених поверхах ніхто не знайшов дверей із такою назвою.

Яке формулювання ви побачите, залежить від того, де виконувався запит:

Де виконувався запитЩо повертається
SQL Editor, міграція, функція або тригерrelation "invoices" does not exist, код 42P01
Ваш застосунок, PostgREST 13 або новішийCould not find the table 'public.invoices' in the schema cache, код PGRST205, HTTP 404
Ваш застосунок, старіший PostgRESTчитання отримує relation "public.invoices" does not exist, HTTP 404, а insert отримує порожню помилку
Ваш застосунок просить схему, яку проєкт не відкриваєInvalid schema: billing, код PGRST106, HTTP 406

Changelog PostgREST датує нове формулювання версією 13.0.0, що вийшла 8 травня 2025 року: "Querying non-existent table now returns PGRST205 error". До того, як пояснив один із його мейнтейнерів у issue #3697, читання йшло в Postgres без перевірки й поверталося як 42P01. Посібник Supabase про захист Data API каже, що версію PostgREST свого проєкту можна перевірити в Dashboard. Власний код PostgREST визначає решту: підказка PGRST205, якщо вона є, називає найближчу таблицю, яку PostgREST знає на тому самому поверсі.

Якщо ж у повідомленні стоїть permission denied for table, таблицю знайшли, а ваш застосунок зупинили біля дверей. Для цього випадку є інше виправлення.

Причина 1: назва на дверях має велику літеру

Назва має збігатися точно, разом із великими літерами, і два з інструментів, якими ви користуєтеся, поводяться з великими літерами не так, як третій.

Документація Postgres вміщує правило в одне речення: "Quoting an identifier also makes it case-sensitive, whereas unquoted names are always folded to lower case." Два інші зберігають те, що ви набрали:

  • Table Editor у Supabase створює таблицю саме з тією назвою, яку ви їй дали. Його код бере назву в лапки, коли їх потрібно, тож Invoices лишається Invoices
  • PostgREST бере в подвійні лапки кожну назву, яку надсилає ваш застосунок (pgFmtIdent), тож .from('invoices') просить invoices і нічого іншого
Як створили таблицюУ SQL пишітьУ застосунку пишіть
create table invoices або Invoices, набране в SQL без лапокinvoices.from('invoices')
create table "Invoices" або Invoices, набране в Table Editor"Invoices", разом із лапками.from('Invoices')

Другий рядок дає цю помилку з обох боків. Ваш застосунок викликає .from('invoices') і отримує PGRST205 зі справжньою назвою в підказці, як у повідомленні на початку цієї сторінки. Або застосунок працює, а функція чи policy, що називає Invoices без лапок, падає з relation "invoices" does not exist, бо Postgres перевів назву в малі літери ще до пошуку.

Щоб побачити кожну таблицю з такою назвою, на будь-якому поверсі й з будь-якими великими літерами, виконайте це в SQL Editor:

select table_schema, table_name
from information_schema.tables
where table_name ilike 'invoices';

Виправлення, яке нічого не змінює у ваших даних, лежить у коді: пишіть "Invoices" з лапками в SQL і точні великі літери в застосунку. Якщо натомість перейменувати таблицю, зламається кожен екран, що досі використовує стару назву. Версія цієї помилки для колонок має ту саму пастку з назвами колонок.

Причина 2: таблиця на іншому поверсі

Ваш застосунок шукає в public, якщо йому не сказати інакше, тож таблиці в будь-якій іншій схемі для нього немає. Тому повідомлення й каже public.invoices: public означає поверх, куди поїхав ліфт.

Таке буває трьома способами:

  • ваш білдер створив таблицю у власній схемі, як-от billing чи private, а застосунок викликає звичайне .from('invoices')
  • схема записана в самій назві, як .from('billing.invoices'). PostgREST читає весь рядок як одну назву таблиці й шукає її в public
  • таблиця називається auth.users. Посібник Supabase про дані користувачів каже: "For security, the Auth schema is not exposed in the auto-generated API." Його відповідь полягає у власній таблиці в public, у його прикладі profiles, яку заповнює тригер, коли хтось реєструється, і цей тригер може падати по-своєму

Таблицю на іншому поверсі читають через .schema('billing').from('invoices'), і ця схема має бути в списку Exposed schemas у налаштуваннях API проєкту, як показує посібник Supabase про власні схеми. Доти відповідь змінюється на PGRST106:

{
  "code": "PGRST106",
  "details": null,
  "hint": "Only the following schemas are exposed: public, graphql_public",
  "message": "Invalid schema: billing"
}

Підказка перелічує поверхи, на яких ліфт таки зупиняється. PostgREST 12 формулює ту саму помилку інакше, зі списком просто в повідомленні: The schema must be one of the following: public, graphql_public. Саме такий текст дає його код для цієї версії, а changelog переніс назву схеми в повідомлення у 13.0.7.

Ваш секретний ключ їде тим самим ліфтом. Відкриття схеми належить до налаштувань API, тож серверний код, що використовує supabase-js із цим ключем, отримує той самий PGRST106.

Ліфт зупиняється лише на відкритих поверхах. Таблиці з іншого поверху немає в public, а прохання про цей поверх за назвою відхиляють, поки його не відкриють.

Що відкриває відкрита схема?

Ліфт починає зупинятися на цьому поверсі, а з дозволами з посібника Supabase кожен відвідувач отримує ключ до кожної кімнати там. Row Level Security тоді лишається єдиним, що вирішує, що кожен із них може прочитати чи змінити.

Посібник про власні схеми має два кроки. Перший полягає в налаштуванні Exposed schemas. Другий складається з блоку SQL, і ось його перші два рядки:

GRANT USAGE ON SCHEMA myschema TO anon, authenticated, service_role;
GRANT ALL ON ALL TABLES IN SCHEMA myschema TO anon, authenticated, service_role;

anon стоїть за кожним запитом від того, хто не ввійшов. ALL означає читати, додавати, змінювати й видаляти. Далі блок продовжується через ALTER DEFAULT PRIVILEGES, що дає такий самий доступ кожній таблиці, створеній на цьому поверсі пізніше.

Це безпечно там, де Row Level Security, правила для кожного рядка, які Supabase перевіряє на кожному запиті, увімкнена й написана для кожної таблиці. Посібник Supabase про захист Data API каже: "Tables created through the Supabase Dashboard have RLS enabled by default. Enable RLS explicitly for tables created in the SQL Editor or through another tool." Схема, яку ваш білдер налаштував у SQL, може містити таблиці без неї. Відкрита з такими дозволами, така таблиця віддає кожен рядок будь-кому з вашим публічним ключем і приймає ще й його зміни.

Перевірте перед відкриттям:

select tablename, rowsecurity
from pg_tables
where schemaname = 'billing';

Кожен рядок зі значенням false позначає таблицю зовсім без правил. Увімкнути Row Level Security на кожній таблиці пояснює, як її ввімкнути й написати перше правило, а "RLS disabled in public" пояснює відкрите правило читання, з яким таблиця лишається так само досяжною навіть із увімкненою Row Level Security.

Можна дати й менше дозволів, ніж у посібнику. Поверху з однією таблицею, яку читають лише користувачі після входу, вистачить двох рядків:

grant usage on schema billing to authenticated;
grant select on billing.invoices to authenticated;

Чи відповідають ваші таблиці незнайомцю, можна перевірити ззовні. Наше безкоштовне сканування читає ваш робочий застосунок тим самим публічним ключем, який отримує відвідувач, показує, які таблиці віддають рядки, і не потребує облікового запису.

Поверх, відкритий із дозволами з посібника, отримує відчинені всі двері. Row Level Security працює як замок у кожній кімнаті, а кімната без замка відкрита будь-кому з вашим публічним ключем.

Відкрити схему чи перенести таблицю?

Вирішуйте за тим, хто читає таблицю. Таблиці, яку ваш застосунок читає з браузера, потрібен дозвіл для ролі, що її читає, і Row Level Security з правилом, де б вона не лежала. Тож перенесення в public заощадить вам лише крок із відкриттям схеми.

Якщо переносите:

alter table billing.invoices set schema public;

Таблиця зберігає свої дозволи й правила. Функція, у тілі якої названо billing.invoices, після цього падає з тією самою помилкою, бо тіло зберігає назву такою, якою її написали.

Таблиця, потрібна лише серверному коду, може лишитися на поверсі, куди ліфт ніколи не їздить. Функція в public може зробити з нею ту одну дію, яка потрібна вашому застосунку, а виклик функції Supabase пояснює, як забрати execute у відвідувачів без входу.

Причина 3: функція з порожнім шляхом пошуку

Функція з search_path = '' не переглядає жодного поверху, тож кожна таблиця в ній потребує схеми попереду: public.invoices, ніколи не просто invoices.

Security Advisor у Supabase просить саме такого налаштування. Його попередження function_search_path_mutable каже: "We recommend pinning functions' search_path to an empty string". Та сама сторінка закінчується словами: "once you set the search_path = '' all references to tables/functions/views/etc in your function's body must be qualified with a schema name."

Отже, попередження виправляють одним рядком, а функція після цього падає на першій назві таблиці без схеми:

create or replace function public.count_invoices()
returns bigint
language plpgsql
set search_path = ''
as $$
begin
  return (select count(*) from invoices);  -- relation "invoices" does not exist
end;
$$;

Напишіть from public.invoices, і вона знову запрацює. Залиште налаштування: воно не дає тому, хто викликає функцію, спрямувати назву без схеми на власну таблицю.

Коли функцію викликають із застосунку через .rpc(), помилка проходить крізь PostgREST як 42P01 з HTTP 404, тим статусом, який код PostgREST дає невідомій таблиці. Тригер на нових реєстраціях, що падає так само, видно як "Database error saving new user".

З порожнім шляхом пошуку немає жодного поверху, який можна переглянути, тож кімнату знаходить лише повна адреса.

Причина 4: таблиці ще немає в цьому проєкті

Таблиця існує в іншій базі або лише у вашому коді.

Один обліковий запис Supabase може мати кілька проєктів, а гілка є окремим проєктом із власною базою. Таблиця, створена в SQL Editor не того проєкту, це кімната в іншому будинку. Версія цієї помилки для колонок показує, як порівняти посилання на проєкт в адресі Supabase вашого застосунку з тим, що в dashboard.

Якщо ви будуєте з Lovable, перегляньте чат у пошуках SQL, який вас просили схвалити. Документація Lovable про Supabase каже, що він "writes the SQL, shows it to you, and asks for your approval in the project chat before running it". Поки ви не схвалили, таблиця існує лише в коді, який її читає.

Перенесення таблиці, від якої залежить ваш застосунок

Збережіть копію бази з часу до перенесення й дізнавайтеся, коли змінюється щось, що відкриває ваш застосунок.

Reeve Care зберігає копію вашої бази даних Supabase:

  • зашифрована копія за розкладом, у власному сховищі Reeve в ЄС, поза вашим обліковим записом Supabase
  • кожна схема, яку ви створили, public та інші, з їхніми таблицями й рядками такими, якими вони були до вашої зміни
  • кожна копія перевірена, перш ніж її зараховують, підрахунком рядків у кожній таблиці
  • ваші завантажені файли теж, щойно ви підключите облікові дані Storage
  • відновлення одним кліком, перед яким береться страхувальна копія поточного стану
  • усе, що робить Monitor

Reeve Monitor і далі перевіряє ззовні:

  • чи доступний ваш застосунок, з перевіркою щохвилини
  • повна перевірка безпеки щогодини, до трьох застосунків
  • повідомлення, коли результат змінюється, тож деплой, який змінює те, що відкриває ваш застосунок, дійде до вас, і вам не доведеться йти дивитися
  • щомісячний звіт простою мовою про те, що він побачив

Що зробити зараз

Що робити

  • Прочитайте повідомлення: назва в лапках показує, що саме просили, а public. перед нею показує, яку схему переглянули.
  • Порівняйте назву з Table Editor, разом із великими літерами. Таблиця з великою літерою в назві потребує подвійних лапок у SQL.
  • Знайдіть, у якій схемі лежить таблиця, запитом до information_schema.tables і викликайте таблицю поза public через .schema('назва').from(...).
  • Перш ніж відкривати схему, виконайте запит до pg_tables і ввімкніть Row Level Security на кожній таблиці зі значенням false.
  • Усередині функції з search_path = '' пишіть кожну таблицю як public.назва.
  • Перевірте, що проєкт, який викликає ваш застосунок, той самий, де ви створили таблицю.

Почніть із двох назв у повідомленні, таблиці та схеми перед нею, бо вони точно кажуть, які двері просили і на якому поверсі. Перш ніж переносити, перейменовувати або відкривати щось, чим користуються люди, збережіть копію бази з часу до зміни.

Поширені запитання

Чому Supabase каже, що моєї таблиці не існує?

Бо в схемі, яку переглянув запит, немає таблиці саме з такою назвою. Перевірте чотири речі по черзі: великі літери в назві, схему, у якій лежить таблиця (ваш застосунок шукає в public, якщо йому не сказати інакше), проєкт, з яким говорить ваш застосунок, і чи виконується запит усередині функції з порожнім search_path, де кожна таблиця потребує схеми попереду.

Що означає "the schema must be one of the following"?

Так звучить помилка PGRST106 у старіших версіях PostgREST. Ваш застосунок попросив схему, яку проєкт не відкриває, наприклад через .schema('назва'), а список після цих слів перелічує відкриті схеми. Новіші версії кажуть "Invalid schema: назва" і кладуть той самий список у підказку. Відкрита схема виправляє помилку, і це варто вирішувати свідомо.

Чи безпечно відкривати схему в Supabase?

Лише тоді, коли кожна таблиця в ній має ввімкнену Row Level Security і написане правило. Посібник Supabase про власні схеми дає anon, ролі кожного відвідувача без входу, повний доступ до кожної таблиці схеми, зокрема до тих, що з'являться там пізніше. Його посібник з безпеки додає, що таблиці, створені в SQL Editor, не отримують Row Level Security за замовчуванням.

Чи мають значення великі літери в назві таблиці Supabase?

Так. Назва, написана в SQL без подвійних лапок, переводиться в малі літери, а Table Editor і supabase-js зберігають великі літери саме так, як їх написали. Таблицю, створену як Invoices, у SQL пишуть "Invoices", у лапках, а у вашому застосунку .from('Invoices').

Може, краще перенести таблицю в public?

Перенесення виправляє ту половину помилки, що стосується схеми, і нічого не дає безпеці: таблиці, яку ваш застосунок читає з браузера, потрібен дозвіл для ролі, що її читає, і Row Level Security з правилом, де б вона не лежала. Таблиця, потрібна лише серверному коду, може лишитися в закритій схемі, а до неї дістанеться функція в public, що робить одну потрібну вашому застосунку дію.

Автор

Vlad Tkachenko

Засновник Reeve

Я щодня дивлюся на застосунки, зібрані в Lovable, Bolt, v0, Cursor і Replit, і на короткий список помилок, які трапляються в них знову і знову.

Більше про автора

Читати далі

Усі статті

Зробіть резервну копію бази Supabase перед наступною зміною

Reeve за розкладом зберігає зашифровану копію і відновить її за вас, якщо щось зламається.

Резервні копії Supabase

Автоматична зовнішня перевірка, а не повний аудит. Відсутність знахідок не є гарантією безпеки.