Skip to content

Commit 8b3cbd6

Browse files
refacto
1 parent 699823b commit 8b3cbd6

File tree

10 files changed

+1569
-1051
lines changed

10 files changed

+1569
-1051
lines changed

ihp/IHP/TypedSql.hs

Lines changed: 26 additions & 1048 deletions
Large diffs are not rendered by default.

ihp/IHP/TypedSql/Bootstrap.hs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
module IHP.TypedSql.Bootstrap
2+
( describeUsingBootstrap
3+
) where
4+
5+
import Control.Exception (bracket_)
6+
import Control.Monad (when)
7+
import qualified Data.ByteString as BS
8+
import qualified Data.ByteString.Char8 as BS8
9+
import Data.Maybe (catMaybes)
10+
import qualified Data.String.Conversions as CS
11+
import System.Directory (canonicalizePath, createDirectoryIfMissing,
12+
doesDirectoryExist, doesFileExist,
13+
findExecutable, removeDirectoryRecursive)
14+
import System.Environment (lookupEnv)
15+
import System.FilePath (isRelative, takeDirectory, takeFileName, (</>))
16+
import System.IO (Handle, hIsEOF)
17+
import System.IO.Temp (withSystemTempDirectory)
18+
import qualified System.Process as Process
19+
20+
import IHP.Prelude
21+
import IHP.TypedSql.Metadata (DescribeResult, describeStatementWith)
22+
23+
data BootstrapConfig = BootstrapConfig
24+
{ bcAppSchemaPath :: !FilePath
25+
, bcIhpSchemaPath :: !(Maybe FilePath)
26+
}
27+
28+
data PgTools = PgTools
29+
{ pgInitdb :: !FilePath
30+
, pgPostgres :: !FilePath
31+
, pgCreatedb :: !FilePath
32+
, pgPsql :: !FilePath
33+
}
34+
35+
describeUsingBootstrap :: FilePath -> String -> IO DescribeResult
36+
describeUsingBootstrap sourcePath sqlText = do
37+
config <- resolveBootstrapConfig sourcePath
38+
withBootstrapDatabase config \dbUrl ->
39+
describeStatementWith dbUrl (CS.cs sqlText)
40+
41+
resolveBootstrapConfig :: FilePath -> IO BootstrapConfig
42+
resolveBootstrapConfig sourcePath = do
43+
sourceDir <- canonicalizePath (takeDirectory sourcePath)
44+
appSchemaPath <- resolveSchemaPath sourceDir
45+
ihpSchemaPath <- resolveIhpSchemaPath sourceDir
46+
pure BootstrapConfig
47+
{ bcAppSchemaPath = appSchemaPath
48+
, bcIhpSchemaPath = ihpSchemaPath
49+
}
50+
51+
resolveSchemaPath :: FilePath -> IO FilePath
52+
resolveSchemaPath sourceDir = do
53+
envSchema <- lookupEnv "IHP_TYPED_SQL_SCHEMA"
54+
case envSchema of
55+
Just path -> resolveRelativePath sourceDir path >>= ensureFileExists "IHP_TYPED_SQL_SCHEMA"
56+
Nothing -> do
57+
findUpwards sourceDir ("Application" </> "Schema.sql") >>= \case
58+
Just found -> pure found
59+
Nothing ->
60+
fail "typedSql: could not find Application/Schema.sql. Set IHP_TYPED_SQL_SCHEMA to an absolute path."
61+
62+
resolveIhpSchemaPath :: FilePath -> IO (Maybe FilePath)
63+
resolveIhpSchemaPath sourceDir = do
64+
envSchema <- lookupEnv "IHP_TYPED_SQL_IHP_SCHEMA"
65+
case envSchema of
66+
Just path -> Just <$> (resolveRelativePath sourceDir path >>= ensureFileExists "IHP_TYPED_SQL_IHP_SCHEMA")
67+
Nothing -> do
68+
envLib <- lookupEnv "IHP_LIB"
69+
fromLib <- case envLib of
70+
Just libPath -> do
71+
let candidate = libPath </> "IHPSchema.sql"
72+
exists <- doesFileExist candidate
73+
pure (if exists then Just candidate else Nothing)
74+
Nothing -> pure Nothing
75+
case fromLib of
76+
Just _ -> pure fromLib
77+
Nothing -> findUpwards sourceDir ("ihp-ide" </> "data" </> "IHPSchema.sql")
78+
79+
resolveRelativePath :: FilePath -> FilePath -> IO FilePath
80+
resolveRelativePath baseDir path = do
81+
let resolved = if isRelative path then baseDir </> path else path
82+
canonicalizePath resolved
83+
84+
ensureFileExists :: String -> FilePath -> IO FilePath
85+
ensureFileExists label path = do
86+
exists <- doesFileExist path
87+
if exists
88+
then pure path
89+
else fail ("typedSql: " <> label <> " points to missing file: " <> path)
90+
91+
findUpwards :: FilePath -> FilePath -> IO (Maybe FilePath)
92+
findUpwards startDir relativePath = go startDir
93+
where
94+
go current = do
95+
let candidate = current </> relativePath
96+
exists <- doesFileExist candidate
97+
if exists
98+
then Just <$> canonicalizePath candidate
99+
else do
100+
let parent = takeDirectory current
101+
if parent == current
102+
then pure Nothing
103+
else go parent
104+
105+
withBootstrapDatabase :: BootstrapConfig -> (BS.ByteString -> IO a) -> IO a
106+
withBootstrapDatabase BootstrapConfig { bcAppSchemaPath, bcIhpSchemaPath } action = do
107+
PgTools { pgInitdb, pgPostgres, pgCreatedb, pgPsql } <- resolvePgTools
108+
withSystemTempDirectory "ihp-typed-sql" \tempDir -> do
109+
let dataDir = tempDir </> "state"
110+
let socketDir = "/tmp" </> takeFileName tempDir
111+
let cleanupSocket = do
112+
exists <- doesDirectoryExist socketDir
113+
when exists (removeDirectoryRecursive socketDir)
114+
bracket_ (createDirectoryIfMissing True socketDir) cleanupSocket do
115+
Process.callProcess pgInitdb [dataDir, "--no-locale", "--encoding", "UTF8"]
116+
117+
let params =
118+
(Process.proc pgPostgres ["-D", dataDir, "-k", socketDir, "-c", "listen_addresses="])
119+
{ Process.std_in = Process.CreatePipe
120+
, Process.std_out = Process.CreatePipe
121+
, Process.std_err = Process.CreatePipe
122+
}
123+
Process.withCreateProcess params \_ _ stderrHandle processHandle -> do
124+
errHandle <- maybe (fail "typedSql: unable to read postgres logs") pure stderrHandle
125+
let stop = do
126+
Process.terminateProcess processHandle
127+
_ <- Process.waitForProcess processHandle
128+
pure ()
129+
let start = do
130+
waitUntilReady errHandle
131+
Process.callProcess pgCreatedb ["app", "-h", socketDir]
132+
let loadSchema file = Process.callProcess pgPsql ["-h", socketDir, "-d", "app", "-v", "ON_ERROR_STOP=1", "-f", file]
133+
forM_ (catMaybes [bcIhpSchemaPath, Just bcAppSchemaPath]) loadSchema
134+
bracket_ start stop do
135+
let dbUrl = CS.cs ("postgresql:///app?host=" <> socketDir)
136+
action dbUrl
137+
138+
resolvePgTools :: IO PgTools
139+
resolvePgTools = do
140+
pgPostgres <- requireExecutable "postgres"
141+
let binDir = takeDirectory pgPostgres
142+
pgInitdb <- findInBinOrPath binDir "initdb"
143+
pgCreatedb <- findInBinOrPath binDir "createdb"
144+
pgPsql <- findInBinOrPath binDir "psql"
145+
pure PgTools { pgInitdb, pgPostgres, pgCreatedb, pgPsql }
146+
147+
findInBinOrPath :: FilePath -> String -> IO FilePath
148+
findInBinOrPath binDir name = do
149+
let candidate = binDir </> name
150+
exists <- doesFileExist candidate
151+
if exists then pure candidate else requireExecutable name
152+
153+
requireExecutable :: String -> IO FilePath
154+
requireExecutable name =
155+
findExecutable name >>= \case
156+
Just path -> pure path
157+
Nothing -> fail ("typedSql: bootstrap requires '" <> name <> "' in PATH")
158+
159+
waitUntilReady :: Handle -> IO ()
160+
waitUntilReady handle = do
161+
done <- hIsEOF handle
162+
if done
163+
then fail "typedSql: postgres exited before it was ready"
164+
else do
165+
line <- BS8.hGetLine handle
166+
if "database system is ready to accept connections" `BS8.isInfixOf` line
167+
then pure ()
168+
else waitUntilReady handle

0 commit comments

Comments
 (0)