diff --git a/CHANGELOG.md b/CHANGELOG.md index 58100995d..8afe123c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #885, Postgres COMMENTs on SCHEMA/TABLE/COLUMN are used for OpenAPI - @ldesgoui - #907, Ability to embed using a specific relation when there are multiple between tables - @ruslantalpa - #930, Split table comment on newline to get OpenAPI operation summary and description - @daurnimator +- #938, Support for range operators - @russelldavies ### Fixed @@ -22,6 +23,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #924, Improve relations initialization time - @9too - #927, Treat blank pre-request as missing - @begriffs +### Changed + +- #938, Deprecate symbol operators with mnemonic names. - @russelldavies + ## [0.4.2.0] - 2017-06-11 ### Added diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 05dbe0df4..6261557a1 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -417,7 +417,9 @@ pgFmtFilter table (Filter fld (Operation hasNot_ ex)) = notOp <> " " <> case ex (op, VText val) -> pgFmtFieldOp op <> " " <> case op of "like" -> unknownLiteral (T.map star val) "ilike" -> unknownLiteral (T.map star val) + -- TODO: The '@@' was deprecated, remove in v0.5.0.0 "@@" -> "to_tsquery(" <> unknownLiteral val <> ") " + "fts" -> "to_tsquery(" <> unknownLiteral val <> ") " "is" -> whiteList val "isnot" -> whiteList val _ -> unknownLiteral val diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 1418a3763..508bb8172 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -161,6 +161,16 @@ operators = M.fromList [ ("notin", "NOT IN"), ("isnot", "IS NOT"), ("is", "IS"), + ("fts", "@@"), + ("cs", "@>"), + ("cd", "<@"), + ("ov", "&&"), + ("sl", "<<"), + ("sr", ">>"), + ("nxr", "&<"), + ("nxl", "&>"), + ("adj", "-|-"), + -- TODO: these are deprecated and should be removed in v0.5.0.0 ("@@", "@@"), ("@>", "@>"), ("<@", "<@")] diff --git a/test/Feature/AndOrParamsSpec.hs b/test/Feature/AndOrParamsSpec.hs index 949d70731..4d91339ec 100644 --- a/test/Feature/AndOrParamsSpec.hs +++ b/test/Feature/AndOrParamsSpec.hs @@ -70,20 +70,66 @@ spec = it "can handle is" $ get "/entities?and=(name.is.null,arr.is.null)&select=id" `shouldRespondWith` [json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] } - it "can handle @@" $ + 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] } + -- TODO: remove in 0.5.0 as deprecated get "/entities?or=(text_search_vector.@@.bar,text_search_vector.@@.baz)&select=id" `shouldRespondWith` [json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] } - it "can handle @> and <@" $ + it "can handle cs and cd" $ do + get "/entities?or=(arr.cs.{1,2,3},arr.cd.{1})&select=id" `shouldRespondWith` + [json|[{ "id": 1 },{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] } + -- TODO: remove in 0.5.0 as deprecated get "/entities?or=(arr.@>.{1,2,3},arr.<@.{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] } + context "operators with not" $ do - it "eq, @>, like can be negated" $ + it "eq, cs, like can be negated" $ do + 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] } + -- TODO: remove in 0.5.0 as deprecated get "/entities?and=(arr.not.@>.{1,2,3},and(id.not.eq.2,name.not.like.*3))&select=id" `shouldRespondWith` [json|[{ "id": 1}]|] { matchHeaders = [matchContentTypeJson] } - it "in, is, @@ can be negated" $ + it "in, is, fts can be negated" $ do + 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] } + -- TODO: remove in 0.5.0 as deprecated get "/entities?and=(id.not.in.(1,3),and(name.not.is.null,text_search_vector.not.@@.foo))&select=id" `shouldRespondWith` [json|[{ "id": 2}]|] { matchHeaders = [matchContentTypeJson] } - it "lt, gte, <@ can be negated" $ + it "lt, gte, cd can be negated" $ do + 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] } + -- TODO: remove in 0.5.0 as deprecated get "/entities?and=(arr.not.<@.{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" $ diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index cb4e26343..365ce1c17 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -98,14 +98,28 @@ spec = do it "matches with ilike using not operator" $ get "/simple_pk?k=not.ilike.xy*&order=extra.asc" `shouldRespondWith` "[]" - it "matches with tsearch @@" $ - get "/tsearch?text_search_vector=@@.foo" `shouldRespondWith` - [json| [{"text_search_vector":"'bar':2 'foo':1"}] |] + it "matches with tsearch fts" $ do + get "/tsearch?text_search_vector=fts.impossible" `shouldRespondWith` + [json| [{"text_search_vector": "'fun':5 'imposs':9 'kind':3" }] |] + { matchHeaders = [matchContentTypeJson] } + get "/tsearch?text_search_vector=fts.possible" `shouldRespondWith` + [json| [{"text_search_vector": "'also':2 'fun':3 'possibl':8" }] |] + { matchHeaders = [matchContentTypeJson] } + -- TODO: remove in 0.5.0 as deprecated + get "/tsearch?text_search_vector=@@.impossible" `shouldRespondWith` + [json| [{"text_search_vector": "'fun':5 'imposs':9 'kind':3" }] |] + { matchHeaders = [matchContentTypeJson] } + get "/tsearch?text_search_vector=@@.possible" `shouldRespondWith` + [json| [{"text_search_vector": "'also':2 'fun':3 'possibl':8" }] |] { matchHeaders = [matchContentTypeJson] } - it "matches with tsearch @@ using not operator" $ - get "/tsearch?text_search_vector=not.@@.foo" `shouldRespondWith` - [json| [{"text_search_vector":"'baz':1 'qux':2"}] |] + it "matches with tsearch fts using not operator" $ do + get "/tsearch?text_search_vector=not.fts.impossible" `shouldRespondWith` + [json| [{"text_search_vector": "'also':2 'fun':3 'possibl':8" }] |] + { matchHeaders = [matchContentTypeJson] } + -- TODO: remove in 0.5.0 as deprecated + get "/tsearch?text_search_vector=not.@@.impossible" `shouldRespondWith` + [json| [{"text_search_vector": "'also':2 'fun':3 'possibl':8" }] |] { matchHeaders = [matchContentTypeJson] } it "matches with computed column" $ @@ -129,11 +143,17 @@ spec = do get "/clients?select=id,projects{id,tasks{id,name}}&projects.tasks.name=like.Design*" `shouldRespondWith` [str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1,"name":"Design w7"}]},{"id":2,"tasks":[{"id":3,"name":"Design w10"}]}]},{"id":2,"projects":[{"id":3,"tasks":[{"id":5,"name":"Design IOS"}]},{"id":4,"tasks":[{"id":7,"name":"Design OSX"}]}]}]|] - it "matches with @> operator" $ + it "matches with cs operator" $ do + get "/complex_items?select=id&arr_data=cs.{2}" `shouldRespondWith` + [str|[{"id":2},{"id":3}]|] + -- TODO: remove in 0.5.0 as deprecated get "/complex_items?select=id&arr_data=@>.{2}" `shouldRespondWith` [str|[{"id":2},{"id":3}]|] - it "matches with <@ operator" $ + it "matches with cd operator" $ do + get "/complex_items?select=id&arr_data=cd.{1,2,4}" `shouldRespondWith` + [str|[{"id":1},{"id":2}]|] + -- TODO: remove in 0.5.0 as deprecated get "/complex_items?select=id&arr_data=<@.{1,2,4}" `shouldRespondWith` [str|[{"id":1},{"id":2}]|] @@ -257,15 +277,15 @@ spec = do it "requesting children 2 levels" $ get "/clients?id=eq.1&select=id,projects{id,tasks{id}}" `shouldRespondWith` [str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|] - + it "requesting children 2 levels (with relation path fixed)" $ get "/clients?id=eq.1&select=id,projects:projects.client_id{id,tasks{id}}" `shouldRespondWith` [str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|] - + it "requesting many<->many relation" $ get "/tasks?select=id,users{id}" `shouldRespondWith` [str|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|] - + it "requesting many<->many relation (with relation path fixed)" $ get "/tasks?select=id,users:users.users_tasks{id}" `shouldRespondWith` [str|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|] diff --git a/test/fixtures/data.sql b/test/fixtures/data.sql index 77b8b9cf0..04eb72727 100644 --- a/test/fixtures/data.sql +++ b/test/fixtures/data.sql @@ -246,8 +246,8 @@ INSERT INTO nullable_integer VALUES (NULL); -- TRUNCATE TABLE tsearch CASCADE; -INSERT INTO tsearch VALUES ('''bar'':2 ''foo'':1'); -INSERT INTO tsearch VALUES ('''baz'':1 ''qux'':2'); +INSERT INTO tsearch VALUES (to_tsvector('It''s kind of fun to do the impossible')); +INSERT INTO tsearch VALUES (to_tsvector('But also fun to do what is possible')); -- @@ -315,6 +315,12 @@ INSERT INTO grandchild_entities VALUES (3, 'grandchild entity 3', 2, null, null, INSERT INTO grandchild_entities VALUES (4, '(grandchild,entity,4)', 2, null, null, '{"a": {"b":"foo"}}'); INSERT INTO grandchild_entities VALUES (5, '(grandchild,entity,5)', 2, null, null, '{"b":"bar"}'); +TRUNCATE TABLE ranges CASCADE; +INSERT INTO ranges VALUES (1, '[1,3]'); +INSERT INTO ranges VALUES (2, '[3,6]'); +INSERT INTO ranges VALUES (3, '[6,9]'); +INSERT INTO ranges VALUES (4, '[9,12]'); + -- -- PostgreSQL database dump complete -- diff --git a/test/fixtures/privileges.sql b/test/fixtures/privileges.sql index 33579ec89..f5b5fd725 100644 --- a/test/fixtures/privileges.sql +++ b/test/fixtures/privileges.sql @@ -56,6 +56,7 @@ GRANT ALL ON TABLE , entities , child_entities , grandchild_entities + , ranges TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 4313a8599..4bc71e712 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -1214,6 +1214,14 @@ create table grandchild_entities ( jsonb_col jsonb ); +-- Table used for testing range operators + +create table ranges ( + id integer primary key, + range numrange +); + + -- OpenAPI description tests comment on table child_entities is 'child_entities comment';