feat: apply to_tsvector() explicitly to the full-text search filtered column, only if it's not of tsvector type
This commit is contained in:
@@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #3727, Log maximum pool size - @steve-chavez
|
- #3727, Log maximum pool size - @steve-chavez
|
||||||
- #1536, Add string comparison feature for jwt-role-claim-key - @taimoorzaeem
|
- #1536, Add string comparison feature for jwt-role-claim-key - @taimoorzaeem
|
||||||
- #3747, Allow `not_null` value for the `is` operator - @taimoorzaeem
|
- #3747, Allow `not_null` value for the `is` operator - @taimoorzaeem
|
||||||
|
- #2255, Apply `to_tsvector()` explicitly to the full-text search filtered column (excluding `tsvector` types) - @laurenceisla
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -193,7 +193,21 @@ The :code:`fts` filter mentioned above has a number of options to support flexib
|
|||||||
|
|
||||||
curl "http://localhost:3000/tsearch?my_tsv=not.wfts(french).amusant"
|
curl "http://localhost:3000/tsearch?my_tsv=not.wfts(french).amusant"
|
||||||
|
|
||||||
Using `websearch_to_tsquery` requires PostgreSQL of version at least 11.0 and will raise an error in earlier versions of the database.
|
.. _fts_to_tsvector:
|
||||||
|
|
||||||
|
Automatic ``tsvector`` conversion
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
If the filtered column is not of type ``tsvector``, then it will be automatically converted using `to_tsvector() <https://www.postgresql.org/docs/current/functions-textsearch.html#TEXTSEARCH-FUNCTIONS-TABLE>`_.
|
||||||
|
This allows using ``fts`` on ``text`` and ``json`` types out of the box, for example.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl "http://localhost:3000/tsearch?my_text_column=fts(french).amusant"
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl "http://localhost:3000/tsearch?my_json_column=not.phfts(english).The%20Fat%20Cats"
|
||||||
|
|
||||||
.. _v_filter:
|
.. _v_filter:
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ module PostgREST.ApiRequest.Types
|
|||||||
, JsonOperand(..)
|
, JsonOperand(..)
|
||||||
, JsonOperation(..)
|
, JsonOperation(..)
|
||||||
, JsonPath
|
, JsonPath
|
||||||
|
, Language
|
||||||
, ListVal
|
, ListVal
|
||||||
, LogicOperator(..)
|
, LogicOperator(..)
|
||||||
, LogicTree(..)
|
, LogicTree(..)
|
||||||
|
|||||||
+24
-18
@@ -265,27 +265,29 @@ data ResolverContext = ResolverContext
|
|||||||
, outputType :: Text -- ^ The output type for the response payload; e.g. "csv", "json", "binary".
|
, outputType :: Text -- ^ The output type for the response payload; e.g. "csv", "json", "binary".
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveColumnField :: Column -> CoercibleField
|
resolveColumnField :: Column -> Maybe ToTsVector -> CoercibleField
|
||||||
resolveColumnField col = CoercibleField (colName col) mempty False (colNominalType col) Nothing (colDefault col) False
|
resolveColumnField col toTsV = CoercibleField (colName col) mempty False toTsV (colNominalType col) Nothing (colDefault col) False
|
||||||
|
|
||||||
resolveTableFieldName :: Table -> FieldName -> CoercibleField
|
resolveTableFieldName :: Table -> FieldName -> Maybe ToTsVector -> CoercibleField
|
||||||
resolveTableFieldName table fieldName =
|
resolveTableFieldName table fieldName toTsV=
|
||||||
fromMaybe (unknownField fieldName []) $ HMI.lookup fieldName (tableColumns table) >>=
|
fromMaybe (unknownField fieldName []) $ HMI.lookup fieldName (tableColumns table) >>=
|
||||||
Just . resolveColumnField
|
Just . flip resolveColumnField toTsV
|
||||||
|
|
||||||
-- | Resolve a type within the context based on the given field name and JSON path. Although there are situations where failure to resolve a field is considered an error (see `resolveOrError`), there are also situations where we allow it (RPC calls). If it should be an error and `resolveOrError` doesn't fit, ensure to check the `cfIRType` isn't empty.
|
-- | Resolve a type within the context based on the given field name and JSON path. Although there are situations where failure to resolve a field is considered an error (see `resolveOrError`), there are also situations where we allow it (RPC calls). If it should be an error and `resolveOrError` doesn't fit, ensure to check the `cfIRType` isn't empty.
|
||||||
resolveTypeOrUnknown :: ResolverContext -> Field -> CoercibleField
|
resolveTypeOrUnknown :: ResolverContext -> Field -> Maybe ToTsVector -> CoercibleField
|
||||||
resolveTypeOrUnknown ResolverContext{..} (fn, jp) =
|
resolveTypeOrUnknown ResolverContext{..} (fn, jp) toTsV =
|
||||||
case res of
|
case res of
|
||||||
-- types that are already json/jsonb don't need to be converted with `to_jsonb` for using arrow operators `data->attr`
|
-- types that are already json/jsonb don't need to be converted with `to_jsonb` for using arrow operators `data->attr`
|
||||||
-- this prevents indexes not applying https://github.com/PostgREST/postgrest/issues/2594
|
-- this prevents indexes not applying https://github.com/PostgREST/postgrest/issues/2594
|
||||||
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False}
|
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False}
|
||||||
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False}
|
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False}
|
||||||
|
-- Do not apply to_tsvector to tsvector types
|
||||||
|
cf@CoercibleField{cfIRType="tsvector"} -> cf{cfJsonPath=jp, cfToJson=True, cfToTsVector=Nothing}
|
||||||
-- other types will get converted `to_jsonb(col)->attr`, even unknown types
|
-- other types will get converted `to_jsonb(col)->attr`, even unknown types
|
||||||
cf -> cf{cfJsonPath=jp, cfToJson=True}
|
cf -> cf{cfJsonPath=jp, cfToJson=True}
|
||||||
where
|
where
|
||||||
res = fromMaybe (unknownField fn jp) $ HM.lookup qi tables >>=
|
res = fromMaybe (unknownField fn jp) $ HM.lookup qi tables >>=
|
||||||
Just . flip resolveTableFieldName fn
|
Just . (\t -> resolveTableFieldName t fn toTsV)
|
||||||
|
|
||||||
-- | Install any pre-defined data representation from source to target to coerce this reference.
|
-- | Install any pre-defined data representation from source to target to coerce this reference.
|
||||||
--
|
--
|
||||||
@@ -311,11 +313,15 @@ withJsonParse ctx field@CoercibleField{cfIRType} = withTransformer ctx "json" cf
|
|||||||
|
|
||||||
-- | Map the intermediate representation type to the output type defined by the resolver context (normally json), if available.
|
-- | Map the intermediate representation type to the output type defined by the resolver context (normally json), if available.
|
||||||
resolveOutputField :: ResolverContext -> Field -> CoercibleField
|
resolveOutputField :: ResolverContext -> Field -> CoercibleField
|
||||||
resolveOutputField ctx field = withOutputFormat ctx $ resolveTypeOrUnknown ctx field
|
resolveOutputField ctx field = withOutputFormat ctx $ resolveTypeOrUnknown ctx field Nothing
|
||||||
|
|
||||||
-- | Map the query string format of a value (text) into the intermediate representation type, if available.
|
-- | Map the query string format of a value (text) into the intermediate representation type, if available.
|
||||||
resolveQueryInputField :: ResolverContext -> Field -> CoercibleField
|
resolveQueryInputField :: ResolverContext -> Field -> OpExpr -> CoercibleField
|
||||||
resolveQueryInputField ctx field = withTextParse ctx $ resolveTypeOrUnknown ctx field
|
resolveQueryInputField ctx field opExpr = withTextParse ctx $ resolveTypeOrUnknown ctx field toTsVector
|
||||||
|
where
|
||||||
|
toTsVector = case opExpr of
|
||||||
|
OpExpr _ (Fts _ lang _) -> Just $ ToTsVector lang
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
-- | Builds the ReadPlan tree on a number of stages.
|
-- | Builds the ReadPlan tree on a number of stages.
|
||||||
-- | Adds filters, order, limits on its respective nodes.
|
-- | Adds filters, order, limits on its respective nodes.
|
||||||
@@ -452,7 +458,7 @@ expandStarsForTable ctx@ResolverContext{representations, outputType} hasAgg rp@R
|
|||||||
|
|
||||||
expandStarSelectField :: Bool -> [Column] -> CoercibleSelectField -> [CoercibleSelectField]
|
expandStarSelectField :: Bool -> [Column] -> CoercibleSelectField -> [CoercibleSelectField]
|
||||||
expandStarSelectField _ columns sel@CoercibleSelectField{csField=CoercibleField{cfName="*", cfJsonPath=[]}, csAggFunction=Nothing} =
|
expandStarSelectField _ columns sel@CoercibleSelectField{csField=CoercibleField{cfName="*", cfJsonPath=[]}, csAggFunction=Nothing} =
|
||||||
map (\col -> sel { csField = withOutputFormat ctx $ resolveColumnField col }) columns
|
map (\col -> sel { csField = withOutputFormat ctx $ resolveColumnField col Nothing }) columns
|
||||||
expandStarSelectField True _ sel@CoercibleSelectField{csField=fld@CoercibleField{cfName="*", cfJsonPath=[]}, csAggFunction=Just Count} =
|
expandStarSelectField True _ sel@CoercibleSelectField{csField=fld@CoercibleField{cfName="*", cfJsonPath=[]}, csAggFunction=Just Count} =
|
||||||
[sel { csField = fld { cfFullRow = True } }]
|
[sel { csField = fld { cfFullRow = True } }]
|
||||||
expandStarSelectField _ _ selectField = [selectField]
|
expandStarSelectField _ _ selectField = [selectField]
|
||||||
@@ -766,7 +772,7 @@ addOrders ctx ApiRequest{..} rReq =
|
|||||||
|
|
||||||
resolveOrder :: ResolverContext -> OrderTerm -> CoercibleOrderTerm
|
resolveOrder :: ResolverContext -> OrderTerm -> CoercibleOrderTerm
|
||||||
resolveOrder _ (OrderRelationTerm a b c d) = CoercibleOrderRelationTerm a b c d
|
resolveOrder _ (OrderRelationTerm a b c d) = CoercibleOrderRelationTerm a b c d
|
||||||
resolveOrder ctx (OrderTerm fld dir nulls) = CoercibleOrderTerm (resolveTypeOrUnknown ctx fld) dir nulls
|
resolveOrder ctx (OrderTerm fld dir nulls) = CoercibleOrderTerm (resolveTypeOrUnknown ctx fld Nothing) dir nulls
|
||||||
|
|
||||||
-- Validates that the related resource on the order is an embedded resource,
|
-- Validates that the related resource on the order is an embedded resource,
|
||||||
-- e.g. if `clients` is inside the `select` in /projects?order=clients(id)&select=*,clients(*),
|
-- e.g. if `clients` is inside the `select` in /projects?order=clients(id)&select=*,clients(*),
|
||||||
@@ -831,7 +837,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
-- where_ = [
|
-- where_ = [
|
||||||
-- CoercibleStmnt (
|
-- CoercibleStmnt (
|
||||||
-- CoercibleFilter {
|
-- CoercibleFilter {
|
||||||
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False},
|
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfToTsVector = Nothing, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False},
|
||||||
-- opExpr = op
|
-- opExpr = op
|
||||||
-- }
|
-- }
|
||||||
-- )
|
-- )
|
||||||
@@ -847,7 +853,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
|
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
|
||||||
--
|
--
|
||||||
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
|
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
|
||||||
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is IsNull)})]
|
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfToTsVector = Nothing, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is IsNull)})]
|
||||||
--
|
--
|
||||||
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
|
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
|
||||||
--
|
--
|
||||||
@@ -866,7 +872,7 @@ addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
|
|||||||
newNullFilters rPlans = \case
|
newNullFilters rPlans = \case
|
||||||
(CoercibleExpr b lOp trees) ->
|
(CoercibleExpr b lOp trees) ->
|
||||||
CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees)
|
CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees)
|
||||||
flt@(CoercibleStmnt (CoercibleFilter (CoercibleField fld [] _ _ _ _ _) opExpr)) ->
|
flt@(CoercibleStmnt (CoercibleFilter (CoercibleField fld [] _ _ _ _ _ _) opExpr)) ->
|
||||||
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
|
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
|
||||||
case (foundRP, opExpr) of
|
case (foundRP, opExpr) of
|
||||||
(Just ReadPlan{relAggAlias}, OpExpr b (Is IsNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
|
(Just ReadPlan{relAggAlias}, OpExpr b (Is IsNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
|
||||||
@@ -900,7 +906,7 @@ resolveLogicTree ctx (Stmnt flt) = CoercibleStmnt $ resolveFilter ctx flt
|
|||||||
resolveLogicTree ctx (Expr b op lts) = CoercibleExpr b op (map (resolveLogicTree ctx) lts)
|
resolveLogicTree ctx (Expr b op lts) = CoercibleExpr b op (map (resolveLogicTree ctx) lts)
|
||||||
|
|
||||||
resolveFilter :: ResolverContext -> Filter -> CoercibleFilter
|
resolveFilter :: ResolverContext -> Filter -> CoercibleFilter
|
||||||
resolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld, opExpr=opExpr}
|
resolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld opExpr, opExpr=opExpr}
|
||||||
|
|
||||||
-- Validates that spread embeds are only done on to-one relationships
|
-- Validates that spread embeds are only done on to-one relationships
|
||||||
validateSpreadEmbeds :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
validateSpreadEmbeds :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
@@ -963,7 +969,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
|
|||||||
resolveOrError :: ResolverContext -> Maybe Table -> FieldName -> Either ApiRequestError CoercibleField
|
resolveOrError :: ResolverContext -> Maybe Table -> FieldName -> Either ApiRequestError CoercibleField
|
||||||
resolveOrError _ Nothing _ = Left NotFound
|
resolveOrError _ Nothing _ = Left NotFound
|
||||||
resolveOrError ctx (Just table) field =
|
resolveOrError ctx (Just table) field =
|
||||||
case resolveTableFieldName table field of
|
case resolveTableFieldName table field Nothing of
|
||||||
CoercibleField{cfIRType=""} -> Left $ ColumnNotFound (tableName table) field
|
CoercibleField{cfIRType=""} -> Left $ ColumnNotFound (tableName table) field
|
||||||
cf -> Right $ withJsonParse ctx cf
|
cf -> Right $ withJsonParse ctx cf
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ module PostgREST.Plan.Types
|
|||||||
, CoercibleLogicTree(..)
|
, CoercibleLogicTree(..)
|
||||||
, CoercibleFilter(..)
|
, CoercibleFilter(..)
|
||||||
, TransformerProc
|
, TransformerProc
|
||||||
|
, ToTsVector(..)
|
||||||
, CoercibleOrderTerm(..)
|
, CoercibleOrderTerm(..)
|
||||||
, RelSelectField(..)
|
, RelSelectField(..)
|
||||||
, RelJsonEmbedMode(..)
|
, RelJsonEmbedMode(..)
|
||||||
@@ -12,8 +13,9 @@ module PostgREST.Plan.Types
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Types (AggregateFunction, Alias, Cast,
|
import PostgREST.ApiRequest.Types (AggregateFunction, Alias, Cast,
|
||||||
Field, JsonPath, LogicOperator,
|
Field, JsonPath, Language,
|
||||||
OpExpr, OrderDirection, OrderNulls)
|
LogicOperator, OpExpr,
|
||||||
|
OrderDirection, OrderNulls)
|
||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName)
|
import PostgREST.SchemaCache.Identifiers (FieldName)
|
||||||
|
|
||||||
@@ -21,6 +23,9 @@ import Protolude
|
|||||||
|
|
||||||
type TransformerProc = Text
|
type TransformerProc = Text
|
||||||
|
|
||||||
|
newtype ToTsVector = ToTsVector (Maybe Language)
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
-- | A CoercibleField pairs the name of a query element with any type coercion information we need for some specific use case.
|
-- | A CoercibleField pairs the name of a query element with any type coercion information we need for some specific use case.
|
||||||
-- |
|
-- |
|
||||||
-- | As suggested by the name, it's often a reference to a field in a table but really it can be any nameable element (function parameter, calculation with an alias, etc) with a knowable type.
|
-- | As suggested by the name, it's often a reference to a field in a table but really it can be any nameable element (function parameter, calculation with an alias, etc) with a knowable type.
|
||||||
@@ -36,6 +41,7 @@ data CoercibleField = CoercibleField
|
|||||||
{ cfName :: FieldName
|
{ cfName :: FieldName
|
||||||
, cfJsonPath :: JsonPath
|
, cfJsonPath :: JsonPath
|
||||||
, cfToJson :: Bool
|
, cfToJson :: Bool
|
||||||
|
, cfToTsVector :: Maybe ToTsVector -- ^ If the field should be converted using to_tsvector(<language>, <field>)
|
||||||
, cfIRType :: Text -- ^ The native Postgres type of the field, the intermediate (IR) type before mapping.
|
, cfIRType :: Text -- ^ The native Postgres type of the field, the intermediate (IR) type before mapping.
|
||||||
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
||||||
, cfDefault :: Maybe Text
|
, cfDefault :: Maybe Text
|
||||||
@@ -43,7 +49,7 @@ data CoercibleField = CoercibleField
|
|||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
unknownField :: FieldName -> JsonPath -> CoercibleField
|
unknownField :: FieldName -> JsonPath -> CoercibleField
|
||||||
unknownField name path = CoercibleField name path False "" Nothing Nothing False
|
unknownField name path = CoercibleField name path False Nothing "" Nothing Nothing False
|
||||||
|
|
||||||
-- | Like an API request LogicTree, but with coercible field information.
|
-- | Like an API request LogicTree, but with coercible field information.
|
||||||
data CoercibleLogicTree
|
data CoercibleLogicTree
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScal
|
|||||||
KeyParams [] -> "FROM " <> callIt mempty
|
KeyParams [] -> "FROM " <> callIt mempty
|
||||||
KeyParams prms -> case arguments of
|
KeyParams prms -> case arguments of
|
||||||
DirectArgs args -> "FROM " <> callIt (fmtArgs prms args)
|
DirectArgs args -> "FROM " <> callIt (fmtArgs prms args)
|
||||||
JsonArgs json -> fromJsonBodyF json ((\p -> CoercibleField (ppName p) mempty False (ppTypeMaxLength p) Nothing Nothing False) <$> prms) False True False <> ", " <>
|
JsonArgs json -> fromJsonBodyF json ((\p -> CoercibleField (ppName p) mempty False Nothing (ppTypeMaxLength p) Nothing Nothing False) <$> prms) False True False <> ", " <>
|
||||||
"LATERAL " <> callIt (fmtParams prms)
|
"LATERAL " <> callIt (fmtParams prms)
|
||||||
|
|
||||||
callIt :: SQL.Snippet -> SQL.Snippet
|
callIt :: SQL.Snippet -> SQL.Snippet
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ import PostgREST.Plan.Types (CoercibleField (..),
|
|||||||
CoercibleSelectField (..),
|
CoercibleSelectField (..),
|
||||||
RelSelectField (..),
|
RelSelectField (..),
|
||||||
SpreadSelectField (..),
|
SpreadSelectField (..),
|
||||||
|
ToTsVector (..),
|
||||||
unknownField)
|
unknownField)
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
rangeLimit, rangeOffset)
|
rangeLimit, rangeOffset)
|
||||||
@@ -252,10 +253,15 @@ pgFmtCallUnary :: Text -> SQL.Snippet -> SQL.Snippet
|
|||||||
pgFmtCallUnary f x = SQL.sql (encodeUtf8 f) <> "(" <> x <> ")"
|
pgFmtCallUnary f x = SQL.sql (encodeUtf8 f) <> "(" <> x <> ")"
|
||||||
|
|
||||||
pgFmtField :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
|
pgFmtField :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
|
||||||
pgFmtField table CoercibleField{cfFullRow=True} = fromQi table
|
pgFmtField table cf = case cfToTsVector cf of
|
||||||
pgFmtField table CoercibleField{cfName=fn, cfJsonPath=[]} = pgFmtColumn table fn
|
Just (ToTsVector lang) -> "to_tsvector(" <> pgFmtFtsLang lang <> fmtFld <> ")"
|
||||||
pgFmtField table CoercibleField{cfName=fn, cfToJson=doToJson, cfJsonPath=jp} | doToJson = "to_jsonb(" <> pgFmtColumn table fn <> ")" <> pgFmtJsonPath jp
|
_ -> fmtFld
|
||||||
| otherwise = pgFmtColumn table fn <> pgFmtJsonPath jp
|
where
|
||||||
|
fmtFld = case cf of
|
||||||
|
CoercibleField{cfFullRow=True} -> fromQi table
|
||||||
|
CoercibleField{cfName=fn, cfJsonPath=[]} -> pgFmtColumn table fn
|
||||||
|
CoercibleField{cfName=fn, cfToJson=doToJson, cfJsonPath=jp} | doToJson -> "to_jsonb(" <> pgFmtColumn table fn <> ")" <> pgFmtJsonPath jp
|
||||||
|
| otherwise -> pgFmtColumn table fn <> pgFmtJsonPath jp
|
||||||
|
|
||||||
-- Select the value of a named element from a table, applying its optional coercion mapping if any.
|
-- Select the value of a named element from a table, applying its optional coercion mapping if any.
|
||||||
pgFmtTableCoerce :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
|
pgFmtTableCoerce :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
|
||||||
@@ -399,9 +405,8 @@ pgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> " " <> p
|
|||||||
[""] -> "= ANY('{}') "
|
[""] -> "= ANY('{}') "
|
||||||
_ -> "= ANY (" <> pgFmtArrayLiteralForField vals fld <> ") "
|
_ -> "= ANY (" <> pgFmtArrayLiteralForField vals fld <> ") "
|
||||||
|
|
||||||
Fts op lang val -> " " <> ftsOperator op <> "(" <> ftsLang lang <> unknownLiteral val <> ") "
|
Fts op lang val -> " " <> ftsOperator op <> "(" <> pgFmtFtsLang lang <> unknownLiteral val <> ") "
|
||||||
where
|
where
|
||||||
ftsLang = maybe mempty (\l -> unknownLiteral l <> ", ")
|
|
||||||
notOp = if hasNot then "NOT" else mempty
|
notOp = if hasNot then "NOT" else mempty
|
||||||
star c = if c == '*' then '%' else c
|
star c = if c == '*' then '%' else c
|
||||||
fmtQuant q val = case q of
|
fmtQuant q val = case q of
|
||||||
@@ -409,6 +414,9 @@ pgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> " " <> p
|
|||||||
Just QuantAll -> "ALL(" <> val <> ")"
|
Just QuantAll -> "ALL(" <> val <> ")"
|
||||||
Nothing -> val
|
Nothing -> val
|
||||||
|
|
||||||
|
pgFmtFtsLang :: Maybe Text -> SQL.Snippet
|
||||||
|
pgFmtFtsLang = maybe mempty (\l -> unknownLiteral l <> ", ")
|
||||||
|
|
||||||
pgFmtJoinCondition :: JoinCondition -> SQL.Snippet
|
pgFmtJoinCondition :: JoinCondition -> SQL.Snippet
|
||||||
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
|
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
|
||||||
pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2
|
pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ spec =
|
|||||||
it "can handle is" $
|
it "can handle is" $
|
||||||
get "/entities?and=(name.is.null,arr.is.null)&select=id" `shouldRespondWith`
|
get "/entities?and=(name.is.null,arr.is.null)&select=id" `shouldRespondWith`
|
||||||
[json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
[json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
it "can handle fts" $ do
|
it "can handle fts on tsvector columns" $ do
|
||||||
get "/entities?or=(text_search_vector.fts.bar,text_search_vector.fts.baz)&select=id" `shouldRespondWith`
|
get "/entities?or=(text_search_vector.fts.bar,text_search_vector.fts.baz)&select=id" `shouldRespondWith`
|
||||||
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
|
[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`
|
get "/tsearch?or=(text_search_vector.plfts(german).Art%20Spass, text_search_vector.plfts(french).amusant%20impossible, text_search_vector.fts(english).impossible)" `shouldRespondWith`
|
||||||
@@ -90,6 +90,17 @@ spec =
|
|||||||
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
|
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
|
||||||
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}
|
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}
|
||||||
]|] { matchHeaders = [matchContentTypeJson] }
|
]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can handle fts on text and json columns" $ do
|
||||||
|
get "/grandchild_entities?or=(jsonb_col.fts.bar,jsonb_col.fts.foo)&select=jsonb_col" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{ "jsonb_col": {"a": {"b":"foo"}} },
|
||||||
|
{ "jsonb_col": {"b":"bar"} }]
|
||||||
|
|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?and=(text_search.not.plfts(german).Art%20Spass, text_search.not.plfts(french).amusant%20impossible, text_search.not.fts(english).impossible)&select=text_search" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{ "text_search": "But also fun to do what is possible" },
|
||||||
|
{ "text_search": "Fat cats ate rats" }]
|
||||||
|
|] { matchHeaders = [matchContentTypeJson] }
|
||||||
it "can handle isdistinct" $
|
it "can handle isdistinct" $
|
||||||
get "/entities?and=(id.gte.2,arr.isdistinct.{1,2})&select=id" `shouldRespondWith`
|
get "/entities?and=(id.gte.2,arr.isdistinct.{1,2})&select=id" `shouldRespondWith`
|
||||||
[json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
[json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ spec = do
|
|||||||
get "/simple_pk?k=not.imatch.^xy&order=extra.asc" `shouldRespondWith` "[]"
|
get "/simple_pk?k=not.imatch.^xy&order=extra.asc" `shouldRespondWith` "[]"
|
||||||
|
|
||||||
describe "Full text search operator" $ do
|
describe "Full text search operator" $ do
|
||||||
|
context "tsvector columns" $ do
|
||||||
it "finds matches with to_tsquery" $
|
it "finds matches with to_tsquery" $
|
||||||
get "/tsearch?text_search_vector=fts.impossible" `shouldRespondWith`
|
get "/tsearch?text_search_vector=fts.impossible" `shouldRespondWith`
|
||||||
[json| [{"text_search_vector": "'fun':5 'imposs':9 'kind':3" }] |]
|
[json| [{"text_search_vector": "'fun':5 'imposs':9 'kind':3" }] |]
|
||||||
@@ -278,6 +279,207 @@ spec = do
|
|||||||
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}
|
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}
|
||||||
]|] { matchHeaders = [matchContentTypeJson] }
|
]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "works with tsvector computed fields" $
|
||||||
|
get "/tsearch_to_tsvector?select=text_search_vector&text_search_vector=fts(simple).of" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"text_search_vector":"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6"}
|
||||||
|
]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "text and json columns" $ do
|
||||||
|
it "finds matches with to_tsquery" $ do
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=fts.impossible" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"text_search": "It's kind of fun to do the impossible"},
|
||||||
|
{"text_search": "C'est un peu amusant de faire l'impossible"}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=fts.impossible" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"jsonb_search" :{"text_search": "It's kind of fun to do the impossible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "C'est un peu amusant de faire l'impossible"}}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can use lexeme boolean operators(&=%26, |=%7C, !) in to_tsquery" $ do
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=fts.fun%26possible" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "But also fun to do what is possible"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=fts.fun%26possible" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "But also fun to do what is possible"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=fts.impossible%7Cpossible" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"text_search": "It's kind of fun to do the impossible"},
|
||||||
|
{"text_search": "But also fun to do what is possible"},
|
||||||
|
{"text_search": "C'est un peu amusant de faire l'impossible"}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=fts.impossible%7Cpossible" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"jsonb_search" :{"text_search": "It's kind of fun to do the impossible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "But also fun to do what is possible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "C'est un peu amusant de faire l'impossible"}}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=fts.fun%26!possible" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "It's kind of fun to do the impossible"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=fts.fun%26!possible" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "It's kind of fun to do the impossible"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "finds matches with plainto_tsquery" $ do
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=plfts.The%20Fat%20Rats" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "Fat cats ate rats"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=plfts.The%20Fat%20Rats" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "Fat cats ate rats"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "finds matches with websearch_to_tsquery" $ do
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=wfts.The%20Fat%20Rats" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "Fat cats ate rats"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=wfts.The%20Fat%20Rats" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "Fat cats ate rats"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can use boolean operators(and, or, -) in websearch_to_tsquery" $ do
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=wfts.fun%20and%20possible" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "But also fun to do what is possible"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=wfts.fun%20and%20possible" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "But also fun to do what is possible"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=wfts.impossible%20or%20possible" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"text_search": "It's kind of fun to do the impossible"},
|
||||||
|
{"text_search": "But also fun to do what is possible"},
|
||||||
|
{"text_search": "C'est un peu amusant de faire l'impossible"}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=wfts.impossible%20or%20possible" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"jsonb_search" :{"text_search": "It's kind of fun to do the impossible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "But also fun to do what is possible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "C'est un peu amusant de faire l'impossible"}}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=wfts.fun%20and%20-possible" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "It's kind of fun to do the impossible"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=wfts.fun%20and%20-possible" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "It's kind of fun to do the impossible"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "finds matches with different dictionaries and uses them as configuration for to_tsvector()" $ do
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=fts(french).amusant" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "C'est un peu amusant de faire l'impossible"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=fts(french).amusant" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "C'est un peu amusant de faire l'impossible"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=plfts(french).amusant%20impossible" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "C'est un peu amusant de faire l'impossible"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=plfts(french).amusant%20impossible" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "C'est un peu amusant de faire l'impossible"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=wfts(french).amusant%20impossible" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "C'est un peu amusant de faire l'impossible"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=wfts(french).amusant%20impossible" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "C'est un peu amusant de faire l'impossible"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can be negated with not operator" $ do
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=not.fts.impossible%7Cfat%7Cfun" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=not.fts.impossible%7Cfat%7Cfun" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=not.fts(english).impossible%7Cfat%7Cfun" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=not.fts(english).impossible%7Cfat%7Cfun" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=not.plfts.The%20Fat%20Rats" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"text_search": "It's kind of fun to do the impossible"},
|
||||||
|
{"text_search": "But also fun to do what is possible"},
|
||||||
|
{"text_search": "C'est un peu amusant de faire l'impossible"},
|
||||||
|
{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=not.plfts.The%20Fat%20Rats" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"jsonb_search" :{"text_search": "It's kind of fun to do the impossible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "But also fun to do what is possible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "C'est un peu amusant de faire l'impossible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=not.wfts(english).impossible%20or%20fat%20or%20fun" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=not.wfts(english).impossible%20or%20fat%20or%20fun" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "Use of the phraseto_tsquery function" $ do
|
||||||
|
it "finds matches" $ do
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=phfts.The%20Fat%20Cats" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "Fat cats ate rats"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=phfts.The%20Fat%20Cats" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "Fat cats ate rats"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "finds matches with different dictionaries and uses them as configuration for to_tsvector()" $ do
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=phfts(german).Art%20Spass" `shouldRespondWith`
|
||||||
|
[json| [{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=phfts(german).Art%20Spass" `shouldRespondWith`
|
||||||
|
[json| [{"jsonb_search" :{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can be negated with not operator" $ do
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&text_search=not.phfts(english).The%20Fat%20Cats" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"text_search": "It's kind of fun to do the impossible"},
|
||||||
|
{"text_search": "But also fun to do what is possible"},
|
||||||
|
{"text_search": "C'est un peu amusant de faire l'impossible"},
|
||||||
|
{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&jsonb_search=not.phfts(english).The%20Fat%20Cats" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"jsonb_search" :{"text_search": "It's kind of fun to do the impossible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "But also fun to do what is possible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "C'est un peu amusant de faire l'impossible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can be used with or query param" $ do
|
||||||
|
get "/tsearch_to_tsvector?select=text_search&or=(text_search.phfts(german).Art%20Spass, text_search.phfts(french).amusant, text_search.fts(english).impossible)" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"text_search": "It's kind of fun to do the impossible"},
|
||||||
|
{"text_search": "C'est un peu amusant de faire l'impossible"},
|
||||||
|
{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tsearch_to_tsvector?select=jsonb_search&or=(jsonb_search.phfts(german).Art%20Spass, jsonb_search.phfts(french).amusant, jsonb_search.fts(english).impossible)" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"jsonb_search" :{"text_search": "It's kind of fun to do the impossible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "C'est un peu amusant de faire l'impossible"}},
|
||||||
|
{"jsonb_search" :{"text_search": "Es ist eine Art Spaß, das Unmögliche zu machen"}}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "matches with computed column" $
|
it "matches with computed column" $
|
||||||
get "/items?always_true=eq.true&order=id.asc" `shouldRespondWith`
|
get "/items?always_true=eq.true&order=id.asc" `shouldRespondWith`
|
||||||
[json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
|
[json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
|
||||||
|
|||||||
@@ -949,7 +949,7 @@ spec =
|
|||||||
[json|[{ "id": 2 }, { "id": 4 }]|]
|
[json|[{ "id": 2 }, { "id": 4 }]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "should work with filters that use the plain with language fts operator" $ do
|
it "should work with filters that use the fts operator on tsvector columns" $ do
|
||||||
get "/rpc/get_tsearch?text_search_vector=fts(english).impossible" `shouldRespondWith`
|
get "/rpc/get_tsearch?text_search_vector=fts(english).impossible" `shouldRespondWith`
|
||||||
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
|
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
@@ -962,6 +962,31 @@ spec =
|
|||||||
get "/rpc/get_tsearch?text_search_vector=wfts.impossible" `shouldRespondWith`
|
get "/rpc/get_tsearch?text_search_vector=wfts.impossible" `shouldRespondWith`
|
||||||
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
|
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "should work with filters that use the fts operator on text and json columns" $ do
|
||||||
|
get "/rpc/get_tsearch_to_tsvector?select=text_search&text_search=fts(english).impossible" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"text_search":"It's kind of fun to do the impossible"},
|
||||||
|
{"text_search":"C'est un peu amusant de faire l'impossible"}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/rpc/get_tsearch_to_tsvector?select=text_search&text_search=plfts.impossible" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"text_search":"It's kind of fun to do the impossible"},
|
||||||
|
{"text_search":"C'est un peu amusant de faire l'impossible"}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/rpc/get_tsearch_to_tsvector?select=text_search&text_search=not.fts(english).fun%7Crat" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"text_search":"C'est un peu amusant de faire l'impossible"},
|
||||||
|
{"text_search":"Es ist eine Art Spaß, das Unmögliche zu machen"}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/rpc/get_tsearch_to_tsvector?select=text_search&text_search=wfts.impossible" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"text_search":"It's kind of fun to do the impossible"},
|
||||||
|
{"text_search":"C'est un peu amusant de faire l'impossible"}]
|
||||||
|
|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "should work with the phraseto_tsquery function" $
|
it "should work with the phraseto_tsquery function" $
|
||||||
get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith`
|
get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith`
|
||||||
|
|||||||
Vendored
+9
@@ -930,3 +930,12 @@ INSERT INTO surr_serial_upsert(name, extra) VALUES ('value', 'existing value');
|
|||||||
|
|
||||||
TRUNCATE TABLE surr_gen_default_upsert CASCADE;
|
TRUNCATE TABLE surr_gen_default_upsert CASCADE;
|
||||||
INSERT INTO surr_gen_default_upsert(name, extra) VALUES ('value', 'existing value');
|
INSERT INTO surr_gen_default_upsert(name, extra) VALUES ('value', 'existing value');
|
||||||
|
|
||||||
|
TRUNCATE TABLE tsearch_to_tsvector CASCADE;
|
||||||
|
INSERT INTO tsearch_to_tsvector(text_search) VALUES ('It''s kind of fun to do the impossible');
|
||||||
|
INSERT INTO tsearch_to_tsvector(text_search) VALUES ('But also fun to do what is possible');
|
||||||
|
INSERT INTO tsearch_to_tsvector(text_search) VALUES ('Fat cats ate rats');
|
||||||
|
INSERT INTO tsearch_to_tsvector(text_search) VALUES ('C''est un peu amusant de faire l''impossible');
|
||||||
|
INSERT INTO tsearch_to_tsvector(text_search) VALUES ('Es ist eine Art Spaß, das Unmögliche zu machen');
|
||||||
|
|
||||||
|
UPDATE tsearch_to_tsvector SET jsonb_search = jsonb_build_object('text_search', text_search);
|
||||||
|
|||||||
Vendored
+13
@@ -3799,3 +3799,16 @@ create table surr_gen_default_upsert (
|
|||||||
name text,
|
name text,
|
||||||
extra text
|
extra text
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create table tsearch_to_tsvector (
|
||||||
|
text_search text,
|
||||||
|
jsonb_search jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
create function test.get_tsearch_to_tsvector() returns setof test.tsearch_to_tsvector AS $$
|
||||||
|
select * from test.tsearch_to_tsvector;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create function test.text_search_vector(test.tsearch_to_tsvector) returns tsvector AS $$
|
||||||
|
select to_tsvector('simple', $1.text_search)
|
||||||
|
$$ language sql;
|
||||||
|
|||||||
Reference in New Issue
Block a user