@@ -3,7 +3,6 @@ Module : PostgREST.Error
33Description : PostgREST error HTTP responses
44-}
55{-# OPTIONS_GHC -fno-warn-orphans #-}
6- {-# LANGUAGE NamedFieldPuns #-}
76{-# LANGUAGE RecordWildCards #-}
87
98module PostgREST.Error
@@ -25,7 +24,7 @@ import qualified Data.Aeson as JSON
2524import qualified Data.ByteString.Char8 as BS
2625import qualified Data.ByteString.Lazy as LBS
2726import qualified Data.CaseInsensitive as CI
28- import qualified Data.FuzzySet as Fuzzy
27+ import qualified Data.FuzzyStrMatch as Fuzzy
2928import qualified Data.HashMap.Strict as HM
3029import qualified Data.Map.Internal as M
3130import qualified Data.Text as T
@@ -43,7 +42,6 @@ import PostgREST.MediaType (MediaType (..))
4342import qualified PostgREST.MediaType as MediaType
4443
4544import PostgREST.Config (Verbosity (.. ))
46- import PostgREST.SchemaCache (SchemaCache (SchemaCache , dbTablesFuzzyIndex ))
4745import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (.. ),
4846 Schema )
4947import PostgREST.SchemaCache.Relationship (Cardinality (.. ),
@@ -52,6 +50,7 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
5250 RelationshipsMap )
5351import PostgREST.SchemaCache.Routine (Routine (.. ),
5452 RoutineParam (.. ))
53+ import PostgREST.SchemaCache.Table (Table (.. ))
5554
5655import PostgREST.Error.Types
5756
@@ -277,7 +276,7 @@ instance ErrorBody SchemaCacheError where
277276 where
278277 onlySingleParams = isInvPost && contentType `elem` [MTTextPlain , MTTextXML , MTOctetStream ]
279278 hint (AmbiguousRpc _) = Just " Try renaming the parameters or the function itself in the database so function overloading can be resolved"
280- hint (TableNotFound schemaName relName schemaCache ) = JSON. String <$> tableNotFoundHint schemaName relName schemaCache
279+ hint (TableNotFound schemaName relName tbls ) = JSON. String <$> tableNotFoundHint schemaName relName tbls
281280
282281 hint _ = Nothing
283282
@@ -303,13 +302,13 @@ instance ErrorBody SchemaCacheError where
303302-- Just "Perhaps you meant 'roles' instead of 'role'."
304303--
305304-- >>> noRelBetweenHint "films" "actors" "api" rels
306- -- Nothing
305+ -- Just "Perhaps you meant 'directors' instead of 'actors'."
307306--
308307-- >>> noRelBetweenHint "noclosealternative" "roles" "api" rels
309- -- Nothing
308+ -- Just "Perhaps you meant 'films' instead of 'noclosealternative'."
310309--
311310-- >>> noRelBetweenHint "films" "noclosealternative" "api" rels
312- -- Nothing
311+ -- Just "Perhaps you meant 'directors' instead of 'noclosealternative'."
313312--
314313-- >>> noRelBetweenHint "films" "noclosealternative" "noclosealternative" rels
315314-- Nothing
@@ -321,11 +320,10 @@ noRelBetweenHint parent child schema allRels = ("Perhaps you meant '" <>) <$>
321320 else (<> " ' instead of '" <> parent <> " '." ) <$> suggestParent
322321 where
323322 findParent = HM. lookup (QualifiedIdentifier schema parent, schema) allRels
324- fuzzySetOfParents = Fuzzy. fromList [qiName (fst p) | p <- HM. keys allRels, snd p == schema]
325- fuzzySetOfChildren = Fuzzy. fromList [qiName (relForeignTable c) | c <- fromMaybe [] findParent]
326- suggestParent = Fuzzy. getOne fuzzySetOfParents parent
327- -- Do not give suggestion if the child is found in the relations (weight = 1.0)
328- suggestChild = headMay [snd k | k <- Fuzzy. get fuzzySetOfChildren child, fst k < 1.0 ]
323+ parentList = [qiName (fst p) | p <- HM. keys allRels, snd p == schema]
324+ childrenList = [qiName (relForeignTable c) | c <- fromMaybe [] findParent]
325+ suggestParent = getFuzzyHint HintRelParent parent parentList
326+ suggestChild = getFuzzyHint HintRelChildren child childrenList
329327
330328-- |
331329-- If no function is found with the given name, it does a fuzzy search to all the functions
@@ -362,43 +360,78 @@ noRelBetweenHint parent child schema allRels = ("Perhaps you meant '" <>) <$>
362360-- Just "Perhaps you meant to call the function api.test(attr, id)"
363361--
364362-- >>> noRpcHint "api" "test" ["noclosealternative"] procs procsDesc
365- -- Nothing
363+ -- Just "Perhaps you meant to call the function api.test(attr, id)"
366364--
367365noRpcHint :: Text -> Text -> [Text ] -> [QualifiedIdentifier ] -> [Routine ] -> Maybe Text
368366noRpcHint schema procName params allProcs overloadedProcs =
369367 fmap ((" Perhaps you meant to call the function " <> schema <> " ." ) <> ) possibleProcs
370368 where
371- fuzzySetOfProcs = Fuzzy. fromList [qiName k | k <- allProcs, qiSchema k == schema]
372- fuzzySetOfParams = Fuzzy. fromList $ listToText <$> [[ppName prm | prm <- pdParams ov] | ov <- overloadedProcs]
369+ listOfProcs = [qiName k | k <- allProcs, qiSchema k == schema]
370+ listOfParams = listToText <$> [[ppName prm | prm <- pdParams ov] | ov <- overloadedProcs]
373371 -- Cannot do a fuzzy search like: Fuzzy.getOne [[Text]] [Text], where [[Text]] is the list of params for each
374372 -- overloaded function and [Text] the given params. This converts those lists to text to make fuzzy search possible.
375373 -- E.g. ["val", "param", "name"] into "(name, param, val)"
376374 listToText = (" (" <> ) . (<> " )" ) . T. intercalate " , " . sort
377375 possibleProcs
378- | null overloadedProcs = getFuzzyHint HintProcedure fuzzySetOfProcs procName
379- | otherwise = (procName <> ) <$> getFuzzyHint HintParams fuzzySetOfParams (listToText params)
376+ | null overloadedProcs = getFuzzyHint HintProcedure procName listOfProcs
377+ | otherwise = (procName <> ) <$> getFuzzyHint HintParams (listToText params) listOfParams
380378
381379-- |
382380-- Do a fuzzy search in all tables in the same schema and return closest result
383- tableNotFoundHint :: Text -> Text -> SchemaCache -> Maybe Text
384- tableNotFoundHint schema tblName SchemaCache {dbTablesFuzzyIndex}
381+ tableNotFoundHint :: Text -> Text -> [ Table ] -> Maybe Text
382+ tableNotFoundHint schema tblName tblList
385383 = fmap (\ tbl -> " Perhaps you meant the table '" <> schema <> " ." <> tbl <> " '" ) perhapsTable
386384 where
387- perhapsTable = (\ fuzzySet -> getFuzzyHint HintTable fuzzySet tblName) =<< HM. lookup schema dbTablesFuzzyIndex
385+ perhapsTable =
386+ if length tblList < maxDbTablesForFuzzySearch
387+ then getFuzzyHint HintTable tblName tblNames
388+ else Nothing
389+ tblNames = [ tableName tbl | tbl <- tblList, tableSchema tbl == schema]
390+ maxDbTablesForFuzzySearch = 500
388391
389392data HintType
390393 = HintTable
391394 | HintProcedure
392395 | HintParams
396+ | HintRelParent
397+ | HintRelChildren
398+ deriving Eq
393399
394- -- | Get hint using Fuzzy Search with at least 0.75 similarity score
395- getFuzzyHint :: HintType -> Fuzzy. FuzzySet -> Text -> Maybe Text
396- getFuzzyHint hintType =
397- let minScore = 0.75 :: Double -- used for table and procedure name hints
400+ -- | Get Fuzzy Hint comparing name with a list of names
401+ getFuzzyHint :: HintType -> Text -> [Text ] -> Maybe Text
402+ getFuzzyHint hintType name nameList =
403+ let
404+ maxDistanceForTableAndProc = 3
398405 in case hintType of
399- HintTable -> Fuzzy. getOneWithMinScore minScore
400- HintProcedure -> Fuzzy. getOneWithMinScore minScore
401- HintParams -> Fuzzy. getOne -- For params, we stick to `getOne` which defaults to 0.33 min score, not a security risk to reveal params
406+ -- TODO: Refactor and make it DRY
407+ HintTable -> checkLevenshteinDistance name nameList maxDistanceForTableAndProc
408+ HintProcedure -> checkLevenshteinDistance name nameList maxDistanceForTableAndProc
409+ HintParams -> checkMinimumLevenshteinDistance name nameList Nothing maxInt
410+ HintRelParent -> checkMinimumLevenshteinDistance name nameList Nothing maxInt
411+ HintRelChildren -> checkMinimumLevenshteinDistance name nameList Nothing maxInt
412+ where
413+ -- |
414+ -- Check Levenshtein Distance and return hint lower than max distance
415+ checkLevenshteinDistance :: Text -> [Text ] -> Int -> Maybe Text
416+ checkLevenshteinDistance _ [] _ = Nothing
417+ checkLevenshteinDistance identName (suggest: suggests) dist =
418+ case Fuzzy. levenshteinLessEqual identName suggest dist of
419+ Just _ -> Just suggest
420+ Nothing -> checkLevenshteinDistance identName suggests dist
421+
422+ -- |
423+ -- Check Levenshtein Distance and return hint with minimum distance
424+ checkMinimumLevenshteinDistance :: Text -> [Text ] -> Maybe Text -> Int -> Maybe Text
425+ checkMinimumLevenshteinDistance _ [] Nothing _ = Nothing
426+ checkMinimumLevenshteinDistance _ [] (Just suggest) _ = Just suggest
427+ checkMinimumLevenshteinDistance identName (suggest: suggests) currentSuggest minDist =
428+ let dist = Fuzzy. levenshtein identName suggest
429+ in if dist < minDist
430+ then
431+ if dist == 0 && hintType == HintRelChildren -- Do not give suggestion if the child is found in the relations (dist = 0)
432+ then checkMinimumLevenshteinDistance identName suggests currentSuggest minDist -- Go with current suggestion
433+ else checkMinimumLevenshteinDistance identName suggests (Just suggest) dist -- Update suggestion
434+ else checkMinimumLevenshteinDistance identName suggests currentSuggest minDist -- Go with current suggestion
402435
403436compressedRel :: Relationship -> JSON. Value
404437-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
0 commit comments