Skip to content

Commit

Permalink
feat: Added --dump-schema CLI option to dump JSON of dbStructure sche…
Browse files Browse the repository at this point in the history
…ma cache.

Added postgrest-dump-schema to nix-shell returning a YAML dump of the test fixtures.

Authored-by: monacoremo <monacoremo>
  • Loading branch information
monacoremo committed Dec 20, 2020
1 parent fe09637 commit bf141ca
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 33 deletions.
70 changes: 65 additions & 5 deletions main/Main.hs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{-# LANGUAGE CPP #-}

module Main where
module Main (main) where

import qualified Data.Aeson as Aeson
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Hasql.Connection as C
import qualified Hasql.Notifications as N
import qualified Hasql.Pool as P
import qualified Hasql.Session as S
import qualified Hasql.Transaction.Sessions as HT

import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
Expand All @@ -24,7 +27,9 @@ import Data.Text.IO (hPutStrLn)
import Data.Time.Clock (getCurrentTime)
import Network.Wai.Handler.Warp (defaultSettings, runSettings,
setHost, setPort, setServerName)
import System.CPUTime (getCPUTime)
import System.IO (BufferMode (..), hSetBuffering)
import Text.Printf (hPrintf)

import PostgREST.App (postgrest)
import PostgREST.Config (AppConfig (..), CLI (..), Command (..),
Expand Down Expand Up @@ -64,9 +69,6 @@ main = do
-- build the 'AppConfig' from the config file path
conf <- readValidateConfig $ cliPath opts

-- dump config and exit if option is set
when (cliCommand opts == CmdDumpConfig) $ dumpAppConfig conf

-- These are config values that can't be reloaded at runtime. Reloading some of them would imply restarting the web server.
let
host = configServerHost conf
Expand All @@ -86,6 +88,19 @@ main = do
poolTimeout = configDbPoolTimeout' conf
logLevel = configLogLevel conf

case cliCommand opts of
CmdDumpConfig ->
do
putStr $ dumpAppConfig conf
exitSuccess
CmdDumpSchema ->
do
dumpedSchema <- dumpSchema conf
putStrLn dumpedSchema
exitSuccess
CmdRun ->
pass

-- create connection pool with the provided settings, returns either a 'Connection' or a 'ConnectionError'. Does not throw.
pool <- P.acquire (poolSize, poolTimeout, dbUri)

Expand Down Expand Up @@ -158,6 +173,7 @@ main = do
putStrLn $ ("Listening on port " :: Text) <> show port
runSettings serverSettings postgrestApplication


-- Time constants
_32s :: Int
_32s = 32000000 :: Int -- 32 seconds
Expand Down Expand Up @@ -316,16 +332,60 @@ listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWo

-- | Re-reads the config at runtime. Invoked on SIGUSR2.
-- | If it panics(config path was changed, invalid setting), it'll show an error but won't kill the main thread.
#ifndef mingw32_HOST_OS
reReadConfig :: FilePath -> IORef AppConfig -> IO ()
reReadConfig path refConf = do
conf <- readValidateConfig path
atomicWriteIORef refConf conf
putStrLn ("Config file reloaded" :: Text)
#endif

-- Utilitarian functions.
-- | Dump DbStructure schema to JSON
dumpSchema :: AppConfig -> IO LBS.ByteString
dumpSchema conf =
do
Right conn <- C.acquire . toS $ configDbUri conf
Right pgVersion <- S.run getPgVersion conn
let
getDbStructureTransaction =
HT.transaction HT.ReadCommitted HT.Read $
getDbStructure
(toList $ configDbSchemas conf)
(configDbExtraSearchPath conf)
pgVersion
(configDbPreparedStatements conf)
Right dbStructure <-
timeToStderr "Loaded schema in %.3f seconds" $
S.run getDbStructureTransaction conn
C.release conn
return $ Aeson.encode dbStructure


-- | Print the time taken to run an IO action to stderr with the given printf string
timeToStderr :: [Char] -> IO a -> IO a
timeToStderr fmtString a =
do
start <- getCPUTime
result <- a
end <- getCPUTime
let
duration :: Double
duration = fromIntegral (end - start) / picoseconds
hPrintf stderr (fmtString ++ "\n") duration
return result


-- | 10^12 picoseconds per second
picoseconds :: Double
picoseconds = 1000000000000


-- Utility functions.
#ifndef mingw32_HOST_OS
whenJust :: Applicative f => Maybe a -> (a -> f ()) -> f ()
whenJust (Just x) f = f x
whenJust Nothing _ = pass
#endif

whenNothing :: Applicative f => Maybe a -> f () -> f ()
whenNothing Nothing f = f
Expand Down
19 changes: 19 additions & 0 deletions nix/tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
, procps
, python3
, runtimeShell
, yq
}:
let
# Wrap the `test/with_tmp_db` script with the required dependencies from Nix.
Expand Down Expand Up @@ -125,6 +126,23 @@ let
${withTmpDb postgresql} "$rootdir/test/memory-tests.sh"
'';

dumpSchema =
name: postgresql:
checkedShellScript
name
''
rootdir="$(${git}/bin/git rev-parse --show-toplevel)"
cd "$rootdir"
env="$(cat ${postgrest.env})"
export PATH="$env/bin:$PATH"
${withTmpDb postgresql} \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
postgrest --dump-schema "$rootdir"/test/io-tests/configs/simple.config \
| ${yq}/bin/yq -y .
'';
in
# Create an environment that contains all the utility scripts for running tests
# that we defined above.
Expand All @@ -139,6 +157,7 @@ buildEnv
(testSpecIdempotence "postgrest-test-spec-idempotence" postgresql).bin
testSpecAllVersions.bin
(testIO "postgrest-test-io" postgresql).bin
(dumpSchema "postgrest-dump-schema" postgresql).bin
] ++ testSpecVersions;
}
# The memory tests have large dependencies (a profiled build of PostgREST)
Expand Down
1 change: 1 addition & 0 deletions postgrest.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ executable postgrest
main-is: Main.hs
hs-source-dirs: main
build-depends: base >= 4.9 && < 4.15
, aeson >= 1.4.7 && < 1.6
, auto-update >= 0.1.4 && < 0.2
, base64-bytestring >= 1 && < 1.3
, bytestring >= 0.10.8 && < 0.11
Expand Down
34 changes: 21 additions & 13 deletions src/PostgREST/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ data CLI = CLI
{ cliCommand :: Command
, cliPath :: FilePath }

data Command = CmdRun | CmdDumpConfig deriving (Eq)
data Command
= CmdRun
| CmdDumpConfig
| CmdDumpSchema
deriving (Eq)

-- | Config file settings for the server
data AppConfig = AppConfig {
Expand Down Expand Up @@ -148,10 +152,18 @@ readCLIShowHelp = customExecParser parserPrefs opts

cliParser :: Parser CLI
cliParser = CLI <$>
flag CmdRun CmdDumpConfig (
long "dump-config" <>
help "Dump loaded configuration and exit"
) <*>
(
flag CmdRun CmdDumpConfig (
long "dump-config" <>
help "Dump loaded configuration and exit"
)
<|>
flag CmdRun CmdDumpSchema (
long "dump-schema" <>
help "Dump loaded schema as JSON and exit (for debugging, output structure is unstable)"
)
)
<*>
strArgument (
metavar "FILENAME" <>
help "Path to configuration file"
Expand Down Expand Up @@ -237,15 +249,11 @@ readCLIShowHelp = customExecParser parserPrefs opts
|]

-- | Dump the config
dumpAppConfig :: AppConfig -> IO ()
dumpAppConfig conf = do
putStr dump
exitSuccess

dumpAppConfig :: AppConfig -> Text
dumpAppConfig conf =
unlines $ (\(k, v) -> k <> " = " <> v) <$>
pgrstSettings ++ appSettings
where
dump = unlines $ (\(k, v) -> k <> " = " <> v) <$>
pgrstSettings ++ appSettings

-- apply conf to all pgrst settings
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
[("db-anon-role", q . configDbAnonRole)
Expand Down
31 changes: 16 additions & 15 deletions src/PostgREST/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Module : PostgREST.Types
Description : PostgREST common types and functions used by the rest of the modules
-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}

Expand Down Expand Up @@ -125,7 +126,7 @@ data DbStructure = DbStructure {
, dbPrimaryKeys :: [PrimaryKey]
, dbProcs :: ProcsMap
, pgVersion :: PgVersion
} deriving (Show, Eq)
} deriving (Show, Eq, Generic, JSON.ToJSON)

-- TODO Table could hold references to all its Columns
tableCols :: DbStructure -> Schema -> TableName -> [Column]
Expand All @@ -140,14 +141,14 @@ data PgArg = PgArg {
, pgaType :: Text
, pgaReq :: Bool
, pgaVar :: Bool
} deriving (Show, Eq, Ord)
} deriving (Show, Eq, Ord, Generic, JSON.ToJSON)

data PgType = Scalar QualifiedIdentifier | Composite QualifiedIdentifier deriving (Eq, Show, Ord)
data PgType = Scalar QualifiedIdentifier | Composite QualifiedIdentifier deriving (Eq, Show, Ord, Generic, JSON.ToJSON)

data RetType = Single PgType | SetOf PgType deriving (Eq, Show, Ord)
data RetType = Single PgType | SetOf PgType deriving (Eq, Show, Ord, Generic, JSON.ToJSON)

data ProcVolatility = Volatile | Stable | Immutable
deriving (Eq, Show, Ord)
deriving (Eq, Show, Ord, Generic, JSON.ToJSON)

data ProcDescription = ProcDescription {
pdSchema :: Schema
Expand All @@ -157,7 +158,7 @@ data ProcDescription = ProcDescription {
, pdReturnType :: RetType
, pdVolatility :: ProcVolatility
, pdHasVariadic :: Bool
} deriving (Show, Eq)
} deriving (Show, Eq, Generic, JSON.ToJSON)

-- Order by least number of args in the case of overloaded functions
instance Ord ProcDescription where
Expand Down Expand Up @@ -225,15 +226,15 @@ data Table = Table {
, tableName :: TableName
, tableDescription :: Maybe Text
, tableInsertable :: Bool
} deriving (Show, Ord)
} deriving (Show, Ord, Generic, JSON.ToJSON)

instance Eq Table where
Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2

tableQi :: Table -> QualifiedIdentifier
tableQi Table{tableSchema=s, tableName=n} = QualifiedIdentifier s n

newtype ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq, Ord)
newtype ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq, Ord, Generic, JSON.ToJSON)

data Column =
Column {
Expand All @@ -249,7 +250,7 @@ data Column =
, colDefault :: Maybe Text
, colEnum :: [Text]
, colFK :: Maybe ForeignKey
} deriving (Show, Ord)
} deriving (Show, Ord, Generic, JSON.ToJSON)

instance Eq Column where
Column{colTable=t1,colName=n1} == Column{colTable=t2,colName=n2} = t1 == t2 && n1 == n2
Expand All @@ -261,7 +262,7 @@ type ViewColumn = Column
data PrimaryKey = PrimaryKey {
pkTable :: Table
, pkName :: Text
} deriving (Show, Eq)
} deriving (Show, Eq, Generic, JSON.ToJSON)

data OrderDirection = OrderAsc | OrderDesc deriving (Eq)
instance Show OrderDirection where
Expand All @@ -286,15 +287,15 @@ data OrderTerm = OrderTerm {
data QualifiedIdentifier = QualifiedIdentifier {
qiSchema :: Schema
, qiName :: TableName
} deriving (Show, Eq, Ord, Generic)
} deriving (Show, Eq, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey)
instance Hashable QualifiedIdentifier

-- | The relationship [cardinality](https://en.wikipedia.org/wiki/Cardinality_(data_modeling)).
-- | TODO: missing one-to-one
data Cardinality = O2M -- ^ one-to-many, previously known as Parent
| M2O -- ^ many-to-one, previously known as Child
| M2M -- ^ many-to-many, previously known as Many
deriving Eq
deriving (Eq, Generic, JSON.ToJSON)
instance Show Cardinality where
show O2M = "o2m"
show M2O = "m2o"
Expand All @@ -315,7 +316,7 @@ data Relation = Relation {
, relFColumns :: [Column]
, relType :: Cardinality
, relJunction :: Maybe Junction -- ^ Junction for M2M Cardinality
} deriving (Show, Eq)
} deriving (Show, Eq, Generic, JSON.ToJSON)

-- | Junction table on an M2M relationship
data Junction = Junction {
Expand All @@ -324,7 +325,7 @@ data Junction = Junction {
, junCols1 :: [Column]
, junConstraint2 :: Maybe ConstraintName
, junCols2 :: [Column]
} deriving (Show, Eq)
} deriving (Show, Eq, Generic, JSON.ToJSON)

isSelfReference :: Relation -> Bool
isSelfReference r = relTable r == relFTable r
Expand Down Expand Up @@ -510,7 +511,7 @@ fstFieldNames (Node (sel, _) _) =
data PgVersion = PgVersion {
pgvNum :: Int32
, pgvName :: Text
} deriving (Eq, Show)
} deriving (Eq, Show, Generic, JSON.ToJSON)

instance Ord PgVersion where
(PgVersion v1 _) `compare` (PgVersion v2 _) = v1 `compare` v2
Expand Down

0 comments on commit bf141ca

Please sign in to comment.