Skip to content

Commit 20b391f

Browse files
committed
perf: Cache dbTables FuzzySet per schema
Calculation of hint message when requested relation is not present in schema cache requires creation of a FuzzySet (to use fuzzy search to find candidate tables). For schemas with many tables it is costly. This patch introduces dbTablesFuzzyIndex in SchemaCache to memoize the FuzzySet creation.
1 parent 5318483 commit 20b391f

File tree

6 files changed

+49
-25
lines changed

6 files changed

+49
-25
lines changed

src/PostgREST/Error.hs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,8 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
4949
RelationshipsMap)
5050
import PostgREST.SchemaCache.Routine (Routine (..),
5151
RoutineParam (..))
52-
import PostgREST.SchemaCache.Table (Table (..))
5352
import Protolude
5453

55-
5654
class (ErrorBody a, JSON.ToJSON a) => PgrstError a where
5755
status :: a -> HTTP.Status
5856
headers :: a -> [Header]
@@ -250,7 +248,7 @@ data SchemaCacheError
250248
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
251249
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
252250
| ColumnNotFound Text Text
253-
| TableNotFound Text Text [Table]
251+
| TableNotFound Text Text (HM.HashMap Schema Fuzzy.FuzzySet)
254252
deriving Show
255253

256254
instance PgrstError SchemaCacheError where
@@ -428,12 +426,12 @@ noRpcHint schema procName params allProcs overloadedProcs =
428426

429427
-- |
430428
-- Do a fuzzy search in all tables in the same schema and return closest result
431-
tableNotFoundHint :: Text -> Text -> [Table] -> Maybe Text
432-
tableNotFoundHint schema tblName tblList
429+
tableNotFoundHint :: Text -> Text -> HM.HashMap Schema Fuzzy.FuzzySet -> Maybe Text
430+
tableNotFoundHint schema tblName dbTablesFuzzyIndex
433431
= fmap (\tbl -> "Perhaps you meant the table '" <> schema <> "." <> tbl <> "'") perhapsTable
434432
where
435433
perhapsTable = Fuzzy.getOne fuzzyTableSet tblName
436-
fuzzyTableSet = Fuzzy.fromList [ tableName tbl | tbl <- tblList, tableSchema tbl == schema]
434+
fuzzyTableSet = fromMaybe Fuzzy.defaultSet (HM.lookup schema dbTablesFuzzyIndex)
437435

438436

439437
compressedRel :: Relationship -> JSON.Value

src/PostgREST/Plan.hs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,15 +172,15 @@ dbActionPlan dbAct conf apiReq sCache = case dbAct of
172172

173173
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Bool -> Either Error CrudPlan
174174
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
175-
qi <- findTable identifier (dbTables sCache)
175+
qi <- findTable identifier sCache
176176
rPlan <- readPlan qi conf sCache apiRequest
177177
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
178178
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
179179
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly qi
180180

181181
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan
182182
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
183-
qi <- findTable identifier (dbTables sCache)
183+
qi <- findTable identifier sCache
184184
rPlan <- readPlan qi conf sCache apiRequest
185185
mPlan <- mutatePlan mutation qi apiRequest sCache rPlan
186186
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
@@ -810,10 +810,10 @@ validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
810810
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
811811

812812
-- | Lookup table in the schema cache before creating read plan
813-
findTable :: QualifiedIdentifier -> TablesMap -> Either Error QualifiedIdentifier
814-
findTable qi@QualifiedIdentifier{..} tableMap =
815-
case HM.lookup qi tableMap of
816-
Nothing -> Left $ SchemaCacheErr $ TableNotFound qiSchema qiName (HM.elems tableMap)
813+
findTable :: QualifiedIdentifier -> SchemaCache -> Either Error QualifiedIdentifier
814+
findTable qi@QualifiedIdentifier{..} SchemaCache{dbTables, dbTablesFuzzyIndex} =
815+
case HM.lookup qi dbTables of
816+
Nothing -> Left $ SchemaCacheErr $ TableNotFound qiSchema qiName dbTablesFuzzyIndex
817817
Just _ -> Right qi
818818

819819
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree

src/PostgREST/Response.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,10 @@ actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versio
209209
in
210210
Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader schema negotiatedByProfile)) rsBody
211211

212-
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ SchemaCache{dbTables} _ _ =
212+
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ SchemaCache{dbTables, dbTablesFuzzyIndex} _ _ =
213213
case HM.lookup qi dbTables of
214214
Just tbl -> respondInfo $ allowH tbl
215-
Nothing -> Left $ Error.SchemaCacheErr $ Error.TableNotFound qiSchema qiName (HM.elems dbTables)
215+
Nothing -> Left $ Error.SchemaCacheErr $ Error.TableNotFound qiSchema qiName dbTablesFuzzyIndex
216216
where
217217
allowH table =
218218
let hasPK = not . null $ tablePKCols table in

src/PostgREST/SchemaCache.hs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,23 @@ import PostgREST.SchemaCache.Table (Column (..), ColumnMap,
6666

6767
import qualified PostgREST.MediaType as MediaType
6868

69-
import Control.Arrow ((&&&))
70-
import Protolude
71-
import System.IO.Unsafe (unsafePerformIO)
69+
import Control.Arrow ((&&&))
70+
import qualified Data.FuzzySet as Fuzzy
71+
import Protolude
72+
import System.IO.Unsafe (unsafePerformIO)
7273

7374
data SchemaCache = SchemaCache
74-
{ dbTables :: TablesMap
75-
, dbRelationships :: RelationshipsMap
76-
, dbRoutines :: RoutineMap
77-
, dbRepresentations :: RepresentationsMap
78-
, dbMediaHandlers :: MediaHandlerMap
79-
, dbTimezones :: TimezoneNames
75+
{ dbTables :: TablesMap
76+
, dbRelationships :: RelationshipsMap
77+
, dbRoutines :: RoutineMap
78+
, dbRepresentations :: RepresentationsMap
79+
, dbMediaHandlers :: MediaHandlerMap
80+
, dbTimezones :: TimezoneNames
81+
, dbTablesFuzzyIndex :: HM.HashMap Schema Fuzzy.FuzzySet
8082
}
8183

8284
instance JSON.ToJSON SchemaCache where
83-
toJSON (SchemaCache tabs rels routs reps hdlers tzs) = JSON.object [
85+
toJSON (SchemaCache tabs rels routs reps hdlers tzs _) = JSON.object [
8486
"dbTables" .= JSON.toJSON tabs
8587
, "dbRelationships" .= JSON.toJSON rels
8688
, "dbRoutines" .= JSON.toJSON routs
@@ -90,7 +92,7 @@ instance JSON.ToJSON SchemaCache where
9092
]
9193

9294
showSummary :: SchemaCache -> Text
93-
showSummary (SchemaCache tbls rels routs reps mediaHdlrs tzs) =
95+
showSummary (SchemaCache tbls rels routs reps mediaHdlrs tzs _) =
9496
T.intercalate ", "
9597
[ show (HM.size tbls) <> " Relations"
9698
, show (HM.size rels) <> " Relationships"
@@ -166,6 +168,8 @@ querySchemaCache conf@AppConfig{..} = do
166168
, dbRepresentations = reps
167169
, dbMediaHandlers = HM.union mHdlers initialMediaHandlers -- the custom handlers will override the initial ones
168170
, dbTimezones = tzones
171+
172+
, dbTablesFuzzyIndex = Fuzzy.fromList <$> HM.fromListWith (<>) ((qiSchema &&& pure . qiName) <$> HM.keys tabsWViewsPks)
169173
}
170174
where
171175
schemas = toList configDbSchemas
@@ -203,6 +207,7 @@ removeInternal schemas dbStruct =
203207
, dbRepresentations = dbRepresentations dbStruct -- no need to filter, not directly exposed through the API
204208
, dbMediaHandlers = dbMediaHandlers dbStruct
205209
, dbTimezones = dbTimezones dbStruct
210+
, dbTablesFuzzyIndex = dbTablesFuzzyIndex dbStruct
206211
}
207212
where
208213
hasInternalJunction ComputedRelationship{} = False

test/load/fixtures.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@ STABLE LANGUAGE SQL AS $$
4141
SELECT 'Hello ' || name || ', how are you?';
4242
$$;
4343

44+
-- Create 1000 tables to test fuzzy string search
45+
-- computing hints for non existing tables
46+
DO
47+
$$
48+
DECLARE
49+
r record;
50+
BEGIN
51+
FOR r IN SELECT
52+
format('CREATE TABLE test.authors_%s ()', n) AS ct
53+
FROM generate_series(1, 1000) n
54+
LOOP
55+
EXECUTE r.ct;
56+
END LOOP;
57+
END
58+
$$;
59+
4460
GRANT USAGE ON SCHEMA test TO postgrest_test_anonymous, postgrest_test_author;
4561
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA test TO postgrest_test_anonymous;
4662

@@ -49,3 +65,5 @@ REVOKE ALL PRIVILEGES ON TABLE
4965
FROM postgrest_test_anonymous;
5066

5167
GRANT ALL ON TABLE authors_only TO postgrest_test_author;
68+
69+
SELECT n, format('CREATE TABLE test.authors_only_%s ()', n) FROM generate_series(1, 1000) n

test/load/targets.http

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ POST http://postgrest/rpc/call_me
3333
@rpc.json
3434

3535
OPTIONS http://postgrest/actors
36+
37+
# Not existing table
38+
GET http://postgrest/actors_0?select=*,roles(*,films(*))

0 commit comments

Comments
 (0)