Skip to content

Query builder queries are not immutable #1208

Open

Description

@jscheel

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

Query builder objects are not immutable, which means that you cannot share a query builder and add separate conditions on each execution.

query = (
    sb.table("items")
    .select("*")
    .eq("account_id", "abc")
)
query.in_("id", ["1", "2", "3"]).execute()
query.in_("id", ["4", "5", "6"]).execute()

This will execute one query with ids = 1, 2, 3 and then the second query will be 1, 2, 3, 4, 5, 6.

Expected behavior

One query with ids = 1, 2, 3 and another with 4, 5, 6.

Discord Discussion

https://discord.com/channels/839993398554656828/1319331194902413384

Activity

  1. self-assigned this
    on Dec 19, 2024
  2. silentworks commented on Dec 25, 2024

    @silentworks
    Contributor

    I've tested this and I'm unable to reproduce it. I get an issue of when one query runs the other doesn't run at all and not the query appending to each other.

  3. jscheel commented on Dec 25, 2024

    @jscheel
    Author

    @silentworks I'll shoot you a full, reproducible case when I get a moment. Maybe a version mismatch or something.

  4. silentworks commented on Dec 26, 2024

    @silentworks
    Contributor

    I created a reproducible with the results I'm getting. You can modify it to see if you can get the results you got and create a PR to the repo https://github.com/silentworks/postgres_py_issue_550

  5. silentworks commented on Jan 8, 2025

    @silentworks
    Contributor

    @jscheel did you get around looking at the version I created? or creating a reproducible example repo of the behavior you are getting?

  6. silentworks commented on Jan 29, 2025

    @silentworks
    Contributor

    Closing this out as no reply from OP over a month now. Also unable to reproduce the issue.

  7. added
    invalidThis doesn't seem right
    and removed
    bugSomething isn't working
    on Jan 29, 2025
  8. jscheel commented on Jan 29, 2025

    @jscheel
    Author
  9. DDoerner commented on Apr 1, 2025

    @DDoerner

    The issue is very noticable when trying to do pagination using a single base query.

    We initially built pagination like this:

    query = client.table(table_name).select("*")
    items = []
    for offset in range(0, MAX_ROWS, PAGE_SIZE):
      next_page = query.range(offset, offset + PAGE_SIZE).execute()
      if next_page.data:
        items.extend(next_page.data)
      ...
    

    But we noticed this actually attempts to repeatedly add the offset and limit parameters to the query object, producing HTTP calls such as ?select=*&offset=0&offset=250&offset=500&offset=750&offset=1000&offset=1250&offset=1500&offset=1750&offset=2000&offset=2250&limit=251&limit=251&limit=251&limit=251&limit=251&limit=251&limit=251&limit=251&limit=251&limit=251.

  10. silentworks commented on Apr 1, 2025

    @silentworks
    Contributor

    @DDoerner I've left a repo above showing that I cannot reproduce what the OP posted. Can you clone it and update it to show your issue and then I'll have a look.

  11. jscheel commented on Apr 3, 2025

    @jscheel
    Author

    @silentworks The bug still appears to exist. I've created a reproducible repo for you here: https://github.com/jscheel/supabase-query-builder-bug

    I enabled logging so that you can see the two requests, with the second one getting both conditions. Sorry I didn't fork your repo, had this example before I saw your repo 😅

  12. silentworks commented on Apr 6, 2025

    @silentworks
    Contributor

    The example your provided doesn't have any data to go with it. This is why I created the example repo as it has data and everything. I'm going to update your example to use data similar to what you originally posted.

    Ok tested this and can now replicate the issue. I will reopen this and start looking into fixing this now.

  13. 3 remaining items

  14. jscheel commented on Apr 6, 2025

    @jscheel
    Author

    If you are concerned about backwards compatibility, then probably best just to show a warning if execute is called a second time but the resolved query does not match the first request. If select is what actually resolves the query, it's possible that could be tacked on the end if someone needs to create a new query from the chain (not sure if that's actually true, not at my computer to test). Otherwise, this probably just a warning and documentation solution. I believe the documented options would be:

    1. Write new query
    2. Use a query factory (just a cleaner number 1)
    3. Use copy

    correct?

  15. silentworks commented on Apr 6, 2025

    @silentworks
    Contributor

    Yes those are the options. I am going to leave this issue open as you have made good suggestions here. I will close this when we have decided on which step to take and update you here.

  16. added
    bugSomething isn't working
    and removed
    invalidThis doesn't seem right
    on Apr 6, 2025
  17. jscheel commented on Apr 11, 2025

    @jscheel
    Author

    @silentworks just thought of one non-bc-breaking approach that could also work. The library could introduce some kind of checkpoint chaining function. This would act as a marker to reset the query chain to after an execute. It would look something like this:

    query = (
        sb.table("items")
        .select("*")
        .eq("account_id", "abc")
        .checkpoint()
    )
    query.in_("id", ["1", "2", "3"]).execute()
    query.in_("id", ["4", "5", "6"]).execute()
    

    "checkpoint" is a terrible name, and there's some edge cases for sure. Not sure if it's worth pursuing at all, but figured I'd throw out the idea all the same 😅

  18. silentworks commented on Apr 17, 2025

    @silentworks
    Contributor

    @jscheel I don't think that would be an option. This library would be further deviating away from the JS library which is something we try to keep to a minimum. This would also mean a change in the API in order to introduce something like this and I'm not just talking the external API, the internal API would have to break in order to make this change.

  19. o-santi commented on Oct 9, 2025

    @o-santi
    Contributor

    I do think there's a way to fix this without too much trouble. It would be a non backwards compatible change in the sense that the queries would no longer be mutable, but at the same time I hardly believe anyone is relying on them being mutable. I'll investigate later and see what I can find.

  20. khushee23 commented on Oct 18, 2025

    @khushee23

    i would like to contribute, how can i do so?

  21. silentworks commented on Oct 31, 2025

    @silentworks
    Contributor

    @o-santi that change would be welcome as the queries shouldn't have been mutable to begin with, so making them immutable would be fixing a long standing bug rather than breaking the library as such.

  22. dimasd-angga commented on Mar 30, 2026

    @dimasd-angga

    is this issue still open? im looking for an issue that i can contribute

  23. silentworks commented on Mar 30, 2026

    @silentworks
    Contributor

    @dimasd-angga it is still open but is set to be fixed in v3 of this library due to it being a breaking change.

  24. added
    pythonPull requests that update Python code
    on Apr 16, 2026
  25. CAN-Aidoo commented on Jul 2, 2026

    @CAN-Aidoo

    @silentworks I have a comprehensive fix ready, and ask whether you'd prefer it target main now or v3 branch?

  26. CAN-Aidoo commented on Jul 2, 2026

    @CAN-Aidoo

    Hi @silentworks — I've prepared a comprehensive fix for this that extends the _clone() approach from #1385 to cover every mutation surface, not just the four filter methods it touches.

    What's covered (that #1385 doesn't):

    • BaseSelectRequestBuilder: order, limit, offset, range
    • BaseFilterRequestBuilder: max_affected, or_, match (also fixes a latent chaining bug in match)
    • BaseRPCRequestBuilder: select, single, maybe_single, csv
    • AsyncQueryRequestBuilder: select, retry (+ retry on the three sibling builders)
    • AsyncSelectRequestBuilder: single, maybe_single, csv, text_search, explain (these return different builder types — clone the request first, don't mutate self)

    Approach: one module-level _clone_request(request) helper + a small _clone(self) on the builder classes. No decorators (learning from #1521 which was closed). All 196 unit tests pass; mypy + ruff clean.

    Since you mentioned on 2026-03-30 that this is scheduled for v3 as a breaking change — would you prefer I open the PR against main now, or wait for a v3 branch to be created? Happy to either route it as-is or gate the immutability behind an opt-in flag if that helps with the v2/v3 transition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpythonPull requests that update Python code

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions