perf: use pg_basetype on PG 17+ for schema cache

This commit is contained in:
Joel Jacobson
2026-08-02 11:47:26 +00:00
committed by Wolfgang Walther
parent 6a7ad33524
commit 131df9b578
11 changed files with 118 additions and 88 deletions
+1
View File
@@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file. From versio
- Support running the admin server on a unix socket by @wolfgangwalther in #5003 - Support running the admin server on a unix socket by @wolfgangwalther in #5003
- Add config `server-reuseport` to allow starting multiple PostgREST instances using the same port on supported platforms by @mkleczek in #4703, #4694 - Add config `server-reuseport` to allow starting multiple PostgREST instances using the same port on supported platforms by @mkleczek in #4703, #4694
- Make config `log-level` reloadable by @taimoorzaeem in #5113 - Make config `log-level` reloadable by @taimoorzaeem in #5113
- Optimize schema cache domain type resolution by using `pg_basetype` on PostgreSQL 17+ by @joelonsql in #4567
### Fixed ### Fixed
+2 -1
View File
@@ -219,7 +219,8 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
liftIO $ when shouldShowWarnings $ liftIO $ when shouldShowWarnings $
observer $ LegacyTargetNameWarningObs (legacyWarnMsg, legacyWarnHint) iMethod (iPath <> Wai.rawQueryString req) -- TODO maybe store rawQueryString in ApiRequest for consistency observer $ LegacyTargetNameWarningObs (legacyWarnMsg, legacyWarnHint) iMethod (iPath <> Wai.rawQueryString req) -- TODO maybe store rawQueryString in ApiRequest for consistency
let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest pgVer <- liftIO $ AppState.getPgVersion appState
let mainQ = Query.mainQuery pgVer plan conf apiReq authResult configDbPreRequest
tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache
obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s
+2 -1
View File
@@ -92,8 +92,9 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer} =
qSchemaCache :: IO (Maybe SchemaCache) qSchemaCache :: IO (Maybe SchemaCache)
qSchemaCache = do qSchemaCache = do
conf@AppConfig{..} <- getConfig appState conf@AppConfig{..} <- getConfig appState
pgVer <- getPgVersion appState
(resultTime, result) <- (resultTime, result) <-
timeItT $ usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf) timeItT $ usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache pgVer conf)
case result of case result of
Left e -> do Left e -> do
markSchemaCachePending appState markSchemaCachePending appState
+2 -1
View File
@@ -63,8 +63,9 @@ runAppCommand conf@AppConfig{..} runCmd = do
dumpSchema :: AppState -> IO LBS.ByteString dumpSchema :: AppState -> IO LBS.ByteString
dumpSchema appState = do dumpSchema appState = do
conf@AppConfig{..} <- AppState.getConfig appState conf@AppConfig{..} <- AppState.getConfig appState
pgVer <- AppState.getPgVersion appState
result <- result <-
AppState.usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf) AppState.usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache pgVer conf)
case result of case result of
Left e -> do Left e -> do
let observer = AppState.getObserver appState let observer = AppState.getObserver appState
@@ -4,6 +4,7 @@ module PostgREST.Config.PgVersion
( PgVersion(..) ( PgVersion(..)
, minimumPgVersion , minimumPgVersion
, pgVersion150 , pgVersion150
, pgVersion170
, pgVersion180 , pgVersion180
, pgVersion190 , pgVersion190
) where ) where
@@ -33,6 +34,9 @@ pgVersion140 = PgVersion 140000 "14.0" "14.0"
pgVersion150 :: PgVersion pgVersion150 :: PgVersion
pgVersion150 = PgVersion 150000 "15.0" "15.0" pgVersion150 = PgVersion 150000 "15.0" "15.0"
pgVersion170 :: PgVersion
pgVersion170 = PgVersion 170000 "17.0" "17.0"
pgVersion180 :: PgVersion pgVersion180 :: PgVersion
pgVersion180 = PgVersion 180000 "18.0" "18.0" pgVersion180 = PgVersion 180000 "18.0" "18.0"
+5 -4
View File
@@ -22,6 +22,7 @@ import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.ApiRequest.Preferences (Preferences (..), shouldExplainCount) import PostgREST.ApiRequest.Preferences (Preferences (..), shouldExplainCount)
import PostgREST.Auth.Types (AuthResult (..)) import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.Config.PgVersion (PgVersion)
import PostgREST.Plan (ActionPlan (..), CrudPlan (..), import PostgREST.Plan (ActionPlan (..), CrudPlan (..),
DbActionPlan (..), InspectPlan (..)) DbActionPlan (..), InspectPlan (..))
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
@@ -38,9 +39,9 @@ data MainQuery = MainQuery
, mqExplain :: Maybe SQL.Snippet -- ^ the explain query that gets generated for the "Prefer: count=estimated" case , mqExplain :: Maybe SQL.Snippet -- ^ the explain query that gets generated for the "Prefer: count=estimated" case
} }
mainQuery :: ActionPlan -> AppConfig -> ApiRequest -> AuthResult -> Maybe QualifiedIdentifier -> MainQuery mainQuery :: PgVersion -> ActionPlan -> AppConfig -> ApiRequest -> AuthResult -> Maybe QualifiedIdentifier -> MainQuery
mainQuery (NoDb _) _ _ _ _ = MainQuery mempty Nothing mempty (mempty, mempty, mempty) mempty mainQuery _ (NoDb _) _ _ _ _ = MainQuery mempty Nothing mempty (mempty, mempty, mempty) mempty
mainQuery (Db plan) conf@AppConfig{..} apiReq@ApiRequest{iTopLevelRange=range, iPreferences=Preferences{..}} authRes preReq = mainQuery pgVer (Db plan) conf@AppConfig{..} apiReq@ApiRequest{iTopLevelRange=range, iPreferences=Preferences{..}} authRes preReq =
let genQ = MainQuery (PreQuery.txVarQuery plan conf authRes apiReq) (PreQuery.preReqQuery <$> preReq) in let genQ = MainQuery (PreQuery.txVarQuery plan conf authRes apiReq) (PreQuery.preReqQuery <$> preReq) in
case plan of case plan of
DbCrud _ WrappedReadPlan{..} -> DbCrud _ WrappedReadPlan{..} ->
@@ -52,4 +53,4 @@ mainQuery (Db plan) conf@AppConfig{..} apiReq@ApiRequest{iTopLevelRange=range, i
DbCrud _ CallReadPlan{..} -> DbCrud _ CallReadPlan{..} ->
genQ (Statements.mainCall crProc crCallPlan crReadPlan preferCount configDbMaxRows range pMedia crHandler) (mempty, mempty, mempty) mempty genQ (Statements.mainCall crProc crCallPlan crReadPlan preferCount configDbMaxRows range pMedia crHandler) (mempty, mempty, mempty) mempty
MayUseDb InspectPlan{ipSchema=tSchema} -> MayUseDb InspectPlan{ipSchema=tSchema} ->
genQ mempty (SqlFragment.accessibleTables tSchema, SqlFragment.accessibleFuncs tSchema, SqlFragment.schemaDescription tSchema) mempty genQ mempty (SqlFragment.accessibleTables tSchema, SqlFragment.accessibleFuncs pgVer tSchema, SqlFragment.schemaDescription tSchema) mempty
+51 -29
View File
@@ -68,6 +68,7 @@ import PostgREST.ApiRequest.Types (AggregateFunction (..), Alias, Cast,
Operation (..), OrderDirection (..), Operation (..), OrderDirection (..),
OrderNulls (..), QuantOperator (..), OrderNulls (..), QuantOperator (..),
SimpleOperator (..)) SimpleOperator (..))
import PostgREST.Config.PgVersion (PgVersion, pgVersion170)
import PostgREST.MediaType (MTVndPlanFormat (..), import PostgREST.MediaType (MTVndPlanFormat (..),
MTVndPlanOption (..)) MTVndPlanOption (..))
import PostgREST.Plan.ReadPlan (JoinCondition (..)) import PostgREST.Plan.ReadPlan (JoinCondition (..))
@@ -612,39 +613,60 @@ accessibleTables schema = SQL.sql (encodeUtf8 [trimming|
where where
encodedSchema = SQL.encoderAndParam (HE.nonNullable HE.text) schema encodedSchema = SQL.encoderAndParam (HE.nonNullable HE.text) schema
accessibleFuncs :: Text -> SQL.Snippet accessibleFuncs :: PgVersion -> Text -> SQL.Snippet
accessibleFuncs schema = baseFuncSqlQuery <> "AND p.pronamespace = " <> encodedSchema <> "::regnamespace" accessibleFuncs pgVer schema = baseFuncSqlQuery pgVer <> "AND p.pronamespace = " <> encodedSchema <> "::regnamespace"
where where
encodedSchema = SQL.encoderAndParam (HE.nonNullable HE.text) schema encodedSchema = SQL.encoderAndParam (HE.nonNullable HE.text) schema
baseFuncSqlQuery :: SQL.Snippet baseTypesCte :: PgVersion -> Text
baseFuncSqlQuery = SQL.sql $ encodeUtf8 [trimming| baseTypesCte pgVer
| pgVer >= pgVersion170 = [trimming|
-- Get base types using pg_basetype() (PG 17+)
base_types AS (
SELECT
t.oid,
bt.typnamespace AS base_namespace,
bt.oid AS base_type
FROM pg_type t
JOIN pg_type bt ON bt.oid = pg_basetype(t.oid)
)
|]
| otherwise = [trimming|
-- Recursively get the base types of domains (PG < 17)
base_types AS (
WITH RECURSIVE
recurse AS (
SELECT
oid,
typbasetype,
typnamespace AS base_namespace,
COALESCE(NULLIF(typbasetype, 0), oid) AS base_type
FROM pg_type
UNION
SELECT
t.oid,
b.typbasetype,
b.typnamespace AS base_namespace,
COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base_type
FROM recurse t
JOIN pg_type b ON t.typbasetype = b.oid
)
SELECT
oid,
base_namespace,
base_type
FROM recurse
WHERE typbasetype = 0
)
|]
-- | SQL query to get accessible functions for OpenAPI.
baseFuncSqlQuery :: PgVersion -> SQL.Snippet
baseFuncSqlQuery pgVer =
let baseCte = baseTypesCte pgVer
in SQL.sql $ encodeUtf8 [trimming|
WITH WITH
base_types AS ( $baseCte,
WITH RECURSIVE
recurse AS (
SELECT
oid,
typbasetype,
typnamespace AS base_namespace,
COALESCE(NULLIF(typbasetype, 0), oid) AS base_type
FROM pg_type
UNION
SELECT
t.oid,
b.typbasetype,
b.typnamespace AS base_namespace,
COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base_type
FROM recurse t
JOIN pg_type b ON t.typbasetype = b.oid
)
SELECT
oid,
base_namespace,
base_type
FROM recurse
WHERE typbasetype = 0
),
arguments AS ( arguments AS (
SELECT SELECT
oid, oid,
+43 -44
View File
@@ -46,6 +46,7 @@ import NeatInterpolation (trimming)
import PostgREST.Config (AppConfig (..), LogLevel (..)) import PostgREST.Config (AppConfig (..), LogLevel (..))
import PostgREST.Config.Database (TimezoneNames, toIsolationLevel) import PostgREST.Config.Database (TimezoneNames, toIsolationLevel)
import PostgREST.Config.PgVersion (PgVersion, pgVersion170)
import PostgREST.SchemaCache.Identifiers (FieldName, import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier (..), QualifiedIdentifier (..),
RelIdentifier (..), Schema, RelIdentifier (..), Schema,
@@ -146,16 +147,16 @@ type SqlQuery = ByteString
maxDbTablesForFuzzySearch :: Int maxDbTablesForFuzzySearch :: Int
maxDbTablesForFuzzySearch = 500 maxDbTablesForFuzzySearch = 500
querySchemaCache :: AppConfig -> SQL.Transaction (SchemaCache, Maybe QueryTimings) querySchemaCache :: PgVersion -> AppConfig -> SQL.Transaction (SchemaCache, Maybe QueryTimings)
querySchemaCache conf@AppConfig{..} = do querySchemaCache pgVer conf@AppConfig{..} = do
SQL.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object SQL.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object
for_ configInternalSCQuerySleepFst (`SQL.statement` sleepCall) -- only used for testing for_ configInternalSCQuerySleepFst (`SQL.statement` sleepCall) -- only used for testing
tabs <- sqlTimedStmt gucTbls conf allTables tabs <- sqlTimedStmt gucTbls conf (allTables pgVer configDbPreparedStatements)
keyDeps <- sqlTimedStmt gucKDeps conf allViewsKeyDependencies keyDeps <- sqlTimedStmt gucKDeps conf allViewsKeyDependencies
m2oRels <- sqlTimedStmt gucRels mempty allM2OandO2ORels m2oRels <- sqlTimedStmt gucRels mempty allM2OandO2ORels
funcs <- sqlTimedStmt gucFuncs conf allFunctions funcs <- sqlTimedStmt gucFuncs conf (allFunctions pgVer configDbPreparedStatements)
cRels <- sqlTimedStmt gucCRels mempty allComputedRels cRels <- sqlTimedStmt gucCRels mempty allComputedRels
reps <- sqlTimedStmt gucDReps conf dataRepresentations reps <- sqlTimedStmt gucDReps conf dataRepresentations
mHdlers <- sqlTimedStmt gucMHdrs conf mediaHandlers mHdlers <- sqlTimedStmt gucMHdrs conf mediaHandlers
@@ -374,47 +375,44 @@ dataRepresentations = SQL.Statement sql mempty decodeRepresentations True
OR (dst_t.typtype = 'd' AND c.castsource IN ('json'::regtype::oid , 'text'::regtype::oid))) OR (dst_t.typtype = 'd' AND c.castsource IN ('json'::regtype::oid , 'text'::regtype::oid)))
|] |]
allFunctions :: SQL.Statement AppConfig RoutineMap allFunctions :: PgVersion -> Bool -> SQL.Statement AppConfig RoutineMap
allFunctions = SQL.Statement funcsSqlQuery params decodeFuncs True allFunctions pgVer = SQL.Statement (funcsSqlQuery pgVer) params decodeFuncs
where where
params = params =
(map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text) <> (map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text) <>
(configDbHoistedTxSettings >$< arrayParam HE.text) (configDbHoistedTxSettings >$< arrayParam HE.text)
baseTypesCte :: Text baseTypesCte :: PgVersion -> Text
baseTypesCte = [trimming| baseTypesCte pgVer
-- Recursively get the base types of domains | pgVer >= pgVersion170 = [trimming|
base_types AS ( base_types AS (
WITH RECURSIVE SELECT t.oid, bt.typnamespace AS base_namespace, bt.oid AS base_type
recurse AS ( FROM pg_type t
SELECT JOIN pg_type bt ON bt.oid = pg_basetype(t.oid)
oid, )
typbasetype, |]
typnamespace AS base_namespace, | otherwise = [trimming|
COALESCE(NULLIF(typbasetype, 0), oid) AS base_type base_types AS (
FROM pg_type WITH RECURSIVE recurse AS (
UNION SELECT oid, typbasetype, typnamespace AS base_namespace,
SELECT COALESCE(NULLIF(typbasetype, 0), oid) AS base_type
t.oid, FROM pg_type
b.typbasetype, UNION
b.typnamespace AS base_namespace, SELECT t.oid, b.typbasetype, b.typnamespace AS base_namespace,
COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base_type COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base_type
FROM recurse t FROM recurse t
JOIN pg_type b ON t.typbasetype = b.oid JOIN pg_type b ON t.typbasetype = b.oid
) )
SELECT SELECT oid, base_namespace, base_type FROM recurse WHERE typbasetype = 0
oid, )
base_namespace, |]
base_type
FROM recurse
WHERE typbasetype = 0
)
|]
funcsSqlQuery :: SqlQuery funcsSqlQuery :: PgVersion -> SqlQuery
funcsSqlQuery = encodeUtf8 [trimming| funcsSqlQuery pgVer =
let baseCte = baseTypesCte pgVer
in encodeUtf8 [trimming|
WITH WITH
$baseTypesCte, $baseCte,
arguments AS ( arguments AS (
SELECT SELECT
oid, oid,
@@ -587,22 +585,23 @@ addViewPrimaryKeys tabs keyDeps =
takeFirstPK = mapMaybe (head . snd) takeFirstPK = mapMaybe (head . snd)
indexedDeps = HM.fromListWith (++) $ fmap ((keyDepType &&& keyDepView) &&& pure) keyDeps indexedDeps = HM.fromListWith (++) $ fmap ((keyDepType &&& keyDepView) &&& pure) keyDeps
allTables :: SQL.Statement AppConfig TablesMap allTables :: PgVersion -> Bool -> SQL.Statement AppConfig TablesMap
allTables = SQL.Statement tablesSqlQuery params decodeTables True allTables pgVer = SQL.Statement (tablesSqlQuery pgVer) params decodeTables
where where
params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text
-- | Gets tables with their PK cols -- | Gets tables with their PK cols
tablesSqlQuery :: SqlQuery tablesSqlQuery :: PgVersion -> SqlQuery
tablesSqlQuery = tablesSqlQuery pgVer =
-- the tbl_constraints/key_col_usage CTEs are based on the standard "information_schema.table_constraints"/"information_schema.key_column_usage" views, -- the tbl_constraints/key_col_usage CTEs are based on the standard "information_schema.table_constraints"/"information_schema.key_column_usage" views,
-- we cannot use those directly as they include the following privilege filter: -- we cannot use those directly as they include the following privilege filter:
-- (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text)); -- (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text));
-- on the "columns" CTE, left joining on pg_depend and pg_class is used to obtain the sequence name as a column default in case there are GENERATED .. AS IDENTITY, -- on the "columns" CTE, left joining on pg_depend and pg_class is used to obtain the sequence name as a column default in case there are GENERATED .. AS IDENTITY,
-- generated columns are only available from pg >= 10 but the query is agnostic to versions. dep.deptype = 'i' is done because there are other 'a' dependencies on PKs -- generated columns are only available from pg >= 10 but the query is agnostic to versions. dep.deptype = 'i' is done because there are other 'a' dependencies on PKs
encodeUtf8 [trimming| let baseCte = baseTypesCte pgVer
in encodeUtf8 [trimming|
WITH WITH
$baseTypesCte, $baseCte,
columns AS ( columns AS (
SELECT SELECT
c.oid AS relid, c.oid AS relid,
+1 -1
View File
@@ -783,7 +783,7 @@ def test_log_query(level, defaultenv):
) )
infinite_recursion_5xx_regx = r'.+: WITH pgrst_source AS.+SELECT "public"\."infinite_recursion"\.\* FROM "public"\."infinite_recursion".+_postgrest_t' infinite_recursion_5xx_regx = r'.+: WITH pgrst_source AS.+SELECT "public"\."infinite_recursion"\.\* FROM "public"\."infinite_recursion".+_postgrest_t'
root_tables_regx = r".+: SELECT n.nspname AS table_schema, .+ FROM pg_class c .+ ORDER BY table_schema, table_name" root_tables_regx = r".+: SELECT n.nspname AS table_schema, .+ FROM pg_class c .+ ORDER BY table_schema, table_name"
root_procs_regx = r".+: WITH base_types AS \(.+\) SELECT pn.nspname AS proc_schema, .+ FROM pg_proc p.+AND p.pronamespace = \$1::regnamespace" root_procs_regx = r".+: WITH.+base_types AS.+pn\.nspname AS proc_schema.+FROM pg_proc p.+p\.pronamespace = \$1::regnamespace"
root_descr_regx = r".+: SELECT pg_catalog\.obj_description\(\$1::regnamespace, 'pg_namespace'\)" root_descr_regx = r".+: SELECT pg_catalog\.obj_description\(\$1::regnamespace, 'pg_namespace'\)"
set_config_regx = ( set_config_regx = (
r".+: select set_config\('search_path', \$1, true\), set_config\(" r".+: select set_config\('search_path', \$1, true\), set_config\("
+3 -3
View File
@@ -48,7 +48,7 @@ main = do
actualPgVersion <- either (panic . show) id <$> P.use pool queryPgVersion actualPgVersion <- either (panic . show) id <$> P.use pool queryPgVersion
-- cached schema cache so most tests run fast -- cached schema cache so most tests run fast
baseSchemaCache <- loadSCache pool testCfg baseSchemaCache <- loadSCache pool actualPgVersion testCfg
let let
initApp sCache config = do initApp sCache config = do
@@ -72,5 +72,5 @@ main = do
describe "Feature.SchemaCacheSpec" Observation.SchemaCacheSpec.spec describe "Feature.SchemaCacheSpec" Observation.SchemaCacheSpec.spec
where where
loadSCache pool conf = loadSCache pool pgVersion conf =
either (panic.show) fst <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ querySchemaCache conf) either (panic.show) fst <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ querySchemaCache pgVersion conf)
+4 -4
View File
@@ -88,7 +88,7 @@ main = do
actualPgVersion <- either (panic . show) id <$> P.use pool queryPgVersion actualPgVersion <- either (panic . show) id <$> P.use pool queryPgVersion
-- cached schema cache so most tests run fast -- cached schema cache so most tests run fast
baseSchemaCache <- loadSCache pool baseCfg baseSchemaCache <- loadSCache pool actualPgVersion baseCfg
metricsState <- Metrics.init (configDbPoolSize baseCfg) metricsState <- Metrics.init (configDbPoolSize baseCfg)
let let
@@ -105,7 +105,7 @@ main = do
-- For tests that run with a different SchemaCache (depends on configSchemas) -- For tests that run with a different SchemaCache (depends on configSchemas)
appDbs config = do appDbs config = do
customSchemaCache <- loadSCache pool config customSchemaCache <- loadSCache pool actualPgVersion config
initApp customSchemaCache config initApp customSchemaCache config
withConfig config = before (app config) withConfig config = before (app config)
@@ -187,5 +187,5 @@ main = do
describe "Feature.Query.PgSafeUpdateSpec.spec" $ Feature.Query.PgSafeUpdateSpec.spec withConfig describe "Feature.Query.PgSafeUpdateSpec.spec" $ Feature.Query.PgSafeUpdateSpec.spec withConfig
where where
loadSCache pool conf = loadSCache pool pgVersion conf =
either (panic.show) fst <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ querySchemaCache conf) either (panic.show) fst <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ querySchemaCache pgVersion conf)