spec :: PgVersion -> SpecWith ((), Application)
spec actualPgVersion =
  describe "and/or params used for complex boolean logic" $ do
    context "used with GET" $ do
      context "or param" $ do
        it "can do simple logic" $
          get "/entities?or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can negate simple logic" $
          get "/entities?not.or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can be combined with traditional filters" $
          get "/entities?or=(id.eq.1,id.eq.2)&name=eq.entity 1&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }

      context "embedded levels" $ do
        it "can do logic on the second level" $
          get "/entities?child_entities.or=(id.eq.1,name.eq.child entity 2)&select=id,child_entities(id)" `shouldRespondWith`
            [json|[
              {"id": 1, "child_entities": [ { "id": 1 }, { "id": 2 } ] }, { "id": 2, "child_entities": []},
              {"id": 3, "child_entities": []}, {"id": 4, "child_entities": []}
            ]|] { matchHeaders = [matchContentTypeJson] }

        it "can do logic on the third level" $
          get "/entities?child_entities.grandchild_entities.or=(id.eq.1,id.eq.2)&select=id,child_entities(id,grandchild_entities(id))"
            `shouldRespondWith`
              [json|[
                {"id": 1, "child_entities": [
                  { "id": 1, "grandchild_entities": [ { "id": 1 }, { "id": 2 } ]},
                  { "id": 2, "grandchild_entities": []},
                  { "id": 4, "grandchild_entities": []},
                  { "id": 5, "grandchild_entities": []}
                ]},
                {"id": 2, "child_entities": [
                  { "id": 3, "grandchild_entities": []},
                  { "id": 6, "grandchild_entities": []}
                ]},
                {"id": 3, "child_entities": []},
                {"id": 4, "child_entities": []}
              ]|]

      context "and/or params combined" $ do
        it "can be nested inside the same expression" $
          get "/entities?or=(and(name.eq.entity 2,id.eq.2),and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can be negated while nested" $
          get "/entities?or=(not.and(name.eq.entity 2,id.eq.2),not.and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can be combined unnested" $
          get "/entities?and=(id.eq.1,name.eq.entity 1)&or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }

      context "operators inside and/or" $ do
        it "can handle eq and neq" $
          get "/entities?and=(id.eq.1,id.neq.2))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle lt and gt" $
          get "/entities?or=(id.lt.2,id.gt.3)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle lte and gte" $
          get "/entities?or=(id.lte.2,id.gte.3)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle like and ilike" $
          get "/entities?or=(name.like.*1,name.ilike.*ENTITY 2)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle in" $
          get "/entities?or=(id.in.(1,2),id.in.(3,4))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle is" $
          get "/entities?and=(name.is.null,arr.is.null)&select=id" `shouldRespondWith`
            [json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle fts" $ do
          get "/entities?or=(text_search_vector.fts.bar,text_search_vector.fts.baz)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/tsearch?or=(text_search_vector.plfts(german).Art%20Spass, text_search_vector.plfts(french).amusant%20impossible, text_search_vector.fts(english).impossible)" `shouldRespondWith`
            [json|[
              {"text_search_vector": "'fun':5 'imposs':9 'kind':3" },
              {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
              {"text_search_vector": "'art':4 'spass':5 'unmog':7"}
            ]|] { matchHeaders = [matchContentTypeJson] }

        when (actualPgVersion >= pgVersion112) $
          it "can handle wfts (websearch_to_tsquery)" $
            get "/tsearch?or=(text_search_vector.plfts(german).Art,text_search_vector.plfts(french).amusant,text_search_vector.not.wfts(english).impossible)"
            `shouldRespondWith`
              [json|[
                     {"text_search_vector": "'also':2 'fun':3 'possibl':8" },
                     {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" },
                     {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
                     {"text_search_vector": "'art':4 'spass':5 'unmog':7" }
              ]|]
              { matchHeaders = [matchContentTypeJson] }

        it "can handle cs and cd" $
          get "/entities?or=(arr.cs.{1,2,3},arr.cd.{1})&select=id" `shouldRespondWith`
            [json|[{ "id": 1 },{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }

        it "can handle range operators" $ do
          get "/ranges?range=eq.[1,3]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=neq.[1,3]&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=lt.[1,10]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=gt.[8,11]&select=id" `shouldRespondWith`
            [json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=lte.[1,3]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=gte.[2,3]&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=cs.[1,2]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=cd.[1,6]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=ov.[0,4]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=sl.[9,10]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=sr.[3,4]&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=nxr.[4,7]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=nxl.[4,7]&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=adj.(3,10]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }

        it "can handle array operators" $ do
          get "/entities?arr=eq.{1,2,3}&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=neq.{1,2}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lt.{2,3}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lt.{2,0}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=gt.{1,1}&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=gt.{3}&select=id" `shouldRespondWith`
            [json|[]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lte.{2,1}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lte.{1,2,3}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lte.{1,2}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=cs.{1,2}&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=cd.{1,2,6}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=ov.{3}&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=ov.{2,3}&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }

        context "operators with not" $ do
          it "eq, cs, like can be negated" $
            get "/entities?and=(arr.not.cs.{1,2,3},and(id.not.eq.2,name.not.like.*3))&select=id" `shouldRespondWith`
              [json|[{ "id": 1}]|] { matchHeaders = [matchContentTypeJson] }
          it "in, is, fts can be negated" $
            get "/entities?and=(id.not.in.(1,3),and(name.not.is.null,text_search_vector.not.fts.foo))&select=id" `shouldRespondWith`
              [json|[{ "id": 2}]|] { matchHeaders = [matchContentTypeJson] }
          it "lt, gte, cd can be negated" $
            get "/entities?and=(arr.not.cd.{1},or(id.not.lt.1,id.not.gte.3))&select=id" `shouldRespondWith`
              [json|[{"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }
          it "gt, lte, ilike can be negated" $
            get "/entities?and=(name.not.ilike.*ITY2,or(id.not.gt.4,id.not.lte.1))&select=id" `shouldRespondWith`
              [json|[{"id": 1}, {"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }

      context "and/or params with quotes" $ do
        it "eq can have quotes" $
          get "/grandchild_entities?or=(name.eq.\"(grandchild,entity,4)\",name.eq.\"(grandchild,entity,5)\")&select=id" `shouldRespondWith`
            [json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }
        it "like and ilike can have quotes" $
          get "/grandchild_entities?or=(name.like.\"*ity,4*\",name.ilike.\"*ITY,5)\")&select=id" `shouldRespondWith`
            [json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }
        it "in can have quotes" $
          get "/grandchild_entities?or=(id.in.(\"1\",\"2\"),id.in.(\"3\",\"4\"))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }

      it "allows whitespace" $
        get "/entities?and=( and ( id.in.( 1, 2, 3 ) , id.eq.3 ) , or ( id.eq.2 , id.eq.3 ) )&select=id" `shouldRespondWith`
          [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }

      context "multiple and/or conditions" $ do
        it "cannot have zero conditions" $
          get "/entities?or=()" `shouldRespondWith`
            [json|{
              "details": "unexpected \")\" expecting field name (* or [a..z0..9_]), negation operator (not) or logic operator (and, or)",
              "message": "\"failed to parse logic tree (())\" (line 1, column 4)"
            }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
        it "can have a single condition" $ do
          get "/entities?or=(id.eq.1)&select=id" `shouldRespondWith`
            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?and=(id.eq.1)&select=id" `shouldRespondWith`
            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
        it "can have three conditions" $ do
          get "/grandchild_entities?or=(id.eq.1, id.eq.2, id.eq.3)&select=id" `shouldRespondWith`
            [json|[{"id":1}, {"id":2}, {"id":3}]|] { matchHeaders = [matchContentTypeJson] }
          get "/grandchild_entities?and=(id.in.(1,2), id.in.(3,1), id.in.(1,4))&select=id" `shouldRespondWith`
            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
        it "can have four conditions combining and/or" $ do
          get "/grandchild_entities?or=( id.eq.1, id.eq.2, and(id.in.(1,3), id.in.(2,3)), id.eq.4 )&select=id" `shouldRespondWith`
            [json|[{"id":1}, {"id":2}, {"id":3}, {"id":4}]|] { matchHeaders = [matchContentTypeJson] }
          get "/grandchild_entities?and=( id.eq.1, not.or(id.eq.2, id.eq.3), id.in.(1,4), or(id.eq.1, id.eq.4) )&select=id" `shouldRespondWith`
            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }

    context "used with POST" $
      it "includes related data with filters" $
        request methodPost "/child_entities?select=id,entities(id)&entities.or=(id.eq.2,id.eq.3)&entities.order=id"
            [("Prefer", "return=representation")]
            [json|[
              {"id":7,"name":"entity 4","parent_id":1},
              {"id":8,"name":"entity 5","parent_id":2},
              {"id":9,"name":"entity 6","parent_id":3}
            ]|]
          `shouldRespondWith`
            [json|[{"id": 7, "entities":null}, {"id": 8, "entities": {"id": 2}}, {"id": 9, "entities": {"id": 3}}]|]
            { matchStatus = 201 }

    context "used with PATCH" $
      it "succeeds when using and/or params" $
        request methodPatch "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
          [("Prefer", "return=representation")]
          [json|{ name : "updated grandchild entity"}|] `shouldRespondWith`
          [json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|]
          { matchHeaders = [matchContentTypeJson] }

    context "used with DELETE" $
      it "succeeds when using and/or params" $
        request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
            [("Prefer", "return=representation")]
            ""
          `shouldRespondWith`
            [json|[{ "id": 1, "name" : "grandchild entity 1" },{ "id": 2, "name" : "grandchild entity 2" }]|]

    it "can query columns that begin with and/or reserved words" $
      get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200

    it "fails when using IN without () and provides meaningful error message" $
      get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`
        [json|{
          "details": "unexpected \"1\" expecting \"(\"",
          "message": "\"failed to parse logic tree ((id.in.1,2,id.eq.3))\" (line 1, column 10)"
        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }

    it "fails on malformed query params and provides meaningful error message" $ do
      get "/entities?or=)(" `shouldRespondWith`
        [json|{
          "details": "unexpected \")\" expecting \"(\"",
          "message": "\"failed to parse logic tree ()()\" (line 1, column 3)"
        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
      get "/entities?and=(ord(id.eq.1,id.eq.1),id.eq.2)" `shouldRespondWith`
        [json|{
          "details": "unexpected \"d\" expecting \"(\"",
          "message": "\"failed to parse logic tree ((ord(id.eq.1,id.eq.1),id.eq.2))\" (line 1, column 7)"
        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
      get "/entities?or=(id.eq.1,not.xor(id.eq.2,id.eq.3))" `shouldRespondWith`
        [json|{
          "details": "unexpected \"x\" expecting logic operator (and, or)",
          "message": "\"failed to parse logic tree ((id.eq.1,not.xor(id.eq.2,id.eq.3)))\" (line 1, column 16)"
        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
spec :: PgVersion -> SpecWith ((), Application)
spec actualPgVersion =
  describe "and/or params used for complex boolean logic" $ do
    context "used with GET" $ do
      context "or param" $ do
        it "can do simple logic" $
          get "/entities?or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can negate simple logic" $
          get "/entities?not.or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can be combined with traditional filters" $
          get "/entities?or=(id.eq.1,id.eq.2)&name=eq.entity 1&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }

      context "embedded levels" $ do
        it "can do logic on the second level" $
          get "/entities?child_entities.or=(id.eq.1,name.eq.child entity 2)&select=id,child_entities(id)" `shouldRespondWith`
            [json|[
              {"id": 1, "child_entities": [ { "id": 1 }, { "id": 2 } ] }, { "id": 2, "child_entities": []},
              {"id": 3, "child_entities": []}, {"id": 4, "child_entities": []}
            ]|] { matchHeaders = [matchContentTypeJson] }

        it "can do logic on the third level" $
          get "/entities?child_entities.grandchild_entities.or=(id.eq.1,id.eq.2)&select=id,child_entities(id,grandchild_entities(id))"
            `shouldRespondWith`
              [json|[
                {"id": 1, "child_entities": [
                  { "id": 1, "grandchild_entities": [ { "id": 1 }, { "id": 2 } ]},
                  { "id": 2, "grandchild_entities": []},
                  { "id": 4, "grandchild_entities": []},
                  { "id": 5, "grandchild_entities": []}
                ]},
                {"id": 2, "child_entities": [
                  { "id": 3, "grandchild_entities": []},
                  { "id": 6, "grandchild_entities": []}
                ]},
                {"id": 3, "child_entities": []},
                {"id": 4, "child_entities": []}
              ]|]

      context "and/or params combined" $ do
        it "can be nested inside the same expression" $
          get "/entities?or=(and(name.eq.entity 2,id.eq.2),and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can be negated while nested" $
          get "/entities?or=(not.and(name.eq.entity 2,id.eq.2),not.and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can be combined unnested" $
          get "/entities?and=(id.eq.1,name.eq.entity 1)&or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }

      context "operators inside and/or" $ do
        it "can handle eq and neq" $
          get "/entities?and=(id.eq.1,id.neq.2))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle lt and gt" $
          get "/entities?or=(id.lt.2,id.gt.3)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle lte and gte" $
          get "/entities?or=(id.lte.2,id.gte.3)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle like and ilike" $
          get "/entities?or=(name.like.*1,name.ilike.*ENTITY 2)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle in" $
          get "/entities?or=(id.in.(1,2),id.in.(3,4))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle is" $
          get "/entities?and=(name.is.null,arr.is.null)&select=id" `shouldRespondWith`
            [json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle fts" $ do
          get "/entities?or=(text_search_vector.fts.bar,text_search_vector.fts.baz)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/tsearch?or=(text_search_vector.plfts(german).Art%20Spass, text_search_vector.plfts(french).amusant%20impossible, text_search_vector.fts(english).impossible)" `shouldRespondWith`
            [json|[
              {"text_search_vector": "'fun':5 'imposs':9 'kind':3" },
              {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
              {"text_search_vector": "'art':4 'spass':5 'unmog':7"}
            ]|] { matchHeaders = [matchContentTypeJson] }

        when (actualPgVersion >= pgVersion112) $
          it "can handle wfts (websearch_to_tsquery)" $
            get "/tsearch?or=(text_search_vector.plfts(german).Art,text_search_vector.plfts(french).amusant,text_search_vector.not.wfts(english).impossible)"
            `shouldRespondWith`
              [json|[
                     {"text_search_vector": "'also':2 'fun':3 'possibl':8" },
                     {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" },
                     {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
                     {"text_search_vector": "'art':4 'spass':5 'unmog':7" }
              ]|]
              { matchHeaders = [matchContentTypeJson] }

        it "can handle cs and cd" $
          get "/entities?or=(arr.cs.{1,2,3},arr.cd.{1})&select=id" `shouldRespondWith`
            [json|[{ "id": 1 },{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }

        it "can handle range operators" $ do
          get "/ranges?range=eq.[1,3]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=neq.[1,3]&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=lt.[1,10]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=gt.[8,11]&select=id" `shouldRespondWith`
            [json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=lte.[1,3]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=gte.[2,3]&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=cs.[1,2]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=cd.[1,6]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=ov.[0,4]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=sl.[9,10]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=sr.[3,4]&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=nxr.[4,7]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=nxl.[4,7]&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=adj.(3,10]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }

        it "can handle array operators" $ do
          get "/entities?arr=eq.{1,2,3}&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=neq.{1,2}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lt.{2,3}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lt.{2,0}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=gt.{1,1}&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=gt.{3}&select=id" `shouldRespondWith`
            [json|[]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lte.{2,1}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lte.{1,2,3}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lte.{1,2}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=cs.{1,2}&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=cd.{1,2,6}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=ov.{3}&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=ov.{2,3}&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }

        context "operators with not" $ do
          it "eq, cs, like can be negated" $
            get "/entities?and=(arr.not.cs.{1,2,3},and(id.not.eq.2,name.not.like.*3))&select=id" `shouldRespondWith`
              [json|[{ "id": 1}]|] { matchHeaders = [matchContentTypeJson] }
          it "in, is, fts can be negated" $
            get "/entities?and=(id.not.in.(1,3),and(name.not.is.null,text_search_vector.not.fts.foo))&select=id" `shouldRespondWith`
              [json|[{ "id": 2}]|] { matchHeaders = [matchContentTypeJson] }
          it "lt, gte, cd can be negated" $
            get "/entities?and=(arr.not.cd.{1},or(id.not.lt.1,id.not.gte.3))&select=id" `shouldRespondWith`
              [json|[{"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }
          it "gt, lte, ilike can be negated" $
            get "/entities?and=(name.not.ilike.*ITY2,or(id.not.gt.4,id.not.lte.1))&select=id" `shouldRespondWith`
              [json|[{"id": 1}, {"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }

      context "and/or params with quotes" $ do
        it "eq can have quotes" $
          get "/grandchild_entities?or=(name.eq.\"(grandchild,entity,4)\",name.eq.\"(grandchild,entity,5)\")&select=id" `shouldRespondWith`
            [json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }
        it "like and ilike can have quotes" $
          get "/grandchild_entities?or=(name.like.\"*ity,4*\",name.ilike.\"*ITY,5)\")&select=id" `shouldRespondWith`
            [json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }
        it "in can have quotes" $
          get "/grandchild_entities?or=(id.in.(\"1\",\"2\"),id.in.(\"3\",\"4\"))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }

      it "allows whitespace" $
        get "/entities?and=( and ( id.in.( 1, 2, 3 ) , id.eq.3 ) , or ( id.eq.2 , id.eq.3 ) )&select=id" `shouldRespondWith`
          [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }

      context "multiple and/or conditions" $ do
        it "cannot have zero conditions" $
          get "/entities?or=()" `shouldRespondWith`
            [json|{
              "details": "unexpected \")\" expecting field name (* or [a..z0..9_]), negation operator (not) or logic operator (and, or)",
              "message": "\"failed to parse logic tree (())\" (line 1, column 4)"
            }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
        it "can have a single condition" $ do
          get "/entities?or=(id.eq.1)&select=id" `shouldRespondWith`
            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?and=(id.eq.1)&select=id" `shouldRespondWith`
            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
        it "can have three conditions" $ do
          get "/grandchild_entities?or=(id.eq.1, id.eq.2, id.eq.3)&select=id" `shouldRespondWith`
            [json|[{"id":1}, {"id":2}, {"id":3}]|] { matchHeaders = [matchContentTypeJson] }
          get "/grandchild_entities?and=(id.in.(1,2), id.in.(3,1), id.in.(1,4))&select=id" `shouldRespondWith`
            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
        it "can have four conditions combining and/or" $ do
          get "/grandchild_entities?or=( id.eq.1, id.eq.2, and(id.in.(1,3), id.in.(2,3)), id.eq.4 )&select=id" `shouldRespondWith`
            [json|[{"id":1}, {"id":2}, {"id":3}, {"id":4}]|] { matchHeaders = [matchContentTypeJson] }
          get "/grandchild_entities?and=( id.eq.1, not.or(id.eq.2, id.eq.3), id.in.(1,4), or(id.eq.1, id.eq.4) )&select=id" `shouldRespondWith`
            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }

    context "used with POST" $
      it "includes related data with filters" $
        request methodPost "/child_entities?select=id,entities(id)&entities.or=(id.eq.2,id.eq.3)&entities.order=id"
            [("Prefer", "return=representation")]
            [json|[
              {"id":7,"name":"entity 4","parent_id":1},
              {"id":8,"name":"entity 5","parent_id":2},
              {"id":9,"name":"entity 6","parent_id":3}
            ]|]
          `shouldRespondWith`
            [json|[{"id": 7, "entities":null}, {"id": 8, "entities": {"id": 2}}, {"id": 9, "entities": {"id": 3}}]|]
            { matchStatus = 201 }

    context "used with PATCH" $
      it "succeeds when using and/or params" $
        request methodPatch "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
          [("Prefer", "return=representation")]
          [json|{ name : "updated grandchild entity"}|] `shouldRespondWith`
          [json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|]
          { matchHeaders = [matchContentTypeJson] }

    context "used with DELETE" $
      it "succeeds when using and/or params" $
        request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
            [("Prefer", "return=representation")]
            ""
          `shouldRespondWith`
            [json|[{ "id": 1, "name" : "grandchild entity 1" },{ "id": 2, "name" : "grandchild entity 2" }]|]

    it "can query columns that begin with and/or reserved words" $
      get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200

    it "fails when using IN without () and provides meaningful error message" $
      get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`
        [json|{
          "details": "unexpected \"1\" expecting \"(\"",
          "message": "\"failed to parse logic tree ((id.in.1,2,id.eq.3))\" (line 1, column 10)"
        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }

    it "fails on malformed query params and provides meaningful error message" $ do
      get "/entities?or=)(" `shouldRespondWith`
        [json|{
          "details": "unexpected \")\" expecting \"(\"",
          "message": "\"failed to parse logic tree ()()\" (line 1, column 3)"
        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
      get "/entities?and=(ord(id.eq.1,id.eq.1),id.eq.2)" `shouldRespondWith`
        [json|{
          "details": "unexpected \"d\" expecting \"(\"",
          "message": "\"failed to parse logic tree ((ord(id.eq.1,id.eq.1),id.eq.2))\" (line 1, column 7)"
        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
      get "/entities?or=(id.eq.1,not.xor(id.eq.2,id.eq.3))" `shouldRespondWith`
        [json|{
          "details": "unexpected \"x\" expecting logic operator (and, or)",
          "message": "\"failed to parse logic tree ((id.eq.1,not.xor(id.eq.2,id.eq.3)))\" (line 1, column 16)"
        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
spec actualPgVersion =
  describe "and/or params used for complex boolean logic" $ do
    context "used with GET" $ do
      context "or param" $ do
        it "can do simple logic" $
          get "/entities?or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can negate simple logic" $
          get "/entities?not.or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can be combined with traditional filters" $
          get "/entities?or=(id.eq.1,id.eq.2)&name=eq.entity 1&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }

      context "embedded levels" $ do
        it "can do logic on the second level" $
          get "/entities?child_entities.or=(id.eq.1,name.eq.child entity 2)&select=id,child_entities(id)" `shouldRespondWith`
            [json|[
              {"id": 1, "child_entities": [ { "id": 1 }, { "id": 2 } ] }, { "id": 2, "child_entities": []},
              {"id": 3, "child_entities": []}, {"id": 4, "child_entities": []}
            ]|] { matchHeaders = [matchContentTypeJson] }

        it "can do logic on the third level" $
          get "/entities?child_entities.grandchild_entities.or=(id.eq.1,id.eq.2)&select=id,child_entities(id,grandchild_entities(id))"
            `shouldRespondWith`
              [json|[
                {"id": 1, "child_entities": [
                  { "id": 1, "grandchild_entities": [ { "id": 1 }, { "id": 2 } ]},
                  { "id": 2, "grandchild_entities": []},
                  { "id": 4, "grandchild_entities": []},
                  { "id": 5, "grandchild_entities": []}
                ]},
                {"id": 2, "child_entities": [
                  { "id": 3, "grandchild_entities": []},
                  { "id": 6, "grandchild_entities": []}
                ]},
                {"id": 3, "child_entities": []},
                {"id": 4, "child_entities": []}
              ]|]

      context "and/or params combined" $ do
        it "can be nested inside the same expression" $
          get "/entities?or=(and(name.eq.entity 2,id.eq.2),and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can be negated while nested" $
          get "/entities?or=(not.and(name.eq.entity 2,id.eq.2),not.and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can be combined unnested" $
          get "/entities?and=(id.eq.1,name.eq.entity 1)&or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }

      context "operators inside and/or" $ do
        it "can handle eq and neq" $
          get "/entities?and=(id.eq.1,id.neq.2))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle lt and gt" $
          get "/entities?or=(id.lt.2,id.gt.3)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle lte and gte" $
          get "/entities?or=(id.lte.2,id.gte.3)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle like and ilike" $
          get "/entities?or=(name.like.*1,name.ilike.*ENTITY 2)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle in" $
          get "/entities?or=(id.in.(1,2),id.in.(3,4))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle is" $
          get "/entities?and=(name.is.null,arr.is.null)&select=id" `shouldRespondWith`
            [json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
        it "can handle fts" $ do
          get "/entities?or=(text_search_vector.fts.bar,text_search_vector.fts.baz)&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/tsearch?or=(text_search_vector.plfts(german).Art%20Spass, text_search_vector.plfts(french).amusant%20impossible, text_search_vector.fts(english).impossible)" `shouldRespondWith`
            [json|[
              {"text_search_vector": "'fun':5 'imposs':9 'kind':3" },
              {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
              {"text_search_vector": "'art':4 'spass':5 'unmog':7"}
            ]|] { matchHeaders = [matchContentTypeJson] }

        when (actualPgVersion >= pgVersion112) $
          it "can handle wfts (websearch_to_tsquery)" $
            get "/tsearch?or=(text_search_vector.plfts(german).Art,text_search_vector.plfts(french).amusant,text_search_vector.not.wfts(english).impossible)"
            `shouldRespondWith`
              [json|[
                     {"text_search_vector": "'also':2 'fun':3 'possibl':8" },
                     {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" },
                     {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
                     {"text_search_vector": "'art':4 'spass':5 'unmog':7" }
              ]|]
              { matchHeaders = [matchContentTypeJson] }

        it "can handle cs and cd" $
          get "/entities?or=(arr.cs.{1,2,3},arr.cd.{1})&select=id" `shouldRespondWith`
            [json|[{ "id": 1 },{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }

        it "can handle range operators" $ do
          get "/ranges?range=eq.[1,3]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=neq.[1,3]&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=lt.[1,10]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=gt.[8,11]&select=id" `shouldRespondWith`
            [json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=lte.[1,3]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=gte.[2,3]&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=cs.[1,2]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=cd.[1,6]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=ov.[0,4]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=sl.[9,10]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=sr.[3,4]&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=nxr.[4,7]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=nxl.[4,7]&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/ranges?range=adj.(3,10]&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }

        it "can handle array operators" $ do
          get "/entities?arr=eq.{1,2,3}&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=neq.{1,2}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lt.{2,3}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lt.{2,0}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=gt.{1,1}&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=gt.{3}&select=id" `shouldRespondWith`
            [json|[]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lte.{2,1}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lte.{1,2,3}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=lte.{1,2}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=cs.{1,2}&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=cd.{1,2,6}&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=ov.{3}&select=id" `shouldRespondWith`
            [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?arr=ov.{2,3}&select=id" `shouldRespondWith`
            [json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }

        context "operators with not" $ do
          it "eq, cs, like can be negated" $
            get "/entities?and=(arr.not.cs.{1,2,3},and(id.not.eq.2,name.not.like.*3))&select=id" `shouldRespondWith`
              [json|[{ "id": 1}]|] { matchHeaders = [matchContentTypeJson] }
          it "in, is, fts can be negated" $
            get "/entities?and=(id.not.in.(1,3),and(name.not.is.null,text_search_vector.not.fts.foo))&select=id" `shouldRespondWith`
              [json|[{ "id": 2}]|] { matchHeaders = [matchContentTypeJson] }
          it "lt, gte, cd can be negated" $
            get "/entities?and=(arr.not.cd.{1},or(id.not.lt.1,id.not.gte.3))&select=id" `shouldRespondWith`
              [json|[{"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }
          it "gt, lte, ilike can be negated" $
            get "/entities?and=(name.not.ilike.*ITY2,or(id.not.gt.4,id.not.lte.1))&select=id" `shouldRespondWith`
              [json|[{"id": 1}, {"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }

      context "and/or params with quotes" $ do
        it "eq can have quotes" $
          get "/grandchild_entities?or=(name.eq.\"(grandchild,entity,4)\",name.eq.\"(grandchild,entity,5)\")&select=id" `shouldRespondWith`
            [json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }
        it "like and ilike can have quotes" $
          get "/grandchild_entities?or=(name.like.\"*ity,4*\",name.ilike.\"*ITY,5)\")&select=id" `shouldRespondWith`
            [json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }
        it "in can have quotes" $
          get "/grandchild_entities?or=(id.in.(\"1\",\"2\"),id.in.(\"3\",\"4\"))&select=id" `shouldRespondWith`
            [json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }

      it "allows whitespace" $
        get "/entities?and=( and ( id.in.( 1, 2, 3 ) , id.eq.3 ) , or ( id.eq.2 , id.eq.3 ) )&select=id" `shouldRespondWith`
          [json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }

      context "multiple and/or conditions" $ do
        it "cannot have zero conditions" $
          get "/entities?or=()" `shouldRespondWith`
            [json|{
              "details": "unexpected \")\" expecting field name (* or [a..z0..9_]), negation operator (not) or logic operator (and, or)",
              "message": "\"failed to parse logic tree (())\" (line 1, column 4)"
            }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
        it "can have a single condition" $ do
          get "/entities?or=(id.eq.1)&select=id" `shouldRespondWith`
            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
          get "/entities?and=(id.eq.1)&select=id" `shouldRespondWith`
            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
        it "can have three conditions" $ do
          get "/grandchild_entities?or=(id.eq.1, id.eq.2, id.eq.3)&select=id" `shouldRespondWith`
            [json|[{"id":1}, {"id":2}, {"id":3}]|] { matchHeaders = [matchContentTypeJson] }
          get "/grandchild_entities?and=(id.in.(1,2), id.in.(3,1), id.in.(1,4))&select=id" `shouldRespondWith`
            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
        it "can have four conditions combining and/or" $ do
          get "/grandchild_entities?or=( id.eq.1, id.eq.2, and(id.in.(1,3), id.in.(2,3)), id.eq.4 )&select=id" `shouldRespondWith`
            [json|[{"id":1}, {"id":2}, {"id":3}, {"id":4}]|] { matchHeaders = [matchContentTypeJson] }
          get "/grandchild_entities?and=( id.eq.1, not.or(id.eq.2, id.eq.3), id.in.(1,4), or(id.eq.1, id.eq.4) )&select=id" `shouldRespondWith`
            [json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }

    context "used with POST" $
      it "includes related data with filters" $
        request methodPost "/child_entities?select=id,entities(id)&entities.or=(id.eq.2,id.eq.3)&entities.order=id"
            [("Prefer", "return=representation")]
            [json|[
              {"id":7,"name":"entity 4","parent_id":1},
              {"id":8,"name":"entity 5","parent_id":2},
              {"id":9,"name":"entity 6","parent_id":3}
            ]|]
          `shouldRespondWith`
            [json|[{"id": 7, "entities":null}, {"id": 8, "entities": {"id": 2}}, {"id": 9, "entities": {"id": 3}}]|]
            { matchStatus = 201 }

    context "used with PATCH" $
      it "succeeds when using and/or params" $
        request methodPatch "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
          [("Prefer", "return=representation")]
          [json|{ name : "updated grandchild entity"}|] `shouldRespondWith`
          [json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|]
          { matchHeaders = [matchContentTypeJson] }

    context "used with DELETE" $
      it "succeeds when using and/or params" $
        request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
            [("Prefer", "return=representation")]
            ""
          `shouldRespondWith`
            [json|[{ "id": 1, "name" : "grandchild entity 1" },{ "id": 2, "name" : "grandchild entity 2" }]|]

    it "can query columns that begin with and/or reserved words" $
      get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200

    it "fails when using IN without () and provides meaningful error message" $
      get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`
        [json|{
          "details": "unexpected \"1\" expecting \"(\"",
          "message": "\"failed to parse logic tree ((id.in.1,2,id.eq.3))\" (line 1, column 10)"
        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }

    it "fails on malformed query params and provides meaningful error message" $ do
      get "/entities?or=)(" `shouldRespondWith`
        [json|{
          "details": "unexpected \")\" expecting \"(\"",
          "message": "\"failed to parse logic tree ()()\" (line 1, column 3)"
        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
      get "/entities?and=(ord(id.eq.1,id.eq.1),id.eq.2)" `shouldRespondWith`
        [json|{
          "details": "unexpected \"d\" expecting \"(\"",
          "message": "\"failed to parse logic tree ((ord(id.eq.1,id.eq.1),id.eq.2))\" (line 1, column 7)"
        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
      get "/entities?or=(id.eq.1,not.xor(id.eq.2,id.eq.3))" `shouldRespondWith`
        [json|{
          "details": "unexpected \"x\" expecting logic operator (and, or)",
          "message": "\"failed to parse logic tree ((id.eq.1,not.xor(id.eq.2,id.eq.3)))\" (line 1, column 16)"
        }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }