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.

  1. Using an embedded table's column in the parent's or(). chefs.name.ilike.%ann% isn't valid there: PostgREST expects an operator after chefs. And if chefs isn't embedded in your select, a filter like chefs.not.is.null is read as a column named chefs on recipes.
  2. Values with commas, dots, parentheses or colons that aren't wrapped in double quotes. apple,pear is split into two filters.
  3. Filtering an aliased embed by its table name. If you select chef:chefs(name), filters must use chef, the alias.
  4. The column really doesn't exist, or the name is spelled differently. createdAt and created_at are different columns.

How to fix it

  1. Filter an embedded table with the referencedTable option, which applies the or to 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.

  2. Quote values that contain reserved characters:

    .or('title.eq."Apple, pear and fig tart",title.eq."Pie (classic)"')
    
  3. 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}%`)
    
  4. Nest and inside or when you need both:

    .or(`status.eq.published,and(status.eq.draft,owner_id.eq.${userId})`)
    
  5. 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 invoker by default, so RLS on both tables still applies.

  6. Check the exact column names:

    select column_name from information_schema.columns
    where table_schema = 'public' and table_name = 'recipes';
    

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us