column does not exist when using or() filters
Fix "column does not exist" and "failed to parse logic tree" errors from supabase-js or() filters, caused by embedded tables, unquoted values and wrong column names.
.or() takes a raw PostgREST filter string, not separate arguments. PostgREST reads every name in that string as a column of the table you're querying, unless it's an embedded resource listed in your select. Most or() errors come from that rule or from values that contain a comma.
What you see
A column error (HTTP 400):
{"code":"42703","details":null,"hint":null,"message":"column recipes.chefs does not exist"}
or a parse error:
{"code":"PGRST100","details":"unexpected ...","hint":null,"message":"\"failed to parse logic tree ((title.ilike.%apple,pear%))\" (line 1, column ...)"}
Why it happens
The string you pass to .or() becomes or=(...) in the URL. PostgREST splits it on commas and reads each piece as column.operator.value.
- Using an embedded table's column in the parent's
or().chefs.name.ilike.%ann%isn't valid there: PostgREST expects an operator afterchefs. And ifchefsisn't embedded in yourselect, a filter likechefs.not.is.nullis read as a column namedchefsonrecipes. - Values with commas, dots, parentheses or colons that aren't wrapped in double quotes.
apple,pearis split into two filters. - Filtering an aliased embed by its table name. If you select
chef:chefs(name), filters must usechef, the alias. - The column really doesn't exist, or the name is spelled differently.
createdAtandcreated_atare different columns.
How to fix it
Filter an embedded table with the
referencedTableoption, which applies theorto that table:const { data } = await supabase .from('recipes') .select('id, title, chefs(name)') .or('name.ilike.%ann%,name.ilike.%bo%', { referencedTable: 'chefs' })This filters the embedded chefs only. To keep only recipes that have a matching chef, use
chefs!inner(name)in the select.Quote values that contain reserved characters:
.or('title.eq."Apple, pear and fig tart",title.eq."Pie (classic)"')Clean user input before putting it in the string:
const term = search.replace(/[",().:]/g, ' ').trim() const { data } = await supabase .from('recipes') .select('id, title') .or(`title.ilike.%${term}%,description.ilike.%${term}%`)Nest
andinsideorwhen you need both:.or(`status.eq.published,and(status.eq.draft,owner_id.eq.${userId})`)For conditions across tables (recipe title or chef name), move the logic into a function and call it with RPC:
create or replace function public.search_recipes(term text) returns setof public.recipes language sql stable set search_path = '' as $$ select r.* from public.recipes r join public.chefs c on c.id = r.chef_id where r.title ilike '%' || term || '%' or c.name ilike '%' || term || '%'; $$;const { data } = await supabase.rpc('search_recipes', { term: 'ann' })The function is
security invokerby default, so RLS on both tables still applies.Check the exact column names:
select column_name from information_schema.columns where table_schema = 'public' and table_name = 'recipes';