From 7560fafbab677638e4439ebca92d96ec65de0c7a Mon Sep 17 00:00:00 2001 From: calebmer Date: Mon, 9 Nov 2015 17:42:53 -0500 Subject: [PATCH 01/12] Refactor DbStructure - Rename `dbstructure` to `db` in App.hs - Rename PgStructure* to DbStructure* - Move `DbStructure` creation to DbStructure.hs --- postgrest.cabal | 6 +- src/PostgREST/App.hs | 12 ++-- .../{PgStructure.hs => DbStructure.hs} | 55 ++++++++++++------- src/PostgREST/Main.hs | 36 ++---------- src/PostgREST/MainTest.hs | 8 +-- src/PostgREST/PgQuery.hs | 2 +- src/PostgREST/QueryBuilder.hs | 14 ++--- src/PostgREST/Types.hs | 20 ++++--- test/Feature/StructureSpec.hs | 2 + test/SpecHelper.hs | 21 ++----- ...{PgStructureSpec.hx => DbStructureSpec.hx} | 4 +- 11 files changed, 81 insertions(+), 99 deletions(-) rename src/PostgREST/{PgStructure.hs => DbStructure.hs} (85%) rename test/Unit/{PgStructureSpec.hx => DbStructureSpec.hx} (93%) diff --git a/postgrest.cabal b/postgrest.cabal index 83eff1d4e..9db15172a 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -70,7 +70,7 @@ executable postgrest , PostgREST.Middleware , PostgREST.Parsers , PostgREST.PgQuery - , PostgREST.PgStructure + , PostgREST.DbStructure , PostgREST.QueryBuilder , PostgREST.RangeQuery , PostgREST.Types @@ -134,7 +134,7 @@ library , PostgREST.Middleware , PostgREST.Parsers , PostgREST.PgQuery - , PostgREST.PgStructure + , PostgREST.DbStructure , PostgREST.QueryBuilder , PostgREST.RangeQuery , PostgREST.Types @@ -165,7 +165,7 @@ Test-Suite spec , PostgREST.Middleware , PostgREST.Parsers , PostgREST.PgQuery - , PostgREST.PgStructure + , PostgREST.DbStructure , PostgREST.QueryBuilder , PostgREST.RangeQuery , PostgREST.Types diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 2f1bf4b67..2779c016d 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -46,7 +46,7 @@ import qualified Hasql.Postgres as P import PostgREST.Config (AppConfig (..)) import PostgREST.Parsers import PostgREST.PgQuery -import PostgREST.PgStructure +import PostgREST.DbStructure import PostgREST.QueryBuilder import PostgREST.RangeQuery import PostgREST.Types @@ -55,7 +55,7 @@ import PostgREST.Auth (tokenJWT) import Prelude app :: DbStructure -> AppConfig -> BL.ByteString -> Request -> H.Tx P.Postgres s Response -app dbstructure conf reqBody req = +app db conf reqBody req = case (path, verb) of ([table], "GET") -> @@ -160,9 +160,9 @@ app dbstructure conf reqBody req = return $ responseLBS status404 [] "" where - allRels = relations dbstructure - allCols = columns dbstructure - allPrKeys = primaryKeys dbstructure + allRels = relations db + allCols = columns db + allPrKeys = primaryKeys db filterCol sc table (Column{colSchema=s, colTable=t}) = s==sc && table==t filterCol _ _ _ = False filterPk sc table pk = sc == pkSchema pk && table == pkTable pk @@ -332,7 +332,7 @@ addFilter (path, flt) (Node rn forest) = where maybeNode = find ((name==).fst.snd.rootLabel) forst toSourceRelation :: Text -> Relation -> Maybe Relation -toSourceRelation mt r@(Relation _ t _ ft _ _ rt _ _) +toSourceRelation mt r@(Relation _ t _ _ ft _ _ _ rt _ _) | mt == t = Just $ r {relTable=sourceSubqueryName} | mt == ft = Just $ r {relFTable=sourceSubqueryName} | Just mt == rt = Just $ r {relLTable=Just sourceSubqueryName} diff --git a/src/PostgREST/PgStructure.hs b/src/PostgREST/DbStructure.hs similarity index 85% rename from src/PostgREST/PgStructure.hs rename to src/PostgREST/DbStructure.hs index 74d620078..911856202 100644 --- a/src/PostgREST/PgStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -3,7 +3,7 @@ {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeSynonymInstances #-} -module PostgREST.PgStructure where +module PostgREST.DbStructure where import Control.Applicative import Control.Monad (join) @@ -21,6 +21,20 @@ import PostgREST.Types import GHC.Exts (groupWith) import Prelude +createDbStructure :: H.Tx P.Postgres s DbStructure +createDbStructure = do + tabs <- allTables + rels <- allRelations + cols <- allColumns rels + keys <- allPrimaryKeys + + return DbStructure { + tables = tabs + , columns = cols + , relations = rels + , primaryKeys = keys + } + doesProc :: forall c s. B.CxValue c Int => (Text -> Text -> B.Stmt c) -> Text -> Text -> H.Tx c s Bool doesProc stmt schema proc = do @@ -64,15 +78,15 @@ columnFromRow (s, t, n, pos, nul, typ, u, l, p, d, e) = parseEnum str = fromMaybe [] $ split (==',') <$> str -relationFromRow :: (Text, Text, [Text], Text, [Text]) -> Relation -relationFromRow (s, t, cs, ft, fcs) = Relation s t cs ft fcs Child Nothing Nothing Nothing +relationFromRow :: (Text, Text, [Text], Text, Text, [Text]) -> Relation +relationFromRow (s, t, cs, fs, ft, fcs) = Relation s t cs fs ft fcs Child Nothing Nothing Nothing Nothing pkFromRow :: (Text, Text, Text) -> PrimaryKey pkFromRow (s, t, n) = PrimaryKey s t n addParentRelation :: Relation -> [Relation] -> [Relation] -addParentRelation rel@(Relation s t c ft fc _ _ _ _) rels = Relation s ft fc t c Parent Nothing Nothing Nothing:rel:rels +addParentRelation rel@(Relation s t c fs ft fc _ _ _ _ _) rels = Relation fs ft fc s t c Parent Nothing Nothing Nothing Nothing:rel:rels -- allTables :: H.Tx P.Postgres s [Table] -- allTables = do @@ -135,9 +149,10 @@ allRelations :: H.Tx P.Postgres s [Relation] allRelations = do rels <- H.listEx $ [H.stmt| WITH table_fk AS ( - SELECT ns.nspname AS table_schema, + SELECT ns1.nspname AS table_schema, tab.relname AS table_name, column_info.cols AS columns, + ns2.nspname AS foreign_table_schema, other.relname AS foreign_table_name, column_info.refs AS foreign_columns FROM pg_constraint, @@ -152,10 +167,10 @@ allRelations = do WHERE attrelid = confrelid AND attnum = ref) AS refs) AS column_info, - LATERAL (SELECT * FROM pg_namespace - WHERE pg_namespace.oid = connamespace) AS ns, + LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = connamespace) AS ns1, LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab, - LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other + LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other, + LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = other.relnamespace) AS ns2 WHERE confrelid != 0 ORDER BY (conrelid, column_info.nums) ) @@ -167,33 +182,35 @@ allRelations = do vcu.table_schema, vcu.view_name AS table_name, array_agg(vcu.column_name::text) AS columns, + table_fk.foreign_table_schema, table_fk.foreign_table_name, table_fk.foreign_columns - FROM information_schema.view_column_usage as vcu + FROM information_schema.view_column_usage AS vcu JOIN table_fk ON table_fk.table_schema = vcu.view_schema AND table_fk.table_name = vcu.table_name AND vcu.column_name = ANY (table_fk.columns) WHERE vcu.view_schema NOT IN ('pg_catalog', 'information_schema') AND columns = table_fk.columns - GROUP BY vcu.table_schema, vcu.view_name, table_fk.foreign_table_name, table_fk.foreign_columns + GROUP BY vcu.table_schema, vcu.view_name, table_fk.foreign_table_schema, table_fk.foreign_table_name, table_fk.foreign_columns ) UNION ( SELECT - vcu.view_schema as table_schema, + table_fk.table_schema, table_fk.table_name, table_fk.columns, - vcu.view_name as foreign_table_name, - array_agg(vcu.column_name::text) as foreign_columns - FROM information_schema.view_column_usage as vcu + vcu.view_schema AS foreign_table_schema, + vcu.view_name AS foreign_table_name, + array_agg(vcu.column_name::text) AS foreign_columns + FROM information_schema.view_column_usage AS vcu JOIN table_fk ON table_fk.table_schema = vcu.view_schema AND table_fk.foreign_table_name = vcu.table_name AND vcu.column_name = ANY (table_fk.foreign_columns) WHERE vcu.view_schema NOT IN ('pg_catalog', 'information_schema') AND foreign_columns = table_fk.foreign_columns - GROUP BY vcu.view_schema, table_fk.table_name, vcu.view_name, table_fk.columns + GROUP BY table_fk.table_schema, table_fk.table_name, vcu.view_schema, vcu.view_name, table_fk.columns ) |] let simpleRelations = foldr (addParentRelation.relationFromRow) [] rels @@ -204,10 +221,10 @@ allRelations = do groupFn (Relation{relSchema=s, relTable=t}) = s<>"_"<>t combinations k ns = filter ((k==).length) (subsequences ns) link2Relation [ - Relation{relSchema=sc, relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c}, - Relation{ relColumns=lc2, relFTable=ft, relFColumns=fc} + Relation{relSchema=ls, relTable=lt, relColumns=lc1, relFSchema=s, relFTable=t, relFColumns=c}, + Relation{ relColumns=lc2, relFSchema=fs, relFTable=ft, relFColumns=fc} ] - | lc1 /= lc2 && length lc1 == 1 && length lc2 == 1 = Just $ Relation sc t c ft fc Many (Just lt) (Just lc1) (Just lc2) + | lc1 /= lc2 && length lc1 == 1 && length lc2 == 1 = Just $ Relation s t c fs ft fc Many (Just ls) (Just lt) (Just lc1) (Just lc2) | otherwise = Nothing link2Relation _ = Nothing @@ -264,7 +281,7 @@ allColumns rels = do lookupFn (Column{colSchema=cs, colTable=ct, colName=cn}) (Relation{relSchema=rs, relTable=rt, relColumns=rc, relType=rty}) = cs==rs && ct==rt && cn `elem` rc && rty==Child lookupFn _ _ = False - relToFk cName (Relation{relFTable=t, relColumns=cs, relFColumns=fcs}) = ForeignKey t <$> c + relToFk cName (Relation{relSchema=s, relFTable=t, relColumns=cs, relFColumns=fcs}) = ForeignKey s t <$> c where pos = elemIndex cName cs c = (fcs !!) <$> pos diff --git a/src/PostgREST/Main.hs b/src/PostgREST/Main.hs index 311e895cc..53cf2b917 100644 --- a/src/PostgREST/Main.hs +++ b/src/PostgREST/Main.hs @@ -2,14 +2,13 @@ module Main where import PostgREST.App --- import PostgREST.QueryBuilder import PostgREST.Config (AppConfig (..), minimumPgVersion, prettyVersion, readOptions) import PostgREST.Error (errResponse, PgError) import PostgREST.Middleware -import PostgREST.PgStructure +import PostgREST.DbStructure import PostgREST.Types import Control.Monad (unless) @@ -27,7 +26,6 @@ import Network.Wai.Middleware.RequestLogger (logStdout) import System.IO (BufferMode (..), hSetBuffering, stderr, stdin, stdout) --- import Data.Maybe (mapMaybe) isServerVersionSupported :: H.Session P.Postgres IO Bool isServerVersionSupported = do @@ -70,38 +68,12 @@ main = do <> show minimumPgVersion) ) supportedOrError - -- what was this code for? - -- roleOrError <- H.session pool $ do - -- Identity (role :: Text) <- H.tx Nothing $ H.singleEx - -- [H.stmt|SELECT SESSION_USER|] - -- return role - -- authenticator <- either hasqlError return roleOrError - let txSettings = Just (H.ReadCommitted, Just True) - metadata <- H.session pool $ H.tx txSettings $ do - rels <- allRelations - cols <- allColumns rels - keys <- allPrimaryKeys - return (rels, cols, keys) - - - dbstructure <- either hasqlError - (\(rels, cols, keys) -> - - return DbStructure { - columns=cols - , relations=rels - , primaryKeys=keys - } - ) metadata - - -- let allRels = relations dbstructure - -- fakeRels = mapMaybe (toSourceRelation "projects") allRels - -- - -- print $ findRelation (fakeRels ++ allRels) "test" "pg_source" "clients" + dbOrError <- H.session pool $ H.tx txSettings createDbStructure + db <- either hasqlError return dbOrError runSettings appSettings $ middle $ \ req respond -> do body <- strictRequestBody req resOrError <- liftIO $ H.session pool $ H.tx txSettings $ - runWithClaims conf (app dbstructure conf body) req + runWithClaims conf (app db conf body) req either (respond . errResponse) respond resOrError diff --git a/src/PostgREST/MainTest.hs b/src/PostgREST/MainTest.hs index 55ebec5bc..45326d33d 100644 --- a/src/PostgREST/MainTest.hs +++ b/src/PostgREST/MainTest.hs @@ -8,7 +8,7 @@ import PostgREST.Config (AppConfig (..), readOptions) import PostgREST.Error (errResponse, PgError) import PostgREST.Middleware -import PostgREST.PgStructure +import PostgREST.DbStructure import PostgREST.Types import Control.Monad (unless) @@ -94,7 +94,7 @@ main = do return (tabs, rels, cols, keys) - dbstructure <- either hasqlError + db <- either hasqlError (\(tabs, rels, cols, keys) -> return DbStructure { @@ -107,10 +107,10 @@ main = do runSettings appSettings $ middle $ \ req respond -> do body <- strictRequestBody req resOrError <- liftIO $ H.session pool $ H.tx txSettings $ - runWithClaims conf (app dbstructure conf body) req + runWithClaims conf (app db conf body) req either (respond . errResponse) respond resOrError - --let allRels = relations dbstructure + --let allRels = relations db -- links = join $ map (combinations 2) $ filter ((>=1).length) $ groupWith groupFn $ filter ( (==Child). relType) allRels -- combinations k ns = filter ((k==).length) (subsequences ns) diff --git a/src/PostgREST/PgQuery.hs b/src/PostgREST/PgQuery.hs index 9d66f6ecb..62cf96df7 100644 --- a/src/PostgREST/PgQuery.hs +++ b/src/PostgREST/PgQuery.hs @@ -281,7 +281,7 @@ pgFmtCondition table (Filter (col,jp) ops val) = _ -> "" valToStr v = case v of VText s -> pgFmtValue opCode s - VForeignKey (QualifiedIdentifier s _) (ForeignKey ft fc) -> pgFmtColumn qi fc + VForeignKey (QualifiedIdentifier s _) (ForeignKey _ ft fc) -> pgFmtColumn qi fc where qi = QualifiedIdentifier (if ft == sourceSubqueryName then "" else s) ft pgFmtColumn :: QualifiedIdentifier -> T.Text -> T.Text diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index cd04960fa..27799ac99 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -35,14 +35,14 @@ addRelations schema allRelations parentNode node@(Node n@(query, (table, _)) for updatedForest = mapM (addRelations schema allRelations (Just node)) forest getJoinConditions :: Relation -> [Filter] -getJoinConditions (Relation s t cs ft fcs typ lt lc1 lc2) = +getJoinConditions (Relation s t cs fs ft fcs typ ls lt lc1 lc2) = case typ of - Child -> zipWith (toFilter t ft) cs fcs - Parent -> zipWith (toFilter t ft) cs fcs - Many -> zipWith (toFilter t (fromMaybe "" lt)) cs (fromMaybe [] lc1) ++ zipWith (toFilter ft (fromMaybe "" lt)) fcs (fromMaybe [] lc2) + Child -> zipWith (toFilter t fs ft) cs fcs + Parent -> zipWith (toFilter t fs ft) cs fcs + Many -> zipWith (toFilter t (fromMaybe "" ls) (fromMaybe "" lt)) cs (fromMaybe [] lc1) ++ zipWith (toFilter ft (fromMaybe "" ls) (fromMaybe "" lt)) fcs (fromMaybe [] lc2) where - toFilter :: Text -> Text -> FieldName -> FieldName -> Filter - toFilter tb ftb c fc = Filter (c, Nothing) "=" (VForeignKey (QualifiedIdentifier s tb) (ForeignKey ftb fc)) + toFilter :: Text -> Text -> Text -> FieldName -> FieldName -> Filter + toFilter tb fsc ftb c fc = Filter (c, Nothing) "=" (VForeignKey (QualifiedIdentifier s tb) (ForeignKey fsc ftb fc)) addJoinConditions :: Text -> ApiRequest -> Either Text ApiRequest addJoinConditions schema (Node (query, (t, r)) forest) = @@ -55,7 +55,7 @@ addJoinConditions schema (Node (query, (t, r)) forest) = where q = addCond updatedQuery (getJoinConditions rel) qq = q{from=linkTable:from q} - _ -> Left "unknow relation" + _ -> Left "unknown relation" where -- add parentTable and parentJoinConditions to the query updatedQuery = foldr (flip addCond) (query{from = parentTables ++ from query}) parentJoinConditions diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 691efad99..a65835473 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -19,7 +19,9 @@ data Table = Table { } deriving (Show) data ForeignKey = ForeignKey { - fkTable::Text, fkCol::Text + fkSchema :: Text, + fkTable :: Text, + fkCol :: Text } deriving (Show, Eq) @@ -36,7 +38,7 @@ data Column = Column { , colDefault :: Maybe Text , colEnum :: [Text] , colFK :: Maybe ForeignKey -} | Star {colSchema :: Text, colTable :: Text } deriving (Show) +} | Star { colSchema :: Text, colTable :: Text } deriving (Show) data PrimaryKey = PrimaryKey { pkSchema::Text, pkTable::Text, pkName::Text @@ -56,13 +58,15 @@ data QualifiedIdentifier = QualifiedIdentifier { data RelationType = Child | Parent | Many deriving (Show, Eq) data Relation = Relation { - relSchema :: Text -, relTable :: Text + relSchema :: Text +, relTable :: Text , relColumns :: [Text] -, relFTable :: Text +, relFSchema :: Text +, relFTable :: Text , relFColumns :: [Text] -, relType :: RelationType -, relLTable :: Maybe Text +, relType :: RelationType +, relLSchema :: Maybe Text +, relLTable :: Maybe Text , relLCols1 :: Maybe [Text] , relLCols2 :: Maybe [Text] } deriving (Show, Eq) @@ -101,7 +105,7 @@ instance ToJSON Column where , "enum" .= colEnum c ] instance ToJSON ForeignKey where - toJSON fk = object ["table".=fkTable fk, "column".=fkCol fk] + toJSON fk = object ["schema".=fkSchema fk, "table".=fkTable fk, "column".=fkCol fk] instance ToJSON Table where toJSON v = object [ diff --git a/test/Feature/StructureSpec.hs b/test/Feature/StructureSpec.hs index 2a28f43db..148a1ca1b 100644 --- a/test/Feature/StructureSpec.hs +++ b/test/Feature/StructureSpec.hs @@ -175,6 +175,7 @@ spec = around withApp $ do }, { "references":{ + "schema":"test", "column":"id", "table":"auto_incrementing_pk" }, @@ -191,6 +192,7 @@ spec = around withApp $ do }, { "references":{ + "schema":"test", "column":"k", "table":"simple_pk" }, diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs index 45cbcbe96..dcc765f0d 100644 --- a/test/SpecHelper.hs +++ b/test/SpecHelper.hs @@ -30,8 +30,7 @@ import PostgREST.App (app) import PostgREST.Config (AppConfig(..)) import PostgREST.Middleware import PostgREST.Error(errResponse) -import PostgREST.PgStructure -import PostgREST.Types +import PostgREST.DbStructure dbString :: String dbString = "postgres://postgrest_test@localhost:5432/postgrest_test" @@ -55,25 +54,13 @@ withApp perform = do <- H.acquirePool pgSettings testPoolOpts let txSettings = Just (H.ReadCommitted, Just True) - metadata <- H.session pool $ H.tx txSettings $ do - rels <- allRelations - cols <- allColumns rels - keys <- allPrimaryKeys - return (rels, cols, keys) - - dbstructure <- case metadata of - Left e -> fail $ show e - Right (rels, cols, keys) -> - return DbStructure { - columns=cols - , relations=rels - , primaryKeys=keys - } + dbOrError <- H.session pool $ H.tx txSettings createDbStructure + db <- either (fail . show) return dbOrError perform $ middle $ \req resp -> do body <- strictRequestBody req result <- liftIO $ H.session pool $ H.tx txSettings - $ runWithClaims cfg (app dbstructure cfg body) req + $ runWithClaims cfg (app db cfg body) req either (resp . errResponse) resp result where middle = defaultMiddle diff --git a/test/Unit/PgStructureSpec.hx b/test/Unit/DbStructureSpec.hx similarity index 93% rename from test/Unit/PgStructureSpec.hx rename to test/Unit/DbStructureSpec.hx index b1c1570dd..6a8a9e4de 100644 --- a/test/Unit/PgStructureSpec.hx +++ b/test/Unit/DbStructureSpec.hx @@ -1,7 +1,7 @@ -module Unit.PgStructureSpec where +module Unit.DbStructureSpec where import Test.Hspec -import PgStructure (Table(..), tables, Column(..), columns, ForeignKey(..), +import DbStructure (Table(..), tables, Column(..), columns, ForeignKey(..), foreignKeys) import Database.HDBC (quickQuery) From 3a682360f33392665fb4093191727080103bb2e1 Mon Sep 17 00:00:00 2001 From: calebmer Date: Mon, 9 Nov 2015 17:50:56 -0500 Subject: [PATCH 02/12] Remove view relations from sql statement --- src/PostgREST/DbStructure.hs | 132 ++++++++++------------------------- 1 file changed, 37 insertions(+), 95 deletions(-) diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 911856202..3a8a36e48 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -148,70 +148,30 @@ tables schema = do allRelations :: H.Tx P.Postgres s [Relation] allRelations = do rels <- H.listEx $ [H.stmt| - WITH table_fk AS ( - SELECT ns1.nspname AS table_schema, - tab.relname AS table_name, - column_info.cols AS columns, - ns2.nspname AS foreign_table_schema, - other.relname AS foreign_table_name, - column_info.refs AS foreign_columns - FROM pg_constraint, - LATERAL (SELECT array_agg(cols.attname) AS cols, - array_agg(cols.attnum) AS nums, - array_agg(refs.attname) AS refs - FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k, - LATERAL (SELECT * FROM pg_attribute - WHERE attrelid = conrelid AND attnum = col) - AS cols, - LATERAL (SELECT * FROM pg_attribute - WHERE attrelid = confrelid AND attnum = ref) - AS refs) - AS column_info, - LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = connamespace) AS ns1, - LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab, - LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other, - LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = other.relnamespace) AS ns2 - WHERE confrelid != 0 - ORDER BY (conrelid, column_info.nums) - ) - - SELECT * FROM table_fk - UNION - ( - SELECT - vcu.table_schema, - vcu.view_name AS table_name, - array_agg(vcu.column_name::text) AS columns, - table_fk.foreign_table_schema, - table_fk.foreign_table_name, - table_fk.foreign_columns - FROM information_schema.view_column_usage AS vcu - JOIN table_fk ON - table_fk.table_schema = vcu.view_schema AND - table_fk.table_name = vcu.table_name AND - vcu.column_name = ANY (table_fk.columns) - WHERE vcu.view_schema NOT IN ('pg_catalog', 'information_schema') - AND columns = table_fk.columns - GROUP BY vcu.table_schema, vcu.view_name, table_fk.foreign_table_schema, table_fk.foreign_table_name, table_fk.foreign_columns - ) - UNION - ( - SELECT - table_fk.table_schema, - table_fk.table_name, - table_fk.columns, - vcu.view_schema AS foreign_table_schema, - vcu.view_name AS foreign_table_name, - array_agg(vcu.column_name::text) AS foreign_columns - FROM information_schema.view_column_usage AS vcu - JOIN table_fk ON - table_fk.table_schema = vcu.view_schema AND - table_fk.foreign_table_name = vcu.table_name AND - vcu.column_name = ANY (table_fk.foreign_columns) - WHERE vcu.view_schema NOT IN ('pg_catalog', 'information_schema') - AND foreign_columns = table_fk.foreign_columns - GROUP BY table_fk.table_schema, table_fk.table_name, vcu.view_schema, vcu.view_name, table_fk.columns - ) + SELECT ns1.nspname AS table_schema, + tab.relname AS table_name, + column_info.cols AS columns, + ns2.nspname AS foreign_table_schema, + other.relname AS foreign_table_name, + column_info.refs AS foreign_columns + FROM pg_constraint, + LATERAL (SELECT array_agg(cols.attname) AS cols, + array_agg(cols.attnum) AS nums, + array_agg(refs.attname) AS refs + FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k, + LATERAL (SELECT * FROM pg_attribute + WHERE attrelid = conrelid AND attnum = col) + AS cols, + LATERAL (SELECT * FROM pg_attribute + WHERE attrelid = confrelid AND attnum = ref) + AS refs) + AS column_info, + LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = connamespace) AS ns1, + LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab, + LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other, + LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = other.relnamespace) AS ns2 + WHERE confrelid != 0 + ORDER BY (conrelid, column_info.nums) |] let simpleRelations = foldr (addParentRelation.relationFromRow) [] rels links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==Child). relType) simpleRelations @@ -289,36 +249,18 @@ allColumns rels = do allPrimaryKeys :: H.Tx P.Postgres s [PrimaryKey] allPrimaryKeys = do pks <- H.listEx $ [H.stmt| - WITH table_pk AS ( - SELECT - kc.table_schema, - kc.table_name, - kc.column_name - FROM - information_schema.table_constraints tc, - information_schema.key_column_usage kc - WHERE - tc.constraint_type = 'PRIMARY KEY' AND - kc.table_name = tc.table_name AND - kc.table_schema = tc.table_schema AND - kc.constraint_name = tc.constraint_name AND - kc.table_schema NOT IN ('pg_catalog', 'information_schema') - ) - SELECT table_schema, - table_name, - column_name - FROM table_pk - UNION ( - SELECT - vcu.view_schema, - vcu.view_name, - vcu.column_name - FROM information_schema.view_column_usage AS vcu - JOIN - table_pk ON table_pk.table_schema = vcu.view_schema AND - table_pk.table_name = vcu.table_name AND - table_pk.column_name = vcu.column_name - WHERE vcu.view_schema NOT IN ('pg_catalog','information_schema') - ) + SELECT + kc.table_schema, + kc.table_name, + kc.column_name + FROM + information_schema.table_constraints tc, + information_schema.key_column_usage kc + WHERE + tc.constraint_type = 'PRIMARY KEY' AND + kc.table_name = tc.table_name AND + kc.table_schema = tc.table_schema AND + kc.constraint_name = tc.constraint_name AND + kc.table_schema NOT IN ('pg_catalog', 'information_schema') |] return $ map pkFromRow pks From e1e4fe6d5c0f4a889678566574c5fecb990b0d5f Mon Sep 17 00:00:00 2001 From: calebmer Date: Tue, 10 Nov 2015 18:49:30 -0500 Subject: [PATCH 03/12] Types reference each other --- src/PostgREST/App.hs | 16 +-- src/PostgREST/DbStructure.hs | 245 ++++++++++++++++------------------ src/PostgREST/PgQuery.hs | 3 +- src/PostgREST/QueryBuilder.hs | 26 ++-- src/PostgREST/Types.hs | 82 ++++++------ 5 files changed, 178 insertions(+), 194 deletions(-) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 2779c016d..f3f914109 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -163,9 +163,9 @@ app db conf reqBody req = allRels = relations db allCols = columns db allPrKeys = primaryKeys db - filterCol sc table (Column{colSchema=s, colTable=t}) = s==sc && table==t + filterCol sc table (Column{colTable=Table{tableSchema=s, tableName=t}}) = s==sc && table==t filterCol _ _ _ = False - filterPk sc table pk = sc == pkSchema pk && table == pkTable pk + filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk path = pathInfo req verb = requestMethod req hdrs = requestHeaders req @@ -289,7 +289,7 @@ convertJson v = (,) <$> (header <$> normalized) <*> (vals <$> normalized) a@(Array _) -> Right a _ -> Left invalidMsg -augumentRequestWithJoin :: Text -> [Relation] -> ApiRequest -> Either Text ApiRequest +augumentRequestWithJoin :: Schema -> [Relation] -> ApiRequest -> Either Text ApiRequest augumentRequestWithJoin schema allRels request = (first formatRelationError . addRelations schema allRels Nothing) request >>= addJoinConditions schema @@ -332,10 +332,10 @@ addFilter (path, flt) (Node rn forest) = where maybeNode = find ((name==).fst.snd.rootLabel) forst toSourceRelation :: Text -> Relation -> Maybe Relation -toSourceRelation mt r@(Relation _ t _ _ ft _ _ _ rt _ _) - | mt == t = Just $ r {relTable=sourceSubqueryName} - | mt == ft = Just $ r {relFTable=sourceSubqueryName} - | Just mt == rt = Just $ r {relLTable=Just sourceSubqueryName} +toSourceRelation mt r@(Relation t _ ft _ _ rt _ _) + | mt == tableName t = Just $ r {relTable=t {tableName=sourceSubqueryName}} + | mt == tableName ft = Just $ r {relFTable=t {tableName=sourceSubqueryName}} + | Just mt == (tableName <$> rt) = Just $ r {relLTable=(\tbl -> tbl {tableName=sourceSubqueryName}) <$> rt} | otherwise = Nothing data TableOptions = TableOptions { @@ -348,7 +348,7 @@ instance ToJSON TableOptions where "columns" .= tblOptcolumns t , "pkey" .= tblOptpkey t ] -parseRequest :: Text -> [Relation] -> NodeName -> Request -> BL.ByteString -> Either Text (Text, Text, Bool) +parseRequest :: Schema -> [Relation] -> NodeName -> Request -> BL.ByteString -> Either Text (Text, Text, Bool) parseRequest schema allRels rootTableName httpRequest reqBody = (,,) <$> selectQuery <*> (if method == "GET" then pure "" else mutateQuery) diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 3a8a36e48..6949a9412 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -3,13 +3,17 @@ {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeSynonymInstances #-} -module PostgREST.DbStructure where +module PostgREST.DbStructure ( + createDbStructure +, doesProcExist +, doesProcReturnJWT +) where import Control.Applicative import Control.Monad (join) import Data.Functor.Identity import Data.List (elemIndex, find, subsequences) -import Data.Maybe (fromMaybe, isJust, mapMaybe) +import Data.Maybe (fromMaybe, fromJust, isJust, mapMaybe) import Data.Monoid import Data.Text (Text, split) import qualified Hasql as H @@ -24,13 +28,13 @@ import Prelude createDbStructure :: H.Tx P.Postgres s DbStructure createDbStructure = do tabs <- allTables - rels <- allRelations - cols <- allColumns rels - keys <- allPrimaryKeys + cols <- allColumns tabs + rels <- allRelations tabs cols + keys <- allPrimaryKeys tabs return DbStructure { tables = tabs - , columns = cols + , columns = addForeignKeys rels cols , relations = rels , primaryKeys = keys } @@ -62,135 +66,79 @@ doesProcReturnJWT = doesProc [H.stmt| AND pg_catalog.pg_get_function_result(p.oid) like '%jwt_claims' |] -tableFromRow :: (Text, Text, Bool) -> Table -tableFromRow (s, n, i) = Table s n i +addForeignKeys :: [Relation] -> [Column] -> [Column] +addForeignKeys rels = map addFk + where + addFk col = col { colFK = fk col } + fk col = join $ relToFk col <$> find (lookupFn col) rels + lookupFn :: Column -> Relation -> Bool + lookupFn c (Relation{relColumns=cs, relType=rty}) = c `elem` cs && rty==Child + -- lookupFn _ _ = False + relToFk col (Relation{relColumns=cols, relFColumns=colsF}) = ForeignKey <$> colF + where + pos = elemIndex col cols + colF = (colsF !!) <$> pos -columnFromRow :: (Text, Text, Text, +columnFromRow :: [Table] -> + (Text, Text, Text, Int, Bool, Text, Bool, Maybe Int, Maybe Int, Maybe Text, Maybe Text) - -> Column -columnFromRow (s, t, n, pos, nul, typ, u, l, p, d, e) = - Column s t n pos nul typ u l p d (parseEnum e) Nothing - + -> Column +columnFromRow tabs (s, t, n, pos, nul, typ, u, l, p, d, e) = + Column table n pos nul typ u l p d (parseEnum e) Nothing where + table = fromJust $ find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs parseEnum :: Maybe Text -> [Text] parseEnum str = fromMaybe [] $ split (==',') <$> str -relationFromRow :: (Text, Text, [Text], Text, Text, [Text]) -> Relation -relationFromRow (s, t, cs, fs, ft, fcs) = Relation s t cs fs ft fcs Child Nothing Nothing Nothing Nothing +relationFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Relation +relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) = Relation table cols tableF colsF Child Nothing Nothing Nothing + where + findTable s t = fromJust $ find (\tbl -> tableSchema tbl == s && tableName tbl == t) allTabs + findCols s t cs = filter (\col -> tableSchema (colTable col) == s && tableName (colTable col) == t && colName col `elem` cs) allCols + table = findTable rs rt + tableF = findTable frs frt + cols = findCols rs rt rcs + colsF = findCols frs frt frcs -pkFromRow :: (Text, Text, Text) -> PrimaryKey -pkFromRow (s, t, n) = PrimaryKey s t n +pkFromRow :: [Table] -> (Schema, Text, Text) -> PrimaryKey +pkFromRow tabs (s, t, n) = PrimaryKey table n + where + table = fromJust $ find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs addParentRelation :: Relation -> [Relation] -> [Relation] -addParentRelation rel@(Relation s t c fs ft fc _ _ _ _ _) rels = Relation fs ft fc s t c Parent Nothing Nothing Nothing Nothing:rel:rels +addParentRelation rel@(Relation t c ft fc _ _ _ _) rels = Relation ft fc t c Parent Nothing Nothing Nothing : rel : rels --- allTables :: H.Tx P.Postgres s [Table] --- allTables = do --- rows <- H.listEx $ [H.stmt| --- SELECT --- n.nspname AS table_schema, --- c.relname AS table_name, --- c.relkind = 'r' OR (c.relkind IN ('v','f')) --- AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8 --- OR (EXISTS --- ( SELECT 1 --- FROM pg_trigger --- WHERE pg_trigger.tgrelid = c.oid --- AND (pg_trigger.tgtype::integer & 69) = 69) ) AS insertable, --- array_to_string(array_agg(r.rolname), ',') AS acl --- FROM pg_class c --- CROSS JOIN pg_roles r --- JOIN pg_namespace n ON n.oid = c.relnamespace --- WHERE c.relkind IN ('v','r','m') --- AND n.nspname NOT IN ('pg_catalog', 'information_schema') --- AND ( --- pg_has_role(r.rolname, c.relowner, 'USAGE'::text) OR --- has_table_privilege(r.rolname, c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR --- has_any_column_privilege(r.rolname, c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text) ) --- --- GROUP BY table_schema, table_name, insertable --- ORDER BY table_schema, table_name --- |] --- return $ map tableFromRow rows +allTables :: H.Tx P.Postgres s [Table] +allTables = do + rows <- H.listEx $ [H.stmt| + SELECT + n.nspname AS table_schema, + c.relname AS table_name, + c.relkind = 'r' OR (c.relkind IN ('v','f')) + AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8 + OR (EXISTS + ( SELECT 1 + FROM pg_trigger + WHERE pg_trigger.tgrelid = c.oid + AND (pg_trigger.tgtype::integer & 69) = 69) ) AS insertable + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v','r','m') + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + GROUP BY table_schema, table_name, insertable + ORDER BY table_schema, table_name; + |] + return $ map tableFromRow rows -tables :: Text -> H.Tx P.Postgres s [Table] -tables schema = do - rows <- H.listEx $ - [H.stmt| - select - n.nspname as table_schema, - relname as table_name, - c.relkind = 'r' or (c.relkind IN ('v', 'f')) and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8 - or (exists ( - select 1 - from pg_trigger - where pg_trigger.tgrelid = c.oid and (pg_trigger.tgtype::integer & 69) = 69) - ) as insertable - from - pg_class c - join pg_namespace n on n.oid = c.relnamespace - where - c.relkind in ('v', 'r', 'm') - and n.nspname = ? - and ( - pg_has_role(c.relowner, 'USAGE'::text) - or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) - or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text) - ) - order by relname - |] schema - return $ map tableFromRow rows +tableFromRow :: (Text, Text, Bool) -> Table +tableFromRow (s, n, i) = Table s n i -allRelations :: H.Tx P.Postgres s [Relation] -allRelations = do - rels <- H.listEx $ [H.stmt| - SELECT ns1.nspname AS table_schema, - tab.relname AS table_name, - column_info.cols AS columns, - ns2.nspname AS foreign_table_schema, - other.relname AS foreign_table_name, - column_info.refs AS foreign_columns - FROM pg_constraint, - LATERAL (SELECT array_agg(cols.attname) AS cols, - array_agg(cols.attnum) AS nums, - array_agg(refs.attname) AS refs - FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k, - LATERAL (SELECT * FROM pg_attribute - WHERE attrelid = conrelid AND attnum = col) - AS cols, - LATERAL (SELECT * FROM pg_attribute - WHERE attrelid = confrelid AND attnum = ref) - AS refs) - AS column_info, - LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = connamespace) AS ns1, - LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab, - LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other, - LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = other.relnamespace) AS ns2 - WHERE confrelid != 0 - ORDER BY (conrelid, column_info.nums) - |] - let simpleRelations = foldr (addParentRelation.relationFromRow) [] rels - links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==Child). relType) simpleRelations - return $ simpleRelations ++ mapMaybe link2Relation links - where - groupFn :: Relation -> Text - groupFn (Relation{relSchema=s, relTable=t}) = s<>"_"<>t - combinations k ns = filter ((k==).length) (subsequences ns) - link2Relation [ - Relation{relSchema=ls, relTable=lt, relColumns=lc1, relFSchema=s, relFTable=t, relFColumns=c}, - Relation{ relColumns=lc2, relFSchema=fs, relFTable=ft, relFColumns=fc} - ] - | lc1 /= lc2 && length lc1 == 1 && length lc2 == 1 = Just $ Relation s t c fs ft fc Many (Just ls) (Just lt) (Just lc1) (Just lc2) - | otherwise = Nothing - link2Relation _ = Nothing - - -allColumns :: [Relation] -> H.Tx P.Postgres s [Column] -allColumns rels = do +allColumns :: [Table] -> H.Tx P.Postgres s [Column] +allColumns tabs = do cols <- H.listEx $ [H.stmt| SELECT DISTINCT info.table_schema AS schema, @@ -232,22 +180,53 @@ allColumns rels = do ) AS enum_info ON (info.udt_name = enum_info.n) ORDER BY schema, position |] - return $ map (addFK . columnFromRow) cols + return $ map (columnFromRow tabs) cols +allRelations :: [Table] -> [Column] -> H.Tx P.Postgres s [Relation] +allRelations tabs cols = do + rels <- H.listEx $ [H.stmt| + SELECT ns1.nspname AS table_schema, + tab.relname AS table_name, + column_info.cols AS columns, + ns2.nspname AS foreign_table_schema, + other.relname AS foreign_table_name, + column_info.refs AS foreign_columns + FROM pg_constraint, + LATERAL (SELECT array_agg(cols.attname) AS cols, + array_agg(cols.attnum) AS nums, + array_agg(refs.attname) AS refs + FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k, + LATERAL (SELECT * FROM pg_attribute + WHERE attrelid = conrelid AND attnum = col) + AS cols, + LATERAL (SELECT * FROM pg_attribute + WHERE attrelid = confrelid AND attnum = ref) + AS refs) + AS column_info, + LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = connamespace) AS ns1, + LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab, + LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other, + LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = other.relnamespace) AS ns2 + WHERE confrelid != 0 + ORDER BY (conrelid, column_info.nums) + |] + let simpleRelations = foldr (addParentRelation . relationFromRow tabs cols) [] rels + links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==Child). relType) simpleRelations + return $ simpleRelations ++ mapMaybe link2Relation links where - addFK col = col { colFK = fk col } - fk col = join $ relToFk (colName col) <$> find (lookupFn col) rels - lookupFn :: Column -> Relation -> Bool - lookupFn (Column{colSchema=cs, colTable=ct, colName=cn}) (Relation{relSchema=rs, relTable=rt, relColumns=rc, relType=rty}) = - cs==rs && ct==rt && cn `elem` rc && rty==Child - lookupFn _ _ = False - relToFk cName (Relation{relSchema=s, relFTable=t, relColumns=cs, relFColumns=fcs}) = ForeignKey s t <$> c - where - pos = elemIndex cName cs - c = (fcs !!) <$> pos + groupFn :: Relation -> Text + groupFn (Relation{relTable=Table{tableSchema=s, tableName=t}}) = s<>"_"<>t + combinations k ns = filter ((k==).length) (subsequences ns) + link2Relation [ + Relation{relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c}, + Relation{ relColumns=lc2, relFTable=ft, relFColumns=fc} + ] + | lc1 /= lc2 && length lc1 == 1 && length lc2 == 1 = Just $ Relation t c ft fc Many (Just lt) (Just lc1) (Just lc2) + | otherwise = Nothing + link2Relation _ = Nothing -allPrimaryKeys :: H.Tx P.Postgres s [PrimaryKey] -allPrimaryKeys = do +allPrimaryKeys :: [Table] -> H.Tx P.Postgres s [PrimaryKey] +allPrimaryKeys tabs = do pks <- H.listEx $ [H.stmt| SELECT kc.table_schema, @@ -263,4 +242,4 @@ allPrimaryKeys = do kc.constraint_name = tc.constraint_name AND kc.table_schema NOT IN ('pg_catalog', 'information_schema') |] - return $ map pkFromRow pks + return $ map (pkFromRow tabs) pks diff --git a/src/PostgREST/PgQuery.hs b/src/PostgREST/PgQuery.hs index 62cf96df7..ebf68adea 100644 --- a/src/PostgREST/PgQuery.hs +++ b/src/PostgREST/PgQuery.hs @@ -281,8 +281,9 @@ pgFmtCondition table (Filter (col,jp) ops val) = _ -> "" valToStr v = case v of VText s -> pgFmtValue opCode s - VForeignKey (QualifiedIdentifier s _) (ForeignKey _ ft fc) -> pgFmtColumn qi fc + VForeignKey (QualifiedIdentifier s _) (ForeignKey Column{colTable=Table{tableName=ft}, colName=fc}) -> pgFmtColumn qi fc where qi = QualifiedIdentifier (if ft == sourceSubqueryName then "" else s) ft + _ -> "" pgFmtColumn :: QualifiedIdentifier -> T.Text -> T.Text pgFmtColumn table "*" = fromQi table <> ".*" diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 27799ac99..679524e3e 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -7,7 +7,7 @@ import Control.Error import Data.List (find) import Data.Monoid import Data.Text hiding (filter, find, foldr, head, last, map, - null, zipWith) + null, zipWith, concatMap) import Control.Applicative import Data.Tree import PostgREST.PgQuery (fromQi, pgFmtCondition, pgFmtSelectItem, @@ -16,11 +16,11 @@ import PostgREST.PgQuery (fromQi, pgFmtCondition, pgFmtSelectItem, import PostgREST.Types import qualified Data.Map as M -findRelation :: [Relation] -> Text -> Text -> Text -> Maybe Relation +findRelation :: [Relation] -> Schema -> Text -> Text -> Maybe Relation findRelation allRelations s t1 t2 = - find (\r -> s == relSchema r && t1 == relTable r && t2 == relFTable r) allRelations + find (\r -> s == (tableSchema . relTable) r && t1 == (tableName . relTable) r && t2 == (tableName . relFTable) r) allRelations -addRelations :: Text -> [Relation] -> Maybe ApiRequest -> ApiRequest -> Either Text ApiRequest +addRelations :: Schema -> [Relation] -> Maybe ApiRequest -> ApiRequest -> Either Text ApiRequest addRelations schema allRelations parentNode node@(Node n@(query, (table, _)) forest) = case parentNode of Nothing -> Node (query, (table, Nothing)) <$> updatedForest @@ -35,14 +35,14 @@ addRelations schema allRelations parentNode node@(Node n@(query, (table, _)) for updatedForest = mapM (addRelations schema allRelations (Just node)) forest getJoinConditions :: Relation -> [Filter] -getJoinConditions (Relation s t cs fs ft fcs typ ls lt lc1 lc2) = +getJoinConditions (Relation t cs ft fcs typ _ lc1 lc2) = case typ of - Child -> zipWith (toFilter t fs ft) cs fcs - Parent -> zipWith (toFilter t fs ft) cs fcs - Many -> zipWith (toFilter t (fromMaybe "" ls) (fromMaybe "" lt)) cs (fromMaybe [] lc1) ++ zipWith (toFilter ft (fromMaybe "" ls) (fromMaybe "" lt)) fcs (fromMaybe [] lc2) + Child -> zipWith (toFilter t) cs fcs + Parent -> zipWith (toFilter t) cs fcs + Many -> zipWith (toFilter t) cs (fromMaybe [] lc1) ++ zipWith (toFilter ft) fcs (fromMaybe [] lc2) where - toFilter :: Text -> Text -> Text -> FieldName -> FieldName -> Filter - toFilter tb fsc ftb c fc = Filter (c, Nothing) "=" (VForeignKey (QualifiedIdentifier s tb) (ForeignKey fsc ftb fc)) + toFilter :: Table -> Column -> Column -> Filter + toFilter tb c fc = Filter (colName c, Nothing) "=" (VForeignKey (QualifiedIdentifier (tableSchema tb) (tableName tb)) (ForeignKey fc)) addJoinConditions :: Text -> ApiRequest -> Either Text ApiRequest addJoinConditions schema (Node (query, (t, r)) forest) = @@ -54,15 +54,15 @@ addJoinConditions schema (Node (query, (t, r)) forest) = Node (qq, (t, r)) <$> updatedForest where q = addCond updatedQuery (getJoinConditions rel) - qq = q{from=linkTable:from q} + qq = q{from=tableName linkTable : from q} _ -> Left "unknown relation" where -- add parentTable and parentJoinConditions to the query updatedQuery = foldr (flip addCond) (query{from = parentTables ++ from query}) parentJoinConditions where - parentJoinConditions = map (getJoinConditions.snd) parents + parentJoinConditions = map (getJoinConditions . snd) parents parentTables = map fst parents - parents = mapMaybe (getParents.rootLabel) forest + parents = mapMaybe (getParents . rootLabel) forest getParents (_, (tbl, Just rel@(Relation{relType=Parent}))) = Just (tbl, rel) getParents _ = Nothing updatedForest = mapM (addJoinConditions schema) forest diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index a65835473..b7f60606f 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -9,66 +9,62 @@ data DbStructure = DbStructure { columns :: [Column] , relations :: [Relation] , primaryKeys :: [PrimaryKey] -} +} deriving (Show, Eq) +type Schema = Text data Table = Table { - tableSchema :: Text -, tableName :: Text + tableSchema :: Schema +, tableName :: Text , tableInsertable :: Bool } deriving (Show) -data ForeignKey = ForeignKey { - fkSchema :: Text, - fkTable :: Text, - fkCol :: Text -} deriving (Show, Eq) +data ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq) - -data Column = Column { - colSchema :: Text -, colTable :: Text -, colName :: Text -, colPosition :: Int -, colNullable :: Bool -, colType :: Text -, colUpdatable :: Bool -, colMaxLen :: Maybe Int -, colPrecision :: Maybe Int -, colDefault :: Maybe Text -, colEnum :: [Text] -, colFK :: Maybe ForeignKey -} | Star { colSchema :: Text, colTable :: Text } deriving (Show) +data Column = + Column { + colTable :: Table + , colName :: Text + , colPosition :: Int + , colNullable :: Bool + , colType :: Text + , colUpdatable :: Bool + , colMaxLen :: Maybe Int + , colPrecision :: Maybe Int + , colDefault :: Maybe Text + , colEnum :: [Text] + , colFK :: Maybe ForeignKey + } + | Star { colTable :: Table } + deriving (Show, Eq) data PrimaryKey = PrimaryKey { - pkSchema::Text, pkTable::Text, pkName::Text -} + pkTable :: Table + , pkName :: Text +} deriving (Show, Eq) data OrderTerm = OrderTerm { - otTerm :: Text + otTerm :: Text , otDirection :: BS.ByteString , otNullOrder :: Maybe BS.ByteString } deriving (Show, Eq) data QualifiedIdentifier = QualifiedIdentifier { - qiSchema :: Text + qiSchema :: Schema , qiName :: Text } deriving (Show, Eq) data RelationType = Child | Parent | Many deriving (Show, Eq) data Relation = Relation { - relSchema :: Text -, relTable :: Text -, relColumns :: [Text] -, relFSchema :: Text -, relFTable :: Text -, relFColumns :: [Text] + relTable :: Table +, relColumns :: [Column] +, relFTable :: Table +, relFColumns :: [Column] , relType :: RelationType -, relLSchema :: Maybe Text -, relLTable :: Maybe Text -, relLCols1 :: Maybe [Text] -, relLCols2 :: Maybe [Text] +, relLTable :: Maybe Table +, relLCols1 :: Maybe [Column] +, relLCols2 :: Maybe [Column] } deriving (Show, Eq) @@ -92,7 +88,7 @@ type ApiRequest = Tree ApiNode instance ToJSON Column where toJSON c = object [ - "schema" .= colSchema c + "schema" .= tableSchema t , "name" .= colName c , "position" .= colPosition c , "nullable" .= colNullable c @@ -103,9 +99,17 @@ instance ToJSON Column where , "references".= colFK c , "default" .= colDefault c , "enum" .= colEnum c ] + where + t = colTable c instance ToJSON ForeignKey where - toJSON fk = object ["schema".=fkSchema fk, "table".=fkTable fk, "column".=fkCol fk] + toJSON fk = object [ + "schema" .= tableSchema t + , "table" .= tableName t + , "column" .= colName c ] + where + c = fkCol fk + t = colTable c instance ToJSON Table where toJSON v = object [ From 16af8fc61edb9f27d92dc9cdfcfa4041d3078976 Mon Sep 17 00:00:00 2001 From: calebmer Date: Wed, 11 Nov 2015 07:51:35 -0500 Subject: [PATCH 04/12] Better relations for views - Better column synonyms detection - Raise relations to accessible schema when possible --- src/PostgREST/DbStructure.hs | 185 +++++++++++++++++++++++--------- src/PostgREST/Main.hs | 3 +- src/PostgREST/Types.hs | 15 ++- test/Feature/StructureSpec.hs | 196 +++++++++++++++++++--------------- test/SpecHelper.hs | 2 +- test/fixtures/schema.sql | 68 ++++++------ 6 files changed, 285 insertions(+), 184 deletions(-) diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 6949a9412..91d728c64 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -10,33 +10,38 @@ module PostgREST.DbStructure ( ) where import Control.Applicative -import Control.Monad (join) +import Control.Monad (join) import Data.Functor.Identity -import Data.List (elemIndex, find, subsequences) -import Data.Maybe (fromMaybe, fromJust, isJust, mapMaybe) +import Data.List (elemIndex, find, subsequences, sort, transpose) +import Data.Maybe (fromMaybe, fromJust, isJust, mapMaybe, listToMaybe) import Data.Monoid -import Data.Text (Text, split) -import qualified Hasql as H -import qualified Hasql.Postgres as P -import qualified Hasql.Backend as B -import PostgREST.PgQuery () +import Data.Text (Text, split) +import qualified Hasql as H +import qualified Hasql.Postgres as P +import qualified Hasql.Backend as B +import PostgREST.PgQuery () import PostgREST.Types -import GHC.Exts (groupWith) +import GHC.Exts (groupWith) import Prelude -createDbStructure :: H.Tx P.Postgres s DbStructure -createDbStructure = do +createDbStructure :: Schema -> H.Tx P.Postgres s DbStructure +createDbStructure schema = do tabs <- allTables cols <- allColumns tabs + syns <- allSynonyms cols rels <- allRelations tabs cols keys <- allPrimaryKeys tabs + let rels' = (manyToManyRelations . raiseRelations schema syns . parentRelations . synonymousRelations syns) rels + cols' = addForeignKeys rels' cols + keys' = synonymousPrimaryKeys syns keys + return DbStructure { tables = tabs - , columns = addForeignKeys rels cols - , relations = rels - , primaryKeys = keys + , columns = cols' + , relations = rels' + , primaryKeys = keys' } doesProc :: forall c s. B.CxValue c Int => @@ -66,6 +71,16 @@ doesProcReturnJWT = doesProc [H.stmt| AND pg_catalog.pg_get_function_result(p.oid) like '%jwt_claims' |] +synonymousColumns :: [(Column,Column)] -> [Column] -> [[Column]] +synonymousColumns allSyns cols = synCols' + where + syns = sort $ filter ((== colTable (head cols)) . colTable . fst) allSyns + synCols  = transpose $ map (\c -> map snd $ filter ((== c) . fst) syns) cols + synCols' = (filter sameTable . filter matchLength) synCols + matchLength cs = length cols == length cs + sameTable (c:cs) = all (\cc -> colTable c == colTable cc) (c:cs) + sameTable [] = False + addForeignKeys :: [Relation] -> [Column] -> [Column] addForeignKeys rels = map addFk where @@ -79,38 +94,52 @@ addForeignKeys rels = map addFk pos = elemIndex col cols colF = (colsF !!) <$> pos -columnFromRow :: [Table] -> - (Text, Text, Text, - Int, Bool, Text, - Bool, Maybe Int, Maybe Int, - Maybe Text, Maybe Text) - -> Column -columnFromRow tabs (s, t, n, pos, nul, typ, u, l, p, d, e) = - Column table n pos nul typ u l p d (parseEnum e) Nothing +synonymousRelations :: [(Column,Column)] -> [Relation] -> [Relation] +synonymousRelations _ [] = [] +synonymousRelations syns (rel:rels) = rel : synRelsP ++ synRelsF ++ synonymousRelations syns rels where - table = fromJust $ find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs - parseEnum :: Maybe Text -> [Text] - parseEnum str = fromMaybe [] $ split (==',') <$> str + synRelsP = synRels (relColumns rel) (\t cs -> rel{relTable=t,relColumns=cs}) + synRelsF = synRels (relFColumns rel) (\t cs -> rel{relFTable=t,relFColumns=cs}) + synRels cols mapFn = map (\cs -> mapFn (colTable $ head cs) cs) $ synonymousColumns syns cols +parentRelations :: [Relation] -> [Relation] +parentRelations [] = [] +parentRelations (rel@(Relation t c ft fc _ _ _ _):rels) = Relation ft fc t c Parent Nothing Nothing Nothing : rel : parentRelations rels -relationFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Relation -relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) = Relation table cols tableF colsF Child Nothing Nothing Nothing +manyToManyRelations :: [Relation] -> [Relation] +manyToManyRelations rels = rels ++ mapMaybe link2Relation links where - findTable s t = fromJust $ find (\tbl -> tableSchema tbl == s && tableName tbl == t) allTabs - findCols s t cs = filter (\col -> tableSchema (colTable col) == s && tableName (colTable col) == t && colName col `elem` cs) allCols - table = findTable rs rt - tableF = findTable frs frt - cols = findCols rs rt rcs - colsF = findCols frs frt frcs + links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==Child). relType) rels + groupFn :: Relation -> Text + groupFn (Relation{relTable=Table{tableSchema=s, tableName=t}}) = s<>"_"<>t + combinations k ns = filter ((k==).length) (subsequences ns) + link2Relation [ + Relation{relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c}, + Relation{ relColumns=lc2, relFTable=ft, relFColumns=fc} + ] + | lc1 /= lc2 && length lc1 == 1 && length lc2 == 1 = Just $ Relation t c ft fc Many (Just lt) (Just lc1) (Just lc2) + | otherwise = Nothing + link2Relation _ = Nothing -pkFromRow :: [Table] -> (Schema, Text, Text) -> PrimaryKey -pkFromRow tabs (s, t, n) = PrimaryKey table n +raiseRelations :: Schema -> [(Column,Column)] -> [Relation] -> [Relation] +raiseRelations schema syns = map raiseRel where - table = fromJust $ find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs + raiseRel rel + | tableSchema table == schema = rel + | isJust newCols = rel{relFTable=fromJust newTable,relFColumns=fromJust newCols} + | otherwise = rel + where + cols = relFColumns rel + table = relFTable rel + newCols = listToMaybe $ filter ((== schema) . tableSchema . colTable . head) (synonymousColumns syns cols) + newTable = (colTable . head) <$> newCols - -addParentRelation :: Relation -> [Relation] -> [Relation] -addParentRelation rel@(Relation t c ft fc _ _ _ _) rels = Relation ft fc t c Parent Nothing Nothing Nothing : rel : rels +synonymousPrimaryKeys :: [(Column,Column)] -> [PrimaryKey] -> [PrimaryKey] +synonymousPrimaryKeys _ [] = [] +synonymousPrimaryKeys syns (key:keys) = key : newKeys ++ synonymousPrimaryKeys syns keys + where + keySyns = filter ((\c -> colTable c == pkTable key && colName c == pkName key) . fst) syns + newKeys = map ((\c -> PrimaryKey{pkTable=colTable c,pkName=colName c}) . snd) keySyns allTables :: H.Tx P.Postgres s [Table] allTables = do @@ -182,6 +211,19 @@ allColumns tabs = do |] return $ map (columnFromRow tabs) cols +columnFromRow :: [Table] -> + (Text, Text, Text, + Int, Bool, Text, + Bool, Maybe Int, Maybe Int, + Maybe Text, Maybe Text) + -> Column +columnFromRow tabs (s, t, n, pos, nul, typ, u, l, p, d, e) = + Column table n pos nul typ u l p d (parseEnum e) Nothing + where + table = fromJust $ find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs + parseEnum :: Maybe Text -> [Text] + parseEnum str = fromMaybe [] $ split (==',') <$> str + allRelations :: [Table] -> [Column] -> H.Tx P.Postgres s [Relation] allRelations tabs cols = do rels <- H.listEx $ [H.stmt| @@ -210,20 +252,17 @@ allRelations tabs cols = do WHERE confrelid != 0 ORDER BY (conrelid, column_info.nums) |] - let simpleRelations = foldr (addParentRelation . relationFromRow tabs cols) [] rels - links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==Child). relType) simpleRelations - return $ simpleRelations ++ mapMaybe link2Relation links + return $ map (relationFromRow tabs cols) rels + +relationFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Relation +relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) = Relation table cols tableF colsF Child Nothing Nothing Nothing where - groupFn :: Relation -> Text - groupFn (Relation{relTable=Table{tableSchema=s, tableName=t}}) = s<>"_"<>t - combinations k ns = filter ((k==).length) (subsequences ns) - link2Relation [ - Relation{relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c}, - Relation{ relColumns=lc2, relFTable=ft, relFColumns=fc} - ] - | lc1 /= lc2 && length lc1 == 1 && length lc2 == 1 = Just $ Relation t c ft fc Many (Just lt) (Just lc1) (Just lc2) - | otherwise = Nothing - link2Relation _ = Nothing + findTable s t = fromJust $ find (\tbl -> tableSchema tbl == s && tableName tbl == t) allTabs + findCols s t cs = filter (\col -> tableSchema (colTable col) == s && tableName (colTable col) == t && colName col `elem` cs) allCols + table = findTable rs rt + tableF = findTable frs frt + cols = findCols rs rt rcs + colsF = findCols frs frt frcs allPrimaryKeys :: [Table] -> H.Tx P.Postgres s [PrimaryKey] allPrimaryKeys tabs = do @@ -243,3 +282,45 @@ allPrimaryKeys tabs = do kc.table_schema NOT IN ('pg_catalog', 'information_schema') |] return $ map (pkFromRow tabs) pks + +pkFromRow :: [Table] -> (Schema, Text, Text) -> PrimaryKey +pkFromRow tabs (s, t, n) = PrimaryKey table n + where + table = fromJust $ find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs + +allSynonyms :: [Column] -> H.Tx P.Postgres s [(Column,Column)] +allSynonyms allCols = do + srcSyns <- H.listEx $ [H.stmt| + WITH synonyms AS ( + SELECT + vcu.table_schema AS src_table_schema, + vcu.table_name AS src_table_name, + vcu.column_name AS src_column_name, + view.table_schema AS syn_table_schema, + view.table_name AS syn_table_name, + view.view_definition AS view_definition + FROM + information_schema.views AS view, + information_schema.view_column_usage AS vcu + WHERE + view.table_schema = vcu.view_schema AND + view.table_name = vcu.view_name AND + view.table_schema NOT IN ('pg_catalog', 'information_schema') AND + (SELECT COUNT(*) FROM information_schema.view_table_usage WHERE view_schema = view.table_schema AND view_name = view.table_name) = 1 + ) + SELECT + src_table_schema, src_table_name, src_column_name, + syn_table_schema, syn_table_name, + (regexp_matches(view_definition, CONCAT('\.(', src_column_name, ')(?=,|$)'), 'gn'))[1] + FROM synonyms + UNION ( + SELECT + src_table_schema, src_table_name, src_column_name, + syn_table_schema, syn_table_name, + (regexp_matches(view_definition, CONCAT('\.', src_column_name, '\sAS\s("?)(.+?)\1(,|$)'), 'gn'))[2] /* " <- for syntax highlighting */ + FROM synonyms + ) + |] + return $ map (\(a,b,c,d,e,f) -> (findCol a b c,findCol d e f)) srcSyns + where + findCol s t c = fromJust $ find (\col -> (tableSchema . colTable) col == s && (tableName . colTable) col == t && colName col == c) allCols diff --git a/src/PostgREST/Main.hs b/src/PostgREST/Main.hs index 53cf2b917..948cb875f 100644 --- a/src/PostgREST/Main.hs +++ b/src/PostgREST/Main.hs @@ -9,7 +9,6 @@ import PostgREST.Config (AppConfig (..), import PostgREST.Error (errResponse, PgError) import PostgREST.Middleware import PostgREST.DbStructure -import PostgREST.Types import Control.Monad (unless) import Control.Monad.IO.Class (liftIO) @@ -69,7 +68,7 @@ main = do ) supportedOrError let txSettings = Just (H.ReadCommitted, Just True) - dbOrError <- H.session pool $ H.tx txSettings createDbStructure + dbOrError <- H.session pool $ H.tx txSettings $ createDbStructure (cs $ configSchema conf) db <- either hasqlError return dbOrError runSettings appSettings $ middle $ \ req respond -> do diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index b7f60606f..9bb4d4cab 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -17,9 +17,9 @@ data Table = Table { tableSchema :: Schema , tableName :: Text , tableInsertable :: Bool -} deriving (Show) +} deriving (Show, Ord) -data ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq) +data ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq, Ord) data Column = Column { @@ -36,7 +36,9 @@ data Column = , colFK :: Maybe ForeignKey } | Star { colTable :: Table } - deriving (Show, Eq) + deriving (Show, Ord) + +type Synonym = (Column,Column) data PrimaryKey = PrimaryKey { pkTable :: Table @@ -116,3 +118,10 @@ instance ToJSON Table where "schema" .= tableSchema v , "name" .= tableName v , "insertable" .= tableInsertable v ] + +instance Eq Table where + Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2 + +instance Eq Column where + Column{colTable=t1,colName=n1} == Column{colTable=t2,colName=n2} = t1 == t2 && n1 == n2 + _ == _ = False diff --git a/test/Feature/StructureSpec.hs b/test/Feature/StructureSpec.hs index 148a1ca1b..339a36044 100644 --- a/test/Feature/StructureSpec.hs +++ b/test/Feature/StructureSpec.hs @@ -14,7 +14,9 @@ spec = around withApp $ do it "lists views in schema" $ request methodGet "/" [] "" `shouldRespondWith` [json| [ - {"schema":"test","name":"auto_incrementing_pk","insertable":true} + {"schema":"test","name":"articleStars","insertable":true} + , {"schema":"test","name":"articles","insertable":true} + , {"schema":"test","name":"auto_incrementing_pk","insertable":true} , {"schema":"test","name":"clients","insertable":true} , {"schema":"test","name":"comments","insertable":true} , {"schema":"test","name":"complex_items","insertable":true} @@ -48,7 +50,6 @@ spec = around withApp $ do ] |] {matchStatus = 200} - describe "Table info" $ do it "is available with OPTIONS verb" $ request methodOptions "/menagerie" [] "" `shouldRespondWith` @@ -153,100 +154,57 @@ spec = around withApp $ do |] it "it includes primary and foreign keys for views" $ - request methodOptions "/insertable_view_with_join" [] "" `shouldRespondWith` + request methodOptions "/projects_view" [] "" `shouldRespondWith` [json| { "pkey":[ "id" ], "columns":[ - { - "references":null, - "default":null, - "precision":64, - "updatable":false, - "schema":"test", - "name":"id", - "type":"bigint", - "maxLen":null, - "enum":[], - "nullable":true, - "position":1 + { + "references":null, + "default":null, + "precision":32, + "updatable":true, + "schema":"test", + "name":"id", + "type":"integer", + "maxLen":null, + "enum":[], + "nullable":true, + "position":1 + }, + { + "references":null, + "default":null, + "precision":null, + "updatable":true, + "schema":"test", + "name":"name", + "type":"text", + "maxLen":null, + "enum":[], + "nullable":true, + "position":2 + }, + { + "references": { + "schema":"test", + "column":"id", + "table":"clients" }, - { - "references":{ - "schema":"test", - "column":"id", - "table":"auto_incrementing_pk" - }, - "default":null, - "precision":32, - "updatable":false, - "schema":"test", - "name":"auto_inc_fk", - "type":"integer", - "maxLen":null, - "enum":[], - "nullable":true, - "position":2 - }, - { - "references":{ - "schema":"test", - "column":"k", - "table":"simple_pk" - }, - "default":null, - "precision":null, - "updatable":false, - "schema":"test", - "name":"simple_fk", - "type":"character varying", - "maxLen":255, - "enum":[], - "nullable":true, - "position":3 - }, - { - "references":null, - "default":null, - "precision":null, - "updatable":false, - "schema":"test", - "name":"nullable_string", - "type":"character varying", - "maxLen":null, - "enum":[], - "nullable":true, - "position":4 - }, - { - "references":null, - "default":null, - "precision":null, - "updatable":false, - "schema":"test", - "name":"non_nullable_string", - "type":"character varying", - "maxLen":null, - "enum":[], - "nullable":true, - "position":5 - }, - { - "references":null, - "default":null, - "precision":null, - "updatable":false, - "schema":"test", - "name":"inserted_at", - "type":"timestamp with time zone", - "maxLen":null, - "enum":[], - "nullable":true, - "position":6 - } - ] + "default":null, + "precision":32, + "updatable":true, + "schema":"test", + "name":"client_id", + "type":"integer", + "maxLen":null, + "enum":[], + "nullable":true, + "position":3 + } + ] } |] @@ -298,3 +256,63 @@ spec = around withApp $ do ] } |] + + it "includes all information on views for renamed columns, and raises relations to correct schema" $ + request methodOptions "/articleStars" [] "" + `shouldRespondWith` [json| + { + "pkey": [ + "articleId", + "userId" + ], + "columns": [ + { + "references": { + "schema": "test", + "column": "id", + "table": "articles" + }, + "default": null, + "precision": 32, + "updatable": true, + "schema": "test", + "name": "articleId", + "type": "integer", + "maxLen": null, + "enum": [], + "nullable": true, + "position": 1 + }, + { + "references": { + "schema": "test", + "column": "id", + "table": "users" + }, + "default": null, + "precision": 32, + "updatable": true, + "schema": "test", + "name": "userId", + "type": "integer", + "maxLen": null, + "enum": [], + "nullable": true, + "position": 2 + }, + { + "references": null, + "default": null, + "precision": null, + "updatable": true, + "schema": "test", + "name": "createdAt", + "type": "timestamp without time zone", + "maxLen": null, + "enum": [], + "nullable": true, + "position": 3 + } + ] + } + |] diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs index dcc765f0d..8a6f9dfe6 100644 --- a/test/SpecHelper.hs +++ b/test/SpecHelper.hs @@ -54,7 +54,7 @@ withApp perform = do <- H.acquirePool pgSettings testPoolOpts let txSettings = Just (H.ReadCommitted, Just True) - dbOrError <- H.session pool $ H.tx txSettings createDbStructure + dbOrError <- H.session pool $ H.tx txSettings $ createDbStructure (cs $ configSchema cfg) db <- either (fail . show) return dbOrError perform $ middle $ \req resp -> do diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 8c17f72ff..de2122959 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -382,8 +382,8 @@ SET search_path = private, pg_catalog; CREATE TABLE articles ( + id integer PRIMARY KEY NOT NULL, body text, - id integer NOT NULL, owner name NOT NULL ); @@ -391,59 +391,43 @@ CREATE TABLE articles ( ALTER TABLE private.articles OWNER TO postgrest_test; -CREATE SEQUENCE articles_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; +CREATE TABLE article_stars ( + article_id int REFERENCES articles(id), + user_id int REFERENCES test.users(id), + created_at timestamp NOT NULL DEFAULT now(), + CONSTRAINT user_article PRIMARY KEY (article_id, user_id) +); +ALTER TABLE private.article_stars OWNER TO postgrest_test; -ALTER TABLE private.articles_id_seq OWNER TO postgrest_test; - - -ALTER SEQUENCE articles_id_seq OWNED BY articles.id; SET search_path = test, pg_catalog; + +CREATE VIEW "articleStars" AS + SELECT article_id AS "articleId", user_id AS "userId", created_at AS "createdAt" + FROM private.article_stars; + +ALTER TABLE test."articleStars" OWNER TO postgrest_test; + +CREATE VIEW articles AS + SELECT * + FROM private.articles; + +ALTER TABLE test.articles OWNER TO postgrest_test; + ALTER TABLE ONLY auto_incrementing_pk ALTER COLUMN id SET DEFAULT nextval('auto_incrementing_pk_id_seq'::regclass); ALTER TABLE ONLY has_fk ALTER COLUMN id SET DEFAULT nextval('has_fk_id_seq'::regclass); - - ALTER TABLE ONLY items ALTER COLUMN id SET DEFAULT nextval('items_id_seq'::regclass); - -SET search_path = private, pg_catalog; - - -ALTER TABLE ONLY articles ALTER COLUMN id SET DEFAULT nextval('articles_id_seq'::regclass); - - -SET search_path = test, pg_catalog; - - - - - - - - SELECT pg_catalog.setval('auto_incrementing_pk_id_seq', 1, true); - - - - - - - - SELECT pg_catalog.setval('has_fk_id_seq', 1, false); @@ -654,6 +638,14 @@ REVOKE ALL ON TABLE projects_view FROM PUBLIC; REVOKE ALL ON TABLE projects_view FROM postgrest_test; GRANT ALL ON TABLE projects_view TO postgrest_test; GRANT ALL ON TABLE projects_view TO postgrest_anonymous; +REVOKE ALL ON TABLE articles FROM PUBLIC; +REVOKE ALL ON TABLE articles FROM postgrest_test; +GRANT ALL ON TABLE articles TO postgrest_test; +GRANT ALL ON TABLE articles TO postgrest_anonymous; +REVOKE ALL ON TABLE "articleStars" FROM PUBLIC; +REVOKE ALL ON TABLE "articleStars" FROM postgrest_test; +GRANT ALL ON TABLE "articleStars" TO postgrest_test; +GRANT ALL ON TABLE "articleStars" TO postgrest_anonymous; --------- @@ -773,4 +765,6 @@ INSERT INTO users_projects VALUES(1,1),(1,2),(2,3),(2,4),(3,1),(3,3); INSERT INTO users_tasks VALUES(1,1),(1,2),(1,3),(1,4),(2,5),(2,6),(2,7),(3,1),(3,5); INSERT INTO comments VALUES (1, 1, 2, 6, 'Needs to be delivered ASAP'); INSERT INTO postgrest.auth (id, pass, rolname) VALUES ('jdoe', '1234', 'postgrest_test_author'); +INSERT INTO private.articles (id, body, owner) VALUES (1, 'No… It''s a thing; it''s like a plan, but with more greatness.', 2), (2, 'Stop talking, brain thinking. Hush.', 3), (3, 'It''s a fez. I wear a fez now. Fezes are cool.', 1); +INSERT INTO private.article_stars (article_id, user_id) VALUES (1,1), (1,2), (2,3), (3,2), (1,3); ---------------- From 0374d4e6513728ff7c2fa0847a977f2e13f11f31 Mon Sep 17 00:00:00 2001 From: calebmer Date: Wed, 11 Nov 2015 09:31:36 -0500 Subject: [PATCH 05/12] Add tables back to DbStructure --- src/PostgREST/App.hs | 3 ++- src/PostgREST/DbStructure.hs | 23 ++++++++++++++++++++++- src/PostgREST/Types.hs | 3 ++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index f3f914109..a51004cf9 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -147,7 +147,7 @@ app db conf reqBody req = -- select * from public.proc(a := "foo"::undefined) where whereT limit limitT ([], _) -> do - body <- encode <$> tables (cs schema) + body <- encode <$> accessibleTables (filter ((== cs schema) . tableSchema) allTabs) return $ responseLBS status200 [jsonH] $ cs body ([table], "OPTIONS") -> do @@ -160,6 +160,7 @@ app db conf reqBody req = return $ responseLBS status404 [] "" where + allTabs = tables db allRels = relations db allCols = columns db allPrKeys = primaryKeys db diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 91d728c64..0798b1120 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -5,6 +5,7 @@ {-# LANGUAGE TypeSynonymInstances #-} module PostgREST.DbStructure ( createDbStructure +, accessibleTables , doesProcExist , doesProcReturnJWT ) where @@ -71,6 +72,26 @@ doesProcReturnJWT = doesProc [H.stmt| AND pg_catalog.pg_get_function_result(p.oid) like '%jwt_claims' |] +accessibleTables :: [Table] -> H.Tx P.Postgres s [Table] +accessibleTables allTabs = do + accessible <- H.listEx $ [H.stmt| + SELECT + n.nspname AS table_schema, + c.relname AS table_name + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE + c.relkind IN ('v','r','m') AND + n.nspname NOT IN ('pg_catalog', 'information_schema') AND ( + pg_has_role(c.relowner, 'USAGE'::text) OR + has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR + has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text) + ) + ORDER BY table_schema, table_name + |] + let isAccessible table = isJust $ find (\(s,n) -> tableSchema table == s && tableName table == n) accessible + return $ filter isAccessible allTabs + synonymousColumns :: [(Column,Column)] -> [Column] -> [[Column]] synonymousColumns allSyns cols = synCols' where @@ -159,7 +180,7 @@ allTables = do WHERE c.relkind IN ('v','r','m') AND n.nspname NOT IN ('pg_catalog', 'information_schema') GROUP BY table_schema, table_name, insertable - ORDER BY table_schema, table_name; + ORDER BY table_schema, table_name |] return $ map tableFromRow rows diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 9bb4d4cab..ae9e8e819 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -6,7 +6,8 @@ import Data.Aeson import Data.Map data DbStructure = DbStructure { - columns :: [Column] + tables :: [Table] +, columns :: [Column] , relations :: [Relation] , primaryKeys :: [PrimaryKey] } deriving (Show, Eq) From b8b073810fdac7d3fd4e750fc592fc693eee999b Mon Sep 17 00:00:00 2001 From: calebmer Date: Wed, 11 Nov 2015 15:53:52 -0500 Subject: [PATCH 06/12] Rename functions --- src/PostgREST/App.hs | 12 ++++++------ src/PostgREST/DbStructure.hs | 24 ++++++++++++------------ src/PostgREST/Main.hs | 6 +++--- test/SpecHelper.hs | 4 ++-- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index a51004cf9..1741b2f6d 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -55,7 +55,7 @@ import PostgREST.Auth (tokenJWT) import Prelude app :: DbStructure -> AppConfig -> BL.ByteString -> Request -> H.Tx P.Postgres s Response -app db conf reqBody req = +app dbStructure conf reqBody req = case (path, verb) of ([table], "GET") -> @@ -71,7 +71,7 @@ app db conf reqBody req = to = frm+queryTotal-1 contentRange = contentRangeH frm to tableTotal status = rangeStatus frm to tableTotal - canonical = urlEncodeVars -- should this be moved to the db (location)? + canonical = urlEncodeVars -- should this be moved to the dbStructure (location)? . sortBy (comparing fst) . map (join (***) cs) . parseSimpleQuery @@ -160,10 +160,10 @@ app db conf reqBody req = return $ responseLBS status404 [] "" where - allTabs = tables db - allRels = relations db - allCols = columns db - allPrKeys = primaryKeys db + allTabs = tables dbStructure + allRels = relations dbStructure + allCols = columns dbStructure + allPrKeys = primaryKeys dbStructure filterCol sc table (Column{colTable=Table{tableSchema=s, tableName=t}}) = s==sc && table==t filterCol _ _ _ = False filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 0798b1120..04aca3d97 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -4,7 +4,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeSynonymInstances #-} module PostgREST.DbStructure ( - createDbStructure + getDbStructure , accessibleTables , doesProcExist , doesProcReturnJWT @@ -26,15 +26,15 @@ import PostgREST.Types import GHC.Exts (groupWith) import Prelude -createDbStructure :: Schema -> H.Tx P.Postgres s DbStructure -createDbStructure schema = do +getDbStructure :: Schema -> H.Tx P.Postgres s DbStructure +getDbStructure schema = do tabs <- allTables cols <- allColumns tabs syns <- allSynonyms cols rels <- allRelations tabs cols keys <- allPrimaryKeys tabs - let rels' = (manyToManyRelations . raiseRelations schema syns . parentRelations . synonymousRelations syns) rels + let rels' = (addManyToManyRelations . raiseRelations schema syns . addParentRelations . addSynonymousRelations syns) rels cols' = addForeignKeys rels' cols keys' = synonymousPrimaryKeys syns keys @@ -115,20 +115,20 @@ addForeignKeys rels = map addFk pos = elemIndex col cols colF = (colsF !!) <$> pos -synonymousRelations :: [(Column,Column)] -> [Relation] -> [Relation] -synonymousRelations _ [] = [] -synonymousRelations syns (rel:rels) = rel : synRelsP ++ synRelsF ++ synonymousRelations syns rels +addSynonymousRelations :: [(Column,Column)] -> [Relation] -> [Relation] +addSynonymousRelations _ [] = [] +addSynonymousRelations syns (rel:rels) = rel : synRelsP ++ synRelsF ++ addSynonymousRelations syns rels where synRelsP = synRels (relColumns rel) (\t cs -> rel{relTable=t,relColumns=cs}) synRelsF = synRels (relFColumns rel) (\t cs -> rel{relFTable=t,relFColumns=cs}) synRels cols mapFn = map (\cs -> mapFn (colTable $ head cs) cs) $ synonymousColumns syns cols -parentRelations :: [Relation] -> [Relation] -parentRelations [] = [] -parentRelations (rel@(Relation t c ft fc _ _ _ _):rels) = Relation ft fc t c Parent Nothing Nothing Nothing : rel : parentRelations rels +addParentRelations :: [Relation] -> [Relation] +addParentRelations [] = [] +addParentRelations (rel@(Relation t c ft fc _ _ _ _):rels) = Relation ft fc t c Parent Nothing Nothing Nothing : rel : addParentRelations rels -manyToManyRelations :: [Relation] -> [Relation] -manyToManyRelations rels = rels ++ mapMaybe link2Relation links +addManyToManyRelations :: [Relation] -> [Relation] +addManyToManyRelations rels = rels ++ mapMaybe link2Relation links where links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==Child). relType) rels groupFn :: Relation -> Text diff --git a/src/PostgREST/Main.hs b/src/PostgREST/Main.hs index 948cb875f..4b12d07b8 100644 --- a/src/PostgREST/Main.hs +++ b/src/PostgREST/Main.hs @@ -68,11 +68,11 @@ main = do ) supportedOrError let txSettings = Just (H.ReadCommitted, Just True) - dbOrError <- H.session pool $ H.tx txSettings $ createDbStructure (cs $ configSchema conf) - db <- either hasqlError return dbOrError + dbOrError <- H.session pool $ H.tx txSettings $ getDbStructure (cs $ configSchema conf) + dbStructure <- either hasqlError return dbOrError runSettings appSettings $ middle $ \ req respond -> do body <- strictRequestBody req resOrError <- liftIO $ H.session pool $ H.tx txSettings $ - runWithClaims conf (app db conf body) req + runWithClaims conf (app dbStructure conf body) req either (respond . errResponse) respond resOrError diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs index 8a6f9dfe6..8971b606f 100644 --- a/test/SpecHelper.hs +++ b/test/SpecHelper.hs @@ -54,7 +54,7 @@ withApp perform = do <- H.acquirePool pgSettings testPoolOpts let txSettings = Just (H.ReadCommitted, Just True) - dbOrError <- H.session pool $ H.tx txSettings $ createDbStructure (cs $ configSchema cfg) + dbOrError <- H.session pool $ H.tx txSettings $ getDbStructure (cs $ configSchema cfg) db <- either (fail . show) return dbOrError perform $ middle $ \req resp -> do @@ -119,7 +119,7 @@ clearProjectsTable :: IO () clearProjectsTable = do pool <- testPool void . liftIO $ H.session pool $ H.tx Nothing $ - H.unitEx $ B.Stmt ("delete from test.projects where id > 4") V.empty True + H.unitEx $ B.Stmt "delete from test.projects where id > 4" V.empty True createItems :: Int -> IO () From 1061854f3575822980ad97206770109d130ec5d3 Mon Sep 17 00:00:00 2001 From: calebmer Date: Wed, 11 Nov 2015 15:55:08 -0500 Subject: [PATCH 07/12] Resolve SQL errors --- test/fixtures/schema.sql | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index de2122959..32129873f 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -450,10 +450,6 @@ SET search_path = private, pg_catalog; - -SELECT pg_catalog.setval('articles_id_seq', 1, false); - - SET search_path = test, pg_catalog; CREATE FUNCTION public.always_true(test.items) RETURNS boolean @@ -514,10 +510,6 @@ ALTER TABLE ONLY auth SET search_path = private, pg_catalog; -ALTER TABLE ONLY articles - ADD CONSTRAINT articles_pkey PRIMARY KEY (id); - - SET search_path = postgrest, pg_catalog; From acb8f8a15491352fcad6beda7b1df6f9515822fc Mon Sep 17 00:00:00 2001 From: calebmer Date: Thu, 12 Nov 2015 08:14:14 -0500 Subject: [PATCH 08/12] Fix failing insert test --- src/PostgREST/QueryBuilder.hs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 679524e3e..e80849091 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -35,23 +35,27 @@ addRelations schema allRelations parentNode node@(Node n@(query, (table, _)) for updatedForest = mapM (addRelations schema allRelations (Just node)) forest getJoinConditions :: Relation -> [Filter] -getJoinConditions (Relation t cs ft fcs typ _ lc1 lc2) = +getJoinConditions (Relation t cs ft fcs typ lt lc1 lc2) = case typ of - Child -> zipWith (toFilter t) cs fcs - Parent -> zipWith (toFilter t) cs fcs - Many -> zipWith (toFilter t) cs (fromMaybe [] lc1) ++ zipWith (toFilter ft) fcs (fromMaybe [] lc2) + Child -> zipWith (toFilter tN ftN) cs fcs + Parent -> zipWith (toFilter tN ftN) cs fcs + Many -> zipWith (toFilter tN ltN) cs (fromMaybe [] lc1) ++ zipWith (toFilter ftN ltN) fcs (fromMaybe [] lc2) where - toFilter :: Table -> Column -> Column -> Filter - toFilter tb c fc = Filter (colName c, Nothing) "=" (VForeignKey (QualifiedIdentifier (tableSchema tb) (tableName tb)) (ForeignKey fc)) + s = tableSchema t + tN = tableName t + ftN = tableName ft + ltN = fromMaybe "" (tableName <$> lt) + toFilter :: Text -> Text -> Column -> Column -> Filter + toFilter tb ftb c fc = Filter (colName c, Nothing) "=" (VForeignKey (QualifiedIdentifier s tb) (ForeignKey fc{colTable=(colTable fc){tableName=ftb}})) addJoinConditions :: Text -> ApiRequest -> Either Text ApiRequest -addJoinConditions schema (Node (query, (t, r)) forest) = +addJoinConditions schema (Node (query, (n, r)) forest) = case r of - Nothing -> Node (updatedQuery, (t, r)) <$> updatedForest -- this is the root node - Just rel@(Relation{relType=Child}) -> Node (addCond updatedQuery (getJoinConditions rel),(t,r)) <$> updatedForest - Just (Relation{relType=Parent}) -> Node (updatedQuery, (t,r)) <$> updatedForest + Nothing -> Node (updatedQuery, (n,r)) <$> updatedForest -- this is the root node + Just rel@(Relation{relType=Child}) -> Node (addCond updatedQuery (getJoinConditions rel),(n,r)) <$> updatedForest + Just (Relation{relType=Parent}) -> Node (updatedQuery, (n,r)) <$> updatedForest Just rel@(Relation{relType=Many, relLTable=(Just linkTable)}) -> - Node (qq, (t, r)) <$> updatedForest + Node (qq, (n, r)) <$> updatedForest where q = addCond updatedQuery (getJoinConditions rel) qq = q{from=tableName linkTable : from q} From 0f5bc34c041d72695a315d60e4296e49bb943d01 Mon Sep 17 00:00:00 2001 From: calebmer Date: Thu, 12 Nov 2015 08:22:27 -0500 Subject: [PATCH 09/12] Remove fromJust assumptions --- src/PostgREST/DbStructure.hs | 46 ++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 04aca3d97..67519a390 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -230,18 +230,20 @@ allColumns tabs = do ) AS enum_info ON (info.udt_name = enum_info.n) ORDER BY schema, position |] - return $ map (columnFromRow tabs) cols + return $ mapMaybe (columnFromRow tabs) cols columnFromRow :: [Table] -> (Text, Text, Text, Int, Bool, Text, Bool, Maybe Int, Maybe Int, Maybe Text, Maybe Text) - -> Column + -> Maybe Column columnFromRow tabs (s, t, n, pos, nul, typ, u, l, p, d, e) = - Column table n pos nul typ u l p d (parseEnum e) Nothing + if isJust table + then Just $ Column (fromJust table) n pos nul typ u l p d (parseEnum e) Nothing + else Nothing where - table = fromJust $ find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs + table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs parseEnum :: Maybe Text -> [Text] parseEnum str = fromMaybe [] $ split (==',') <$> str @@ -273,12 +275,15 @@ allRelations tabs cols = do WHERE confrelid != 0 ORDER BY (conrelid, column_info.nums) |] - return $ map (relationFromRow tabs cols) rels + return $ mapMaybe (relationFromRow tabs cols) rels -relationFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Relation -relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) = Relation table cols tableF colsF Child Nothing Nothing Nothing +relationFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation +relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) = + if isJust table && isJust tableF && length cols == length rcs && length colsF == length frcs + then Just $ Relation (fromJust table) cols (fromJust tableF) colsF Child Nothing Nothing Nothing + else Nothing where - findTable s t = fromJust $ find (\tbl -> tableSchema tbl == s && tableName tbl == t) allTabs + findTable s t = find (\tbl -> tableSchema tbl == s && tableName tbl == t) allTabs findCols s t cs = filter (\col -> tableSchema (colTable col) == s && tableName (colTable col) == t && colName col `elem` cs) allCols table = findTable rs rt tableF = findTable frs frt @@ -302,16 +307,19 @@ allPrimaryKeys tabs = do kc.constraint_name = tc.constraint_name AND kc.table_schema NOT IN ('pg_catalog', 'information_schema') |] - return $ map (pkFromRow tabs) pks + return $ mapMaybe (pkFromRow tabs) pks -pkFromRow :: [Table] -> (Schema, Text, Text) -> PrimaryKey -pkFromRow tabs (s, t, n) = PrimaryKey table n +pkFromRow :: [Table] -> (Schema, Text, Text) -> Maybe PrimaryKey +pkFromRow tabs (s, t, n) = + if isJust table + then Just $ PrimaryKey (fromJust table) n + else Nothing where - table = fromJust $ find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs + table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs allSynonyms :: [Column] -> H.Tx P.Postgres s [(Column,Column)] allSynonyms allCols = do - srcSyns <- H.listEx $ [H.stmt| + syns <- H.listEx $ [H.stmt| WITH synonyms AS ( SELECT vcu.table_schema AS src_table_schema, @@ -342,6 +350,14 @@ allSynonyms allCols = do FROM synonyms ) |] - return $ map (\(a,b,c,d,e,f) -> (findCol a b c,findCol d e f)) srcSyns + return $ mapMaybe (synonymFromRow allCols) syns + +synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe (Column,Column) +synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = + if isJust col1 && isJust col2 + then Just (fromJust col1,fromJust col2) + else Nothing where - findCol s t c = fromJust $ find (\col -> (tableSchema . colTable) col == s && (tableName . colTable) col == t && colName col == c) allCols + col1 = findCol s1 t1 c1 + col2 = findCol s2 t2 c2 + findCol s t c = find (\col -> (tableSchema . colTable) col == s && (tableName . colTable) col == t && colName col == c) allCols From 2f6254f44c4845fb602dbd273f675ccd390b272f Mon Sep 17 00:00:00 2001 From: calebmer Date: Thu, 12 Nov 2015 15:01:23 -0500 Subject: [PATCH 10/12] Rename DbStructure names --- src/PostgREST/App.hs | 8 ++++---- src/PostgREST/Config.hs | 2 +- src/PostgREST/DbStructure.hs | 8 ++++---- src/PostgREST/MainTest.hs | 8 ++++---- src/PostgREST/Types.hs | 8 ++++---- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 1741b2f6d..a395353a3 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -160,10 +160,10 @@ app dbStructure conf reqBody req = return $ responseLBS status404 [] "" where - allTabs = tables dbStructure - allRels = relations dbStructure - allCols = columns dbStructure - allPrKeys = primaryKeys dbStructure + allTabs = dbTables dbStructure + allRels = dbRelations dbStructure + allCols = dbColumns dbStructure + allPrKeys = dbPrimaryKeys dbStructure filterCol sc table (Column{colTable=Table{tableSchema=s, tableName=t}}) = s==sc && table==t filterCol _ _ _ = False filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index a83ae50b9..17a94ff07 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -28,7 +28,7 @@ import Data.Text (strip) import Data.Version (versionBranch) import Network.Wai import Network.Wai.Middleware.Cors (CorsResourcePolicy (..)) -import Options.Applicative hiding (columns) +import Options.Applicative import Paths_postgrest (version) import Prelude diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 67519a390..81dbb2deb 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -39,10 +39,10 @@ getDbStructure schema = do keys' = synonymousPrimaryKeys syns keys return DbStructure { - tables = tabs - , columns = cols' - , relations = rels' - , primaryKeys = keys' + dbTables = tabs + , dbColumns = cols' + , dbRelations = rels' + , dbPrimaryKeys = keys' } doesProc :: forall c s. B.CxValue c Int => diff --git a/src/PostgREST/MainTest.hs b/src/PostgREST/MainTest.hs index 45326d33d..f9b90e9cc 100644 --- a/src/PostgREST/MainTest.hs +++ b/src/PostgREST/MainTest.hs @@ -98,10 +98,10 @@ main = do (\(tabs, rels, cols, keys) -> return DbStructure { - tables=tabs - , columns=cols - , relations=rels - , primaryKeys=keys + dbTables=tabs + , dbColumns=cols + , dbRelations=rels + , dbPrimaryKeys=keys } ) metadata runSettings appSettings $ middle $ \ req respond -> do diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index ae9e8e819..19a8d05b0 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -6,10 +6,10 @@ import Data.Aeson import Data.Map data DbStructure = DbStructure { - tables :: [Table] -, columns :: [Column] -, relations :: [Relation] -, primaryKeys :: [PrimaryKey] + dbTables :: [Table] +, dbColumns :: [Column] +, dbRelations :: [Relation] +, dbPrimaryKeys :: [PrimaryKey] } deriving (Show, Eq) type Schema = Text From 3794d358b4e1dcb17c8162909fecdca462239a4f Mon Sep 17 00:00:00 2001 From: calebmer Date: Thu, 12 Nov 2015 15:09:28 -0500 Subject: [PATCH 11/12] Prettify monads --- src/PostgREST/DbStructure.hs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 81dbb2deb..7eda5dc8a 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -238,11 +238,9 @@ columnFromRow :: [Table] -> Bool, Maybe Int, Maybe Int, Maybe Text, Maybe Text) -> Maybe Column -columnFromRow tabs (s, t, n, pos, nul, typ, u, l, p, d, e) = - if isJust table - then Just $ Column (fromJust table) n pos nul typ u l p d (parseEnum e) Nothing - else Nothing +columnFromRow tabs (s, t, n, pos, nul, typ, u, l, p, d, e) = buildColumn <$> table where + buildColumn tbl = Column tbl n pos nul typ u l p d (parseEnum e) Nothing table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs parseEnum :: Maybe Text -> [Text] parseEnum str = fromMaybe [] $ split (==',') <$> str @@ -310,12 +308,8 @@ allPrimaryKeys tabs = do return $ mapMaybe (pkFromRow tabs) pks pkFromRow :: [Table] -> (Schema, Text, Text) -> Maybe PrimaryKey -pkFromRow tabs (s, t, n) = - if isJust table - then Just $ PrimaryKey (fromJust table) n - else Nothing - where - table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs +pkFromRow tabs (s, t, n) = PrimaryKey <$> table <*> pure n + where table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs allSynonyms :: [Column] -> H.Tx P.Postgres s [(Column,Column)] allSynonyms allCols = do @@ -353,10 +347,7 @@ allSynonyms allCols = do return $ mapMaybe (synonymFromRow allCols) syns synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe (Column,Column) -synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = - if isJust col1 && isJust col2 - then Just (fromJust col1,fromJust col2) - else Nothing +synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2 where col1 = findCol s1 t1 c1 col2 = findCol s2 t2 c2 From 0bf3dd9b1c914068c4de6a71023d4cbbdeaddb14 Mon Sep 17 00:00:00 2001 From: calebmer Date: Sat, 14 Nov 2015 08:02:09 -0500 Subject: [PATCH 12/12] Remove extraneous files --- src/PostgREST/MainTest.hs | 129 -------------------------------------- src/mock.hs | 43 ------------- 2 files changed, 172 deletions(-) delete mode 100644 src/PostgREST/MainTest.hs delete mode 100644 src/mock.hs diff --git a/src/PostgREST/MainTest.hs b/src/PostgREST/MainTest.hs deleted file mode 100644 index f9b90e9cc..000000000 --- a/src/PostgREST/MainTest.hs +++ /dev/null @@ -1,129 +0,0 @@ -module Main where - - -import PostgREST.App -import PostgREST.Config (AppConfig (..), - minimumPgVersion, - prettyVersion, - readOptions) -import PostgREST.Error (errResponse, PgError) -import PostgREST.Middleware -import PostgREST.DbStructure -import PostgREST.Types - -import Control.Monad (unless) -import Control.Monad.IO.Class (liftIO) -import Data.Aeson (encode) -import Data.Functor.Identity -import Data.Monoid ((<>)) -import Data.String.Conversions (cs) -import Data.Text (Text) -import qualified Hasql as H -import qualified Hasql.Postgres as P -import Network.Wai -import Network.Wai.Handler.Warp hiding (Connection) -import Network.Wai.Middleware.RequestLogger (logStdout) -import System.IO (BufferMode (..), - hSetBuffering, stderr, - stdin, stdout) --- import Data.Maybe (mapMaybe) --- import Data.List (subsequences) --- import Control.Monad (join) --- import PostgREST.QueryBuilder --- import GHC.Exts (groupWith) - - -isServerVersionSupported :: H.Session P.Postgres IO Bool -isServerVersionSupported = do - Identity (row :: Text) <- H.tx Nothing $ H.singleEx [H.stmt|SHOW server_version_num|] - return $ read (cs row) >= minimumPgVersion - -hasqlError :: PgError -> IO a -hasqlError = error . cs . encode - - -main :: IO () -main = do - hSetBuffering stdout LineBuffering - hSetBuffering stdin LineBuffering - hSetBuffering stderr NoBuffering - - -- let dbString = "postgres://postgrest_test@localhost:5432/postgrest_test" :: String - -- conf = AppConfig dbString 3000 "postgrest_anonymous" "test" False "safe" 10 :: AppConfig - - conf <- readOptions - let port = configPort conf - - unless ("secret" /= configJwtSecret conf) $ - putStrLn "WARNING, running in insecure mode, JWT secret is the default value" - Prelude.putStrLn $ "Listening on port " ++ - (show $ configPort conf :: String) - - let pgSettings = P.StringSettings $ cs (configDatabase conf) - appSettings = setPort port - . setServerName (cs $ "postgrest/" <> prettyVersion) - $ defaultSettings - middle = logStdout . defaultMiddle - - poolSettings <- maybe (fail "Improper session settings") return $ - H.poolSettings (fromIntegral $ configPool conf) 30 - pool :: H.Pool P.Postgres <- H.acquirePool pgSettings poolSettings - - supportedOrError <- H.session pool isServerVersionSupported - either hasqlError - (\supported -> - unless supported $ - error ( - "Cannot run in this PostgreSQL version, PostgREST needs at least " - <> show minimumPgVersion) - ) supportedOrError - - -- what was this code for? - -- roleOrError <- H.session pool $ do - -- Identity (role :: Text) <- H.tx Nothing $ H.singleEx - -- [H.stmt|SELECT SESSION_USER|] - -- return role - -- authenticator <- either hasqlError return roleOrError - - let txSettings = Just (H.ReadCommitted, Just True) - metadata <- H.session pool $ H.tx txSettings $ do - tabs <- allTables - rels <- allRelations - cols <- allColumns rels - keys <- allPrimaryKeys - return (tabs, rels, cols, keys) - - - db <- either hasqlError - (\(tabs, rels, cols, keys) -> - - return DbStructure { - dbTables=tabs - , dbColumns=cols - , dbRelations=rels - , dbPrimaryKeys=keys - } - ) metadata - runSettings appSettings $ middle $ \ req respond -> do - body <- strictRequestBody req - resOrError <- liftIO $ H.session pool $ H.tx txSettings $ - runWithClaims conf (app db conf body) req - either (respond . errResponse) respond resOrError - - --let allRels = relations db - -- links = join $ map (combinations 2) $ filter ((>=1).length) $ groupWith groupFn $ filter ( (==Child). relType) allRels - -- combinations k ns = filter ((k==).length) (subsequences ns) - - --print $ findRelation allRels "test" "projects" "users" - --mapM_ print $ mapMaybe link2Relation links - - -- where - -- groupFn :: Relation -> Text - -- groupFn (Relation{relSchema=s, relTable=t}) = s<>"_"<>t - -- link2Relation [ - -- Relation{relSchema=sc, relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c}, - -- Relation{ relColumns=lc2, relFTable=ft, relFColumns=fc} - -- ] - -- | lc1 /= lc2 && length lc1 == 1 && length lc2 == 1 = Just $ Relation sc t c ft fc Many (Just lt) (Just lc1) (Just lc2) - -- | otherwise = Nothing - -- link2Relation _ = Nothing diff --git a/src/mock.hs b/src/mock.hs deleted file mode 100644 index 411b3c309..000000000 --- a/src/mock.hs +++ /dev/null @@ -1,43 +0,0 @@ -arr = eitherDecode "[{\"a\":10},{\"a\":20}]" :: Either String Value -ob = eitherDecode "{\"a\":10}"::Either String Value - -rc :: Request -rc = Request { - -- | Request method such as GET. - requestMethod = "POST" - , pathInfo = ["menagerie"] - , requestHeaders = [("Content-Type", "text/csv")] -- :: H.RequestHeaders - } -bc :: BL.ByteString -bc = [str|integer->sub->sub2,double,varchar,boolean,date,money,enum - |13,3.14159,testing!,false,1900-01-01,$3.99,foo - |12,0.1,NULL,true,1929-10-01,12,bar - |] - -rj :: Request -rj = Request { - -- | Request method such as GET. - requestMethod = "POST" - , pathInfo = ["menagerie"] - , requestHeaders = [("Content-Type", "application/json")] -- :: H.RequestHeaders - } -bj :: BL.ByteString -bj = [str|{ - | "integer->sub->>sub2": 13, "double": 3.14159, "varchar": "testing!" - | , "boolean": false, "date": "1900-01-01", "money": "$3.99" - | , "enum": "foo" - |} - |] -bj2 :: BL.ByteString -bj2 = [str|[ - |{ - | "integer->sub->>sub2": 13, "double": 3.14159, "varchar": "testing!" - | , "boolean": false, "date": "1900-01-01", "money": "$3.99" - | , "enum": "foo" - |}, - |{ - | "integer->sub->>sub2": 13, "double": 3.14159, "varchar": "testing!" - | , "boolean": false, "date": "1900-01-01", "money": "$3.99" - | , "enum": "foo" - |}] - |]